提问人:n. m. could be an AI 提问时间:9/8/2019 最后编辑:S.S. Annen. m. could be an AI 更新时间:7/20/2023 访问量:40936
为什么我应该始终启用编译器警告?
Why should I always enable compiler warnings?
问:
我经常听到在编译 C 和 C++ 程序时,我应该“始终启用编译器警告”。为什么有必要这样做?我该怎么做?
有时我还听说我应该“将警告视为错误”。我应该吗?我该怎么做?
答:
为什么要启用警告?
众所周知,C 和 C++ 编译器在默认情况下不善于报告一些常见的程序员错误,例如:
- 忘记初始化变量
- 忘记函数中的值
return
- 参数和族与格式字符串不匹配
printf
scanf
- 函数无需事先声明即可使用(仅限 C)
这些可以被检测和报告,只是通常不是默认的;此功能必须通过编译器选项显式请求。
如何启用警告?
这取决于您的编译器。
Microsoft C 和 C++ 编译器可以理解 、、 和 等开关。至少使用 . 并且可能会对系统头文件发出虚假警告,但如果您的项目使用这些选项之一进行干净的编译,那就去做吧。这些选项是互斥的。/W1
/W2
/W3
/W4
/Wall
/W3
/W4
/Wall
大多数其他编译器都理解 -Wall
、-Wpedantic
和 -Wextra
等选项。 是必不可少的,其余的都是推荐的(请注意,尽管它的名字,只启用最重要的警告,而不是所有警告)。这些选项可以单独使用,也可以一起使用。-Wall
-Wall
您的 IDE 可能有办法从用户界面启用这些功能。
为什么应将警告视为错误?它们只是警告!
编译器警告表示代码中存在潜在的严重问题。上面列出的问题几乎总是致命的;其他人可能是也可能不是,但你希望编译失败,即使它被证明是一个误报。调查每个警告,找到根本原因,然后进行修复。如果出现误报,请解决它,即使用不同的语言功能或构造,以便不再触发警告。如果这被证明是非常困难的,请根据具体情况禁用该特定警告。
您不想只是将警告作为警告,即使所有这些警告都是误报。对于发出的警告总数少于 7 的非常小的项目来说,这可能是可以的。除此之外,新的警告很容易迷失在熟悉的旧警告中。不允许那样做。只需使所有项目都干净地编译即可。
请注意,这适用于程序开发。如果您以源代码形式向世界发布您的项目,那么最好不要在已发布的构建脚本中提供 -Werror
或等效项。用户可能会尝试使用不同版本的编译器或完全不同的编译器来生成项目,这可能会启用一组不同的警告。您可能希望他们的构建成功。保持警告处于启用状态仍然是一个好主意,这样看到警告消息的人就可以向您发送错误报告或补丁。
如何将警告视为错误?
这同样是通过编译器开关完成的。 是针对Microsoft的,其他大多数都使用.无论哪种情况,如果生成任何警告,编译都将失败。/WX
-Werror
这够了吗?
可能不是!随着优化水平的提高,编译器开始越来越仔细地查看代码,这种更仔细的审查可能会发现更多的错误。因此,不要满足于警告开关本身,在启用优化的情况下进行编译时,请始终使用它们(或 ,或者如果使用 MSVC)。-O2
-O3
/O2
评论
众所周知,C 是一种相当低级的语言,就像 HLL 一样。C++虽然看起来是一种比C高得多的语言,但仍然具有许多特征。其中一个特点是,这些语言是由程序员设计的,是为程序员设计的,特别是那些知道自己在做什么的程序员。
(对于这个答案的其余部分,我将专注于 C。我要说的大部分内容也适用于C++,尽管可能没有那么强烈。尽管正如 Bjarne Stroustrup 的名言,“C 很容易搬起石头砸自己的脚;C++使它更难,但是当你这样做时,它会让你的整条腿都炸掉。
如果你知道自己在做什么——真的知道自己在做什么——有时你可能不得不“打破规则”。但大多数时候,我们大多数人都会同意,善意的规则让我们所有人都远离麻烦,而一直肆意违反这些规则是一个坏主意。
但是在 C 和 C++ 中,你可以做的大量事情是“坏主意”,但并不是正式的“违反规则”。有时它们在某些时候是一个坏主意(但有时可能是站得住脚的);有时,它们几乎一直都是一个坏主意。但传统一直是不对这些事情发出警告——因为,再一次,假设程序员知道他们在做什么,他们不会在没有充分理由的情况下做这些事情,他们会被一堆不必要的警告所惹恼。
但当然,并不是所有的程序员都真正知道自己在做什么。特别是,每个 C 程序员(无论多么有经验)都会经历一个初级 C 程序员的阶段。即使是有经验的 C 程序员也会粗心大意并犯错误。
最后,经验表明,不仅程序员确实会犯错误,而且这些错误可能会产生真正的严重后果。如果你犯了一个错误,而编译器没有警告你,并且不知何故,程序不会立即崩溃或因此而做一些明显错误的事情,那么这个错误就会潜伏在那里,隐藏起来,有时长达数年,直到它造成一个非常大的问题。
所以事实证明,大多数时候,警告毕竟是个好主意。即使是有经验的程序员也已经了解到这一点(实际上,“尤其是有经验的程序员已经学会了这一点”),总的来说,警告往往利大于弊。因为每次你故意做错事并且警告是令人讨厌的,可能至少有十次你不小心做错了什么,警告使你免于进一步的麻烦。大多数警告都可以在您真正想做“错误”事情的那几次被禁用或解决。
(这种“错误”的一个典型例子是测试。大多数时候,这确实是一个错误,所以现在大多数编译器都会警告它——有些甚至是默认的。但是,如果您确实想同时分配和测试结果,则可以通过键入 来禁用警告。if(a = b)
b
a
if((a = b))
第二个问题是,为什么要要求编译器将警告视为错误?我想说这是因为人性,特别是说“哦,这只是一个警告,那不是那么重要,我稍后会清理它”的太容易的反应了。但是,如果你是一个拖延症患者(我不了解你,但我是一个世界级的拖延症患者),那么基本上很容易推迟必要的清理工作——如果你养成了忽视警告的习惯,那么就会越来越容易错过一个重要的警告信息,这些信息就坐在那里,没有被注意到,在你无情地忽视的所有警告信息中。
因此,要求编译器将警告视为错误是你可以自己玩的一个小把戏,以绕过这个人为的弱点,强迫自己今天修复警告,否则你的程序将无法编译。
就我个人而言,我并不坚持将警告视为错误——事实上,如果我说实话,我可以说我不倾向于在我的“个人”编程中启用该选项。但你可以肯定,我已经在工作中启用了这个选项,我们的风格指南(我写的)要求使用它。我想说的是——我怀疑大多数专业程序员都会说——任何不将警告视为 C 语言错误的商店都是不负责任的行为,没有遵守普遍接受的行业最佳实践。
评论
if(a = b)
if (returnCodeFromFoo = foo(bar))
foo
if (returnCodeFromFoo = foo(bar))
某些警告可能意味着代码中可能存在语义错误或可能的 UB。例如 之后,未使用的变量,被局部屏蔽的全局变量,或有符号和无符号的比较。许多警告与编译器中的静态代码分析器有关,或者与编译时可检测到的违反 ISO 标准有关,这些标准“需要诊断”。虽然这些情况在特定情况下可能是合法的,但大多数情况下它们都是设计问题的结果。;
if()
一些编译器,例如 GCC,有一个命令行选项来激活“错误警告”模式。这是一个很好的工具,虽然很残酷,但可以教育新手程序员。
放轻松:你不必,没有必要。-Wall 和 -Werror 是由代码重构狂人为自己设计的:它是由编译器开发人员发明的,以避免在用户端的编译器或编程语言更新后破坏现有构建。该功能什么都不是,而是关于中断或不中断构建的决定。
使用与否完全取决于您的喜好。我一直在使用它,因为它可以帮助我纠正错误。
评论
-Wall and -Werror was designed by code-refactoring maniacs for themselves.
[需要引证]
-Wall
-Werror
警告包括一些最熟练的 C++ 开发人员可以烘焙到应用程序中的最佳建议。他们值得留在身边。
C++ 是一种图灵完备语言,在很多情况下,编译器必须简单地相信你知道你在做什么。但是,在许多情况下,编译器可以意识到您可能不打算编写所写的内容。一个典型的例子是与参数不匹配的 printf() 代码,或者传递给 printf 的 std::strings(这不会发生在我身上!在这些情况下,您编写的代码不是错误。它是一个有效的 C++ 表达式,具有供编译器执行操作的有效解释。但是编译器有一种强烈的预感,你只是忽略了一些现代编译器很容易检测到的东西。这些是警告。它们是编译器显而易见的东西,使用其可以使用的所有严格的 C++ 规则,您可能忽略了这些规则。
关闭警告或忽略警告,就像选择忽略那些比你更熟练的人的免费建议一样。这是傲慢的一课,当你飞得离太阳太近而你的翅膀融化,或者发生内存损坏错误时,就会结束。在两者之间,我随时都会从天上掉下来!
“将警告视为错误”是这一理念的极端版本。这里的想法是,你解决了编译器给你的每一个警告——你听取了每一点免费建议并采取行动。这是否是你的开发模式,取决于团队和你正在开发的产品类型。这是僧侣可能拥有的苦行方法。对于某些人来说,它效果很好。对于其他人来说,它没有。
在我的许多应用程序中,我们不会将警告视为错误。我们这样做是因为这些特定的应用程序需要在多个平台上使用多个不同年龄的编译器进行编译。有时我们发现,实际上不可能在一侧修复警告,而不会在另一个平台上变成警告。所以我们只是小心翼翼。我们尊重警告,但我们不会为它们向后弯腰。
评论
equals
hashCode
处理警告不仅可以生成更好的代码,还可以使您成为更好的程序员。警告会告诉你一些今天对你来说似乎微不足道的事情,但总有一天这个坏习惯会卷土重来,咬掉你的头。
使用正确的类型,返回该值,计算该返回值。花点时间思考一下,“在这种情况下,这真的是正确的类型吗?“我需要退货吗?”还有大人物;“这个代码会在未来10年内被移植吗?”
首先养成编写无警告代码的习惯。
将警告视为错误只是一种自律的手段:您正在编译一个程序来测试这个闪亮的新功能,但在您修复草率的部分之前,您不能这样做。Werror 没有提供其他信息。它只是非常清楚地设置了优先级:
在修复现有代码中的问题之前,不要添加新代码
重要的是心态,而不是工具。编译器诊断输出是一种工具。MISRA C(用于嵌入式 C)是另一个工具。使用哪一个并不重要,但可以说编译器警告是你能得到的最简单的工具(它只是一个标志要设置),而且信噪比非常高。所以没有理由不使用它。
没有任何工具是万无一失的。如果你写,大多数工具不会告诉你你定义的π精度很差,这可能会导致未来的问题。大多数工具不会引起人们的注意,即使众所周知,在大型项目中给变量起无意义的名称和使用幻数是导致灾难的一种方式。你必须明白,你编写的任何“快速测试”代码都只是一个测试,在你继续执行其他任务之前,你必须把它做好,而你仍然看到它的缺点。如果保持该代码不变,则在花费两个月时间添加新功能后对其进行调试将更加困难。const float pi = 3.14;
if(tmp < 42)
一旦你进入了正确的心态,使用.将警告作为警告将允许您做出明智的决定,是运行即将启动的调试会话,还是中止它并首先修复警告仍然有意义。-Werror
评论
clippy
clippy
您应该始终启用编译器警告,因为编译器通常可以告诉您代码出了什么问题。为此,请将 -Wall
-Wextra
传递给编译器。
通常应将警告视为错误,因为警告通常表示代码有问题。但是,通常很容易忽略这些错误。因此,将它们视为错误将导致生成失败,因此您不能忽略这些错误。若要将警告视为错误,请将 -Werror
传递给编译器。
非固定警告迟早会导致代码中出现错误。
例如,调试分段错误需要程序员跟踪故障的根源(原因),该根源通常位于代码中与最终导致分段错误的行相比的先前位置。
非常典型的情况是,原因是编译器发出了警告但您忽略了该行,而导致分段错误的行是最终引发错误的行。
修复警告会导致解决问题...经典之作!
上述的演示...请考虑以下代码:
#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 伊壁鸠鲁教授曾经说过的那样:
分段故障
为了在实际场景中对此进行调试,需要从导致分段故障的线路开始,并尝试追踪原因的根源......他们将不得不搜索那边大量代码发生了什么......i
str
直到有一天,他们发现自己处于这样的情况:他们发现该字符串未初始化,因此它具有垃圾值,这会导致索引字符串(方式)超出其边界,从而导致分割错误。idx
如果他们没有忽略警告,他们就会立即发现这个错误!
评论
idx
这是对 C 的具体回答,也是为什么这对 C 来说比其他任何事情都重要得多。
#include <stdio.h>
int main()
{
FILE *fp = "some string";
}
此代码编译时显示警告。在地球上几乎所有其他语言(汇编语言除外)中,错误都是 C 语言中的警告,而 C 语言中的警告几乎总是伪装的错误。警告应该是固定的,而不是禁止的。
使用 GCC,我们将其作为 .gcc -Wall -Werror
这也是对一些Microsoft不安全的API警告高度咆哮的原因。大多数编写 C 语言的人都学会了将警告视为错误的艰难方法,而这些东西似乎不是同一种东西,并且需要不可移植的修复程序。
其他答案都很好,我不想重复他们所说的话。
“为什么要启用警告”的另一个方面没有被正确触及,那就是它们对代码维护有很大帮助。当你编写一个相当大的程序时,不可能一次把整个事情放在你的脑海中。你通常有一个或三个你正在积极编写和思考的功能,也许你的屏幕上有一三个文件可以参考,但大部分程序存在于后台的某个地方,你必须相信它继续工作。
打开警告,并尽可能精力充沛地出现在你的脸上,有助于提醒你,如果你改变的东西给你看不见的东西带来麻烦。
以 Clang 警告为例。如果在枚举上使用开关并错过了可能的枚举值之一,则会触发警告。您可能会认为这是一个不太可能犯的错误:在编写 switch 语句时,您可能至少查看了枚举值列表。您甚至可能有一个 IDE 为您生成开关选项,不留人为错误的余地。-Wswitch-enum
六个月后,当您向枚举添加另一个可能的条目时,此警告才真正发挥作用。同样,如果你正在考虑有问题的代码,你可能会没事的。但是,如果此枚举用于多种不同的目的,并且它是您需要额外选项的其中一个目的,则很容易忘记更新六个月未接触的文件中的开关。
你可以像思考自动化测试用例一样思考警告:它们帮助你确保代码是合理的,并在你第一次编写代码时做你需要的事情,但它们更有助于确保它继续做你需要的事情,而你正在推动它。不同之处在于,测试用例的工作范围非常狭窄,你必须编写它们,而警告则广泛地适用于几乎所有代码的合理标准,并且它们由制作编译器的棺材非常慷慨地提供。
评论
作为使用传统嵌入式 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”的声明会掩盖全局声明 [-影子]
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
由于某些原因,C++ 中的编译器警告非常有用。
它允许您向您展示您可能在哪些方面犯了错误,这可能会影响您的操作的最终结果。例如,如果您没有初始化变量,或者您使用“=”而不是“==”(只是示例)
它还允许您显示您的代码不符合 C++ 标准的地方。这很有用,因为例如,如果代码符合实际标准,则很容易将代码移动到其他平台。
通常,警告对于显示代码中存在错误的位置非常有用,这些错误可能会影响算法的结果或防止用户使用程序时出现错误。
您绝对应该启用编译器警告,因为某些编译器不擅长报告一些常见的编程错误,包括以下内容:
- 初始化变量被遗忘
- 从函数 get missed 返回值
- printf 和 scanf 系列中的简单参数与格式字符串不匹配
- 函数无需事先声明即可使用,尽管这仅在 C 中发生
因此,由于这些功能可以被检测和报告,只是通常不是默认的;因此,必须通过编译器选项显式请求此功能。
警告是等待发生的错误。 因此,您必须启用编译器警告并整理代码以删除任何警告。
忽略警告意味着您留下了草率的代码,这不仅可能在将来给其他人带来问题,而且还会使重要的编译消息不那么被您注意到。
编译器输出越多,任何人注意到或打扰的次数就越少。越干净越好。这也意味着你知道自己在做什么。警告是非常不专业、粗心和冒险的。
将警告视为错误只有一个问题:当您使用来自其他来源(例如 Microsoft 库、开源项目)的代码时,它们没有正确完成工作,并且编译代码会产生大量警告。
我总是编写我的代码,这样它就不会产生任何警告或错误,并清理它,直到它编译时不产生任何外来的噪音。我必须处理的垃圾让我感到震惊,当我不得不构建一个大项目并看到一连串的警告时,我感到很震惊,编译应该只宣布它处理了哪些文件。
我还记录了我的代码,因为我知道软件的真正生命周期成本主要来自维护,而不是最初编写它,但那是另一回事......
评论
-Wall
-Wall -Wextra
编译器警告是你的朋友
我在传统的 Fortran 77 系统上工作。编译器告诉我有价值的事情:子例程调用中的参数数据类型不匹配,以及如果我有未使用的变量或子例程参数,则在将值设置到变量之前使用局部变量。这些几乎总是错误。
当我的代码干净地编译时,97% 它都可以工作。与我一起工作的另一个人在关闭所有警告的情况下进行编译,在调试器中花费数小时或数天,然后请我帮忙。我只是在打开警告的情况下编译他的代码,并告诉他要修复什么。
C++编译器接受编译代码,这显然会导致未定义的行为,这是编译器的一个主要缺陷。他们不解决这个问题的原因是这样做可能会破坏一些可用的构建。
大多数警告应该是阻止生成完成的致命错误。仅显示错误并无论如何都进行构建的默认值是错误的,如果您不覆盖它们以将警告视为错误并留下一些警告,那么您最终可能会导致程序崩溃并随机执行操作。
评论
int i; if (fun1()) i=2; if (fun2()) i=3; char s="abcde"[i];
fun1()
fun2()
false
我曾经在一家制造电子测试设备的大型(财富 50 强)公司工作。
我们团队的核心产品是一个MFC程序,多年来,它产生了数百个警告。这在几乎所有情况下都被忽略了。
当错误发生时,这是一场令人毛骨悚然的噩梦。
在那个职位之后,我很幸运地被聘为一家新创业公司的第一位开发人员。
我鼓励对所有构建都采用“无警告”策略,将编译器警告级别设置为非常嘈杂。
我们的做法是使用 #pragma 警告 - 推送/禁用/弹出开发人员确定确实没问题的代码,以及调试级别的日志语句,以防万一。
这种做法对我们来说效果很好。
评论
#pragma warning
所有百分比都偏离了现实,并不意味着要认真对待。
99% 的警告对于正确性完全没有用。但是,这 1% 会使您的代码无法正常工作(通常在极少数情况下)。重要的是,其他答案没有。
警告来自编译器开发人员。有一个“C”标准和一致性。但警告是编译器开发人员发出的信号,表明你给他们带来了问题。也就是说,这些可能是编译器编写者知道导致低效或错误构造的事情。这就像无视水管工说你不能在那里放厕所,然后告诉他们无论如何都要这样做。
下一个启用警告的人会认为你不称职,因为你没有启用警告。他们不知道 99% 的代码是正确的,并认为只有 50% 是正确的。
另一个经常被警告捕获的问题是死代码。即,永远不能做任何事情的代码。这可能是人们讨厌继承带有警告的代码的原因。他们所看到的 75% 的内容可能是无用的。
无警告代码让其他人相信代码是可移植的,并且能够适应工具、代码更新和一般的位腐烂。无警告代码让其他开发人员相信他们正在查看的代码不是疯狂的意大利面条或微妙的 boloney。他们也可能只是发现一两个错误。
评论