完整且详细的C语言教程,从入门到精通(2.1)--C语言关键字(C89标准下的数据类型、IEEE 754 标准、大小端、pragma pack)
你们好,我是一名嵌入式工程师。我正在持续更新《嵌入式工程师的C语言实战》系列教程,目前已更新1篇。如果你也在学习嵌入式,欢迎点个关注,我们一起从零到一。
文章目录
C89标准下的数据类型(12个)
数据类型(12个):char short int long float double signed unsigned struct union enum void
总结表格(现代 64 位 Linux/macOS 常见值)
| 类型 | 通常大小(字节) | 举例 |
|---|---|---|
| char | 1 | char c = ‘x’; |
| short | 2 | short s = 1000; |
| int | 4 | int i = 123456; |
| long | 8 (Unix) / 4 (Win) | long L = 100000L; |
| long long | 8 | long long ll = 1LL<<40; |
| float | 4 | float f = 3.14f; |
| double | 8 | double d = 3.14159; |
| struct | 成员总和对齐后 | 见上例 |
| union | 最大成员大小 | 见上例 |
| enum | 通常 4 | enum Color {R,G,B}; |
| void | 0(不占) | void* ptr; |
一、整型家族(char, short, int, long, long long)
1. char
标准规定:恰好 1 字节(C标准规定 sizeof(char) == 1)。
常见大小:1 字节(8位)。
取值范围:signed char (-128 ~ 127) 或 unsigned char (0 ~ 255)
用法:
存储单个字符(如 ‘A’)
处理原始字节数据(如文件读写、网络数据包)
char ch = 'A'; // 字符
unsigned char byte = 0xFF; // 一个字节的数据
char str[] = "Hello"; // 字符串数组(自动加'\0')
注意:char 的符号性可以由signed, unsigned决定,但有一个重要例外,char 单独使用时,符号性由编译器决定。
在 C/C++ 中,char 实际上分成了三种不同的类型:
| 类型 | 符号性 | 说明 |
|---|---|---|
| signed char | 有符号 | 范围通常是 -128 ~ 127 |
| unsigned char | 无符号 | 范围通常是 0 ~ 255 |
| char | 由编译器决定 | 可以是 signed char,也可以是 unsigned char |
关键例子的演示:
#include <stdio.h>
int main() {
char c = 200; // 200 超过了 signed char 的范围(-128~127)
signed char sc = 200;
unsigned char uc = 200;
printf("char c = %d\n", c); // 输出取决于编译器!
printf("signed char sc = %d\n", sc); // -56(溢出)
printf("unsigned char uc = %d\n", uc); // 200
}
2. short (short int)
标准规定:至少 2 字节,且 short ≤ int ≤ long
常见大小:2 字节(16位)
取值范围:-32,768 ~ 32,767 (signed) 或 0 ~ 65,535 (unsigned)
用法:节省内存时使用(如存储年龄、月份等小整数)
short age = 25;
unsigned short port = 8080;
2.1 几种short 的实际大小可能的情况
下面用一个表格来展示,在不同的硬件下,short 的实际大小会如何变化。假设 sizeof(short) == 2 是标准下限:
| 硬件平台 | char 位数 | sizeof(short) | short 实际位数 | 备注 |
|---|---|---|---|---|
| x86 / ARM | 8 位 | 2 | 16 位 | 最常见的情况(我们通常认为的 “short = 2字节”) |
| 某些 32 位 DSP | 16 位 | 2 | 32 位 | “字节”是 16 位,所以 2 个“字节”就是 32 位 |
| AVR 单片机 | 8 位 | 2 | 16 位 | 标准用法 |
| 理论上 | 32 位 | 2 | 64 位 | 极少见,但标准允许 |
| 违反标准 | 8 位 | 1 | 8 位 | ❌ 不符合标准要求(小于 2 字节) |
注:DSP是 Digital Signal Processor(数字信号处理器),一种专门为快速处理数字信号而设计的微处理器(可以理解成一种特殊用途的 CPU)。
3. int
标准规定:至少 2 字节(通常为 4 字节)
常见大小:
16位系统:2 字节(如古董级DOS)
32/64位系统:4 字节(现代绝大多数环境)
用法:最常用的整数类型(循环计数器、数组索引等)
int count = 100;
int result = a + b;
4. long (long int)
标准规定:至少 4 字节,且 sizeof(long) ≥ sizeof(int)
常见大小:
Windows 64位:4 字节(LLP64模型)
Linux/macOS 64位:8 字节(LP64模型)
用法:需要比 int 更大范围时(文件大小、时间戳等)
long filesize = 123456789L; // 注意后缀 L
注意:加后缀 L 的核心作用是明确告诉编译器,这个整数字面量是 long 类型,而不是默认的 int 类型。
4.1 long (long int)数据后缀 L的作用
场景一:防止数值溢出(最常见的原因)
假设你用的平台是 16 位系统(如一些嵌入式单片机),int 通常是 16 位(范围 -32768 到 32767)。
// 错误示范
long filesize = 40000; // 40000 是 int 类型,已经超出 16 位 int 的范围 -> 溢出
40000 作为 int 已经放不下了,编译器会产生溢出警告或错误。
// 正确做法
long filesize = 40000L; // 40000L 是 long 类型,16位系统下 long 通常是32位 -> 安全
L 后缀告诉编译器直接按 long 处理,避免了先作为 int 溢出、再赋值给 long 的尴尬。
场景二:控制表达式中的类型
当数字参与复杂运算时,类型决定了整个表达式的行为。
long result = 10 * 1024 * 1024 * 4; // 危险!中间计算可能溢出 int
long result = 10L * 1024 * 1024 * 4; // 安全,表达式被提升为 long
第一个式子中,所有操作数都是 int,乘积可能超出 int 范围后溢出,再把溢出的结果赋值给 long。
第二个式子中,第一个数 10L 是 long,整个表达式会按 long 计算,不会溢出。
场景三:函数重载/可变参数(C++ 更常见)
void foo(int x);
void foo(long x);
foo(10); // 调用 foo(int)
foo(10L); // 调用 foo(long)
没有 L,你无法区分调用哪个重载版本。
场景四:避免 int 转 long 的符号扩展问题(罕见但隐蔽)
unsigned long x = 0xFFFFFFFFL; // 正确,x 得到 0xFFFFFFFF
unsigned long y = 0xFFFFFFFF; // 危险!如果 int 是32位,0xFFFFFFFF 是 -1 的 int 表示
y 会先把 0xFFFFFFFF 作为 int(值为 -1),再转换成 unsigned long(变成全1的64位值 0xFFFFFFFFFFFFFFFF),完全不是预期的效果。
L大小写和常见惯例
可以用大写 L 或小写 l,但强烈建议用大写 L。
小写 l 容易和数字 1 混淆:100l 是 100 还是 101?难以分辨。
5. long long (C99起)
标准规定:至少 8 字节
常见大小:8 字节(几乎所有平台统一)
用法:需要极大整数(如内存地址差值、超大循环)
long long big_num = 9223372036854775807LL; // 后缀 LL
unsigned long long very_big = 18446744073709551615ULL;
二、 浮点型(float, double)
1. float, double
这些严格遵循 IEEE 754 标准(现代几乎所有平台)。
| 类型 | 常见大小 | 有效位数 | 取值范围(约) | 用法 |
|---|---|---|---|---|
| float | 4 字节 | 7-8 位 | ±1.2e-38 ~ ±3.4e38 | 节省内存的浮点数(图形、嵌入式) |
| double | 8 字节 | 15-16 位 | ±2.3e-308 ~ ±1.8e308 | 默认浮点类型(科学计算、普通小数) |
float pi_approx = 3.14159f; // 必须加 f 后缀
double pi = 3.141592653589793; // 默认就是 double
2. IEEE 754 标准
IEEE 754 是一个标准,float 是这个标准在 32 位上的具体实现。
| 名称 | 总位数 | 符号位 | 指数位 | 尾数位 |
|---|---|---|---|---|
| binary16 (half) (半精度) | 16 | 1 | 5 | 10 |
| binary32 (float)(单精度) | 32 | 1 | 8 | 23 |
| binary64 (double)(双精度) | 64 | 1 | 11 | 52 |
| binary128 (quad) (四精度) | 128 | 1 | 15 | 112 |
上述表格的意思是,如32位单精度浮点数(float)的划分如下:
例如,float a = 7.123456; 会用 IEEE 754 binary32 标准,把这个数:
拆成符号(0)
加上偏移量算出指数位(129 → 10000001)
截取尾数位到23位(11000111110011010110001)
组合成一个32位二进制数存入内存
2.1 IEEE 754 标准具体计算过程如下
例如上文所述,float a = 7.123456;
第一步:判断符号位 (S)
7.123456 是正数。
规则:IEEE 754 规定,符号位 S = 0 表示正数。
S = 0
第二步:转换为二进制
标准规定,必须先转换成二进制科学计数法。
整数部分 (7):
7 的二进制是 111。
小数部分 (0.123456):
用"乘2取整法"转换:
0.123456 × 2 = 0.246912 → 整数部分 0
0.246912 × 2 = 0.493824 → 整数部分 0
0.493824 × 2 = 0.987648 → 整数部分 0
0.987648 × 2 = 1.975296 → 整数部分 1
0.975296 × 2 = 1.950592 → 整数部分 1
0.950592 × 2 = 1.901184 → 整数部分 1
…(会无限循环下去,因为0.123456在二进制中是无限循环小数)
得到近似:0.123456 ≈ 0.000111111001101011… (二进制)
合并:
7.123456 ≈ 111.000111111001101011… (二进制)
第三步:规格化 (写成 1.M 形式)
这是标准的核心规则:任何数字都写成 1.xxx × 2^E 的形式。
111.000111… = 1.1100011111001101011… × 2^2
因为我们把小数点向左移动了2位(从 111. 变成 1.110…),所以指数是 +2。
此时的尾数部分(M):1100011111001101011…(去掉开头的 1.,只保留小数部分)
实际指数:2
第四步:计算指数位 (E)
这是标准中最巧妙也最容易混淆的规则:指数要加上一个偏移量(bias)。
| 格式 | 指数位 n | 偏移量 = 2^(n-1) - 1 |
|---|---|---|
| binary16 (half) | 5 | 2^4 - 1 = 16 - 1 = 15 |
| binary32 (float) | 8 | 2^7 - 1 = 128 - 1 = 127 |
| binary64 (double) | 11 | 2^10 - 1 = 1024 - 1 = 1023 |
| binary128 (quad) | 15 | 2^14 - 1 = 16384 - 1 = 16383 |
binary32 的偏移量 = 127
E = 实际指数 + 127 = 2 + 127 = 129
129 的 8 位二进制:10000001
E = 10000001
第五步:处理尾数位 (M)
标准规定,尾数只存储规格化后 1.M 的小数部分,开头的 1. 被隐含丢弃。
规格化后的尾数:1100011111001101011…
M = 23位:我们需要截断或舍入到23位(因为 float 只有23位空间)。
标准规定了舍入规则(默认向最近偶数舍入),我们截取前23位:
11000111110011010110001… → 取前23位:11000111110011010110001
(注意,这个截断/舍入过程就是精度损失的来源,7.123456 无法被精确存储了。)
M = 11000111110011010110001
第六步:组合成32位
现在把所有部分按标准规定的顺序拼起来:
结果(二进制):
01000000111000111110011010110001
结果(十六进制,便于查看):
40 E3 F3 31 (取决于CPU大小端序,通常内存中可能是 31 F3 E3 40)
注:大小端序(Endianness)指的是多字节数据在内存中存储时的字节顺序。
3. 大小端序(Endianness)说明
直观例子
假设有一个 32 位整数 0x12345678,它由 4 个字节组成:
高位字节:0x12
低位字节:0x78
在内存地址 0x1000 开始存储时:
| 存储方式 | 地址 0x1000 | 地址 0x1001 | 地址 0x1002 | 地址 0x1003 |
|---|---|---|---|---|
| 大端序 | 0x12 | 0x34 | 0x56 | 0x78 |
| 小端序 | 0x78 | 0x56 | 0x34 | 0x12 |
大端序:高位字节存低地址(符合人类书写习惯,从左到右从高到低)
小端序:低位字节存低地址(符合 CPU 自然处理习惯)
int x = 0x12345678;
short *p = (short*)&x; // 小端:*p = 0x5678(低16位)
// 大端:*p = 0x1234(高16位),通常不是想要的
各平台采用的标准
| 平台/架构 | 端序 | 常见设备 |
|---|---|---|
| x86 / x86-64 (Intel, AMD) | 小端 | PC、服务器、笔记本 |
| ARM | 可配置(通常小端) | 手机、平板、嵌入式 |
| RISC-V | 小端 | 新兴开源处理器 |
| PowerPC | 可配置(传统大端) | IBM 服务器、旧 Mac |
| MIPS | 可配置 | 路由器、嵌入式 |
| SPARC | 大端 | Sun/Oracle 服务器 |
| 网络协议 | 大端 | TCP/IP、UDP、HTTP 等 |
一句话:PC/服务器/手机几乎都是小端;网络传输永远是大端
三、类型修饰符(signed, unsigned)
它们不独立存在,必须与整型结合(char, short, int, long, long long)。
signed(可省):表示有符号数
unsigned:表示无符号数(仅非负数,范围扩大一倍)
unsigned int positive_only = 4000000000U; // 0 ~ 4.29e9
signed char temp = -10; // 通常直接写 char temp = -10;
注意:char 的符号性由编译器决定(GCC for ARM 通常 unsigned,x86 通常 signed),请参考char。
四、构造类型(struct, union, enum)
1. struct(结构体)
大小:各成员大小之和 + 内存对齐填充字节
举例:
struct Student {
char name[20]; // 20 字节
int age; // 4 字节
char grade; // 1 字节
};
// 在 64 位系统上,sizeof(struct Student) 很可能 = 28 (20+4+1+3填充)
1.1 结构体内存计算规则
1.1.1对齐规则
| 规则 | 说明 |
|---|---|
| 成员对齐 | 每个成员的起始偏移量必须是其自身大小的整数倍 |
| 结构体对齐 | 结构体的总大小必须是其最大成员大小的整数倍 |
| 填充字节 | 编译器在成员之间或末尾插入无用字节来满足对齐 |
注意:不同编译器和平台可能有差异(如 #pragma pack 可修改对齐值)。
常见类型对齐要求(64位系统典型值)
| 类型 | 大小(字节) | 对齐要求(起始偏移必须是几的倍数) |
|---|---|---|
| char | 1 | 1 |
| short | 2 | 2 |
| int | 4 | 4 |
| float | 4 | 4 |
| long | 8 | 8 |
| double | 8 | 8 |
| char*(指针) | 8 | 8 |
举例说明
例1:基本对齐
struct Example1 {
char a; // 1字节,偏移0
int b; // 4字节,偏移必须是4的倍数 → 偏移4
char c; // 1字节,偏移8
};
布局分析:
| 偏移 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 内容 | a | 填充 | 填充 | 填充 | b | b | b | b | c | 填充 | 填充 | 填充 |
a 占偏移0
b 需要偏移4 → 偏移1-3填充3字节
c 占偏移8
结构体最大成员 int 是4字节 → 总大小需是4的倍数 → 当前9字节,末尾填充3字节到12
结果: sizeof(struct Example1) = 12
例2:优化排序(减少填充)
struct Example2 {
int b; // 4字节,偏移0
char a; // 1字节,偏移4
char c; // 1字节,偏移5
};
布局分析:
| 偏移 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 内容 | b | b | b | b | a | c | 填充 | 填充 |
b 占偏移0-3
a 占偏移4
c 占偏移5
最大成员 int 4字节 → 当前6字节,末尾填充2字节到8
结果: sizeof(struct Example2) = 8(比例1节省4字节)
技巧:按成员大小从大到小排列可以最小化填充。
例3:含数组的结构体
struct Example3 {
char name[5]; // 5字节,偏移0-4
int age; // 4字节,偏移必须是4的倍数 → 偏移8
};
布局分析:
| 偏移 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 内容 | name[0] | name[1] | name[2] | name[3] | name[4] | 填充 | 填充 | 填充 | age | age | age | age |
name[5] 占偏移0-4
age 需要偏移4的倍数 → 偏移8(偏移5-7填充3字节)
最大成员 int 4字节 → 当前12字节,已是4的倍数
结果: sizeof(struct Example3) = 12
例4:含嵌套结构体
struct Inner {
char c; // 1字节,偏移0
int x; // 4字节,偏移4
}; // 总大小8(偏移0-7)
struct Outer {
char d; // 1字节,偏移0
struct Inner s; // 大小8字节,对齐要求是4(Inner最大成员int)
char e; // 1字节,偏移12(?需要计算)
};
布局分析:
| 偏移 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 内容 | d | 填充 | 填充 | 填充 | s.c | 填充 | 填充 | 填充 | s.x | s.x | s.x | s.x | e | 填充 | 填充 | 填充 |
d 占偏移0
s 对齐要求是4 → 从偏移4开始
s.c 占偏移4
s.x 需要偏移8(偏移5-7填充)
e 占偏移12(因为 s 占用了4-11)
最大成员是 int(4字节) → 当前13字节,末尾填充3字节到16
结果: sizeof(struct Outer) = 16
例5:double 对齐(64位系统)
struct Example5 {
char a; // 1字节,偏移0
double b; // 8字节,对齐要求8 → 偏移8
};
| 偏移 | 0 | 1-7 | 8-15 |
|---|---|---|---|
| 内容 | a | 填充(7字节) | b |
最大成员 double 8字节 → 当前16字节,已是8的倍数
结果: sizeof(struct Example5) = 16
1.1.2 pragma pack ——用于控制结构体的内存对齐方式
#pragma pack 是一个编译器指令,用于控制结构体的内存对齐方式。它可以改变默认的对齐规则,让结构体成员更紧凑地排列,减少填充字节。
默认的对齐规则会插入填充字节,导致结构体变大。在以下场景中,我们需要打破默认规则:
| 场景 | 原因 |
|---|---|
| 网络协议 | 数据包格式固定,不能有多余填充 |
| 文件格式 | 如 BMP、WAV 等二进制文件结构固定 |
| 嵌入式系统 | 内存极其有限,需要节省每一字节 |
| 跨平台通信 | 不同平台对齐规则不同,需要统一布局 |
| 硬件寄存器映射 | 需要精确控制内存布局 |
基本用法
1. 按 n 字节对齐
#pragma pack(n) // n = 1, 2, 4, 8, 16... 后续结构体按 n 字节对齐
struct MyStruct {
// ...
};
#pragma pack() // 恢复默认对齐
2. 推入/弹出(用于嵌套恢复)
#pragma pack(push, 1) // 保存当前对齐值,并设为 1 字节对齐
struct Packed {
// ...
};
#pragma pack(pop) // 恢复之前保存的对齐值
实际效果对比
默认对齐 vs #pragma pack(1)
#include <stdio.h>
// 默认对齐
struct Default {
char a; // 1字节
int b; // 4字节,起始偏移需是4的倍数 → 填充3字节
short c; // 2字节,起始偏移8 → 无填充
}; // 总大小:1+3+4+2=10,对齐到4倍数 → 12
#pragma pack(1) // 强制 1 字节对齐
struct Packed {
char a; // 1字节,偏移0
int b; // 4字节,偏移1(不需要是4的倍数!)
short c; // 2字节,偏移5
}; // 总大小:1+4+2 = 7(无任何填充)
#pragma pack()
int main() {
printf("Default: %zu\n", sizeof(struct Default)); // 12
printf("Packed: %zu\n", sizeof(struct Packed)); // 7
return 0;
}
真实应用举例网络数据包头:
#pragma pack(push, 1) // 保存当前对齐值,并设为 1 字节对齐
struct TCPHeader {
uint16_t src_port;
uint16_t dst_port;
uint32_t seq_num;
uint32_t ack_num;
uint8_t data_offset : 4;
uint8_t reserved : 3;
uint8_t flags : 5; // 实际位域,此处简化
uint16_t window;
uint16_t checksum;
uint16_t urgent_ptr;
};
#pragma pack(pop)
// 确保发送的数据包与协议定义完全一致
send(sock, &header, sizeof(header), 0);
保存当前对齐值”指的是:在 push 的那一刻,编译器内部有一个“当前对齐值”的设置,push 把它备份到了一个内部栈里。
具体过程如下:
每个编译单元都有一个“当前对齐值”
默认情况下它是编译器的默认值(比如在 MSVC 中默认是 8,在 GCC 中默认是成员的自然对齐)
如果之前你写过 #pragma pack(4),那么这个“当前对齐值”就变成了 4
#pragma pack(push, 1) 做了两件事
第一步(push):把当前的“对齐值”复制一份,压入内部栈(保存起来,稍后用于恢复)
第二步:将当前对齐值修改为 1
#pragma pack(pop) 也做了两件事
第一步:从内部栈弹出一个之前保存的对齐值
第二步:将当前对齐值恢复成弹出的那个值
内部栈的原理(类比)
可以把 push 想象成叠盘子:
初始:当前值 = 8(默认)
第1次 pack(push, 4):
栈:[8] ← 保存旧值
当前值 = 4
第2次 pack(push, 1):
栈:[8, 4] ← 再保存旧值 4
当前值 = 1
第3次 pack(pop):
从栈顶弹出 4
栈:[8]
当前值 = 4
第4次 pack(pop):
从栈顶弹出 8
栈:[]
当前值 = 8
1.1.3 使用编译属性(更现代)——用于控制结构体的内存对齐方式
// GCC/Clang
struct __attribute__((packed)) MyStruct {
char a;
int b;
};
// C11 标准(C11 及以后)
struct MyStruct {
char a;
int b;
} __attribute__((packed));
attribute((packed)) 的作用:
attribute((packed)) 是 GCC 和 Clang 编译器提供的一个扩展语法,它的作用和 #pragma pack(1) 类似:告诉编译器在存储这个结构体时,不要插入任何填充字节(取消内存对齐)。
简单说:让结构体成员紧挨着存放,一个挨一个,中间不塞任何无用字节。作用与pack(push, 1)几乎相同。
attribute((packed))与 #pragma pack(1) 的对比
| 特性 | attribute((packed)) | #pragma pack(1) |
|---|---|---|
| 作用范围 | 只作用于紧跟着的那个结构体 | 后续所有结构体,直到遇到 #pragma pack() |
| 可读性 | 结构体定义时就能看到,意图明确 | 可能在文件开头很远的地方,不易察觉 |
| 粒度控制 | 精细(每个结构体单独控制) | 粗粒度(影响一大片) |
| 恢复 | 不需要恢复,影响局部 | 需要 pop 或 pack() 恢复 |
| 兼容性 | GCC/Clang 专用 | 大多数编译器都支持(MSVC 也支持 #pragma pack) |
1.2 struct(结构体)表达形式—— 存储不同数据的组合
形式1:先声明类型,后定义变量
最常用,代码清晰
// 声明结构体类型(此时不分配内存)
struct Student {
char name[50];
int age;
float score;
};
// 后定义变量(此时分配内存)
struct Student stu1;
struct Student stu2 = {"张三", 20, 85.5}; // 顺便初始化
形式2:声明类型的同时定义变量
struct Student {
char name[50];
int age;
float score;
} stu1, stu2; // 直接定义了两个变量
// 后续可以继续使用该类型
struct Student stu3;
形式3:匿名结构体(一次性使用,C11起支持)
struct {
int x;
int y;
} point = {10, 20}; // 直接定义变量,类型无法再次使用
// 错误:后续不能再定义 struct point p2; 因为没有类型名
形式4:使用 typedef 简化(工业界最常用)
typedef struct {
char name[50];
int age;
float score;
} Student; // 现在 Student 是一个类型别名
Student stu1 = {"李四", 22, 90.0}; // 不需要写 struct 关键字
Student *ptr = &stu1;
2. union(共用体)
大小:最大成员的大小(所有成员共享同一块内存)
用法:节省内存或类型双关(需谨慎)
union Data {
int i; // 4 字节
float f; // 4 字节
char c; // 1 字节
};
// sizeof(union Data) = 4
1.1 union节省内存与类型双关
1. 节省内存:所有成员共享同一块内存
这是联合体的设计初衷。它让多个变量共用同一块内存空间,但同一时间只能使用其中一个成员。
工作原理:联合体的大小等于其最大成员的大小。所有成员都从这块内存的同一个起始地址开始存储。
适用场景:
内存极度受限的嵌入式系统:例如,用一个变量在不同时间存储不同类型的数据。
构建可变类型(Variant):C语言中实现"一个变量,多种类型"的简单方式。
解析协议数据包:数据包的不同阶段可能代表不同含义。
举例说明:
union Data {
int i; // 4 字节
float f; // 4 字节
char str[20]; // 20 字节(最大)
};
// sizeof(union Data) == 20,而不是 4+4+20=28
这个联合体可以在不同时间被用来存储一个整数、一个浮点数或一个字符串,但不能同时存储。当你写入 f 时,之前写入的 i 的内容就会被覆盖。
2. 类型双关:用A类型写入,用B类型读取
"类型双关"指的是将同一块内存中的数据,解释为另一种完全不同的类型。这是C/C++中一种底层的、有技巧性的操作,它绕过了类型系统。
常见用法:获取一个浮点数的十六进制表示,或者快速解析二进制数据。
举例说明:
#include <stdio.h>
union FloatPun {
float f;
unsigned int u;
};
int main() {
union FloatPun pun;
pun.f = 3.14159f;
// 用整数类型读取浮点数的内存表示
printf("Hex representation of %f: 0x%X\n", pun.f, pun.u);
return 0;
}
写入:通过联合体的 f 成员(float 类型)将浮点数 3.14159f 的二进制表示写入共享内存。
双关读取:过联合体的 u 成员(unsigned int 类型)将同一块内存中的数据解释为一个无符号整数来读取。
这就好比你在一个盒子里放了一支钢笔(float),然后不看盒子里的东西,就宣称里面是一支铅笔(unsigned int),并把它当铅笔用。盒子里的二进制数据没变,但解读方式完全不同。
这段代码在某些场景下(如想分析浮点数的内部结构)可能有用,但它依赖于一些特定的假设(如 float 和 int 大小相同、字节序相同),因此存在风险。
1.2 union(共用体)表达形式—— 同一块内存存储不同数据
形式1:先声明类型,后定义变量
union Data {
int i;
float f;
char str[20];
};
union Data d1;
d1.i = 100; // 此时存储 int
printf("%d\n", d1.i);
d1.f = 3.14; // 覆盖了之前的 int,此时存储 float
形式2:声明类型的同时定义变量
union Data {
int i;
float f;
char str[20];
} d1, d2;
形式3:匿名联合体(常嵌套在结构体中)
struct Vehicle {
int type; // 0:car, 1:bike
union { // 匿名联合体,节省内存
int car_doors;
int bike_wheels;
}; // 不需要成员名,直接访问
};
struct Vehicle v;
v.type = 0;
v.car_doors = 4; // 直接访问,不需要 v.xxx.car_doors
形式4:typedef 简化
typedef union {
int int_val;
float float_val;
char char_val;
} Value;
Value v;
v.int_val = 42;
关键注意:同一时刻只能使用一个成员,否则会相互覆盖。
3. enum(枚举)
大小:通常等于 int(4 字节),但编译器可能优化为更小
用法:定义具名常量
enum Color { RED, GREEN, BLUE };
enum Color my_color = RED;
// sizeof(enum Color) 通常为 4
3.1 enum(枚举)表达形式—— 定义命名整数常量
形式1:先声明类型,后定义变量
enum Color {
RED, // 0
GREEN, // 1
BLUE // 2
};
enum Color myColor = RED;
if (myColor == GREEN) {
printf("绿色\n");
}
形式2:指定枚举值(任意整数)
enum Status {
OK = 0,
WARNING = 100,
ERROR = 200,
UNKNOWN = -1 // 可以有负值
};
enum Status s = WARNING;
printf("%d\n", s); // 输出 100
形式3:声明类型的同时定义变量
enum Month {
JAN=1, FEB, MAR, APR, MAY, JUN,
JUL, AUG, SEP, OCT, NOV, DEC
} currentMonth, nextMonth;
currentMonth = JAN;
nextMonth = FEB;
形式4:typedef 简化(最常用)
typedef enum {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
} Weekday;
Weekday today = MONDAY;
形式5:匿名枚举(用于定义常量)
enum {
MAX_BUFFER = 1024,
TIMEOUT_MS = 5000,
RETRY_COUNT = 3
};
char buffer[MAX_BUFFER]; // 直接使用常量
五、特殊类型(void)
void:不占任何字节(sizeof(void) 在标准中非法,某些扩展允许为1)
用法:
函数无返回值:void func(void)
通用指针:void* ptr(可指向任何类型,大小等于普通指针)
void* malloc(size_t size); // 返回通用指针
void exit(int status); // 无返回值
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,主播是一个马上粉丝量超1个的嵌入式内容主播。谢谢!
更多推荐


所有评论(0)