观前提醒
看完本篇文章的时间在12min到15min之间。
文章目录
写在前面
本篇文章为自定义类型系列讲解的第一篇,而本篇文章讲解的时自定义类型的第一部分内容——结构体。同时,本篇文章也是结构体内容的详解,希望对你的结构体学习有所帮助。
1 结构体的声明
//结构体声明例1
struct MyStruct
{
int a;
char b;
};
这是一个简单的结构体声明,相信不少读者对这个声明都能信手拈来,但是结构体声明的知识远没有那么浅显。
1.1 结构体基础概念
简单地说,结构体是几个类型的值的集合,这些类型被称为成员变量,成员变量的类型可以不一样,而这个集合组成一个结构,组成一个新的类型。
1.2 声明
声明的方法和例1一样,很简单直白。这里举两个例子:
//例2
struct Student
{
char name[20];
int age;
char sex[8];
char phnum[20];
};
//例3
struct Commodity
{
char name[30];
int price;
int num;
};
但是有一种特殊声明,可以在声明结构时,进行不完全的声明。例如
//例4
struct
{
int a;
char b;
int c;
}a;
//例5
struct
{
int a;
char b;
int c;
}*p;
这种结构体被称为匿名结构体,省略了结构体标签,这也意味着除了一开始声明时定义的变量,后面是无法再想创建这个类型的变量的。
此外,例4和例5的成员变量是一样的,那么是否可以认为这两个结构体是同一个类型呢?
p = &a;
这一句代码在vs2022上运行会出现变量类型不兼容的警告。说明即使是成员变量相同,编译器也认为这是两个完全不同的结构体。
1.3 自引用
//例6
struct Example
{
int num;
struct Example next;
};
//例7
struct Example
{
int num;
struct Example* next;
};
例6的自引用似乎看起来比例7的更方便直接,但是实际上是否可行呢?试想一下,当在以例6的方法定义时,用sizeof(struct Example)
求结构体大小时,这个大小该是多少。所以例6的写法其实是错误的。
注意:无法使用typedef
重命名后的结构体变量名称来定义结构体成员变量。例子:
//错误示范
typedef struct
{
int num;
Example* next;
}Example;
//正确示范
typedef struct Example
{
int num;
struct Example* next;
}Example;
1.4 定义与初始化
这部分比较简单,下面我放几个例子解释一下如何定义和初始化。
struct Stu
{
char name[20];
int age;
}a;//声明结构体类型并定义一个变量
struct Stu b;//定义一个结构体变量
struct Candy
{
char name[30];
int price;
int num;
}n = { "chewing gum",10,1 };//声明结构体类型,定义并初始化一个变量
struct Candy m = { "Mints",6,1 };//定义并初始化一个变量
struct Reward
{
struct Stu stu;
struct Candy candy;
};
struct Reward a = { {"judy",6},{"lollipop",20,5} };//嵌套定义初始化
1.5 结构体内存对齐
研究结构体的内存对齐自然是用来计算结构体内存大小的,同时,这也是一个非常热门的考点,一定要掌握好。下面贴出几个代码做实验,如果可以的话,建议将先代码复制到编译器运行看是什么结果。
//实验1
struct S1
{
char a;
int b;
char c;
};
//printf("%d\n", sizeof(struct S1));//计算结构体S1的大小
//实验2
struct S2
{
char a;
char b;
int c;
};
//printf("%d\n", sizeof(struct S2));//计算结构体S2的大小
下图是这两个实验代码运行的结果。
很明显,虽然这两个结构体的成员变量相同,但是显示的的所占空间大小却不是一样的。所以很明显,在一个结构体变量中,结构体的成员变量的内存分布并不是紧挨着的,而是存在一定程度上的间隔,而这些间隔正是内存对齐这个机制所产生的。
那么,要想知道结构体的内存大小是如何计算的,就要先理解内存对齐的规则:
- 成员变量的存储位置总是在结构体对齐数整数倍的偏移量的位置。由于偏移量是从0开始计算起的,所以第一个变量的对齐数的整数倍是0,也就是说第一个成员变量在结构体偏移量为0的位置。
其中,对齐数 = 自身内存大小和默认对齐数两个数中的较小值
(vs的默认值是8,且据笔者所知,大部分编译器是没有设置这个默认对齐数的) - 结构体的大小为结构体成员变量中的最大对齐数的整数倍。
- 如果是嵌套了结构体的情况,那么嵌套结构体的对齐数是自身成员变量中最大对齐数,这个结构体的大小依然是成员(含嵌套结构体)变量的最大对齐数的整数倍
以上就是内存对齐的规则。其中,结构体成员变量的偏移量可以用宏offsetof
求得。S1
,S2
内存对齐的情况如图所示:
对于S1
,char a
在偏移量为0的位置,之后三个位置由于都不是int b
对齐数4的整数倍,所以直接跳过,从偏移量为4的位置开始存入int b
,存完数据后到了偏移量为8的位置,为对齐数1的整数倍,存入char c
,但此时结构体整体的内存并没有停止,由于此时总大小为9,不是成员变量中最大对齐数4的整数倍,所以继续往下占用空间,直到总大小为4的整数倍为止。
对于S2
,第一个成员变量char a
在偏移量为0的位置,而第二个成员变量char b
则直接在偏移量为1的位置,之后跳过两个位置在偏移量为4的位置存储int c
。此时总大小为8,是结构体成员变量最大对齐数的整数倍,所以不再继续往下占用空间,结构体总大小为8。
下图是用offsetof
求成员变量的偏移量验证刚刚的推导,事实与结论符合。
再给出下面两个实验代码,我会给出分析图和运行结构,请自行分析。
//实验3
struct S3
{
double a;
char b;
};
//printf("%d\n", sizeof(struct S3));//计算结构体S3的大小
//实验4
struct S4
{
struct S3 a;
int b;
char c;
};
//printf("%d\n", sizeof(struct S4));//计算结构体S4的大小
在分析完以上几个结构体的存储时,我们发现内存对齐有时可能会浪费大量的空间,可能会有人会有疑问,既然内存对齐会造成那么大的空间浪费,那为什么当初设计的时候要这样设计呢?
内存对齐的设计,大体上考虑以下两个因素:
- 不是所有的平台都能访问任意位置的任意类型的数据的。某些硬件平台只能在特定位置取出特定数据,若不存在内存对齐则会出现硬件异常。这是考虑了代码的泛用性。
- 数据结构(尤其是栈)应该让数据尽可能靠近自然边界。
这个比较难理解,由于处理数据时要用处理器上的寄存器,而寄存器访问内存数据时并不是一个一个字节访问的,一个寄存器的宽度是32位或者64位(根据平台而定),也就是说是一块一块空间访问的,而内存不对齐是必然会出现某一块空间的数据是属于两个变量的,这时候需要寄存器重新访问一遍空间以获取另外一个数据。这就造成了效率上的降低。
总的来说,内存对齐是一个以空间换时间的设计。
1.6修改默认对齐数
#pragma pack(4)//设置默认对齐数为8
struct S5
{
double a;
int b;
char c;
};
#pragma pack()//取消设置,还原默认对齐数
在对齐方式不合适时,可以这样修改默认对齐数。
2 位段
2.1 位段是什么
位段和结构体类似,只是位段的成员类型必须是int\unsigned int\signed int\char
(整型家族)中的一种,并且每个成员变量后面会有一个冒号加数字。例如:
//位段演示
struct Se
{
int a : 3;
char b : 3;
int c : 5;
};
这个数字的意义就是为这个成员变量开辟的空间,单位是bit。
2.2位段的内存分配
位段的内存开辟是按一个字节(char
)或者四个字节(int
)来的。且位段涉及很多不确定因素,不跨平台,设计可移植的程序时应该避免使用位段。下面是位段的空间使用说明。
struct Se
{
char a : 3;//3bit
char b : 4;//4bit
char c : 5;//5bit
char d : 4;//4bit
};
int main()
{
struct Se s = { 0 };
s.a = 10;//二进制:1010
s.b = 12;//二进制:1100
s.c = 3;//二进制:11
s.d = 4;//二进制:100
printf("%d\n", s.a);//3bit 1010 -> 010 -> 2
printf("%d\n", s.b);//4bit 1100 -> 1100(最高位为符号位,进行原反补计算) -> -4
printf("%d\n", s.c);//5bit 11 -> 00011 -> 3
printf("%d\n", s.d);//4bit 100 -> 0100 -> 4
return 0;
}
代码运行结果
2.3位段的跨平台问题
int
被当成有符号数还是无符号数是不确定的。上文代码中的情况就是被编译器当作有符号数。- 位段的最大位数不确定。如16位的机器最多16和32位的机器最多,写有27位的成员的位段显然不能在16位的机器上运行。
- 位段成员的数据是从左到右还是从右到左尚未又标准规定。
- 例如上文位段代码,成员a,b并未将第一个字节的空间占满,那么成员c是否会利用起来那个剩下的空间是不确定的。
结语
好的,到这里,自定义类型的第一部分也就是最长最难啃的部分就完结了,非常感谢各位读者能读完这篇文章,如果你觉得做的还不错的话,可以点赞收藏分享,让更多的朋友知道,当然,如果你觉得有什么问题的话也欢迎在评论区留言或私信告诉我哦!下期再会!
彩蛋
在下方的两个仓库可以获取我这篇文章的分析图和源码哦。
gitee:路径:Custom type\structs
GitHub:路径:Custom type\structs