快速設定
如果你沒興趣/沒時間看具體解釋、只想快速排錯,請明確:這里列出了個人認為應當當作error但被C編譯器(少量情況是C++編譯器)默認設定為warning的編譯選項(CFLAGS/CXXFLAGS),比“忽略所有warning”要更安全,比開啟“視所有warning為error”要寬松精准。支持包括主流的Visual Studio和GCC這兩個編譯器。
- CMakeLists.txt中的設定
if (CMAKE_SYSTEM_NAME MATCHES "Windows")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /we4013 /we4431 /we4133 /we4716 /we6244 /we6246 /we4457 /we4456 /we4172 /we4700 /we4477 /we4018 /we4047")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} /we4013 /we4431 /we4133 /we4716 /we6244 /we6246 /we4457 /we4456 /we4172 /we4700 /we4477 /we4018 /we4047")
elseif (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Darwin")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=implicit-function-declaration -Werror=implicit-int -Werror=incompatible-pointer-types -Werror=return-type -Werror=shadow -Werror=return-local-addr -Werror=uninitialized -Werror=format -Werror=sign-compare -Werror=int-conversion")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -Werror=implicit-function-declaration -Werror=implicit-int -Werror=incompatible-pointer-types -Werror=return-type -Werror=shadow -Werror=return-local-addr -Werror=uninitialized -Werror=format -Werror=sign-compare -Werror=int-conversion")
endif()
- Visual Studio中的設定
項目屬性->配置屬性->C/C++->高級->將特定的警告視為錯誤,填入相應的警告、錯誤代號:
4013;4431;4133;4716;6244;6246;4457;4456;4172;4700;4477;4018;4047;4013;4431;4133;4716;6244;6246;4457;4456;4172;4700;4477;4018;4047
- 基於Makefile
CFLAGS += -Werror=implicit-function-declaration -Werror=implicit-int -Werror=incompatible-pointer-types -Werror=return-type -Werror=shadow -Werror=return-local-addr -Werror=uninitialized -Werror=format -Werror=sign-compare -Werror=int-conversion
- 直接調用gcc/clang
gcc xxx.c -Werror=implicit-function-declaration -Werror=implicit-int -Werror=incompatible-pointer-types -Werror=return-type -Werror=shadow -Werror=return-local-addr -Werror=uninitialized -Werror=format -Werror=sign-compare -Werror=int-conversion
向錯誤的執念開炮,向C編譯器開炮
說說為什么要定制上面一大串CFLAGS/CXXFLAGS:默認的CFLAGS/CXXFLAGS過分相信程序員,而小白則無法駕馭。問題比較嚴重的是純C的代碼,C++稍微好一些,因此這里主要說C特有的,剩余少量的是C/C++共有的問題。
編譯警告應當被忽略嗎?warning不重要嗎?
很多程序小白(甚至工作多年的老鳥)認為:
C代碼報error需要消滅掉,報warning沒啥事兒的趕緊提交版本/給QA測試/上線,PM或老板等着呢/別浪費我不必要時間/warning都是雞毛蒜皮問題...
遺憾的是這種想法並不罕見,似乎覺得“不crash就沒問題”的心態,一旦出問題查起來很可能手忙腳亂,因為crash/bug很可能不好重現(血淚教訓:移植ncnn為純C代碼,忘記include相應頭文件,手機上運行出現難復現的crash)
.c文件被C編譯(而不是C++編譯器)編譯。最常見的case是(純C代碼,C++沒有這個問題):沒有找到函數聲明的情況下調用函數。也就是,沒有實現函數xx()
,或者實現了函數但是沒有#include
頭文件,然后調用xx()
。細分下來又有這幾種情況:
- (1)編譯目標是庫文件(而不是可執行文件),
xx()
不是編譯器內置函數;編譯階段僅僅報warning,運行時結果不對/不穩定 - (2)編譯目標是可執行文件,
xx()
不是編譯器內置函數;鏈接階段報錯說找不到符號(函數定義) - (3)編譯目標是庫文件(而不是可執行文件),
xx()
是編譯器內置函數;編譯階段僅僅報warning,運行時結果正確 - (4)編譯目標是可執行文件,
xx()
是編譯器內置函數;編譯階段報warning;運行時結果正確
上述四種情況我們一一舉例說明。每個例子都基於CMake構建。
(1)編庫時調用了未定義函數(非編譯器內置函數),編譯只報warning;鏈接該庫時報error
CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(hoho)
add_library(hoholib src/hoholib.c)
add_executable(hohoexe src/hohoexe.c)
target_link_libraries(hohoexe hoholib)
hoholib.c
void hello() {
const char* name = "Chris";
print_hello(name);
}
hohoexe.c
#include <stdio.h>
int main() {
hello();
return 0;
}
VS2017編譯輸出:
GCC編譯輸出:
可以看到,問題在鏈接階段才會報error,編譯階段僅報warning。編庫是不需要鏈接的,只需要編譯。如果忽略編庫階段的上述warning那就是埋雷。
(2)編庫時調用了未定義函數(編譯器內置同名函數),編譯只報warning;鏈接該庫時報error
首先明確下什么是編譯器內置函數:對於gcc而言,定義了printf、fabs等函數,而這些函數是在C標准庫、math庫中定義的,gcc為了優化而提供了自己的實現,而如果用戶沒有鏈接相應的庫、沒有包含相應的頭文件,則鏈接階段找不到對應的符號表,但能找到built-in函數,因而直接調用built-in函數。這就是為什么“把(1)中調用的未定義函數換成fabs、printf等函數,gcc下鏈接階段也不會報錯反而能正確輸出結果”的原因。參考:關於gcc內置函數和c隱式函數聲明的認識以及一些推測
遺憾的是,這種取巧的做法對於Visual Studio行不通,因為cl.exe並沒有和gcc完全相同的編譯器內置函數。cl.exe的編譯器內置函數叫做Compiler Intrinsics,並沒有定義printf、fabs等函數。這就解釋了“為什么調用了printf、fabs等gcc內置同名函數的代碼,gcc下鏈接正常運行正確但在VS下鏈接出錯”。
還是上面的CMake配置,C代碼為:
hoholib.c
void hello() {
const char* name = "Chris";
printf("hello, %s\n", name);
}
hohoexe.c
#include <stdio.h>
int main() {
hello();
return 0;
}
VS下編譯報錯,gcc下則編譯鏈接都無error,可以運行並得到預期結果。
(3)編可執行時.c代碼中使用了未定義的函數(編譯器內置同名函數)
這種情況下,gcc編譯鏈接無error且結果正確,VS則可能編譯就報錯,也可能編譯鏈接通過但結果不對。
cmake_minimum_required(VERSION 3.14)
project(hoho)
add_executable(hohoexe src/hohoexe.c)
如果hohoexe.c的代碼是這樣:
int main() {
const char* name = "Chris";
printf("hello, %s\n", name);
return 0;
}
則,VS下編譯報錯,gcc下編譯鏈接無error且結果符合預期。
如果hohoexe.c的代碼是這樣:
#include <stdio.h>
int main() {
double x = -3.3;
double y = fabs(x);
printf("fabs(%lf)=%lf\n", x, y);
return 0;
}
則VS下編譯鏈接無error但結果不對:
fabs(-3.300000)=-858993460.000000
(4)編可執行時.c代碼中使用了未定義的函數(編譯器無內置同名函數)
這種情況下,VS和gcc都直接編譯報錯,沒什么好說的:
#include <stdio.h>
int main() {
const char* name = "Chris";
print_hello(name);
return 0;
}
簡單總結一下上述(1)~(4):對於printf、fabs、sin等常見函數,gcc有內置函數的實現使得一些代碼盡管報warning但也能運行;同樣的代碼在Visual Studio下沒法編譯鏈接;對於用戶自定義的函數,如果是編庫,則編譯階段只報warning不報error,如果是可執行程序則會報error。對於小白和老菜鳥們,應該無論如何都把“未聲明函數就使用”強制作為error,絕對不虧。C編譯器的這個現象不免讓人疑惑:你這該報錯的不報錯,誤導人啊!然而有種說法是為了兼容老版本代碼。嗯,簡直無語的C編譯器默認編譯選項!
被C編譯器默認報為warning而不是error、但實際上又很重要的編譯選項,還有很多,而其中很多編譯選項在C++中是默認為error的。如果項目允許,不妨使用C++編譯器。而對於必須使用純C的項目,就需要把C編譯器中的這些嚴重warning都設定為error,提前發現問題解決問題。
個人總結的應當視作error的warning
下列警告應當視作錯誤(血淚教訓):
1. 函數沒有聲明就使用
VS下為/we4013
。gcc下用-Werror=implicit-function-declaration
2. 函數雖然有聲明,但是聲明不完整,沒有寫出返回值類型。
VS下開關為/we4431
。gcc下用-Werror=implicit-int
。注:其實implicit-function-declaration和implicit-int可以用一個implicit來替代。
3. 指針類型不兼容
VS下為/we4133
。gcc下用-Werror=incompatible-pointer-types
4. 函數應該有返回值但是沒有return返回值
VS下為/we4716
。gcc下用-Werror=return-type
5. 使用了影子變量(shadow variable)
內層作用域重新聲明/定義了與外層作用域中同名的變量。舉一個例子說明shadow變量的危害:
void set_value(int* val) {
double r = 0.0;
if(isRandom) {
double r = this->generateRandomNumber();
}
*val = r;
}
上述代碼運行后,val的值始終是0而不可能被改成隨機值。
VS下有好幾個開關:/we6244 /we6246 /we4457 /we4456
(MSDN上還有個 /we2082但實際用的時候提示無效: 命令行 warning D9014: 值“2082”對於“/we”無效;假定為“5999”)。gcc下用-Werror=shadow
6. 函數返回局部變量的地址
VS下的開關:/we4172
。gcc下用-Werror=shadow -Werror=return-local-addr
。
7. 變量沒有初始化就使用
函數調用完畢,無法保證用過的棧幀空間后續被如何使用(編譯器是否開啟優化、棧幀布局結構都有影響),不可僥幸。
VS下的開關:/we4700
。gcc下用-Werror=uninitialized
。
8. printf等語句中的格式串和實參類型不匹配
例如%d匹配到了double,結果肯定不對,應當提前檢查出來。
VS下的開關:/we4477
。gcc下用-Werror=format
。
9. 把unsigned int和int類型的兩個變量比較
有符號數可能在比較之前被轉換為無符號數而導致結果錯誤。
VS下的開關:/we4018
。gcc下用-Werror=sign-compare
。
10. 把int指針和int相互賦值
雖說可以把指針的值(一個地址)當做一個int(其實是unsigned int)來理解,但考慮這種情況:int a=*p被寫成int a=p而引發錯誤。
VS下的開關:/we4047
。gcc下用-Werror=int-conversion
。
因為上述N條規則是我自行制定的,有些是C++下默認視為錯誤,有些則是C++下也為警告。因此不妨把CFLAGS和CXXFLAGS都添加這些檢查規則。
在開發環境中配置上述CFLAGS
建議基於CMakeLists.txt,現有Visual Studio工程也可配置,具體見文章第一部分“快速配置”。
其他配置方式說明:
- .c代碼中使用
#pragma warning (error: xxxx)
。缺點:只有visual studio工程能用;不能確保所有文件有效 - Visual Studio工程屬性中配置->配置屬性->C/C++->高級->將特定的警告視為錯誤,填寫"xxxx"。缺點:只適合Visual Studio;優點:非CMake生成的VS工程,適合。
- gcc編譯時指定flags,例如
gcc gcc xxx.c -Werror=implicit-function-declaration
CMakeLists.txt中配置說明:set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /weXXXX")
(windows)或set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=xxxx")
。
其中,windows格式中XXXX為警告編號;gcc下xxxx為警告對應的字符串。這種方式個人推薦。
TODO
C++編譯器默認鏈接C++標准庫,C++標准庫包含了math庫;C編譯器默認鏈接C標准庫,C標准庫不包含math庫(參考:Why do you have to link the math library in C?)。問題來了:對於gcc,如果純C代碼調用了math函數而沒有設定鏈接選項-lm
,會使用gcc的built-in函數;同樣的代碼,VS2017並沒有內置math庫的函數,沒有鏈接數學庫的秦廣下,為什么也能正確運行?
#include <stdio.h>
#include <math.h>
int main() {
double x = -3.3;
double y = fabs(3.3);
printf("fabs(%lf)=%lf\n", x, y);
return 0;
}
參考
/w, /W0, /W1, /W2, /W3, /W4, /w1, /w2, /w3, /w4, /Wall, /wd, /we, /wo, /Wv, /WX (Warning Level)
How to set compiler options with CMake in Visual Studio 2017
Make one gcc warning an error?
Can I treat a specific warning as an error?