前言:本文的目的是記錄C語言中那些容易被忽略的細節。我打算每天抽出一點時間看書整理,堅持下去,今天是第一篇,也許下個月的今天是第二篇,明年的今天又是第幾篇呢?……我堅信,好記性不如爛筆頭。第三篇了,fight~...
第一篇鏈接:C語言中容易被忽略的細節(第一篇)
第二篇鏈接:C語言中容易被忽略的細節(第二篇)
1、__attribute__((noreturn))
__attribute__可設置函數屬性、變量屬性和類型屬性。__attribute__((noreturn))設置了函數屬性,noreturn通知編譯器函數從不返回值,當遇到函數需要返回值卻還沒運行到返回值處就已退出來的情況,該屬性可以避免出現錯誤信息。例如:
void Test(); int main(void) { int x; scanf("%d", &x); if (x > 0) Test(); else return 0; }上面代碼在編譯階段提示警告:warning: control reaches end of non-voidfunction。產生警告的原因是:Test()函數沒有返回值,所以main()函數有可能沒有返回值,程序不知如何處理。
void Test() __attribute__((noreturn));若將Test()的聲明修改為以上,則相當於通知編譯器Test()函數從不返回值,也就不會產生警告了。
注意:(1)attribute前後的下劃線輸入的方法是:Shift+減號(輸入兩次);(2)__attribute__機制有很多其他用處,是GNU C的特色,建議讀到相關內容時候再總結,做到有的放矢。
2、如果編譯器按照內存地址遞減的方式來給變量分配內存,以下代碼有什麼問題?
int i, a[10]; for (i = 0; i <= 10; i++) a[i] = 0;
解析:數組越界,程序陷入死循環。內存中數組a之後的四個字節實際上分配給了整形變量i,對a[10]賦值為0實際上是將計數器i的值設置為0。
3、在if/else結構中,要盡量把為TRUE的概率較高的條件判斷置於前面,這樣可以提高該段程序的性能。switch的效率比if/else結構高,即使程序真的不需要default處理,也應保留語句default:break;
if語句判斷其條件表達式的真假並不是通過把它的計算結果轉換為布爾類型的臨時變量進行的,而是將其結果直接和0進行比較,如果不等於0則表示真,否則為假。不要將布爾變量直接與true、1、-1、0等進行比較。
bool flag; if (flag) //表示flag為真 if (!flag) //表示flag為假(2)整型變量與零值比較
int value; if (value == 0) if (value != 0)
(3)浮點變量與零值比較
計算機表示浮點數(float或double)都有一個精度限制。對於超過了精度限制的浮點數,計算機會把它們精度之外的小數部分截斷。因此,本來不相等的兩個浮點數在計算機中可能就變成相等的了。例如:float a = 10.222222225, b = 10.222222229;在數學上a和b是不等的,但在32位計算機中它們就是相等的。(注:float可保證6位有效數字,double和long double可保證10位有效數字)在針對實際應用環境編程時,總是有個精度要求,而直接比較一個浮點數和另外一個值(浮點數或整數)是否相等(==)或不等(!=)可能得不到符合實際需要的結果,因為==和!=比較操作采用的精度往往比實際應用中要求的精度高。
可將“>”和“<”直接用於浮點數之間比較及浮點數和整數的比較。!(a > b) && !(a < b)與a == b的語義是等價的,所以也不建議用於判斷浮點數相等與否。
#define EPSILON 1e-6 if (abs(x - y) <= EPSILON) //x等於y if (abs(x - y) > EPSILON) //x不等於y if (abs(x) <= EPSILON) //x等於0 if (abs(x) > EPSILON) //x不等於0
(4)指針變量與零值比較
指針變量的零值是“空值”(記為NULL),即不指向任何對象。盡管NULL的值與0相同,但兩者意義不同(類型不同)。
if (p == NULL) if (p != NULL)
備注:使用if (NULL == p)、if (100 == i)這種寫法比較好,因為如果誤將==寫為=,因為編譯器不允許對常量賦值,就可以檢查到錯誤。
5、va_list、va_start()、va_arg()和va_end()
C標准函數庫的stdarg.h頭文件定義了可變參數函數使用的宏。可變參數函數內部必須定義一個va_list變量,然後使用宏va_start、va_arg和va_end來讀取。相關定義如下:
typedef char* va_list; void va_start ( va_list ap, prev_param ); /* ANSI version */ type va_arg ( va_list ap, type ); void va_end ( va_list ap );
舉個簡單例子如下:
#include#include double average(int count, ...) { va_list ap; int j; double tot = 0; va_start(ap, count); //使va_list指向起始的參數 for(j=0; j C語言中可變參數函數在沒有長度檢查和類型檢查,在傳入過少的參數或不符的類型時可能會出現溢位的情況。
6、求值順序
C語言中只有4個運算符(&&,||,? :和, )存在規定的求值順序。&&和||首先對左側操作數求值,只在需要時才對右側操作數求值;對於a ? b : c,操作數a首先被求值,根據a的值再求操作數b或c的值;對於逗號運算符,先對左側操作數求值,然後該值被“丟棄”,再對右側操作數求值。例如:
i = 0; while (i < n) y[i] = x[i++];以上從數組x中復制前n個元素到數組y中的代碼是不正確的。因為賦值運算符並不保證任何求值順序。y[i]的地址將在i的自增操作執行之前被求值還是之後求值是不確定的。
注意:運算符的優先級與求值順序是完全不同的概念。
7、對於數組結尾之後的下一個元素,取它的地址是合法的,讀取這個元素的值的結果是未定義的,而且絕少有C編譯器能夠檢測出這個錯誤。
8、字符數組不一定是字符串
字符數組是元素為字符變量的數組,字符串是以’\0’(ASCII碼值為0x00)為結束字符的字符數組。
(1)對於字符數組來說,它並不在乎中間或末尾有沒有’\0’結束字符,因為數組知道它自己有多少個元素,況且’\0’對它來說是一個合法的元素。
(2)如果字符數組中沒有’\0’結束標志,卻被當做字符串來用時可能會導致“內存訪問沖突”或篡改其他內存單元,strlen函數的結果異常等。舉個簡單的例子:
#includeint main(void) { char arr1[] = {'a', 'b', '\0', 'c', 'd'}; char arr2[] = "Hello"; char *p = "Hello"; printf("%d %d\n", sizeof(arr1), strlen(arr1)); //結果5 2 printf("%d %d\n", sizeof(arr2), strlen(arr2)); //結果6 5 printf("%d %d\n", sizeof(p), strlen(p)); //結果4 5 return 0; }
9、不要用字面常量來初始化引用
const int &a = 0;以上語義並非是把引用初始化為NULL,而是創建一個臨時的int對象並用0來初始化它,然後再用它來初始化引用a,而該臨時對象將一直保留到a銷毀的時候才會銷毀。
10、引用的創建和銷毀並不會調用類的構造函數和析構函數。在二進制層面,引用一般是通過指針來實現的,只不過編譯器幫我們完成了轉換。