C/C++學習筆記八(斷言與異常處理)
斷言
斷言是什麼?簡單而言,斷言是對某種假設條件進行檢查。
C語言中,在assert.h中,斷言被定義為巨集的形式(assert(expression)
),而不是函式。
assert將通過檢查表示式的值來決定是否需要終止程式,如果表示式為真(1)則忽略斷言,程式繼續執行。如果表示式為假(0),那麼首先向錯誤流strerr列印一條錯誤資訊,然後通過abort函式終止程式的執行。
斷言用法的簡單例子:
int a,b;
a = 1;
b = 1 ;
assert(b!=0);
printf("a/b = %d\n",a/b);
通過檢視assert.h,NDEBUG巨集開啟狀態時assert巨集是可用的。
預設情況下,assert巨集只有在Debug版本才起作用,而在Release版本中將被忽略。但在許多作業系統的C程式中,Release版本中也將NDEBUG巨集依然為開啟狀態。
也便是說如果需要用到斷言時,使用者可以通過重定義自己的ASSERT。例子如下:
#ifdef DEBUG
#define ASSERT(condition) \
do{ \
if(condition) \
{ \
NULL; \
} \
else{ \
assert(condition); \
} \
}while(0)
#else
#define ASSERT(condition) NULL
#endif
避免使用斷言去檢查程式錯誤
在斷言的使用中,應該遵循這樣的一個規定:對來自系統內部的可靠資料使用斷言,對於外部不可靠資料不能使用斷言,而應該使用錯誤處理程式碼。
換句話而言,斷言是用來處理不應該發生的非法情況,而對於可能發生的應該使用錯誤處理程式碼。
對於使用者輸入,與外部系統進行協議互動時的情況,也不能使用斷言進行引數的判斷,這種情況屬於正常的錯誤檢查。
下面的例子說明了斷言的使用場景
char * Strdup(const char * src){
assert(src!=NULL);
char * result = NULL;
size_t len = strlen(src) +1;
result = (char *)malloc(len);
assert(result != NULL);
return result;
}
例子中第一個斷言assert(src!=NULL)
用於判斷傳入的引數的正確性,保證引數不為NULL
第二個斷言assert(result != NULL)
檢查函式返回值是否為NULL。
例子中的兩個斷言,第一個是合法的,而第二個不合法,第一個合法是因為傳入的引數必須不為NULL,斷言如果成功,則說明呼叫程式碼存在問題,這屬於非法的情況,此處屬於斷言的正確使用情況。
第二個斷言則不同,malloc對於返回NULL的情況屬於呼叫正常情況,這應該使用正常的錯誤處理邏輯,不應該使用斷言。
避免在斷言表示式中使用改變上下文的語句
在assert巨集只有在Debug版本中情況下,應該避免斷言表示式中使用改變環境的語句。
如下例子因為斷言語句的緣故,將導致不同的編譯版本產生不同的結果。
int test(int i)
{
assert(i++);
return i;
}
因此應該避免在斷言表示式中使用改變上下文環境的語句,也就是確保斷言僅僅作為一個檢查而存在,不應該參與正常語句的處理。
異常處理
獲取錯誤程式碼errno
error 是用於表達不同錯誤值的一個全域性變數。如果一個系統呼叫或庫函式呼叫失敗,可以通過errno的值來確定問題所在。
因errno是一個全域性變數,在呼叫不同系統呼叫或者庫函式失敗時都有可能修改它的值,因為在使用errno時,應先將其清0
errno = 0;
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
if (errno!=0) {
printf("error : %d \n",errno);
printf("錯誤資訊 : %s \n",strerror(errno));
}
}
但errno並不是所有的庫函式都適合使用,就error而言庫函式一般分為如下幾種。
1.函式返回值無法判斷錯誤,需進一步從errno中獲取錯誤資訊
函式 | 返回值 | errno值 |
---|---|---|
fgetwc、fputwc | WEOF | EILSEQ |
strtol、wcstol | LONG_MIN或LONG_MAX | ERANGE |
strtoll、wcstoll | LLONG_MIN或LLONG_MAX | ERANGE |
strtoul、wcstoul | ULONG_MAX | ERANGE |
strtoull、wcstoull | ULLONG_MAX | ERANGE |
strtoumax、wcstoumax | UINTLLONG_MAX | ERANGE |
strtod、wcstod | 0或者+-HUGE_VAL | ERANGE |
strtof、wcstof | 0或者+-HUGE_VALF | ERANGE |
strtold、wcstold | 0或者+-HUGE_VALL | ERANGE |
strtoimax、wcstoimax | IMAX_MIN或INTMAX_MAX | ERANGE |
以字串轉成長整型函式strtol為例,
在64位機器下,long長度為8位元組,最大值LONG_MAX 為 0x7fffffffffffffff,當變數longStr 取超出長整型最大值的字串”0xffffffffffffffff”和剛好等於最大值的字串”0x7fffffffffffffff”時,函式的返回值都為相同的LONG_MAX。此時金聰返回值是無法判斷函式的執行的成功與否。這個時要判斷errno的值。如下例中,會列印出錯誤的資訊。
errno =0;
//LONG_MAX的最大值為0x7fffffffffffffff
const char * longStr = "0xffffffffffffffff";
long ret = strtol(longStr,NULL,16);
if (ret == LONG_MAX) {
if (errno!=0) {
printf("error : %d \n",errno);
printf("錯誤資訊 : %s \n",strerror(errno));
}else{
printf("等於long的最大值\n");
}
}
2.函式返回值可知錯誤,errno可知更詳細的錯誤
函式 | 返回值 | errno值 |
---|---|---|
ftell() | -1L | positive |
fgetpos()、fsetpos() | nonzero | positive |
mbrtowc()、mbsrtowcs() | (size_t)(-1) | EILSEQ |
signal() | SIG_ERR | positive |
wcrtomb()、wcsrtombs | (size_t)(-1) | EILSEQ |
mbrtoc16()、mbrtoc32() | (size_t)(-1) | EILSEQ |
c16rtomb()、cr21rtomb | (size_t)(-1) | EILSEQ |
3.有不同標準文件的庫函式
有些函式在不同的標準下對errno有不同的定義,例如fopen中便是一個例子。C99並沒有對使用fopen是對errno做要求,但POSIX.1卻宣告瞭錯誤時返回NULL,並將錯誤碼寫入errno。
避免使用goto語句
goto語句有很多優點,例如goto語句可以非常方便的在區域性作用域中跳出多層迴圈,執行如無條件的跳轉。
但正因為goto語句可以靈活的跳轉,如果不加以限制它會破壞程式的結構化風格,使得程式碼難以理解與測試,同時不加限制的使用goto語句可能跳過變數的初始化、重要的計算等語句。
以下例子在a小於0或者a小於等於100時會使用goto跳轉到標記為Error的語句中。
注意goto只能在區域性作用域中跳轉。
void testGoto(int a)
{
if (a>0) {
if (a>100) {
printf(" a = %d \n",a);
}else{
goto Error;
}
}else{
goto Error;
}
Error:
printf("Test Error a = %d \n",a);
}
避免使用setjmp與longjmp
相比與goto語句只能在區域性作用域中跳轉,setjump與longjmp可以進行跨作用域跳轉,也就是跨函式跳轉。
我們知道函式呼叫都以函式棧的形式進行呼叫與退出,既然要做到跨函式跳轉,那便需要對當前的函式棧進行儲存與還原,而setjmp的作用便是儲存當前函式棧至型別jmp_buf結構體變數中,而longjmp的作用便是從此結構體中恢復,還原函式棧。
而相對於goto僅在作用域內跳轉,setjmp和longjmp則使程式碼更加的難以維護以及可讀。
小結
- C語言中,使用函式的返回值來標誌函式是否執行成功(預設成功返回1,失敗返回0)當使用介面時,必須對函式進行正確性的驗證,檢查它的返回值,並且對每個錯誤的返回值進行相應的處理以及提示。
- 同樣的道理,如果作為介面的開發方,需要對函式的各種情況反映到返回值中。
- 編寫程式碼是,無論使用什麼樣的錯誤處理方式,發現程式中錯誤最好的方法便是執行程式,讓資料在函式中流動,在判斷邏輯中查詢到函式出錯的地方。
相關文章
- C++語言程式設計筆記 - 第12章 - 異常處理C++程式設計筆記
- swoft 學習筆記之異常處理筆記
- 【C++】 63_C語言異常處理C++C語言
- C++ 異常處理C++
- C++異常處理C++
- OS學習筆記一: 中斷與異常筆記
- C與C++中的異常處理 (轉)C++
- C++筆記--異常C++筆記
- C++異常處理與臨時副本C++
- c++異常處理格式C++
- c++ 異常處理(2)C++
- c++ 異常處理(1)C++
- c++異常處理 (轉)C++
- windows核心程式設計---未處理異常,向量化異常處理與C++異常Windows程式設計C++
- 【C++】 C++異常捕捉和處理C++
- SpringMVC學習筆記10-異常處理SpringMVC筆記
- 《C++ Primer》學習筆記(五):迴圈、分支、跳轉和異常處理語句C++筆記
- Golang 學習筆記八 錯誤異常Golang筆記
- C與C++中的異常處理11 (轉)C++
- C與C++中的異常處理13 (轉)C++
- C與C++中的異常處理12 (轉)C++
- C與C++中的異常處理14 (轉)C++
- C與C++中的異常處理15 (轉)C++
- C與C++中的異常處理16 (轉)C++
- C與C++中的異常處理17 (轉)C++
- C與C++中的異常處理3 (轉)C++
- C與C++中的異常處理4 (轉)C++
- C與C++中的異常處理5 (轉)C++
- C與C++中的異常處理7 (轉)C++
- C與C++中的異常處理6 (轉)C++
- C與C++中的異常處理9 (轉)C++
- C與C++中的異常處理8 (轉)C++
- C與C++中的異常處理10 (轉)C++
- C++異常處理機制C++
- 異常處理 - Go 學習記錄Go
- (十五)C++學習 | 強制型別轉換 異常處理C++型別
- java異常處理筆記Java筆記
- Go語言學習筆記 - PART12 - 異常處理機制與單元測試Go筆記