• C基础知识-结构体(详解)


    目录

    结构体简介

    结构体数组

    定义结构体变量

    访问结构体成员

    结构体的初始化

    结构体列表初始化

    结构体指定初始化

    结构体数组

    结构体指针

    结构体指针获取结构体成员

    结构体指针作为函数参数

    字节对齐

    1、按一个字节对齐

    2、按编译器默认进行对齐

    3、字节对齐规则

    字节对齐的原因

    对齐的作用和原因

    总结

    结构体与函数

    1.下面传递结构体变量

    2.地址传递

    3.结构体成员的地址传递和值传递

    链式结构


    结构体简介

    结构体的一般形式为:
    struct 结构体名:用来标识(zhì)一个结构体
    {
    成员列表:成员可以是任何c语言的数据类型
    }; 注意:这里一定要写分号:因为声明结构体也是一个c语言语句

    声明了一个结构体,相当于构造了一种新的数据类型,此时系统并不为它分配内存空间,只有定义力结构体类型的变量,系统才为该变量分配内存空间,声明了结构体后,就可以定义结构体类型的变量,定义
    的一般形式为:
    结构体名 变量名;
    例如:struct person p1,p2;
    这个是以person为数据类型,定义了两个变量p1,p2;struct 为定义结构体变量的关键字.定义力结构图变量后,系统会为他分配空间

    字节对齐:就是加快系统的访问效率

    三种定义结构体的方法:

    一:

    1. struct person //声明了结构体名字
    2. {
    3. char name[20];
    4. int age;
    5. char sex;
    6. char phone[15];
    7. }p1,p2;// 同时定义了两个结构体变量


    二:

    1. struct
    2. {
    3. char name[20];
    4. int age;
    5. char sex;
    6. char phone[15];
    7. }p1,p2;// 这个只是定义了两个结构图变量,但是没有声明结构体的名字


    三:

    1. struct date
    2. {
    3. int year;
    4. int monyh;
    5. int day;
    6. };
    7. struct person
    8. {
    9. char name[20];
    10. int age;
    11. char sex;
    12. char phone[15];
    13. struct date brithday;
    14. char *string;
    15. }


    总结:定义在前面的是结构体名字,在后面的是变量名字

    引用和初始化结构体变量
    一般的形式是:结构图变量.成员名
    例子:strcpy(p1.name,“John”);
    p1.age=24;
    .的作用:.是成员运算符,用于取得结构体中的成员,他在所有的运算符中 优先级最高
    嵌套的初始化的方式:p1.birthday.year = 1985;//这里的p1和birthday都是结构图变量名
    定义的同时进行初始化: struct person p2={“Bill”,22,‘m’,“987654321”};
    注意:结构体中的变量成员也可以像其他的变量一样进行各种初始化 p1.age++ //.的优先级是最高的

    结构体和数组:结构体中可以有数组类型的成员,数组的元素也可以是结构体
    例子:struct student s[2] ={{1000,“zhang”,‘m’,21,“shanghai”}{10001,“li”,‘f’,20,“bei jing”}};

    结构体和指针
    结构体中各个成员按顺序连读存放在内存中,一个结构体指针指向结构体变量,结构体指针所保存的是他指向的结构体变量所占内存的首地址
    例:

    1. struct student s1;
    2. struct student *p;
    3. s1.number=10002;
    4. s1.sex=‘m’;
    5. s.age=22;
    6. strcpy(s1.name,“Liu”);
    7. strcpy(s1.addr,“Guang Dong”);
    8. p=&s1;
    9. printf("%d,%s,%c,%d,%s\n",p->number,p->sex,p->age,p->addr);


    在c语言中为了方便使用和直观,吧(*p).number改为p->number.这两种写法都是正确的,但是基本上使用的是第二种
    语法: 指针->结构体成员
    p->number++ 这个是可以的,因为->与.的结合性都是一样高的,所以先与这两个结合,取得number的值过后,然后再对这个值进行自加
    ++pp->number 这个的意思是:先得到number的值,然后再自加
     


    结构体数组

    定义结构体变量

    结构体是一种自定义数据类型,可以用来定义变量

    1. struct test t1,t2;
    2. 定义的t1,t2,都是test类型,都具有同test一样的变量
    3. 定义的结构体变量是编译器根据test"模板"为变量分配空间

    声明结构体的同时定义结构体变量:将定义的结构体变量名放到结构体定义后面

    结构体声明与与定义

    访问结构体成员

    使用结构成员运算符——点(.)访问结构体中的成员

    结构体变量名.结构体成员
    

    结构体的初始化

    结构体列表初始化

    结构体使用初始化列表进行初始化,可对结构体成员进行逐一赋值和部分赋值

    1. #include
    2. struct test{
    3. int num ;
    4. float value;
    5. };
    6. int main()
    7. {
    8. struct test t1 = {6,7.7}; //逐一初始化
    9. struct test t2 = {5}; //部分初始化
    10. printf("t1.num=%d\n",t1.num);
    11. printf("t1.value=%f\n",t1.value);
    12. printf("t2.num=%d\n",t2.num);
    13. printf("t2.value=%f\n",t2.value);
    14. return 0;
    15. }

    结构体指定初始化

    结构器可以使用指定初始化器通过点运算符和结构体成员名对特定的结构体成员进行指定初始化

    1. #include
    2. struct test{
    3. int num ;
    4. float value;
    5. int count;
    6. };
    7. int main()
    8. {
    9. struct test t1 = {.value = 5.5,.count = 10};
    10. printf("t1.num=%d\n",t1.num);
    11. printf("t1.value=%f\n",t1.value);
    12. printf("t1.count=%d\n",t1.count);
    13. return 0;
    14. }

    结构体数组

    结构体数据就是指数组中的每一个元素都是结构体

    1. #include
    2. struct test{
    3. int num ;
    4. float value;
    5. char ch;
    6. };
    7. int main()
    8. {
    9. struct test testArr[3]={ {1,1,2},{3,3,4},{5,5,6} };
    10. for(int i = 0; i<3 ; i++)
    11. {
    12. printf("testArr[%d].num=%d\n",i,testArr[i].num);
    13. printf("testArr[%d].value=%f\n",i,testArr[i].value);
    14. printf("testArr[%d].ch=%d\n",i,testArr[i].ch);
    15. }
    16. return 0;
    17. }

    结构体指针

    当一个指针变量指向结构体时,称为结构体指针

    struct 结构体名 *变量名;
    

    结构体变量名与数组名不一样,数组名在表达式中会被转换成数组指针,而结构体变量名不会,无论在任何表达式中标表示的都是整个结构体本身,要活的结构体变量的地址,则必须使用取地址符&

    结构体指针获取结构体成员

    1. 通过结构体指针获取结构体成员的方式
    2. 1、(*ptr).structMember
    3. 2、ptr->structMember
    4. .运算符高于*,所以(*ptr)括号不能少
    1. #include
    2. struct test{
    3. int num;
    4. char ch;
    5. double doub;
    6. }t1;
    7. int main()
    8. {
    9. struct test t1 = {5,'A',5.6};
    10. struct test *ptr = &t1;
    11. //解引用和点运算符
    12. printf("type1:(*ptr).num=%d\n",(*ptr).num);
    13. //指针
    14. printf("type2: ptr->ch=%c\n",ptr->ch);
    15. return 0;
    16. }

    结构体指针作为函数参数

    结构体变量名代表整个结构体变量本身,当把结构体变量本身作为参数传递时,结构体内部的结构体成员较多时会造成时间和空间开销大,影响程序的运行效率

    使用结构体指针作为参数传递时,因为传递的是地址,会比原本含有较多结构体的结构体变量更加块,但是也会造成结构体变量数据被修改的可能

    1. #include
    2. #include
    3. struct test{
    4. int num;
    5. char ch;
    6. double doub;
    7. }t1;
    8. void printA(struct test str)
    9. {
    10. str.num += 5;
    11. printf("printA函数的num=%d\n",str.num);
    12. }
    13. void printB(struct test* te)
    14. {
    15. te->num += 15;
    16. printf("printB函数的num=%d\n",te->num);
    17. }
    18. int main()
    19. {
    20. clock_t start, finish,start1,finish1;
    21. struct test t1 = {5,'A',5.6};
    22. struct test *ptr = &t1;
    23. start = clock();
    24. printA(t1);
    25. finish = clock();
    26. double Total_time = (double)(finish - start);
    27. printf("printA函数之后,main函数的num=%d\n",t1.num);
    28. printf("printA运行时间=%f\n",Total_time);
    29. start1 = clock();
    30. printB(ptr);
    31. finish1 = clock();
    32. double Total_time1 = (double)(finish1 - start1);
    33. printf("printB函数之后,main函数的num=%d\n",t1.num);
    34. printf("printB运行时间=%f\n",Total_time1);
    35. return 0;
    36. }

    字节对齐

    结构体中的各个成员理论上在内存中都是连续存储的,但实际上是根据字节对齐规则进行存储的

    1. #pragma pack(n)
    2. 指定按照n字节对齐规则进行存储

    1、按一个字节对齐

    1. #include
    2. //调整结构体的边界对齐,按一个字节对齐
    3. #pragma pack(1)
    4. struct test{
    5. int num;
    6. char ch[4];
    7. double doub;
    8. char cha;
    9. }t1;
    10. int main()
    11. {
    12. printf("siezof(t1)=%zd\n",sizeof(t1));
    13. return 0;
    14. }

    2、按编译器默认进行对齐

    3、字节对齐规则

    字节对齐的基本规则

    1. 结构体内部任何K字节的基本对象相对于结构体首地址的偏移,必须是K的整数倍
    2. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除
    3. 结构体的总大小为结构体最宽基本类型成员大小的整数倍
    1. #include
    2. struct bitField{
    3. int num : 16;
    4. int com :8;
    5. char cn :4;
    6. int count :16;
    7. }b1;
    8. struct test{
    9. int num;
    10. char a;
    11. int b;
    12. int c;
    13. char e[5];
    14. }t1;
    15. int main()
    16. {
    17. printf("sizeof(b1)=%zd\n",sizeof(b1));
    18. printf("sizeof(t1)=%zd\n",sizeof(t1));
    19. return 0;
    20. }

    结构体位域

    字节对齐的原因

    计算机内存是以字节(Byte)为单位划分的,理论上CPU可以访问任意编号的字节,但实际情况并非如此。CPU 通过地址总线来访问内存,一次能处理几个字节的数据,就命令地址总线读取几个字节的数据。32 位的 CPU 一次可以处理4个字节的数据,那么每次就从内存读取4个字节的数据

    对齐的作用和原因

    各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多

    将一个数据尽量放在一个步长之内,避免跨步长存储,这称为内存对齐。在32位编译模式下,默认以4字节对齐;在64位编译模式下,默认以8字节对齐

    总结

    结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间,真正需要开辟内存空间来存储的是结构体成员变量,结构体中的各个成员理论上在内存中都是连续存储的,但实际上是根据字节对齐规则进行存储的


    结构体与函数

      首先结构体做函数参数有三种传递方式

    一是传递结构体变量,这是值传递,二是传递结构体指针,这是地址传递,三是传递结构体成员,当然这也分为值传递和地址传递。

    以传引用调用方式传递结构比用传值方式传递结构效率高。以传值方式传递结构需要对整个结构做一份拷贝。 

    下面看一个列子,student结构体中包含该学生的各种信息,我们在change函数中对其进行部分修改,再在主函数中输出其结果

    1.下面传递结构体变量

    1. #include
    2. #include
    3. #define format "%d\n%s\n%f\n%f\n%f\n"
    4. struct student
    5. {
    6. int num;
    7. char name[20];
    8. float score[3];
    9. };
    10. void change( struct student stu );
    11. int main()
    12. {
    13. struct student stu;
    14. stu.num = 12345;
    15. strcpy(stu.name, "Tom");
    16. stu.score[0] = 67.5;
    17. stu.score[1] = 89;
    18. stu.score[2] = 78.6;
    19. change(stu);
    20. printf(format, stu.num, stu.name, stu.score[0], stu.score[1],stu.score[2]);
    21. printf("\n");
    22. return 0;
    23. }
    24. void change(struct student stu)
    25. {
    26. stu.score[0] = 100;
    27. strcpy(stu.name, "jerry");
    28. }


      可以看到最终输出的值并未改变……


    2.地址传递

    1. "font-family:Arial Black;font-size:12px;">#include
    2. #include
    3. #define format "%d\n%s\n%f\n%f\n%f\n"
    4. struct student
    5. {
    6. int num;
    7. char name[20];
    8. float score[3];
    9. };
    10. void change( struct student* stu );
    11. int main()
    12. {
    13. struct student stu;
    14. stu.num = 12345;
    15. strcpy(stu.name, "Tom");
    16. stu.score[0] = 67.5;
    17. stu.score[1] = 89;
    18. stu.score[2] = 78.6;
    19. change(&stu);
    20. printf(format, stu.num, stu.name, stu.score[0], stu.score[1],stu.score[2]);
    21. printf("\n");
    22. return 0;
    23. }
    24. void change(struct student* p)
    25. {
    26. p->score[0] = 100;
    27. strcpy(p->name, "jerry");
    28. }


    可以看到,通过地址传递修改了结构体内的数据

    用&stu做实参,&stu是结构体变量stu的地址。在调用函数时将该地址传送给形参p(p是指针变量)。这样p就指向stu。

    在change函数中改变结构体内成员的值,在主函数中就输出了改变后的值

    3.结构体成员的地址传递和值传递

    这个类似于单一变量的传递,这里也没必要说了,当然是地址传递才能修改。

    把一个完整的结构体变量作为参数传递,要将全部成员值一个一个传递,费时间又费空间,开销大。如果结构体类型中的成员很多,或有一些成员是数组,则程序运行效率会大大降低。在这种情况下,用指针做函数参数比较好,能提高运行效率。


    链式结构

    链表是一种常见的基础数据结构,结构体指针在这里得到了充分的利用。链表可以动态的进行存储分配,也就是说,链表是一个功能极为强大的数组,他可以在节点中定义多种数据类型,还可以根据需要随意增添,删除,插入节点。链表都有一个头指针,一般以head来表示,存放的是一个地址。链表中的节点分为两类,头结点和一般节点,头结点是没有数据域的。链表中每个节点都分为两部分,一个数据域,一个是指针域。说到这里你应该就明白了,链表就如同车链子一样,head指向第一个元素:第一个元素又指向第二个元素;……,直到最后一个元素,该元素不再指向其它元素,它称为“表尾”,它的地址部分放一个“NULL”(表示“空地址”),链表到此结束。

    作为有强大功能的链表,对他的操作当然有许多,比如:链表的创建,修改,删除,插入,输出,排序,反序,清空链表的元素,求链表的长度等等。

    初学链表,一般从单向链表开始

    1. --->NULL
    2. head

    这是一个空链表。

    1. ---->[p1]---->[p2]...---->[pn]---->[NULL]
    2. head p1->next p2->next pn->next

    有n个节点的链表。

    创建链表

    1. typedef struct student{
    2. int score;
    3. struct student *next;
    4. } LinkList;

    一般创建链表我们都用typedef  struct,因为这样定义结构体变量时,我们就可以直接可以用LinkList  *a;定义结构体类型变量了。

    初始化一个链表,n为链表节点个数。

    1. LinkList *creat(int n){
    2. LinkList *head, *node, *end;//定义头节点,普通节点,尾部节点;
    3. head = (LinkList*)malloc(sizeof(LinkList));//分配地址
    4. end = head; //若是空链表则头尾节点一样
    5. for (int i = 0; i < n; i++) {
    6. node = (LinkList*)malloc(sizeof(LinkList));
    7. scanf("%d", &node->score);
    8. end->next = node;
    9. end = node;
    10. }
    11. end->next = NULL;//结束创建
    12. return head;
    13. }

    修改链表节点值

    修改链表节点值很简单。下面是一个传入链表和要修改的节点,来修改值的函数。

    1. void change(LinkList *list,int n) {//n为第n个节点
    2. LinkList *t = list;
    3. int i = 0;
    4. while (i < n && t != NULL) {
    5. t = t->next;
    6. i++;
    7. }
    8. if (t != NULL) {
    9. puts("输入要修改的值");
    10. scanf("%d", &t->score);
    11. }
    12. else {
    13. puts("节点不存在");
    14. }
    15. }

    删除链表节点

    删除链表的元素也就是把前节点的指针域越过要删除的节点指向下下个节点。即:p->next = q->next;然后放出q节点的空间,即free(q);

    1. void delet(LinkList *list, int n) {
    2. LinkList *t = list, *in;
    3. int i = 0;
    4. while (i < n && t != NULL) {
    5. in = t;
    6. t = t->next;
    7. i++;
    8. }
    9. if (t != NULL) {
    10. in->next = t->next;
    11. free(t);
    12. }
    13. else {
    14. puts("节点不存在");
    15. }
    16. }

    插入链表节点

    我们可以看出来,插入节点就是用插入前节点的指针域链接上插入节点的数据域,再把插入节点的指针域链接上插入后节点的数据域。根据图,插入节点也就是:e->next = head->next;  head->next = e;

    增加链表节点用到了两个结构体指针和一个int数据。

    1. void insert(LinkList *list, int n) {
    2. LinkList *t = list, *in;
    3. int i = 0;
    4. while (i < n && t != NULL) {
    5. t = t->next;
    6. i++;
    7. }
    8. if (t != NULL) {
    9. in = (LinkList*)malloc(sizeof(LinkList));
    10. puts("输入要插入的值");
    11. scanf("%d", &in->score);
    12. in->next = t->next;//填充in节点的指针域,也就是说把in的指针域指向t的下一个节点
    13. t->next = in;//填充t节点的指针域,把t的指针域重新指向in
    14. }
    15. else {
    16. puts("节点不存在");
    17. }
    18. }

    输出链表

    输出链表很简单,边遍历边输出就行了。

    1. while (h->next != NULL) {
    2. h = h->next;
    3. printf("%d ", h->score);
    4. }
  • 相关阅读:
    南大通用GBase8s 常用SQL语句(285)
    Android逆向学习(五)app进行动态调试
    【从互联网商业思维的角度分析商业模式在国内各大互联网产品的运用】
    RibbitMQ学习笔记之交换机实战
    FreeSql 将 Saas 租户方案精简到极致[.NET ORM SAAS]
    【C++并发编程】(九)线程池
    【机器学习的数学基础】(一)线性代数(Linear Algebra)(上+)
    处理数据 根据 pid 添加到父级 children
    多层索引MultiIndex
    C语言程序设计-8 函 数
  • 原文地址:https://blog.csdn.net/mooczhimahu/article/details/126439488