前面学到了C语言中的内置类型:char,int…
C语言中还有一种类型是自定义类型,其中包括了结构体,联合体和枚举。
那么今天来学习自定义类型中的结构体类型。
本章重点
目录
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。
下里面来定义一个描述学生的类型:
- struct stu
- {
- //学生的相关属性
- char name[20];//成员name
- int age;//成员age
- } s1,s2;
结构体标签一般是关于结构体是用来干什么的,s1和s2是 struct stu 类型的变量。
注意:结构体只是创造出来的一中类型,不用初始化。
上面的代码也可以这样写,省去变量列表,也是没问题的。
- struct stu
- {
- //学生的相关属性
- char name[20];//成员name
- int age;//成员age
- };
若此时结构体在main函数里面,s1和s2是局部变量;若在main外面是全局变量。
- #include
-
- int main()
- {
- struct stu
- {
- //学生的相关属性
- char name[20];//成员name
- int age;//成员age
- } s1, s2;//局部变量
- return 0;
- }
- #include
-
- struct stu
- {
- //学生的相关属性
- char name[20];//成员name
- int age;//成员age
- } s1, s2;//全局变量
-
- int main()
- {
-
- return 0;
- }
struct stu 是声明的一个结构体类型,我们还可以拿他创建一个s3变量。所创建出来的s3是局部变量。
- #include
-
- struct stu
- {
- //学生的相关属性
- char name[20];//成员name
- int age;//成员age
- } s1, s2;//全局变量
-
- int main()
- {
- struct stu s3;//局部变量
-
- return 0;
- }
外面可以在创建结构体的同时创建变量。也可以在main里利用结构体创建变量。但是一定要注意的是结构体的分号不能丢。
在声明的时候,可以不完全声明
结构体的名字可以省略,变成 匿名结构体类型
- //匿名结构体类型
- struct
- {
- //学生的相关属性
- char name[20];//成员name
- int age;//成员age
- } s1, s2;
匿名结构体的使用
- //匿名结构体类型
- struct
- {
- //学生的相关属性
- char name[20];//成员name
- int age;//成员age
- } s1;
-
- int main()
- {
-
- return 0;
- }
s1是用匿名结构体创建的变量。
注意:这个变量只能使用一次 - 声明类型的时候所创建的变量s1以后就用不了。
- struct
- {
- int a;
- char b;
- float c;
- } x;
-
- struct
- {
- int a;
- char b;
- float c;
- } a[20], *p;
-
上面的两个结构体在声明的时候省略掉了结构体标签
那么问题来了?
在上面代码的基础上,下面的代码合法吗?
p = &x;
代码演示:
- struct
- {
- int a;
- char b;
- float c;
- } x;
-
- struct
- {
- int a;
- char b;
- float c;
- } a[20], *p;
-
- int main()
- {
- p = &x;
- return 0;
- }
代码虽然跑过了,但是会报警告。
原因:因为编译器会把上面的两个声明当做是两个完全不同的类型。
在结构中包含是一个类型为该结构本身的成员
数据结构是数据在内存中的存储结构,有线性,和树形(二叉树)。
其中线性又分为顺序表和链表。
其中顺序表是数据在内存中连续存放的,通过找到第一个数据就可以找到后面的数据。
但是链表的数据不是连续存放的,那么就需要通过上一个数据能找到下一个数据,那么这是后可以通过结构体的自引用
但是,下列结构体的自引用是错误的
错误写法:
- struct Dode
- {
- int data;
- struct Dode next;//自引用
- };
-
- int main()
- {
- sizeof(struct Dode);
- return 0;
- }
未来定义一个结点的时候,这个结点既可以放一个数值,又可以放下一个结点。这样就可以做到1结点找到2结点,2结点找到3结点,直到找到5结点,但是这样写存在问题。
结构体里面有一个date,还有一个next;而next又是struct Dode类型的,所以next也有有个next,在这里会一直套下去,sizeof无法计算大小,程序会报错。
正确写法:
可以在一个结点中存放数据和下一个结点的地址
- struct Dode
- {
- int data;
- struct Dode* next;
- };
-
- int main()
- {
- sizeof(struct Dode);
- return 0;
- }
这个时候结点中存放的是一个数据和下一个结点的地址,用来存放数据的叫数据域,存放下一个结点的地址的叫指针域。
这个时候我们就可以把一个一个的数据串联起来,形成一个链表了。
总结:
结构体里面包含一个同类型的结构体是不行的,要包含一个的结构体指针。
有了结构体类型,那该如何定义变量呢?
- struct point
- {
- int x;
- int y;
- }p1 = { 2, 3 };
用结构体定义变量就像是用图纸盖房子,即使是有了图纸也不能直接盖房子,还需要有砖头、水泥等材料和工具才行。结构体就相当于是图纸,所创建的成员变量就相当于是砖头和水泥。利用这几样定义了一个p1变量,这叫定义;在定义变量的时候赋值,这叫初始化。上面2,3就是给p1赋的值,可以把p1看成一座房子,而2,3则是房子里的家具。
定义:
- struct point
- {
- int x;
- int y;
- }p1;//声明结构体的同时定义p1变量
-
- int main()
- {
- struct point p2;//定义结构体变量p2
- }
可以在声明结构体类型的时候定义变量,也可以用所声明的类型定义变量。
初始化:
- struct stu
- {
- char name[20];
- int age;
- } p3 = { 1,2,3 };
-
- int main()
- {
- struct stu s = { "lisi", 20 };
- return 0;
- }
在定义变量之后紧跟着赋值。
结构体的大小如何计算?
代码结果:
这两段代码只是成员变量的顺序变了其他的都一样,那为什么结果不一样呢?
- 第一个成员在与结构体变童偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=编译器默认的-一个对齐数与该成员大小的较小值。(VS中默认的值为8)
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
代码1:
- struct S1
- {
- char c1;
- int i;
- char c2;
- };
-
- int main()
- {
- printf("%d\n", sizeof(struct S1));
- return 0;
- }
代码结果:
- 根据对起规则知S1中的c1存放在偏移量为0处。占用一个字节即0.
- 因为int类型为4个字节小于8,那么该成员S1中的 i 的对齐数是4,即 i 要存放在4的倍数的偏移量处,即存放在偏移量为4处。占用了四个字节即4~7
- 因为char类型为1个字节小于8,那么该成员i的对齐数是1,即i要存放在1的倍数的偏移量处,即存放在偏移量为8处。占用一个字节即8。
- 计算上面的字节数为9,不是该结构体总大小为最大对齐数4的倍数,那么要继续增加到12,那么结构体的大小就是12。
对齐规则很重要!很重要!很重要!
代码2:
-
- struct S2
- {
- char c1;
- char c2;
- int i;
- };
-
- int main()
- {
- printf("%d\n", sizeof(struct S2));
- return 0;
- }
代码结果:
- 根据对起规则知c1存放在偏移量为0处。占用一个字节即0.
- 因为char类型为1个字节小于8,那么该成员i的对齐数是1,即i要存放在1的倍数的偏移量处,即存放在偏移量1处。占用一个字节即1。
- 因为int类型为4个字节小于8,那么该成员i的对齐数是4,即i要存放在4的倍数的偏移量处,即存放在偏移量为4处。占用了四个字节即4~7
- 计算上面的字节数为8,是该结构体总大小为最大对齐数4的倍数,结构体的大小就是8。
下面给出两个练习题,来练习巩固前面讲的对齐规则。要注意的是练习2中的S4有一个成员是结构体(S3),S4的大小就包括了S3的大小。
练习1:
- struct S3
- {
- double d;
- char c;
- int i;
- };
-
- int main()
- {
- printf("%d\n", sizeof(struct S3));
- return 0;
- }
代码结果是,16
练习2:
- struct S3
- {
- double d;
- char c;
- int i;
- };
-
- struct S4
- {
- char c1;
- struct S3 s3;
- double d;
- };
-
- int main()
- {
- printf("%d\n", sizeof(struct S3));
- printf("%d\n", sizeof(struct S4));
- return 0;
- }
代码结果是,32
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取
某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总结:结构体的内存对齐是拿空间来换取时间的做法。
在设计结构体的时候,尽量让占用空间小的成员尽量集中在一起,因为这样既可以满足对齐,又可以节省空间。
在使用的时候,尽量用S2来代替S1的写法,因为可以节省更多的空间。
#pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
一般修改默认对齐数都为2的倍数。
- struct S
- {
- int i;
- double d;
- };
-
- int main()
- {
- printf("%d\n", sizeof(struct S));
- return 0;
- }
代码结果:

默认对齐数是8,所以代码结果是16
- #pragma pack(4)//将对齐数改为4
-
- struct S
- {
- int i;
- double d;
- };
- #pragma pack()//将对齐数改为默认值8
-
- int main()
- {
- printf("%d\n", sizeof(struct S));
- return 0;
- }
代码结果:

修改对齐数后,代码输出12.
- struct S
- {
- int date[1000];
- int num;
- };
-
- void print1(struct S ss)
- {
- int i = 0;
- for (i = 0; i < 3; i++)
- {
- printf("%d ", ss.date[i]);
- }
- printf("%d\n", ss.num);
- }
-
- void print2(struct S* ps)
- {
- int i = 0;
- for (i = 0; i < 3; i++)
- {
- printf("%d ", ps -> date[i]);
- }
- printf("%d\n", ps -> num);
- }
-
- int main()
- {
- struct S s = { {1,2,3}, 100 };
- print1(s);
- print2(&s);
- return 0;
- }
代码结果:

print1 和 print2 哪个函数的效果更好呢?
答案是:print1
函数传参的时候。参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。
参数具体是怎样压栈的,我在其它的文章中有做详细讲解,下面是链接。
函数的栈帧的开辟与销毁:http://t.csdn.cn/2HSWx
位段是有结构体实现的
位段的声明和结构是类似的,有两个不同:
注意:成员后面的数字不能超过类型的大小。若a是 int 类型,数字最大是32。
实际上 char 也可以是位段的成员,因为字符在底层存储的是 ASCII 码,并且只要是整形家族就都可以。
例1:
- struct A
- {
- int _a : 2;
- int _b : 5;
- int _c : 10;
- int _d : 30;
- };
位段的位其实是比特位的意思。
a后面的数字表示,a只需要2个比特位。
a后面的数字表示,b只需要5个比特位。
a后面的数字表示,c只需要10比特位。
a后面的数字表示,d只需要30个比特位。
4个成员明明是 int 类型,应该是32比特位,那2、5、10、30又是什么意思呢?
比如说_a不需要那么多的空间,所以只分配了2个比特位的空间,也就是用多少取多少。
这里也就可以看出,位段其实是用来节省空间的。
例2:
- #include
-
- struct A
- {
- int _a : 2;
- int _b : 5;
- int _c : 10;
- int _d : 30;
- };
-
- int main()
- {
- printf("%d\n", sizeof(struct A));
- return 0;
- }
代码结果:

1个整形4个字节,4个整形应该是16个字节,那结果为什么会是8呢?4个成员一共是47个比特位,那给6个字节,就够用了,应该输出的是6,但是结果也不对。
注意:所谓的节省空间并不是极致的节省空间,而是适当的节省空间。如果不使用位段的话,那将会开辟16个字节的空间。
具体是什么原因。在下面的位段的内存分配有做详细介绍。
前面例2的解释:因为先开辟4个字节的空间,一共是32个比特位。4个成员一共有47个比特位,分配完之后还剩下15个比特位没有空间存放了,所以在开辟4个字节给剩下的15个比特位使用,所以才是8个字节。
一般都是位段的类型都会是同一种类型,要不然的话,这个位段会非常复杂。
例3:
- struct S
- {
- char a : 3;
- char b : 4;
- char c : 5;
- char d : 4;
- };
-
- int main()
- {
- printf("%d\n", sizeof(struct S));
- return 0;
- }
情况1:
如果输出3则说明是情况1,空间不够的话会直接开辟新的空间,多余出来的比特位会直接浪费掉。
情况2:
如果输出2则说明是情况2,空间不够的话会直接再开辟1个字节的空间,之前剩下的空间也会继续参与分配。
代码结果:

例4:
- struct S
- {
- char a : 3;
- char b : 4;
- char c : 5;
- char d : 4;
- };
-
- int main()
- {
- struct S s = { 0 };
- s.a = 10;
- s.b = 12;
- s.c = 3;
- s.d = 4;
- return 0;
- }
存储前的内存:

存储后的内存:

总结:
跟结构相比, 位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
