第一章
第一節
1,register關鍵字簡化了編譯器,卻將包袱丟給了程序員。
第三節
1,宏處理器只應該適量使用。最好只用于命名常量,并為一些適當的結構提供簡捷的記法。
2,不要使用C預處理器修改語言的基礎結構。
第八節
1,K&R C中,函數聲明的形參類型檢測在鏈接時或者不作檢查,函數定義頭以分號結尾。
2,雖然編譯器忽略形參名,但函數聲明的形參名對程序員有好處。
3,除非需要使用類型提升,否則不要使用K&R C的寫法定義函數。
4,從const char *到const char **
第九節
1,const char * 和 char * const區別
const char *的const修飾的對象是其指針所指的char對象。
char * const的const修飾的對象是該指針本身的值。
2,從char **賦值到const char **
如果從char *復制到const char *,如很多的字符串處理函數,都是可以的。
但是從char **賦值到const char **,就不行了。
因為char**的含義是:指向,一個指向char類型的指針,的指針。
而const char **的含義是:指向,一個指向char類型的,帶const限定的指針,的指針。
即兩個指針的類型并不相同。
但是將char **賦值給char * const *是沒有問題的。因為const修飾的是指針的對象,不再是指針本身。
第十節
1,c中的浮點數據類型:
float, double, long double三種,其長度分別為4,8, 12個字節。不可在其前加unsigned修飾。
2,尋常算術轉換:
如果一個操作數是long double,則另一個操作數被轉換為long double;如果一個操作數是double,
則另一個操作數被轉為double,再如果一個操作數是float,則另一個操作數被轉為float。
結果也一樣跟著被轉換。
3,c中的字符及整型
char , short int, int , long int , long long int
修飾short, long , long long其后可不接int,默認為int(問題,不為int還能是其他么?)
4,ansi c的值保留原則:
多種不同類型的整型混合運算的時候,其轉換是依照類型的大小進行。
5,整型提升:
c專家編程中1.10節的內容看得懂,但梳理不了。
第十一節
#pragma指示器的作用是由編譯器定義的。
第二章
第一節
1,分析編程語言缺陷的一種方法就是將所有缺陷分成三種:多做之過,少做之過,和誤做之過。
第二節
1,switch的default語句可以出現在case列表的任意位置,并在所有case均無法匹配時被執行。
2,switch在花括號之后的任意語句都可以進行變量聲明,但在case之前的語句不會被執行。
3,switch內部的任何語句都可以包含標簽。
4,switch缺省采用"fall througth",在97%的情況下都是錯誤的。
5,拖尾逗號沒有存在的必要(字符串數組的最后一個逗號)
6,c中對信息可見性的選擇非常少。
第三節
1,apple = sizeof(int) * p; 不懂。
2,當按常規方式使用時,可能會引起誤會的任何運算符,都是c運算符存在錯誤優先級的表現。
3,“有些運算符是的優先級是錯誤的”,忘記這些錯誤,就是對了。
4,結合性,在幾個操作符具有相同優先級時決定先執行哪個。
5,在函數調用中,各個參數的計算順序是不確定的。
第四節
1,unix和ansi c都不能區分傳遞給程序的選項和參數。
2,空格在c中有時是必不可少的,例如:z=x+++++y;z=*x/*y;
3,c++的行注釋可能會破壞原來正確的c代碼,如:
a //*
//*/ b
4,避免函數返回值被覆蓋的最好方法也許就是要求調用者提供自行分配內存。
5,將lint程序作為一個獨立的工具通常意味著將lint程序束之高閣。
第五節
1,即使可以保證你的編程語言100%可靠,你仍然可能成為算法中災難性bug的犧牲品。
第三章
第一節
1,K&R 承認C語言聲明的語法有時會帶來嚴重的問題。
2,指向數組的指針
char (*j)[20];
j = (char (*)[20])malloc(20);
3,telnet程序的一行代碼:
char * const *(*next)();
第二節
1,列出的兩個表看不懂,不知道有什么用。
第三節,第四節
1,理解c語言聲明的優先級規則和圖標分析c語言聲明,感覺作用不大。感覺記住了運算符的優先級和結合性才是王道。
第五節
1,signal函數的ANSI C聲明:
void (*signal(int sig, void(*func)(int)))(int);
分析:signal跟其后的括號結合,則signal是一個函數;signal函數的返回值跟signal前的星號結合,即返回一個指針。
到此,最左邊括號的全部內容分析完。返回的指針右邊又是一個括號(int),則返回的指針是個函數指針,該指針指向
的函數返回void類型。
2,typedef的混亂語法:
可以將多個聲明器塞到一個聲明中;
typedef可以不放在聲明的開始處。
如:
typedef int *ptr, (*fun)(), arr[5];
/*
*ptr是int指針類型,fun是返回值為int類型的函數指針類型,arr是長度為5的int數組類型。
*/
unsigned const long typedef int volatile *kumquat;
第六節
1,可以用其他類型說明符對宏類型名進行擴展,但對typedef所定義的類型名不能這么做,如:
#define peach int
unsigned peech i; /* ok */
typedef int banana;
unsigned banana i; /* error */
2,在連續幾個變量聲明中,用tpyedef定義的類型能夠保證聲明中所有的變量均為同一類型,而用#define定義的類型則無法保證。如:
#define int_ptr int *;
int_ptr chalk, cheese; /* cheese為整型 */
typedef int * int_ptr;
int_ptr a, b; /* a,b均為整型指針 */
第七節
1,不要使用typedef來省略struct關鍵字,因為struct關鍵字可以提示一些信息。
2,typedef應該用在:
數組,結構,指針及函數的組合類型。
可移植類型。
3,應該始終在結構的定義中使用結構標簽,即使它并非必須。
4,typedef struct my_tag {int i;} my_type;這個typedef聲明引入了my_type這個名字作為"struct my_tag{int i;}"的簡寫。
第八節
1,編寫一個能夠分析c語言的聲明并將它們翻譯成通俗語言的程序。
2,該節有偽代碼,第九節有詳細c代碼。
第四章
第三節
1,代碼:
/*file1*/
int s[3] = {1, 2, 3};
/*file2*/
extern int *s;
在file2中不能使用下標進行數組訪問,如s[0]。
解釋:當在file2使用s[0]的時候,其內存訪問方式為,提取s本身的地址,得到一個指針,再提取指針指向的地址,該地址加上偏移,取該偏移地址的值。
而在file1使用使用s[0]的時候,其內存訪問方位為,提取s本身的地址,加上偏移,取偏移地址的值。
而在file1,file2中變量s本身的值是相同的,但在file2中的s作為指針,其指向并未賦值。
可以使用以下代碼驗證:
/*file1*/
int p[10] = {1, 2, 3};
int *s = p;
/*file2*/
extern int *p;
extern int *s;
int
main(int argc, char *argv[])
{
printf("%p\n", p);
printf("%p\n", &p);
printf("%p\n", s);
printf("%d\n", (int)(&p)[1]);
return 0;
}
2,似乎書上最后(4.3節)對:“聲明為extern char *p,定義為char p[10],使用指針的p[i]進行訪問”的解釋不正確。
書上的意思是,對p[i]可以得到一個字符,而編譯器理解成了指針,從而錯誤,
但實際錯誤應該是對p指向地址不確定。
3,對于多維數組的聲明,需要提供除左邊一維之外其他維的長度。
4,數組名在符號表中被映射為一個地址,不可修改。因此數組名是個左值,但不是可修改的左值。
第五章
第一節
1,絕大多數編譯器通常由多達六七個稍小的程序鎖組成,包括:預處理器,語法語義檢查器,代碼生成器,匯編程序,優化器,鏈接器和調用這些程序的驅動器。
2,PC的鏈接器一般只提供幾個基本的I/O服務,稱作BIOS。存在于內存中固定的地點,并不是每個可執行程序的一部分。
3,靜態鏈接:庫函數的代碼被加入到可執行文件。
4,動態鏈接收集模塊準備執行的三個階段:link-editing, loading, runtime linking。
5,在main函數執行前,運行時載入器將共享數據對象載入進程地址空間,但直到被實際調用,才解析它們。
第二節
1,動態鏈接庫必須保證的四個特定函數庫:
libc(c運行時函數庫),libsys(其他系統函數),libX(X windowing)和libnsl(網絡服務)。
2,鏈接器通過將庫文件名或路徑名植入可執行程序來保證程序在運行時能找到所需函數庫。
3,有些函數庫只能使用動態鏈接,如果使用了其中一個,程序就必須使用動態鏈接。
4,位置無關代碼?
第三節
1,頭文件名字并不與它對于的函數庫名相似;函數定義在一個函數庫中,卻聲明在多個頭文件中。
2,cpp, gcc, as, ld
3,在動態鏈接庫中,所有的庫符號進入輸出文件的虛擬地址空間中,所有的符號對于鏈接到一起的所有文件都是可見的。相反,對于靜態鏈接,在處理archive時,它只是在archive中查找載入器當前所知道的未定義符號。
第四節
1,Interpositioning就是通過編寫與庫函數同名的函數來覆蓋該庫函數的行為。
2,使用Interpositioning,不僅自己調用的該庫函數會被自己的版本覆蓋,另一些調用該庫函數的庫函數都會調用你自己的版本。
第六章
1,編程語言理論的經典對立之一就是代碼和數據的區別。
第一節
1,a.out - 匯編語言程序和鏈接編輯輸出格式
“匯編語言程序輸出”這個名字的產生純屬歷史原因。
第二節
1,目標文件和可執行文件可以有幾種不同的格式,linux采用ELF(可擴展鏈接器格式)。
2,section是ELF文件中的最小組織單位。一個段(segments)一般包含幾個section。
3,BSS段不保存在目標文件中(除了記錄BSS段在運行時所需要的大小)
第三節
1,從本質上說,段在正在執行的程序中是一塊內存區域,每個區域都有特定的目的。
第四節
1,堆棧段有三個主要用途:
為局部變量提供存儲空間
保存函數調用棧幀
臨時存儲區,如算術表達式計算,alloca函數
2,除了遞歸調用之外,堆棧并非必須的。因為在編譯時可以知道局部變量的大小,參數和返回地址所需的空間固定大小。
第五節
1,c語言自動提供的服務之一就是跟蹤調用鏈。
2,linux棧幀結構:參數,返回地址,局部變量。
第六節
1,對于程序員來說,auto關鍵字幾乎沒有什么用處,因為它只能用于函數內部。但是在函數內部聲明的數據缺省就是這種分配。唯一能用到auto的地方就是使你的聲明更加清楚整齊。
2,過程活動記錄并不一定要存在于堆棧中。事實上,盡可能地將過程活動記錄的內容放到寄存器中會使函數調用的速度更快,效果更好。
第七節
1,保證局部變量在longjmp過程中一直保持它的值的唯一可靠方法是將它聲明為volatile(這使用于那些值在setjmp執行和longjmp返回之間會改變的變量)。
2,setjmp和longjmp在c++中變異為更普通的異常處理機制catch和throw。
3,想goto一樣,setjmp和longjmp使得程序難以理解和調試。如果不是處于特殊需要,最好避免使用它們。
第十一節
1,用grep來調試操作系統內核,一個非比尋常的概念。有時候甚至連源代碼工具都可以幫助解決運行時問題!
第七章
第三節
1,所有現代的計算機系統,從最大的超級計算機到最小的工作站,都使用了虛擬內存。
第八章
第三節
1,類型提升的出現并不局限于涉及操作符和混合類型操作數的表達式。
2,ANSI C表示如果編譯器能夠保證運算結果一致,也可以省略類型提升,這通常出現在表達式中存在常量操作數的時候。
3,在ANSI C中,如果使用了適當的函數原型,類型提升就不會發生,否則也會發生。在被調用的函數內部,提升后的參數被裁剪為原先聲明的大小。
4,這就是為什么單個的printf格式字符串%d能適應幾個不同類型,short,char,int,而不論實際傳遞的參數是上述類型的哪一個。
第四節
1,ANSI C函數原型的目的是使C語言成為一種更加可靠的語言。建立原型就是為了消除一種普通(但很難發現)的錯誤,就是形參和實參之間類型不匹配。
第五節
1,缺省的函數聲明(函數在另外的文件定義)是K&R C的聲明?
2,K&R C函數聲明和K&R C函數定義,會發生類型提升。
3,ANSI C函數聲明和ANSI C函數定義,不會發聲類型提升。
4,ANSI C和K&R C的聲明和定義混合的時候,注意K&R C的聲明會提升后傳遞參數,而K&R C的定義會按提升之后接受參數。
5,由于不懂匯編,書上那些代碼不太明白。
第六節
1,curses函數庫提供了基于字符的屏幕控制函數。
第七節
1,在c語言中,有好幾種方法來表達FSM(有限自動機),但它們絕大多數都是基于函數指針數組,如:
extern int a(), b(), c(), d();
int (*state[])() = {a, b, c, d};
2,調用函數和通過指針調用函數(或任意層次的指針間接引用)可以使用同一種語法。至于數組,也有一個對應的方法。這種做法進一步惡化了本來就有缺陷的“聲明與使用相似”的設計哲學。
第八節
1,如果你擁有十分復雜的數據結構時,你可以編寫并編譯一個函數,用于遍歷整個數據結構并打印出來。這個函數不會在代碼的任何地方調用,但它卻是可執行文件的一部分。它就是debugging hooks。
2,有時候,花點時間把編程問題分解成幾個部分往往是解決它的最快方法。
第九節
1,調用qsort時,可以在調用時對第四個參數(函數指針)進行強制類型轉換,而無須在函數內進行。
第九章
第一節
1,聲明可以分三種情況:外部聲明;定義;函數參數聲明。
2,對編譯器而言,一個數組就是一個地址,一個指針就是一個地址的地址。
第二節
1,表達式中的數組名(與聲明不同)被編譯器當作一個指向該數組第一個元素的指針。
2,下標總是與指針的偏移量相同。
3,在函數參數的聲明中,數組名被編譯器當作指向該數組第一個元素的指針。
4,在下列情況中,對數組的引用不能用指向該數組第一個元素的指針來替代:
(1)數組作為sizeof()的操作數。
(2)使用&操作符取數組的地址。
(3)數組是一個字符串(或寬字符串)常量初始值(不懂)
5,因為編譯器需要知道指針進行解引用操作時應該取幾個字節,以及每個下標的步長應該取幾個字節,所以指針總有類型限制。
6,c語言把數組下標改寫成指針偏移量的根本原因是指針和偏移量是底層硬件所使用的基本模型。
第三節
1,之所以要把傳遞給函數的數組參數轉換為指針是出于效率的考慮,這個理由常常也是對違反軟件工程做法的辯解。
2,在函數調用中,數組和函數參數是傳址調用(函數參數都是傳址?有什么表現?)
3,在函數內部對數組參數的任何引用都將產生一個對指針的引用。
4,有一樣操作只能在指針里進行而無法在數組中進行,那就是修改它的值。
第六節
1,盡管術語上稱作“多維數組”,但C語言實際上只支持“數組的數組”。
2,由于“行/列主序”這個術語只適用于恰好是二維的多維數組,所以更確切的術語是“最右的下標先變化”。
3,只有字符串常量才可以初始化指針數組。
第十章
第二節
1,用于實現多維數組的指針數組有多種名字,如“Iliffe向量”,“display”,或“dope向量”。
第三節
1,用字符串指針填充Iliffe向量來創建一個“鋸齒狀數組”。
2,Iliffe向量可能會使字符串分配于內存中不同的頁面中,導致更加頻繁的頁面交換。
3,“數組名被寫成一個指針參數”,規則并是遞歸定義的。數組的數組會被改寫為“數組的指針”,而不是“指針的指針”。
第六節
1,對多維數組作為參數傳遞的支持缺乏是C語言存在的一個內在限制。
2,函數可以返回一個結構體。
3,向printf函數傳遞一個NULL指針會導致程序的崩潰。
第七節
1,全局數組變量,不能使用變量來確定長度。
2,程序信息具有啟發性,而非煽動性,并且要避免使用諸如帶有褻瀆性,口語化,幽默或者夸張的非專業用語。尤其是,如果你規規距距地這樣做,就可以避免在凌晨3點鐘被叫醒。
4,在實踐中,不要把realloc()函數的返回值直接賦給字符指針。如果realloc()函數失敗,它會使該指針的值編程NULL,這樣就無法對現有的表進行訪問。
備注:
這個月里諸多不順,但還是一點一點翻完了這本書。自己做的筆記極少會看,算做個紀念咯。
2012-05-28