C语言关键字—typedef/sizeof/struct/union/enum/inline笔记(下)

C语言进阶 — 关键字详解(下)

上篇:C语言关键字(上)—— staticexternvolatileconst

本篇聚焦嵌入式开发中与类型定义、编译优化、数据组织相关的关键字:typedefsizeofregisterinlineenumstructunion


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 自测题

用以下题目检验对 typedefsizeofinlineenumstructunion 的理解。


题 1:typedef vs #define 陷阱

以下代码中 abcd 分别是什么类型?

typedef int* IntPtr_t;
#define IntPtr_d int*

IntPtr_t a, b;
IntPtr_d c, d;
点击查看答案

- aint*
- bint*
- cint*
- dint ❌(不是指针!)

#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 = 7r1 的值取决于编译器求值顺序(未定义行为),可能是 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;p2uint8_t 而非指针。应改用 typedef

**C**:register int *idx; 后面的 &(*idx)register 变量取地址,编译报错。register 变量没有内存地址,不能使用 & 运算符。