赞
踩
目录
在C语言中,有许多的内置类型,如char、int、double等等,但仅仅有这些类型是远远不够的。
假如要描述一个学生,那么他的名字、身高、体重、成绩等等都需要考虑。
为此,C语言提供了结构体这样的自定义类型。结构体的每个成员可以是不同类型的变量。
结构体的关键字:struct
还是以描述一个学生为例子
- struct stu
- {
- char name[20];//名字
- double height;//身高
- double weight;//体重
- int grades;//成绩
- };//不能丢分号
在声明结构体中,有一种特殊的声明:匿名结构体
- struct
- {
- int x;
- int y;
- }x;
- //匿名结构体类型
-
- struct
- {
- int x;
- int y;
- }*p;
匿名结构体只能使用一次,当你使用匿名结构体后,就不能再使用了。
在上面的基础上,我们思考这样的操作行不行?
答案是否定的,编译器会报警告。
所以我们要谨慎对待匿名结构体。
- struct S1
- {
- int i;
- int j;
- }s1;
- //声明类型的同时定义变量s1
-
- s1 = { 6,7 };
- //初始化s1
- struct Stu
- {
- char name[25];//名字
- int age;//年龄
- };
-
- struct Stu xiaoming = { "xiaoming",18 };//初始化
- struct Stu xiaohong = { .age = 17, .name = "xiaohong" };
- //一般来说,初始化是有顺序的,但是也可以指定顺序来给变量初始化
- //.age是访问结构体成员中的age
- struct S1
- {
- int i;
- int j;
- }s1;
- //声明类型的同时定义变量s1
-
- struct S2
- {
- int num;
- struct S1 s1;
- struct Stu* p;
- }s2 = { 3,{3,4},NULL };
- //结构体嵌套初始化
上面这三种方式是比较常见的结构体变量的定义和初始化。
我们可以思考下在结构体中包含⼀个类型为该结构体本⾝的成员是否可以呢?
比如定义⼀个链表的节点
仔细思考下,我们就会发现这样的做法是不可取的。⼀个结构体中再包含⼀个同类型的结构体变量,相当于结构体自己调用自己,无限创建结构体变量,这样结构体变量的大小就会无穷的大,这样当然不行,
正确的做法应该是传递节点的地址:
我们再来看下一个错误的例子:
这种写法是错误的,因为Node是对前面结构体类型的重命名,不可以使用Node提前在重命名结构体中创建成员变量。
正确的做法如下:
访问结构体成员有两种方式:
1.用点操作符(.)访问。
2.结构体指针 -> 成员名
可以看到这两种访问方式是等效的。
上面的两种传参方式都可以,传值或传地址。但是我们一般都是传递结构体的地址,因为当结构体成员太多,大小太大时,参数压栈的的系统开销比较大,所以会导致性能的下降。
总结:结构体传参应该传递结构体的地址
当了解完结构体的基本使用后,我们就来计算下结构体的大小
这涉及到一个非常重要的知识点:结构体的内存对齐。
对齐的规则:
1.结构体第一个成员对齐到结构体变量起始位置(偏移量为0)的地址。
2.其他成员对齐到对齐数的整数倍地址处,每个成员变量都有一个对齐数。
对齐数 == 编译器默认的对齐数 与 该成员所占字节大小的较小值。
在VS中,默认对齐数为8;在Linux的gcc中,没有默认对齐数,对齐数就是成员自身大小
3.结构体的总大小是最大对齐数(所有结构体成员变量所对应的对齐数中最大的那个)的整数倍。
4.如果是嵌套结构体的情况,嵌套的结构体成员对齐到自身成员的最大对齐数的整数倍处,结构体的总大小是最大对齐数(比较对象包含嵌套结构体中成员的对齐数)的整数倍。
下面我们看两个具体的例子:
例子1:
首先,第一个变量是a,对齐到起始地址处;第二个成员变量i,由于其对齐数是4,所以对齐到4的整数倍的地址处,也就是地址编号为4(偏移量为4)的地址处,中间空出的3个字节大小的空间被浪费掉了;第三个成员变量b,对齐数为1,那就对齐到地址编号为8的地址处。 此时,该结构体已经占据了9个字节大小的空间(包含前面浪费的3个字节大小的空间),但由于结构体的总大小是最大对齐数的整数倍,该结构体的最大对齐数为4,所以最终结构体的大小是12个字节(又浪费了3个字节的空间)。
例子2(嵌套结构体):
存在结构体内存对齐的原因:
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两 个8字节内存块中。
总结:结构体的内存对齐就是用空间来换取时间和效率的做法。
如果想要结构体节省空间,那么我们就尽可能让空间小的成员放在一起。
修改默认对齐数:
我们用#pragma来修改默认对齐数
- #pragma pack(1)
- //设置对齐数为1
位段是由结构体来实现的,形式如下:
位段的功能:指定结构体成员变量所占用的比特位,从而应对实际问题的不同情况,并且节省空间,提高空间利用率。
位段的成员必须是 int 、unsigned int或signed int ,在C99中位段成员的类型也可以 选择其他类型。
位段的内存分配:
当为位段分配空间时,我们会遇到以下问题:
1.申请到的内存中,是从右到左使用,还是从左到右使用?
2.当剩余的空间不足以下一个成员使用时,是浪费掉,还是继续使用?
以上的问题具有不确定性,没有一个确切的标准。
在VS中,申请的空间是从右左使用,当剩余空间不足下一个成员使用时,剩余空间会被浪费掉。
例子:
开辟空间的时候会以所有结构体成员中类型所占最大的字节数来开辟:
故下面的例子开辟了4个字节的空间。
位段的使用事项:
由于位段的几个成员共用一个字节,导致了个别成员的起始位置不是该字节的起始位置,所以就无法对这些特殊的成员进行取地址“&”操作。
一个字节才拥有一个地址,字节内部的比特位是没有地址的。
- scanf("%d", &b1.c);
- //错误的操作
-
- int cc = 0;
- scanf("%d", &cc);
- b1.c = cc;
- //正确的操作
有缘再会,拜拜!
摸鱼摸鱼
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。