![[C语言] 详解结构体, 详细分析结构体对齐](https://dxyt-july-image.oss-cn-beijing.aliyuncs.com/image-20230409155622571.webp)
Table of Contents
结构体 Link to 结构体
1. 结构体概念 Link to 1. 结构体概念
结构体是一些值的集合, 这些值成为成员变量。结构的每个成员可以是不同类型的变量。结构体属于自定义类型。
1.1 结构体声明 Link to 1.1 结构体声明
1.1.1 标准声明 Link to 1.1.1 标准声明
1234
struct User {
char Name[20];
char Sex[10];
};
此结构体的类型就是
struct User
User
是结构体标签(tag)
1.1.2 特殊声明(不完全声明) Link to 1.1.2 特殊声明(不完全声明)
1234567891011
struct {
char Name[20];
char Sex[10];
int age;
}User1;
struct {
char Name[20];
char Sex[10];
int age;
}*p;
这种声明方法省略了结构体标签
tag)
, 直接定义的User1
和p
均被称为匿名结构体变量
。无法在main
函数中再次定义此种结构体变量 并且, 虽然表面上User1
和*p
的结构体类型
好像一样。但是如果
p = &User1;
进行编译, 将会报出警告。警告内容是:指针 到 指针 的类型不兼容
。说明 两个变量的结构体类型并不相同, 编译器认为,两个成员一样的匿名结构体类型是两种不同的类型
1.2 结构体的自引用 Link to 1.2 结构体的自引用
以 链表节点 为例:
12345
struct Node {
int data;
struct Node* next;
}
// 自引用 结构体类型的指针
但是
123456
typedef struct {
int data;
Node* next;
}Node;
// 不行
// 结构体内部使用 Node* 时, typedef 还未生效
1.3 结构体变量的定义和初始化 Link to 1.3 结构体变量的定义和初始化
示例:
1234567891011121314151617181920212223
// 结构体声明
struct Contact {
char Addr[30];
char Tele[12];
};
// 嵌套结构体的结构体声明
struct User {
char Name[20];
char Sex[10];
int age;
struct Contact C;
};
int main() {
struct Contact C1 = {"CSDN", "2xxxxxxxxxx" };//结构体变量的定义和初始化
struct Contact C2 = { }; // 结构体变量所有位初始化为0, 与{0}同作用
struct Contact C3 = { .Tele = "2xxxxxxxxxx" };//结构体变量的定义 部分初始化
struct User U1 = { "July3", "Male", 19, { "CSDN", "1xxxxxxxxxx" } }; //嵌套结构体的结构体变量的定义和初始化
return 0;
}
使用{}
对结构体变量初始化, 可以这样理解:
- 未指定初始化值的成员, 均被初始化为0
- 如果指定了, 就初始化为指定值
1.4 结构体内存对齐 * Link to 1.4 结构体内存对齐 *
要弄明白什么是结构体内存对齐, 我们先来看一下结构体大小
下面这段结构体的大小是多少?
12345
struct S1 {
char c1;
int i;
char c2;
};
我们用 sizeof
求出此结构体类型的大小是: 12
字节
但是 int
类型大小是 4
字节, char
类型的大小是 1
字节。这个结构体大小不应该是 6
字节吗? 为什么是 12
字节呢?
这就是由于内存对齐的原因:
在正式开始讲解什么是内存对齐之前, 我们先来了解一个概念:
结构体成员的偏移量
什么是结构体成员的偏移量?
一个结构体的每个成员地址相对于其结构体类型的首地址的偏移量, 就是结构体成员的偏移量。
怎么计算结构体成员的偏移量?
C语言中, 计算结构体成员的偏移量有一个函数:
offsetof
offsetof
C
1size_t offsetof( structName, memberName);
offsetof
函数的两个参数是: 结构体类型名, 计算偏移量的成员名所在头文件是
stddef.h
我们先计算一下上边这段结构体类型, 各成员的偏移量(
%zu是输出 size_t 类型的数据的指定格式
)
第一个,
c1
的偏移量是0
;其次,
i
的偏移量是4
;最后,
c2
的偏移量是8
.我们做图明确出来:
可以非常明显的看出, 结构体成员
c1
到i
之间, 有三个字节的空间是空的不过, 这也才占用了
9
字节的空间, 但是我们计算的 结构体的大小 是12
, 所以我们判断, 在c2
之后还有三个字节的空间 被这个结构体所占用。所以, 此结构体的内存空间占用情况, 可能是这样的:
那么, 为什么呢?为什么会有 开辟了, 但是没有用到 的空间呢?一个结构体类型的大小, 到底如何计算呢?
1.4.1 结构体内存对齐规则 Link to 1.4.1 结构体内存对齐规则
结构体的第一个成员变量存放在结构体变量 开始位置的 0 偏移处
从第二个成员变量开始, 要对齐到 某个对齐数 的整数倍的地址处
对齐数: 编译器的默认对齐数 与 该成员自身大小 的较小值
- VS编译器 中 对齐数默认为 8
- 如果编译器默认没有对齐数, 对齐数就是成员自身大小
结构体总大小必须是 最大对齐数的整数倍
最大对齐数: 结构体所有成员中, 对齐数最大的成员的对齐数
如果是结构体嵌套了结构体的情况, 被嵌套的结构体对齐到自己的最大对齐数的整数倍处, 结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
了解了结构体内存对齐规则, 就来详细了解一下结构体到底是如何内存对齐的
1.4.2 结构体内存对齐详解 Link to 1.4.2 结构体内存对齐详解
1.4.2.1 示例1: Link to 1.4.2.1 示例1:
我们以上面举过例子的结构体为例:
12345
struct S1 {
char c1;
int i;
char c2;
};
先看一下总大小:
然后我们具体来计算一下:
除第一个成员外的结构体成员 成员自身大小成员自身大小(类型大小) 编译器默认对齐数 实际对齐数(取较小值) i
(int)
48 4 c2
(char)
18 1 根据规则:
c1
存放在结构体变量 开始地址的 0 偏移处
i
的对齐数是4
, 所以存放在偏移量是4
的整数倍 处至少是
4
c2
的对齐数是1
, 所以存放在偏移量是1
的整数倍 处
结构体总大小必须为 最大对齐数的整数倍, 在此结构体中即为
4
的整数倍。
c2
所在空间已经是 第9
个字节, 所以此结构体总大小 最小为12
所以, 结构体大小为
12
字节
1.4.2.2 示例2: Link to 1.4.2.2 示例2:
再来看下边这个例子:
12345
struct S2 {
char c1;
char c2;
int i;
};
我们将, 上一个结构体成员中的, i
和c2
换一换位置结果又是什么呢?
我们发现只是换了一下位置, 结构体大小就减少了 4
个字节
这次又是怎么对齐和计算的呢?
除第一个成员外的结构体成员 成员自身大小成员自身大小(类型大小) 编译器默认对齐数 实际对齐数(取较小值) c2
(char)
18 1 i
(int)
48 4
c1
存放在结构体变量 开始地址的 0 偏移处
c2
的对齐数是1
, 所以存放在偏移量是1
的整数倍 处,c2
下面就可以
i
的对齐数是4
, 所以存放在偏移量是4
的整数倍 处, 至少是4
结构体总大小必须为 最大对齐数的整数倍, 在此结构体中即为
4
的整数倍
i
存放完, 结构体占8
个字节, 正好是4
的倍数, 所以不用再占用其他空间
此结构体总大小为:
8
字节
1.4.2.3 示例3: Link to 1.4.2.3 示例3:
再来一个例子:
12345
struct S3 {
double n;
char c1;
int i;
};
我们先自己计算:
除第一个成员外的结构体成员 成员自身大小(类型大小) 编译器默认对齐数 实际对齐数(取较小值) c1
(char)
18 1 i
(int)
48 4
n
存放在结构体变量 开始地址的0
偏移处
c2
的对齐数是1
, 所以存放在偏移量是1
的整数倍 处
i
的对齐数是4
, 所以存放在偏移量是4
的整数倍处, 至少是12
结构体总大小必须为 最大对齐数的整数倍, 在此结构体中即为
8
的整数倍。
i
存放完, 结构体占16
个字节, 正好是8
的倍数, 所以不用再占用其他空间
此结构体总大小为:
16
字节
我们来验证一下:
确实跟我们计算的一样, 这个结构体大小为 16
字节
1.4.2.4 示例4(嵌套结构体的结构体) Link to 1.4.2.4 示例4(嵌套结构体的结构体)
1234567891011
struct S3 {
double n;
char c1;
int i;
};
struct S4 {
int n;
struct S3 s;
char c1;
};
那么嵌套有结构体的结构体S4
, 它的成员怎么去对齐, 它的大小怎么计算呢?
按照内存对齐的规则:
如果是结构体嵌套了结构体的情况, 被嵌套的结构体对齐到自己的最大对齐数的整数倍处, 结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
被嵌套的结构体的对齐数, 就是它的 对齐数最大的成员 的对齐数
所以, 我们先来计算:
除第一个成员外的结构体成员 成员自身大小(类型大小) 对齐数最大的成员的对齐数 编译器默认对齐数 实际对齐数 s
(struct S3)
16(double)
88 8 c1
(char)
18 1
n
(大小为4
)存放在结构体变量 开始地址的0
偏移处
s
(大小为16
)的对齐数是8
, 所以存放在偏移量是8
的整数倍 处
c1
(大小为1
)的对齐数是1
, 所以存放在偏移量是1
的倍数 处
结构体总大小必须为 最大对齐数的整数倍, 在此结构体中即为
8
的整数倍。
c1
存放完, 已经占用25
字节, 所以此结构体总大小 最小为32
此结构体总大小为:
32
字节
举过 4 个例子之后, 结构体的内存对齐应该就已经解释清楚了
不过, 我们再来总结一下规则:
结构体的第一个成员, 就存放在结构体偏移量为 0 的地址处
第二个及以后的成员, 需要在 对齐数的整数倍 的地址处存放
结构体的总大小, 必须是其成员的最大对齐数的整数倍
如果是结构体嵌套了结构体的情况, 被嵌套的结构体的对齐数, 就是 它内部成员的最大对齐数
例如:
C
1234567891011struct S3 { double n; char c1; int i; }; struct S4 { int n; struct S3 s; char c1; };
在 结构体
struct S4
声明中, 嵌套了结构体struct S3
那么, 变量
s
的对齐数, 就是结构体struct S3
内部的成员的最大对齐数(为8
), 变量s
的大小, 就是结构体struct S3
的大小(为16
)
1.4.3 为什么存在内存对齐 Link to 1.4.3 为什么存在内存对齐
硬件平台原因(移植的问题)
不是所有的硬件平台都能访问 任意地址上 的 任意数据 ;
某些硬件平台只能在 某些地址处 取 某些特定类型的数据(经过对齐的特定类型的数据), 否则抛出硬件异常。
性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于, 若访问未对齐的内存, 处理器需要作两次内存访问;而对 对齐的内存访问仅需要一次访问
举个例子:
C123456789struct S { char c; int i; } int main() { struct S s1; return 0; }
上边的代码, 创建了一个结构体变量, 我们来假设两种情况
假设 不对齐内存
假设 对齐内存
假设, 我们是 32 位环境, 一次可以读取 4 字节
那么对于
第一种情况
:我们要访问完整的
i
的数据, 就需要访问两次因为, 第一次只访问到了
i
的 前三个字节, 第二次访问了i
的最后一个字节对于
第二种情况
:我们要访问完整的
i
的数据, 只需要一次访问跳过前 4 个字节, 直接访问
i
的数据, 效率要更高一些所以, 内存对齐, 有一定的性能的提升
所以, 结构体的内存对齐, 是一种用空间来换取时间的做法。
1.4.4 结构体声明(设计)的优化 Link to 1.4.4 结构体声明(设计)的优化
了解了结构体内存对齐, 知道了结构体变量内部的成员, 在内存中可能并不是紧密排列的, 是需要对齐的。是一种拿空间换取时间的做法。
那么有没有一种做法, 可以在对其的同时尽量的节省空间呢?
答案是: 有!
我们 举过的例子, 示例 2
和 示例 3
仅仅是将结构体内部的成员换了一个位置, 就节省了 4 个字节的空间。
1234567891011
struct S1 {
char c1;
int i;
char c2;
};
// ↓↓↓
struct S2 {
char c1;
char c2;
int i;
};
看过上边的对比, 我们发现 c1
和 c2
对空间的占用都很小, 都是 1
字节。两个占用内存空间小的变量挤在一起, 就能减少空间的浪费。
所以呢, 如果 既要满足对齐, 还要尽量节省空间, 就
让占用空间小的成员尽量集中在一起
1.4.5 默认对齐数的修改 Link to 1.4.5 默认对齐数的修改
上边对齐规则中, 我们提到 VS编译器中, 结构体的 默认对齐数 是8。而在 Linux 平台下的 gcc
编译器 是没有默认对齐数的。但是, 我们可以通过一个预处理指令 来设置本项目中的默认对齐数。
1
#pragma pack (n)
这句预处理指令是设置默认对齐数用的, n
就是 要设置的默认对齐数的值
C
1234567891011121314#pragma pack(1) //设置默认对齐数为1 #include <stdio.h> struct S1 { char c1; int i; char c2; } int main() { printf("%zu", sizeof(struct S1)); return 0; }
此时,
struct S1
的总大小变成了6
字节, 而我们没有改变的时候是12
字节
注意!!默认对齐数千万不要乱改, 一般改为 2 的次方。
1.5 结构体传参 Link to 1.5 结构体传参
如果我们想要写一个比如, 修改结构体变量、输出结构体变量等功能的函数, 就会遇到和其他函数一样的问题——传参。
函数调用结构体变量时, 怎么传参给函数呢?
结构体变量直接传参
C1234567891011121314struct S { int data[1000]; int num; }; struct S s1 = { {2,0,0,2}, 2022 }; void print1(struct S s) { printf("%d\n", s.num); } int main() { print1(s1); return 0; }
结构体变量地址传参
C1234567891011121314struct S { int data[1000]; int num; }; struct S s1 = { {2,0,0,2}, 2022 }; void print2(struct S* ptrs) { printf("%d\n", ptrs->num); } int main() { print2(&s1); return 0; }
以上两种结构体的传参方式, 大家思考一下, 哪一种传参方式好一些, 为什么?
先来分析一下, 结构体传参, 传值和传地址, 有什么不同:
如果传值:
给函数传入结构体变量的值, 函数需要将结构体变量的值完全拷贝一份, 再存储到形参。可以修改形参的值, 但不能直接访问原结构体变量。
如果传地址:
给函数传入结构体变量的地址, 函数需要将地址拷贝一份, 存储至形参。并且可以通过地址, 来直接访问原结构体变量。
所以这样来看, 应该是 传结构体地址的方式好一些。具体原因:
函数传参时, 参数是需要压栈的, 会有时间和空间上的系统开销。
如果传递一个结构体对象的时候, 结构体过大, 函数压栈的系统开销就会比较大, 会导致项目性能的下降。