我经常听到有人说,在编译C和c++程序时,我应该“总是启用编译器警告”。为什么这是必要的?我怎么做呢?
有时我也听到我应该“把警告当作错误”。我应该吗?我怎么做呢?
我经常听到有人说,在编译C和c++程序时,我应该“总是启用编译器警告”。为什么这是必要的?我怎么做呢?
有时我也听到我应该“把警告当作错误”。我应该吗?我怎么做呢?
当前回答
一些警告可能意味着代码中可能出现语义错误或可能出现UB。例如;if()之后,一个未使用的变量,一个被局部变量掩盖的全局变量,或者有符号和无符号的比较。许多警告与编译器中的静态代码分析器或在编译时检测到的违反ISO标准有关,这“需要诊断”。虽然在特定情况下,这些事件可能是合法的,但大多数情况下,它们是设计问题的结果。
一些编译器,例如GCC,有一个命令行选项来激活“警告为错误”模式。这是一个很好的工具,如果残酷,教育新手。
其他回答
非固定的警告迟早会导致代码中的错误。
例如,调试分段错误需要程序员追踪错误的根源(原因),它通常位于代码中比最终导致分段错误的行更前面的位置。
很典型的情况是,导致错误的行是编译器发出警告而你忽略的行,而导致分段错误的行是最终抛出错误的行。
修复警告就等于修复了问题……一个经典的!
以上的演示…考虑下面的代码:
#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使用时没有初始化,因此它有一个垃圾值,这导致索引字符串(方式)超出其界限,从而导致分割错误。
如果他们没有忽视这个警告,他们就会立即发现这个漏洞!
将警告视为错误只是自律的一种方式:您正在编译一个程序来测试那个闪亮的新功能,但是在您修复那些草率的部分之前,您无法测试。werror提供了其他信息。它只是非常明确地设定了优先级:
在修复现有代码中的问题之前,不要添加新代码
重要的是心态,而不是工具。编译器诊断输出是一种工具。MISRA C(嵌入式C)是另一个工具。使用哪一种并不重要,但可以说编译器警告是最简单的工具(只需设置一个标志),而且信噪比非常高。所以没有理由不使用它。
No tool is infallible. If you write const float pi = 3.14;, most tools won't tell you that you defined π with a bad precision which may lead to problems down the road. Most tools won't raise an eyebrow on if(tmp < 42), even if it's commonly known that giving variables meaningless names and using magic numbers is a way to disaster in big projects. You have to understand that any "quick test" code you write is just that: a test, and you have to get it right before you move on to other tasks, while you still see its shortcomings. If you leave that code as is, debugging it after you spend two months adding new features will be significantly harder.
一旦你进入了正确的心态,使用-Werror就没有意义了。将警告作为警告将允许您做出明智的决定,是否仍然有意义运行您即将开始的调试会话,还是中止它并首先修复警告。
处理警告不仅能写出更好的代码,还能让你成为更好的程序员。警告会告诉你一些今天对你来说微不足道的事情,但总有一天坏习惯会回来咬你的头。
使用正确的类型,返回该值,计算该返回值。花点时间思考“在这种情况下,这真的是正确的类型吗?”“我需要把这个还回去吗?”最重要的是;“这个代码在未来10年里还能移植吗?”
首先要养成编写无警告代码的习惯。
忽略警告意味着您留下了草率的代码,这不仅会在将来给其他人带来问题,而且还会使您不太注意到重要的编译消息。
编译器输出越多,就越不会有人注意到。越干净越好。这也意味着你知道自己在做什么。警告是非常不专业、粗心和危险的。
作为使用遗留嵌入式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 =)
这只是编译器可以为您进行双重检查的许多事情中的三件。还有很多其他的方法,比如使用未初始化的变量。