目录
1. 结构体类型的声明
1.1 结构体的使用
1.1.1 结构体的声明
结构体是利用struct关键字进行声明的
struct Stu
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
//为了方便后续使用,可以使用typedef进行重命名
typedef struct Stu
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
char id[20];//学号
} Stu;
1.1.2 结构体变量的初始化
初始化结构体变量可以直接按顺序初始化结构体变量,也可以通过“.操作符”进行乱序初始化
int main()
{
Stu stu1 = { "zhangsan", 18, "man","2345365543" };
Stu stu2 = { .age = 15, .sex = "woman", .id = "6545463156", .name = "lisi" };
return 0;
}
当然,还可以使用->初始化结构体变量
1.2 结构体的特殊声明
在结构体声明时,可以不完全的声明
//匿名结构体
struct
{
char c;
int i;
double d;
} s;
int main()
{
s.c = 'c';
s.i = 4;
s.d = 1.1;
printf("%c %d %lf\n", s.c, s.i, s.c);
return 0;
}
注意:
匿名结构体基本上只能使用一次
1.3 结构体的自引用
在结构体中包含一个结构体本身的成员可行吗?
例如,定义一个链表节点:
struct Node
{
int date;
struct Node next;
};
很明显,这样是不合理的,若是真这么定义的话,是不是就类似于没有递归终点的递归呢,这样大小就会变成无穷大
正确的自引用方式:
struct Node
{
int date;
struct Node* next;
};
通过包含一个指向另一个结构体成员的指针来实现结构体的自引用,后续要操作下一个结构体可以通过指针去访问
2. 结构体的大小
先来看看猜下下面这个结构体大小是多少
struct S
{
char c1;
int i;
char c2;
} ;
有的人可能认为结构体大小就是所有成员变量大小之和,所以这个结构体大小是6,那么这是对的吗?
很遗憾,你猜错了。此时有人会问12是怎么计算得出来的呢?
想要计算结构体的大小,那么我们就要先知道一个概念:结构体内存对齐
2.1 结构体内存对齐
2.1.1 对齐规则
1. 结构体的第一个成员对齐和结构体变量起始位置偏移0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数 与 该成员变量的大小的较小值
- VS 中默认的值为 8
- Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍
4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
//先来看看该结构体各成员变量的对齐数
struct S
{
char c1;//对齐数为1
int i;//对齐数为4
char c2;//对齐数为1
} ;//最大对齐数为4
从偏移量为0的地址开始,c1占据一个字节;而后第二个成员变量i的对齐数为4,所以它要从偏移量为4的倍数开始,储存四个字节的大小;第三个成员变量c2的对齐数为1,所以它要对齐到1的倍数就可以了,此时共占据了九个字节的大小;最后,因为结构体的大小为最大对齐数的整数倍,所以,该结构体的大小为12
再来看个嵌套结构体的大小
struct S1 //大小为16
{
double d;//对齐数为8
char c;//对齐数为1
int i;//对齐数为4
};
struct S2
{
char c1;//对齐数为1
struct S1 s1;//对齐数为8
double d;//对齐数为8
};
从偏移量为0的位置开始,c1占据一个字节;s1的大小为16,s1的最大对齐数为8,所以s1占据的空间为从偏移量为8的地址开始的16个字节的空间;d的对齐数为8,所以d占据的空间为偏移量为24开始的8个字节的空间;S2的大小为32个字节。
2.1.2 为什么存在内存对齐?
大部分的参考资料都是这样说的:
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
为了访问未对齐的内存,处理器需要作两次访问;而对齐的内存访问仅需一次访问。假设一个处理器总是从内存中取8个字节,此时需要读取一个double类型的数据,若是数据的地址都对齐成8的整数倍,那么只需要一个内存读取操作就可以了。如果地址没有对齐成8的整数倍,那么double类型的数据可能会被分放在两个8字节内存块中,此时则需要两个内存读取操作才可以完成。
总结来说:结构体的对齐是拿空间来换取时间的做法。
在设计结构体时,为了做到既满足对齐,又节省空间,我们需要做到:
让占用空间小的成员尽量集中在一起
//例如:
struct S1
{
char c1;
int i;
char c2;
};struct S2
{
char c1;
char c2;
int i;
};
此时S1和S2成员变量相同,但S1的大小为12个字节,而S2的大小为8个字节。
2.1.3 修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数
#pragma pack(1) //设置默认对齐数为1
#pragma pack() //取消设置的对齐数,还原成默认
3. 结构体传参
结构体传参和其他传参相似,都有传结构体和传结构体地址两种方式。
但是,需要进行结构体传参时,请尽量选择传结构体地址。
原因:
函数传参时,参数是需要压栈,会有时间和空间上的系统开销
如果传递一个对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
4. 结构体实现位段
4.1 什么是位段
位段的声明和结构体是类似的,有两个不同:
1. 位段的成员必须是int、unsigned int 、signed int 或 char(整型家族),在C99中位段成员的类型也可以选择其他类型。
2. 位段的成员名后边有一个冒号和一个数字。
比如:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
位段的位其实是二进制位的位,上述位段总计47个比特位,所以位段大小为8个字节。
那么位段是用来干什么用的呢?
struct S
{
int _a;
int _b;
int _c;
int _d;
};
设想一下,若是有以上结构体,其四个成员变量分别储存0,1,2,3。这四个整数中,最大的3仅占2个比特位,但是却都消耗了一个整型(32个比特位)的大小,那么是不是会显得很浪费?此时使用位段就能节省空间了。
总的来说:位段就是用来节省空间的
4.2 位段的内存分配
1. 位段的成员可以是 int、unsigned int 、signed int 或 char 等类型
2. 位段的空间上是按照以4个字节(int)或1个字节(char)的方式来开辟的
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
这里讲一下VS中位段的内存分配规则
1. C语言中并未规定位段的使用是从左向右还是从右向左,此时内存的使用方式就取决于编译器,VS的使用方式是从右向左
2. VS中,使用位段时会先按照所选择类型分配一块空间,若位段足够容纳下一个位段成员的大小时,位段会继续存放,直到位段不能容纳下一个位段成员的大小时,才会申请下一块空间,此时会舍弃剩余的位
4.3 位段的跨平台问题
1. int位段被当成有符号数还是无符号数是不确定的
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出现问题
3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
4.4 位段使用的注意事项
位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置时没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。
所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入另一个变量,再用这个变量给位段成员进行赋值。