15_结构体介绍
微软的结构体变量前缀是tag
struct tagStudent
{
char szName[5];
int nAge;
float fHeight;
double dblWeight;
unsigned short int wID;
char nGender;
};
结构体内存对齐
结构体对齐是C/C++编程中一个非常重要的概念,涉及到如何将结构体的成员变量在内存中进行排列,从而确保高效的内存访问和避免硬件访问错误。结构体对齐不仅仅影响结构体的大小,还影响访问速度。默认是8字节,但是1、2、4、8、16字节都可以。
1. 结构体成员的偏移量必须满足特定条件,以确保正确的内存对齐和访问效率
每个成员的偏移量(即该成员相对于结构体起始地址的位置)必须是它自身对齐要求的整数倍。例如,整型成员通常要求4字节对齐(32位系统和大多数64位系统),这意味着它在内存中的地址必须是4的倍数。浮点型或双精度浮点型可能要求8字节对齐。
如果结构体成员不满足这个对齐要求,编译器通常会在需要的地方插入填充字节(padding),以确保每个成员都能以它所需的对齐方式存储和访问。
例子:
struct example {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};
在这个例子中,编译器可能会在char a
和int b
之间插入3个字节的填充字节,以确保int b
从一个4字节对齐的地址开始。
内存布局可能如下:
|偏移|内容| |-|-| |0|a (1字节)| |1|填充 (3字节)| |4|b (4字节)| |8|c (1字节)| |9|填充 (3字节)|
结构体的大小为12字节,尽管其中只有6字节是有效数据,其余的6字节是填充字节。
2. 通过调整结构体成员的顺序和编译选项中的对齐值,可以优化结构体的大小和访问速度
通过调整结构体成员的顺序,可以减少填充字节的数量,从而减小结构体的整体大小。通常,较大的数据类型(如double
或int
)应该放在结构体的前面,而较小的类型(如char
)可以放在后面,这样可以减少填充字节。
优化例子:
struct optimized {
int b; // 4字节
char a; // 1字节
char c; // 1字节
};
优化后的内存布局:
|偏移|内容| |-|-| |0|b (4字节)| |4|a (1字节)| |5|c (1字节)| |6|填充 (2字节)|
通过调整顺序,这个结构体的大小变为8字节,而之前未优化的结构体大小是12字节。
编译器对齐选项:
可以通过调整编译器选项来更改默认的对齐方式。例如,#pragma pack(n)
指令允许你控制结构体成员之间的对齐方式,将对齐值设置为n字节。使用较小的对齐值可以减小结构体大小,但可能会导致访问速度下降,因为硬件通常对对齐的内存访问更为高效。
3. 在某些情况下,可能需要使用特殊的编译指令或宏来精确控制结构体的内存布局
为了精确控制结构体的对齐方式,C语言提供了诸如#pragma pack
、__attribute__((packed))
等编译指令,允许你强制编译器按照特定的对齐要求排列结构体成员。
#pragma pack
:
#pragma pack(1) // 强制1字节对齐
struct packed_example {
char a; // 1字节
int b; // 4字节
};
#pragma pack() // 恢复默认对齐
在上述例子中,由于#pragma pack(1)
的作用,结构体成员将被紧密排列,没有填充字节。a
将占用第1字节,b
紧接着占用第2-5字节,结构体的总大小为5字节。
图片里是最漂亮的修改对齐值的
__attribute__((packed))
:
这种语法通常用于GCC和Clang编译器,具有类似的作用:
struct packed_example {
char a;
int b;
} __attribute__((packed));
使用packed
属性后,结构体中的成员将没有填充字节,成员紧密排列。
实践中的注意事项:
优化结构体大小:通过调整成员顺序,减少填充字节,能显著减少结构体大小。这在需要存储大量结构体的场景中尤为重要,如嵌入式系统或大数据处理。
访问速度:虽然紧凑排列结构体成员可以节省内存空间,但可能会导致内存访问的速度下降,因为现代处理器通常更快地处理对齐的数据。如果访问速度比内存占用更为重要,可能需要在结构体中保留适当的填充字节。
跨平台考虑:不同的架构和平台对内存对齐有不同的要求,例如某些处理器可能强制要求4字节或8字节对齐,因此为了跨平台兼容,调整结构体的对齐时需格外小心。
实例代码展示:
#include <stdio.h>
#include <stddef.h> // for offsetof
#pragma pack(1)
struct packed_example {
char a;
int b;
char c;
};
int main() {
struct packed_example s;
printf("Size of packed struct: %zu\n", sizeof(s)); // 输出结构体大小
printf("Offset of a: %zu\n", offsetof(struct packed_example, a));
printf("Offset of b: %zu\n", offsetof(struct packed_example, b));
printf("Offset of c: %zu\n", offsetof(struct packed_example, c));
return 0;
}
#pragma pack()
上面的代码展示了如何通过#pragma pack(1)
来紧凑排列结构体成员,并使用offsetof
宏查看每个成员的偏移量。编译时需要注意,你可以通过取消#pragma pack
或者设置不同的对齐值来观察不同的结构体大小。
实战要求
/*
设编译对齐值为 Zp
设结构体成员的地址和结构体首地址之差为 offset
设结构体成员类型为 member type
必须满足:
offset % min(Zp, sizeof(member type)) == 0
定义结构体自身的对齐值为 StructAlign
StructAlign = max(sizeof(member1 type), sizeof(member2 type), ...., sizeof(memberEnd type))
设整个结构体的空间长度为 size
必须满足:
size % min(Zp, StructAlign) == 0
*/
1. 设编译器对齐值为 Zp
Zp
是编译器设定的对齐方式,通常是一个常量,表示内存的对齐单位。例如,编译器可能会规定对齐单位为 1 字节、2 字节、4 字节或 8 字节,具体取决于目标平台和编译器选项。对齐值会影响结构体成员的地址如何排列,目的是提升内存访问效率。
2. 设结构体成员的地址和结构体首地址之差为 offset
offset
是结构体成员相对于结构体首地址的偏移量。在内存中,结构体的每个成员都有自己的起始地址,而这个地址与结构体的起始地址之间的差值就是 offset
。
规则:
offset % min(Zp, sizeof(member type)) == 0
这条规则的意思是,结构体中每个成员的偏移量必须是 Zp
和该成员类型大小中较小者的整数倍。这样做的原因是为了满足平台的对齐要求以及提升内存读取的效率。
3. 定义结构体自身的对齐值为 StructAlign
StructAlign = max(sizeof(member1 type), sizeof(member2 type), ..., sizeof(memberEnd type))
StructAlign
是整个结构体的对齐方式,取决于结构体中最大成员类型的大小。举例来说,如果一个结构体有成员类型分别为 int
(4字节)和 double
(8字节),那么整个结构体的对齐值 StructAlign
就会是 8 字节。
4. 设整个结构体的空间长度为 size
size
是结构体在内存中占用的总空间长度。为了确保整个结构体的大小与系统内存对齐需求一致,需要满足以下条件:
size % min(Zp, StructAlign) == 0
这意味着,结构体的总大小 size
必须是 Zp
和 StructAlign
中较小者的整数倍。这样可以确保结构体的大小不会破坏内存对齐要求,进而保证在内存中高效访问。
成员对齐:每个成员的偏移量必须满足特定的对齐规则,确保成员按特定字节对齐。
结构体整体对齐:结构体的总大小也需要满足最小对齐要求。
通过这些对齐规则,编译器可以确保结构体在内存中排列合理,避免跨字节读取,进而提升程序执行效率。
网络通信的结构体
在网络通讯中,使用结构体传递数据时的确需要特别考虑一些问题,确保数据在不同平台和架构之间传输时不会出现错误。以下是进一步的细节说明:
1. 对齐值统一
结构体的对齐值影响内存布局,不同编译器和平台可能采用不同的对齐策略(如4字节对齐或8字节对齐)。如果在网络通讯中不加以控制,发送端和接收端的内存布局不同,可能会导致数据解析错误。因此:
解决方法:通过编译器指令或pragma设置结构体的对齐值,使其在所有平台上保持一致。例如,在GCC中使用
__attribute__((packed))
来确保无填充字节。
2. 跨平台一致性(设置对齐值为1)
在网络通讯中,结构体通常被用于定义数据包的格式,而不同平台的对齐规则可能导致字节填充的差异。因此,为了确保数据包在不同平台上能正确解析,通常将结构体的对齐值设置为1,避免因填充字节导致的数据不一致:
解决方法:通过手动设置结构体的对齐方式为1字节(即禁用字节填充)。在GCC和Clang中可以使用
__attribute__((packed))
,在MSVC中则可以使用#pragma pack(1)
。
3. 字节序和字节对齐问题
网络通讯中,数据的字节序(大端/小端)需要统一,避免数据传输后在不同平台上因字节序不同而导致数据错位。此外,字节对齐问题在不同系统间可能不同:
解决方法:
对于字节序问题,使用标准库函数进行字节序转换,例如
htons
(host to network short),htonl
(host to network long)等,以确保数据在网络传输中始终使用大端序(network byte order)。对于字节对齐问题,结合使用
__attribute__((packed))
或#pragma pack(1)
以及手动序列化/反序列化结构体的方式传递数据,避免对齐问题带来的影响。
统一对齐值:确保发送端和接收端的数据布局一致,避免因对齐差异导致数据读取错误。
避免填充字节:设置对齐值为1,即为不对齐,避免跨平台时不同的内存填充方式带来的问题。
字节序转换:使用网络字节序(大端序)进行数据传输,并在必要时进行字节序转换,确保数据在不同平台解析正确。
结构体寻址方式
struct tagType obj;
// 计算 obj.member 的地址:
int member_address = (int)&obj + member_offset;
// 访问或修改 obj.member 的值:
*(member_type *)(member_address) = obj.member;
struct tagType obj;
// 计算 obj.member 的地址:
int memberaddress = (int)&obj + memberoffset;
// 访问或修改 obj.member 的值:
(membertype )(memberaddress) = obj.member;
1. 结构体与成员偏移量
在 C 语言中,结构体是多个数据成员的组合,而这些成员在内存中是按一定的顺序存储的。每个成员相对于结构体起始地址(即 &obj
)都有一个固定的偏移量,称为成员偏移量。因此,通过结构体的起始地址加上成员的偏移量,我们可以获得某个成员在内存中的实际地址。
图片中的说明如下:
struct tagType obj;
obj
是一个tagType
结构体的实例。通过
&obj
可以得到这个结构体变量在内存中的地址。
2. 成员地址计算
(int)&obj + member offset
&obj
表示结构体obj
的起始地址。member offset
是结构体成员相对于obj
起始地址的偏移量,这个偏移量可以通过编译器提供的offsetof
宏来获取。(int)
的作用是将地址转换为整数,以便进行偏移量的加法运算(注意在 64 位系统上,这里应使用long
类型来存储完整的地址)。&obj + member offset
就可以得到该成员的具体地址。
3. 成员访问
obj.member = *(member type *)(obj.member address)
通过计算出的成员地址
(obj.member address)
,使用类型转换将其转换为对应成员类型的指针member type *
。使用
*
操作符来解引用该地址,以访问或修改该成员的值。
通过
&obj
获取结构体起始地址,并加上成员的偏移量,即可得到成员的实际地址。通过解引用该地址,可以访问或修改结构体成员的值。
这种寻址方式通常应用于低级内存操作,例如在嵌入式开发或需要直接操作内存的场合。
实例
#include <stdio.h>
#include <stddef.h> // 用于 offsetof 宏
// 定义一个简单的结构体
struct Person {
int age;
float height;
char name[20];
};
int main() {
struct Person p1 = {25, 175.5, "Alice"};
// 获取结构体 p1 的起始地址
printf("Address of p1: %p\n", (void*)&p1);
// 获取成员 age 的地址
int age_offset = offsetof(struct Person, age); // 计算 age 的偏移量
printf("Offset of age: %d\n", age_offset);
int* age_ptr = (int*)((char*)&p1 + age_offset); // 通过偏移量和基地址计算出 age 的地址
printf("Address of age: %p\n", (void*)age_ptr);
printf("Value of age: %d\n", *age_ptr);
// 获取成员 height 的地址
int height_offset = offsetof(struct Person, height); // 计算 height 的偏移量
printf("Offset of height: %d\n", height_offset);
float* height_ptr = (float*)((char*)&p1 + height_offset); // 通过偏移量和基地址计算出 height 的地址
printf("Address of height: %p\n", (void*)height_ptr);
printf("Value of height: %.2f\n", *height_ptr);
// 获取成员 name 的地址
int name_offset = offsetof(struct Person, name); // 计算 name 的偏移量
printf("Offset of name: %d\n", name_offset);
char* name_ptr = (char*)((char*)&p1 + name_offset); // 通过偏移量和基地址计算出 name 的地址
printf("Address of name: %p\n", (void*)name_ptr);
printf("Value of name: %s\n", name_ptr);
return 0;
}
代码说明:
结构体定义:
struct Person {
int age;
float height;
char name[20];
};
定义了一个 Person
结构体,包含三个成员:age
(整数)、height
(浮点数)、name
(字符数组)。
获取成员偏移量:
使用
offsetof
宏可以获取结构体成员相对于结构体起始地址的偏移量。比如offsetof(struct Person, age)
获取的是age
在结构体中的偏移量。
成员地址计算:
(char*)&p1 + age_offset
:通过将结构体的起始地址转为char*
,再加上成员的偏移量来计算成员的实际地址。(int*)((char*)&p1 + age_offset)
:将成员的地址转换为对应成员类型的指针类型。
解引用访问成员值:
通过解引用计算出的指针,可以读取成员的值,例如
*age_ptr
。
输出示例:
Address of p1: 0x7ffeee64a410
Offset of age: 0
Address of age: 0x7ffeee64a410
Value of age: 25
Offset of height: 4
Address of height: 0x7ffeee64a414
Value of height: 175.50
Offset of name: 8
Address of name: 0x7ffeee64a418
Value of name: Alice