你们好,我是一名嵌入式工程师。我正在持续更新《嵌入式工程师的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)的划分如下:

00bit 05bit 10bit 15bit 20bit 25bit 30bit 符号位 S (bit 31) 指数位 E (bits 30-23) 尾数位 M (bits 22-0) 高地址 → 低地址 单精度浮点数(32位)位域布局

例如,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位
现在把所有部分按标准规定的顺序拼起来:

00bit 05bit 10bit 15bit 20bit 25bit 30bit 符号位 S (bit 31)0 指数位 E (bits 30-23)10000001 尾数位 M (bits 22-0) 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个的嵌入式内容主播。谢谢!

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐