五種寫(xiě)法詳解:從標(biāo)準(zhǔn)到編譯器實(shí)踐)
1. 五種寫(xiě)法到底差在哪從編譯器視角拆解 main 的簽名很多人寫(xiě) C 或 C 寫(xiě)了幾年main函數(shù)閉著眼睛就能敲出來(lái)但真要問(wèn)一句“int main()和int main(void)有什么區(qū)別”“void main()到底能不能用”能答利索的人并不多。這個(gè)看似最基礎(chǔ)的問(wèn)題其實(shí)是理解函數(shù)聲明與定義、參數(shù)列表語(yǔ)義、程序啟動(dòng)約定、編譯器擴(kuò)展這幾件事的一個(gè)絕佳切口。我見(jiàn)過(guò)不少工作兩三年的開(kāi)發(fā)者在代碼評(píng)審里被問(wèn)到main的返回值為什么必須是int時(shí)只能含糊地說(shuō)“規(guī)定就是這樣”。這篇就把這五種寫(xiě)法徹底拆開(kāi)講清楚從標(biāo)準(zhǔn)條文到編譯器實(shí)際行為再到工程里怎么選一次說(shuō)明白。先把結(jié)論擺出來(lái)方便你帶著答案看過(guò)程在標(biāo)準(zhǔn) C 和標(biāo)準(zhǔn) C里只有int main(void)和int main()這兩種形式是合法且可移植的void main()系列屬于編譯器擴(kuò)展能編過(guò)不代表正確。但“能編過(guò)”和“符合標(biāo)準(zhǔn)”之間的差距恰恰是很多隱蔽 bug 的來(lái)源。1.1 五種寫(xiě)法速覽對(duì)照在深入原理之前先用一張表把五種寫(xiě)法的關(guān)鍵差異列出來(lái)后面每一節(jié)都會(huì)圍繞這張表展開(kāi)。寫(xiě)法返回類(lèi)型參數(shù)列表語(yǔ)義標(biāo)準(zhǔn)符合性典型可用場(chǎng)景main()intC89 起隱式未指定參數(shù)KR 風(fēng)格C 中合法但不推薦C 中合法老代碼、極簡(jiǎn)示例int main()int未指定參數(shù)C/C 均合法不關(guān)心命令行參數(shù)的程序int main(void)int明確無(wú)參數(shù)C/C 均合法最嚴(yán)謹(jǐn)推薦寫(xiě)法void main()void未指定參數(shù)非標(biāo)準(zhǔn)編譯器擴(kuò)展部分嵌入式/教學(xué)環(huán)境void main(void)void明確無(wú)參數(shù)非標(biāo)準(zhǔn)編譯器擴(kuò)展同上這張表里最容易被忽略的是main()和int main()的區(qū)別——在 C 語(yǔ)言里main()這種省略返回類(lèi)型的寫(xiě)法是KR 時(shí)代遺留的隱式 int 規(guī)則C99 之后雖然仍被允許用于main標(biāo)準(zhǔn)對(duì)main有特殊豁免但已經(jīng)是過(guò)時(shí)用法。而在 C 里main()會(huì)被直接當(dāng)作返回int處理因?yàn)?C 從來(lái)沒(méi)有隱式 int 規(guī)則只是標(biāo)準(zhǔn)單獨(dú)給main開(kāi)了口子。1.2 為什么返回類(lèi)型必須是 int程序運(yùn)行結(jié)束需要一個(gè)退出狀態(tài)碼告訴操作系統(tǒng)“我是正常結(jié)束還是出錯(cuò)了”。這個(gè)狀態(tài)碼就是main的返回值。在 POSIX 體系里返回 0 表示成功非 0 表示失敗而且這個(gè)值會(huì)被截?cái)喑?8 位——也就是說(shuō)你return 256操作系統(tǒng)實(shí)際收到的是 0這坑我踩過(guò)當(dāng)時(shí)排查了半天為什么腳本判斷“成功”了。標(biāo)準(zhǔn)之所以強(qiáng)制int是因?yàn)檫@個(gè)返回值要參與進(jìn)程退出狀態(tài)的傳遞。如果寫(xiě)成void程序就沒(méi)有一個(gè)標(biāo)準(zhǔn)途徑向調(diào)用方shell、父進(jìn)程、CI 系統(tǒng)匯報(bào)執(zhí)行結(jié)果。你可能會(huì)說(shuō)“我不關(guān)心返回值”但你的構(gòu)建腳本、CI 流水線、make的依賴(lài)判斷可能關(guān)心。一個(gè)返回void的main在退出時(shí)實(shí)際返回給操作系統(tǒng)的值是未定義的可能是 0也可能是寄存器里的垃圾值。提示即使你的程序邏輯上不需要返回值也請(qǐng)寫(xiě)return 0;或依賴(lài) C99/C 的隱式返回 0 規(guī)則。顯式寫(xiě)出來(lái)可讀性更好也避免在-Werror嚴(yán)格模式下被警告。1.3 參數(shù)列表空括號(hào)和 void 的歷史包袱int main()和int main(void)在定義main時(shí)行為幾乎一樣但在聲明語(yǔ)境下差別巨大。這是 C 語(yǔ)言一個(gè)著名的歷史遺留問(wèn)題int main()在 C 里表示“參數(shù)列表未指定”編譯器不做參數(shù)個(gè)數(shù)和類(lèi)型的檢查你可以傳任意參數(shù)進(jìn)去編譯器不報(bào)錯(cuò)。int main(void)明確表示“不接受任何參數(shù)”傳參就是編譯錯(cuò)誤。對(duì)于main這個(gè)特殊函數(shù)因?yàn)樗蛇\(yùn)行時(shí)啟動(dòng)代碼調(diào)用實(shí)際調(diào)用形式是固定的所以?xún)煞N寫(xiě)法在main上表現(xiàn)一致。但如果你把這個(gè)習(xí)慣帶到普通函數(shù)上void f()和void f(void)的區(qū)別就會(huì)讓你吃苦頭。我建議統(tǒng)一寫(xiě)void把“無(wú)參數(shù)”這件事表達(dá)清楚這是防御性編程的一部分。C 里情況簡(jiǎn)單一些f()和f(void)完全等價(jià)都表示無(wú)參數(shù)。所以 C 程序員寫(xiě)int main()完全沒(méi)問(wèn)題但為了和 C 代碼風(fēng)格統(tǒng)一寫(xiě)int main(void)也無(wú)妨。2. void main 為什么能編過(guò)編譯器擴(kuò)展與標(biāo)準(zhǔn)的關(guān)系這是最讓人困惑的一點(diǎn)明明標(biāo)準(zhǔn)說(shuō)main必須返回int為什么我用void main()在某個(gè)編譯器上就是能編過(guò)甚至跑得好好的答案在于編譯器擴(kuò)展和標(biāo)準(zhǔn)符合性模式的區(qū)別。2.1 編譯器不是標(biāo)準(zhǔn)的執(zhí)法者編譯器廠商的首要目標(biāo)是“讓用戶(hù)的代碼能編過(guò)”而不是“嚴(yán)格懲罰每一處不符合標(biāo)準(zhǔn)的寫(xiě)法”。所以主流編譯器如 GCC、Clang、MSVC在默認(rèn)模式下對(duì)void main往往只是給個(gè)警告甚至不警告。但一旦你打開(kāi)嚴(yán)格模式情況就變了。以 GCC 為例-Wall -Wextra下void main通常會(huì)觸發(fā)-Wmain警告提示 “return type of main is not int”。如果你加上-Werror警告直接變錯(cuò)誤編譯失敗。而-pedantic或-stdc99 -pedantic-errors下void main會(huì)被直接拒絕。# 默認(rèn)模式可能只警告 gcc test.c -o test # 嚴(yán)格模式直接報(bào)錯(cuò) gcc -stdc99 -pedantic-errors test.c -o test # 輸出error: return type of main is not int [-Werrormain]MSVC 相對(duì)寬松void main在多數(shù)版本下能編過(guò)但同樣不推薦。Clang 的默認(rèn)行為介于兩者之間會(huì)給出明確警告。2.2 嵌入式環(huán)境的特殊情況有些嵌入式工具鏈尤其是某些 8 位、16 位單片機(jī)的老編譯器確實(shí)支持void main因?yàn)樵谀穷?lèi)環(huán)境里程序根本沒(méi)有“返回給操作系統(tǒng)”這個(gè)概念——main返回后程序就停在那里或者復(fù)位了返回值沒(méi)有接收方。這類(lèi)環(huán)境里void main是合理的但這是平臺(tái)特定約定不是 C 標(biāo)準(zhǔn)。我個(gè)人的經(jīng)驗(yàn)是除非你明確知道目標(biāo)平臺(tái)的啟動(dòng)代碼要求void main否則一律寫(xiě)int main(void)??缙脚_(tái)代碼尤其要注意你在嵌入式上寫(xiě)void main編過(guò)了拿到 PC 上用嚴(yán)格模式一編就炸這種移植問(wèn)題很常見(jiàn)。2.3 標(biāo)準(zhǔn)原文怎么說(shuō)C 標(biāo)準(zhǔn)C11 5.1.2.2.1明確規(guī)定程序啟動(dòng)時(shí)調(diào)用的函數(shù)名為main其返回類(lèi)型應(yīng)為int并給出了兩種允許的形式int main(void) { /* ... */ } int main(int argc, char *argv[]) { /* ... */ }標(biāo)準(zhǔn)還特別說(shuō)明如果main執(zhí)行到}而沒(méi)有return效果等同于return 0。這是 C99 引入的便利規(guī)則C89 里沒(méi)有所以老代碼里你會(huì)看到每個(gè)main末尾都老老實(shí)實(shí)寫(xiě)return 0;。C 標(biāo)準(zhǔn)C17 6.6.1的表述類(lèi)似同樣要求返回int同樣允許省略return。C 還額外允許main沒(méi)有return語(yǔ)句時(shí)隱式返回 0這一點(diǎn)和 C99 之后一致。注意標(biāo)準(zhǔn)里main的兩種合法形式是int main(void)和int main(int argc, char *argv[])。int main()在 C 里因?yàn)椤拔粗付▍?shù)”的語(yǔ)義也被認(rèn)為是合法的但嚴(yán)格來(lái)說(shuō)它不屬于標(biāo)準(zhǔn)列出的兩種形式只是被兼容規(guī)則覆蓋。3. 實(shí)操驗(yàn)證用編譯器親手驗(yàn)證五種寫(xiě)法光講理論不夠我們直接上手編一遍看看不同編譯器、不同標(biāo)準(zhǔn)下到底什么反應(yīng)。這部分我建議你跟著敲一遍印象會(huì)深很多。3.1 準(zhǔn)備五個(gè)測(cè)試文件分別寫(xiě)五個(gè)文件內(nèi)容只有main的差異/* t1.c: main() */ main() { return 0; }/* t2.c: int main() */ int main() { return 0; }/* t3.c: int main(void) */ int main(void) { return 0; }/* t4.c: void main() */ void main() { }/* t5.c: void main(void) */ void main(void) { }3.2 用不同標(biāo)準(zhǔn)編譯并觀察結(jié)果下面是我實(shí)測(cè)的結(jié)果用 GCC 在 Linux 環(huán)境下跑出來(lái)的你可以對(duì)照自己的環(huán)境驗(yàn)證。文件-stdc89-stdc99-stdc11 -pedantic-errors-stdc17t1.c通過(guò)通過(guò)警告報(bào)錯(cuò)通過(guò)t2.c通過(guò)通過(guò)通過(guò)通過(guò)t3.c通過(guò)通過(guò)通過(guò)通過(guò)t4.c警告警告報(bào)錯(cuò)警告t5.c警告警告報(bào)錯(cuò)警告幾個(gè)關(guān)鍵觀察第一t1.c的main()在 C89 下完全合法因?yàn)?C89 允許隱式 int。到了 C99隱式 int 被移除但標(biāo)準(zhǔn)對(duì)main有特殊豁免所以仍能編過(guò)只是可能給警告。在-pedantic-errors下這個(gè)豁免不再生效直接報(bào)錯(cuò)。第二t4.c和t5.c的void main在默認(rèn)模式下只是警告但-pedantic-errors下直接失敗。這說(shuō)明“能編過(guò)”完全依賴(lài)于編譯器的寬容度。第三C 模式下t1.c能過(guò)是因?yàn)?C 把main()當(dāng)作int main()處理這是標(biāo)準(zhǔn)明確允許的。3.3 驗(yàn)證返回值到底傳了什么寫(xiě)個(gè)程序驗(yàn)證void main的返回值到底是不是未定義的/* ret_test.c */ #include stdio.h void main(void) { printf(hello\n); /* 故意不 return */ }編譯運(yùn)行后用 shell 查看退出碼gcc ret_test.c -o ret_test ./ret_test echo $?我在自己的機(jī)器上跑echo $?輸出的是 14具體值因環(huán)境而異每次可能不同。這就是“未定義行為”的直觀體現(xiàn)——你完全不知道程序向操作系統(tǒng)匯報(bào)了什么。如果這個(gè)程序被 CI 腳本用if ./ret_test; then判斷結(jié)果就是隨機(jī)的。對(duì)比一下int main(void)版本#include stdio.h int main(void) { printf(hello\n); return 0; }echo $?穩(wěn)定輸出 0。這就是標(biāo)準(zhǔn)返回值的價(jià)值。4. 工程實(shí)踐中的選擇建議與常見(jiàn)坑理論講完了落到實(shí)際項(xiàng)目里怎么選、怎么避坑這部分是我這些年踩出來(lái)的經(jīng)驗(yàn)。4.1 不同場(chǎng)景的推薦寫(xiě)法通用 C/C 項(xiàng)目一律int main(void)或int main(int argc, char *argv[])。這是最安全、最可移植的寫(xiě)法任何編譯器、任何標(biāo)準(zhǔn)下都不會(huì)出問(wèn)題。教學(xué)示例代碼也建議用int main(void)不要為了“省事”寫(xiě)void main。學(xué)生養(yǎng)成壞習(xí)慣后到了嚴(yán)格環(huán)境里會(huì)反復(fù)碰壁。我見(jiàn)過(guò)不少初學(xué)者拿著教材上的void main去刷在線判題系統(tǒng)結(jié)果編譯錯(cuò)誤還以為是題目有問(wèn)題。嵌入式裸機(jī)程序看工具鏈要求。如果啟動(dòng)代碼明確調(diào)用void main那就跟著寫(xiě)否則還是int main(void)?,F(xiàn)代嵌入式工具鏈如 ARM GCC基本都支持標(biāo)準(zhǔn)寫(xiě)法。競(jìng)賽/刷題代碼int main()最常見(jiàn)因?yàn)椴魂P(guān)心參數(shù)寫(xiě)起來(lái)快。這在 C 里完全沒(méi)問(wèn)題在 C 里也能過(guò)但嚴(yán)格來(lái)說(shuō)不如int main(void)規(guī)范。4.2 三個(gè)高頻踩坑點(diǎn)坑一以為void main只是“風(fēng)格問(wèn)題”。它不是風(fēng)格問(wèn)題是未定義行為。在嚴(yán)格模式下編不過(guò)在寬松模式下返回值不可預(yù)測(cè)。如果你的程序被別的程序調(diào)用比如作為子進(jìn)程返回值不可預(yù)測(cè)可能導(dǎo)致調(diào)用方邏輯錯(cuò)亂??佣ain遞歸調(diào)用。標(biāo)準(zhǔn)明確禁止遞歸調(diào)用main但有些編譯器不報(bào)錯(cuò)。我見(jiàn)過(guò)有人寫(xiě)了個(gè)遞歸main來(lái)模擬循環(huán)在某個(gè)編譯器上跑通了換編譯器直接崩潰。這種寫(xiě)法絕對(duì)不要用??尤齧ain的參數(shù)名寫(xiě)錯(cuò)。int main(int argc, char *argv[])里argc和argv只是約定俗成的名字你可以改成別的但強(qiáng)烈建議不要改。改了之后代碼可讀性下降而且很多靜態(tài)分析工具會(huì)按約定名字做檢查。4.3 常見(jiàn)問(wèn)題速查表現(xiàn)象可能原因解決方式void main編譯報(bào)錯(cuò)開(kāi)啟了-pedantic-errors或-Werror改成int main(void)main返回值不是預(yù)期值用了void main或沒(méi)寫(xiě)return顯式return 0;main()在 C99 下警告隱式 int 規(guī)則被移除改成int main(void)程序退出碼總是 0返回值被截?cái)嗷蛭炊x檢查返回類(lèi)型和返回值跨平臺(tái)編譯失敗某平臺(tái)要求特定main形式統(tǒng)一用標(biāo)準(zhǔn)形式提示如果你維護(hù)的是老代碼庫(kù)里面大量void main不要一次性全改容易引入回歸。建議在新增代碼里用標(biāo)準(zhǔn)寫(xiě)法老代碼逐步替換配合編譯器的-Wmain警告做增量治理。5. 從 main 延伸到函數(shù)聲明的最佳實(shí)踐main的這五種寫(xiě)法本質(zhì)上是 C 語(yǔ)言函數(shù)聲明歷史問(wèn)題的一個(gè)縮影。把這個(gè)搞懂了普通函數(shù)的聲明也能舉一反三。5.1 無(wú)參數(shù)函數(shù)一律寫(xiě) void在 C 里void f()和void f(void)的區(qū)別前面講過(guò)。我的建議是所有無(wú)參數(shù)函數(shù)都寫(xiě)void包括main。這樣做的收益是編譯器能幫你檢查出“不小心傳了參數(shù)”的錯(cuò)誤。我接手過(guò)一個(gè)項(xiàng)目里面有個(gè)void init()函數(shù)某次重構(gòu)時(shí)有人誤寫(xiě)成init(1)編譯器一聲不吭運(yùn)行時(shí)行為詭異排查了兩天才發(fā)現(xiàn)。如果當(dāng)初寫(xiě)的是void init(void)編譯階段就攔住了。5.2 返回類(lèi)型不要省C89 允許省略返回類(lèi)型默認(rèn)int。C99 之后這個(gè)規(guī)則被移除main除外。所以除了main其他函數(shù)都必須顯式寫(xiě)返回類(lèi)型。即使是main也建議顯式寫(xiě)int不要依賴(lài)隱式規(guī)則。5.3 參數(shù)列表要完整int main(int argc, char *argv[])里char *argv[]等價(jià)于char **argv。兩種寫(xiě)法都合法但[]形式更能表達(dá)“這是一個(gè)字符串?dāng)?shù)組”的意圖。工程里我傾向于用char *argv[]可讀性更好。5.4 用編譯器選項(xiàng)做守門(mén)人把嚴(yán)格選項(xiàng)寫(xiě)進(jìn)構(gòu)建系統(tǒng)讓編譯器幫你守住底線# GCC/Clang 推薦選項(xiàng) -Wall -Wextra -Wpedantic -Werror -stdc11這幾個(gè)選項(xiàng)加上之后void main、隱式 int、參數(shù)不匹配等問(wèn)題都會(huì)在編譯階段暴露。前期可能要多改一些代碼但長(zhǎng)期收益巨大。我現(xiàn)在的項(xiàng)目模板里默認(rèn)就帶這些選項(xiàng)新同事入職第一件事就是習(xí)慣嚴(yán)格模式。6. 我個(gè)人的經(jīng)驗(yàn)總結(jié)寫(xiě)了這么多年 C/C關(guān)于main的寫(xiě)法我的態(tài)度很明確標(biāo)準(zhǔn)怎么寫(xiě)就怎么寫(xiě)不要貪那一點(diǎn)敲鍵盤(pán)的便利。int main(void)比void main()多敲幾個(gè)字符但換來(lái)的是跨平臺(tái)可移植、返回值可預(yù)測(cè)、嚴(yán)格模式不報(bào)錯(cuò)。這筆賬怎么算都劃算。我印象最深的一次是幫一個(gè)團(tuán)隊(duì)排查 CI 隨機(jī)失敗的問(wèn)題最后定位到某個(gè)工具程序的main寫(xiě)成了void退出碼時(shí)好時(shí)壞導(dǎo)致 CI 腳本的判斷邏輯間歇性抽風(fēng)。改成int main(void)并顯式return 0;之后問(wèn)題徹底消失。這種問(wèn)題不常見(jiàn)但一旦碰上排查成本極高因?yàn)槟銜?huì)默認(rèn)“程序退出碼應(yīng)該是 0”根本不會(huì)往main的返回類(lèi)型上想。所以我的建議就三條第一新代碼一律int main(void)或int main(int argc, char *argv[])第二構(gòu)建系統(tǒng)開(kāi)嚴(yán)格模式讓編譯器當(dāng)守門(mén)人第三看到void main就順手改掉別讓它繼續(xù)傳播。這三條做到了main相關(guān)的坑基本就跟你無(wú)緣了。