结构体是一些值的集合,这些值成为成员变量。结构的每个成员可以是不同类型的变量。结构体属于自定义类型。
1.1 结构体声明 1.1.1 标准声明struct User
{
char Name[20];
char Sex[10];
};
1.1.2 特殊声明(不完全声明)此结构体的类型就是
struct User
User
是结构体标签(tag)
struct
{
char Name[20];
char Sex[10];
int age;
}User1;
struct
{
char Name[20];
char Sex[10];
int age;
}*p;
1.2 结构体的自引用这种声明方法省略了结构体标签
(tag)
, 直接定义的User1
和p
均被称为匿名结构体变量
。无法在main
函数中再次定义此种结构体变量
并且,虽然表面上User1
和*p
的结构体类型
好像一样。但是如果
p = &User1;
进行编译,将会报出警告。警告内容是:指针 到 指针 的类型不兼容
。说明 两个变量的结构体类型并不相同,编译器认为,两个成员一样的匿名结构体类型是两种不同的类型
以 链表节点 为例:
struct Node
{
int data;
struct Node* next;
}
// 自引用 结构体类型的指针
但是
typedef struct
{
int data;
Node* next;
}Node;
// 不行
// 结构体内部使用 Node* 时, typedef 还未生效
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
字节呢?
这就是由于内存对齐的原因:
1.4.1 结构体内存对齐规则在正式开始讲解什么是内存对齐之前,我们先来了解一个概念:
结构体成员的偏移量
什么是结构体成员的偏移量?
一个结构体的每个成员地址相对于其结构体类型的首地址的偏移量,就是结构体成员的偏移量。
怎么计算结构体成员的偏移量?
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
之后还有三个字节的空间 被这个结构体所占用。所以,此结构体的内存空间占用情况,可能是这样的:
那么,为什么呢?为什么会有 开辟了,但是没有用到 的空间呢?一个结构体类型的大小,到底如何计算呢?
-
结构体的第一个成员变量存放在结构体变量 开始位置的 0 偏移处
-
从第二个成员变量开始,要对齐到 某个对齐数 的整数倍的地址处
对齐数:编译器的默认对齐数 与 该成员自身大小 的较小值
- VS编译器 中 对齐数默认为 8
- 如果编译器默认没有对齐数,对齐数就是成员自身大小
-
结构体总大小必须是 最大对齐数的整数倍
最大对齐数:结构体所有成员中,对齐数最大的成员的对齐数
-
如果是结构体嵌套了结构体的情况,被嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
了解了结构体内存对齐规则,就来详细了解一下结构体到底是如何内存对齐的
1.4.2 结构体内存对齐详解 1.4.2.1 示例1:我们以上面举过例子的结构体为例:
struct S1
{
char c1;
int i;
char c2;
};
先看一下总大小:
然后我们具体来计算一下:
1.4.2.2 示例2:
除第一个成员外的结构体成员 成员自身大小成员自身大小(类型大小) 编译器默认对齐数 实际对齐数(取较小值) i
(int)
48 4 c2
(char)
18 1 根据规则:
c1
存放在结构体变量 开始地址的 0 偏移处
i
的对齐数是4
,所以存放在偏移量是4
的整数倍 处至少是
4
c2
的对齐数是1
,所以存放在偏移量是1
的整数倍 处结构体总大小必须为 最大对齐数的整数倍,在此结构体中即为
4
的整数倍。
c2
所在空间已经是 第9
个字节,所以此结构体总大小 最小为12
所以,结构体大小为
12
字节
再来看下边这个例子:
struct S2
{
char c1;
char c2;
int i;
};
我们将,上一个结构体成员中的,i
和c2
换一换位置结果又是什么呢?
我们发现只是换了一下位置,结构体大小就减少了 4
个字节
这次又是怎么对齐和计算的呢?
1.4.2.3 示例3:
除第一个成员外的结构体成员 成员自身大小成员自身大小(类型大小) 编译器默认对齐数 实际对齐数(取较小值) c2
(char)
18 1 i
(int)
48 4
c1
存放在结构体变量 开始地址的 0 偏移处
c2
的对齐数是1
,所以存放在偏移量是1
的整数倍 处,c2
下面就可以
i
的对齐数是4
,所以存放在偏移量是4
的整数倍 处至少是
4
- 结构体总大小必须为 最大对齐数的整数倍,在此结构体中即为
4
的整数倍。
i
存放完,结构体占8
个字节,正好是4
的倍数,所以不用再占用其他空间此结构体总大小为:
8
字节
再来一个例子:
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
字节
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 个例子之后,结构体的内存对齐应该就已经解释清楚了
不过,我们再来总结一下规则:
1.4.3 为什么存在内存对齐
结构体的第一个成员,就存放在结构体偏移量为 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
)
-
硬件平台原因(移植的问题)
不是所有的硬件平台都能访问 任意地址上 的 任意数据 ;
某些硬件平台只能在 某些地址处 取 某些特定类型的数据(经过对齐的特定类型的数据),否则抛出硬件异常。
-
性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,若访问未对齐的内存,处理器需要作两次内存访问;而对 对齐的内存访问仅需要一次访问举个例子:
struct S { char c; int i; } int main() { struct S s1; return 0; }
上边的代码,创建了一个结构体变量,我们来假设两种情况
-
假设 不对齐内存
-
假设 对齐内存
假设,我们是 32 位环境,一次可以读取 4 字节
那么对于
第一种情况
:我们要访问完整的
i
的数据,就需要访问两次因为,第一次只访问到了
i
的 前三个字节,第二次访问了i
的最后一个字节对于
第二种情况
:我们要访问完整的
i
的数据,只需要一次访问跳过前 4 个字节,直接访问
i
的数据,效率要更高一些所以,内存对齐,有一定的性能的提升
所以,结构体的内存对齐,是一种用空间来换取时间的做法。
-
了解了结构体内存对齐,知道了结构体变量内部的成员,在内存中可能并不是紧密排列的,是需要对齐的。是一种拿空间换取时间的做法。
那么有没有一种做法,可以在对其的同时尽量的节省空间呢?
答案是:有!
我们 举过的例子,示例 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) //设置默认对齐数为1 #include
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; }
以上两种结构体的传参方式,大家思考一下,哪一种传参方式好一些,为什么?
先来分析一下,结构体传参,传值和传地址,有什么不同:
如果传值:
给函数传入结构体变量的值,函数需要将结构体变量的值完全拷贝一份,再存储到形参。可以修改形参的值,但不能直接访问原结构体变量。
如果传地址:
给函数传入结构体变量的地址,函数需要将地址拷贝一份,存储至形参。并且可以通过地址,来直接访问原结构体变量。
所以这样来看,应该是 传结构体地址的方式好一些。具体原因:
函数传参时,参数是需要压栈的,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,函数压栈的系统开销就会比较大,会导致项目性能的下降。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)