C语言关键字—typedef/sizeof/struct/union/enum/inline笔记(下)
- C语言进阶
- 11小时前
- 16热度
- 0评论
C语言进阶 — 关键字详解(下)
上篇:C语言关键字(上)—— static、extern、volatile、const。
本篇聚焦嵌入式开发中与类型定义、编译优化、数据组织相关的关键字:typedef、sizeof、register、inline、enum、struct、union。
5 typedef 关键字
typedef 为已有类型创建一个别名,提高代码可读性和可移植性。
5.1 基本用法
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
uint8_t data = 0xFF;
uint32_t addr = 0x40021000;
5.2 结构体别名
// 不用 typedef:每次都要写 struct
struct GPIO_Config {
uint32_t pin;
uint32_t mode;
};
struct GPIO_Config cfg; // 必须带 struct
// 用 typedef:简洁
typedef struct {
uint32_t pin;
uint32_t mode;
} GPIO_Config_t;
GPIO_Config_t cfg; // 直接使用
5.3 函数指针别名
函数指针语法复杂,typedef 可以大幅简化:
// 不用 typedef
void (*callback)(int, int); // 声明一个函数指针变量
// 用 typedef
typedef void (*Callback_t)(int, int); // 定义函数指针类型
Callback_t callback; // 声明变量,清晰易读
// 实际应用:回调函数注册
typedef void (*IRQ_Handler_t)(void);
void register_irq(uint8_t irq_num, IRQ_Handler_t handler)
{
irq_table[irq_num] = handler;
}
5.4 typedef vs #define
typedef char *String_t;
#define String_d char *
String_t s1, s2; // s1 和 s2 都是 char* ✅
String_d s3, s4; // s3 是 char*,s4 是 char ❌ 陷阱!
// 宏展开后:char *s3, s4; → s4 只是 char
6 sizeof 运算符
sizeof 虽然看起来像函数,但它是编译期运算符,在编译阶段就已确定结果。
6.1 基本用法
printf("char: %zu\n", sizeof(char)); // 1
printf("short: %zu\n", sizeof(short)); // 2
printf("int: %zu\n", sizeof(int)); // 通常 4(平台相关)
printf("float: %zu\n", sizeof(float)); // 4
printf("double: %zu\n", sizeof(double)); // 8
printf("指针: %zu\n", sizeof(int *)); // 32位系统=4, 64位系统=8
6.2 数组与指针的区别
int arr[10];
int *ptr = arr;
sizeof(arr); // 40(10 × 4 字节,整个数组大小)
sizeof(ptr); // 4 或 8(指针本身的大小)
// 常用技巧:计算数组元素个数
int count = sizeof(arr) / sizeof(arr[0]); // 10
数组作为函数参数时会退化为指针,
sizeof 得到的是指针大小而非数组大小。
```c
void func(int arr[])
{
sizeof(arr); // ❌ 得到指针大小 4/8,而非数组大小
}
```
6.3 结构体对齐
typedef struct {
char a; // 1 字节
int b; // 4 字节
char c; // 1 字节
} Example_t;
sizeof(Example_t); // 可能是 12,而非 6!(内存对齐)
编译器为了提高 CPU 读取效率,会在结构体成员之间插入填充字节(padding)。
7 register 关键字
register 建议编译器将变量存放在 CPU 寄存器中,以加快访问速度。
7.1 用法
void delay(register unsigned int count)
{
while (count--) {
// count 存在寄存器中,自减操作更快
}
}
7.2 限制
register变量不能取地址(&运算符不可用),因为寄存器没有内存地址- 现代编译器(GCC
-O2以上)会自动进行寄存器分配优化,register更多是一种提示 - 嵌入式中偶尔用于时间关键的循环变量
register int i;
int *p = &i; // ❌ 编译报错:不能对 register 变量取地址
8 inline 关键字(C99)
inline 建议编译器在调用处展开函数体,避免函数调用开销(类似宏函数但有类型检查)。
8.1 用法
static inline int max(int a, int b)
{
return (a > b) ? a : b;
}
int result = max(x, y);
// 编译器可能将其展开为:
// int result = (x > y) ? x : y;
8.2 inline vs 宏函数
| 特性 | inline 函数 |
宏函数(#define) |
|---|---|---|
| 类型检查 | ✅ 有 | ❌ 无 |
| 调试 | ✅ 可调试 | ❌ 无法断点 |
| 副作用 | ✅ 参数只计算一次 | ❌ 可能多次计算 |
| 展开时机 | 编译期(编译器决定) | 预处理期(一定展开) |
// 宏的副作用陷阱
#define SQUARE(x) ((x) * (x))
int a = 5;
SQUARE(a++); // 展开为 ((a++) * (a++)),a 被加了两次!❌
// inline 没有这个问题
static inline int square(int x) { return x * x; }
square(a++); // a 只增加一次 ✅
对于短小、频繁调用的函数,推荐使用
static inline。
9 enum 关键字
enum(枚举)定义一组命名的整型常量,提升代码可读性和可维护性。
9.1 基本用法
typedef enum {
LED_OFF = 0,
LED_ON = 1
} LED_State_t;
typedef enum {
UART1 = 0,
UART2, // 自动 = 1
UART3, // 自动 = 2
UART_MAX // 常用技巧:最后一个成员表示总数
} UART_Channel_t;
LED_State_t led = LED_ON;
9.2 状态机应用
枚举在嵌入式状态机中非常常用:
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_ERROR,
STATE_STOP
} SystemState_t;
SystemState_t state = STATE_IDLE;
void state_machine(void)
{
switch (state) {
case STATE_IDLE:
if (start_button_pressed())
state = STATE_RUNNING;
break;
case STATE_RUNNING:
motor_run();
if (error_detected())
state = STATE_ERROR;
break;
case STATE_ERROR:
motor_stop();
alarm_on();
state = STATE_STOP;
break;
case STATE_STOP:
// 等待复位
break;
}
}
9.3 enum vs #define
| 特性 | enum |
#define |
|---|---|---|
| 类型 | 有枚举类型 | 无类型 |
| 调试 | 调试器可显示名称 | 已被替换,不可见 |
| 作用域 | 遵循 C 作用域规则 | 全局(从定义到文件末尾) |
| 自动编号 | ✅ 支持 | ❌ 需手动 |
10 struct 与 union
10.1 struct(结构体)
结构体将不同类型的数据组合在一起,各成员独立占用内存。
typedef struct {
uint8_t id;
uint8_t type;
uint16_t length;
uint8_t data[64];
} Packet_t;
Packet_t pkt;
pkt.id = 0x01;
pkt.type = 0x02;
pkt.length = 10;
位域(Bit Field)
嵌入式中常用位域精确控制每个 bit:
typedef struct {
uint8_t mode : 2; // 2 bits: 0~3
uint8_t enable : 1; // 1 bit: 0 或 1
uint8_t speed : 3; // 3 bits: 0~7
uint8_t reserved : 2; // 2 bits: 保留
} GPIO_Config_t; // 总共 1 字节
GPIO_Config_t cfg;
cfg.mode = 0x02; // 输出模式
cfg.enable = 1; // 使能
cfg.speed = 0x03; // 高速
10.2 union(联合体)
联合体中所有成员共享同一块内存,大小等于最大成员的大小。同一时刻只能有效使用一个成员。
typedef union {
uint32_t word; // 4 字节
uint16_t half[2]; // 4 字节
uint8_t byte[4]; // 4 字节
} Data32_t; // 总共只占 4 字节
Data32_t data;
data.word = 0x12345678;
printf("byte[0] = 0x%02X\n", data.byte[0]); // 小端:0x78
printf("byte[3] = 0x%02X\n", data.byte[3]); // 小端:0x12
printf("half[0] = 0x%04X\n", data.half[0]); // 小端:0x5678
```c
union {
uint16_t value;
uint8_t bytes[2];
} endian_test;
endian_test.value = 0x0102;
if (endian_test.bytes[0] == 0x02)
printf("小端 (Little-Endian)\n"); // ARM、x86
else
printf("大端 (Big-Endian)\n"); // 部分网络设备
```
10.3 struct 与 union 结合
在嵌入式协议解析和寄存器映射中经常组合使用:
// 寄存器映射:既可以按位操作,也可以整体读写
typedef union {
uint32_t all; // 整体访问
struct {
uint32_t mode : 4; // bit[3:0]
uint32_t speed : 2; // bit[5:4]
uint32_t pull : 2; // bit[7:6]
uint32_t reserved : 24;
} bits; // 按位访问
} GPIO_CR_t;
volatile GPIO_CR_t *GPIOA_CR = (volatile GPIO_CR_t *)0x40010800;
// 按位设置
GPIOA_CR->bits.mode = 0x03; // 输出模式
GPIOA_CR->bits.speed = 0x01; // 中速
// 整体清零
GPIOA_CR->all = 0x00000000;
10.4 struct vs union 对比
| 特性 | struct |
union |
|---|---|---|
| 内存 | 各成员分别占用内存 | 所有成员共享同一块内存 |
| 大小 | 所有成员大小之和(+ 对齐填充) | 最大成员的大小 |
| 同时访问 | ✅ 所有成员可同时使用 | ❌ 同一时刻只有一个成员有效 |
| 典型用途 | 数据打包(协议帧、配置等) | 类型转换、寄存器映射 |
11 关键字速查表
| 关键字 | 作用 | 嵌入式场景 |
|---|---|---|
typedef |
类型别名 | 统一类型命名、函数指针简化 |
sizeof |
编译期求大小 | 缓冲区计算、数组元素计数 |
register |
建议寄存器存储 | 时间关键循环(现代编译器自动优化) |
inline |
建议内联展开 | 短小高频函数 |
enum |
命名整型常量 | 状态机、配置选项 |
struct |
聚合不同类型数据 | 协议帧、外设配置 |
union |
共享内存的多视图 | 寄存器映射、大小端转换 |
12 自测题
用以下题目检验对 typedef、sizeof、inline、enum、struct、union 的理解。
题 1:typedef vs #define 陷阱
以下代码中 a、b、c、d 分别是什么类型?
typedef int* IntPtr_t;
#define IntPtr_d int*
IntPtr_t a, b;
IntPtr_d c, d;
点击查看答案
- a → int* ✅
- b → int* ✅
- c → int* ✅
- d → int ❌(不是指针!)
#define IntPtr_d int* 是纯文本替换,展开后为 int* c, d;,等价于 int *c; int d;。只有 c 是指针,d 是普通 int。
typedef 定义的是一个完整类型别名,不存在这个问题。
题 2:sizeof 数组退化
以下代码输出什么?(假设 32 位系统,int 为 4 字节)
void print_size(int arr[])
{
printf("inside: %zu\n", sizeof(arr));
}
int main(void)
{
int data[10];
printf("outside: %zu\n", sizeof(data));
print_size(data);
return 0;
}
点击查看答案
```
outside: 40
inside: 4
```
- sizeof(data) 在 main 中,data 是数组,大小 = 10 × 4 = 40 字节
- sizeof(arr) 在函数参数中,数组退化为指针,大小 = 4 字节(32 位指针)
要在函数中知道数组大小,必须额外传递长度参数。
题 3:结构体内存对齐
以下两个结构体大小分别是多少?(假设 32 位系统,默认 4 字节对齐)
typedef struct {
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
} StructA_t;
typedef struct {
char a; // 1 byte
char c; // 1 byte
int b; // 4 bytes
} StructB_t;
点击查看答案
- sizeof(StructA_t) = 12
- sizeof(StructB_t) = 8
StructA_t 的内存布局:a(1) + padding(3) + b(4) + c(1) + padding(3) = 12
StructB_t 的内存布局:a(1) + c(1) + padding(2) + b(4) = 8
成员相同,但排列顺序不同导致大小不同。嵌入式中 RAM 紧张时,应将小成员集中排列以减少 padding。
题 4:enum 自动编号
以下枚举成员的值分别是多少?
typedef enum {
ERR_NONE,
ERR_TIMEOUT,
ERR_OVERFLOW = 10,
ERR_CRC,
ERR_UNKNOWN
} ErrorCode_t;
点击查看答案
| 成员 | 值 |
| :--- | :--- |
| ERR_NONE | 0 |
| ERR_TIMEOUT | 1 |
| ERR_OVERFLOW | 10(手动赋值) |
| ERR_CRC | 11(从 10 开始递增) |
| ERR_UNKNOWN | 12 |
规则:未赋值的成员 = 前一个成员 + 1。第一个成员默认从 0 开始。
题 5:union 内存共享
以下代码在小端系统(ARM Cortex-M)上输出什么?
typedef union {
uint32_t word;
uint8_t byte[4];
} Data32_t;
Data32_t d;
d.word = 0xAABBCCDD;
printf("byte[0]=0x%02X\n", d.byte[0]);
printf("byte[3]=0x%02X\n", d.byte[3]);
点击查看答案
```
byte[0]=0xDD
byte[3]=0xAA
```
小端存储:低字节在低地址。0xAABBCCDD 在内存中的排列为:
| 地址 | byte[0] | byte[1] | byte[2] | byte[3] |
| :--- | :--- | :--- | :--- | :--- |
| 值 | 0xDD | 0xCC | 0xBB | 0xAA |
union 让我们可以用不同"视角"访问同一块内存。
题 6:位域操作
以下代码中 sizeof(Reg_t) 是多少?reg.all 的值是什么?
typedef union {
uint8_t all;
struct {
uint8_t enable : 1;
uint8_t mode : 2;
uint8_t speed : 3;
uint8_t reserved : 2;
} bits;
} Reg_t;
Reg_t reg;
reg.all = 0x00;
reg.bits.enable = 1;
reg.bits.mode = 2;
reg.bits.speed = 5;
点击查看答案
sizeof(Reg_t) = 1(字节)
位域布局(从低位到高位):
| bit7-6 | bit5-3 | bit2-1 | bit0 |
| :--- | :--- | :--- | :--- |
| reserved=0 | speed=5(101) | mode=2(10) | enable=1(1) |
二进制:00_101_10_1 = 0x2D = 45
reg.all = 0x2D
题 7:inline 与宏的副作用
以下代码中 a 的最终值分别是多少?
#define DOUBLE_M(x) ((x) + (x))
static inline int double_f(int x) { return x + x; }
int a = 5;
int r1 = DOUBLE_M(a++); // A
int b = 5;
int r2 = double_f(b++); // B
点击查看答案
**A**:DOUBLE_M(a++) 展开为 ((a++) + (a++)),a 被自增两次,最终 a = 7。r1 的值取决于编译器求值顺序(未定义行为),可能是 10 或 11。
**B**:double_f(b++) 中 b++ 作为参数只计算一次,传入值 5,b 变为 6。r2 = 10。
这就是 inline 函数比宏安全的原因——参数只求值一次。
题 8:综合题
找出以下嵌入式代码中的所有问题:
#define PTR_TYPE uint8_t*
typedef struct {
uint8_t header;
uint32_t payload;
uint8_t checksum;
} __attribute__((packed)) Packet_t;
void process(int data[])
{
int len = sizeof(data) / sizeof(data[0]); // A
PTR_TYPE p1, p2; // B
p1 = malloc(10);
p2 = malloc(10);
register int *idx; // C
int addr = (int)&(*idx);
}
点击查看答案
共 3 个问题:
**A**:sizeof(data) 得到的是指针大小(4 或 8),不是数组大小。数组作为参数会退化,len 的计算结果错误。应额外传递长度参数。
**B**:PTR_TYPE p1, p2; 展开为 uint8_t* p1, p2;,p2 是 uint8_t 而非指针。应改用 typedef。
**C**:register int *idx; 后面的 &(*idx) 对 register 变量取地址,编译报错。register 变量没有内存地址,不能使用 & 运算符。