
C语言自定义变量类型结构体理论:从初见到精通
延续上篇文章,本文将为大家带来C语言结构体的更多更深入的内容
一,结构体的自引用
在了解自引用之前,我们先解释一下线性数据结构之一的"链表"

链表:顾名思义,每个数据之间通过一种链式链接形成的一种数据结构。在这样的结构中,每一个数据块(我们称之为结点)需要具备两种能力:存放数据和找到下一个结点。
因此,结构体刚好可以胜任这份工作:
我们可以写一个结构体来介绍
objectivec
struct Node
{
int data;//存放数据
struct Node*next;//指向下一个结点
};
这个结构体有两个成员变量:
存放数据的变量我们称之为:数据域
存放下一个结点指针的变量我们称之为:指针域
**结构体可以找到下一个同类型结构体的这种操作,我们称之为:"**结构体的自引用"。
这样,结构体就可以通过自引用找到链表中的所有元素了。
二,结构体的内存对齐
至此:我们已经掌握了结构体的所有基本操作了
接下来,让我们思考一个问题:计算结构体的内存大小。
事实上当我们直接计算结构体的内存时会遇到这样的问题:
objectivec
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
如果运行,那么计算结果一定会出乎你的预料:是的,结果是8而不是6.
为什么会出现这种情况呢:
事实上,结构体内部存在对齐现象。
在解释这个问题之前,我们需要了解一点别的东西:
1,偏移量:

如图,我们把这样一张图看作是内存的一个片段(用来存放结构体),
在内存中,我们定义开始存放数据的位置叫起始位置
以这个位置为基准,自上而下存储数据,每个格子代表一字节。
每个字节相对于起始位置的距离我们称之为:偏移量(如图)
2,offsetof宏
offsetof宏用于计算结构体成员相对于结构体成员相对于结构体变量的偏移量
offsetof
offsetof (type,member)
Return member offset
This macro with functional form returns the offset value in bytes of member member in the data structure or union type type .
The value returned is an unsigned integral value of type size_t with the number of bytes between the specified member and the beginning of its structure.
Return value
A value of type size_t with the offset value of member in type.
这是对C-library原文的引用,可知,
offsetof宏有两个参数:结构体类型名和结构体成员变量名
返回值:偏移量(size_t类型)
应用举例:
objectivec
struct S1
{
char c1;
int i;
char c2;
};
size_t test=offsetof(struct S1,c1);
printf("%zu",test);
了解了这么多之后,让我们正式探讨结构体对齐的内涵。
3,结构体的对齐规则
- 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处。
2, 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对齐数=编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。
VS 中默认的值为 8。
Linux中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩。
结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍。
如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
我们用两个例子来解释这些规则
objectivec
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));

根据规则,因为char类型的大小是一个字节,其倍数可以是所有正整数,
char1放在偏移0的位置,char2放在偏移1的位置。
int的大小是4个字节,VS默认对齐数是8,8>4,对齐数是4,在偏移4开始向后4个字节。
最后整个内存的大小是8个字节是4的倍数,不需要补充空缺。
objectivec
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
看代码,
-
double
类型的d
占8个字节。 -
char
类型的c
占1个字节。 -
int
类型的i
占4个字节。
由于字节对齐规则,double
类型要求8字节对齐,char
类型后会填充3字节,使得 int
类型从第 8 + 1 + 3 = 12 字节开始,最终 struct S3
的大小为 8 + 1 + 3 + 4 = 16 字节
-
char
类型的c1
占1个字节。 -
struct S3
类型的s3
占16个字节。 -
double
类型的d
占8个字节。
char
类型后需要填充7字节以满足 struct S3
的8字节对齐要求,struct S3
本身大小为16字节,之后 double
类型直接从第 1 + 7 + 16 = 24 字节开始,因此 struct S4
的大小为 1 + 7 + 16 + 8 = 32 字节 。

4, 为什么存在内存对⻬?
⼤ 部分的参考资料都是这样说的:
平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。
性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到:
让 占⽤空间⼩的成员尽量集中在⼀起
5,修改默认对⻬数
#pragma这个预处理指令,可以改变编译器的默认对⻬数。
objectivec
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
如代码:通过**#pragma(1)** 指令将默认对齐数改成1,通过**#pragma()** 恢复指令**。**
三,补充一下结构体传参的内容
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。
结论: 结构体传参的时候,要传结构体的地址而不是结构体。
四,结构体与位段
1,什么是位段
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int 、 unsigned int 或 signed int ,
(在C99中位段成员的类型也可以 选择其他类型。)
- 位段的成员名后边有⼀个冒号和⼀个数字。
举例
objectivec
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A就是⼀个位段类型。
2,位段的内存分配
-
位段的成员可以是 int unsigned int
-
位段的空间上是按照需要以4个字节( signed int 或者是 char 等类型 int )或者1个字节( char )的⽅式来开辟的。
-
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
举例
objectivec
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;

3,位段的跨平台问题
-
int 位段被当成有符号数还是⽆符号数是不确定的。
-
位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会 出问题。
-
位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
-
当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利⽤,这是不确定的。
总结: 跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
4,位段的应⽤
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥ 使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络 的畅通是有帮助的

5,位段使⽤的注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位 置处是没有地址的。
内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊ 放在⼀个变量中,然后赋值给位段的成员。
objectivec
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}