近來學習操做系統這門課,課程的實驗基於linux 0.11,因而從圖書館借來了 C Traps and Pitalls 和 Expert C programming,打算提升一下c語言水平。
先從前一本開始。這本書很薄,即便是英文版也只有140多頁,講的都是c語言中容易犯錯的地方。
注意:這篇筆記並無包括整本書的內容,而只是摘抄了本人須要的知識(加上了一些本身的理解)。如需完整了解,還請自行看書。linux
extra space包括空格、製表符和換行
好比你以爲這個函數聲明太長程序員
int* foo(int arg1, const char *arg2, double arg3, struct bar *arg4) { /*...*/ }
能夠寫成編程
int* foo( int arg1, const char *arg2, double arg3, struct bar *arg4) { /*...*/ }
編譯器從左到右分析符號,使獲取的符號儘量地長。a---b;
等價於 a-- -b;
數組
因此 a = b/*p;
這種寫法極可能不是你想要表達的意思。 /*
會被認爲是註釋符的左邊,因而它後面的內容都成了註釋,實現除法的寫法應該是 a = b / *p;
。緩存
可是,形如a-----b
就不能理解成 (a--)-- (-b)
,而應該是 (a--)-(--b)
,由於 (a--)--
這樣是不符合語法的。函數
int a = 90;
和 int b = 090;
是不同的呀。
後者在9的前面多了一個0,編譯器就會覺得090是八進制,因而b的值就是9*8^1 + 0*8^0 = 72d
學習
char就至關於整型,只是它的大小是1字節。相應地,字符'a'就是對應ASCII碼裏的數字。
char*是指針,32位下大小爲4字節。與其它指針無異。
一個用雙引號括起來的字符串(如"hello"
),就是一個指向字符串(加上結尾的'\0'
)首地址的指針縮寫。
因此spa
printf("hello");
相似於操作系統
char* str = "hello"; printf(str);
假若有函數debug
void bar(int arg);
那麼:bar();
執行函數int (*p)(int a) = bar;
bar是函數地址,能夠賦值給函數指針(固然類型要相同)
Only two things can be done to an array: determine its size and obtain a pointer to element 0 of the array. All other array operations are actually done with pointers, even if they are written with what look like subscripts.
若是定義指針來當數組用,須要手動用malloc在運行時堆得到空間;而定義數組,編譯器就自動在棧幀裏分配空間。指針和數組的操做類似,關於數組的操做大部分都是基於指針的。
這裏要指出的是關於char類型數組和指針的兩個不一樣。
假定如今須要一個hello的字符串,能夠這麼寫:
char* str = "hello"; char str[6] = {'h','e','l','l','o','\0'};
前者編譯器會在字符串後面加上結束符 '\0'
,後者則須要本身注意留出空間存放該符號。
字符串常量(好比上面的 "hello"
)存放在常量區,在編譯時就肯定好了,在運行的時候不能修改。因此下面的操做是錯誤的。
char* str = "hello"; //指針指向常量區 str[3] = 'a'; //錯誤,修改了常量區的內容
而使用數組則不會出現這樣的問題,由於char數組會將內存常量的內容複製到棧幀中。
char str[6] = "hello"; //將內存常量中的"hello"複製給棧幀中的數組 str[3] = 'a'; //正確,修改的是棧幀中的內容
不能將整個數組看成參數傳入函數,使用數組名做爲參數會把數組首地址做爲指針傳入函數。因而
int strlen(char s[]) { /* stuff */ }
跟
int strlen(char *s) { /* stuff */ }
的效果是同樣的。因此常見的參數 char* argv[]
和 char** argv
也是同樣的。
更準確的說法是,數組做爲參數傳入函數會「退化」成指針。在定義該數組的函數裏使用 sizeof
能夠正確得到數組大小,而將該數組首地址傳入函數後,只用 sizeof
只能得到指針大小。例如
int main() { char str[] = "hello"; printf("%u", sizeof(str)); return 0; } //輸出結果爲6
int main(int argc, char* argv[]) { printf("%u", sizeof(argv[0])); return 0; } //輸出結果爲4
寫for循環的時候,會遇到一個問題:
for (int i = 0; i < 10; i++) { a[i] = 0; }
for (int i = 1; i <= 10; i++) { a[i] = 0; }
哪一個寫法好呢?本書給出了後者寫法的理由。
左閉右開
若是 x>=20 && x<=40
爲真,那麼x有多少種可能的取值?20?答案應該是 40 - 20 + 1 = 21
。這樣的寫法彷佛挺反直覺,容易出錯,若是寫成左閉右開 x>=20 && x<41
就好多了。相似地,在C語言裏,數組第1個元素下標爲0,因而定義一個10個元素的數組,最大的下標爲9。可能挺反直覺,可是隻要利用好這特色,使代碼簡潔,邏輯簡單。例如:
#define N 128 static char buffer[N]; void bufwrite(char *p, int n) { int i; int temp; for (i = 0; i < N && (temp=getchar()) != EOF; i++) { bufer[i] = temp; } }
那麼退出循環的時候i
就必定是緩衝區中元素的個數了。
其實,採用左開右閉寫法習慣,更可能是由於c語言本數對內存的描述是地址加偏移量。好比數組int a[10]
不存在a[10]
而存在a[0]
。咱們主動迎合這種作法能夠避免很多麻煩。不然,想要一個10個元素的數組就要定義int a[11]
。這麼作增長了出錯的可能。
操做符&&
||
?:
,
都是從左到右執行。
其中, &&
左邊操做數爲假,右邊不會執行; ||
左邊操做數爲真,右邊不會執行。能夠利用這個特色簡化代碼,例如
int a[100]; int i = 0; while (i < 100 && a[i] != 0) { /* stuff */ } 其中,`&&` 保證了數組訪問不會越界。
除了特定的操做符 &&
||
?:
,
,操做符對其操做數的求值順序是不肯定的。常見的
int i = 0; while (i < n) { y[i] = x[i++]; }
賦值號左邊的i是不肯定的。正確寫法能夠爲:
int i = 0; while (i < n) { y[i] = x[i]; i++; }
兩個unsigned類型相加不會有溢出的問題(簡單捨去進位),一個unsigned和一個int相加也沒有問題(int將會被轉換爲unsigned)。兩個int相加就可能存在溢出問題了。
假定兩個int都爲正數。對於檢查溢出,這樣寫是不對的
if (a + b < 0)
由於在檢查的過程當中已經致使溢出發生。而不一樣機器對溢出的處理是不同的,不可以假定溢出了什麼事都不發生。因此檢查溢出時應該避免溢出發生:
if ((unsigned)a + (unsigned)b > INT_MAX) //或 if (a > INT_MAX - b)
c程序是先編譯後連接。具體能夠看CSAPP的第七章,有關於連接的基本介紹
爲了兼容舊版本的c程序,對於函數的聲明,須要忽略參數。以庫函數square爲例,聲明爲double square();
,只要在調用函數的時候可以正確地傳入參數,就能夠正常調用函數。
有一點須要注意:對於上面square的聲明,若是傳入函數的參數爲float類型,它在傳入時會被轉化爲double類型,這樣沒什麼不妥;對於參數類型爲short、char的函數,若是聲明時忽略參數,傳入的參數會被轉化爲int,就會產生問題:函數要的是8位的char,而int的長度通常都不是8,進行位運算可能會出現問題。
固然,以上都是爲了兼容纔在聲明時忽略參數。咱們寫程序仍是老老實實把聲明寫完整吧。
好比,定義一個全局變量 int a;
,而在另外之外一個c文件錯誤地聲明爲 extern long a;
。顯然,這樣是錯的。編譯器和連接器足夠聰明的話,能發現這個問題,而後給出錯誤。不然,編譯、連接經過,問題潛伏在程序中。程序可能正常運行(int和long都爲32位等緣由),或者出錯(聲明爲double,定義爲int等)。總之,程序員有義務保證聲明和定義一致。
定義和聲明不一致致使錯誤很正常啊,爲何要拿一小節來說呢?由於定義全局變量 char str[];
和聲明 extern char* str;
就是定義和聲明不一致的錯誤,而咱們不易察覺。問題在於:char指針不是char數組啊(Orz二者像歸像,仍是有不一樣啊....)
char a[128]; //a表明這個數組的首地址 char *b = a; //b是一個指針變量,它的值爲數組a的首地址 a++; //錯誤 b++; //正確
體現的是char類型的數組與指針的區別。
頭文件瞭解決上面一小節的問題。作法是,將全部外部變量的聲明都寫在頭文件,而後將頭文件include在每一個涉及這些變量的文件中,包括定義該變量的文件。緣由是,在同一文件中聲明和定義一個全局變量,那麼編譯器就能夠檢查聲明和定義是否一致,若是一致,在其它文件中只要include這個頭文件,就可使用該變量了。將函數聲明放入頭文件也是一樣道理。
有一點要說明:函數默認是全局的(除非是 static
),因此在其餘文件聲明的時候能夠不加 extern
,但出於閱讀方便,咱們都是加上 extern
(標準庫頭文件都有);變量則必定要加(不加 extern
就變成定義了)。
探究:若是不使用頭文件會怎樣?能夠直接引用外部函數和變量嗎?
若是沒有聲明就直接引用外部變量和函數,那麼編譯器就假定函數的返回值爲int,變量爲int類型,而後連接器把引用連接起來。若是碰巧引用的變量的確是int型,那麼程序正常,不然類型對不上發生錯誤。
在沒有外部聲明的狀況下,函數內部不能直接引用其餘文件的全局變量。
先看一段代碼
#include <stdio.h> int main() { char c; while ((c = getchar()) != EOF) { putchar(c); } return 0; }
代碼的功能就是將輸入流的內容轉到輸出流中,直到輸入流爲空。如今來講說代碼存在的問題。首先要說明,char爲 unsigned char
或 signed char
是由編譯器決定的,大部分編譯器默認爲 signed char
,EOF
通常定義爲-1。假定int爲32位。
若是char爲 unsigned char
。當getchar返回 EOF(0xffffffff)
,那麼變量c被賦值爲 0xff
。與EOF(int)比較,c須要擴展爲0x000000ff
(無符號擴展用零填充),二者不相等,循環將不會中止。
若是char是 signed char
。當getchar讀到字符 0xff
,因而返回 0x000000ff
,c被賦值爲 0xff
。與EOF(int)比較,c須要擴展爲 0xffffffff
(有符號擴展用符號位填充),二者相等,循環提早結束。
出於以上,c也應該改成int,使代碼正常工做。
注意:某些編譯器直接拿getchar的返回值與EOF比較。這樣雖然不能正確表達代碼的意思,但能使程序正常工做。
手動爲文件分配緩衝區,可使用setbuf函數,在緩衝區滿時輸出。
#include <stdio.h> int main() { int c; char buf[BUFSIZ]; setbuf(stdout, buf); while ((c = getchar()) != EOF) { putchar(c); } return 0; }
這段代碼出錯在於,緩衝輸出在main結束的時候,這時buf數組已經不存在。
解決辦法是爲數組buf加上關鍵字 static
,成爲靜態變量(但不建議在函數內部定義靜態函數);或者使用malloc函數,如
setbuf(stdout, malloc(BUFSIZE));
這樣,main結束時緩衝區仍然存在。可是要時刻留意malloc以後要不要free!
並沒有規定:若是庫函數運行正常,要將errno設置爲0。因而如下寫法錯誤:
call library function if (errno != 0) complain
那麼在使用函數前就把errno設置爲0呢?
errno = 0; call library function if (errno != 0) complain
不行!由於庫函數內部可能會調用其它庫函數。好比調用fopen,函數會調用其它庫函數去檢查某個文件是否存在,若是文件不存在,則errno會被設置,而後建立新文件,返回指針。這時候fopen是正常工做的,可是errno卻不爲0。
那errno該怎麼用?最好的辦法應該是結合返回值使用了。應該在函數返回錯誤信息後,再檢查errno
call library routine if (error return) examine errno
原則是signal處理函數儘量簡單。最好是輸出相關信息後就用exit退出程序。緣由是,信號接收可能出如今任什麼時候候(malloc時接收到信號,信號處理又調用malloc);並且信號處理完後不一樣機器有不一樣操做(某些機器在某些信號處理後重復失敗的操做,如除數爲零)。
切記宏只是簡單地複製粘貼!
如下面的定義爲例
#define MAX(a,b) ((a)>(b)?(a):(b))
不加括號必然引發悲劇,不用多說。
其次,謹慎使用 ++
--
之類的運算, MAX(a,a++)
也會產生奇怪的結果。
再次,謹慎嵌套,如 MAX(a, MAX(b, MAX(c, d))
,展開後表達式很長,debug時會很痛苦,並且會產生沒必要要的重複運算。
好比,謹慎在宏用 if
。假如assert是這樣定義的:
#define assert(e) if(!e) assert_error(__FILE__,__LINE__)
那麼
if (a > 0 && b > 0) assert(x > y); else assert(x < y);
展開就變成
if (a > 0 && b > 0) { if (!(x > y)) assert_error(__FILE__,__LINE__); else if (!(x < y)) assert_error(__FILE__,__LINE__); }
這顯然不是咱們要的結果。可能會想到將assert用花括號圍起來。但這樣就會在if和else之間出現語法錯誤。
實際上,assert是這樣定義的:
#define assert(e) ((void)((e)||_assert_error(__FILE__,__LINE__))
它是一個值而不是語句。
閱讀完這本書後,對這些陷阱能夠總結爲:只作正確的事,不要作一些感受應該正確的事。有些陷阱是歷史緣由,有些是奇怪的緣故,還有些是邏輯的問題。總之,謹慎!同時對c的認識又加深了:c是貼近硬件的語言。還加上嵌入彙編的功能,難怪寫操做系統要用c;還有,這本書提升了我查文檔的能力!這本書對數組和指針的解釋比較分散,建議閱讀c專家編程做爲系統瞭解。