13. 结构体与const:const结构体、const成员、const指针的交互

const 这个关键字,在 C 语言里就像一把双刃剑。用好了,代码健壮性直接上一个台阶;用不好,编译器的报错能让你怀疑人生。尤其是当 const 遇上结构体,再加上指针,这三者搅在一起,很多老手都会翻车。

我个人习惯,在嵌入式项目中,能加 const 的地方绝不手软。为什么?因为嵌入式设备跑起来,一个野指针或者意外修改,轻则死机,重则烧板子。我就在一个电机驱动项目里吃过亏——一个本应只读的配置结构体被某个函数偷偷改了,结果电机转着转着突然反向,差点把机械臂甩出去。从那以后,我对 const 的用法就格外较真。

13.1 const 结构体变量

先看最基础的情况:定义一个 const 结构体变量。

struct Sensor {
    int id;
    float range;
    char unit;
};

const struct Sensor temp_sensor = {1, 100.0f, 'C'};

这个 temp_sensor 一旦初始化,所有成员都不能改。你想想看,如果某个传感器参数是出厂校准值,就应该这么写。编译器会帮你拦住所有试图修改的行为。

注意: const 结构体变量必须在定义时初始化。不能先声明再赋值。这是 C 语言的硬性规定。

我在项目中经常把系统参数表定义成 const 结构体数组,放在 Flash 里。这样既省 RAM,又防止运行时被篡改。

13.2 const 结构体成员

有时候,我们只想保护结构体里的某个成员,而不是整个结构体。比如一个结构体里,有些字段是固定的,有些字段需要运行时修改。

struct DeviceConfig {
    const int device_id;   // 只读,初始化后不能改
    int baudrate;          // 可读写
    float voltage;         // 可读写
};

这里 device_id 被 const 修饰。初始化之后,任何试图修改它的代码都会编译报错。其他成员则不受影响。

小技巧: 如果你在写驱动库,把硬件版本号、芯片 ID 这类固定信息用 const 成员保护起来,是个好习惯。调用者想改也改不了。

不过说实话,const 成员在实际项目中用得不算特别多。因为大多数时候,要么整个结构体 const,要么完全不 const。但当你需要设计一个“半可变”的数据结构时,这个特性就很有用了。

13.3 const 与结构体指针

这才是真正的重头戏。const 和指针组合起来,有四种常见写法。我当年刚学的时候,也被绕得晕头转向。咱们一个一个拆开看。

写法 含义 能否修改指针 能否修改指向的内容
const struct S *p 指向 const 结构体的指针 可以 不可以
struct S * const p 指向结构体的 const 指针 不可以 可以
const struct S * const p 指向 const 结构体的 const 指针 不可以 不可以
struct S const *p 与第一种等价 可以 不可以

嗯,这里要注意:const struct S *pstruct S const *p 是完全等价的。const 放在类型前面还是后面,效果一样。但 struct S * const p 就完全不同了。

13.3.1 指向 const 结构体的指针

struct Sensor {
    int id;
    float range;
};

struct Sensor s = {1, 100.0f};
const struct Sensor *p = &s;

// p->id = 2;      // 错误!不能修改
// p->range = 200;  // 错误!不能修改
p = &some_other;    // 可以,指针本身可以变

这种写法最常见。比如你写一个打印函数,只想读取结构体数据,不想修改它。就应该用 const 指针参数。

void print_sensor(const struct Sensor *ps) {
    printf("ID: %d, Range: %.1f\n", ps->id, ps->range);
    // 函数体内不会意外修改 ps 指向的内容
}

我曾经在一个通信协议栈里,所有解析函数都用了这种 const 指针。后来同事接手代码,说“你这接口一看就知道哪些数据会被改,哪些不会,太清楚了”。

13.3.2 指向结构体的 const 指针

struct Sensor s = {1, 100.0f};
struct Sensor * const p = &s;

p->id = 2;          // 可以,内容可以改
// p = &other;      // 错误!指针本身不能改

这种写法,说白了就是“指针一旦指向某个对象,就不能再指向别处”。但指向的对象内容可以随便改。在嵌入式里,常用于固定映射的硬件寄存器地址。

核心区别: const 在 * 左边,保护的是指向的内容;const 在 * 右边,保护的是指针本身。

13.3.3 双重 const

const struct Sensor * const p = &s;
// 既不能改指针,也不能改内容

这种写法比较极端。我一般只在全局常量指针或者只读查找表里用。比如一个固定的传感器校准参数表,既不能换表,也不能改表里的数据。

13.4 const 结构体作为函数参数

函数参数用 const 结构体指针,是 C 语言里最推荐的实践。原因很简单:传结构体本身会拷贝整个结构体,浪费栈空间和时间;传指针只拷贝 4 或 8 个字节。加上 const 后,调用者可以放心地把数据交给你。

// 不推荐:传整个结构体,拷贝开销大
void process(struct Sensor s);

// 推荐:传 const 指针,高效且安全
void process(const struct Sensor *ps);

我见过一些新手代码,结构体几百个字节,还按值传递。每次调用都拷贝一次,性能直接拉胯。改成 const 指针后,代码跑得飞快,而且语义更清晰。

避坑指南: 如果你在函数内部需要修改结构体,但调用者传进来的是 const 指针,千万别用强制类型转换去掉 const。我曾经这么干过,结果调试了三天才发现是这里出了问题。正确的做法是:要么修改函数接口,要么在函数内部拷贝一份再改。

13.5 知识体系图

下面这张图,把 const 与结构体、指针的交互关系梳理清楚了。你可以把它当作一个速查表。

const 与结构体 const 结构体变量 所有成员只读 必须在定义时初始化 常用于固定参数表 const 结构体成员 部分成员只读 其他成员可读写 适用于半可变结构 const 与指针 const 在 * 左边 → 内容只读,指针可变 const 在 * 右边 → 指针只读,内容可变 两边都有 const → 指针和内容都只读 函数参数推荐用 const 指针传结构体

13.6 实际项目中的建议

说了这么多,总结几条我在项目里实际用下来的经验:

  • 函数参数优先用 const 指针:除非结构体很小(比如 8 字节以内),否则别传值。const 指针既高效又安全。
  • 全局常量用 const 结构体:比如系统配置、校准参数、查找表。放在 const 段,还能省 RAM。
  • 别滥用强制类型转换去掉 const:这基本等于告诉编译器“你别管我,我知道我在干什么”。但大多数时候,你不知道。
  • const 指针的写法,从右往左读:比如 const struct S * const p,从右往左:p 是一个 const 指针,指向一个 const struct S。这样不容易搞混。

个人习惯: 我写代码时,只要函数不修改结构体,参数一律加 const。哪怕一开始觉得没必要,也加上。因为代码会迭代,三个月后你自己都记不清哪个参数会被改。const 就是最好的文档。

嗯,关于 const 与结构体的交互,就聊到这里。记住一点:const 不是束缚,是保护。保护你的数据不被意外修改,保护你的代码逻辑不被破坏。用好它,你的 C 代码会稳健很多。


公众号:蓝海资料掘金营,微信deep3321