<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                # 練習20:Zed的強大的調試宏 > 原文:[Exercise 20: Zed's Awesome Debug Macros](http://c.learncodethehardway.org/book/ex20.html) > 譯者:[飛龍](https://github.com/wizardforcel) 在C中有一個永恒的問題,它伴隨了你很長時間,然而在這個練習我打算使用一系列我開發的宏來解決它。到現在為止你都不知道它們的強大之處,所以你必須使用它們,總有一天你會來找我說,“Zed,這些調試宏真是太偉大了,我應該把我的第一個孩子的出生歸功于你,因為你治好了我十年的心臟病,并且打消了我數次想要自殺的念頭。真是要謝謝你這樣一個好人,這里有一百萬美元,和Leo Fender設計的Snakehead Telecaster電吉他的原型。” 是的,它們的確很強大。 ## C的錯誤處理問題 幾乎每個編程語言中,錯誤處理都非常難。有些語言盡可能試圖避免錯誤這個概念,而另一些語言發明了復雜了控制結構,比如異常來傳遞錯誤狀態。當然的錯誤大多是因為程序員嘉定錯誤不會發生,并且這一樂觀的思想影響了他們所用和所創造的語言。 C通過返回錯誤碼或設置全局的`errno`值來解決這些問題,并且你需要檢查這些值。這種機制可以檢查現存的復雜代碼中,你執行的東西是否發生錯誤。當你編寫更多的C代碼時,你應該按照下列模式: + 調用函數。 + 如果返回值出現錯誤(每次都必須檢查)。 + 清理創建的所有資源。 + 打印出所有可能有幫助的錯誤信息。 這意味著對于每一個函數調用(是的,每個函數)你都可能需要多編寫3~4行代碼來確保它正常功能。這些還不包括清理你到目前創建的所有垃圾。如果你有10個不同的結構體,3個方式。和一個數據庫鏈接,當你發現錯誤時你應該寫額外的14行。 之前這并不是個問題,因為發生錯誤時,C程序會像你以前做的那樣直接退出。你不需要清理任何東西,因為OS會為你自動去做。然而現在很多C程序需要持續運行數周、數月或者數年,并且需要優雅地處理來自于多種資源的錯誤。你并不能僅僅讓你的服務器在首次運行就退出,你也不能讓你寫的庫使使用它的程序退出。這非常糟糕。 其它語言通過異常來解決這個問題,但是這些問題也會在C中出現(其它語言也一樣)。在C中你只能夠返回一個值,但是異常是基于棧的返回系統,可以返回任意值。C語言中,嘗試在棧上模擬異常非常困難,并且其它庫也不會兼容。 ## 調試宏 我使用的解決方案是,使用一系列“調試宏”,它們在C中實現了基本的調試和錯誤處理系統。這個系統非常易于理解,兼容于每個庫,并且使C代碼更加健壯和簡潔。 它通過實現一系列轉換來處理錯誤,任何時候發生了錯誤,你的函數都會跳到執行清理和返回錯誤代碼的“error:”區域。你可以使用`check`宏來檢查錯誤代碼,打印錯誤信息,然后跳到清理區域。你也可以使用一系列日志函數來打印出有用的調試信息。 我現在會向你展示你目前所見過的,最強大且卓越的代碼的全部內容。 ```c #ifndef __dbg_h__ #define __dbg_h__ #include <stdio.h> #include <errno.h> #include <string.h> #ifdef NDEBUG #define debug(M, ...) #else #define debug(M, ...) fprintf(stderr, "DEBUG %s:%d: " M "\n", __FILE__, __LINE__, ##__VA_ARGS__) #endif #define clean_errno() (errno == 0 ? "None" : strerror(errno)) #define log_err(M, ...) fprintf(stderr, "[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__) #define log_warn(M, ...) fprintf(stderr, "[WARN] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__) #define log_info(M, ...) fprintf(stderr, "[INFO] (%s:%d) " M "\n", __FILE__, __LINE__, ##__VA_ARGS__) #define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; } #define sentinel(M, ...) { log_err(M, ##__VA_ARGS__); errno=0; goto error; } #define check_mem(A) check((A), "Out of memory.") #define check_debug(A, M, ...) if(!(A)) { debug(M, ##__VA_ARGS__); errno=0; goto error; } #endif ``` 是的,這就是全部代碼了,下面是它每一行所做的事情。 dbg.h:1-2 防止意外包含多次的保護措施,你已經在上一個練習中見過了。 dbg.h:4-6 包含這些宏所需的函數。 dbg.h:8 `#ifdef`的起始,它可以讓你重新編譯程序來移除所有調試日志信息。 dbg.h:9 如果你定義了`NDEBUG`之后編譯,沒有任何調試信息會輸出。你可以看到`#define debug()`被替換為空(右邊沒有任何東西)。 dbg.h:10 上面的`#ifdef`所匹配的`#else`。 dbg.h:11 用于替代的`#define debug`,它將任何使用`debug("format", arg1, arg2)`的地方替換成`fprintf`對`stderr`的調用。許多程序員并不知道,但是你的確可以創建列斯與`printf`的可變參數宏。許多C編譯器(實際上是C預處理器)并不支持它,但是gcc可以做到。這里的魔法是使用`##__VA_ARGS__`,意思是將剩余的所有額外參數放到這里。同時也要注意,使用了`__FILE__`和`__LINE__`來獲取當前`fine:line`用于調試信息。這會非常有幫助。 dbg.h:12 `#ifdef`的結尾。 dbg.h:14 `clean_errno`宏用于獲取`errno`的安全可讀的版本。中間奇怪的語法是“三元運算符”,你會在后面學到它。 dbg.h:16-20 `log_err`,`log_warn`和`log_info`宏用于為最終用戶記錄信息。它們類似于`debug`但不能被編譯。 dbg.h:22 到目前為止最棒的宏。`check`會保證條件`A`為真,否則會記錄錯誤`M`(帶著`log_err`的可變參數),之后跳到函數的`error:`區域來執行清理。 dbg.h:24 第二個最棒的宏,`sentinel`可以放在函數的任何不應該執行的地方,它會打印錯誤信息并且跳到`error:`標簽。你可以將它放到`if-statements`或者`switch-statements`的不該被執行的分支中,比如`default`。 dbg.h:26 簡寫的`check_mem`宏,用于確保指針有效,否則會報告“內存耗盡”的錯誤。 dbg.h:28 用于替代的`check_debug`宏,它仍然會檢查并處理錯誤,尤其是你并不想報告的普遍錯誤。它里面使用了`debug`代替`log_err`來報告錯誤,所以當你定義了`NDEBUG`,它仍然會檢查并且發生錯誤時跳出,但是不會打印消息了。 ## 使用dbg.h 下面是一個例子,在一個小的程序中使用了`dbg.h`的所有函數。這實際上并沒有做什么事情,知識想你演示了如何使用每個宏。我們將在接下來的所有程序中使用這些宏,所有要確保理解了如何使用它們。 ```c #include "dbg.h" #include <stdlib.h> #include <stdio.h> void test_debug() { // notice you don't need the \n debug("I have Brown Hair."); // passing in arguments like printf debug("I am %d years old.", 37); } void test_log_err() { log_err("I believe everything is broken."); log_err("There are %d problems in %s.", 0, "space"); } void test_log_warn() { log_warn("You can safely ignore this."); log_warn("Maybe consider looking at: %s.", "/etc/passwd"); } void test_log_info() { log_info("Well I did something mundane."); log_info("It happened %f times today.", 1.3f); } int test_check(char *file_name) { FILE *input = NULL; char *block = NULL; block = malloc(100); check_mem(block); // should work input = fopen(file_name,"r"); check(input, "Failed to open %s.", file_name); free(block); fclose(input); return 0; error: if(block) free(block); if(input) fclose(input); return -1; } int test_sentinel(int code) { char *temp = malloc(100); check_mem(temp); switch(code) { case 1: log_info("It worked."); break; default: sentinel("I shouldn't run."); } free(temp); return 0; error: if(temp) free(temp); return -1; } int test_check_mem() { char *test = NULL; check_mem(test); free(test); return 1; error: return -1; } int test_check_debug() { int i = 0; check_debug(i != 0, "Oops, I was 0."); return 0; error: return -1; } int main(int argc, char *argv[]) { check(argc == 2, "Need an argument."); test_debug(); test_log_err(); test_log_warn(); test_log_info(); check(test_check("ex20.c") == 0, "failed with ex20.c"); check(test_check(argv[1]) == -1, "failed with argv"); check(test_sentinel(1) == 0, "test_sentinel failed."); check(test_sentinel(100) == -1, "test_sentinel failed."); check(test_check_mem() == -1, "test_check_mem failed."); check(test_check_debug() == -1, "test_check_debug failed."); return 0; error: return 1; } ``` 要注意`check`是如何使用的,并且當它為`false`時會跳到`error:`標簽來執行清理。這一行讀作“檢查A是否為真,不為真就打印M并跳出”。 ## 你會看到什么 當你執行這段代碼并且向第一個參數提供一些東西,你會看到: ```sh $ make ex20 cc -Wall -g -DNDEBUG ex20.c -o ex20 $ ./ex20 test [ERROR] (ex20.c:16: errno: None) I believe everything is broken. [ERROR] (ex20.c:17: errno: None) There are 0 problems in space. [WARN] (ex20.c:22: errno: None) You can safely ignore this. [WARN] (ex20.c:23: errno: None) Maybe consider looking at: /etc/passwd. [INFO] (ex20.c:28) Well I did something mundane. [INFO] (ex20.c:29) It happened 1.300000 times today. [ERROR] (ex20.c:38: errno: No such file or directory) Failed to open test. [INFO] (ex20.c:57) It worked. [ERROR] (ex20.c:60: errno: None) I shouldn't run. [ERROR] (ex20.c:74: errno: None) Out of memory. ``` 看到`check`失敗之后,它是如何打印具體的行號了嗎?這會為接下來的調試工作節省時間。同時也觀察`errno`被設置時它如何打印錯誤信息。同樣,這也可以節省你調試的時間。 ## C預處理器如果擴展宏 現在我會想你簡單介紹一些預處理器的工作原理,讓你知道這些宏是如何工作的。我會拆分`dbg.h`中阿最復雜的宏并且讓你運行`cpp`來讓你觀察它實際上是如何工作的。 假設我有一個函數叫做`dosomething()`,執行成功是返回0,發生錯誤時返回-1。每次我調用`dosomething`的時候,我都要檢查錯誤碼,所以我將代碼寫成這樣: ```c int rc = dosomething(); if(rc != 0) { fprintf(stderr, "There was an error: %s\n", strerror()); goto error; } ``` 我想使用預處理器做的是,將這個`if`語句封裝為更可讀并且便于記憶的一行代碼。于是可以使用這個`check`來執行`dbg.h`中的宏所做的事情: ```c int rc = dosomething(); check(rc == 0, "There was an error."); ``` 這樣更加簡潔,并且恰好解釋了所做的事情:檢查函數是否正常工作,如果沒有就報告錯誤。我們需要一些特別的預處理器“技巧”來完成它,這些技巧使預處理器作為代碼生成工具更加易用。再次看看`check`和`log_err`宏: ```c #define log_err(M, ...) fprintf(stderr, "[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__) #define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; } ``` 第一個宏,`log_err`更簡單一些,只是將它自己替換為`fprintf`對`stderr`的調用。這個宏唯一的技巧性部分就是在`log_err(M, ...)`的定義中使用`...`。它所做的是讓你向宏傳入可變參數,從而傳入`fprintf`需要接收的參數。它們是如何注入`fprintf`的呢?觀察末尾的`##__VA_ARGS__`,它告訴預處理器將`...`所在位置的參數注入到`fprintf`調用的相應位置。于是你可以像這樣調用了: ```c log_err("Age: %d, name: %s", age, name); ``` `age, name`參數就是`...`所定義的部分,這些參數會被注入到`fprintf`中,輸出會變成: ```c fprintf(stderr, "[ERROR] (%s:%d: errno: %s) Age %d: name %d\n", __FILE__, __LINE__, clean_errno(), age, name); ``` 看到末尾的`age, name`了嗎?這就是`...`和`##__VA_ARGS__`的工作機制,在調用其它變參宏(或者函數)的時候它會起作用。觀察`check`宏調用`log_err`的方式,它也是用了`...`和`##__VA_ARGS__`。這就是傳遞整個`printf`風格的格式字符串給`check`的途徑,它之后會傳給`log_err`,二者的機制都像`printf`一樣。 下一步是學習`check`如何為錯誤檢查構造`if`語句,如果我們剖析`log_err`的用法,我們會得到: ```c if(!(A)) { errno=0; goto error; } ``` 它的意思是,如果`A`為假,則重置`errno`并且調用`error`標簽。`check`宏會被上述`if`語句·替換,所以如果我們手動擴展`check(rc == 0, "There was an error.")`,我們會得到: ```c if(!(rc == 0)) { log_err("There was an error."); errno=0; goto error; } ``` 在這兩個宏的展開過程中,你應該了解了預處理器會將宏替換為它的定義的擴展版本,并且遞歸地來執行這個步驟,擴展宏定義中的宏。預處理器是個遞歸的模板系統,就像我之前提到的那樣。它的強大來源于使用參數化的代碼來生成整個代碼塊,這使它成為便利的代碼生成工具。 下面只剩一個問題了:為什么不像`die`一樣使用函數呢?原因是需要在錯誤處理時使用`file:line`的數值和`goto`操作。如果你在函數在內部執行這些,你不會得到錯誤真正出現位置的行號,并且`goto`的實現也相當麻煩。 另一個原因是,如果你編寫原始的`if`語句,它看起來就像是你代碼中的其它的`if`語句,所以它看起來并不像一個錯誤檢查。通過將`if`語句包裝成`check`宏,就會使這一錯誤檢查的邏輯更清晰,而不是主控制流的一部分。 最后,C預處理器提供了條件編譯部分代碼的功能,所以你可以編寫只在構建程序的開發或調試版本時需要的代碼。你可以看到這在`dbg.h`中已經用到了,`debug`宏的主體部分只被編譯器用到。如果沒有這個功能,你需要多出一個`if`語句來檢查是否為“調試模式”,也浪費了CPU資源來進行沒有必要的檢查。 ## 附加題 + 將`#define NDEBUG`放在文件頂端來消除所有調試信息。 + 撤銷上面添加的一行,并在`MakeFile`頂端將`-D NDEBUG`添加到`CFLAGS`,之后重新編譯來達到同樣效果。 + 修改日志宏,使之包含函數名稱和`file:line`。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看