为什么我应该总是启用编译器警告?

我经常听到有人说,在编译C和c++程序时,我应该“总是启用编译器警告”。为什么这是必要的?我怎么做呢?

有时我也听到我应该“把警告当作错误”。我应该吗?我怎么做呢?

38148 次浏览

为什么要启用警告?

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

  • 忘记初始化变量
  • 忘记return函数的值
  • printfscanf族中的参数不匹配格式字符串
  • 函数的使用没有事先声明(仅限C语言)

这些可以被检测和报告,只是通常不是默认情况;此特性必须通过编译器选项显式地请求。

如何启用警告?

这取决于你的编译器。

微软C和c++编译器理解诸如/W1/W2/W3/W4/Wall之类的开关。至少使用/W3/W4/Wall可能会对系统头文件发出虚假警告,但如果你的项目使用其中一个选项编译干净,那么就使用它。这些选择是相互排斥的。

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

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

为什么我要把警告当作错误?它们只是警告!

编译器警告表示代码中存在潜在的严重问题。上面列出的问题几乎都是致命的;其他可能是,也可能不是,但你想编译失败即使,结果是一个假警报。调查每个警告,找到根本原因,并修复它。在出现假警报的情况下,可以绕过它——也就是说,使用不同的语言特性或构造,以便不再触发警告。如果这被证明是非常困难的,禁用特定的警告,具体情况的基础上。

你不希望只是把警告作为警告,即使它们都是假警报。对于发出的警告总数小于7的非常小的项目来说,这是可行的。再多一点,新的警告就很容易淹没在大量熟悉的旧警告中。不要允许这种情况发生。只是让你所有的项目编译干净。

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

如何将警告视为错误?

这同样是通过编译器开关完成的。/WX是微软的,大多数其他公司使用-Werror。在这两种情况下,如果产生任何警告,编译都会失败。

这就够了吗?

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

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

(对于这个答案的其余部分,我将专注于C。我要说的大部分内容也适用于c++,尽管可能没有那么强烈。尽管正如内定所说的那样,C让你很容易搬起石头砸自己的脚;c++使它更难,但当你这样做的时候,你的整条腿都完蛋了。.)

如果你知道你在做什么——真的知道你在做什么——有时候你可能不得不“打破规则”。但大多数时候,我们大多数人都会同意,善意的规则让我们所有人都远离麻烦,一直肆意违反这些规则是一个坏主意。

但是在C和c++中,你可以做的“坏主意”的事情多得惊人,但它们在形式上并不“违反规则”。有时它们在某些时候是一个坏主意(但在其他时候可能是站得住脚的);有时它们几乎总是个坏主意。但是传统一直是来警告这些事情——因为,再一次,假设是程序员知道他们在做什么,他们不会在没有充分理由的情况下做这些事情,他们会被一堆不必要的警告惹恼。

但是,当然不是所有的程序员都知道他们在做什么。特别是,每个C程序员(无论多么有经验)都要经历一个开始C程序员的阶段。即使是有经验的C程序员也会粗心大意,犯错误。

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

所以事实证明,大多数时候,警告毕竟是一个好主意。即使是有经验的程序员也知道(实际上,这是“特别是有经验的程序员已经知道”),总的来说,警告往往利大于弊。每次你故意做错事,而警告让人讨厌时,可能至少有十次你是意外做错事,而警告让你避免了更多的麻烦。而且当你真的想做“错误”的时候,大多数警告都可以被禁用或绕过。事情

(一个典型的“错误”的例子;是测试if(a = b)。大多数时候,这确实是一个错误,所以现在大多数编译器都会对此发出警告——有些甚至是默认的。但是如果你真的想要将b赋值给a并测试结果,你可以通过输入if((a = b))来禁用警告。)

第二个问题是,为什么要要求编译器将警告视为错误?我想说这是人性的原因,特别是,太容易说“哦,这只是一个警告,那不是那么重要,我以后会清理”的反应。但如果你是一个拖延者(我不知道你,但我是一个世界级的拖延者),很容易把必要的清理基本上永远推迟——如果你养成了忽视警告的习惯,它会越来越容易错过重要的警告信息,它坐在那里,不被注意,在所有你无情地忽视的警告中。

因此,要求编译器将警告视为错误是你可以对自己玩的一个小技巧,以避开这个人类的弱点,强迫自己修复警告今天,因为否则你的程序将无法编译。

就我个人而言,我并不坚持将警告视为错误——事实上,如果我诚实的话,我可以说我不倾向于在我的“个人”中启用这个选项。编程。但是您可以确信,我已经在工作中启用了这个选项,我们的风格指南(由我编写)要求使用它。我想说的是——我怀疑大多数专业程序员都会说——任何不把C语言中的警告视为错误的程序都是不负责任的行为,没有遵守普遍接受的行业最佳实践。

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

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

别着急:你不必这么做,也没有必要。- wall-Werror是由代码重构狂人为自己设计的:它由编译器开发人员发明,以避免在用户端编译器或编程语言更新后破坏现有的构建。特性本身并不是什么,而是关于是否破坏构建的决定。

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

警告包含了一些最熟练的c++开发人员可以放入应用程序中的最佳建议。他们值得留在身边。

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

关闭或忽略警告,就像选择忽略那些比你更有经验的人的免费建议。这是一个傲慢的教训,当你飞得离太阳太近,翅膀会融化或发生内存损坏错误时结束。在这两者之间,我愿意随时从天上掉下来!

“视警告为错误”;是这种哲学的极端版本。这里的想法是你解决编译器给你的每一个警告——你听取每一个免费的建议并采取行动。这对你来说是否是一个好的开发模式取决于你的团队以及你所开发的产品类型。这是僧侣可能有的苦行方式。对一些人来说,效果很好。对另一些人来说,则不然。

在我的许多应用程序中,我们不将警告视为错误。我们这样做是因为这些特定的应用程序需要在多个平台上编译,使用多个不同年代的编译器。有时我们会发现,如果在一个平台上修复一个警告,而不将其转化为另一个平台上的警告,实际上是不可能的。所以我们只是小心行事。我们尊重警告,但我们不会为它们竭尽全力。

处理警告不仅能写出更好的代码,还能让你成为更好的程序员。警告会告诉你一些今天对你来说微不足道的事情,但总有一天坏习惯会回来咬你的头。

使用正确的类型,返回该值,计算该返回值。花点时间思考“在这种情况下,这真的是正确的类型吗?”“我需要把这个还回去吗?”最重要的是;“这个代码在未来10年里还能移植吗?”

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

将警告视为错误只是自律的一种方式:你正在编译一个程序来测试那个闪亮的新功能,但是你不能,直到你修复了粗心的部分。-Werror没有提供额外的信息。它只是非常明确地设定了优先级:

在修复现有代码中的问题之前,不要添加新代码

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

没有工具是绝对正确的。如果你写const float pi = 3.14;,大多数工具不会告诉你你定义π的精度不好,这可能会导致后续的问题。大多数工具不会对if(tmp < 42)表示惊讶,即使众所周知,赋予变量无意义的名称和使用神奇的数字在大项目中是一种灾难。必须理解任何“快速测试”;你写的代码只是一个测试,在你继续其他任务之前,你必须把它做好,而你仍然可以看到它的缺点。如果您让代码保持原样,那么在花费两个月时间添加新特性之后,调试它将变得非常困难。

一旦你进入了正确的心态,使用-Werror就没有意义了。将警告作为警告将允许您做出明智的决定,是否仍然有意义运行您即将开始的调试会话,还是中止它并首先修复警告。

你应该总是启用编译器警告,因为编译器经常会告诉你代码哪里出了问题。为此,将-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;
}

当用“Wextra"传递给GCC的标志,给出:

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 伊壁鸠鲁教授曾经说过的:

段错误

为了在现实场景中调试这一点,人们将从导致分段错误的行开始,并试图跟踪原因的根源是什么……他们将不得不在那里的大量代码中搜索istr发生了什么……

直到有一天,他们发现自己处于一种情况,他们发现idx被未初始化地使用,因此它有一个垃圾值,这导致索引字符串(方式)超出其边界,从而导致分割错误。

如果他们没有忽视这个警告,他们就会立即发现这个漏洞!

这是C的一个具体答案,以及为什么这对C来说比其他任何东西都重要。

#include <stdio.h>


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

此代码使用警告编译。地球上几乎所有其他语言(除了汇编语言)的错误都是C中的警告, C中的警告几乎总是伪装的错误。警告应该被修正,而不是被压制。

在GCC中,我们这样做gcc -Wall -Werror

这也是微软一些不安全API警告引起高度不满的原因。大多数编写C语言的人已经学会了将警告视为错误的艰难方法,而这些东西出现的不是同一种东西,需要不可移植的修复。

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

“为什么启用警告”的另一个方面是;没有被恰当提及的是,它们极大地帮助了代码维护。当你编写一个相当大的程序时,你不可能一下子把所有的东西都记在脑子里。你通常有一三个你正在积极编写和思考的函数,也许在屏幕上有一三个文件可以参考,但程序的大部分存在于后台的某个地方,你必须相信它一直在工作。

如果你改变的某些东西给你看不见的东西带来了麻烦,你就会提醒自己。

铿锵声警告-Wswitch-enum为例。如果您在枚举上使用开关而漏掉了一个可能的枚举值,则会触发警告。您可能认为这是一个不太可能犯的错误:在编写switch语句时,您可能至少查看了枚举值列表。您甚至可能有一个IDE为您生成开关选项,不为人为错误留下任何空间。

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

您可以像考虑自动化测试用例一样考虑警告:它们帮助您确保代码是合理的,并在您第一次编写时执行您需要的操作,但它们更有助于确保在您催促它时它继续执行您需要的操作。不同的是,测试用例的工作范围非常窄,只满足代码的需求,你必须自己编写它们,而警告则广泛适用于几乎所有代码的合理标准,而且它们是由制作编译器的研究人员慷慨提供的。

作为使用遗留嵌入式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) < / p >

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

foo.c:在函数' foo '中:

foo.c:9:20:警告:未使用变量' c ' (-Wunused-variable) < / p >

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) < / p >

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 =] < / p >


这只是编译器可以为您进行双重检查的许多事情中的三件。还有很多其他的方法,比如使用未初始化的变量。

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

  1. 它可以告诉你在哪里你可能犯了一个错误,这可能会影响你的操作的最终结果。例如,如果你没有初始化一个变量,或者如果你使用"="而不是"=="(这只是例子)

  2. 它还允许显示你的代码不符合c++标准的地方。这很有用,因为如果代码符合实际标准,那么将很容易将代码移到其他平台。

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

你一定要启用编译器警告,因为一些编译器不擅长报告一些常见的编程错误,包括以下:

  • 初始化变量会被遗忘
  • 从一个被错过的函数返回一个值
  • printf和scanf族中的简单参数与格式字符串不匹配
  • 函数的使用没有事先声明,尽管这只在C中发生

所以这些函数可以被检测和报告,只是通常不是默认情况;所以这个特性必须通过编译器选项显式地请求。

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

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

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

将警告视为错误只有一个问题:当你使用来自其他来源的代码时(例如,微软库,开源项目),< >强他们< / >强没有正确地完成他们的工作,编译他们的代码会生成的警告。

总是写我的代码,所以它不会产生任何警告或错误,并清理它,直到它编译而不产生任何无关的噪音。我不得不处理的垃圾让我感到震惊,当我不得不构建一个大项目时,看着一串警告从编译器应该只声明它处理了哪些文件的地方经过时,我感到震惊。

我也记录我的代码,因为我知道软件真正的生命周期成本主要来自维护,而不是最初的编写,但这是另一回事……

编译器警告是你的朋友

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

当我的代码编译干净,97%的工作。与我一起工作的另一个人在编译时关闭了所有警告,在调试器中花费数小时或数天,然后让我帮忙。我只是用警告编译他的代码,然后告诉他要修改什么。

c++编译器接受明显导致未定义行为在所有的编译代码,这是编译器的一个主要缺陷。他们不修复这个问题的原因是,这样做可能会破坏一些可用的构建。

大多数警告应该是阻止构建完成的致命错误。默认情况下只显示错误并进行构建是错误的,如果您不覆盖它们,将警告视为错误,并留下一些警告,那么您可能会导致程序崩溃并做一些随机的事情。

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

我的团队的核心产品是一个MFC程序,多年来,它产生了数以百计的警告。在几乎所有的案例中都被忽略了。

当出现bug时,这简直是一场噩梦。

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

我鼓励所有构建都采用“无警告”策略,并将编译器警告级别设置为相当吵闹的级别。

我们的实践是使用# pragma警告 - push/disable/pop用于开发人员确信确实没问题的代码,以及调试级别的日志语句,以防万一。

这种做法对我们很有效。