【发布时间】:2020-01-10 13:21:03
【问题描述】:
我经常听到在编译 C 和 C++ 程序时我应该“始终启用编译器警告”。为什么这是必要的?我怎么做?
有时我也听说我应该“将警告视为错误”。我是不是该?我该怎么做?
【问题讨论】:
标签: c++ c warnings compiler-warnings c++-faq
我经常听到在编译 C 和 C++ 程序时我应该“始终启用编译器警告”。为什么这是必要的?我怎么做?
有时我也听说我应该“将警告视为错误”。我是不是该?我该怎么做?
【问题讨论】:
标签: c++ c warnings compiler-warnings c++-faq
【讨论】:
-Wall and -Werror was designed by code-refactoring maniacs for themselves. [需要引用]
-Wall 和-Werror 的情况下进行编译,它只是在问这是否是个好主意。哪一个,从你的最后一句话,听起来你说的是。
出于某些原因,C++ 中的编译器警告非常有用。
它允许向您显示您可能在哪里犯了可能影响您的操作最终结果的错误。例如,如果您没有初始化变量,或者如果您使用“=”而不是“==”(这里只是示例)
它还允许向您显示您的代码不符合 C++ 标准的地方。它很有用,因为如果代码符合实际标准,例如,将很容易将代码移动到其他平台。
一般来说,警告对于向您显示代码中的错误非常有用,这些错误可能会影响您的算法结果或防止用户使用您的程序时出现一些错误。
【讨论】:
我曾在一家制造电子测试设备的大型(财富 50 强)公司工作。
我小组的核心产品是一个MFC 程序,多年来,它产生了数以百计的警告。几乎在所有情况下都被忽略了。
当出现错误时,这是一场可怕的噩梦。
在那之后,我很幸运地被聘为一家新创业公司的第一位开发人员。
我鼓励所有构建都采用“无警告”政策,将编译器警告级别设置为非常嘈杂。
我们的做法是使用#pragma warning - 推送/禁用/弹出开发人员确信确实没问题的代码,以及调试级别的日志语句,以防万一。
这种做法对我们很有效。
【讨论】:
#pragma warning 不仅抑制警告,它还具有双重目的,即快速与其他程序员沟通某些事情是故意的而不是偶然的,并充当搜索标签,用于在出现问题时快速定位潜在问题区域但修复错误/警告没有解决它。
编译器警告是你的朋友
我在旧的Fortran 77 系统上工作。编译器告诉我有价值的事情:子程序调用中的参数数据类型不匹配,如果我有一个未使用的变量或子程序参数,则在将值设置到变量之前使用局部变量。这些几乎都是错误。
当我的代码编译干净时,97% 的代码都能正常工作。与我一起工作的另一个人在编译时关闭所有警告,在调试器中花费数小时或数天,然后请我提供帮助。我只是编译他的代码并显示警告并告诉他要修复什么。
【讨论】:
将警告视为错误只有一个问题:当您使用来自其他来源(例如 Microsoft 库、开源项目)的代码时,它们 strong> 没有做好他们的工作,编译他们的代码会产生 吨 的警告。
我总是编写我的代码,这样它就不会产生任何警告或错误,并清理它直到它编译而不会产生任何无关的噪音。我必须处理的垃圾让我感到震惊,当我必须构建一个大型项目并观看一连串警告时,我感到震惊,编译应该只宣布它处理了哪些文件。
我还记录了我的代码,因为我知道软件的真正生命周期成本主要来自维护,而不是最初编写它,但那是另一回事...
【讨论】:
-Wall,而你使用-Wall -Wextra。
忽略警告意味着您留下了草率的代码,这不仅可能会在将来给其他人带来问题,而且还会使您不太注意重要的编译消息。
编译器输出越多,人们就越不会注意到或打扰。越清洁越好。这也意味着你知道你在做什么。警告是非常不专业、粗心和冒险的。
【讨论】:
您绝对应该启用编译器警告,因为某些编译器不善于报告一些常见的编程错误,包括:
所以这些功能可以被检测和报告,只是通常不是默认的;因此必须通过编译器选项明确请求此功能。
【讨论】:
作为使用遗留嵌入式 C 代码的人,启用编译器警告有助于在提出修复建议时显示许多弱点和需要调查的领域。在 GCC 中,使用 -Wall 和 -Wextra 甚至 -Wshadow 变得至关重要。我不会一一列举,但我会列出一些弹出的有助于显示代码问题的问题。
这很容易指出未完成的工作和可能未使用所有传递变量的区域,这可能是一个问题。让我们看一个可能触发这个的简单函数:
int foo(int a, int b)
{
int c = 0;
if (a > 0)
{
return a;
}
return 0;
}
在没有-Wall 或-Wextra 的情况下编译它不会返回任何问题。 -Wall 会告诉你 c 从未被使用过:
foo.c:在函数'foo'中:
foo.c:9:20: 警告:未使用的变量‘c’ [-Wunused-变量]
-Wextra 还会告诉你你的参数b 没有做任何事情:
foo.c:在函数'foo'中:
foo.c:9:20: 警告:未使用的变量‘c’ [-Wunused-变量]
foo.c:7:20: 警告:未使用的参数 ‘b’ [-Wunused-parameter] int foo(int a, int b)
这有点难,直到使用-Wshadow 才出现。让我们将上面的示例修改为仅添加,但是恰好有一个与本地同名的全局,这在尝试同时使用两者时会引起很多混乱。
int c = 7;
int foo(int a, int b)
{
int c = a + b;
return c;
}
-Wshadow 开启后,很容易发现这个问题。
foo.c:11:9: 警告:'c' 的声明会影响全局声明 [-Wshadow]
foo.c:1:5: 注意:阴影声明在这里
这在 GCC 中不需要任何额外的标志,但它仍然是过去问题的根源。一个尝试打印数据但出现格式错误的简单函数可能如下所示:
void foo(const char * str)
{
printf("str = %d\n", str);
}
这不会打印字符串,因为格式化标志是错误的,GCC 会很高兴地告诉你这可能不是你想要的:
foo.c:在函数'foo'中:
foo.c:10:12:警告:格式“%d”需要 “int”类型的参数,但参数 2 的类型为“const char *” [-Wformat=]
这些只是编译器可以为您仔细检查的众多内容中的三项。还有很多其他人喜欢使用其他人指出的未初始化变量。
【讨论】:
possible loss of precision”和“comparison between signed and unsigned”警告。我发现很难掌握有多少“程序员”忽略了这些(事实上,我不确定为什么它们不是错误)
sizeof 的结果是无符号的,但默认的整数类型是有符号的。 sizeof 结果类型 size_t 通常用于与类型大小相关的任何内容,例如对齐或数组/容器元素计数,而整数通常用作“int”,除非另有说明必需的”。考虑到有多少人因此被教导使用int 来迭代他们的容器(比较int 和size_t),让它成为一个错误会破坏一切。 ;P
这是对 C 的特定答案,以及为什么这对 C 比对其他任何事物都重要。
#include <stdio.h>
int main()
{
FILE *fp = "some string";
}
此代码编译时带有警告。地球上几乎所有其他语言(除了汇编语言)中的错误和应该是错误都是 C 中的警告。C 中的警告几乎总是伪装的错误。警告应该被修复,而不是被禁止。
使用 GCC,我们以 gcc -Wall -Werror 执行此操作。
这也是一些微软非安全 API 警告的高调性的原因。大多数 C 语言编程人员已经学会了将警告视为错误的艰难方法,而这些东西似乎不是同一类东西,并且需要不可移植的修复。
【讨论】:
其他答案都很好,我不想重复他们所说的。
“为什么要启用警告”的另一个没有被正确触及的方面是它们对代码维护有很大帮助。当你编写一个相当大的程序时,你不可能一下子把整个事情都记在脑子里。你通常有一个或三个你正在积极编写和思考的函数,也许你的屏幕上有一个或三个你可以参考的文件,但大部分程序都存在于后台某个地方,你必须相信它继续工作。
发出警告,并尽可能让它们充满活力并出现在您的脸上,这有助于在您更改的内容对您看不到的内容造成麻烦时提醒您。
以Clang 警告-Wswitch-enum 为例。如果您在枚举上使用开关并错过了可能的枚举值之一,则会触发警告。您可能认为这是一个不太可能犯的错误:您可能至少在编写 switch 语句时查看了枚举值列表。您甚至可能拥有一个为您生成开关选项的 IDE,不会为人为错误留下任何余地。
当六个月后您在枚举中添加另一个可能的条目时,这个警告就真正出现了。同样,如果您正在考虑有问题的代码,您可能会没事的。但是,如果此枚举用于多种不同的目的,并且它是您需要额外选项的目的之一,那么很容易忘记更新您六个月未接触过的文件中的开关。
您可以像考虑自动化测试用例一样考虑警告:它们可以帮助您确保代码是合理的,并在您第一次编写代码时执行您需要的操作,但它们更有助于确保它在你刺激它的同时继续做你需要的事情。不同之处在于,测试用例非常严格地满足您的代码要求并且您必须编写它们,而警告则广泛适用于几乎所有代码的合理标准,并且它们是由制作编译器的研究人员非常慷慨地提供的。
【讨论】:
例如,调试segmentation fault 需要程序员追踪故障的根源(原因),该根源通常位于代码中比最终导致分段错误的行更早的位置。
非常典型的情况是,原因是编译器发出了您忽略的警告的行,而导致分段错误的行是最终引发错误的行。
修复警告导致修复问题...经典!
以上的演示...考虑以下代码:
#include <stdio.h>
int main(void) {
char* str = "Hello, World!!";
int idx;
// Colossal amount of code here, irrelevant to 'idx'
printf("%c\n", str[idx]);
return 0;
}
当使用传递给 GCC 的“Wextra”标志编译时,给出:
main.c: In function 'main':
main.c:9:21: warning: 'idx' is used uninitialized in this function [-Wuninitialized]
9 | printf("%c\n", str[idx]);
| ^
我可以忽略并执行代码......然后我会目睹一个“大”分段错误,正如我的 IP Epicurus 教授曾经说过的那样:
分段错误
为了在现实世界的场景中进行调试,人们将从导致分段错误的行开始,并尝试追踪原因的根源......他们必须搜索 @ 发生了什么987654328@ 和 str 在那大量的代码中......
直到有一天,他们发现idx 未初始化使用,因此它具有垃圾值,这导致索引字符串(方式)超出其范围,从而导致分段错误。
如果他们没有忽略警告,他们会立即发现错误!
【讨论】:
idx 恰好是您在测试中预期的值(如果预期值为 0,则不太可能),并且实际上恰好指向一些不应该打印的敏感数据部署时。
将警告视为错误只是一种自律方式:您正在编译一个程序来测试该闪亮的新功能,但您不能修复马虎的部分。 -Werror 没有提供其他信息。它只是非常清楚地设定了优先事项:
在修复现有代码中的问题之前不要添加新代码
真正重要的是心态,而不是工具。编译器诊断输出是一种工具。 MISRA C(用于嵌入式 C)是另一种工具。使用哪一个并不重要,但可以说编译器警告是您可以获得的最简单的工具(它只需设置一个标志)并且信噪比非常高。所以没有理由不使用它。
没有工具是万无一失的。如果你写const float pi = 3.14;,大多数工具不会告诉你你定义的 π 精度很差,这可能会导致问题。大多数工具不会在if(tmp < 42) 上引起注意,即使众所周知,给变量取无意义的名称和使用幻数是大型项目中的灾难。 您必须了解,您编写的任何“快速测试”代码都只是:一个测试,您必须在继续执行其他任务之前正确完成它,同时您仍然可以看到它的缺点。如果您保持该代码不变,那么在您花费两个月的时间添加新功能后对其进行调试将变得更加困难。
一旦你进入正确的心态,使用-Werror 就没有意义了。将警告作为警告可以让您做出明智的决定,是继续运行您将要开始的调试会话,还是中止它并首先修复警告。
【讨论】:
clippy linting 工具实际上都会警告常量“3.14”。它实际上是一个example in the docs。但正如您可能从名称中猜到的那样,clippy 以积极提供帮助而自豪。
众所周知,C 是一种相当低级的语言,如HLLs go。尽管 C++ 似乎是一种比 C 高级得多的语言,但它仍然具有许多相同的特征。其中一个特点是,这些语言是由程序员设计的,是为程序员设计的——特别是那些知道自己在做什么的程序员。
(对于这个答案的其余部分,我将专注于 C。我要说的大部分内容也适用于 C++,尽管可能没有那么强烈。尽管正如 Bjarne Stroustrup 所说的那样," C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off."。)
如果你知道自己在做什么——真的知道你在做什么——有时你可能不得不“打破规则”。但大多数时候,我们大多数人都会同意,善意的规则可以让我们都免于麻烦,而一直肆无忌惮地打破这些规则是一个坏主意。
但是在 C 和 C++ 中,您可以做的很多事情都是“坏主意”,但在形式上并不是“违反规则”。有时它们在某些时候是个坏主意(但在其他时候可能是可以辩护的);有时它们几乎一直都是个坏主意。但是传统一直不警告这些事情 - 因为,再次假设程序员知道他们在做什么,他们不会在没有充分理由的情况下做这些事情,并且他们会被一堆不必要的警告惹恼。
当然,并非所有程序员真的都知道他们在做什么。尤其是,每个 C 程序员(无论多么有经验)都会经历一个成为 C 初学者的阶段。即使是经验丰富的 C 程序员也可能粗心大意并犯错误。
最后,经验表明,不仅程序员确实会犯错误,而且这些错误可能会产生真实、严重的后果。如果你犯了一个错误,而编译器并没有警告你,并且程序不会立即崩溃或因此而做一些明显错误的事情,那么错误可能会潜伏在那里,隐藏起来,有时会隐藏多年,直到它导致真的大问题。
事实证明,在大多数情况下,警告毕竟是个好主意。即使是经验丰富的程序员也已经学会(实际上,“尤其是经验丰富的程序员已经学会了”),总的来说,警告往往利大于弊。每次你故意做错事而警告令人讨厌,你可能至少有十次不小心做错事,警告让你免于进一步的麻烦。当你真的想做“错误”的事情时,大多数警告都可以被禁用或解决。
(这种“错误”的典型例子是测试if(a = b)。大多数情况下,这是一个错误,所以现在大多数编译器都会警告它——有些甚至默认情况下。但是如果你 真的 想要将b 分配给a 并测试结果,您可以通过键入if((a = b)) 来禁用警告。)
第二个问题是,为什么要让编译器将警告视为错误?我会说这是因为人性,特别是说“哦,这只是一个警告,这不是那么重要,我稍后会清理它”的非常容易的反应。但是如果你是一个拖延者(我不了解你,但我是一个可怕的拖延者),基本上永远推迟必要的清理工作很容易——如果你进入忽略警告的习惯,在您忽略的所有警告消息中,您会越来越容易错过重要警告消息,而这些警告消息却被忽略了。
因此,要求编译器将警告视为错误是一个小技巧,您可以自己玩来绕过这个人类的弱点。
就个人而言,我并不坚持将警告视为错误。 (事实上,如果我说实话,我可以说我几乎从未在我的“个人”编程中启用该选项。)但你可以确定我已经在工作中启用了该选项,我们的风格指南(我写)强制其使用。我会说——我怀疑大多数专业程序员会说——任何不将警告视为 C 语言错误的商店都是不负责任的行为,没有遵循公认的行业最佳实践。
【讨论】:
if(a = b),因此我们不需要警告它。” (然后有人列出了 10 个已发布产品中由该特定错误导致的 10 个严重错误。)“好吧,没有有经验的 C 程序员会写出这样的......”
if (returnCodeFromFoo = foo(bar)) 并表示它,在一个地方捕获和测试代码(假设 only i> foo 的目的是产生副作用!)真正经验丰富的程序员可能知道这不是一种好的编码风格这一事实是不切实际的;)
if (returnCodeFromFoo = foo(bar)) 之类的东西,那么他们会添加注释并关闭警告(这样当维护程序员在 4 年后查看它时,他/她会意识到代码是故意的。也就是说,我与(在 Microsoft C++ 领域)坚持将 /Wall 与将警告视为错误相结合的人一起工作。嗯,它不是(除非你想放入很多抑制 cmets)。
警告包含一些最熟练的 C++ 开发人员可以融入应用程序的最佳建议。它们值得保留。
C++ 作为一种Turing complete 语言,在很多情况下编译器必须简单地相信您知道自己在做什么。但是,在很多情况下,编译器会意识到您可能并不打算编写您所写的内容。一个经典的例子是 printf() 代码与参数不匹配,或者 std::strings 传递给 printf (不是那个 曾经发生在我身上!)。在这些情况下,您编写的代码不是错误。它是一个有效的 C++ 表达式,编译器可以对其进行有效的解释。但是编译器有一种强烈的预感,即您只是忽略了一些现代编译器很容易检测到的东西。这些是警告。它们对于编译器来说是显而易见的,使用 C++ 的所有严格规则,你可能会忽略它们。
关闭或忽略警告就像选择忽略那些比您更熟练的人的免费建议。这是一个傲慢的教训,当你fly too close to the sun and your wings melt 或发生内存损坏错误时结束。两者之间,我随时都会从天上掉下来!
“将警告视为错误”是这一理念的极端版本。这里的想法是你解决编译器给你的每一个警告——你听取每一个免费的建议并采取行动。这对您来说是否是一个好的开发模型取决于团队以及您正在开发的产品类型。这是和尚可能有的苦行。对于某些人来说,它工作得很好。对于其他人来说,它没有。
在我的许多应用程序中,我们不会将警告视为错误。我们这样做是因为这些特定的应用程序需要在多个平台上使用多个不同年龄的编译器进行编译。有时我们发现实际上不可能修复一侧的警告而不将其变成另一个平台上的警告。所以我们只是小心翼翼。我们尊重警告,但不会因为警告而退缩。
【讨论】:
equals / hashCode),这是报告的实施质量问题。
某些警告可能意味着代码中可能存在语义错误或可能存在UB。例如。 ; 在if() 之后,一个未使用的变量,一个被局部屏蔽的全局变量,或者有符号和无符号的比较。许多警告与编译器中的静态代码分析器或在编译时可检测到的违反 ISO 标准有关,这“需要诊断”。虽然这些事件在某一特定情况下可能是合法的,但大多数情况下它们将是设计问题的结果。
一些编译器,例如 GCC,有一个命令行选项来激活“警告为错误”模式。这是一个很好但很残酷的工具来教育新手程序员。
【讨论】:
C 和 C++ 编译器在报告一些常见的程序员错误方面出了名的糟糕默认情况下,例如:
return 函数中的值printf 和 scanf 系列中的参数与格式字符串不匹配这些可以被检测和报告,只是通常不是默认的;此功能必须通过编译器选项明确请求。
这取决于你的编译器。
Microsoft C 和 C++ 编译器可以理解 /W1、/W2、/W3、/W4 和 /Wall 等开关。至少使用/W3。 /W4 和 /Wall 可能会针对系统头文件发出虚假警告,但如果您的项目使用这些选项之一编译干净,那就去吧。这些选项是相互排斥的。
大多数其他编译器都理解-Wall、-Wpedantic 和-Wextra 等选项。 -Wall 是必不可少的,其余的都是推荐的(请注意,尽管有它的名字,-Wall 只启用最重要的警告,而不是所有)。这些选项可以单独使用,也可以一起使用。
您的 IDE 可能有办法从用户界面启用这些功能。
编译器警告表明您的代码中存在潜在的严重问题。上面列出的问题几乎总是致命的;其他人可能会也可能不会,但您希望编译失败即使结果是虚惊一场。调查每个警告,找到根本原因并修复它。在误报的情况下,解决它 - 也就是说,使用不同的语言功能或构造,以便不再触发警告。如果这被证明非常困难,请根据具体情况禁用该特定警告。
您不想只留下警告作为警告,即使它们都是误报。对于发出的警告总数少于 7 个的非常小的项目来说,这可能是可以的。此外,新警告很容易在大量熟悉的旧警告中丢失。不允许那样。只需让您的所有项目都能干净地编译。
请注意,这适用于程序开发。如果您以源代码形式向全世界发布您的项目,那么最好不要在您的已发布 构建脚本中提供-Werror 或等效项。人们可能会尝试使用不同版本的编译器或完全不同的编译器来构建您的项目,这可能会启用不同的警告集。您可能希望他们的构建成功。启用警告仍然是一个好主意,这样看到警告消息的人就可以向您发送错误报告或补丁。
这再次通过编译器开关完成。 /WX 用于 Microsoft,大多数其他人使用 -Werror。无论哪种情况,如果产生任何警告,编译都会失败。
【讨论】:
C++ 编译器接受的编译代码显然会导致未定义的行为完全这一事实是编译器的一个主要缺陷。他们不解决这个问题的原因是因为这样做可能会破坏一些可用的构建。
大多数警告应该是阻止构建完成的致命错误。默认只显示错误并进行构建是错误的,如果您不覆盖它们以将警告视为错误并留下一些警告,那么您可能最终会导致程序崩溃并执行随机操作。
【讨论】:
int i; if (fun1()) i=2; if (fun2()) i=3; char s="abcde"[i]; 当且仅当fun1() 和fun2() 都可以在同一函数执行中返回false 时,此代码才表现出未定义的行为。哪个可能是真的,也可能不是,但是编译器如何判断?
警告是等待发生的错误。 因此,您必须启用编译器警告并整理您的代码以删除任何警告。
【讨论】:
处理警告不仅可以生成更好的代码,还可以让你成为更好的程序员。警告会告诉你今天对你来说似乎微不足道的事情,但有一天这个坏习惯会卷土重来,咬你一口。
使用正确的类型,返回该值,评估该返回值。花点时间思考“在这种情况下,这真的是正确的类型吗?” “我需要退货吗?”还有大人物; “这段代码在未来 10 年内是否可以移植?”
首先养成编写无警告代码的习惯。
【讨论】: