做投资的网站,装修平台加盟,芜湖网站备案咨询电话,汉中建设工程招投标网目录 #x1f680;前言✍️结构体类型的声明#x1f4af;结构体定义#x1f4af;结构的特殊声明 #x1f99c;结构的自引用#x1f4bb;结构体内存对齐#x1f4af;对齐规则#x1f4af;为什么存在内存对齐#x1f4af;修改默认对齐数 #x1f40d;结构体传参#x1… 目录 前言✍️结构体类型的声明结构体定义结构的特殊声明 结构的自引用结构体内存对齐对齐规则为什么存在内存对齐修改默认对齐数 结构体传参结构体实现位段什么是位段位段的内存分配位段的跨平台问题位段的应用位段使用的注意事项 总结 前言 大家好我是 EnigmaCoder。本文收录于我的专栏 C感谢您的支持 在C语言编程体系里结构体是整合不同类型数据的重要工具它能够将多个相关数据组合为一个有机整体显著提升数据处理的效率与便捷性。无论是小型代码项目还是大型复杂系统开发结构体都占据着关键地位。深入掌握结构体知识不仅有助于提升编程技能还能优化代码质量使其更高效、易维护。接下来让我们全面且深入地探讨C语言结构体的各个方面从基础声明到内存对齐、传参方式再到特殊的位段实现。 ✍️结构体类型的声明
结构体定义
结构体是不同类型数据的集合体这些组成数据被称为成员变量每个成员的类型可以各不相同。定义结构体时需要明确结构体标签tag和成员列表。例如定义一个描述学生信息的结构体
// 定义名为Stu的结构体用于存储学生相关信息
struct Stu {char name[20]; // 用于存储学生姓名最多可容纳20个字符int age; // 存储学生年龄char sex[5]; // 存储学生性别最多5个字符char id[20]; // 存储学生学号最多20个字符
};在这个结构体中struct是定义结构体的关键字Stu作为结构体标签方便后续引用name、age、sex、id是不同类型的成员变量分别描述学生的不同属性。
结构的特殊声明
匿名结构体在声明时不设置结构体标签这种结构体若不重命名通常仅能使用一次。因为编译器会将不同的匿名结构体声明视作不同类型例如
// 定义一个匿名结构体并创建变量x
struct {int a; // 成员a类型为intchar b; // 成员b类型为charfloat c; // 成员c类型为float
}x;// 定义另一个匿名结构体创建数组a和指针p
struct {int a; // 成员a类型为intchar b; // 成员b类型为charfloat c; // 成员c类型为float
}a[20], *p;
// p x; 该行代码非法编译器将两个匿名结构体视为不同类型上述代码中虽然两个匿名结构体成员相同但由于缺少标签编译器将它们识别为不同类型导致p x;赋值操作不被允许。
结构的自引用
在结构体内部直接包含同类型结构体变量会导致结构体大小无限递归这种做法不合理。正确的自引用方式是使用指针。以链表节点结构体定义为例
// 定义链表节点结构体Node
struct Node {int data; // 存储节点数据struct Node* next; // 指向下一个节点的指针
};在Node结构体中next成员是指向struct Node类型的指针通过它可构建链表结构。若使用typedef对匿名结构体重命名时要避免在结构体内部提前使用重命名后的类型如下代码是错误的
// 错误示例在匿名结构体内部提前使用未定义的Node类型
typedef struct {int data; // 成员data类型为intNode* next; // 此处使用Node类型错误因为Node还未定义
}Node;正确的做法是
// 正确定义结构体并使用typedef重命名
typedef struct Node {int data; // 成员data类型为intstruct Node* next; // 指向下一个节点的指针
}Node;先定义带标签的结构体再使用typedef重命名可避免上述错误。
结构体内存对齐
对齐规则
结构体的第一个成员在内存中的起始地址与结构体变量的起始地址重合偏移量为0。后续成员变量需对齐到特定数字对齐数的整数倍地址处。对齐数是编译器默认对齐数和该成员变量大小两者中的较小值。在VS编译器中默认对齐数为8而Linux的gcc编译器没有默认对齐数对齐数就是成员自身大小。结构体的总大小必须是所有成员对齐数中的最大值的整数倍。当结构体中嵌套其他结构体时嵌套的结构体成员要对齐到其自身成员最大对齐数的整数倍位置整个结构体的大小则是所有最大对齐数包含嵌套结构体中成员的对齐数的整数倍。
通过以下练习加深理解
// 练习1计算结构体S1的大小
struct S1 {char c1; // 第一个成员占1字节int i; // 第二个成员在VS中对齐数为4默认8与4的较小值需对齐到4的倍数地址char c2; // 第三个成员对齐数为1占1字节
};
// 在VS中S1的大小为8字节1 3填充 4 1
printf(%d\n, sizeof(struct S1)); // 练习2计算结构体S2的大小
struct S2 {char c1; // 第一个成员占1字节char c2; // 第二个成员对齐数为1占1字节int i; // 第三个成员对齐数为4需对齐到4的倍数地址
};
// 在VS中S2的大小为8字节1 1 2填充 4
printf(%d\n, sizeof(struct S2)); // 练习3计算结构体S3的大小
struct S3 {double d; // 第一个成员占8字节对齐数为8char c; // 第二个成员对齐数为1占1字节int i; // 第三个成员对齐数为4需对齐到4的倍数地址
};
// 在VS中S3的大小为16字节8 1 3填充 4
printf(%d\n, sizeof(struct S3)); // 练习4结构体嵌套问题计算结构体S4的大小
struct S4 {char c1; // 第一个成员占1字节struct S3 s3; // 嵌套结构体成员S3中最大对齐数为8s3需对齐到8的倍数地址double d; // 第三个成员对齐数为8
};
// 在VS中S4的大小为32字节1 7填充 16 8
printf(%d\n, sizeof(struct S4)); 在这些练习中根据对齐规则分析每个结构体成员的存储位置和填充字节情况从而准确计算出结构体的大小。
为什么存在内存对齐
内存对齐主要基于平台和性能两方面考虑
平台原因并非所有硬件平台都能访问任意内存地址上的任意数据。部分硬件平台对数据的访问地址有限制若访问未对齐的数据可能引发硬件异常。例如某些硬件要求特定类型数据必须存储在特定地址边界上否则无法正常读取或写入数据。性能原因数据结构尤其是栈在自然边界上对齐能提升访问效率。访问未对齐内存时处理器可能需要进行多次内存访问操作而对齐的内存访问仅需一次。例如若处理器每次从内存读取8个字节数据数据地址必须是8的倍数才能一次完成读写操作。若数据未对齐可能需分两次访问不同的8字节内存块降低了系统性能。
结构体内存对齐本质上是用空间换取时间的策略。在设计结构体时将占用空间小的成员集中放置有助于节省内存空间。例如
// 对比S1和S2结构体成员相同但顺序不同
struct S1 {char c1; // 占1字节int i; // 对齐数为4需对齐到4的倍数地址char c2; // 占1字节
};struct S2 {char c1; // 占1字节char c2; // 占1字节int i; // 对齐数为4需对齐到4的倍数地址
};
// S1在VS中大小为8字节S2在VS中大小为8字节但S2布局更节省空间在这个例子中S1和S2结构体成员相同但S2将两个char类型成员放在一起使int成员对齐时无需额外填充字节从而在一定程度上节省了内存。
修改默认对齐数
使用#pragma pack()预处理指令可改变编译器的默认对齐数。例如#pragma pack(1)将默认对齐数设为1之后使用#pragma pack()可取消设置恢复默认对齐数。示例如下
#include stdio.h
// 将默认对齐数设置为1
#pragma pack(1)
struct S {char c1; // 占1字节int i; // 占4字节char c2; // 占1字节
};
// 取消设置的对齐数还原为默认
#pragma pack() int main() {// 输出结果为6因为设置对齐数为1后不再有填充字节printf(%d\n, sizeof(struct S)); return 0;
}在上述代码中通过#pragma pack(1)设置对齐数为1结构体成员紧密排列无填充字节所以struct S的大小为1 4 1 6字节。取消设置后后续结构体定义将恢复默认对齐规则。
结构体传参
传递结构体对象时如果结构体规模较大参数压栈会带来较大的系统开销进而降低性能。因此结构体传参时优先选择传递结构体地址。例如
// 定义结构体S
struct S {int data[1000]; // 包含1000个int类型元素的数组int num; // 一个int类型的成员
};// 定义函数print1参数为结构体S的对象
void print1(struct S s) {// 输出结构体成员num的值printf(%d\n, s.num);
}// 定义函数print2参数为结构体S的指针
void print2(struct S* ps) {// 通过指针访问结构体成员num并输出其值printf(%d\n, ps-num);
}int main() {// 初始化结构体S的对象sstruct S s {{1,2,3,4}, 1000}; // 调用print1函数传递结构体对象print1(s); // 调用print2函数传递结构体地址print2(s); return 0;
}在这段代码中print1函数传递结构体对象函数调用时会将整个结构体内容复制到函数栈帧对于大型结构体复制操作耗时耗空间。而print2函数传递结构体地址仅需将一个指针值压栈系统开销小性能更优。
结构体实现位段
什么是位段
位段的声明与结构体类似但有两个显著区别一是位段成员类型通常为int、unsigned int、signed intC99标准支持更多类型二是成员名后会紧跟一个冒号和一个数字用于指定该成员占用的二进制位数。例如
// 定义一个名为A的位段类型
struct A {int _a:2; // 成员_a占用2位int _b:5; // 成员_b占用5位int _c:10; // 成员_c占用10位int _d:30; // 成员_d占用30位
};在struct A中_a、_b、_c、_d是位段成员冒号后的数字表示它们各自占用的位数通过这种方式可在有限的内存空间内紧凑存储多个小数据。
位段的内存分配
位段成员类型多样内存空间按4字节int类型或1字节char类型的方式开辟。不过位段存在诸多不确定因素不具备良好的跨平台性。示例如下
// 定义一个位段结构体S
struct S {char a:3; // 成员a占用3位char b:4; // 成员b占用4位char c:5; // 成员c占用5位char d:4; // 成员d占用4位
};
// 初始化位段结构体S的对象s
struct S s {0};
// 给位段成员赋值
s.a 10;
s.b 12;
s.c 3;
s.d 4; 在上述代码中struct S是位段结构体s是其对象。初始化时所有位段成员为0后续分别赋值。由于位段按位存储赋值时需注意数值范围不能超出位段允许的最大值。
位段的跨平台问题
位段在跨平台使用时存在诸多问题主要体现在以下方面
int位段在不同平台上可能被解释为有符号数或无符号数缺乏一致性导致程序行为不可预测。不同平台支持的位段最大位数不同16位机器和32位机器的最大位数限制不同若代码中指定的位数超出目标平台限制会引发错误。位段成员在内存中的分配方向从左向右或从右向左没有统一标准不同平台实现方式不同增加了程序的不确定性。当结构体包含多个位段且后一个位段成员无法完全容纳在前一个位段剩余空间时是舍弃剩余位还是利用不同平台处理方式不同。
鉴于这些跨平台问题注重可移植性的程序应谨慎使用位段。
位段的应用
在网络协议的IP数据报格式中许多属性仅需几个二进制位就能描述此时使用位段既能实现功能需求又能节省内存空间减少网络传输的数据量提高网络传输效率。例如
// 模拟IP数据报部分位段
struct IPHeader {unsigned int version:4; // 4位版本号unsigned int tos:8; // 8位服务类型unsigned int total_length:16; // 16位总长度// 其他位段成员可继续添加
};在IPHeader结构体中利用位段定义IP数据报的部分属性version用4位表示版本号tos用8位表示服务类型total_length用16位表示总长度有效节省内存方便网络数据处理。
位段使用的注意事项
位段的部分成员起始位置可能并非字节的起始位置这部分位置没有内存地址。由于内存按字节分配地址字节内部的二进制位没有独立地址因此不能对位段成员使用取地址操作符也就无法使用scanf直接给位段成员输入值。正确做法是先将输入值存储到普通变量中再赋值给位段成员。例如
// 定义位段结构体A
struct A {int _a:2; // 成员_a占用2位int _b:5; // 成员_b占用5位int _c:10; // 成员_c占用10位int _d:30; // 成员_d占用30位
};int main() {// 初始化位段结构体A的对象sastruct A sa {0}; // scanf(%d, sa._b); 该行代码错误不能对位段成员使用操作符// 正确的示范int b 0; // 先将输入值存储到变量bscanf(%d, b); // 再将b的值赋给位段成员_bsa._b b; return 0;
}总结 C语言结构体涵盖了丰富的知识从基础的类型声明、变量初始化到内存对齐优化、传参方式选择再到特殊的位段应用每个环节都有其独特要点和应用场景。在实际编程中应根据具体需求合理运用这些特性。比如在处理大量数据时精心设计结构体布局利用内存对齐提高性能在频繁调用函数传递结构体时选择传地址方式减少开销。同时要充分考虑位段的跨平台问题在对可移植性要求高的项目中谨慎使用。通过深入理解和灵活运用结构体知识能编写出更高效、可靠的代码在C语言编程道路上不断进阶。