结构体布局、对齐、填充与联合体
为什么结构体存在填充字节?
- 假设平台要求:
char:1字节对齐uint16_t:2字节对齐uint32_t:4字节对齐
|
1 2 3 4 5 |
struct PacketHeader { char version; uint32_t length; uint16_t command; }; |
- 典型布局为:
| 成员 | 偏移 | 大小 |
version |
0 |
1 |
| 填充 | 1 |
3 |
length |
4 |
4 |
command |
8 |
2 |
| 尾部填充 | 10 |
2 |
|
1 |
sizeof(struct PacketHeader) == 12 |
用 offsetof 观察真实布局
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include <stddef.h> #include <stdint.h> #include <stdio.h> struct LayoutA { char tag; uint32_t length; uint16_t command; }; struct LayoutB { uint32_t length; uint16_t command; char tag; }; int main(void) { printf( "LayoutA: size=%zu align=%zu\n", sizeof(struct LayoutA), _Alignof(struct LayoutA)); printf( " tag=%zu length=%zu command=%zu\n", offsetof(struct LayoutA, tag), offsetof(struct LayoutA, length), offsetof(struct LayoutA, command)); printf( "LayoutB: size=%zu align=%zu\n", sizeof(struct LayoutB), _Alignof(struct LayoutB)); printf( " length=%zu command=%zu tag=%zu\n", offsetof(struct LayoutB, length), offsetof(struct LayoutB, command), offsetof(struct LayoutB, tag)); return 0; } |
|
1 2 3 4 5 6 |
// 常见 64 位平台可能输出 LayoutA: size=12 align=4 tag=0 length=4 command=8 LayoutB: size=8 align=4 length=0 command=4 tag=6 |
- 将大对齐成员放在前面,经常能减少填充,但不是标准保证的优化规则
标准保证和不保证什么?
C标准保证:- 成员按照声明顺序排列
- 后声明成员的地址位于先声明成员之后
- 结构体开头不会在第一个成员之前插入填充
- 成员之间及结构体末尾可以存在填充
offsetof可以取得普通成员的偏移
- C 标准不保证:
int一定是4字节- 同一个结构体在不同编译器中布局相同
- 不同编译选项下布局相同
- 枚举和位域的具体表示相同
- 结构体布局等于网络数据格式
不要直接发送结构体
- 危险代码
|
1 2 3 4 5 6 7 |
struct PacketHeader header = { .version = 1, .length = 100, .command = 2 }; send(socket, &header, sizeof header, 0); |
- 主要问题:
- 可能发送填充字节
- 填充字节可能包含未初始化数据
length和command使用主机字节序- 不同平台的结构体大小可能不同
- 接收端直接转换指针可能不满足对齐要求
- 编译器打包选项可能改变
ABI - 即使两端当前都是
x86,也只是碰巧兼容,不是可靠协议设计
更可靠的方式:显式编码
- 定义固定的 6 字节协议头
|
1 2 3 |
version: 1 byte command: 1 byte length: 4 bytes,大端序 |
- 这里协议长度永远是 6 字节,不依赖
sizeof(struct)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <stdint.h> void encode_header( unsigned char output[6], uint8_t version, uint8_t command, uint32_t length) { output[0] = version; output[1] = command; output[2] = (unsigned char)(length >> 24); output[3] = (unsigned char)(length >> 16); output[4] = (unsigned char)(length >> 8); output[5] = (unsigned char)length; } uint32_t decode_length(const unsigned char input[6]) { return ((uint32_t)input[2] << 24) | ((uint32_t)input[3] << 16) | ((uint32_t)input[4] << 8) | (uint32_t)input[5]; } |
为什么不直接使用 #pragma pack(1)?
- 一些编译器支持
|
1 2 3 4 5 6 7 8 |
#pragma pack(push, 1) struct PackedHeader { uint8_t command; uint32_t length; }; #pragma pack(pop) |
|
1 2 3 4 5 6 |
// 或者 GCC 扩展 struct __attribute__((packed)) PackedHeader { uint8_t command; uint32_t length; }; |
- 但它们不能解决所有问题:
- 都不是可移植标准
C - 仍然存在大小端问题
- 未对齐访问可能降低性能
- 某些
CPU上未对齐访问可能直接异常 - 取得打包成员地址可能产生未对齐指针
- 编译器之间语法和行为可能不同
- 都不是可移植标准
结构体不能用 == 比较
C不支持
|
1 2 3 4 5 6 7 8 9 10 |
struct Point { int x; int y; }; struct Point first; struct Point second; if (first == second) { /* 编译错误 */ } |
- 也不应该简单使用
|
1 |
memcmp(&first, &second, sizeof first); |
- 因为:
- 填充字节可能不同
- 某些类型可能存在多种对象表示
- 语义相等不等于字节表示相等
- 应该逐成员比较:
|
1 2 3 4 5 6 7 |
bool point_equal( const struct Point* first, const struct Point* second) { return first->x == second->x && first->y == second->y; } |
|
1 2 3 |
// 结构体赋值则是合法的 second = first; |
联合体:多个类型共享同一块存储
|
1 2 3 4 |
union Number { int integer; double real; }; |
- 所有成员从同一个地址开始
|
1 2 3 4 5 |
union Number number; // 写入 real 会覆盖原来的 integer 表示 number.integer = 10; number.real = 3.14; |
- 联合体大小通常至少能容纳最大成员,并满足最严格成员的对齐要求
|
1 |
sizeof(union Number) >= sizeof(double) |
- 推荐用法:带标签联合体
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
enum ValueKind { VALUE_INTEGER, VALUE_REAL }; struct Value { enum ValueKind kind; union { int integer; double real; } data; }; |
- 设置整数
- 这种模式相当于手动实现简化版的
C++std::variant
- 这种模式相当于手动实现简化版的
|
1 2 3 4 |
struct Value value = { .kind = VALUE_INTEGER, .data.integer = 42 }; |
|
1 2 3 4 5 |
// 读取时根据标签判断 if (value.kind == VALUE_INTEGER) { printf("%d\n", value.data.integer); } |
|
1 2 3 4 5 6 7 8 |
// 必须保持不变量 kind == VALUE_INTEGER → 读取 data.integer kind == VALUE_REAL → 读取 data.real // 不要随意写入一个成员、读取另一个成员做类型转换 // C 对这种表示重解释有特殊规则,但结果依赖具体表示,甚至可能产生陷阱表示,不适合作为可移植代码 // 需要复制位表示时优先使用 memcpy |
枚举不是固定宽度协议字段
C17
|
1 2 3 4 |
enum Command { CMD_LOGIN = 1, CMD_LOGOUT = 2 }; |
|
1 2 3 4 |
// 枚举对象的具体大小和兼容整数类型与实现有关,因此不要直接发送 enum Command command; send(socket, &command, sizeof command, 0); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 协议中应明确使用固定宽度整数: uint16_t wire_command; // 解析后再验证并转换: enum Command command; switch (wire_command) { case CMD_LOGIN: command = CMD_LOGIN; break; case CMD_LOGOUT: command = CMD_LOGOUT; break; default: /* 非法协议值 */ break; } |
C23支持固定底层类型枚举,例如:
|
1 2 3 4 |
enum Command : uint16_t { CMD_LOGIN = 1, CMD_LOGOUT = 2 }; |
位域
|
1 2 3 4 5 |
struct Flags { unsigned int connected : 1; unsigned int encrypted : 1; unsigned int priority : 3; }; |
- 位域可以紧凑保存标志,但其许多细节由实现决定:
- 位的分配顺序
- 是否跨越存储单元
- 对齐方式
- 整体大小
- 普通
int位域是否有符号 - 与硬件或网络位序的对应关系
|
1 2 3 |
// 而且不能获取位域地址 &flags.connected; /* 错误 */ |
- 所以位域适合:
- 当前程序内部的紧凑状态
- 编译器、
CPU、ABI都完全受控的硬件映射
作用域、存储期、链接与头文件
四种主要存储期
- 自动存储期
- 进入代码块时对象开始存在,离开代码块时生命周期结束
- 每次函数调用都有独立对象
|
1 2 3 4 |
void process(void) { int count = 0; } |
|
1 2 3 4 5 6 7 8 |
// 未初始化的自动变量具有不确定值 void process(void) { int count; printf("%d\n", count); /* 未定义行为 */ } |
- 静态存储期
- 文件作用域对象以及
static局部对象 - 它们在整个程序运行期间存在,并且在程序启动前完成初始化
- 文件作用域对象以及
|
1 2 3 4 5 6 |
int global_count; void process(void) { static int call_count; } |
|
1 2 3 |
// 未显式初始化时会被零初始化 static int call_count; /* 初始值为 0 */ |
- 分配存储期
- 通过动态内存函数获得
- 从成功分配开始,到对应的
free()或成功改变它的realloc()结束
|
1 |
int* data = malloc(100 * sizeof *data); |
|
1 2 3 4 5 6 7 8 9 10 |
// 指针变量和动态对象具有不同生命周期 int* create_value(void) { int* pointer = malloc(sizeof *pointer); return pointer; } // 局部变量 pointer 在返回时消失,但它指向的动态对象仍然存在 |
- 线程存储期
C11提供_Thread_local- 每个线程都有独立对象,该对象在线程执行期间存在
- 它适合线程本地统计或错误上下文,但不等于原子变量,也不能解决共享状态同步
|
1 |
static _Thread_local unsigned long request_count; |
作用域
- 文件作用域
- 在所有函数外声明
- 名字从声明位置一直可见到当前源文件末尾
|
1 2 3 |
int global_count; void server_start(void); |
- 块作用域
|
1 2 3 4 5 6 7 8 9 10 |
void example(void) { int outer = 1; { int inner = 2; } /* inner 在这里不可见 */ } |
- 名字遮蔽
- 局部的
value遮蔽了文件作用域的value - 全局对象依然存在,只是当前没有
C语法可以像C++的::value那样直接指定它
- 局部的
|
1 2 3 4 5 6 7 8 |
int value = 10; void example(void) { int value = 20; printf("%d\n", value); /* 20 */ } |
链接
- 链接主要回答
server.c中的名字和main.c中的同名名字,是否表示同一个实体?
- 外部链接
- 文件作用域普通函数和非
static对象默认具有外部链接 - 其他翻译单元可以通过声明使用它们
- 文件作用域普通函数和非
|
1 2 3 4 5 |
int connection_count; void server_start(void) { } |
- 内部链接
- 文件作用域使用
static
- 文件作用域使用
|
1 2 3 4 5 6 7 |
// 这些名字只在当前翻译单元中可见 static int connection_count; static void close_all_connections(void) { } |
- 无链接
- 普通局部变量通常没有链接
|
1 2 3 4 5 6 |
// 即使另一个函数中也有 count,它们仍是两个完全不同的对象 void process(void) { int count = 0; } |
static 的两个主要含义
- 文件作用域的
staticclient_count具有静态存储期- 名字具有内部链接
remove_client只在当前.c文件中可见
|
1 2 3 |
static int client_count; static void remove_client(int socket); |
- 块作用域的
static- 作用域只在函数体内
- 没有外部链接
- 生命周期覆盖整个程序
- 只初始化一次
- 多次调用共享同一个对象
|
1 2 3 4 5 6 |
int next_id(void) { static int current_id; return ++current_id; } |
|
1 2 |
int first = next_id(); /* 1 */ int second = next_id(); /* 2 */ |
extern 通常只声明,不定义
- 头文件中
- 表示某个翻译单元中存在一个具有外部链接的
connection_count
- 表示某个翻译单元中存在一个具有外部链接的
|
1 |
extern int connection_count; |
- 在一个
.c文件中提供唯一定义
|
1 |
int connection_count = 0; |
- 注意
|
1 2 3 |
// 虽然有 extern,但因为存在初始化器,它仍然是定义 extern int connection_count = 0; |
文件作用域的 int count
- 下面不是普通的
extern声明
|
1 2 |
// 它是一个暂定定义。整个程序最终必须满足外部定义规则 int connection_count; |
|
1 2 3 4 5 6 |
// 不要把它放在头文件里 /* 错误的 connection.h */ int connection_count; // 如果两个 `.c` 文件都包含该头文件,就会在多个翻译单元中产生定义 |
- 正确结构
|
1 2 |
/* connection.h */ extern int connection_count; |
|
1 2 |
/* connection.c */ int connection_count = 0; |
|
1 2 3 |
// 更好的设计通常是不公开可写全局变量,而是提供函数: size_t connection_count(void); |
完整的多文件模块
- 头文件包含:
- 类型定义
- 函数声明
- 必需头文件
include guard
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 没有包含可写全局变量定义 #ifndef COUNTER_H #define COUNTER_H #include <stdbool.h> #include <stddef.h> struct Counter { size_t value; }; void counter_init(struct Counter* counter); bool counter_increment(struct Counter* counter); size_t counter_value(const struct Counter* counter); size_t counter_total_increments(void); #endif |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include "counter.h" #include <stdint.h> static size_t total_increments; static bool can_increment(const struct Counter* counter) { return counter != NULL && counter->value < SIZE_MAX; } void counter_init(struct Counter* counter) { if (counter == NULL) { return; } *counter = (struct Counter){0}; } bool counter_increment(struct Counter* counter) { if (!can_increment(counter)) { return false; } ++counter->value; ++total_increments; return true; } size_t counter_value(const struct Counter* counter) { return counter == NULL ? 0 : counter->value; } size_t counter_total_increments(void) { return total_increments; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include "counter.h" #include <stdio.h> int main(void) { struct Counter first; struct Counter second; counter_init(&first); counter_init(&second); counter_increment(&first); counter_increment(&first); counter_increment(&second); printf("first=%zu\n", counter_value(&first)); printf("second=%zu\n", counter_value(&second)); printf("total=%zu\n", counter_total_increments()); return 0; } |
头文件应该放什么?
- 适合放在头文件:
- 公共类型
- 公共函数声明
- 宏和常量接口
- 必需的前置声明
- 小型
static inline函数
- 通常不应放:
- 普通函数定义
- 可写全局对象定义
- 只供当前
.c使用的辅助函数 - 不必要的内部结构
- 私有实现所需的头文件
- 头文件保护
|
1 2 3 4 5 6 |
#ifndef CONNECTION_H #define CONNECTION_H /* 内容 */ #endif |
|
1 2 3 |
// 或 #pragma once // 但 #pragma once 不是 ISO C 标准的一部分,虽然主流编译器普遍支持 |
头文件中的 static
- 如果在头文件中写
|
1 2 3 4 |
static int helper(void) { return 42; } |
- 每个包含它的
.c文件都会获得一份自己的内部链接函数,可能造成:- 多份代码
- 未使用函数警告
- 每个翻译单元拥有不同的函数地址
- 调试和覆盖统计更复杂
- 对于很小的头文件辅助函数,通常使用
|
1 2 3 4 |
static inline int minimum(int first, int second) { return first < second ? first : second; } |
static inline表示每个翻译单元拥有自己的内部链接版本- 不要把它简单等同于“编译器一定内联”;是否真正展开仍由优化器决定
C的普通inline链接规则和C++不完全相同,现阶段优先使用- 普通公共函数:头文件声明,
.c文件定义 - 小型头文件辅助函数:
static inline
- 普通公共函数:头文件声明,
C 与 C++ 的 const 全局变量差异
- 在
C中- 放在文件作用域时,默认具有外部链接
|
1 |
const int maximum_clients = 1024; |
- 如果它只供当前
.c使用
|
1 |
static const int maximum_clients = 1024; |
- 如果作为公共对象
|
1 2 |
/* limits.h */ extern const int maximum_clients; |
|
1 2 |
/* limits.c */ const int maximum_clients = 1024; |
使用不透明类型隐藏实现
- 公共头文件
|
1 2 3 4 5 6 7 8 9 10 11 12 |
#ifndef CONNECTION_H #define CONNECTION_H typedef struct Connection Connection; Connection* connection_create(int socket); void connection_destroy(Connection* connection); int connection_socket(const Connection* connection); #endif |
|
1 2 3 |
// 头文件只声明 struct Connection; // 调用者不知道其成员,因此不能直接访问 |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 真实结构放在 connection.c #include "connection.h" #include <stdlib.h> struct Connection { int socket; unsigned char* receive_buffer; size_t used; size_t capacity; }; |
- 好处:
- 隐藏内部实现
- 减少调用者重新编译
- 防止直接破坏内部不变量
- 更容易保持
ABI稳定
预处理器、宏、条件编译与 _Generic
概述
- 预处理器工作在真正编译之前,主要处理
token和文本,不理解C类型、作用域、生命周期与求值规则 - 预处理器从
C89就存在C99增加可变参数宏C11增加_GenericC23又加入__VA_OPT__、#elifdef、#warning等能力
查看预处理结果
|
1 2 3 |
#define BUFFER_SIZE 1024 int buffer[BUFFER_SIZE]; |
|
1 2 3 |
// 预处理后大致变为 int buffer[1024]; |
- 只输出宏展开、头文件包含和条件编译之后的结果,不执行正式编译
|
1 2 |
// 可以使用 GCC 查看 gcc -std=c17 -E example.c |
对象式宏
|
1 2 3 |
#define BUFFER_SIZE 1024 #define SERVER_PORT 8899 #define PROJECT_NAME "easy_server" |
- 预处理器只是替换
token
|
1 2 3 4 5 |
unsigned char buffer[BUFFER_SIZE]; // 转换为 unsigned char buffer[1024]; |
- 宏没有类型
|
1 2 3 4 |
#define SERVER_PORT 8899 // 如果需要带类型的只读对象,可以写 static const unsigned short server_port = 8899; |
- 如果需要
C17整数常量表达式
|
1 2 3 |
enum { BUFFER_SIZE = 1024 }; |
- 常见选择
| 需求 | 推荐 |
| 条件编译开关 | 宏 |
| 整数常量表达式 | enum 或宏 |
| 有地址、有明确类型的对象 | static const |
| 小型计算函数 | static inline |
C23 编译期对象 |
constexpr |
函数式宏必须加括号
- 错误定义
|
1 |
#define DOUBLE(x) x + x |
|
1 2 3 4 5 6 |
// 调用 int result = DOUBLE(2) * 3; // 展开成 int result = 2 + 2 * 3; // 结果是 8,不是预期的 12 |
- 改进
- 宏参数和完整结果通常都应加括号
|
1 |
#define DOUBLE(x) ((x) + (x)) |
- 但是这个宏仍然不安全
- 在
C17中,两次修改之间没有确定的先后关系,产生未定义行为 - 括号只能解决运算优先级,不能解决参数被重复求值
- 在
|
1 2 3 4 5 |
int index = 1; int result = DOUBLE(index++); // 展开成 int result = ((index++) + (index++)); |
优先使用 static inline
|
1 2 3 4 |
static inline int double_int(int value) { return value + value; } |
|
1 2 |
// `index++` 只执行一次 int result = double_int(index++); |
inline不保证编译器一定内联- 它主要提供函数定义和链接方面的语义
- 优化器可以内联普通函数,也可能拒绝内联
inline函数
- 对于头文件里的小工具,常用
|
1 2 3 4 5 6 |
static inline size_t minimum_size( size_t first, size_t second) { return first < second ? first : second; } |
- 与宏相比,它具有:
- 类型检查
- 参数只求值一次
- 正常作用域
- 更好的调试体验
- 不会因缺少括号改变表达式含义
多语句宏使用 do while (0)
- 错误写法
|
1 2 3 |
#define DISCONNECT(socket) \ close(socket); \ socket = -1 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 调用 if (failed) DISCONNECT(socket); else retry(); // 展开后结构会被破坏 if (failed) close(socket); socket = -1; else retry(); |
- 正确模式
- 宏只应在函数确实无法表达需求时使用
|
1 2 3 4 5 |
#define DISCONNECT(socket) \ do { \ close(socket); \ (socket) = -1; \ } while (0) |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 不过这个操作更适合写成函数 static inline void disconnect_socket(int* socket) { if (socket == NULL || *socket < 0) { return; } close(*socket); *socket = -1; } |
可变参数日志宏
C99增加__VA_ARGS__- 宏会自动记录调用位置
|
1 2 3 4 5 6 7 8 |
#include <stdio.h> #define LOG_ERROR(...) \ do { \ fprintf(stderr, "[%s:%d] ", __FILE__, __LINE__);\ fprintf(stderr, __VA_ARGS__); \ fputc('\n', stderr); \ } while (0) |
|
1 2 3 4 |
// 使用 LOG_ERROR("connection closed"); LOG_ERROR("recv failed, code=%d", error_code); |
- 注意:
- 格式字符串与参数仍需匹配
- 宏本身不提供类型安全
__FILE__和__LINE__是预定义宏__func__是函数内部的预定义标识符,可以记录函数名
- 生产项目中,还应考虑线程安全、日志级别和异步日志生命周期
条件编译
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#ifndef NET_DEBUG #define NET_DEBUG 0 #endif #if NET_DEBUG #define NET_TRACE(...) \ do { \ fprintf(stderr, __VA_ARGS__);\ fputc('\n', stderr); \ } while (0) #else #define NET_TRACE(...) ((void)0) #endif |
- 编译时启用
|
1 |
gcc -DNET_DEBUG=1 server.c -o server |
- 禁用
|
1 |
gcc -DNET_DEBUG=0 server.c -o server |
- 这里要区分
|
1 2 3 |
// 只检查宏是否定义 #ifdef NET_DEBUG // 即使-DNET_DEBUG=0,它仍然被定义,#ifdef 分支仍会启用 |
- 如果宏表示布尔开关,应写
|
1 |
#if NET_DEBUG |
- 禁用宏可能移除参数求值
- 因此不要把程序必须发生的副作用放进调试宏参数
|
1 2 3 4 5 |
NET_TRACE("result=%d", calculate_result()); // 当日志关闭时,整个调用被替换成 ((void)0); // calculate_result() 根本不会执行 |
跨平台条件编译
- 可以把
Windows/POSIX差异集中到一个头文件
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#ifndef SOCKET_PLATFORM_H #define SOCKET_PLATFORM_H #if defined(_WIN32) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include <winsock2.h> typedef SOCKET socket_handle_t; #define INVALID_SOCKET_HANDLE INVALID_SOCKET static inline int socket_handle_close(socket_handle_t socket) { return closesocket(socket); } #else #include <unistd.h> typedef int socket_handle_t; #define INVALID_SOCKET_HANDLE (-1) static inline int socket_handle_close(socket_handle_t socket) { return close(socket); } #endif #endif |
#if 不理解C类型
- 不能这样写
- 预处理器不知道
sizeof和C类型
- 预处理器不知道
|
1 2 |
#if sizeof(int) == 4 #endif |
- 应该使用编译期断言
|
1 2 3 |
_Static_assert( sizeof(int) >= 4, "int must be at least 32 bits"); |
|
1 2 3 4 5 |
// C23 可以写 static_assert( sizeof(int) >= 4, "int must be at least 32 bits"); |
- 条件编译适合判断预处理宏
|
1 2 3 4 5 |
#if defined(_WIN32) #elif defined(__linux__) #else #error Unsupported platform #endif |
字符串化与token拼接
- 字符串化
#
|
1 2 3 4 5 6 |
#define STRINGIFY_IMPL(value) #value #define STRINGIFY(value) STRINGIFY_IMPL(value) #define SERVER_PORT 8899 printf("%s\n", STRINGIFY(SERVER_PORT)); |
|
1 2 3 4 5 6 |
// 两层宏会输出 8899 // 如果只使用一层 #define STRINGIFY(value) #value // STRINGIFY(SERVER_PORT)可能得到字符串 "SERVER_PORT",因为 # 会阻止参数先完成普通宏展开 |
Token拼接##
|
1 2 3 4 5 6 |
#define COMMAND(name) CMD_##name enum { COMMAND(LOGIN), COMMAND(LOGOUT) }; |
|
1 2 3 4 5 6 |
// 展开为 enum { CMD_LOGIN, CMD_LOGOUT }; |
C11 _Generic
C没有函数重载,C11的_Generic可以根据表达式类型在编译期选择表达式
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#include <stdio.h> static void print_int(int value) { printf("int: %d\n", value); } static void print_long(long value) { printf("long: %ld\n", value); } static void print_double(double value) { printf("double: %.2f\n", value); } #define PRINT_VALUE(value) \ _Generic((value), \ int: print_int, \ long: print_long, \ double: print_double \ )(value) int main(void) { int first = 10; long second = 20L; double third = 3.14; PRINT_VALUE(first); PRINT_VALUE(second); PRINT_VALUE(third); return 0; } |
- 过程是:
_Generic((value), ...)检查value的类型- 选择对应函数
- 再调用选中的函数
- 控制表达式本身不会被求值,所以这里真正的函数参数只求值一次
- 可以增加默认分支
|
1 2 3 4 5 6 7 |
#define PRINT_VALUE(value) \ _Generic((value), \ int: print_int, \ long: print_long, \ double: print_double, \ default: print_unknown \ )(value) |
_Generic的限制:- 是精确的
C类型选择,不是C++模板 - 不支持用户定义的隐式重载体系
- 未选择分支也必须能够正常解析
- 宏展开错误通常较难阅读
- 复杂接口可能降低可维护性
- 是精确的
- 适合用于:
- 类型安全的数学包装
- 日志或打印接口
- 不同整数类型的辅助函数
- 模拟少量函数重载
一个相对安全的泛型最小值
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
static inline int minimum_int(int first, int second) { { return first < second ? first : second; } static inline long minimum_long(long first, long second) { return first < second ? first : second; } static inline double minimum_double( double first, double second) { return first < second ? first : second; } #define MINIMUM(first, second) \ _Generic(((first) + (second)), \ int: minimum_int, \ long: minimum_long, \ double: minimum_double \ )((first), (second)) |
- 相比传统宏
MINIMUM最终调用真实函数,所以每个实参只求值一次
|
1 |
#define BAD_MINIMUM(a, b) ((a) < (b) ? (a) : (b)) |
- 但函数实参的求值顺序仍未指定,因此不要这样写
- 即使没有重复求值,两个参数之间相互依赖也可能产生未定义或未指定行为
|
1 |
MINIMUM(index++, index); |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ COM组件_303/07
- ♥ C标准库_cctype12/15
- ♥ 关于创建文件以及umask的问题06/09
- ♥ X86_64汇编学习记述四08/09
- ♥ ASIO:学习一08/01
- ♥ STL_slist08/28
热评文章
- C标准库_cctype 0
- 关于创建文件以及umask的问题 0
- Zlib记述:一 0
- C相关记述一 0
- C++_volatile 0
- C_学习一 0