C語言如何不用goto、多處return進行錯誤處理?
碼農一枚。
我司規定C代碼中不得出現多個return,不能出現goto。那麼一個函數若是可能出現多個錯誤情況時如何處理?Linux內核驅動中大量使用了goto來進行錯誤處理,這又是我司不允許的。如下情況:
int foo()
{
int err = 0; // 錯誤碼
int *pArray = NULL;pArray = (int *)malloc(100 * sizeof(int));
//如何優雅的處理pArray不為NULL並返回錯誤碼...
//有可能出現其他錯誤,如何處理錯誤並返回錯誤碼
...return err; //僅在最後返回錯誤碼
}
解鈴還需系鈴人。不妨問你們公司定下這個規矩的人。
你們公司是做汽車電子的嗎?禁止goto和多處return是MISRA C(https://en.wikipedia.org/wiki/MISRA_C)里規定的(感謝Enzo Jiang指出該條款是引用自IEC 61508)。
以下引用自MISRA C 2004版:Rule 14.4 (required):
The goto statement shall not be used.禁止使用goto語句。Rule 14.7 (required): A function shall have a single point of exit at the end of the function.函數只允許有一個位於函數末尾的出口(也就是return)
由於MISRA基本上是屬於汽車嵌入式行業規範,所以如果你是混這圈的那不遵守也不行。這個規定也不是完全沒有道理,goto和多處return確實容易出現資源忘記回收導致內存泄露之類的問題。
解決方法嘛,可以用do while 0套在外面,用break來提前跳出。不過為了避免return而祭出do while 0大法有點利用語法鑽規則空子的味道。int maki() {
int err = 0;
do {
...
...
if(...) {
err = 1;
break;
}
...
...
if(...) {
err = 2;
break;
}
...
...
} while(0);
...
...
return err;
}
int nico() {
int err = 0;
...
...
if(...) {
err = 1;
}
if(!err) {
...
...
if(...) {
err = 2;
}
}
if(!err) {
...
...
if(...) {
err = 3;
}
}
...
...
return err;
}
- do { if (...) break; } while(0); 可以做到一層跳出,多層跳出要變數。
- 把函數內部盡量 extract function 重構,每個小函數的返回控制就會變得簡單。這類重構可能有函數調用的性能代價。
常用的規避這個操蛋的規定的方法是用 do { } while(0) 配合 break 。
不過,這很可能不是這個規定的本意。要追求本意只能問哪個當初定出這個規定的傢伙,這是個非常古老的規定,不過我總覺得如果他活在今天,重新制定這個規定,也未必還會堅持這麼想了。
我認為這個規定的本意其實是:因為不使用多處 return 會造成縮進級別增加,而這就強迫了你把較深層級別的邏輯單獨拉出來寫成函數。
換句話說,本意是為了強制你產生更多的縮進,而更多的縮進就強迫了很多人將內層代碼獨立成函數,從而避免單一函數過長。
舉個例子:
int foo(char *a, int b, int c)
{
if (a==NULL) return 1;
if (b==0) return 2;
if (c&<0) return 3;
// blabla...
return 0;
}
如果禁止 多處 return ,就只好寫成:
int foo(char *a, int b, int c)
{
int flag=0;
if (a!=NULL) {
if (b!=0) {
if (c&>=0) {
// blabla...
} else {
flag = 3;
}
} else {
flag = 2;
}
} else {
flag = 1;
}
return flag;
}
在後面一個例子中第三層嵌套內的代碼如果很多,很有可能有人會考慮把它單獨拿出來成為一個函數,因而一個函數就從此變成了兩個函數!代碼量增加了,單個函數也縮短了,看起來皆大歡喜有木有!
我相信很少有人會覺得,後面一種寫法是必要的。同時我也相信很少有人會覺得後面一種寫法更清晰。不過後面一種寫法,如果按行數算工作量的話,明顯能算更多工作量對不對?或許這才是制定這種規定的理由?
當然有些人可能認為本意是為了避免忘記沒有釋放的資源,不過現實有很多例子表明禁止使用return跟goto同樣容易忘記,或者說反而更容易忘記。個人認為這個出發點不太有道理。
這裡插個嘴,很多人批評這個規定很SB,也有人提到了這個規定在MISRA裡面存在。事實上這個規定也不是MISRA規定的,而是MISRA的母規範IEC 61508規定的。MISRA這麼規定也只是為了合規。
而IEC 61508的涉及面就比較廣泛了,適用領域包括1.汽車 2.鐵路 3.核電 4.機電系統 5.製造業儀錶 這五個領域的嵌入式系統的軟體開發方法規範,這個規範是用來規定其子規範的,所以基本裡面的所有話都成了金句。其中對於編碼方式做了限制(除了單點退出,禁止goto,還包括限制指針的使用(需要證明非用不可)、限制庫的使用、禁止對象的使用等,還有一些條款要求函數的調用周期是常數並且可以計算出來的)。而這個標準制定的時間比較早,儘管有過好幾次修訂(最近一次是2012年),但是因為涉及到安全,在best-practice方面的規定會趨向保守,有些甚至是反常識(包括反生產力)的。另外IEC 61508不針對特定語言(前兩個版本中C語言在裡面不屬於推薦語言,推薦語言是Ada,2012年增加了Java和限定了庫的使用的C作為推薦語言),所以規定比較繁瑣。
不過這個標準裡面還有句很有意思的話:所有在使用中被證明可靠的代碼可以不遵循那些規範,但推薦重構的時候遵守。
事實上IEC 61508對於代碼是否安全的衡量最終標準是 一個很大機時下的故障率。
另外如果真的要問為什麼會有人提出這個規定的,你們去問Dijkstra。。。這個觀點提出的時候,主要是用彙編和FORTRAN寫程序的。。我大概知道為什麼會有這種規定了,單處 return 加禁止 goto,等於要求寫這樣的代碼:
void foobar() {
void* a = 0, b = 0, c = 0;
do {
a = createA();
if (!a) break;
b = createB();
if (!b) break;
c = createC();
if (!c) break;
//...
} while (0);
if (c) freeC(c);
if (b) freeB(b);
if (a) freeA(a);
}
這樣寫倒也不是不行,就是憑空多了一個縮進,怪彆扭的。
---- 分割線 ----
我至今不知道誰先發明了不允許多處 return 這種規定的,動機是什麼。
是為了利於調試?增加可讀性?這很有效嗎?除非你在沒有單步調試器的環境下,純靠加代碼打 log,單進單出可能有點意義。是為了釋放資源嗎?那麼請看最後一段。
不允許多處 return 有一個顯而易見的壞處,它會明顯增加代碼的縮進層次,我相信寫過 c/c++ 或者 php、js 這類類 c 語言的同學都會有感覺。手機碼字不方便,明天我會給出一個簡單的例子。
另外在很多情況下,你必須使用很奇怪的流程才能實現單處 return,這樣增加了思考成本不說,反而降低了代碼的可讀性。
所以我的建議是,能 return 就 return,能早 return 絕不晚 return,能用 return 減少大括弧層次,堅決用。這不僅是代碼格式上的考慮,深層次的動機是,早 return 的寫法讓代碼「邏輯塊」變得更小(邏輯塊位於 return 和 return 之間),邏輯更單純,一段儘早 return 的代碼,一定比最後 return 的代碼更好懂,也更符合直覺。
每次看到有人為了最後一行 return,在循環中記錄返回值,break,然後在循環體外 return,我都覺得純屬有病。
至於 goto,平時自然是不用為好,太難把控。但是在那種依次申請資源,其中一個失敗後倒序釋放資源的情況中,不用 goto 純屬給自己找麻煩。剛才有朋友在評論中說 c 語言單處 return 是為了資源釋放,其實當你多次 return 發現 hold 不住的時候,就應該果斷上 goto。如果規範實在不讓用 goto,那就只能在 while 中用 break 跳出,在結尾處用挨個釋放資源。只不過這樣代碼不好看。多處return和goto原則上是需要避免的,但是為了避免而避免就沒啥意思了。
do while break的寫法有一些弊端。
第一,如果你需要goto的地方,本身在一個甚至兩個while/for裡面,你的程序會非常噁心。你需要set flag,break,check flag,break,check flag,break。這麼寫真的好看?簡潔?易懂?
第二,在大部分不得不用goto或者return的地方,一般都涉及了和err_code不完全一致的資源釋放或者解鎖之類的操作。這個時候你僅僅保留err_code是不夠的,goto有多少個label就要有多少個enum。額外補充一個enum真的比goto label好看么?
第三do {
if (err_happened()) {
err_flag = ERR1;
break;
}
} while (0);
if (err_flag == 1) {
release_resource();
err_code = ERR_CODE1;
}
return err_code;
這個和goto的寫法真的有區別么?有優勢么?我反正是完全沒看出來。
在寫firmware的時候,幾乎每一行程序都有可能出err,難道每一行程序後面都要跟一個if和break?
如果為了公司的coding standard不得不做這樣的事情,我可以理解。但是認為這樣比goto有優勢,我保留意見。
最後,請允許我重申一下我剛開始的觀點,goto和多處return是應該盡量避免的,在不過度犧牲代碼可讀性的情況下。用Go寫因為有defer和自帶GC,推薦儘早return。
寫龜派氣功式if嵌套直到有人煩了為止XD
————其實有一個比較好的習慣是把函數拆小,每個控制在50行以內,這種問題就會少很多。邏輯也更清晰些。
沒有異常處理的語言就是這樣的,所以說Go的錯誤處理設計真心是SB級別的
其實吧說得教條主義一點,不用goto也不用return就會寫得很彆扭的程序,多半是因為邏輯沒有拆分成更清晰的方法,像一團麵條一樣,以後修改還是重構都會出問題。
但是邏輯複雜這個事情明顯就應該怪產品而不是程序員啊……其實有些錯誤處理想想就是挺蠢的,比如malloc返回值是否為空,我其實就沒見過malloc發生內存不足了程序還能繼續正常運行下去的情況……還不如check一下然後直接coredump了。「不用goto」和「不能多處return」是嚴重矛盾的:goto最大的作用就是不多處return,而是統一在結尾位置釋放資源,然後返回。現在倆都不讓用,這是打算嵌套多少層縮進、設多少個狀態變數?
一般這樣很多這樣sb規定的公司不會進行代碼審查....所以隨便用吧……邪惡的猜測下,規定這麼死就是為了不審查...
讓定規矩的人來給出方案,看看是不是真的就更清晰了,單純的禁止不能令人信服。個人覺得gcc的__label__擴展對於goto來說真是好用
#define SEARCH(value, array, target)
do {
__label__ found;
typeof (target) _SEARCH_target = (target);
typeof (*(array)) *_SEARCH_array = (array);
int i, j;
int value;
for (i = 0; i &< max; i++)
for (j = 0; j &< max; j++)
if (_SEARCH_array[i][j] == _SEARCH_target)
{ (value) = i; goto found; }
(value) = -1;
found:;
} while (0)
對於,do{break;} while(0)模式難以應付的嵌套循環,這個語言擴展可以讓編譯器檢測label的作用範圍,避免亂跳出錯。
單入多出和濫用goto不是說代碼一定會出問題,但確實會大大增加出問題的概率,linux內核中使用,萬一崩潰了,對代碼作者也是沒啥損失的,但在生產環境中,特別是安全相關係統中,保守是一個比較好的選擇。安全相關係統除了功能完整之外,還有幾個特性,可靠性,安全性,代碼要求可讀性,可測性和可維護性,這些方面都有一些具體指標,遵守這些指標代碼就沒有問題了嗎?當然不是,這些都是經驗的總結,有理由相信,不遵守這些規則甚至變法違反這些規則,更不容易達到要求。一些系統動輒十數年甚至數十年的生命周期,要相信個人編碼能力允許使用?這是開玩笑好嗎。讀你代碼的人可能經驗豐富,也可能是什麼也不明白的菜鳥,也可能有人已經辭職,也可能已經高升,那麼多子系統,怎麼可能過度依賴當時開發人員的水平?統一規定,把常見的坑避開才是最有效率,對項目最優的方案。總結就是濫用一時爽,XXXXX。
謝邀。我主要是搞c++的,隨便說說吧。先說說對這兩個東西的理解。1,goto的事呢,反正我是絕對不允許的。原因很簡單,就是對人類不友好,容易出問題。當然我知道這是有爭議的,但是我個人的原則是,能往簡單了寫,就不要搞複雜,並且保持代碼風格的統一化。
至於說linux裡面有,我不去質疑,大神們肯定是有犧牲可讀性來想達到某種目的。
2,多return這個事,基本上能不用就不用。確實容易出錯。比如一上來new一個指針,正常的情況下,釋放了。加個分支,return的時候忘了。比較無害的有兩種常見的多return是ok的。1,int fun(int *a)
{
if(a==NULL)
{
return 0;
}
...
}
2,
int fun(int condition)
{
if(condition==1)
{
return fun1(condition)
}
else if(condition==2)
{
return fun2(condition)
}
else
{
return 0;
}
}
再來說處理方法。
第一個,就是加函數和分支第二個,就是函數要足夠短,我會要求在30行以內(35行我可以忍,40就不行)。比如題主的例子int checkError1(int *pArray,int err)
{
if(err==0)
{
...
}
return err;
}
int checkError2(int *pArray,int err)
{
if(err==0)
{
...
}
return err;
}
int checkError3(int *pArray,int err)
{
if(err==0)
{
...
}
return err;
}
int foo()
{
int err = 0; // 錯誤碼
int *pArray = NULL;
pArray = (int *)malloc(100 * sizeof(int));
//如何優雅的處理pArray不為NULL並返回錯誤碼
err=checkError1(pArray,err);
//有可能出現其他錯誤,如何處理錯誤並返回錯誤碼
err=checkError2(pArray,err);
err=checkError3(pArray,err);
...
return err; //僅在最後返回錯誤碼
}
另,個人不推薦do-while的方式,增加理解難度,屬於為了寫代碼的代碼。
不用goto、多處return只是規定的一部分吧,所以回答中的
1、用do { } while(0) 配合 break 的方法的:2、說申請資源、釋放資源的3、說「一般這樣很多這樣sb規定的公司不會進行代碼審查....所以隨便用吧」的 請搜索以下C++TEST、LDRA Testbed等自動化測試工具。個人覺得這幾個規定,像@Enzo Jiang 說的一樣,「適用領域包括1.汽車 2.鐵路 3.核電 4.機電系統 5.製造業儀錶 這五個領域的嵌入式系統的軟體開發方法規範」,更多是從業務領域提的,主要是是代碼邏輯可控,不隨意跳出,業務流清楚。不是這些領域的,可能對這些規定會嗤之以鼻,不會感覺到這些的好處。
最後,附以下我們的編碼規定:自己重寫一個assert,出錯就輸出strerror然後崩,符合unix哲學。
說不支持用goto但喜歡到處return的人,大概是沒有體驗過需求變更後,需要在函數返回前增加統一處理時,維護一個到處return的函數時那種吃shi般的心情吧。
到處return從格式上來看和在函數結尾寫個label然後到處goto到這個label差不多,都是打亂函數內部層次結構的寫法。我們項目也有這樣的規定,我也常常遇到題主所說的困擾,我自己的做法是盡量去遵守,除非因此帶來太深的縮進或者比較明顯的影響代碼性能或者可讀性時才考慮多處return。多寫寫就明白給函數唯一一個入口和唯一一個出口是多整潔多讓人舒心的事情。就算不問你司的人,你司的代碼庫里就沒有過一個函數處理其他錯誤的代碼?grep 一下。如果沒有,我覺得你可以自己去改規則,做CTO了。。
do {...if(錯誤) break;} while(0);
推薦閱讀:
※有誰是完全使用Linux的,辦公 娛樂各方面可以脫離windows應用?
※有沒有基於Debian、滾動發行、能第一時間收到安全補丁、kernel版本更新最快的Linux發行版?
※哪些Linux發行版是滾動發行(Rolling Release)的?
※大型c++項目在linux下如何調試?
※到底學習 Linux 應該學習什麼?學習框架是什麼?