首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从 C/C++ 分号与宏怪语法,看透 Rust 现代语言设计范式

从 C/C++ 分号与宏怪语法,看透 Rust 现代语言设计范式

作者头像
早起的鸟儿有虫吃
发布2026-05-20 13:14:11
发布2026-05-20 13:14:11
590
举报

先看一段代码

代码语言:javascript
复制

//Nginx(高性能 Web 服务器)
#define ngx_log_error(level, log, err, fmt, ...) \
do {                                              \
    if (log->log_level >= level)                  \
        ngx_log_write(log, level, err, fmt, ##__VA_ARGS__); \
} while(0)

//Redis(最流行的缓存数据库)
#define redisLog(level, fmt, ...)     \
do {                                   \
    if (level <= server.verbosity)     \
        _redisLog(level, fmt, ##__VA_ARGS__); \
} while(0)

你发现2个问题

do { ... } while(0) 就执行1次 ,这不是画蛇添足吗?

为何 Linux 内核、Redis、Nginx、FreeBSD、TCP/IP 协议栈 都采用这个方式

先说答案:这个规则是Linux 内核编码的强制要求

Linux 内核官方《CodingStyle》文档明确规定:

所有包含多条语句的宏,都必须用 do { ... } while(0) 包裹,保证宏像函数一样安全调用

  • 链接:https://www.kernel.org/doc/html/latest/process/coding-style.html
  • 章节位置:第 12 节 "Macros, Enums and RTL" 中明确规定了 do-while(0)
  • Macros with multiple statements should be enclosed in a do - while block:

引言

深耕 C/C++ 开发的人,几乎都踩过分号带来的诡异语法坑:

if-else 因多余分号编译报错、多行宏为规避语法漏洞,被迫用上反直觉的 do{...}while(0) 写法。

究其根源,是传统 C/C++ 编译器无视换行、只认分号作为语句结束标记,硬生生衍生出一堆语法补丁和编码潜规则。

而跳出老式语言的桎梏,再看 Rust 的语言设计: 编译器依靠语法结构自动断句,彻底摆脱分号作为语句结束符的束缚; 分号也被赋予全新语义,仅用来丢弃表达式返回值。

一、青铜:你知道,别人也知道的

1.1 函数有调用开销,宏原地展开 零开销

普通函数调用要:

压栈 → 跳转 → 执行 → 返回 → 出栈

哪怕再小的函数,也有栈开销、指令跳转开销

Linux 内核、驱动、嵌入式、高频底层代码:

很多代码每毫秒执行上万次,承受不起函数调用损耗。 【日志优化需求关于日志打印不要太多哦,百万千万行日志很容易出现】

宏是预处理直接文本粘贴,原地嵌代码,没有任何调用跳转,和直接写裸代码一模一样快

1.2 C 语言没有泛型函数,宏可以万能适配任意类型

C 没有 C++ 模板、没有泛型。

比如 Linux 内核经典:链表遍历

代码语言:javascript
复制
list_for_each_entry(pos, head, member)

能遍历任意结构体的链表:设备、进程、文件、驱动节

普通函数必须固定死结构体类型,写一百种结构就要写一百个遍历函数,根本不现实。

宏可以参数化类型、偏移,一个宏通吃所有结构体,函数做不到

1.3 宏能拿到调用处的 文件名、行号、函数名

日志、断言、报错定位,必须知道:

在哪一行、哪个文件、哪个函数出的错。

内置宏:

__FILE____LINE____func__

  • 写在普通函数里:拿到的是「工具函数自身」的行号,不是调用处的行号,毫无意义。
  • 写在宏里:原地展开,拿到的是你调用这一行的真实代码位置。

白银:工业级开发踩过的坑 别人知道,你不知道的 情况

do{...}while(0) 是保护语法的,

但保护不了你在宏里写 return 这种情况

原文:

代码语言:javascript
复制
Things to avoid when using macros:
使用宏时应避免的事项:

macros that affect control flow: 
影响控制流的宏:

#define FOO(x)                                  \
        do {                                    \
                if (blah(x) < 0)                \
                        return -EBUGGERED;      \
        } while (0) ?
is a very bad idea. 
It looks like a function call but exits the calling function;
 don’t break the internal parsers of those who will read the code
 
义了一个带 return 的宏,
看起来是普通函数调用,实际会直接退出调用它的外层函数
这会彻底欺骗读代码的人,破坏程序控制流,是绝对的坏写法

FOO(val); //所有人都会以为这只是一个普通的函数调用,执行完会继续往下走。 但实际:满足条件直接 return,函数直接退出,后面的代码全不执行!

do_something(); // 不在执行

save_data(); // 不在执行

do{...}while(0) 保证代码执行到 while(0)

为什么这些写 在if else

do{...}while(0) 就是为了 吃掉这个分号,让语法永远正确

不然:error: ‘else’ without a previous ‘if’

基本原理:

  • if(条件) 后面 只能跟 1 条语句
  • 想写多行代码,必须用 {} 包起来 → {} 整体算 1 条语句
  • **{} 后面绝对不能写分号 ;**!!

举例:

  1. 错误宏(用 {}
代码语言:javascript
复制
#define INIT() { a=0; b=0; }

// 你调用时写了分号!
if(flag)
    INIT();
else
    ...

#展开后(自动多加了分号,直接炸)

代码语言:javascript
复制
if(flag)
    { a=0; b=0; };  // ❌ 错误:{} + 分号
else
    ...

2. 正确宏(用 do{...}while(0)

代码语言:javascript
复制
#define INIT() do{ a=0; b=0; } while(0)

// 调用加分号完全没问题
if(flag)
    INIT();
else
    ...

展开后(语法完美)

代码语言:javascript
复制
if(flag)
    do{ a=0; b=0; } while(0);  // ✅ 正确:单语句+分号
else
    ...
go 官方设计:程序员不写分号,编译器自动加、

规则

编译器看到 换行,就自动在行尾插入分号。

代码语言:javascript
复制
package main
import "fmt"

func main() {
    a := 10          // 编译器自动加 ;
    fmt.Println(a)   // 编译器自动加 ;
}

Rust 比 Go 更彻底:

  1. 不写分号:这行是表达式,有返回值;
  2. 写分号:这行是语句,无返回值(手动丢弃)

Rust 函数返回值(最经典用法)

Rust 函数最后一行不写分号,就是函数的返回值

代码语言:javascript
复制
// 定义加法函数,返回 i32 类型
fn add(a: i32, b: i32) -> i32 {
    a + b   // 无分号 = 表达式 = 函数返回值
}


fn main() {
    let sum = add(10, 20);
    println!("{}", sum); // 输出:30
}

main 是特殊函数,强制返回空值 所以 main 里的所有代码,都必须是**语句(丢弃返回值),因此要分号

你问:为什么 C 语言宏必须用 do{...}while(0)

答案:

因为 C/C++ 分号是强制语句结束符{} 宏加分号会语法爆炸,

必须用 do-while 补坑。

Rust 没有这个破问题

  1. Rust 自动断句,不需要分号结尾
  2. Rust 用表达式 / 语句区分逻辑
  3. Rust 根本不需要 do{...}while(0) 这种丑陋的语法补丁

特性

C++

Go

Rust

分号是谁写的?

程序员强制手写

编译器自动插入

程序员按需写

分号用来干嘛?

标记语句结束

编译器内部用,人类无感

丢弃返回值(语义)

代码块 {} 后加分号

❌ 致命错误

✅ 自动处理,不会错

✅ 完全没问题

会有 else 匹配错误

✅ 高频出现(宏的根源)

❌ 绝对不会

❌ 绝对不会

需不需要 do{...}while(0)

✅ 必须用(修复宏 bug)

❌ 不需要

❌ 不需要

二、编译原理

  • 为什么 C/C++ 必须强制程序员手动书写分号 ; 作为语句结束标记,编译器才能正确解析代码?
  • 为什么 Go、Rust 无需程序员手动写分号,编译器也能自动完成语句分割?
1972 年 C 语言诞生时的极简设计
  • C/C++ 编译器的词法分析器会完全忽略所有空白字符: 换行、空格、制表符 \t 统统被当作「无意义分隔符」,不参与语法解析
  • 编译器没有任何自动识别语句结束的能力必须靠分号 ; 标记语句结束
  • {}复合语句,本身不是终结符;

go底层设计规则

Go 保留了分号作为语法分隔符,但彻底交给编译器自动处理

  • 词法分析器识别换行符,将换行作为「隐式语句结束标记」;
  • 编译器在换行前自动插入分号,程序员完全不用手写。

运行

代码语言:javascript
复制
a := 10
b := 20
// 编译器自动转为:a := 10; b := 20;

✅ 程序员不写分号,编译器内部补全,无任何语法风险。

Rust底层设计规则

Rust 从语法层面彻底废除了分号作为语句结束符

  • 编译器靠换行、大括号、语法结构自动分割语句,完全不需要分号断句;
  • 分号 ; 被重新定义为 「丢弃表达式返回值」的语义符号 **,和语句结束无关。

// 无分号 = 表达式(有返回值) fn add(a:i32, b:i32) -> i32 { a + b }

// 有分号 = 语句(丢弃返回值) let x = 10;

总结

  • C/C++ 编译器无视换行分号是唯一语句结束标记,必须手写; 空白字符无意义 → 多行宏文本替换后产生 {...}; → 语法崩溃 → 必须用 do{...}while(0) 修复。
  • Go 编译器识别换行自动插分号,程序员零负担; 无人工分号错误,无宏兼容问题。
  • Rust编译器靠语法自动断句,分号仅用于丢弃返回值; 彻底脱离分号语法束缚,是现代语言的设计范式

为什么写这篇文章

知识地图:年度目标--沟通交流

目前状态:

  • 声音太小 别人别人听不清楚
  • 哪怕听到了,根本不知道表达什么,无法编解码
  • 自己也不清楚自己表示什么

换个方式:

  • 先写清楚,然后在说清楚

最小行动:

  • 做知识搬运工,把别人最新演讲/文章 在重复写一次
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后端开发成长指南 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、青铜:你知道,别人也知道的
    • 1.1 函数有调用开销,宏原地展开 零开销
  • 1.2 C 语言没有泛型函数,宏可以万能适配任意类型
  • 1.3 宏能拿到调用处的 文件名、行号、函数名
  • 白银:工业级开发踩过的坑 别人知道,你不知道的 情况
  • 2. 正确宏(用 do{...}while(0))
  • 展开后(语法完美)
  • 二、编译原理
    • go底层设计规则
    • Rust底层设计规则
  • 总结
  • 为什么写这篇文章
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档