先看一段代码:加-Wall -Wextra编译时它一条警告都不报,可换成-Wconversion就立刻报出conversion from 'long long int' to 'int' may change value:一个悄悄丢精度的隐式窄化。这说明两件事:警告是编译器送给你的免费代码审查,但默认的-Wall并不覆盖全部。这篇不凭记忆复述警告,而是把每条警告的真实原文抓下来贴出来,再讲清它为什么危险、怎么修。
1. 引子:为什么「我编译没警告」不等于「代码没问题」
新手对警告的第一印象是「噪音」:一堆unused variable有什么好报的?直到线上出事,才发现它们真正在说的是另外几件事。
同一份代码,不同起手式能看见的「问题」(示意,不是计数) g++ -std=c++17 demo.cpp → 基本什么都看不见 g++ -std=c++17 -Wall -Wextra demo.cpp → 常见的笔误、类型不匹配 g++ -std=c++17 -Wall -Wextra -Wpedantic \ -Wconversion -Wshadow -Wold-style-cast ... → 再加上窄化、遮蔽、C 风格转换 g++ ... -Werror → 上面每一条都变成编译失败 问题一直都在,只是没人问编译器。-Wall里的 “all” 是个历史遗留的名字,并不是字面意义的「全部」。GCC 手册对它的定义是「所有关于某些用户会觉得可疑的构造的警告」:名字里带 all,实际是一个精选集合。像-Wconversion、-Wshadow、-Wold-style-cast这些噪音较大、误报较多的警告都不在-Wall里,想用必须显式打开。所以「我们开了-Wall」这句话的信息量,比它听起来小得多。
官方文档:GCC — Warning Options
2. 起手的编译选项
# 日常开发的基线:开满常用警告,并把警告当错误g++-std=c++17-Wall-Wextra-Wpedantic-Wconversion-Wshadow-Wold-style-cast\-Werror-O2demo.cpp-odemo# 只想要报告、不想被 -Werror 打断排查时:临时摘掉 -Werrorg++-std=c++17-Wall-Wextra-Wconversion-Wshadow-O2demo.cpp-odemo下面所有警告原文都来自 gcc-13.2.0,-std=c++17 -O2,并显式带上该条警告需要的开关。为了省地方只保留关键行,正文会说明每条的实际开关。
3. 十三条高频警告逐条解读
3.1-Wunused-variable:变量定义了却没用
触发片段:
voidlog(intvalue){inttmp=value;// tmp 从头到尾没被用过}$ g++ -std=c++17 -O2 -Wall -Wextra -c demo.cpp prog.cc: In function 'void log(int)': prog.cc:3:9: warning: unused variable 'tmp' [-Wunused-variable] 3 | int tmp = value; | ^~~为什么危险:它几乎总是「写了一半的代码」的残留 —— 本来打算记日志、打算参与计算,结果忘了。真正的危险不是这个变量本身,而是它暗示的意图和实现不一致:你以为变了,其实没变。
怎么修:删掉。如果参数是为了保持接口兼容而故意不用,用[[maybe_unused]]明确表达这个意图,而不是靠不写参数名(那反而会让签名更难读)。
官方文档:[[maybe_unused]] — cppreference
3.2-Wunused-parameter:参数收了却没用
intarea(intwidth,intheight){returnwidth*width;// height 没参与计算}prog.cc: In function 'int area(int, int)': prog.cc:2:25: warning: unused parameter 'height' [-Wunused-parameter] 2 | int area(int width, int height) { | ~~~~^~~~~~为什么危险:参数是函数契约的一部分。声明了height却不用,调用方会以为它在起作用,这类 bug 在重命名、重构时特别常见。
怎么修:要么真的用上(很可能公式漏了项),要么删掉;确实必须保留签名时(比如回调接口),加[[maybe_unused]]。
3.3-Wsign-compare:有符号/无符号比较
#include<vector>boolhas(conststd::vector<int>&v,intidx){returnidx<v.size();// int 与 size_t 比较}prog.cc: In function 'bool has(std::vector<int>&, int)': prog.cc:4:16: warning: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'} [-Wsign-compare] 4 | return idx < v.size(); | ~~~~^~~~~~~~~~为什么危险:符号不一致时,有符号数会被转成无符号数,-1于是变成4294967295,idx为负时这个比较会得出完全相反的结果。
怎么修:让两边同类型。循环/比较一律用std::size_t,或者用范围 for 让下标消失。
3.4-Wreorder:初始化列表的顺序和声明顺序不一致
structPoint{intx;inty;Point(inta,intb):y(b),x(a){}// 写成 y 在前};prog.cc: In constructor 'Point::Point(int, int)': prog.cc:4:9: warning: 'Point::y' will be initialized after [-Wreorder] 4 | int y; | ^ prog.cc:3:9: warning: 'int Point::x' [-Wreorder] 3 | int x; | ^ prog.cc:5:5: warning: when initialized here [-Wreorder] 5 | Point(int a, int b) : y(b), x(a) {} | ^~~~~为什么危险:C++永远按成员声明顺序初始化,与你写在初始化列表里的顺序无关。这个例子恰好无害(两个int互不影响),但如果成员之间有依赖,比如size_依赖data_,「看起来」的顺序和真实顺序不一致就会读到未初始化的值。
怎么修:把初始化列表按声明顺序排列。现在就改,哪怕当前无害,因为后续加成员时依赖关系会变。
3.5-Wreturn-type:函数有返回类型却可能不返回值
intclassify(intn){if(n>0){return1;}}// n <= 0 时没有 returnprog.cc: In function 'int classify(int)': prog.cc:4:1: warning: control reaches end of non-void function [-Wreturn-type] 4 | } | ^为什么危险:走到函数结尾却没有返回值是 UB:调用方会拿到一个不确定的值。这与「返回 0」完全不同,编译器也不是在抱怨你漏了注释。
怎么修:每个分支都返回,或者末尾补一个显式的兜底返回(return -1;),并且让函数只有一个return出口时更好审查。
3.6-Wshadow:变量遮蔽(需要显式开-Wshadow)
inttotal=0;voidadd(inttotal){// 参数遮蔽了全局 total(void)total;}$ g++ -std=c++17 -O2 -Wall -Wextra -Wshadow -c demo.cpp prog.cc: In function 'void add(int)': prog.cc:3:14: warning: declaration of 'total' shadows a global declaration [-Wshadow] 3 | void add(int total) { | ~~~~^~~~~ prog.cc:2:5: note: shadowed declaration is here 2 | int total = 0; | ^~~~~为什么危险:遮蔽之后,函数体里那个名字指向的是参数,全局的total一行都没改。「以为在改全局」是这类 bug 的典型形态,而且肉眼极难发现。
怎么修:改名。参数、局部变量、成员变量各有清晰的命名约定(比如成员带_后缀),是成本最低的方案。
3.7-Wmaybe-uninitialized:只在优化开启时才报(需要-O1及以上)
intcompute(intflag){intresult;// 未初始化if(flag>0){result=flag;}returnresult;// flag <= 0 时读到不确定值}同一个文件,换优化级别结果完全不同:
$ g++ -std=c++17 -O0 -Wall -Wextra -c demo.cpp # 无任何输出 $ g++ -std=c++17 -O2 -Wall -Wextra -c demo.cpp prog.cc: In function 'int compute(int)': prog.cc:5:12: warning: 'result' may be used uninitialized [-Wmaybe-uninitialized] 5 | return result; | ^~~~~~ prog.cc:3:9: note: 'result' was declared here 3 | int result; | ^~~~~~这是最需要警惕的一条警告:-O0下它一条都不报,-O2下才报:因为它在优化器做完数据流分析之后才能给出结论。所以**「我本地编译没警告」完全不代表没问题**,很可能只是没开优化。
为什么危险:读取未初始化的变量是 UB,值不确定且每次运行可能不同。更麻烦的是它可能漏报:数据流一复杂(跨函数、经过指针、循环里赋值),分析就跟不上了。
怎么修:声明即初始化,用int result{};起手。Core Guidelines 的 ES.20「总是初始化对象」就是针对这一类问题。
3.8-Wconversion:隐式窄化(不在-Wall里,必须显式开)
intshrink(longlongvalue){returnvalue;// long long → int,可能丢高位}$ g++ -std=c++17 -O2 -Wall -Wextra -Wconversion -c demo.cpp prog.cc: In function 'int shrink(long long int)': prog.cc:3:12: warning: conversion from 'long long int' to 'int' may change value [-Wconversion] 3 | return value; | ^~~~~ $ g++ -std=c++17 -O2 -Wall -Wextra -c demo.cpp # 同样的代码,无任何输出为什么危险:long long到int的截断是静默的,超出范围时结果由实现定义(不是 UB,但一样是「算出个你没想要的值」)。这类窄化在数值处理代码里是精度事故的常见来源。
怎么修:用static_cast<int>(...)把意图显式写出来,并在转换前确认范围(可用 C++20 的std::in_range<int>(value))。注意-Wconversion误报相对多,建议先单独开一段时间摸清噪音来源,再纳入-Werror。
3.9-Wold-style-cast:C 风格强制转换(需要显式开)
inttruncate(doubled){return(int)d;// C 风格转换}$ g++ -std=c++17 -O2 -Wall -Wextra -Wold-style-cast -c demo.cpp prog.cc: In function 'int truncate(double)': prog.cc:3:17: warning: use of old-style cast to 'int' [-Wold-style-cast] 3 | return (int)d; | ^ | ------ | static_cast<int> (d)为什么危险:C 风格转换会依次尝试const_cast、static_cast、reinterpret_cast三者中最先能编译过的那个:也就是说它可能悄悄做了一次reinterpret_cast,而你只想要一次数值转换。编译器在这里甚至直接把建议写进了提示:static_cast<int> (d)。
怎么修:改用命名转换。要数值转换就用static_cast,一眼能看出意图,也能被 grep 审查。
3.10-Wnon-virtual-dtor:基类有虚函数却无虚析构
structBase{virtualvoidrun();~Base(){}// 非虚析构};$ g++ -std=c++17 -O2 -Wall -Wextra -Wnon-virtual-dtor -c demo.cpp prog.cc:2:8: warning: 'struct Base' has virtual functions and accessible non-virtual destructor [-Wnon-virtual-dtor] 2 | struct Base { | ^~~~为什么危险:通过基类指针delete一个派生类对象时,如果析构函数不是虚的,派生类的析构函数不会被调用,派生类持有的资源直接泄漏(这是 UB 还是「只析构一半」,取决于标准版本与具体写法,但无论如何都是错的)。
怎么修:有虚函数的基类,析构函数要么virtual(且 public),要么 protected 且非虚。用override标记派生类的重写,让编译器帮你核对签名。
3.11-Wsizeof-array-argument:对数组形参取 sizeof
#include<cstddef>std::size_tcount(intdata[]){returnsizeof(data)/sizeof(data[0]);// 想算元素个数}$ g++ -std=c++17 -O2 -Wall -Wextra -c demo.cpp prog.cc: In function 'std::size_t count(int*)': prog.cc:4:19: warning: 'sizeof' on array function parameter 'data' will return size of 'int*' [-Wsizeof-array-argument] 4 | return sizeof(data) / sizeof(data[0]); | ~^~~~~ prog.cc:3:23: note: declared here 3 | std::size_t count(int data[]) { | ~~~~^~~~~~为什么危险:形参里的int data[]会被调整成int* data(注意 GCC 在提示里已经把它打印成count(int*)),于是sizeof(data)得到的是指针大小(8),不是数组大小。这个错误在很多平台上「看起来能跑」,因为它恰好等于元素个数上的一些巧合值。
怎么修:显式把长度传进来,或者收窄参数类型(传std::array/std::vector),或者用「数组引用」模板保留长度信息。
3.12-Wdeprecated-declarations:用了标记为弃用的接口
[[deprecated("use compute_v2 instead")]]intcompute_v1(intx){returnx;}intcall(intx){returncompute_v1(x);}prog.cc: In function 'int call(int)': prog.cc:3:36: warning: 'int compute_v1(int)' is deprecated: use compute_v2 instead [-Wdeprecated-declarations] 3 | int call(int x) { return compute_v1(x); } | ~~~~~~~~~~^~~ prog.cc:2:46: note: declared here 2 | [[deprecated("use compute_v2 instead")]] int compute_v1(int x) { return x; } | ^~~~~~~~~~为什么危险:弃用标记是团队协作的时间表:这个接口将来会消失。忽略它等于把技术债留给未来的自己。
怎么修:按提示迁移到新接口。这条警告还有个额外价值:[[deprecated("原因")]]里的那句原因会直接出现在警告文本里,是写给未来的自己看的,值得认真写。
3.13-Wswitch:switch 漏了枚举值
enumclassColor{Red,Green,Blue};intcode(Color c){switch(c){caseColor::Red:return0;caseColor::Green:return1;}return-1;}prog.cc: In function 'int code(Color)': prog.cc:4:12: warning: enumeration value 'Blue' not handled in switch [-Wswitch] 4 | switch (c) { | ^为什么危险:每个case通常对应一种业务分支,漏一个就是少处理一种情况。-Wswitch只在switch没有default分支时才报:因为有了default它就无法判断你是否「有意兜底」。
怎么修:列出所有枚举值,故意不写default,让编译器替你查漏。将来给枚举加成员时,这条警告会立刻告诉你哪些switch需要更新:这是枚举加default的唯一坏处。
4. 修法示例:把上面几条一次清零
4.1 消掉-Wunused-parameter与-Wsign-compare
#include<cstddef>#include<iostream>#include<vector>// 确实不用的参数:用 [[maybe_unused]] 表达意图,而不是删掉名字intarea(intwidth,[[maybe_unused]]intheight){returnwidth*width;}intmain(){conststd::vector<int>v{10,20,30};// 循环变量与 size() 同为无符号,-Wsign-compare 消失for(std::size_t i=0;i<v.size();++i){std::cout<<v[i]<<' ';}std::cout<<'\n';std::cout<<"area(3, 4) = "<<area(3,4)<<'\n';}10 20 30 area(3, 4) = 94.2 消掉-Wreorder:初始化列表按声明顺序
#include<iostream>#include<string>#include<utility>classTimer{public:// 声明顺序是 start_ 在前、name_ 在后;初始化列表保持同样顺序Timer(intstart,std::string name):start_(start),name_(std::move(name)){}voiddump()const{std::cout<<name_<<" start="<<start_<<'\n';}private:intstart_;std::string name_;};intmain(){constTimer t{10,"render"};t.dump();}render start=104.3 消掉-Wswitch:列全枚举值
#include<iostream>enumclassColor{Red,Green,Blue};constchar*to_text(Color c){switch(c){// 覆盖全部枚举值,故意不写 defaultcaseColor::Red:return"red";caseColor::Green:return"green";caseColor::Blue:return"blue";}return"unknown";// 兜底,同时消掉 -Wreturn-type}intmain(){for(constColor c:{Color::Red,Color::Green,Color::Blue}){std::cout<<to_text(c)<<' ';}std::cout<<'\n';}red green blue4.4 消掉-Wsizeof-array-argument:把长度显式传进来
#include<cstddef>#include<iostream>// 形参写成指针 + 长度,语义清晰,也不会误判 sizeofintsum(constint*data,std::size_t count){inttotal=0;for(std::size_t i=0;i<count;++i){total+=data[i];}returntotal;}// 想保留「数组长度」信息又不手写长度:用数组引用的模板版本template<std::size_t N>intsum(constint(&data)[N]){returnsum(data,N);}intmain(){constintraw[4]={1,2,3,4};std::cout<<"sum(raw, 4) = "<<sum(raw,4)<<'\n';std::cout<<"sum(raw) = "<<sum(raw)<<'\n';}sum(raw, 4) = 10 sum(raw) = 105. 警告速查表:谁抓什么问题,严重到什么程度
| 警告 | 抓什么问题 | 严重程度 | 是否在-Wall | 建议处置 |
|---|---|---|---|---|
-Wunused-variable | 定义了没用 | 低 | 是 | 删掉,或[[maybe_unused]] |
-Wunused-parameter | 参数收了没用 | 低 | 是 | 用上 / 删掉 /[[maybe_unused]] |
-Wsign-compare | 有符号无符号比较 | 高 | 是 | 统一类型,循环用std::size_t |
-Wreorder | 初始化顺序与声明顺序不符 | 高 | 是 | 按声明顺序写初始化列表 |
-Wreturn-type | 有返回值却可能不返回 | 高 | 是 | 补全所有分支 |
-Wshadow | 变量遮蔽外层同名变量 | 中 | 否,需显式开 | 改名,约定命名后缀 |
-Wmaybe-uninitialized | 可能读到未初始化值 | 高 | 是(需-O1及以上) | 声明即初始化{} |
-Wconversion | 隐式窄化丢精度 | 中 | 否,需显式开 | 显式static_cast+ 范围检查 |
-Wold-style-cast | C 风格强制转换 | 中 | 否,需显式开 | 改命名转换 |
-Wnon-virtual-dtor | 多态基类非虚析构 | 高 | 是 | 虚析构或 protected 析构 |
-Wsizeof-array-argument | 对数组形参取 sizeof | 中 | 是 | 显式传长度 / 用容器 |
-Wdeprecated-declarations | 用了弃用接口 | 中 | 是 | 按提示迁移 |
-Wswitch | switch 漏枚举值 | 高 | 是 | 列全枚举值,不写default |
-Wsequence-point | 同一表达式多次改一个变量 | 高 | 是 | 拆成两条语句 |
「严重程度」的口径:高= 会导致结果错误、崩溃或未定义行为;中= 会导致可维护性差或未来的精度问题;低= 主要是整洁度。
6.-Werror的取舍:要清零,但别让它无缘无故打断构建
把警告当错误能保证「警告清零」,这是-Werror最大的价值:没有它,警告会缓慢堆积到没人看。但它有两个必须知道的代价:
- 编译器版本差异会打断构建。新版本 GCC 常常新增警告,或让既有分析更精确,于是昨天还干净的代码今天编译失败。典型场景是 CI 用了滚动更新的镜像,本地却是老版本。
-Werror会放大误报的伤害。-Wconversion、-Wshadow这类噪音较大的警告一旦进了-Werror,正常的代码重构会被反复挡住。
务实的分层做法:
日常开发 / CI 的推荐配置 ──────────────────────────────────────────────────────────────── 必开且 -Werror: -Wall -Wextra ← 集合稳定、误报少 建议开、先观测: -Wpedantic -Wshadow ← 摸清噪音来源后再收 单独一轮、不阻断: -Wconversion -Wold-style-cast -Wdouble-promotion 兜底: -Werror 之外再加 -Wno-error=<噪音项> 做例外更稳的做法是把警告集合钉在具体编译器版本上:CI 镜像固定版本,升级编译器时单独开一个改动,把新增警告一次性处理完,而不是让它突然打断别人的构建。
7. 把警告当文档用
每条警告文本末尾的方括号里就是它的开关名(例如[-Wsign-compare]),这就是查文档的入口。GCC 手册的 Warning Options 一节按字母序列出了全部警告,每条都写明「它检测什么」「由谁启用」,比搜索引擎靠谱:
# 想让某条警告只报不拦,或者明确关掉它(关之前先确认原因)g++-std=c++17-Wall-Wextra-Wno-sign-compare demo.cpp-odemo# 想看清某条警告到底覆盖了什么,查手册对应条目# 例:https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wsign-compare官方文档:GCC — Warning Options | Clang — Diagnostic flags(与 GCC 兼容的警告名)
8. 延伸阅读
- GCC — Warning Options:全部
-W开关的权威列表,含「这条警告由谁启用」。 - cppreference — Compiler support / 各标准库的诊断宏:配合
-Wconversion做范围检查时的工具。 - C++ Core Guidelines:ES 系列几乎每条都能对应到一条编译器警告。
- cppreference — std::size_t:
-Wsign-compare的根源,循环变量类型的正确选择。 - Clang — Diagnostics Reference:Clang 侧同名警告的完整索引,跨编译器排查时对得上号。
本知识库内的相关篇目:
- 《悬垂指针与悬垂引用:生命周期错误的排查套路》 —— 指针没变、地址没变、代码没改,读出来的值却从正确变成了垃圾——这就是悬垂(dangling)。
- 《未定义行为(UB)清单:这些代码为什么「能跑」却不合法》 —— 有符号溢出、越界访问、读未初始化变量……这些代码在你机器上「能跑」
- 《用 Compiler Explorer 看汇编:从 C++ 到指令》 —— Compiler Explorer(godbolt.org)怎么用
9. 一句话总结
警告是编译器免费送的代码审查,态度应该是开满并清零;-Wall里的 “all” 并不是全部(-Wconversion、-Wshadow、-Wold-style-cast都要显式开),而-Wmaybe-uninitialized更提醒了一件事:它只在开启优化时才报,所以「没警告」从来不等于「没问题」。落地建议是-Wall -Wextra -Werror作为基线、噪音大的警告先单独观测再纳入,看到不认识的警告就去 GCC 手册按方括号里的名字查条目:那才是它真正的说明书。