_COUNT_ 自动计数宏
COUNT 自动计数宏
在嵌入式 C 工程里,经常需要「每展开一次宏就得到一个不同的整数」或「拼出不会重名的符号」。GCC / Clang / MSVC 等编译器提供内置宏 __COUNTER__;项目里常再包一层 _COUNT_,统一命名、便于检索,也避免和业务里的 COUNT 之类名字冲突。
核心定义
/* count.h — 建议在公共头里只定义一次 */
#ifndef COUNT_H
#define COUNT_H
/* 每次预处理展开 _COUNT_,得到 0、1、2、… 递增的整型字面量 */
#define _COUNT_ __COUNTER__
/* 与标识符拼接:生成唯一符号名 */
#define _PASTE_(a, b) a##b
#define _PASTE(a, b) _PASTE_(a, b)
#define _UNIQUE_(prefix) _PASTE(prefix, _COUNT_)
#endif
展开示例:
int a = _COUNT_; /* 0 */
int b = _COUNT_; /* 1 */
int c = _COUNT_; /* 2 */
__COUNTER__ 行为要点
| 特性 | 说明 |
|---|---|
| 起始值 | 每个翻译单元(.c 文件)从 0 开始 |
| 递增时机 | 每次宏被展开时加 1,与运行时无关 |
与 __LINE__ 对比 |
__LINE__ 随 #include、行号变化,同一行可重复;_COUNT_ 在同一 TU 内按展开次序单调递增,更适合生成唯一名 |
| 未使用的实参 | 作为宏实参传入但未被使用时,部分编译器不会递增(以实现为准) |
# 字符串化、## 粘贴 |
多数编译器在未真正展开为独立 token 时不递增 |
| 预编译头 | 若 PCH 里已展开过 __COUNTER__,再包含可能影响计数连续性,需注意 PCH 与宏混用 |
标准 C 尚未正式纳入 __COUNTER__(C23 提案 P3384),但 GCC、Clang、IAR、Keil ARMCC/ARMClang、MSVC(较新版本) 在嵌入式场景里普遍可用;移植前用目标工具链各写一行 _COUNT_ 验证即可。
典型用法
1. 生成唯一的静态对象名(注册表、单例表)
同一宏在多个 .c 里展开时,若都放在函数外的全局/静态作用域,需要全局唯一符号名:
#define REGISTER_DRIVER(drv) \
static const driver_t _UNIQUE_(drv_inst_) = (drv); \
/* 链接段或构造函数里把 &drv_inst_x 登记进表 */
REGISTER_DRIVER(driver_uart);
REGISTER_DRIVER(driver_spi);
/* 展开为 drv_inst_0、drv_inst_1 … */
TensorFlow 的 REGISTER_KERNEL_BUILDER、部分 RTOS / 协议栈的「自动注册」宏,本质都是 __COUNTER__ + ## 这一套。
2. 头文件内联函数里的「局部」唯一变量
C 里不能在函数内用 static 做「每次调用不同名字」;用 _COUNT_ 在宏展开点固定一个展开序号,再交给第二层宏生成符号:
#define _DEFER_(x) x
#define _EXPAND_(x) _DEFER_(x)
#define FOR_EACH_ITEM(item, code) \
_EXPAND_(FOR_EACH_ITEM_IMPL(_COUNT_, item, code))
#define FOR_EACH_ITEM_IMPL(n, item, code) \
enum { _UNIQUE_(fe_idx_) = n }; \
code
/* 多次包含同一宏列表时,n 不同,避免重复定义 */
3. 与 X-Macro 配合:自动槽位 / 端点号
枚举或配置表希望「写一行宏就占一个连续编号」,又不想手写 0,1,2…:
#define EP_LIST \
X(uart) \
X(spi) \
X(i2c)
typedef enum {
#define X(name) EP_##name = _COUNT_,
EP_LIST
#undef X
EP_MAX
} ep_id_t;
USB/QMK 等固件里用 __COUNTER__ 自动分配 endpoint number 是同类思路。
4. 调试:区分多次展开的静态断言
#define STATIC_ASSERT_MSG(cond, msg) \
typedef char _UNIQUE_(static_assert_) [(cond) ? 1 : -1]
同一文件里多处断言时,错误信息不会都挤在同一个 typedef 名上(配合 _Static_assert 更推荐,但老编译器仍用此法)。
完整小例子
#include "count.h"
#define TABLE_ENTRY(name, fn) \
{ .id = _COUNT_, .name = #name, .handler = fn }
static const struct {
unsigned id;
const char *name;
void (*handler)(void);
} cmd_table[] = {
TABLE_ENTRY(reset, cmd_reset),
TABLE_ENTRY(status, cmd_status),
TABLE_ENTRY(version, cmd_version),
};
/* id 自动为 0, 1, 2,无需手写 */
可移植兜底(无 __COUNTER__ 时)
极老编译器没有 __COUNTER__ 时,只能退化为 __LINE__(同一行重复展开会撞名)或放弃自动计数、改用手写枚举:
#if defined(__COUNTER__)
# define _COUNT_ __COUNTER__
#elif defined(__LINE__)
# define _COUNT_ __LINE__
#else
# define _COUNT_ 0 /* 失去自动递增,仅作占位 */
#endif
作用域与包含关系(重要限制)
_COUNT_ 的计数范围是翻译单元(TU),不是「某个 .c 文件」或「某个头文件」各自一份。#include 只是把头文件文本插入当前 TU 的预处理流程,不会为头文件单独开计数器。
同一 .c 与其包含的头文件:共享一条计数链
/* foo.h */
#define DECLARE(x) int _UNIQUE_(obj_) = (x);
/* main.c */
#include "foo.h"
DECLARE(1); /* _COUNT_ → 0,生成 obj_0 */
int a = _COUNT_; /* → 1,不是 0 */
DECLARE(2); /* → 2,生成 obj_2 */
预处理顺序是:先展开头文件里已经出现的 _COUNT_,再展开 .c 里后面的 _COUNT_,数字连续递增,不会在进 .c 主体时重新从 0 开始。
| 场景 | 是否共享同一 _COUNT_ 序列 |
|---|---|
同一 .c 与其 #include 的头文件 |
是 |
头文件被多个不同 .c 各自包含 |
否,每个 .c 各自从 0 计 |
| 头文件有 include guard,只被粘贴一次 | 该头内 _COUNT_ 只消耗一次,但仍计入包含它的那个 .c 的序列 |
不同 .c 之间:互不影响
每个 .c(及它展开的全部头文件)是独立 TU,各自从 0 起算。不能假定 module_a.c 里用到 _COUNT_ == 3 时,module_b.c 里下一个也是 4;链接后符号名、枚举值若依赖跨文件连续编号,必须改用手写枚举或集中在一个 .c 里生成。
容易踩坑的情况
- 头文件里直接写
_COUNT_(例如在头里REGISTER_XXX()或带_UNIQUE_的静态对象):所有包含该头的.c都会在各自 TU 内先消耗一段编号;同一.c里若后面还有命令表、X-Macro 列表,得到的下标会从偏大处继续,不是从 0 起。 - 以为「只在 .c 里用就从 0 开始」:若前面 include 的公共头、驱动注册宏已经展开过
_COUNT_,.c里第一次_COUNT_可能已是 5、10 等。 - 同一宏调用里多次
_COUNT_:一次调用内写两次会得到两个不同数字;需要「只递增一次」时,应先把_COUNT_作为实参传给子宏(两层宏,见下文「使用注意」)。 - include guard 只防止头被重复粘贴;第一次包含时其中的
_COUNT_仍会正常计数。
推荐组织方式
- 定义
_COUNT_/_UNIQUE_的宏放在公共头;实际展开(REGISTER_xxx(...)、X-Macro 列表)尽量放在.c。 - 需要「从 0 连续编号」的表(命令表、端点表):在单个 .c 内集中写宏列表并展开,且避免在此之前 include 会消耗
_COUNT_的头;或把这类头放在 include 顺序靠后、仅该.c使用的私有头里。 - 头文件只提供宏定义,由调用方在
.c里写REGISTER_DRIVER(uart),计数发生在调用处,语义更清晰。
flowchart LR
subgraph TU["一个翻译单元 main.c"]
H["#include foo.h\n_DECLARE → _COUNT_=0"]
C1["main.c DECLARE(1) → 1"]
C2["main.c _COUNT_ → 2"]
H --> C1 --> C2
end
subgraph TU2["另一个翻译单元 other.c"]
O["#include foo.h 从 0 重新计"]
end
使用注意
-
不要指望
_COUNT_在运行时变化;它是编译期常量,可用于enum、数组长度、case 标签(若值为常量表达式)。 -
同一宏调用里多次写
_COUNT_会得到不同数字;若需要「一次调用只递增一次」,应先把_COUNT_传给子宏的形参,再在子宏里只用该形参拼接名字:#define REGISTER(x) REGISTER_IMPL(_COUNT_, x) #define REGISTER_IMPL(n, x) static obj_t obj_##n = (x) -
命名:项目统一用
_COUNT_前缀,避免与业务宏COUNT(x)、计数器变量count混淆;_COUNT_定义头加 include guard。
小结
_COUNT_ 通常就是 __COUNTER__ 的项目别名:每展开一次加 1,配合 ## 生成唯一标识符、自动表项下标、注册宏实例名。嵌入式里写驱动注册表、命令表、X-Macro 枚举时很实用。务必记住:同一 .c 与其 include 的头共享计数,不同 .c 互不影响;写宏时用「先捕获计数再展开」的两层宏,并确认目标链支持 __COUNTER__。
- 感谢你赐予我前进的力量

