編寫合格的C代碼(1):經過編譯選項將特定警告視爲錯誤

快速設定

若是你沒興趣/沒時間看具體解釋、只想快速排錯,請明確:這裏列出了我的認爲應當看成error但被C編譯器(少許狀況是C++編譯器)默認設定爲warning的編譯選項(CFLAGS/CXXFLAGS),比「忽略全部warning」要更安全,比開啓「視全部warning爲error」要寬鬆精準。支持包括主流的Visual Studio和GCC這兩個編譯器。程序員

  1. 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()
  1. Visual Studio中的設定

項目屬性->配置屬性->C/C++->高級->將特定的警告視爲錯誤,填入相應的警告、錯誤代號:windows

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安全

  1. 基於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
  1. 直接調用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++共有的問題。bash

編譯警告應當被忽略嗎?warning不重要嗎?

不少程序小白(甚至工做多年的老鳥)認爲:函數

C代碼報error須要消滅掉,報warning沒啥事兒的趕忙提交版本/給QA測試/上線,PM或老闆等着呢/別浪費我沒必要要時間/warning都是雞毛蒜皮問題...佈局

遺憾的是這種想法並不罕見,彷佛以爲「不crash就沒問題」的心態,一旦出問題查起來極可能手忙腳亂,由於crash/bug極可能很差重現(血淚教訓:移植ncnn爲純C代碼,忘記include相應頭文件,手機上運行出現難復現的crash)visual-studio

.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)

內層做用域從新聲明/定義了與外層做用域中同名的變量。

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工程也可配置,具體見文章第一部分「快速配置」。

其餘配置方式說明:

  1. .c代碼中使用#pragma warning (error: xxxx)。缺點:只有visual studio工程能用;不能確保全部文件有效
  2. Visual Studio工程屬性中配置->配置屬性->C/C++->高級->將特定的警告視爲錯誤,填寫"xxxx"。缺點:只適合Visual Studio;優勢:非CMake生成的VS工程,適合。
  3. 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?

Is there an equivalent of gcc's -Wshadow in visual C++

已釋放的棧內存

GCC編譯選項

關於gcc內置函數和c隱式函數聲明的認識以及一些推測

Why do you have to link the math library in C?

相關文章
相關標籤/搜索