一、结构体
在C语言中,结构体(struct
)是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起,形成一个整体。结构体非常适合用于表示复杂的数据结构,如点、矩形、学生信息等。下面是结构体的定义、初始化、使用和操作的详细介绍 。
1. 结构体的定义
结构体类型的定义形式:
//形式1 先定义类型,然后定义变量
struct 结构体名
{
成员列表;
};
struct 结构体名 d;
//形式2 定义类型的同时,定义变量
struct 结构体名
{
成员列表;
}d; //
//形式3 定义类型的同时,定义变量,可以省略 结构体名
struct
{
成员列表;
}d; //如果,结构体类型只用一次
struct 关键字:表示 是在构造一个结构体的类型
结构体名:用来 描述 结构体这个类型 一个名称
成员列表 : 表示 要描述的复杂数据中用到的 具体的成员变量, 定义的方式,与之前变量的方式相同,多个成员变量之间,用分号隔开。
最后 有一个';'分号表示 结束
2. 结构体变量的声明与初始化
struct Point {//定义结构体
int x; // x坐标
int y; // y坐标
};
看每个成员变量,具体是什么数据类型、根据 各个成员变量 自身的数据类型,进行初始化。初始化的顺序,按照定义的顺序,依次初始化。
2.1 声明结构体变量
struct Point p1;
2.2初始化结构体变量
struct Point p1 = {10, 20}; // 初始化p1的x为10,y为20
struct Point P2 = {.x=10};//部分初始化
struct Point p3;//逐个赋值
p3.x=10;
p3.y=20;
2.3访问结构体成员
使用点运算符(.
)可以访问结构体的成员
printf("x = %d, y = %d\n", p1.x, p1.y);
2.4结构体数组
可以声明结构体数组,用于存储多个结构体对象。
struct Point points[3] = {{0, 0}, {1, 2}, {3, 4}};
2.5结构体指针
可以定义指向结构体的指针,并通过箭头运算符(->
)来访问结构体成员。
struct Point *ptr = &p1;
printf("x = %d, y = %d\n", ptr->x, ptr->y);
2.6. 结构体与函数
传值
传值方式将整个结构体复制一份传递给函数:传值方式适合结构体较小的情况,但会有一定的性能开销。
void printPoint(struct Point p) {
printf("x = %d, y = %d\n", p.x, p.y);
}
传指针
传指针方式传递结构体的地址,不会复制结构体数据,效率更高:使用这种方式可以直接修改传入的结构体数据。
void movePoint(struct Point *p, int dx, int dy) {
p->x += dx;
p->y += dy;
}
二、结构体对齐
结构体在内存中的布局可能会受到编译器的对齐规则影响。编译器可能会插入一些填充字节以确保结构体的成员在内存中的对齐。可以参考下面的结构体对齐规则图片。
以32位为例说明
struct t{
char a;
short b;
char c;
};
具体分析
-
char a
的对齐:char
类型为1字节,起始地址对齐到1字节边界。它在结构体中的偏移量是0。 -
short b
的对齐:short
类型为2字节,起始地址必须对齐到2字节边界。由于char a
只占1字节,因此b
必须从偏移量为2的地方开始。这意味着在a
之后会有1字节的填充(padding)。所以,
b
的实际存储位置是偏移量2到3(共2字节)。 -
char c
的对齐:char
类型为1字节,它紧跟在b
之后。但是,b
占用了偏移量2到3的两个字节,因此c
的起始地址应该是偏移量4。所以,
c
的实际存储位置是偏移量4(共1字节)。 -
结构体大小的对齐:根据规则,结构体的大小必须是其最大对齐要求的整数倍。对于这个结构体,最大对齐要求是2字节对齐。到目前为止,我们的结构体大小为5字节。为了对齐到2字节的边界,结构体最终的大小应为6字节。
最终的内存布局
char a
:占用偏移量0- 1字节填充:占用偏移量1
short b
:占用偏移量2-3char c
:占用偏移量4- 1字节填充:占用偏移量5
结构体 t
的总大小
根据以上分析,struct t
的总大小应为 6字节。
struct t {
char a; // 1字节
short b[4]; // 4个short,每个2字节,共8字节
char c; // 1字节
};
注意:
short b[4]
的对齐:
short
类型为2字节,数组b[4]
需要对齐到2字节边界。a
占用偏移量0,因此b[4]
应该从偏移量2开始。为了确保这一点,在a
之后会有1字节的填充。b[4]
数组占用8字节(偏移量2到9)
-
char c
的对齐:char c
类型为1字节,它紧跟在b[4]
之后。b[4]
占用偏移量2到9,因此c
应该从偏移量10开始。
-
结构体大小的对齐:
- 根据规则,结构体的大小必须是其最大对齐要求的整数倍。在这个结构体中,
short
的对齐要求是2字节,而结构体的大小应当是4字节对齐的。 - 目前我们计算出的结构体大小为11字节(1字节
a
+ 1字节填充 + 8字节b[4]
+ 1字节c
)。 - 为了对齐到4字节的边界,编译器会在最后再填充3个字节,使结构体的总大小为12字节。
- 根据规则,结构体的大小必须是其最大对齐要求的整数倍。在这个结构体中,
最终的内存布局
char a
:占用偏移量0(1字节)- 1字节填充:占用偏移量1
short b[4]
:占用偏移量2到9(共8字节)char c
:占用偏移量10(1字节)- 2字节填充:占用偏移量11到12
结构体 t
的总大小:根据以上分析,struct t
的总大小应为 12字节
struct t {
char a; // 1字节
int b; // 4字节
float c; // 4字节
long h; // 8字节 (假设为8字节, 取决于平台,64位平台)
long int d; // 8字节 (假设为8字节, 取决于平台)
char e; // 1字节
};
具体分析(64位平台上)
-
char a
的对齐:char a
类型为1字节,起始地址对齐到1字节边界。它在结构体中的偏移量是0。
-
int b
的对齐:-
int
类型为4字节,对齐到4字节边界。由于a
只占1字节,因此b
必须从偏移量4的地方开始。这意味着在a
之后会有3字节的填充。 -
b
的实际存储位置是偏移量4到7。
-
-
float c
的对齐:float
类型为4字节,对齐到4字节边界。它紧跟在b
之后。c
的实际存储位置是偏移量8到11。
-
long h
的对齐:-
long
类型为8字节,对齐到8字节边界。它需要从偏移量16开始,因为c
占用偏移量8到11,而要对齐到8字节边界,编译器会在之后再插入4字节的填充。 -
h
的实际存储位置是偏移量16到23。
-
-
long int d
的对齐:long int
类型为8字节,对齐到8字节边界。它紧跟在h
之后,占用偏移量24到31。
-
char e
的对齐:char e
类型为1字节,紧跟在d
之后。它占用偏移量32,但为了保持结构体的对齐要求,编译器会在最后再填充7个字节,使结构体的总大小对齐到8字节边界。
最终的内存布局
char a
:占用偏移量0(1字节)- 3字节填充:占用偏移量1到3
int b
:占用偏移量4到7(4字节)float c
:占用偏移量8到11(4字节)- 4字节填充:占用偏移量12到15
long h
:占用偏移量16到23(8字节)long int d
:占用偏移量24到31(8字节)char e
:占用偏移量32(1字节)- 7字节填充:占用偏移量33到39
结构体 t
的总大小:根据以上分析,struct t
的总大小应为 40字节。
struct t {
char a; // 1字节
int b[3]; // 3个int,每个4字节,共12字节
float c; // 4字节
long h; // 8字节 (假设为8字节,取决于平台)
long double d; // 16字节 (假设为16字节,取决于平台)
int u; // 4字节
};
-
char a
的对齐:char a
类型为1字节,起始地址对齐到1字节边界。它在结构体中的偏移量是0。
-
int b[3]
的对齐:int
类型为4字节,b[3]
需要对齐到4字节边界。- 由于
a
只占1字节,因此b
必须从偏移量4开始,这意味着在a
之后会有3字节的填充。 b[3]
数组占用12字节(偏移量4到15)。
-
float c
的对齐:float
类型为4字节,对齐到4字节边界。它紧跟在b[3]
之后,占用偏移量16到19。
-
long h
的对齐:long
类型为8字节,对齐到8字节边界。它需要从偏移量24开始,因为c
占用偏移量16到19,为了对齐到8字节边界,编译器会在之后再插入4字节的填充。h
的实际存储位置是偏移量24到31。
-
long double d
的对齐:long double
类型为16字节,对齐到16字节边界。它紧跟在h
之后,占用偏移量32到47。
-
int u
的对齐:int u
类型为4字节,对齐到4字节边界。它紧跟在d
之后,占用偏移量48到51。- 为了保持结构体的对齐要求(64位、超过4字节、按照成员最大的字节数对齐即位16字节对齐),编译器会在最后再填充4=12个字节,使结构体的总大小对齐到16字节边界。
所以总字节为64
三、内存对齐
内存对齐是指在计算机内存中,将数据存储在特定的地址上,这些地址是数据类型大小的整数倍。内存对齐通常由编译器自动处理,但也可以手动调整。内存对齐的主要目的是提高访问数据的效率,同时满足硬件平台的要求。
1. 什么是内存对齐
内存对齐要求数据在内存中的起始地址必须是某种特定的地址倍数。例如,假设一个系统要求4字节的整数类型(int
)必须存储在4的倍数地址上(如0x0000、0x0004、0x0008等),否则会导致访问效率下降甚至产生错误,一个数据被拆分为多次次读取、读取后还要进行合并,才能得到一个数据、导致效率低,所以要内存对齐。
2.为什么要内存对齐
提高内存访问效率:许多处理器在访问内存时,如果数据存储在一个对齐的地址上,会比访问非对齐的数据更快。例如,访问4字节对齐的整数数据,只需要一次内存读取操作,而非对齐数据可能需要多次操作才能完成访问。
硬件要求:某些处理器架构(如某些RISC架构)要求数据必须对齐,否则会引发硬件异常。虽然现代处理器通常支持非对齐访问,但这往往会导致性能下降。
简化内存控制逻辑:内存对齐使得内存控制单元可以简化处理逻辑,减少不必要的复杂性。对齐的数据可以在单次访问中读取或写入,而不需要跨多个内存地址进行分段处理。
四、共用体
共用体(union)是C语言中的一种数据结构,它允许在相同的内存位置存储不同的数据类型。共用体与结构体(struct)的主要区别在于结构体的每个成员都有自己独立的内存,而共用体的所有成员共享同一块内存。
共用体通常用于节省内存。例如,当需要存储多个不同类型的数据但在任何时刻只需要其中之一时,共用体是一个非常有效的选择。常见的用例包括:
- 网络数据包处理,其中数据包可能包含不同类型的数据但共用一个存储空间。
- 处理不同类型的硬件寄存器数据时,通过共用体可以方便地访问同一块内存中的不同数据类型
union Data {
int i;
float f;
char str[20];
};
共用体的内存分配
共用体的大小取决于其最大成员的大小。例如,如果 int
是4字节,float
也是4字节,而 char str[20]
是20字节,那么 union Data
的大小就是20字节。
data.i = 10; // 存储int值
data.f = 220.5; // 存储float值,会覆盖int值
strcpy(data.str, "C Programming"); // 存储字符串,覆盖之前的int和float值
需要注意的是,只有最后一次存储的数据是有效的,因为每次存储都会覆盖之前的数据。
利用共用体判断大小端。
union m{
int h;
char c;
};
main(void){
union m ch;
ch.c=1;
printf("%d\n",ch.c);
}
m是4字节的空间、给char c 赋值。如果是大端存储(低位数据在高位地址),1会被赋值到高位,低位0;小端(低位数据在低位地址)的话,1会被赋值到低位、高位为0;看c的值是0还是1,是1则小端、0则大端。