PAYATHON 2026

如何在单独的 C 文件中定义 constant expression 常量数组?

技术老齐

结论

一条 extern const 声明,无法让另一个 translation unit 将数组元素用作 C 标准规定的 constant expression。

const 表示程序不能通过该对象修改其值,但这不会使对象成为编译期常量。数组定义是否与使用处位于同一个 translation unit,也不影响这条语言规则。原代码之所以能通过编译,可能是编译器做了常量折叠,或允许使用 GNU 扩展。这种写法不属于可移植的标准 C。

如果其他 translation unit 要在静态初始化、数组长度或 case 标签中使用这些值,就需要在头文件里用 enum、宏或 C23 constexpr 提供相应的标量定义。大型数组仍可只在一个 .c 文件中定义。

为什么 extern const 不行

下面的声明只告诉当前 translation unit,其他地方定义了一个只读对象:

extern const pin_definition_t pin_definitions[];

编译 bar.c 时,编译器看不到这个对象的初始化器。表达式:

pin_definitions[12].pin_mode

需要读取对象的值,而读取普通 const 对象不属于标准 C 要求的 integer constant expression。

因此,下面的静态初始化不能依赖外部数组:

static const uint8_t shared_mode =
    pin_definitions[12].pin_mode ||
    pin_definitions[89].pin_mode ||
    pin_definitions[102].pin_mode;

即使数组定义与使用处在同一个源文件中,普通 const 数组的元素也不会自动成为标准意义上的 constant expression。编译器看到定义后或许能将表达式折叠,但这取决于具体实现或扩展。

链接时优化(LTO)同样解决不了语言层面的约束。LTO 可以优化合法程序,不能保证一个不符合静态初始化要求的表达式变成合法 C。

先把数组长度改成真正的编译期常量

下面的写法也有这个问题:

extern const uint_fast8_t PIN_COUNT;
extern const pin_definition_t pin_definitions[PIN_COUNT];

PIN_COUNT 是对象,并非 integer constant expression。文件作用域数组的长度应使用 enum 或宏:

// pin_definitions.h

#ifndef PIN_DEFINITIONS_H
#define PIN_DEFINITIONS_H

#include <stdint.h>

typedef struct {
    uint32_t pin_address;
    uint8_t pin_mode;
} pin_definition_t;

enum {
    PIN_COUNT = 127
};

extern const pin_definition_t pin_definitions[PIN_COUNT];

#endif

数组只需在一个 .c 文件中定义:

// pin_definitions.c

#include "pin_definitions.h"

const pin_definition_t pin_definitions[PIN_COUNT] = {
    { .pin_address = 0x0000000a, .pin_mode = 0xea },
    { .pin_address = 0x0000000f, .pin_mode = 0x12 },
    // ...
};

这样,最终程序中只有一份数组。

数组初始化时还要注意 =。标准语法如下:

const pin_definition_t pin_definitions[PIN_COUNT] = {
    // ...
};

单独公开编译期计算所需的值

如果 bar.c 必须使用编译期常量,可以将它依赖的模式值定义成 enum 常量或宏,放在头文件中,而不是从外部数组读取:

// pin_definitions.h

enum {
    PIN_COUNT = 127,

    PIN_12_MODE  = 0xea,  // 使用实际数据
    PIN_89_MODE  = 0x00,
    PIN_102_MODE = 0x12,

    SHARED_MODE =
        (PIN_12_MODE != 0) ||
        (PIN_89_MODE != 0) ||
        (PIN_102_MODE != 0)
};

extern const pin_definition_t pin_definitions[PIN_COUNT];

这样,bar.c 就能进行标准的静态初始化:

#include "pin_definitions.h"

void do_something_else(void)
{
    static const uint8_t shared_mode = SHARED_MODE;

    // ...
}

此处的 SHARED_MODE 是 integer constant expression。数组仍然只有一份,头文件仅包含少量标量常量,不会复制整个数组。

逻辑或运算符 || 的结果只能是 0 或 1。如果需要合并多个 bit mask,应改用按位或 |:

enum {
    SHARED_MODE = PIN_12_MODE | PIN_89_MODE | PIN_102_MODE
};

数据较多时避免手工维护两份内容

数组元素较多,又需要由同一份数据生成编译期标量时,可以使用代码生成工具或 X macro。例如,将原始数据放入一个 .def 文件,并为每项命名:

// pin_data.def

PIN(PIN_0,  0x0000000a, 0xea)
PIN(PIN_1,  0x0000000f, 0x12)
// ...

头文件可据此生成数量和模式常量:

// pin_definitions.h

#ifndef PIN_DEFINITIONS_H
#define PIN_DEFINITIONS_H

#include <stdint.h>

typedef struct {
    uint32_t pin_address;
    uint8_t pin_mode;
} pin_definition_t;

enum {
    PIN_COUNT = 0
#define PIN(name, address, mode) + 1
#include "pin_data.def"
#undef PIN
};

#define PIN(name, address, mode) enum { name##_MODE = (mode) };
#include "pin_data.def"
#undef PIN

extern const pin_definition_t pin_definitions[PIN_COUNT];

#endif

数组则只在实现文件中生成:

// pin_definitions.c

#include "pin_definitions.h"

const pin_definition_t pin_definitions[PIN_COUNT] = {
#define PIN(name, address, mode) \
    { .pin_address = (address), .pin_mode = (mode) },
#include "pin_data.def"
#undef PIN
};

其他源文件可以直接使用具名的编译期常量:

enum {
    SHARED_MODE =
        (PIN_12_MODE != 0) ||
        (PIN_89_MODE != 0) ||
        (PIN_102_MODE != 0)
};

这种写法会让各个 translation unit 看到数据对应的 token,但最终 binary 中不会产生多份数组对象。如果数据本身由其他文件或工具生成,同时生成 .h 和 .c 往往更直接。

C23 的 constexpr 能做什么

C23 增加了 constexpr。编译器完整支持该特性时,constexpr 对象可以参与 C23 允许的常量表达式计算,但当前 translation unit 必须能看到它的定义。

如果定义放在另一个 .c 文件中,使用处只能通过 extern const 引用,那么这个外部对象仍不会变成 constexpr。若将完整的 constexpr 数组定义放回头文件,每个 translation unit 又会各自拥有一份定义。

因此,constexpr 适合处理单个 translation unit 内的编译期计算,无法跨越 translation unit 传递数组内容。当前需求可以采用以下结构:

  • 在一个 .c 文件中用 const 定义大型数组;
  • 在头文件中用 extern 声明数组;
  • 使用 enum 或宏定义数组长度;
  • 以 enum、宏或可见的 constexpr 定义提供需要用作 constant expression 的派生值;
  • 数据较多时,使用 X macro 或代码生成工具维护单一数据源。

另外,__pin_definition_t 这类以双下划线开头的标识符由实现保留。应用代码应使用普通名称,也可以用匿名结构体配合 typedef。

备注:内容仅供参考。