赞
踩
结构体的完全声明:
-
- struct tag
- {
- member_list;
- }variable_list;
- //结构体的标签:tag
- //结构体的类型:struct tag
- //结构的成员列表:member_list
- //结构体变量列表:variable_list
当不完全声明时,比如说省略结构体的标签,这时,这种结构体就被称为匿名结构体。
匿名结构体也称为未命名结构体,由于没有名称,因此不会创建它们的直接对象(或变量),通常我们在嵌套结构或联合中使用它们。
值得注意的是,匿名结构体类型可以说是“一次性用品”,用了一次以后再也用不了了。
-
- struct
- {
- char c;
- int a;
- double d;
- }s1;
- struct
- {
- char c;
- int a;
- double d;
- }* p1;//一个看上去指向和s1相同类型的指针
- int main()
- {
- p1 = &s1;//这样写是不可以的,编译器会认为它是两个不同的类型
- return 0;
- }
如上代码是错误的,虽然两个匿名结构体,看上去它们的成员都是一样的,表面上是同一类型,但实际上他们是两种不同的类型。因此,因为在定义的时候没有写结构体类型名称,编译器会把它们当做两种不同的类型,然后报错。
匿名结构体的优点:
嵌套在结构体中的结构体为匿名结构时,可以直接访问其成员。
-
- #include <stdio.h>
-
- struct Stu
- {
- char* name;
- char gender;
- int age;
- struct//嵌套定义了这个匿名结构体
- {
- int Student_ID;
- long phone_number;
- };
- };
-
- int main(void)
- {
- struct Stu A= {"A", 'M', 19, {30, 13930422035}};
- printf("%d\n", A.Student_ID);
- }
如果不使用匿名结构体,则上述例子会变成这样:
-
- #include <stdio.h>
-
- struct Stu_information
- {
- int Student_ID;
- long phone_number;
- };
-
- struct Stu A
- {
- char* name;
- char gender;
- int age;
- struct Stu_information;
- };
-
- int main(void)
- {
- struct Stu A = {"A", 'M', 19, {30, 13930422035}};
-
- printf("%d\n", A.Stu_information.Student_ID);
- }
对比上述两个例子可以看出:
使用匿名结构体,结构体对象 A 可以通过 A.Student_ID直接访问匿名结构体成员变量 .Student_ID,代码比较简洁,反之则必须通过A.Stu_information.Student_ID 来访问结构体成员变量 ,比较繁琐。
我们不由地思考一个问题,结构体把自己当作自己的一个成员变量,这是否可行呢?
-
- struct Node
- {
- int date;
- struct Node n;//错误!
- };
结论是:这样写是绝对错误的!
请想一想,如果我们想要求struct Node 的大小,其中,成员data占4个字节,那么struct Node n成员的大小是多少呢?
这就陷入了一个思维上的死循环:要求它自己的大小,那么首先就要求它自己的大小?这是什么鬼啊?!
所以这样写是绝对不可以的!!!
那么,如果想要正确的自引用,我们应该怎么做呢?
这里要先补充一点关于“数据结构”的知识:
数据的存储可以有很多方式,比如“顺序存储”、“二叉树”等等,这些存储方式都可以让我们很顺利地从一个数据找到另一个数据。
所以,关于结构体的自引用,我们可以把它看作是通过一个结构体,可以找到下一个结构体。这样一个一个像线一样访问数据,即“线性数据结构(通过链表方式可以顺序访问内存空间中不相邻的数据)”,我们就要使用到结构体指针。而指针的大小跟其所指向的类型无关,仅跟编译器有关,32位平台上指针大小为4个字节,在64位平台上,指针大小为8个字节。所以,通过指针大小的确定性,我们就可以确定结构体类型的大小了。
-
- struct Node //结构体的自引用
- {
- int date;
- struct Node* next;
- };
- int main()
- {
- struct Node n1;
- struct Node n2;
- n1.next = &n2;//这样就可以通过n1找到n2了.
- return 0;
- }
相当于每个结构体,前面存数据,后面下一个结点地址的指针,通过该结点可以找到下一个结构体。
匿名结构体的重命名+自引用:
-
- typedef struct
- {
- int data;
- int d;
- }Node; //一种错误的写法
-
- typedef struct Node
- {
- int data;
- int d;
- }Node; //正确
-
-
- struct
- {
- int data;
- struct Node* next;
- }Node1;
- //结构体在未定义Node之前就定义了一个Node*的指针,这样是错误的!!!
-
- typedef struct
- {
- int date;
- struct Node* next;//在此之前,都没有出现Node
- }Node;
- //把一个匿名结构体重命名为Node?不可以!
-
-
- //正确的写法是这样的:
- typedef struct Node
- {
- int data;
- struct Node* next;
- }Node;
-
- struct S
- {
- int a;
- char c;
- };
- struct F
- {
- int a;
- char arr[10];
- struct S;
- };
- int main()
- {
- struct F pd= { 12,"sgdkjklaj",{345,'o'}};//结构体的嵌套初始化
- struct S s2 = { 2000,'p'};//默认顺序初始化
- struct S s3 = { .c = 'r',.a = 2000 };//自己确定初始化的顺序
- }
1.第一个成员永远在结构体变量偏移量为0的地址处。
2.从第二个成员开始,以后每个成员变量都要对齐到某个数字(对齐数)的整数倍的地址处。
这个对齐数=编译器默认的一个对齐数 与 该成员大小的较小值。
vs2017环境中,VS编译器默认的值为8
一个是Linux环境,gcc编译器下,没有默认对齐数,对齐数就是成员自身的大小
3、当成员全部存放进去后,结构体总大小必须是所有成员的对齐数中最大对齐数(每个成员变量都有一个对齐数)的整数倍。如果不够,则浪费空间。
4、如果嵌套了结构体的情况,嵌套的结构体成员要对齐到自己成员的最大对齐数的整数倍处。结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
接下来,让我们举一些例子:
为什么会存在内存对齐呢?
1.平台原因(移植原因)∶不是所有的硬件平台都能访问任意地址上的任意数据的; 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。(所以我们对齐到能够被硬件访问的位置,在对齐位置进行存储数据)
⒉性能原因∶数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;
而对齐的内存访问仅需要一次访问。它能够提高效率。
总体来说︰ 结构体的内存对齐是拿空间来换取时间的做法。
64位平台上,编译器一次访问8个字节。
但是,节省空间不能无脑地节省空间啊。
如果既要满足对齐,又要节省空间,怎么办呢?
解决方案:把占用空间较小的数据集中在一起。
-
- struct s1
- {
- char c1;
- int i;
- char c2;
- };//12个字节
- struct s2
- {
- char c1;
- char c2;
- int i;
-
- };//8个字节
设置默认对齐数:
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数来满足需求。
如何修改默认对齐数呢?通过#pragma修改。
-
- #include <stdio.h>
- #pragma pack(8)//设置默认对齐数为8
- struct s1
- {
- char c1;
- int i;
- char c2;
- };
- #pragma pack()//取消设置的默认对齐数,还原为默认
-
- #pragma pack(1)//设置默认对齐数为1,这样其实相当于根本没有对齐数了。
- struct s2
- {
- char c1;
- int i;
- char c2;
- };
- #pragma pack()//取消设置的默认对齐数,还原为默认
- int main()
- {
- printf("%d\n", sizeof(struct s1));
- printf("%d\n", sizeof(struct s2));
- return 0;
- }
offsetof,这是一个宏,用来计算结构体成员相对于结构体起始位置的偏移量
-
- #include<stddef.h>
- struct S
- {
- char c1;
- char c2;
- int i;
- };
- int main()
- {
- struct S s = {0};
- printf("%d\n",offsetof(struct S,c1)); //0
- printf("%d\n", offsetof(struct S, c2));//1
- printf("%d\n", offsetof(struct S, i)); //4
- return 0;
- }
-
- #include <stdio.h>
- struct S
- {
- int data[1000];
- int num;
- };
- struct S s = { {1, 2, 3, 4}, 1000 };
- //结构体传参
- void print1(struct S s)
- {
- printf("%d \n", s.num);
- }
- // 结构体地址传参
- void print2(struct S* ps)
- {
- printf("%d \n", ps->num);
- }
- int main()
- {
- print1(s);//传结构体
- print2(&s);//传结构体地址,用一个结构体指针接收
- return 0;
- }
上面的print1和print2哪一个更好呢?
print2更好一点
对于结构体的传参首选传递地址,原因如下:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
那么如何应对传地址的不安全性呢?很简单,用一个const 就可以了。
什么是位段 ?
位段的声明和结构是类似的,但是有两点不同︰
1.位段的成员必须是int、unsigned int,signed int、char这些整型家族的类型。
2.位段的成员名后边有一个冒号和一个数字。
-
- #include<stdio.h>
- struct A
- {
- int _a : 2; //int _a;
- int _b : 5; //int _b;
- int _c : 10; //int _c;
- int _d : 30; //int _d;这是结构体
- };
- int main()
- {
- struct A sa={0};
- printf("%d\n", sizeof(sa));
- return 0;
- }
位段位段,“位”指的是二进制位。
如果_a只会存储0,1,2,3(00,01,10,11),那么这个时候2个比特位就够了。所以位段是一种更节省空间的方法。
-
- #include<stdio.h>
- struct A
- {
- int _a : 2;
- int _b : 5;
- int _c : 10;
- int _d : 30;
- };//47个比特位
- //实际上占了8个字节
- //当然,这已经很节省空间了!
位段的空间上是按照需要以4个字节(int )或者1个字节(char)来开辟的。
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。(所以一些细节方面不能够确定下来)
现在还有一些问题待确定:
1、空间是从左向右使用还是从右向左使用?
2、当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用呢?
-
- #include<stdio.h>
- struct S
- {
- char a : 4;
- char b : 3;
- char c : 5;
- char d : 4;
- }s;
- int main()
- {
- struct S s = { 0 };
- s.a = 10;
- s.b = 12;
- s.c = 3;
- s.d = 4;
- return 0;
- }
但是,这些在不同编译器上都是不同的!
位段的跨平台问题:
1.int位段被当成有符号数还是无符号数是不确定的。
2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32),写成27,在16位机器会出问题。
3.位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总的来说,跟结构体相比,位段可以达到同样的效果,并且可以很好地节省空间,但是有跨平台问题。
位段的应用:
关于位段的一些练习题:
-
- #define MAX_SIZE A+B
- struct _Record_Struct
- {
- unsigned char Env_Alarm_ID : 4;
- unsigned char Para1 : 2;
- unsigned char state;
- unsigned char avail : 1;
- }*Env_Alarm_Record;
- struct _Record_Struct *pointer =
- (struct _Record_Struct*)malloc(sizeof(struct _Record_Struct) * MAX_SIZE);
问:当A=2, B=3时,pointer分配( )个字节的空间?
位段成员作为unsighed char类型,是按1个字节开辟内存的,第一个成员Env_Alarm_ID开辟一个字节等于8个bit位,被Env_Alarm_ID用去4个bit,还剩4个bit可以给Para1用。Para1用去2个bit,故位段Env_Alarm_ID , Para1一共用一个字节;
state不是位段重新开辟一个字节的内存,它直接占一个字节;
位段avail只占1个bit,但也要开辟1个字节的空间用来存放。
所以前面的算下来总共开辟了3个字节的空间。
(sizeof(struct _Record_Struct) * MAX_SIZE)=3*2+3(这里一定要注意宏定义并没有写成(A+B)的形式,所以并不是先计算A+B)=9.
故pointer分配的空间为9个字节
-
- int main()
- {
- unsigned char puc[4];
- struct tagPIM
- {
- unsigned char ucPim1;
- unsigned char ucData0 : 1;
- unsigned char ucData1 : 2;
- unsigned char ucData2 : 3;
- }*pstPimData;
- pstPimData = (struct tagPIM*)puc;
- memset(puc,0,4);
- pstPimData->ucPim1 = 2;
- pstPimData->ucData0 = 3;
- pstPimData->ucData1 = 4;
- pstPimData->ucData2 = 5;
- printf("%02x %02x %02x %02x\n",puc[0], puc[1], puc[2], puc[3]);
- return 0;
- }
代码结果是?
画个图:
最后,想要补充一个非常易错的练习题:
-
- #include<stdio.h>
- struct ord
- {
- int x,y
- }dt[2] = {1,2,3,4};
- int main()
- {
- struct ord* p = dt;
- printf("%d,", ++p->x);
- printf("%d\n", ++p->y);
- return 0;
- }
这个程序的运行结果是什么?
是不是有点意想不到?
让我们来简单分析一下:
结构体其实也是可以这样赋值的,和数组是很类似的。
printf("%d,", ++p->x);
printf("%d\n", ++p->y);
这两条语句中,->这个操作符的优先级是高于++的,所以其实是先取出p指向的x和y这两个数值,然后再对这两个数++。
所以实际上,p这个指针自始至终都指向dt[1]。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。