结构是一些值的集合。
这些值被称为成员。
每个成员可以是不同类型的变量。
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
上面代码声明了一个描述学生的结构体类型。
这个类型的标签是Stu,整个结构体的类型是struct Stu。
声明了结构体的类型之后,我们就可以定义结构体变量了。
下面定义一个struct Stu结构体类型的变量,变量名为stu。
struct Stu stu;
事实上,我们可以在声明结构体类型的同时定义结构体变量。
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}stu1,stu2; //分号不能丢
提问:结构体中能否包含类型为该结构体本身的成员?
struct Node
{
int data;
struct Node next;
};
答:不可以。
因为C语言中,结构体所占内存必须在编译时确定。
如果一个结构体中含有它本身,这个结构体所占内存大小会循环成一个无法计算的数值。
正确的自引用方式:
struct Node
{
int data;
struct Node* next;
};
考你一下:
上面代码的结构体类型是什么?
没错,就是struct Node。
我们不难发现,结构体类型的名称较为冗长。
那有没有比较简便的表示方式呢?
我们可以利用typedef给结构体类型取别名:
typedef struct Node
{
int data;
struct Node* next;
}Node;
上面代码在声明结构体的同时,给结构体类型取了别名叫Node。
注意,此时 } 后面的Node并不是定义的结构体变量,而是typedef给struct Node类型取的别名。
如何定义变量:
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
初始化:定义变量的同时赋初值
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
记住:结构体传参,要传结构体的地址。
看完上文,我们已经掌握了结构体的基本使用。
现在我们深入讨论一个问题:计算结构体的大小。
先来掌握结构体的内存对齐规则:
1. 第一个成员在结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到对齐数整数倍的地址处。
对齐数 = 编译器默认对齐数与该成员大小相比的较小值。(VS中默认为8)
3. 结构体总大小要是最大对齐数的整数倍。
最大对齐数:结构体所有成员的对齐数中最大的那个。
4. 如果嵌套了结构体:
嵌套的结构体对齐到自己的最大对齐数的整数倍处。
结构体的总大小要是所有最大对齐数的整数倍。
下面来看几个例子:
如图所示,c1的对齐数是1,作为第一个成员,对齐到偏移量为0的地址处。
(下面简写,不再重述偏移量为x的地址处,用数字替代)
i的对齐数是4,对齐到4的整数倍,也就是4处,占用4、5、6、7四个字节。
c2的对齐数是1,对齐到1的整数倍,也就是8处。
此时已占用9个字节。
而结构体总大小要是最大对齐数的整数倍,此例中最大对齐数是i的对齐数4。
故总共需占用12个字节。
理解了上面,让我们再看几个例子:
如图,c1的对齐数是1,作为第一个成员,对齐到0处。
c2的对齐数是1,对齐到1的整数倍,也就是1处。
i的对齐数是4,对齐到4的整数倍,也就是4处,占4、5、6、7四个字节。
此时总共占用了8个字节,刚好是最大对齐数4的整数倍。
故此结构体大小为8。
再来看:
理解了前面两个例子,相信你很快就能看懂本例的含义。
那么,为什么会存在内存对齐呢?
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特
定类型的数据。
2. 性能原因:
访问未对齐的内存,处理器要访问两次;而访问对齐的内存仅需访问一次。
总体来说,结构体的内存对齐是以空间换时间的做法
了解完结构体占用内存的规则后,如果想要节省空间,我们应该如何设计结构体?
答:让占用空间小的成员尽量集中在一起。
此处读者可以自行验证。
另外,我们可以使用#pragma 这个预处理指令,来改变默认对齐数
#include
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
C语言还提供了一个宏,可以计算结构体中某变量相对于首地址的偏移。
offsetof宏:
讲完了结构体,就不得不讲讲结构体实现位段的能力。
什么是位段:
C语言允许,一个结构体以比特位为单位,指定成员所占内存大小
这种以位为单位的成员,就称为位段
位段的声明和结构是类似的,但是有两个不同:
1.位段的成员类型一般情况下都是相同的,且必须是整型家族(int、char等)。
2.位段的成员名后边有一个冒号和一个数字,表示指定的内存大小。
比如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
上述代码中,A就是一个位段类型。
冒号后面的数字表示需要的比特位个数(1字节=8比特位)。
位段类型的大小如何计算:
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
先根据成员类型开辟空间:
如果是int则先开辟4个字节(32个比特位)。
如果是char则先开辟1个字节(8个比特位)。
先开辟一次,后面不够再开辟,每次开辟都如此。
将a、b、c、d的值从十进制转换为二进制(研究比特位)。
再根据位段声明中,成员指定的比特位数,对变量的二进制值进行截断。
再将其放入内存的字节中,丢弃多余的部分。
内存不够了再重新开辟,本例中总共开辟了3次,也就是3个字节。
我们将刚刚内存中存储的数据,从二进制转换成十六进制。
利用VS进行调试监视,来验证我们的模拟:
不难看出,在本例中,位段的内存分配确实如此。
因此,与结构相比,位段可以很好地节省空间。
但是,位段有很多不确定的因素:
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段在内存中从左向右分配,还是从右向左分配,标准尚未定义。
3. 当一个结构包含两个位段,第二个位段较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
因此,位段是不跨平台的,注重可移植的程序应该避免使用位段。