前言
所谓「奇技淫巧」,通常指那些短小、巧妙、初见让人拍案的写法:用宏生成枚举和字符串表、用位运算判断 2 的幂、用offsetof从成员反推外层结构体、用do { } while (0)把多语句宏包成一个块。它们在系统编程、嵌入式、Linux 内核和大量 C 项目里确实存在,也确实好用。
但「技巧」和「好代码」不是同义词。这里面混着三类东西:标准保证的可移植写法(如offsetof、字符串字面量拼接)、编译器扩展(如 GNU 的语句表达式、__builtin_expect、__attribute__((packed)))、以及看似聪明实则未定义行为的写法(如对同一个变量做 XOR 交换、对有符号数左移溢出、用packed结构体做未对齐访问)。前两类可以用,但要清楚边界;第三类应当避免。
本文按预处理、编译期与类型、位运算、常见误用四块整理,每个技巧都标注它属于标准还是扩展,并给出替代方案。代码以 C11 和 C++17 为参照,涉及 C++20 的地方单独注明。
一、预处理与编译期技巧
多语句宏用do { } while (0)包起来。如果直接写成{ ... },在if (x) MACRO(); else ...中会因分号出现悬空 else;如果写成多条语句不加包裹,则只能用在单语句位置。do { } while (0)在语法上是一条语句,末尾的分号自然衔接。
// 适用于 C11
#include <stdio.h>
#define LOG_IF(cond, msg) do { \
if (cond) { \
fprintf(stderr, "%s\n", (msg)); \
} \
} while (0)
void check(int value) {
if (value < 0)
LOG_IF(value < -100, "far below zero");
else
LOG_IF(value == 0, "zero");
}宏参数必须加括号,并且要意识到参数可能被求值多次。#define MAX(a, b) ((a) > (b) ? (a) : (b))在MAX(i++, j++)上会自增两次,且求值次数依赖比较结果。C++ 里应改用std::max或函数模板;C 里可以写static inline函数。
X 宏(X-macro)批量生成代码。把「名字和值」的清单定义成一个宏,再用不同的展开宏生成枚举、字符串表、解析函数。这样新增一项只需改一处,避免枚举与字符串表不同步。
// 适用于 C11
#define STATUS_LIST(X) \
X(OK, 0) \
X(IO_ERROR, 1) \
X(BAD_ARG, 2)
#define AS_ENUM(name, code) STATUS_##name = code,
typedef enum { STATUS_LIST(AS_ENUM) } Status;
#define AS_TEXT(name, code) [code] = #name,
static const char* const status_text[] = { STATUS_LIST(AS_TEXT) };
/* status_text[STATUS_IO_ERROR] 是 "IO_ERROR" */#name是字符串化运算符,把宏参数变成字符串字面量;##是记号拼接,把STATUS_和OK拼成STATUS_OK。这两个运算符只能出现在宏定义里。注意[code] = ...是 C99 的指定初始化器,C++ 不支持对数组这样写,所以这段是 C 代码。
字符串字面量相邻拼接。"abc" "def"在翻译阶段就合并为"abcdef",这是标准行为,常用来把长字符串或宏生成的片段拼在一起。它发生在编译期,没有运行开销。
编译期与类型相关的技巧
求数组长度。sizeof(a) / sizeof(a[0])是最常见的宏,但它只对真正的数组有效;一旦数组退化成指针(例如作为函数参数),结果就是指针大小除以元素大小。C++17 可以用std::size(a),它对内置数组返回正确长度,对容器调用size()。
// 适用于 C11
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
int values[16];
/* ARRAY_SIZE(values) == 16 */offsetof与 container_of。offsetof(type, member)是标准宏(头文件<stddef.h>),返回成员在标准布局类型中的字节偏移。Linux 内核常用它实现「从成员指针反推外层对象指针」:
// 适用于 C11;container_of 是常见扩展写法,非标准保证
#include <stddef.h>
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
struct item {
int id;
int value;
};
/* 已知 &obj.value,反推 struct item * */这个技巧依赖成员确实位于该结构体内部,且指针来自真实对象;对空指针、错误类型或非标准布局类型使用,行为不受标准保证。C++ 里offsetof只对标准布局类型(standard-layout)有定义,对多态类型使用属于条件支持,不要依赖。
指定初始化器。C99 允许struct Point p = { .y = 2, .x = 1 };,顺序任意;C++20 起支持指定初始化,但要求按成员声明顺序书写,且不能跳过前面的成员再回头。
// 需要 C++20
struct Point { int x; int y; };
Point p{ .x = 1, .y = 2 }; // C++20 合法
// Point q{ .y = 2, .x = 1 }; // 顺序不符,C++20 不允许C++17 及以前,GCC/Clang 把指定初始化作为扩展接受,MSVC 的支持情况随版本变化,不能当成可移植写法。
static_assert。C++11 起是关键字,用于编译期断言;C11 用_Static_assert。它比#if更适合检查类型大小、对齐和常量表达式,错误信息直接指向源码。
二、位运算与整数技巧
位运算技巧的前提是只用无符号类型。有符号数的左移溢出、负数右移、以及把位运算结果当作有符号值使用,都可能落入实现定义行为或 UB。
| 技巧 | 写法 | 前提与说明 |
|---|
| 最低位的 1 | x & (0u - x) | x为无符号;结果为 2 的幂 |
| 清除最低位的 1 | x & (x - 1u) | 常用于统计 1 的个数 |
| 判断 2 的幂 | x != 0 && (x & (x - 1u)) == 0 | 无符号;0 不是 2 的幂 |
| 统计 1 的个数 | C++20std::popcount(x) | 头文件<bit>,需要 C++20 |
| 末尾 0 的个数 | C++20std::countr_zero(x) | 需要 C++20;x为 0 时结果有定义 |
| 交换两数 | 临时变量或std::swap | XOR 交换有别名陷阱,不推荐 |
C++20 的<bit>把这些操作标准化了,比自己手写循环或依赖__builtin_popcount更清晰:
// 需要 C++20
#include <bit>
#include <cstdint>
bool power_of_two(std::uint32_t x) {
return x != 0 && (x & (x - 1u)) == 0;
}
int bits(std::uint32_t x) {
return std::popcount(x); // 返回 int
}C++17 及以前,GCC/Clang 提供__builtin_popcount、__builtin_ctz,MSVC 有__popcnt等内建函数,名字和可用性因编译器而异,使用时要加平台判断。
XOR 交换看起来最「奇技」:
/* 仅用于说明别名陷阱,不推荐使用 */
a ^= b;
b ^= a;
a ^= b;当a和b是同一个对象(比如对数组同一元素操作,或通过两个指向同一地址的指针)时,第一行就把值变成 0,最终两个都变 0。它也不比一次临时变量赋值更快,现代编译器对普通交换的优化已经很成熟。这类技巧应当直接放弃。
三、容易被误用的「技巧」
语句表达式({ ... })。GNU C 扩展,允许把多条语句包成一个表达式,常被用来写「类型安全的 MAX 宏」:
/* GNU 扩展,非标准 C/C++ */
#define MAX(a, b) ({ \
__typeof__(a) _a = (a); \
__typeof__(b) _b = (b); \
_a > _b ? _a : _b; \
})它能避免重复求值,但__typeof__和语句表达式都不是 C++ 标准,MSVC 不支持。C++ 里用函数模板,C 里用static inline函数,代价是类型受限于函数签名,但可移植性远好于宏。
__attribute__((packed))。让编译器取消结构体填充,常用于解析二进制协议。代价是成员可能未对齐,直接访问未对齐地址在某些架构上是 UB 或触发总线错误,只能逐字节memcpy出来再解释。协议解析更稳妥的做法是按字节读取并显式组装整数。
变长数组(VLA)。int n = f(); int a[n];是 C99 的特性,C11 起变为可选,C++ 标准从未支持。GCC 的 C++ 前端把它当扩展,栈空间不足时会直接崩溃。需要运行期长度就用malloc/free(C)或std::vector(C++)。
#include <bits/stdc++.h>。GCC 特有的「万能头」,把标准库头文件一次性包含进来。它不可移植、显著拖慢编译,且掩盖了真实的依赖关系。逐个包含需要的头文件才是正确做法。
gets()。C11 已从标准中移除,因为它无法限制输入长度,存在缓冲区溢出风险。用fgets并检查换行,或 C++ 的std::getline。
四、完整示例:用 X 宏维护状态表
下面这段 C 代码把状态码、枚举和可打印文本绑在同一份清单上,新增状态只改STATUS_LIST一处。它同时演示了do { } while (0)与ARRAY_SIZE。
// 适用于 C11
#include <stddef.h>
#include <stdio.h>
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
#define STATUS_LIST(X) \
X(OK, 0) \
X(IO_ERROR, 1) \
X(BAD_ARG, 2)
#define AS_ENUM(name, code) STATUS_##name = code,
typedef enum { STATUS_LIST(AS_ENUM) } Status;
#define AS_TEXT(name, code) [code] = #name,
static const char* const status_text[] = { STATUS_LIST(AS_TEXT) };
#define CHECK(cond, st) do { \
if (!(cond)) { \
fprintf(stderr, "check failed: %s\n", \
status_text[(st)]); \
return (st); \
} \
} while (0)
static Status parse_positive(int value) {
CHECK(value > 0, STATUS_BAD_ARG);
return STATUS_OK;
}
int main(void) {
Status st = parse_positive(-1);
printf("%s (%zu states)\n", status_text[st], ARRAY_SIZE(status_text));
return 0;
}CHECK里的return (st);决定了它只能用在返回Status的函数中,这是宏的典型局限:它看不见类型,也无法参与重载。C++ 里更推荐抛异常或返回std::optional/std::expected(后者需要 C++23)。
常见坑点
- ❌
#define SQUARE(x) x * x,调用SQUARE(a + 1)得到a + 1 * a + 1。✅ 每个参数和整体都加括号:((x) * (x));更好的做法是用inline函数或模板。 - ❌
MAX(i++, j++)依赖宏只求值一次。✅ 宏会重复求值;改用std::max或先自增到临时变量。 - ❌ 对同一个变量做 XOR 交换,或两个指针指向同一元素。✅ 用临时变量或
std::swap,语义清晰且没有别名陷阱。 - ❌ 用
x << 31或有符号溢出「构造」常量。✅ 有符号左移溢出是 UB;用无符号类型和1u << n,并确保n小于位宽。 - ❌ 在 C++ 中对多态类型使用
offsetof或container_of。✅ 只对标准布局类型使用;多态类型的布局不由标准保证。 - ❌ 用
__attribute__((packed))结构体直接访问成员。✅ 未对齐访问可能出错;按字节读取后用memcpy组装,或使用序列化库。 - ❌ 在 C++ 里使用 VLA 或语句表达式并认为可移植。✅ 它们是编译器扩展;改用
std::vector和内联函数/模板。 - ❌ 继续使用
gets、无边界sprintf、strcpy。✅ 用fgets、snprintf、std::string,把长度约束写进接口。
总结
| 技巧 | 标准归属 | 建议 |
|---|
do { } while (0)宏 | C/C++ 标准写法 | 可用,注意参数括号 |
| X 宏 | 标准预处理能力 | 可用,适合枚举与字符串同步 |
offsetof/ container_of | offsetof标准;container_of 扩展 | 仅标准布局类型,谨慎使用 |
| 指定初始化器 | C99;C++20(顺序受限) | 按标准版本使用 |
| 位运算技巧 | 标准(限无符号) | 可用,优先<bit>(C++20) |
语句表达式、__typeof__ | GNU 扩展 | 换成函数/模板 |
packed、VLA、万能头 | 扩展或非标准 | 避免或严格隔离 |
奇技淫巧的真正价值,是让你看懂别人代码里的这些写法,并在合适的时候用更清晰的等价物替换它们。判断标准很简单:这个写法是标准保证的,还是某个编译器的扩展?它会不会重复求值、依赖未定义行为、或者牺牲可移植性?能回答这三个问题,再用不迟。