C語言陷阱與技巧第8節,輸出適當的信息,有利於定位錯誤和異常代碼

2020-12-06 IT劉小虎

把複雜的任務拆分成簡單的子模塊

在C語言程序開發中,程式設計師常常會把一個複雜的任務拆分成若干個較為簡單的子模塊,這些子模塊可以看做是複雜任務的各個組成部分。因此,程式設計師將子模塊逐個完成後,就可以將其像「積木」一樣搭建起來,進而解決複雜任務。

之所以要這麼做,是因為若直接編寫C語言代碼一次性解決複雜任務,往往會讓整個代碼「揉作一團」,不僅開發時容易引入 bug,而且後期維護起來也比較痛苦。想像一下,若將任務拆分成若干個子模塊,以後發現問題時,可能只需要修改某個子模塊就可以了。但是若沒有這麼做,查找和解決問題就必須對全部代碼下手了。哪一種開發風格維護起來方便,相信讀者自然明白。

將任務拆分成子模塊後,每個子模塊常常被封裝成一個C語言函數,所以,最後的「堆積木」其實就是調用各個C語言函數。不過,每一個子模塊都有可能得到正常結果,也有可能得到異常結果,這通常用C語言函數的返回值區分。在「堆積木」階段調用各個函數時,應該根據被調用函數的返回值做不同的處理。

例如,某個子模塊負責計算用戶輸入數字的 log(對數) 值時,如果用戶輸入的是正數,則該子模塊能夠得到正常的結果。但如果用戶輸入的是負數,子模塊顯然就無法得到正常結果了。

下面是一個例子

這裡的例子代碼儘可能的簡單,是為了將重心放在討論主題上:

int cond(){staticint cnt = 0; srandom(time(NULL)+ cnt++); if(random()%10 < 5)return -1;return0;}int fun1(){/** 其他邏輯 */int ret = cond();if(ret == -1)return -1;/** 其他邏輯 */return0;}int fun2(){/** 其他邏輯 */int ret = cond();if(ret == -1)return -1;/** 其他邏輯 */return0;}int main(){if(!fun1() && !fun2())printf("cond is true\n");elseprintf("cond is false\n");return0;}

cond() 函數產出 0~10 的隨機數,如果隨機數小於 5 就返回 -1(模擬異常結果),否則返回 0(模擬正常結果)。fun1() 函數和 fun2() 函數都會根據 cond() 函數的返回值做一些進一步的工作(上面的C語言代碼略過了「進一步工作」)。在 main() 函數中「堆積木」調用 fun1() 和 fun2() 函數時,使用了 if 語句判斷它們的返回值,並且根據返回值做了不同的處理。

現在編譯這段C語言程序並執行,得到如下結果:

# gcc t.c -g# ./a.out cond is true# ./a.out cond is false從輸出結果可以發現,C語言程序輸出了「cond is false」(模擬異常)。我們往往不希望程序輸出異常結果,所以看到異常結果後,就需要知道為什麼會出現這個結果。

適當的輸出信息有利於定位異常

查看C語言原始碼,發現程序輸出異常結果是因為 main() 函數裡的 if(!fun1() && !fun2()) 為假,但是無論 fun1() 還是 fun2() 返回 -1,都會導致 if 條件表達式為假,這麼看來, main() 函數調用 fun1() 和 fun2() 函數的方式就不太合適了,因為到這裡我們已經無法繼續追蹤異常原因了。似乎 main() 函數這麼寫更合適,相關C語言代碼如下,請看:

int main(){int ret1 = fun1();int ret2 = fun2();if(!ret1 && !ret2)printf("cond is true\n");elseif(ret1 && ret2)printf("cond is false because of fun1 and fun2\n");elseif(ret1)printf("cond is false because of fun1\n");elseprintf("cond is false because of fun2\n");return0;}

編譯修改後的C語言代碼並執行,得到如下結果:

# gcc t.c# ./a.out cond is false because of fun1 and fun2# ./a.out cond is false because of fun1# ./a.out cond is false because of fun2# ./a.out cond is false because of fun2# ./a.out cond is true這次我們就知道異常輸出是哪個函數導致的了,不過僅調用兩個函數就寫了這麼多行極有可能用不到的錯誤提示代碼,太麻煩了,如果其他地方也需要用到類似的調用,就更麻煩了,有沒有更方便的方法呢?我們嘗試將錯誤提示信息塞入 fun1() 和 fun2() 函數試試,如下修改 fun1() 和 fun2() 函數的代碼:

int fun1(){/** 其他邏輯 */int ret = cond();if(ret == -1){printf("fun1 get unexpected cond\n");return -1; }/** 其他邏輯 */return0;}int fun2(){/** 其他邏輯 */int ret = cond();if(ret == -1){printf("fun2 get unexpected cond\n");return -1; }/** 其他邏輯 */return0;}

現在使用修改之前的 main() 函數如下:

編譯並執行這段C語言代碼並執行,得到如下輸出:

# gcc t.c # ./a.out fun2 get unexpected condcond is false# ./a.out fun1 get unexpected condcond is false# ./a.out cond is true這樣一來,我們既能根據輸出推斷異常是由哪個函數導致的,也能儘可能的保持C語言代碼的簡潔性。不過代碼還是有一點點囉嗦:

fun1 get unexpected condfun2 get unexpected cond這兩句輸出僅有 fun1 和 fun2 是不同的,但是我們卻需要完整的寫兩遍幾乎一樣的語句,而且以後萬一需要修改,還需要兩處都修改,一來麻煩,二來容易出錯。能不能避免這種情況呢?

使用__FUNCTION__,__LINE__,__FILE__等關鍵字

在C語言程序的編譯階段,編譯器會將__FUNCTION__,__LINE__,__FILE__這幾個關鍵字解釋為「所在函數名」,「所在行號」,「所在文件名」。所以有了這幾個關鍵字,我們就沒有必要再手動輸入函數名了,針對本節提到的例子,完全可以使用上一節介紹的 define 宏定義:

#define error_info() printf("%s get unexpected cond (%s line:%d)\n",\ __FUNCTION__, __FILE__, __LINE__)...if(ret == -1){ error_info();return -1; }...

編譯並執行這段C語言代碼,得到如下結果:

# gcc t.c -g# ./a.out cond is true# ./a.out fun1 get unexpected cond (t.c line:26)cond is false# ./a.out fun2 get unexpected cond (t.c line:41)cond is false可以看出,程序不僅把異常的函數名輸出了,還把該函數所在的文件名(t.c) 以及行號(line:26, line:41)輸出了,這樣的調試信息看起來非常舒服,在大型項目開發中,實用性很強。

類似的調試宏還有TIMEDATE等,就不一一演示了。

小結

本節討論了在C語言程序開發中,複雜任務常被拆分成多個子模塊並一一封裝為函數,這些函數可能有正常處理結果,也有可能有異常處理結果,所以本節討論了輸出基本調試信息對定位問題的重要性,並在最後介紹了幾種C語言程序開發常用的調試宏,這些宏在大型項目開發中實用性很強。

相關焦點

  • C語言陷阱與技巧第15節,為什麼每調用一次函數,就需要一次if判斷...
    但是,可能因為某種原因,something 文件沒有生成,那麼上面這段C語言代碼編譯得到的程序就什麼也不會輸出了。遇到這種什麼都沒有輸出的情況,初學者甚至可能會以為程序沒有運行。# gcc t.c# ./a.out #要是這段C語言代碼隱藏在一個比較大的項目間,something 文件是由其他邏輯生成的,這時要定位問題代碼可能就要花些功夫了。
  • C語言陷阱與技巧第13節,1位元組(Byte)一定等於8位(bit)嗎?C語言操作...
    >上面第二行C語言代碼將 status 的第3個位(bit 2)設置為 1,第三行C語言代碼將 status 的第1個位(bit 0)設置為 0。似乎可以藉助C語言的聯合體(union)和位域(bit field)語法,間接的實現位操作,請看下面的C語言代碼:union convert{ unsignedchar status;struct __bits { unsignedchar bit0:1;
  • C語言陷阱與技巧31節,都說void*指針是「萬能指針」,它萬能在哪
    例如下面這段C語言代碼:#include <stdio.h>void myprint(void *p){char c = p[0];printf("c=%c\n", c);}int main()
  • C語言陷阱與技巧第2節,使用inline函數可以提升程序效率,但是讓...
    打開 Linux 內核原始碼,會發現內核在定義C語言函數時,有很多都帶有 「inline」關鍵字,請看下圖,那麼這個關鍵字有什麼作用呢?inline 關鍵字的作用在C語言程序開發中,inline 一般用於定義函數,inline 函數也被稱作「內聯函數」,C99 和 GNU C 均支持內聯函數。那麼在C語言中,內聯函數和普通函數有什麼不同呢?
  • C語言之const和volatile"究極"學習
    ,修改const全局變量將導致程序崩潰標準c語言編譯器不會將const修飾的全局變量存儲於只讀存儲區中,而是存儲於可修改的全局數據區,其值依然可以改變3、代碼示例:(1)只讀變量代碼示例:#include <stdio.h>int main(){   const int a =10;   printf("a =
  • 言C語言陷阱與技巧第21節,函數只能返回一個值嗎?有多個返回值怎麼...
    如今幾乎找不到只專注於一門程式語言的程式設計師了。大多數程式設計師在自己平時的工作和生活中,一般都使用不止一門程式語言,例如小編在工作中主要使用的是C語言,但是有時候驗證算法也會使用 matlab 和 python,在業餘做別的項目時還會用到 C#。
  • 「C語言從入門到入土」必備C語言基礎筆記整理
    編輯:首先是編輯,就是在編譯器中輸入原始碼,後綴名是.c||編譯:然後是對目標程序進行編譯,如果源程序沒有錯誤,得到目標程序,後綴.obj(VS編譯快捷鍵方式是Ctrl+F7)|Output a number輸出一個數。(反斜槓n 「\n」表示換行;反斜槓? 「\?」表示問號)printf("輸出三個數 :\n%d\n%d\n%d\n",a,b,c); //變量要先定義,後使用。
  • Python基礎教程(一) - 錯誤和異常
    程式設計師的一生中,錯誤幾乎每天都在發生。在過去的一個時期,錯誤要麼對程序是致命的,要麼產生一堆無意義的輸出。所以,人們需要一個柔和的處理錯誤的方法,而不是終止執行。當然,這一切都是在異常和異常處理出現之前的事了。
  • 深入理解C語言
    我相信你對a的輸出相當有把握,就分別是4,5,6,因為那個靜態變量。對於c呢,你應該也比較肯定,那是一堆亂數。但是你可能不知道b的輸出會是什麼?答案是1,2,3。為什麼和c不一樣呢?因為,如果要初始化,每次調用函數裡,編譯器都要初始化函數棧空間,這太費性能了。但是c的編譯器會初始化靜態變量為0,因為這只是在啟動程序時的動作。
  • 診斷Java代碼:Double Descent錯誤模式
    與可怕的 空指針異常(該異常除了報告空指針之外,對於將要發生的事情什麼也不說)不同,類強制轉換異常相對來說容易調試。  類強制轉換經常發生在遞歸下行數據結構的程序中,通常是當代碼的某些部分在每次方法調用中下行了兩級且在第二次下行時調度不當時發生的。程式設計師可通過學習 Double Descent 錯誤模式來識別這種問題。
  • Python語言教程算術運算與算術表達式的介紹
    Python語言教程算術運算與算術表達式的介紹 Python語言教程在算術運算符與算術的表達方式是我們值得學習的知識。下面我們就來詳細的看看Python語言教程中的相關信息。
  • C語言相關文件的基本知識
    文件有不同的類型,在程序的設計中,主要有兩種文件; 1.程序文件;包括源程序文件(後綴為.c),目標文件(後綴為.obj),可執行文件(後綴為.exe)等,這類文件的內容是程序代碼。為了簡化用戶對輸人輸出設備的操作,使用戶不必去區分各種輸入輸出設備之間的區別,作業系統把各種設備都統一作為文件來處理。輸人輸出是數據傳送的過程,數據如流水一樣從一處流向另一處,因此常將輸入輸出形象地稱為流(stream),即數據流。流表示了信息從源到目的端的流動。
  • 「記」詳解C語言之格式
    ,在它的主體設計完成後,Thompson和Ritchie用它完全重寫了UNIX,且隨著UNIX的發展,c語言也得到了不斷的完善。為了利於C語言的全面推廣,許多專家學者和硬體廠商聯合組成了C語言標準委員會,並在之後的1989年,誕生了第一個完備的C標準,簡稱「C89」,也就是「ANSI c」,截至2020年,最新的C語言標準為2017年發布的 「C17」。
  • 2003年10月甘肅省高等教育自學考試C語言程序設計試卷
    一、單項選擇題(在每小題的四個備選答案中,選出一個正確的答案,並將其代碼填入題幹後的括號內。以下c語言函數聲明中,不正確的是()  Avoid fun (int x, int y); Bfun (int x, int y);  Cint fun (int x,y); Dchar *fun (char *s);  9.下列選項中,不合法的C語言關鍵字是 ()  Aauto Bdefault Cstatic Dvar  10
  • 奇怪的C語言代碼,在變量前加上(void)是什麼操作?有什麼用?
    對於初學者來說,閱讀項目原始碼是學習和鞏固C語言編程能力的一個好方法——從前輩們的一些優秀C語言項目中,我們能夠學到很多編寫程序方面的思考方式,也就是一些程式設計師所謂的「編程思維」,看得多了,編寫C語言程序自然就手到擒來了。
  • 定義只有一個數組成員的C語言結構體有什麼用?
    C語言代碼示例編譯並執行上述C語言代碼,得到如下輸出:# gcc t.c# ./a.outsizeof arr is 8可見,在函數 fun() 內部,sizeof(arr) 並不等於數組長度 16,而是等於 8(指針長度,我的機器是 64 位的,指針佔用內存空間為 8 個字節),這說明即使函數 fun() 的參數C語言代碼明確指定為 fun(char arr[16]),在函數內部,arr 還是退化成指針了。
  • R語言常用數據處理代碼整理
    現基於各類R語言入門書整理R中常見的數據處理代碼。目 錄1. 預覽數據集2. 查看數據集結構3. 數據類型轉化4. 批量轉化變量為因子型5. 列變量重命名6. 數據集增刪列變量6.1 創建新變量6.2 剔除列變量7. 數據集中列排序8. 數據集中行排序8.1 升序排列8.2 降序排列8.3 缺失值排序9.
  • C語言發展簡史
    2、K&R C1978 年,Dennis Ritchie 和 Brian Kernighan 合作推出了《The C Programming Language》的第一版(按照慣例,經典著作一定有簡稱,該著作簡稱為 K&R),書末的參考指南 (Reference Manual) 一節給出了當時 C 語言的完整定義,成為那時 C
  • 在C語言中如何高效地複製和連接字符串?
    就目前而言,在編程領域中,C語言的運用非常之多,它兼顧了高級語言的彙編語言的優點,相較於其它程式語言具有較大優勢。本文中展示的示例代碼僅僅用於說明目的。它們可能包含細微的錯誤,不應該被視為最佳代碼實踐。標準解決方案這種返回函數的第一個參數的設計,有時候會被不明白其用途的用戶所質疑。
  • 剖析C語言中a=a+++a的無聊問題
    看法二:  a=a+++++a的編譯和執行結果是隨機的,可能有些屌絲編譯器自認為自己很牛,可以處理這樣的語句,並把它編譯出來而不報任何警告。那麼我首先建議這樣的編譯器別用了,其次我要說這個東西的編譯結果並不重要,重要的是千萬不要在項目代碼中這樣寫。