Skip to content

警告旗标进阶:从 -Wall -Wextra 到 -Werror -Wpedantic ​

引言:编译器想帮咱们,是咱们没让它开口 ​

很多人对编译器的态度是“只要没报 error,能编过就行”,至于那一屏飘过去的 warning:,压根没当回事。等程序在运行时崩出一个诡异的值,回头一看,那个 bug 编译器早就用一条 warning 跟咱们说过,只是咱们没开那个旗标、或者没看它。

这一章咱们把 gcc 最常用的几个警告旗标挨个真跑一遍,看它们各自抓什么、又各自漏什么。读完您会明白为什么本课程把 -Wall -Wextra 当标配、把 -Werror 在 CI 里当硬门、还要拿 -Wpedantic 区分“能编过”和“真的是标准 C”,以及为什么哪怕全开,编译器还是会漏,那部分得留给第 11 章的 sanitizer。

最容易骗到人的一条先摆出来:-Wall 并不是“所有警告”。Wall 里的 all 是历史遗留的营销,它只开“一组 gcc 认为值得默认开的警告”,还有大量警告(有些相当要命)得靠 -Wextra 和专门的 -Wxxx 才会亮。下面咱们就用真例子拆开看。

-Wall 不够:-Wextra 补上了什么 ​

咱们先看一个 -Wall 完全没声音、但 -Wextra 一抓一个准的例子:

C
int f(int x, int y) {
    return x;
} /* y 根本没用上 */

咱们只开 -Wall,编译器一声不吭:

终端
$ gcc -std=c11 -Wall -c d1.c -o d1.o
$                          ← 空,没任何 warning

咱们加上 -Wextra,立刻现形:

终端
$ gcc -std=c11 -Wall -Wextra -c d1.c -o d1.o
d1.c: In function 'f':
d1.c:1:18: warning: unused parameter 'y' [-Wunused-parameter]

“未使用的参数”算不上致命,但 -Wextra 还会替咱们抓很多 -Wall 漏掉的东西(比如某些空语句、签名不一致的松散检查)。所以只开 -Wall 是不够的,本课程从第 1 章起就一直是 -Wall -Wextra 一起开。

咱们再看一个 -Wall 本来就能抓、而且是真坑的典型:把 == 写成 =:

C
int g(int x) {
    if (x = 5)
        return 1;
    return 0;
} /* 本来想写 x == 5 */
终端
$ gcc -std=c11 -Wall -c d2.c -o d2.o
d2.c:1:20: warning: suggest parentheses around assignment used as truth value [-Wparentheses]

-Wparentheses(包含在 -Wall 里)识破了“条件里写的是赋值 = 而不是比较 ==”,给咱们一句提醒。这种笔误要是没被旗标拦下,运行时表现为“循环永远进 / 永远不进”,能让人查到怀疑人生。

-Werror:让警告直接挡住构建 ​

咱们光报警告,还是会忽略。-Werror 把所有警告升级成错误,有 warning 就别想编过:

终端
$ gcc -std=c11 -Wall -Werror -c d2.c -o d2.o
d2.c:1:20: error: suggest parentheses around assignment used as truth value [-Werror=parentheses]

咱们注意原来的 warning: 变成了 error:,编译直接失败。本仓库的 CI 就是这么用 -Werror 当硬门的:提交的代码要是有 warning,CI 当场红给咱们看。

不过 -Werror 也有它的麻烦:升级编译器之后,新版 gcc 经常会新增一类警告,咱们原来干干净净的代码突然就开始报新 warning、被 -Werror 一挡,构建莫名其妙就红了。所以工程里常见的做法是“整体开 -Werror,但对个别暂时没法处理的警告用 -Wno-xxx 局部豁免”(比如 -Wno-unused-parameter),或者用 -Werror=xxx 只把特定警告升级成错误、其余还停留在 warning。这是个“严”与“不折腾”之间的工程取舍。

-Wpedantic:“能编过”不等于“标准 C” ​

gcc 默认是挺纵容的,哪怕咱们指定了 -std=c89,它还是会悄悄放行很多 C89 之后才有的、或者根本是 GNU 扩展的写法。咱们来看 long long(C99 才有,C89 里没有):

C
long long x = 0; /* C99 起,C89 没有这个类型 */
终端
$ gcc -std=c89 -c d4.c -o d4.o
$                          ← 静默通过!gcc 没吭声
$ gcc -std=c89 -Wpedantic -c d4.c -o d4.o
d4.c:1:6: warning: ISO C90 does not support 'long long' [-Wlong-long]

只写 -std=c89,gcc 默默接受了 long long,咱们以为自己写的是“严格 C89”,其实混进了 C99 的东西,换一个较真的编译器(或加 -pedantic-errors)就过不去。加上 -Wpedantic,它才如实告诉咱们“这玩意儿 ISO C90 不支持”。

这一点和第 1 章那条“-std=gnuXX 默默开 GNU 扩展”是一回事:gcc 的默认姿态是“能让咱们过就让咱们过”,而 -Wpedantic 就是逼它“严格按标准挑刺”。本课程示例一律显式 -std=cXX,需要严格时配 -Wpedantic,目的是让代码换编译器(尤其换到 clang、或者未来的新 gcc)时不被默认纵容坑到。

-Wconversion:连 -Wextra 都不抓的数据丢失 ​

下面这种“把大类型塞进小类型”的隐式截断,-Wall -Wextra 全开了也未必搭理咱们:

C
int narrow(long big) {
    return big;
} /* long 截成 int,可能丢数据 */
终端
$ gcc -std=c11 -Wall -Wextra -c d5.c -o d5.o
$                          ← 空,没 warning
$ gcc -std=c11 -Wall -Wextra -Wconversion -c d5.c -o d5.o
d5.c:1:31: warning: conversion from 'long int' to 'int' may change value [-Wconversion]

-Wconversion 才会提醒咱们“这次 long 缩成 int 可能丢数据”。这类截断在嵌入式/协议解析里特别常见(把 uint32_t 塞进 uint16_t 之类),是潜伏的 bug 源。-Wconversion 比较激进、在一些老代码上会刷一大片,所以没进标配,但咱们写新代码时值得一开。

-Wuninitialized:警告是有盲区的,别太信它 ​

读未初始化的变量是 UB,-Wuninitialized(-Wall 含)本来该抓。咱们先试简单的直读,它确实抓得到,而且这台 gcc 16 上 -O0、-O2 都抓:

C
int use_u(void) {
    int u;
    return u;
} /* 直接读未初始化 */
终端
$ gcc -std=c11 -Wall -O0 -c d6.c -o d6.o
d6.c:1:33: warning: 'u' is used uninitialized [-Wuninitialized]
$ gcc -std=c11 -Wall -O2 -c d6.c -o d6.o
d6.c:1:33: warning: 'u' is used uninitialized [-Wuninitialized]

但咱们把读取藏进一个条件分支,gcc 16 在 -O0 和 -O2 都没报:

C
int cond(int flag) {
    int u;
    if (flag)
        u = 10; /* flag==0 时 u 没被赋值 */
    return u;   /* 这条路径上读的是未初始化的 u */
}
终端
$ gcc -std=c11 -Wall -O0 -c d7.c -o d7.o   ← 空
$ gcc -std=c11 -Wall -O2 -c d7.c -o d7.o   ← 也空!两个都没报

这就是 -Wuninitialized(其实也是所有警告旗标)的真相:它做的是 best-effort 的静态分析,能力有限、有明显盲区。-Wuninitialized 尤其依赖优化器的数据流分析(所以老资料会说它“只在开了 -O 才灵”),但即便开了 -O2,这种“某条路径才未初始化”的情形它照样漏。结论很硬:“没报警告”绝不等于“没有 UB”,这正是为什么第 11 章咱们要上 sanitizer,靠运行期的真插桩来兜这个底。

本课程的标配旗标集 ​

咱们把前面几条收拢成一套可以直接抄进 Makefile/CMake 的标配。日常编译至少 -std=c11 -Wall -Wextra。CI 里加 -Werror 当硬门,必要时 -Wpedantic 查严格标准符合性。写新库时再叠加 -Wconversion、-Wshadow 这些更细的。这套不是教条,是踩过“只开 -Wall 结果漏了一片”的坑之后攒出来的下限。

小结 ​

把这一章收拢:-Wall 名字唬人但不是“所有警告”,-Wextra 是它的必备搭档。-Werror 把警告变成 CI 的硬门,但升级编译器时新警告会让它突然变红,要会用 -Wno-xxx 局部豁免。-Wpedantic 区分“能编过”和“真的是标准 C”,逼 gcc 别再纵容非标写法。-Wconversion 抓 -Wall/-Wextra 都不管的数据截断。而 -Wuninitialized 即便开了 -O2 也有盲区,条件分支里的未初始化读取它一声不吭,所以警告只是 best-effort,真正的兜底是第 11 章的 sanitizer,咱们别拿“没 warning”当“没 bug”。

下一章咱们把 -std 各档、-O0..-O3 优化级别、-g 调试信息一起系统讲清楚,顺便解释为什么这一章好几个旗标(尤其 -Wuninitialized)的行为会和 -O 级别绑在一起。

参考资源 ​

  • man gcc 的 Warning Options 一节(-Wall/-Wextra 各自到底开哪些子警告的完整清单)
  • GCC 手册:-Wpedantic / -pedantic-errors、-Wconversion、-Wshadow、-Werror=...、-Wno-...
  • ISO/IEC 9899:long long 自 C99 引入(-Wlong-long 在 C89 模式下报警的依据)
  • 本仓库 .github/workflows/ci.yml(-Werror 作为 CI 硬门的真实写法,第 17 章逐行拆)

87bb5f2 · 87bb5f2 · 2026-09-21