大家好!从这篇文章开始我们详细说一下自定义类型。自定义类型有数组,结构体,枚举,联合。前面我们已经说过数组了,这里就说后面三种,首先,我们详解一下第一种:结构体。
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
例如描述一个学生:
这部分内容,我在前面的基础部分说过了,这里就不细说了。
在声明结构的时候,可以不完全的声明。
比如:
这样的叫做匿名结构体类型。
上面的两个结构在声明的时候省略掉了结构体标签(tag)。
那么问题来了?
警告:
编译器会把上面的两个声明当成完全不同的两个类型。所以是非法的。
在结构中包含一个类型为该结构本身的成员是否可以呢?
struct Node
{
int data;
struct Node next;
};
这样的自引用是否可行?
其实是不行的,因为我们不知道sizeof(struct Node)是多少。
正确的自引用方式:
struct Node
{
int data;
struct Node* next;
};
然后,我们再看一个问题:
typedef struct
{
int data;
Node* next;
}Node;
这样写代码,可行吗?
不行,因为在结构体里我们已经使用了Node,但是Node是后面定义的。所以就乱了。
正确的写法是:
typedef struct Node
{
int data;
struct Node* next;
}Node;
这里的Node和struct Node是一样的。
这部分内容我在基础部分说过了,不懂的可以去看看。
我们已经掌握了结构体的基本使用了。现在我们深入讨论一个问题:计算结构体的大小。这也是一个特别热门的考点: 结构体内存对齐。
我们来看一下这个代码:
struct S1
{
char c1;//1
int i;//4
char c2;//1
};
这个结构体的大小是多少呢?
估计很多人的第一反应是6个字节,其实不是。
结果是12个字节。为什么呢?首先,我们说一个函数offsetof
这个函数是返回数据结构或联合类型类型中成员成员的偏移值(以字节为单位)。
然后我们来看一下这三个成员的偏移量。
想要知道为什么,首先得掌握结构体的对齐规则:
我们先假设用struct S1类型创建一个变量s。
s下面的0,1,2,3…就是偏移量。
1.第一个成员在与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值(VS中默认的值为8)。
我们知道i是4个字节,比8小,所以对齐数是4。i就会放到4的整数倍处。
然后c2是1个字节,比8小,对齐数是1。c2放到1的倍数处。
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
这这句话我们就知道最大对齐数是4,而结构体总大小是它整数倍。前面我们已经使用了9个字节的空间,所以我们应该浪费3个字节的空间,为12才是4的倍数。
然后我们讨论一下结构体嵌套问题。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
举一个例子:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c;
struct S1 s2;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S2));
return 0;
}
首先,我们知道了struct S1是12个字节,它里面的最大对齐数是4。所以它应该对齐到4的倍数处。
struct S2结构的最大对齐数是8,然后我们已经使用了24个字节,是8的倍数。所以struct S2大小为24个字节。
为什么存在内存对齐?
大部分的参考资料都是如是说的:
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
解释一下:
struct S
{
char c;
int i;
};
如果没有对齐:
假如32位机器上,我们cpu一次拿4个字节,我们要访问i要两次才能全面把i拿出来。
如果对齐了:
i我们只需要访问一次就全拿出来了。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
看下面的代码:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
如果你在VS上测试过后,你会发现S1占12个字节,S2占了8个字节。
#pragma 这个预处理指令,可以改变我们的默认对齐数。
在这里struct S1的对齐数被改为1了。
结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。
这部分内容我在前面基础篇说过了,这里就不多说。
位段的声明和结构是类似的,有两个不同:
1.位段的成员可以是 int,unsigned int,signed int 或者是 char (属于整形家族)类型。
2.位段的成员名后边有一个冒号和一个数字。
比如:
struct A {
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A就是一个位段类型。那位段A的大小是多少?
首先,要懂得A的大小是多少,我们要知道位段的位是什么?
这里的位是二进制位。
:2的意思是_a只占2个bit位,:b只占5个bit位…
1.位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2.位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
看下面的代码:
struct S {
char a:3;
char b:4;
char c:5;
char d:4;
};
char类型是按1个字节来开辟空间的。所以我们首先来开辟1个字节(8bit),a占3个bit,还剩5bit,b占4个bit,还剩1个bit。c占5个bit,不够,该怎么办?
我们有两种情况:1.剩下的1个bit浪费,再开辟一个字节(8bit)。2.剩下的1个bit用上,再开辟一个字节(8bit)。
第一种,开辟了8个bit,c占5个,还剩3个。d占4个,不够,再开辟一个字节。
第二种,用了上面剩下的1个bit,c还剩4个,开辟1个字节,还剩4bit,正好够d使用。
如果是第一种情况,我们需要3个字节。第二种情况,我们只需要2个字节。
结果是什么,我们来看一下:
是3个字节,是第一种那个剩下bit不用的情况。
其实在C语言里,我们没有规定是用剩下的位,还是不用剩下的位,这是根据编译器来决定的,所以位段是不跨平台的,但位段能为我们节省空间。
估计很多人不理解为什么一个int类型或char类型,只占2个bit,或5个bit。
这是我给大家举例子,实际上,我们要根据实际情况来定,假设我们只需要01,00,10,11,那么我们就可以用2个bit来表示,如果用32位,则会浪费空间。
我们看一下下面的代码:
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;
}
那么有一个问题:s空间是如何开辟的?
首先,我们知道struct S 是3个字节,然后我们给s里的成员赋值,是如何赋值的呢?
a:10---->01010,但只占3个bit,所以只取010
b:12---->01100,但只占4个bit,所以只取1100
c: 3---->00011,但只占5个bit,所以只取00011
d: 4---->00100,但只占4个bit,所以只取0100
然后,我们要将这些放到s里面,怎么放?首先,第一个问题,在每一个字节里,我们每一位是从左向右放,还是从右向左放,我们先假设从右向左放。
上面我们测试在VS编译器下,剩下的位不用。放的情况如下图所示:
我们将转换为为16进制是62 03 04。然后我们看一下编译器的结果:
结果和我们假设一样。
总结:
到这里,我们就把与结构体相关的内容说完了,内存对齐和位段都挺重要的,希望能理解它们。下一篇文章,我会讲一下自定义类型中的枚举和联合体。如果大家认为我有哪些不足之处或者知识上的错误都可以告诉我,我会在之后的文章中不断改正,也请大家多多包涵。如果大家觉得这篇文章有用的话,也希望大家可以给我关注点赞,你们的支持就是对我最大的鼓励,我们下一篇文章再见。