为什么我应该始终启用编译器警告?

Why should I always enable compiler warnings?

我经常听说在编译 C 和 C++ 程序时我应该 "always enable compiler warnings"。为什么这是必要的?我怎么做?

有时候我也听说我应该"treat warnings as errors"。我是不是该?我该怎么做?

为什么要启用警告?

C 和 C++ 编译器在默认情况下 报告一些常见的程序员错误是出了名的糟糕,例如:

  • 忘记初始化变量
  • 忘记return来自函数的值
  • printfscanf 系列中的参数与格式字符串不匹配
  • 一个函数在没有事先声明的情况下被使用(仅限 C)

这些可以被检测和报告,只是通常默认情况下没有;必须通过编译器选项明确请求此功能。

如何启用警告?

这取决于您的编译器。

Microsoft C 和 C++ 编译器理解 /W1/W2/W3/W4/Wall 等开关。至少使用 /W3/W4/Wall 可能会发出系统头文件的虚假警告,但如果您的项目使用这些选项之一编译干净,那就去吧。这些选项是互斥的。

大多数其他编译器理解像 -Wall, -Wpedantic and -Wextra 这样的选项。 -Wall 是必不可少的,其余的都是推荐的(请注意,尽管它的名字,-Wall 只启用最重要的警告,而不是 所有 警告。这些选项可以单独使用,也可以一起使用。

您的 IDE 可能有办法从用户界面启用这些功能。

为什么我应该将警告视为错误?它们只是警告!

编译器警告表示您的代码中存在潜在的严重问题。上面列出的问题几乎总是致命的;其他人可能会也可能不会,但您希望编译失败 即使 结果是虚惊一场。调查每个警告,找到根本原因并修复它。在误报的情况下,解决它 - 即使用不同的语言功能或结构,以便不再触发警告。如果事实证明这非常困难,请根据具体情况禁用该特定警告。

您不希望只将警告保留为警告,即使它们都是误报。对于发出的警告总数少于 7 个的非常小的项目来说,这可能没问题。任何更多,新警告很容易在熟悉的旧警告的洪流中迷失。不允许这样。只是让你所有的项目都能干净地编译。

注意这适用于程序开发。如果您以源代码形式向全世界发布项目,那么最好不要在 released 构建脚本中提供 -Werror 或等效项。人们可能会尝试使用不同版本的编译器或完全不同的编译器来构建您的项目,这可能会启用一组不同的警告。您可能希望他们的构建成功。保持警告启用仍然是一个好主意,这样看到警告消息的人可以向您发送错误报告或补丁。

如何将警告视为错误?

这又是通过编译器开关完成的。 /WX 适用于 Microsoft,大多数其他人使用 -Werror。在任何一种情况下,如果产生任何警告,编译都会失败。

够了吗?

可能不会!当您提高优化级别时,编译器开始越来越仔细地检查代码,这种更仔细的检查可能会发现更多错误。因此,不要满足于警告开关本身,在启用优化的情况下编译时始终使用它们(-O2-O3,或者 /O2,如果使用 MSVC)。

C 是众所周知的一种相当低级的语言 HLLs。 C++,尽管它看起来是一种比 C 高级得多的语言,但它仍然具有许多相同的特征。其中一个特点是,这些语言是由程序员设计的,为程序员设计的——具体来说,是为知道自己在做什么的程序员设计的。

(对于这个答案的其余部分,我将重点关注 C。我要说的大部分内容也适用于 C++,尽管可能不那么强烈。尽管 Bjarne Stroustrup has famously said, "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 程序员也会粗心大意并犯错误。

最后,经验表明,不仅程序员确实会犯错误,而且这些错误可能会产生真实、严重的后果。如果你犯了一个错误,而编译器没有警告你,并且程序没有立即崩溃或因此而做一些明显错误的事情,错误可能会潜伏在那里,隐藏,有时会隐藏多年,直到它导致真的个大问题。

事实证明,在大多数情况下,警告毕竟是个好主意。即使是有经验的程序员也了解到(实际上,“尤其是 有经验的程序员已经了解到这一点”),总的来说,警告往往利大于弊。对于每次你故意做错事并且警告是一件令人讨厌的事情,可能至少有十次你不小心做错了事并且警告使你免于进一步的麻烦。当你真的想做“错误”的事情时,大多数警告都可以被禁用或解决那几次。

(这种“错误”的一个 classic 示例是测试 if(a = b)。大多数时候,这确实是一个错误,所以现在大多数编译器都会对此发出警告——一些即使是默认设置。但是如果您 真的 既想将 b 分配给 a 又想测试结果,您可以通过键入 if((a = b)) 来禁用警告.)

第二个问题,为什么要让编译器把警告当成错误?我会说这是因为人性,特别是说“哦,那只是一个警告,这不是那么重要,我稍后会清理它”的过于容易的反应。但如果你是一个拖延者(我不了解你,但我是一个世界 - class 拖延者)基本上永远推迟必要的清理很容易 - 如果你养成习惯如果忽略警告,就会越来越容易错过一条 重要 警告消息,而这些消息就在您无情地忽略的所有消息中,未被注意到。

因此,要求编译器将警告视为错误是您可以对自己耍的一个小把戏,以绕过这个人类弱点,强迫自己在今天修复警告,因为否则你的程序将无法编译。

就个人而言,我并不坚持将警告视为错误 — 事实上,老实说,我不倾向于在我的“个人”编程中启用该选项。但你可以确定我在工作中启用了该选项,我们的风格指南(我写的)强制使用它。我想说——我怀疑大多数专业程序员会说——任何不将警告视为 C 中的错误的商店都是不负责任的行为,没有遵守普遍接受的行业最佳实践。

一些警告可能意味着代码中可能存在语义错误或可能 UB。例如。 ;if()之后,未使用的变量,被local屏蔽的全局变量,或者signed和unsigned的比较。许多警告与编译器中的静态代码分析器或编译时可检测到的违反 ISO 标准有关,这些“需要诊断”。虽然这些事件在某一特定情况下可能是合法的,但大多数情况下它们都是设计问题的结果。

一些编译器,例如 GCC,有一个命令行选项来激活“警告为错误”模式。这是一个很好的,如果残酷,教育新手编码器的工具。

放轻松:你不必,也没有必要。 -Wall and -Werror 是由代码重构狂热者为自己设计的:它是由编译器开发人员发明的,以避免在用户端更新编译器或编程语言后破坏现有构建。功能没什么,但是关于破坏或不破坏构建的决定。

使用与否完全取决于您的喜好。我一直在使用它,因为它可以帮助我改正错误。

警告包含一些最熟练的 C++ 开发人员可以融入应用程序的最佳建议。它们值得保留。

C++ 作为一种 Turing complete 语言,在很多情况下编译器必须相信您知道自己在做什么。但是,在很多情况下,编译器可以意识到您可能并不打算编写您编写的内容。一个典型的例子是 printf() 代码不匹配参数,或者 std::strings 传递给 printf(不是 曾经 发生在我身上!)。在这些情况下,您编写的代码不是错误。它是一个有效的 C++ 表达式,具有供编译器执行的有效解释。但是编译器有一种强烈的预感,你只是忽略了一些现代编译器很容易检测到的东西。这些是警告。它们对编译器来说是显而易见的,使用 C++ 的所有严格规则,您可能会忽略这些。

关闭或忽略警告就像选择忽略那些比您更熟练的人提供的免费建议。这是一个傲慢的教训,它会在您 fly too close to the sun and your wings melt 或发生内存损坏错误时结束。两者之间,天上掉下我随便哪天!

“将警告视为错误”是这种哲学的极端版本​​。这里的想法是,您解决编译器给您的 每个 警告——您听取所有免费建议并采取行动。这是否是适合您的开发模型取决于团队以及您正在开发的产品类型。这是僧侣可能拥有的苦行者方法。对于某些人来说,它效果很好。对于其他人,它没有。

在我的许多应用程序中,我们不会将警告视为错误。我们这样做是因为这些特定的应用程序需要在多个平台上使用多个不同年龄的编译器进行编译。有时我们发现实际上不可能修复一个警告而不会在另一个平台上变成警告。所以我们只是小心。我们尊重警告,但我们不会为之屈服。

处理警告不仅可以编写更好的代码,还可以让您成为更好的程序员。警告会告诉您一些今天对您来说似乎微不足道的事情,但总有一天,这种坏习惯会卷土重来,咬掉您的脑袋。

使用正确的类型,return那个值,评估那个return值。花时间反思 "Is this really the correct type in this context?" "Do I need to return this?" 重要的是; "Is this code going to be portable for the next 10 years?"

首先养成编写无警告代码的习惯。

将警告视为错误只是一种自律的方式:你正在编译一个程序来测试那个闪亮的新功能,但是你不能直到你修复这个草率的问题部分。 -Werror 没有提供其他信息。它只是非常清楚地设置了优先级:

Don't add new code until you fix problems in the existing code

真正重要的是心态,而不是工具。编译器诊断输出是一种工具。 MISRA C(用于嵌入式 C)是另一个工具。使用哪一个并不重要,但可以说编译器警告是您可以获得的最简单的工具(它只是一个要设置的标志)并且信噪比非常高。所以没有理由使用它。

没有绝对可靠的工具。如果你写 const float pi = 3.14;,大多数工具不会告诉你你定义的 π 精度不佳,这可能会导致问题。大多数工具不会在 if(tmp < 42) 上引起注意,即使众所周知,赋予变量无意义的名称和使用幻数在大型项目中是一种灾难。 必须明白,您编写的任何“快速测试”代码都只是:一个测试,在您继续执行其他任务之前,您必须正确完成它,同时您仍然可以看到它缺点。如果您按原样保留该代码,则在花费两个月添加新功能后对其进行调试将变得更加困难。

一旦你有了正确的心态,使用 -Werror 就没有意义了。将警告作为警告将使您能够做出明智的决定,它是否对 运行 您将要启动的调试会话仍然有意义,或者中止它并首先修复警告。

您应该始终启用编译器警告,因为编译器通常会告诉您代码有什么问题。为此,您将 -Wall -Wextra 传递给编译器。

您通常应将警告视为错误,因为警告通常表示您的代码存在问题。但是,通常很容易忽略这些错误。因此,将它们视为错误将导致构建失败,因此您不能忽略这些错误。要将警告视为错误,请将 -Werror 传递给编译器。

非固定警告 迟早会 导致您的代码出错 .


例如,调试 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 教授曾经说:

Segmentation fault

为了在现实世界中对此进行调试,人们会从导致分段错误的行开始并尝试追踪原因的根源......他们将不得不搜索发生了什么到 istr 里面的大量代码...

直到有一天,他们发现自己处于这样一种情况,即他们发现 idx 未初始化使用,因此它具有垃圾值,导致索引字符串(方式)超出其范围,这会导致分段错误。

如果他们没有忽略警告,他们会立即发现错误!

这是对 C 的具体回答,以及为什么这对 C 比对其他任何事情都重要得多。

#include <stdio.h>

int main()
{
   FILE *fp = "some string";
}

此代码编译时带有 警告。地球上几乎所有其他语言(汇编语言除外)的错误是什么,应该是 C 中的 warnings。C 中的警告几乎总是伪装的错误。警告应该被修复,而不是被抑制。

对于 GCC,我们这样做是 gcc -Wall -Werror

这也是对某些 Microsoft 非安全 API 警告非常恼火的原因。大多数使用 C 语言编程的人都学会了将警告视为错误的艰难方法,而这些东西似乎并不是同一类东西,需要不可移植的修复。

其他的回答都很好,我不想重复他们说的。

“为什么启用警告”的另一个方面没有被正确触及,它们对代码维护有很大帮助。当你编写一个非常大的程序时,你不可能一下子把所有的事情都记在脑海里。你通常有一个或三个你正在积极编写和思考的函数,也许你屏幕上有一个或三个你可以参考的文件,但程序的大部分存在于后台某处,你必须相信它继续工作。

打开警告,并尽可能让它们充满活力地出现在您的脸上,这有助于在您更改的内容给您看不到的内容带来麻烦时提醒您。

Clang 警告 -Wswitch-enum 为例。如果您在枚举上使用开关并错过其中一个可能的枚举值,则会触发警告。您可能认为这是不太可能犯的错误:您可能至少在编写 switch 语句时查看了枚举值列表。您甚至可能有一个 IDE 为您生成开关选项,不留任何人为错误的余地。

当您在六个月后向枚举中添加另一个可能的条目时,此警告才真正发挥作用。同样,如果您正在考虑有问题的代码,您可能会没事。但是,如果此枚举用于多个不同的目的,并且它用于其中一个您需要额外选项的目的,那么很容易忘记更新您六个月未触及的文件中的开关。

您可以像看待自动测试用例一样看待警告:它们帮助您确保代码合理并在您首次编写代码时执行您需要的操作,但它们的帮助更大以确保它在你刺激它的同时继续做你需要的事情。不同之处在于,测试用例的工作范围非常狭窄,只能满足您的代码要求,您必须编写它们,而警告则可以广泛地满足几乎所有代码的合理标准,并且它们由编译器的研究人员非常慷慨地提供。

作为使用遗留嵌入式 C 代码的人,启用编译器警告有助于在提出修复建议时显示出许多弱点和需要调查的领域。在 GCC 中,使用 -Wall and -Wextra and even -Wshadow 变得至关重要。我不会一一列举所有危险,但我会列出一些已弹出的帮助显示代码问题的危险。

遗留变量

这个可以很容易地指出未完成的工作和可能没有使用所有传递变量的区域,这可能是一个问题。让我们看一个可能触发这个的简单函数:

int foo(int a, int b)
{
   int c = 0;

   if (a > 0)
   {
        return a;
   }
   return 0;
}

在没有 -Wall-Wextra returns 的情况下编译这个没有问题。 -Wall 会告诉您 c 从未被使用过:

foo.c: In function ‘foo’:

foo.c:9:20: warning: unused variable ‘c’ [-Wunused-variable]

-Wextra 还会告诉您参数 b 没有做任何事情:

foo.c: In function ‘foo’:

foo.c:9:20: warning: unused variable ‘c’ [-Wunused-variable]

foo.c:7:20: warning: unused parameter ‘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: warning: declaration of ‘c’ shadows a global declaration [-Wshadow]

foo.c:1:5: note: shadowed declaration is here

格式化字符串

这不需要 GCC 中的任何额外标志,但它在过去仍然是问题的根源。尝试打印数据但出现格式错误的简单函数可能如下所示:

void foo(const char * str)
{
    printf("str = %d\n", str);
}

这不会打印字符串,因为格式化标志是错误的,GCC 会很高兴地告诉您这可能不是您想要的:

foo.c: In function ‘foo’:

foo.c:10:12: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘const char *’ [-Wformat=]


这些只是编译器可以为您仔细检查的许多事情中的三个。还有很多其他人喜欢使用其他人指出的未初始化变量。

出于某些原因,C++ 中的编译器警告非常有用。

  1. 它允许向您展示您可能在哪里犯了错误,这可能会影响您的操作的最终结果。例如,如果您没有初始化一个变量,或者如果您使用“=”而不是“==”(只有示例)

  2. 它还允许向您展示您的代码哪里不符合 C++ 的标准。这很有用,因为如果代码符合实际标准,则很容易将代码移动到其他平台,例如。

一般来说,警告非常有用,可以告诉您代码中的错误,这些错误会影响算法的结果,或者在用户使用您的程序时防止出现错误。

您绝对应该启用编译器警告,因为某些编译器不善于报告一些常见的编程错误,包括以下内容:

  • 忘记初始化变量
  • Return 函数中的一个值被遗漏了
  • printf 和 scanf 系列中的简单参数与格式字符串不匹配
  • 一个函数在没有事先声明的情况下被使用,尽管这只发生在 C 中

因此,由于可以检测和报告这些功能,通常默认情况下不会;因此必须通过编译器选项明确请求此功能。

警告是等待发生的错误。 因此,您必须启用编译器警告并整理代码以删除任何警告。

忽略警告意味着您留下了草率的代码,这些代码不仅会在将来给其他人带来问题,而且还会使您不太注意重要的编译消息。

编译器输出越多,人们就越不会注意到或打扰。越干净越好。这也意味着您知道自己在做什么。警告非常不专业、粗心和有风险。

只有 一个 将警告视为错误的问题:当您使用来自其他来源(例如 Microsoft 库、开源项目)的代码时,他们没有做好他们的工作,编译他们的代码会产生的警告。

总是 编写我的代码,这样它就不会产生任何警告或错误,并清理它直到它编译时不会产生任何外来噪音。我必须处理的垃圾让我感到震惊,当我不得不构建一个大项目并看到一连串的警告经过编译应该只宣布它处理了哪些文件时,我感到很震惊。

我还记录了我的代码,因为我知道软件的实际生命周期成本主要来自维护,而不是最初编写它,但那是另一回事...

编译器警告是你的朋友

我在遗留 Fortran 77 系统上工作。编译器告诉我有价值的事情:子例程调用中的参数数据类型不匹配,如果我有一个未使用的变量或子例程参数,则在将值设置到变量之前使用局部变量。这些几乎总是错误。

当我的代码编译干净时,97% 可以正常工作。与我一起工作的另一个人在关闭所有警告的情况下进行编译,在调试器中花费数小时或数天,然后请我提供帮助。我只是在打开警告的情况下编译他的代码,然后告诉他要修复什么。

事实上,C++ 编译器接受明显导致未定义行为的编译代码根本是编译器的一个主要缺陷。他们不解决这个问题的原因是这样做可能会破坏一些可用的构建。

大多数警告应该是阻止构建完成的致命错误。仅显示错误并执行构建的默认设置是错误的,如果您不覆盖它们以将警告视为错误并留下一些警告,那么您最终可能会导致程序崩溃并执行随机操作。

我曾经在一家制造电子测试设备的大型(财富 50 强)公司工作。

我小组的核心产品是一个 MFC 程序,多年来,该程序产生了数百个警告。几乎在所有情况下都被忽略了。

当出现错误时,这简直就是一场噩梦。

在那个职位之后,我很幸运地被聘为新创业公司的第一个开发人员。

我鼓励对所有构建实施 'no warning' 政策,将编译器警告级别设置为非常嘈杂。

我们的做法是将 #pragma warning - push/disable/pop 用于开发人员确定非常好的代码,以及调试级别的日志语句,以防万一

这种做法对我们很有效。