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