C語言的設計哲學:
一切工作程序員自己負責。
語言中的所有特性都不需要隱式的運行時支持。
程序員所做的都是對的。
程序員應該知道自己在干什么,并保證自己的所作所為是正確的。
第1章-- C: 穿越時空的迷霧
小即是美。事物發(fā)展都有個過程,由簡入繁,不能一開始就想得太復雜,Multics, IBM的OS/360都是因此而失敗。
C語言的許多特性是為了方便編譯器設計者而建立的。----唉,怎么這個樣子
C語言的基本數據類型直接與底層硬件相對應。----確實如此
register關鍵字,這個設計可以說是一個失誤,如果讓編譯器在使用各個變量時自動處理寄存器的分配工作,顯然比一經聲明就把這類變量在生命周期內始終保留在寄存器里要好,使用register關鍵字,簡化了編譯器,卻把包袱丟給了程序員。
C編譯器不曾實現(xiàn)的一些功能必須通過其他途徑實現(xiàn)----標準I/O庫和C預處理器。
在宏擴展中,空格會對擴展的結果造成很大的影響。宏后面不可加';',它不是C語句。宏最好只用于命名常量,并為一些適當的結構提供簡捷的記法。宏名應該大寫這樣便很容易與函數調用區(qū)分開來。
const關鍵字原先如果命名為readonly就好多了。
const int *p;是指不能夠通過通過p來改變int的值,即:*p = 30和p[3] = 4都是錯誤,但p是可以改變。
const int *與int *是相容的,都是指向int的指針;const int **與int **不相容,前者是指向const int *的指針,int **是指向int *的指針。
盡量不要在你的代碼中使用無符號類型,以免增加不必要的復雜性。只有在使用位段和二進制掩碼時,才可以使用無符號數。應該在表達式中使用強制類型轉換,使操作數均為有符號數,或者無符號數,這樣就不必由編譯器來選擇結果的類型。有個例子,在ANSI C中,-1 < (unsigned char)1為真,而-1 < (unsigned int)1 為假。
第2章-- 這不是Bug,而是語言特性
進步——是計算機軟件工程和編程語言設計藝術逐步發(fā)展的重要動因。這也是為什么C++語言令人失望的原因:它對C語言中存在的一些最基本問題沒有什么改進,而它對C語言最重要的擴展(類)卻是建立在脆弱的C類型模型上。
按照C語言的理念,程序員應該知道自己在干什么,而且保證自己的所作所為是正確的。
多做之過:fall through作為switch的默認行為是個失誤;相鄰的字符串自動合并成一個字符串;太多的缺省可見性,全局可見,一個大型函數一群“內部”函數不得不在該函數的外部進行定義。沒有人會記得在它們之前加上static限定符,所以他們在缺省情況下是全局可見的。
誤做之過:
C語言中符號重載:static 在函數內部,表示該變量的值在各個調用間一直保持延續(xù)性;在函數這一極,表示該函數只對文本文件可見。extern用于函數定義表示全局可見(屬于冗余),用于變量,表示它在其他地方定義。
運算符優(yōu)先級存在的問題:.優(yōu)先級高于*, p.f表示(p.f);函數()高于*;==和!=高于位運算符(val & mask != 0)表示val & (mask != 0);==和!=高于賦值符,c = getchar() != EOF表示c = (getchar() != EOF);算數運算符高于移位運算符 msb<<4 + lsb表示msb<<(4+lsb);逗號最低。
有些專家建議在C語言中記牢兩個優(yōu)先級就夠了:乘除先于加減,在涉及其他的操作符時一律加括號。
結合性,在幾個操作符具有相同優(yōu)先級時決定先執(zhí)行哪一個。
計算的次序之所以未定義,是想讓編譯器充分利用自身架構的特點,或者充分利用存儲于寄存器的值。
如果對于堆棧的每次訪問之前都要檢查其大小和訪問權限,對于軟件來說代價太大了,根本不可行。
gets(char *s),不檢查緩沖區(qū)的空間,而fgets(char *s, int n, FILE *stream)可以對讀入的字符數設置一個上限n。fgets對緩沖大小進行限制的方式,更為安全。
少錯之過,標準參數的處理以及把lint程序錯誤的從編譯器中分離出來。
Lint Early, Lint Often Lint is your software conscience. It tells you when you are doing bad things. Always use lint. Listen to your conscience.gcc as lint,使用-Wall:enable a bunch of warning。gcc --help=warning查詢。
linux上可以使用splint。
讓充滿Bug的代碼快速通過編譯實在是不劃算。----我習慣于寫過代碼后用眼睛看一遍,確認無誤后再編譯調試,看來以后可以在中間加上一步用lint檢查。
大型緩沖區(qū)如果閑置不用是非常浪費空間的。
如果程序員可以在同一代碼塊中同時進行malloc和free操作,內存管理是最輕松的。
深刻教訓:即使可以保證你的編程語言100%可靠,你仍然可能成為算法中災難的犧牲品。----確實如此,學好算法。
第3章-- 分析C語言的聲明
聲明器(declarator), 就是標識符以及與它組合與它組合在一起的任何指針,函數括號,數組下標等。以下形式: 標識符
或 標識符[下標]
或 標識符(參數)
或 (聲明器)
----注意括號不能亂加,就兩個地方可以加括號
聲明格式:類型說明符 聲明器[,聲明器];
類型說明符: int char void等
存儲類型: extern static register auto
類型限定符: const volatile
理解C語言聲明的優(yōu)先級規(guī)則
A 聲明從它的名字開始讀取,然后按照優(yōu)先級順序依次讀取。
B 優(yōu)先級從高到底依次是:
B.1 聲明中被括號括起來的那部分
B.2 后綴操作符:
括()表示一個函數,
[]表示這是一個數組。
B.3 前綴操作符:
*表示指向...的指針
C 如果const和(或)volatile關鍵字與類型說明符(如int,long等)相鄰,它作用于類型說明符;其他情況下const和(或)volatile關鍵字作用于它左邊緊鄰的指針*號。
用優(yōu)先級規(guī)則分析C語言聲明:
char * const *(*next)();
char *(* c[10])(int **p);
如果需要頻繁地對整個數組進行賦值操作,可以通過把它放入struct中。
在調用函數中,參數傳遞時首先盡可能地存放到寄存器中(追求速度)。
union也可以把同一個數據解釋成兩種不同的東西,不用強制類型轉換。
typedef和宏文本替換之間存在一個關鍵性的區(qū)別:typedef看成是一種徹底的"封裝"類型——在它聲明后不能再往里面增加別的東西。首先,可以用其他類型說明符對宏類型名進行擴展,但對typedef所定義的類型名稱不能這樣做。typedef int banana; unsigned banana i; /*錯誤!非法 */;其次連續(xù)幾個變量聲明。
----由于typedef由編譯器解釋的,而宏是由預處理器解釋的
typedef void (*ptr_to_func)(int);//這樣來定義函數指針的別名。
不要為了方便起見對結構使用typedef,這樣做唯一的好處是能使你不必書寫struct關鍵字,但這個關鍵字可以向你提示一些信息。
應該始終在struct的定義中使用結構標簽,即使它并非必須。這種做法可以使代碼更為清晰。結構標簽的名字可以取一個以"_tag"結尾的名字。
C語言中存在多種名字空間:
-
標簽名(label name) -
標簽(tag): 這個名字空間用于所有的結構、枚舉和聯(lián)合 -
成員名:每個結構或聯(lián)合都有自身的名字空間 -
其他
在同一個名字空間,任何名字必須具有唯一性。
----C中也有名字空間,沒注意啊。
第4章-- 令人震驚的事實:數組和指針并不相同
extern對象聲明告訴編譯器對象的類型和名字,對象的內存分配則在別處進行。
X = Y;
在這個上下文環(huán)境里,符號X的含義是X所代表的地址。這被成為左值。
在這個上下文環(huán)境里,符號Y的含義是Y所代表的地址的內容。這被稱為右值。
左值在編譯時可知,左值表示存儲結果的地方。
右值直到運行時才知。如無特別說明,Y的值是指右值。
數組名是個左值,但不是可修改的左值。
指針是間接尋址,數組名是直接尋址,這就是兩者在訪問數據時的區(qū)別。指針的值是運行時從內存取得的,數名的值是編譯時已經確定的。
專業(yè)的C程序員必須熟練的掌握malloc()函數,并且學會用指針操縱匿名內存。
第5章-- 對鏈接的思考
動態(tài)鏈接優(yōu)點:
-
1.可執(zhí)行文件的體積小,節(jié)省磁盤空間和虛擬內存。 -
2.所有動態(tài)鏈接到某個特定函數庫的可執(zhí)行文件在運行時共享該函數庫在內存中的一個單獨拷貝。
只使用動態(tài)鏈接。
gcc創(chuàng)建動態(tài)鏈接庫和使用
創(chuàng)建:gcc tomato.c -fPIC -shared -o libfruit.so
使用:gcc test.c -Wl,--rpath,. -L. -lfruit
這樣只要a.out和libfruit.so放在同一個目錄就可以了
與位置無關的代碼(position-independent code),對于共享庫顯得格外有用,因為每個使用共享庫的進程一般都會把它映射到不同的虛擬地址(盡管共享同一份物理拷貝),只要修改一下偏移量表就可以了。
grep很有用??!
始終將-l函數庫選項放在編譯命令行的最右邊。
警惕Interpositoning。缺省全局作用域。
準則:不要讓程序中的任何符號成為全局的,除非有意把他們作為程序的接口之一。
ldd程序print shared library dependencies。
第6章-- 運動的詩章:運行時數據結構
編程語言理論的經典對立之一就是代碼和數據的區(qū)別。
代碼和數據的區(qū)別也可以是編譯時和運行時的分界線。編譯器的絕大部分工作都跟翻譯代碼有關;必要的數據存儲管理的絕大部分都在運行時進行。
linux可執(zhí)行文件用文件第一個字節(jié)來標注,7F開頭,緊跟后面的是"ELF",代表Executable and Linking Format.
可執(zhí)行文件由文本段、數據段和bss段組成,運行size a.out可查看各段大小。
bss段保存沒有值的變量,事實上只是,給出了運行時所需要的bss段大小。
運行時數據結構有好幾種:堆棧,過程活動記錄,數據,堆等。
堆棧有3個用處:
堆棧為函數內部聲明的局部變量提供存儲空間。
進行函數調用時,堆棧存儲與此有關的一些維護信息。
堆棧也可以被看作暫時存儲區(qū)。比如計算表達式,存儲中間結果。
alloca()函數分配的內存位于堆棧中,函數結束后自動銷毀。
發(fā)現(xiàn)數據段和文本段的位置,以及位于數據段中的堆,方法是聲明位于這些段的變量,并打印它們的地址。
過程活動記錄:局部變量,參數,指向先前結構的指針,返回地址。
Fedora上測了下,一個只有一個int參數的函數調用,要用32個字節(jié),參數4個,返回地址4,esp和ebp其他不知道。fame.h中是匯編,沒太看懂。
編譯器的設計者會盡可能地把過程活動記錄的內容放到寄存器中,這樣可以提高速度。
static變量保存在數據段,而不是堆棧中。
auto關鍵字幾乎沒什么用處,因為它只能用于函數內部,但是在函數內部聲明的數據缺省就是這種分配。
setjmp和longjmp,在C++中變異為更普通的異常處理機制“catch”和“throw”。
對于如何在進程中支持不同的控制線程,只要簡單地為每個控制線程分配不同的堆棧即可。
有用的C語言工具:
indent 代碼縮進工具
默認GNU風格,使用-kr選項按K&R風格。還有各種各樣選項,可以定制。
語法: indent [選項] [源文件列表]
indent [選項] [源文件] [-o 輸出文件]
ldd 用來查看程式運行所需的共享庫,常用來解決程式因缺少某個庫文件而不能運行的一些問題。
nm 打印目標文件的符號表。
strace 工具trace system calls and signals
用法:strace [選項] command
gdb---哈哈,常用
time顯示程序所使用的實際時間和CPU時間
gprof列出程序的運行時分析圖。
標準的代碼優(yōu)化技巧包括:消除循環(huán),函數代碼就地擴展,公共子表達式消除,改進寄存器分配,省略運行時對數組邊界的檢查,循環(huán)不變量代碼移動,操作符長度削減(把指數操作轉變?yōu)槌朔ú僮?,把乘法操作轉變?yōu)橐莆徊僮骰蚣臃ú僮鳎┑取?/p>
第7章-- 對內存的思考
內存泄漏(leak)檢查工具:
mtrace
valgrind
malloc所分配的內存通常會圓乘為下一個大于申請數的2的整數次方。
總線錯誤,幾乎都是由于未對齊的讀或寫引起的。----目前l(fā)inux好像不出現(xiàn)錯誤
段錯誤是由于MMU(內存管理單元,負責支持虛擬內存的硬件)的異常所致,而該異常通常是由于解除引用(查看指針所指地址的內容)一個未初始化或非法值的指針引起的。
Keep it Simple, Stupid !
條件操作符簡潔,允許我們高高興興的在一行內寫下代碼,而無需不必要的代碼膨脹。
最可能導致段錯誤的常見編程錯誤是:
-
壞指針的錯誤。free(p);后值空 p = NULL;
-
改寫錯誤。如數組越界。
-
指針釋放引起的錯誤。
第8章-- 為什么程序員無法分清萬圣節(jié)和圣誕節(jié)
很無厘頭的開始。
類型提升:在任何表達式中,并不局限于涉及操作符和混合類型的操作數的表達式。
char, 位段, enum, unsigned char, short, unsigned char -> int
float -> double
任何數組 -> 相應類型的指針。----注意
函數的參數也是表達式,所以也會發(fā)生類型提升。不用函數原型,會先提升再自動剪裁。
如果使用了函數原型,缺省參數提升就不會發(fā)生,與實際類型相符合。----但數組到指針的提升仍會發(fā)生
不需要按回車鍵就能得到一個字符,單字符I/O----用于游戲編程,這個我就不看了
有限自動機(FSM)可以用作程序的控制結構。它的基本思路是用一張表保存所有可能的狀態(tài),并列出進入每個狀態(tài)時可能執(zhí)行的所有動作,其中最后一個動作就是計算(通常在當前狀態(tài)和下一次輸入字符的基礎上,另外再經過一次表查詢)下一個應該進入的狀態(tài)。你從一個“初始狀態(tài)”開始。在這一過程中,翻譯表可能告訴你進入了一個錯誤的狀態(tài),表示一個預期之外的或錯誤的輸入。你不停地在各種狀態(tài)間轉換,直到到達結束狀態(tài)。
在C語言中,有好幾種方法可以用來表達FSM,但他們絕大多數都是基于函數指針數組。一個函數指針數組可以像下面這樣聲明:
void (*state)MAX_STATES;
debugging hooks
調試器調試時可以調用函數,比如gdb用call 函數名,對于復雜的數據結構可以編寫一個函數,用于遍歷數據結構并打印出來。----時過境遷,現(xiàn)在強大的GUI調試器,這個已不怎么有用了。
可調式性編碼
可調式性編碼意味著把系統(tǒng)分成幾個部分,先讓程序總體結構運行。只有基本的程序能夠運行之后你才能為那些復雜的細節(jié)完善、性能調優(yōu)和算法優(yōu)化進行編碼。
有時候,花點時間把編程問題分解成幾個部分往往是解決它的最快辦法。----確實得花時間,動腦筋來分解。
作者描寫其同事,寫散列表就是個例子啊。最初,使散列函數返回0,這樣所有元素都存儲于第0個位置后面的鏈表中。----這使得程序很容易調試
復雜類型轉換,先寫一個對象的聲明,然后刪去標識符,最后放在左面,如int (*compar)(int *)。
不加類型說明符,聲明變量默認是int;函數默認返回值是int, 一般放在eax(第一個寄存器)中。int幾乎是C語言所有的默認方式。應該也是C最善于處理的數據類型。
qsort函數原型:void qsort(void *base, size_t count, size_t size, int (*compar)(const void* element1, const void *element2));
compar函數參數可以定義為(const void *)類型,這需要在compar函數內部cast為所處理類型;也可以直接定義為所處理類型的指針,在調用qsort函數時需要將compar函數cast為(int (*)(const void *, constvoid *),一開始我以為這樣不正確(因為qsort函數內部還是會調用compar的,這樣類型就不匹配了啊),其實是正確的,因為這種類型檢查是編譯時做的(gcc 使用-c選項),鏈接時不做類型檢查,只要能找到那個函數名就行,運行時取參數更不管這些東西了,是用ebp+offset直接抓來的。
第9章-- 再論數組
數組的聲明就是數組,指針的聲明就是指針,兩者不能混淆。聲明與定義必須對應。
對于編譯器而言,一個數組就是一個地址,一個指針就是一個地址的地址。----左值
什么時候數組和指針是相同的?
C語言標準對此作了如下說明:
-
規(guī)則1. 表達式中的數組名(與聲明不同)被編譯器當作一個指向該數組第一個元素的指針。 -
規(guī)則2. 下標(subscript)總是與指針的偏移(an offset from a pointer相同 -
規(guī)則3. 在函數參數的聲明中,數組名被編譯器當作指向該數組的第一個元素的指針----這里數組是指一維數組
指針有類型限制,是因為編譯器需要知道對指針進行解除引用時應該取幾個字節(jié),以及每個下標的步長。
sizeof(數組名)結果是數組所占字節(jié)數(真正的數組,不是函數形參),由此可見是可以數組名包含了長度信息,并可以通過sizeof取得,所以C中檢查數組是否越界訪問是能夠做到的,但是很容易用指針避開,就像用指針可以修改const一樣。我覺得編譯器可以打開一個選項,是否檢查數組越界訪問。
把作為形參的數組和指針等同起來是出于效率原因的考慮。在C語言中,所有非數組形式數據實參均以傳值形式。如果要copy整個數組,無論在時間上還是內存空間上的開銷都可能是非常大的。
int apricot[2][3][5]; // apricot 兩個[3][5]的數組,2*3個[5]的數組,2*3*5個int
int (*p)[3][5] = apricot; // 步長 3 * 5
int (*r)[5] = apricot[0]; // 步長 5
int *t = apricot[0][0]; // 步長 1
int u = apricot[0][0][0];
指向數組第一個元素的指針與數組名等同。
內存中數組的布局
C語言中,最右邊的下標最先變化,這個約定被稱為"行主序"。
只有字符串常量才可以初始化指針數組,因為可執(zhí)行文件中字符串常量是作為數據存儲。而161這樣的字面常量只出現(xiàn)在代碼中。
數組與指針可交換性的總結:
-
用a[i]這樣的形式對數組進行訪問總是被編譯器”改寫“或解釋為像*(a+i)這樣的指針訪問。
-
指針始終就是指針。它絕不可以改寫成數組。只是可以使用下標形式訪問指針。
-
在特定上下文中,也就是指針作為函數的參數(也就只有這種情況--注意),一個數組的聲明可以看作是一個指針。作為函數參數的數組始終會被編譯器修改成為指向數組中第一個元素的指針。
第10章-- 再論指針
數組和指針參數是如何被編譯器修改的?
“數組名被改寫成一個指針參數”規(guī)則并不是遞歸定義的。數組的數組會被改寫成“數組的指針”,而不是“指針的指針”。
數組的數組 char c[8][10]; char (*c)[10]; 數組的指針
指針數組 char *c[15]; char**c; 指針的指針
指針的指針 char **c; char **c; 不改變----指針與指針不用修改
數組的指針 char (*c)[64]; char (*c)[64]; 不改變----注意,指向一個長度為64的char數組的數組名的指針,訪問數組中元素這樣做:(*c)[0]。
int a[20];
int **p = &a; // 錯誤,指針的指針與數組的指針不兼容
int (*t)[20] = &a; // 正確,t為由20個int的數組的指針。
----此處括號是必須的,因為[]的優(yōu)先級比*高
Iliffe向量,創(chuàng)建一個一維數組,數組中的元素是指向其他東西的指針。
例如main(int argc, char *argv[]),第二個參數會被改寫成char **。(注意,只有把二維數組改為一個指向向量的指針數組的前提下才可以這么做!)
在C語言中,傳遞多維數組必須提供除最左面一維以外的所有維的長度。
可以放棄多維數組的形式,提供自己的下標方式,如char_array[row_size*i + j] = ...
模擬動態(tài)數組,當表滿后,用realloc()對數組重新分配內存,并確保realloc操作成功。
重分配操作很可能把原先的整個內存塊移到一個不同的位置,這樣表格中元素的地址便不再有效。為了避免麻煩,應該使用下標而不是元素的地址。----這也是STL中引入迭代器的一個原因吧
“增加”和“刪除”操作都必須通過函數來進行,這樣才能維持表的完整性。
第11章-- 你懂得C,所以C++不在話下
類內部定義的函數是inline函數
重載是編譯時解析的。
多態(tài)——運行時綁定。latebinding
new和delete操作符,用于取代malloc()和free()函數,能夠自動完成sizeof的計算工作,并會自動調用合適的構造函數和析構函數。new能真正的創(chuàng)建一個對象,malloc()函數只是分配內存。
C++的設計受限于嚴格的兼容性、內部一致性和高效率。
復用是軟件科學的一個崇高而又朦朧的目標。----很多時候不如另起爐灶從頭開始
管理和市場狀況是導致許多公司破產的原因,比單純的技術失敗更為常見。那些不時刻注意顧客需求的公司終究難以為繼,最能掌握這項藝術的公司往往能獲得成功。
附錄A-- 程序員工作面試的秘密
面試的關鍵在于正確理解問題!你需要仔細地聽,如果不理解問題或者覺得它的定義不清,可以要求一個更好的解釋。
提供一種尋找可靠答案的好方法。
鏈表環(huán)的檢測。
mango[i++] += y; // i++僅執(zhí)行一次
優(yōu)秀的程序員將會休息的更好,精力更加充沛,而蹩腳的程序員則很可能困得腦袋常常和桌子打架。
人類的最高目標是奮斗、尋求、創(chuàng)造。
往期精彩
學習嵌入式可以帶娃,不信你們看
第10期 | ringbuff,通用FIFO環(huán)形緩沖區(qū)實現(xiàn)庫
代碼寫得很牛逼但UI界面卻搞得很丑?來,楊工帶你!
分享一個近期開源火爆全網的額溫槍方案(硬件+源碼)
火爆全網開源額溫槍同平臺之華大HC32L136 SDK開發(fā)入門
覺得本次分享的文章對您有幫助,隨手點[在看]并轉發(fā)分享,也是對我的支持。
免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!





