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