C 语言结构体:从入门到进阶的全面解析

news/2025/2/26 6:50:45/

一、结构体类型的声明

1.1 结构的声明

结构体是一种自定义的数据类型,允许将不同类型的数据组合成一个整体。声明语法如下:

struct 结构体名 {数据类型 成员1;数据类型 成员2;// ...
};

示例:

struct Student {char name[20];int age;float score;
};

1.2 结构体变量的创建和初始化

  • 创建方式
struct Student stu1;  // 先声明类型,后定义变量
struct { int x; int y; } point;  // 匿名结构体

初始化方式

struct Student stu2 = {"张三", 18, 90.5};
struct Student stu3 = { .age = 20, .name = "李四",.score = 85.0};  // C99指定初始化器

1.3 结构的特殊声明

匿名结构体可以直接定义变量,但无法重复使用:

struct {int a;char b;
} anon_var;

1.4 结构的自引用

用于构建链表等复杂结构:

struct Node {int data;struct Node* next;  // 正确方式
};

错误示例

struct Node {int data;Node* next;  // 错误:未定义类型Node
};

二、结构体内存对齐

2.1 对齐规则

⾸先得掌握结构体的这四个对⻬的规则:
规则 1:结构体的第一个成员对齐到和结构体变量起始位置偏移量为 0 的地址处

可以把结构体想象成一个大箱子,这个箱子从地址 0 开始摆放。结构体的第一个成员就像是第一个要放进箱子的物品,它会直接放在箱子的最开始位置,也就是地址 0 处,不需要考虑其他对齐因素。

struct Example1 {char c;  // 第一个成员,直接放在起始地址 0 处int i;
};
规则 2:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值
 
  • 对齐数的计算:对于结构体中除第一个成员之外的其他成员,需要先计算它们的对齐数。对齐数是由编译器默认的对齐数和成员自身大小这两个值中较小的那个决定的。不同的编译器默认对齐数可能不同,例如在 Visual Studio(VS)中默认值为 8,而在 Linux 的 gcc 编译器中没有默认对齐数,此时对齐数就是成员自身的大小。
  • 成员放置位置:计算出对齐数后,该成员就要放在这个对齐数的整数倍的地址处。如果当前地址不是对齐数的整数倍,就需要在前面填充一些字节,直到达到对齐数的整数倍。

示例

#include <stdio.h>struct Example2 {char c;  // 第一个成员,放在地址 0 处int i;   // 成员大小为 4 字节,VS 中默认对齐数为 8,对齐数取较小值 4// 由于 char 占 1 字节,当前地址 1 不是 4 的整数倍,需要填充 3 字节// 所以 i 从地址 4 开始存放
};int main() {printf("Size of Example2: %zu\n", sizeof(struct Example2));return 0;
}

在这个例子中,char 类型的 c 放在地址 0 处,int 类型的 i 对齐数为 4,因为当前地址 1 不是 4 的整数倍,所以要在 c 后面填充 3 个字节,i 从地址 4 开始存放。

规则 3:结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍

结构体的所有成员都按照前面的规则放置好后,结构体整体占用的内存大小并不是成员实际占用字节数的简单相加,而是要保证结构体的总大小是所有成员对齐数中最大那个对齐数的整数倍。如果实际占用的内存大小不是最大对齐数的整数倍,就需要在结构体的末尾填充一些字节,使其达到最大对齐数的整数倍。

示例

#include <stdio.h>struct Example3 {char c;  // 对齐数为 1,放在地址 0 处short s; // 对齐数为 2,由于 c 占 1 字节,当前地址 1 不是 2 的整数倍,填充 1 字节,s 从地址 2 开始存放int i;   // 对齐数为 4,s 占 2 字节,当前地址 4 是 4 的整数倍,i 从地址 4 开始存放
};int main() {printf("Size of Example3: %zu\n", sizeof(struct Example3));return 0;
}

在这个结构体中,char 的对齐数是 1,short 的对齐数是 2,int 的对齐数是 4,最大对齐数是 4。成员实际占用的字节数是 1(c) + 1(填充)+ 2(s) + 4(i) = 8 字节,8 是 4 的整数倍,所以结构体的总大小就是 8 字节。

规则 4:如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍

当结构体中嵌套了另一个结构体时,嵌套的结构体就像一个 “小箱子”,这个 “小箱子” 要放在合适的位置。它的起始地址要对齐到它自己内部成员中最大对齐数的整数倍处。计算整个结构体的总大小的时候,要把嵌套结构体内部成员的对齐数也考虑进来,最终结构体的整体大小要保证是所有最大对齐数(包括嵌套结构体成员的对齐数)的整数倍。

示例

#include <stdio.h>struct Inner {char c;  // 对齐数为 1int i;   // 对齐数为 4,最大对齐数是 4
};struct Example4 {char c1;         // 对齐数为 1,放在地址 0 处struct Inner in; // 嵌套结构体,最大对齐数是 4,当前地址 1 不是 4 的整数倍,填充 3 字节,in 从地址 4 开始存放short s;         // 对齐数为 2,in 占 8 字节(1 + 3 填充 + 4),当前地址 12 是 2 的整数倍,s 从地址 12 开始存放
};int main() {printf("Size of Example4: %zu\n", sizeof(struct Example4));return 0;
}

在这个例子中,嵌套结构体 Inner 的最大对齐数是 4,所以 Inner 要对齐到 4 的整数倍地址处。整个结构体 Example4 的所有成员最大对齐数也是 4,最终结构体的总大小要保证是 4 的整数倍。经过计算和填充,结构体 Example4 的总大小是 14 字节(1 + 3 填充 + 8 + 2),刚好是 4 的整数倍,所以最终大小就是 14 字节。

2.2 为什么存在内存对齐?

1. 硬件效率:按块读取内存

现代 CPU 读取内存是一块一块来的,就像从书架上拿书,一次拿一摞(比如 4 本或 8 本)。要是数据没对齐,就像书没摆好,CPU 得多次伸手去拿,才能凑齐想要的数据,效率低。而数据对齐后,CPU 一次就能拿到完整的数据,速度快多了。

2. 兼容性:不同平台要求不同

不同的硬件平台,就像不同的书架,对书的摆放要求不一样。有些书架要求书必须按顺序一本本对齐放,要是放乱了,就拿不出来或者拿错。所以程序在不同硬件上跑,数据对齐得符合人家的要求,不然就可能出错。

3. 性能优化:减少访问次数

内存访问就像去书架找书,次数多了很麻烦。合理对齐数据,能让 CPU 一次拿到更多有用的数据,就像一次能拿一摞需要的书,不用来回跑好几趟,程序自然就跑得快啦。

2.3 修改默认对齐数

使用#pragma pack()指令:

#pragma pack(2)  // 设置对齐数为2
struct Test {char a;  // 1字节,起始地址0int b;   // 4字节 → 按2对齐,起始地址2
};           // 总大小:6字节(2+4=6)
#pragma pack() // 恢复默认对齐

三、结构体传参

  • 值传递
void print_stu(struct Student s) { ... }  // 效率低,复制整个结构体
  • 指针(地址)传递

void print_stu(const struct Student* s) { ... }  // 推荐方式

注意:指针传递需确保指针有效,避免野指针问题


四、结构体实现位段

4.1 什么是位段

在 C 语言里,有些数据所需存储空间极小,像表示开关状态(开或关),1 位二进制数(0 或 1)就足够;表示 8 种不同等级,用 3 位二进制数(能表示 0 - 7)就行。但基本数据类型如 char 占 1 字节(8 位),int 一般占 4 字节(32 位),用它们存储这类数据会浪费内存。

位段可解决此问题,它允许在结构体中精准指定每个成员使用的二进制位数,从而高效利用内存。示例如下:

#include <stdio.h>struct MyFlags {unsigned int is_open : 1;  // 1 位表示开关状态unsigned int level : 3;    // 3 位表示 8 种等级unsigned int mode : 2;     // 2 位表示 4 种模式
};int main() {struct MyFlags flags;flags.is_open = 1;flags.level = 5;flags.mode = 2;printf("is_open: %u\n", flags.is_open);printf("level: %u\n", flags.level);printf("mode: %u\n", flags.mode);return 0;
}

4.2 位段的内存分配

按类型分配

位段通常按 intunsigned int 或 signed int 类型分配内存,常见系统中 int 占 4 字节(32 位)。

依次存放规则

位段成员在内存中依次存放。若前面位段成员占用位数与当前位段成员要占用的位数之和,未超过当前分配的内存块(通常 32 位),则当前位段成员接着分配;若超过,则从下一个内存块开始分配。

未命名位段用途

未命名位段可作 “占位符”,调整后续位段成员的起始位置,灵活控制内存使用。示例:

#include <stdio.h>struct BitFieldExample {unsigned int part1 : 5; unsigned int part2 : 3; unsigned int : 2;       unsigned int part3 : 4; 
};int main() {printf("Size of BitFieldExample: %zu bytes\n", sizeof(struct BitFieldExample));return 0;
}

该结构体中,各成员位段总和未超 32 位,所以通常占 4 字节。

4.3 位段的跨平台问题

存储顺序差异

不同编译器处理位段时,位段在内存中的存储顺序可能不同,有的从低位开始分配,有的从高位开始,这会使相同代码在不同编译器下,位段成员存储位置有差异。

长度限制不同

不同平台和编译器对位段成员长度限制有别,部分编译器不允许位段成员位数超特定值。

负数处理不同

有符号位段在不同编译器处理负数的方式可能不同,导致代码在不同平台运行结果有差异。

4.4 位段的应用

网络协议解析

网络协议头部包含众多标志位和状态信息,用很少位数就能表示,位段可方便解析处理这些头部信息。例如 IPv4 协议头部的 4 位版本号和 4 位首部长度:

#include <stdio.h>struct IPv4Header {unsigned int version : 4;     unsigned int header_length : 4; 
};int main() {unsigned char header_data = 0x45; struct IPv4Header *ip_header = (struct IPv4Header *)&header_data;printf("Version: %u\n", ip_header->version);printf("Header Length: %u\n", ip_header->header_length);return 0;
}

设备寄存器控制

嵌入式系统开发中,常与硬件设备寄存器交互,寄存器很多位有特定含义,用于控制设备功能,位段可方便操作寄存器各位。

节省内存

在嵌入式系统或资源受限设备中,位段能精确控制数据占用位数,节省大量内存,提升程序运行效率。

4.5 位段使用的注意事项

类型要求

位段类型必须是 intunsigned int 或 signed int,其他类型(如 charfloat)不可用。

不能取地址

不能使用 & 运算符获取位段成员地址,因为位段成员可能只占字节部分位,无独立内存地址。

跨平台兼容性

不同编译器和平台处理位段有差异,编写代码时要留意跨平台兼容性,充分测试。

避免位数溢出

给位段成员赋值时,不能超出其表示范围,否则会溢出,导致不可预期结果。例如:

#include <stdio.h>struct WrongUsage {unsigned int small_num : 2;
};int main() {struct WrongUsage wu;wu.small_num = 5;  // 2 位位段只能表示 0 - 3,赋值 5 会溢出printf("small_num: %u\n", wu.small_num);return 0;
}


五、扩展知识

5.1 柔性数组成员

用于动态数组:

struct Array {int len;int data[];  // 柔性数组成员
};
// 动态分配:
struct Array* arr = malloc(sizeof(struct Array) + 10*sizeof(int));

5.2 结构体与枚举的结合

typedef enum { MALE, FEMALE } Gender;struct Person {char name[20];Gender sex;
};

5.3 结构体常用操作

  • 结构体比较:逐个成员比较
  • 结构体复制:使用memcpy()或直接赋值(C99 支持)
  • 结构体打印:自定义格式化输出函数


六、常见问题解答

  1. 结构体可以包含自身类型吗?

    • 不能直接包含,但可以包含指针(自引用)
  2. 内存对齐会浪费空间吗?

    • 是的,但这是空间与时间的权衡,现代编译器会优化
  3. 位段能跨字节边界吗?

    • 取决于编译器,可能导致不可移植性

七、总结

结构体是 C 语言中最重要的复合数据类型之一,掌握内存对齐规则和位段技术能显著提升程序性能。在实际开发中,应根据场景选择合适的结构体设计方式,同时注意跨平台兼容性问题。


http://www.ppmy.cn/news/1574963.html

相关文章

【软件开发阶段一】【软件开发项目管理:高效推进项目进程】

大家好,今天咱们来聊聊软件开发项目管理这个话题。如果你是个程序员,或者你曾经参与过软件开发项目,那你一定知道,项目管理这事儿,简直就是一场“代码与时间的赛跑”。稍有不慎,项目进度就会像脱缰的野马一样,狂奔而去,留下你在风中凌乱。所以,今天咱们就来好好聊聊,…

(论文)检测部分欺骗音频的初步调查

Paper–An Initial Investigation for Detecting Partially Spoofed Audio 摘要 所有现有的欺骗性语音数据库都包含整个欺骗性的攻击数据。 在实践中&#xff0c;使用仅部分欺骗的话语来装载成功的攻击是完全合理的。根据定义&#xff0c;部分欺骗的话语包含欺骗和真实段的混…

小智AI桌宠机器狗

本文主要介绍如何利用开源小智AI制作桌宠机器狗 1 源码下载 首先下载小智源码,下载地址, 下载源码后,使用vsCode打开,需要在vscode上安装esp-idf,安装方式请自己解决 2 源码修改 2.1添加机器狗控制代码 在目录main/iot/things下添加dog.cc文件,内容如下; #include…

Python入门教程丨3.8 网络编程

1. 预备知识&#xff1a;网络协议 1.1 什么是网络协议&#xff1f; [!note] 网络协议(Protocol)是计算机网络中不同设备之间进行数据通信所必须遵循的规则和标准&#xff0c;它规定了数据的格式、传输顺序、速度以及如何处理错误等&#xff0c;协议是计算机网络通信的基础&…

leetcode刷题-动态规划08

代码随想录动态规划part08|121. 买卖股票的最佳时机、122.买卖股票的最佳时机II、123.买卖股票的最佳时机III 121.买卖股票的最佳时机122.买卖股票的最佳时机II123.买卖股票的最佳时机III -- 困难 121.买卖股票的最佳时机 leetcode题目链接 代码随想录文档讲解 思路&#xff1a…

从单片机的启动说起一个单片机到点灯发生了什么下——使用GPIO点一个灯

目录 前言 HAL库对GPIO的抽象 核心分析&#xff1a;HAL_GPIO_Init 前言 我们终于到达了熟悉的地方&#xff0c;对GPIO的初始化。经过漫长的铺垫&#xff0c;我们终于历经千辛万苦&#xff0c;来到了这里。关于GPIO的八种模式等更加详细的细节&#xff0c;由于只是点个灯&am…

谈谈 ES 6.8 到 7.10 的功能变迁(2)- 字段类型篇

我们继续来了解一下从 ES 6.8 到 ES 7.10 新增的功能。本篇主要介绍新增的字段类型&#xff0c;会简要概述一下新增字段类型的使用场景和限制&#xff0c;提供简单的测试代码。 Flattened 扁平化对象字段 功能说明 解决场景 该功能主要用于处理具有大量不确定键的 JSON 对象…

常见排序算法以及实现

在本文中&#xff0c;所有排序算法考虑的都是升序情况。只要我们能搞懂算法原理&#xff0c;逆序也是很容易就能实现的。所有的排序算法的代码&#xff0c;都可以在下面这道题中测试。&#xff08;当然有些排序实现的结果会导致不能AC&#xff0c;但并不能说明是错的&#xff0…