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