0.規則
<The Elements of Programming Style>
<The Elements of Style>程序員
1.假想的編譯程序
(1)使用編譯器提供的全部的可選警告設施數據庫
加強類型靜態檢查的能力
eg: void* memchr(const void* str, int ch, int size);
那個調用該函數時,即便互換其字符ch和大小size參數,編譯器也不會發出警告安全
可是若是在函數原型中使用更加精確的類型,就能夠加強原型提供的錯誤檢查能力
void* memchr(const char* str, unsigned char ch, size_t size);函數
注:引入無符號數能夠加強類型檢查能力,可是也致使 無符號數帶來的隱式轉換錯誤(有符號數必須轉爲無符號數)工具
(2)使用lint等靜態檢查工具來檢查編譯器漏掉的錯誤
(3)若是有單元測試,就進行單元測試單元測試
2.本身設計並使用斷言測試
/* memcpy v1: 拷貝不重疊的內存塊 */ void* memcpy(void* to, const void* from, size_t size) { void* pto = (char*)to; const void* pfrom = (const char*)from; if (pto == NULL || pfrom == NULL) { fprintf(stderr, "Bad args in memcpy\n"); abort(); } while (size-- > 0) *pto++ = *pfrom++; return pto; }
/* memcpy v2: 拷貝不重疊的內存塊 */ void* memcpy(void* to, const void* from, size_t size) { void* pto = (char*)to; const void* pfrom = (const char*)from; #ifdef DEBUG if (pto == NULL || pfrom == NULL) { fprintf(stderr, "Bad args in memcpy\n"); abort(); } #endif while (size-- > 0) *pto++ = *pfrom++; return pto; }
既要維護程序的 release 版本,又要維護程序的 debug 版本
利用#ifdef DEBUG 調試宏這種方法的關鍵是保證調試代碼不會在最終產品中出現編碼
/* memcpy v3: 拷貝不重疊的內存塊 */ void* memcpy(void* to, void* from, size_t size) { void* pto = (char*)to; const void* pfrom = (const char*)from; assert(pto != NULL && pfrom != NULL); while (size-- > 0) *pto+= = *pfrom++; return pto; }
儘量多的使用斷言,及早發現錯誤:
必須使用斷言對函數的每一個指針參數進行檢查
必須當即使用斷言對獲取的資源(malloc獲取的指針, fopen獲取的FILE指針, 打開的數據庫鏈接,獲取的文件描述符等等)進行安全檢查spa
/* memcpy v4: 拷貝不重疊的內存塊 */ void* memcpy(void* to, const void* from, size_t size) { void* pto = (char*)to; const void* pfrom = (const char*)from; assert(pto != NULL && pfrom != NULL); assert(pto >= pfrom + size || pfrom >= pto + size); /* 檢查是否重疊 */ while (size-- > 0) *pto++ = *pfrom++; return pto; }
在程序中使用斷言檢查語法中未定義行爲特性的非法使用debug
3.爲子系統設防
內存管理程序,可能犯的錯誤:
a.分配一個內存塊並使用未經初始化的內容
b.釋放一個內存塊但繼續引用其中的內容
c.調用realloc對一個內存塊進行擴展,所以原來的內容發生了存儲位置的變化,但程序引用的還是原來存儲位置的內容
d.分配一個內存塊後當即"失去"了它,由於沒有保存指向所分配內存塊的指針
e.讀寫操做越過了所分配內存塊的邊界
f.沒有對錯誤狀況進行檢查
/* new_memory v1 : 分配一個內存塊 */ int new_memory(void** ptr, size_t size) { unsigned char** p = (unsigned char**)ptr; *p = (unsigned char*)malloc(size); return (*p != NULL); }
而後以下調用:
if (new_memory(&block, 32))
成功,block指向所分配的內存塊
else
不成功,block等於NULL
根據ANSI標準,調用malloc存在兩處未定義行爲,必須加以處理:
a.分配長度爲0時,結果未定義
b.malloc分配成功,返回的內存塊的內容未定義,能夠是0,也能夠是隨意的信息
/* new_memory v2: 分配一個內存塊 */ /* 加上內存塊大小的檢查和內存塊的填充初始化 */ #define INIT_VALUE 0xA3 int new_memory(void** ptr, size_t size) { unsigned char** p = (unsigned char**)ptr; assert(ptr != NULL && size != 0); *p = (unsigned char*)malloc(size); #ifdef DEBUG { if (*p != NULL) memset(*p, INIT_VALUE, size); } #endif return (*p != NULL); }
在程序的調試版本中保存額外的信息,就能夠提供更強的錯誤檢查
只要相應的 release 版本可以知足要求,就能夠在debug版本加入儘量多的調試代碼來檢查錯誤
4.對程序進行逐條跟蹤
5.糖果機界面
/* strdup: 爲一個字符串創建副本 */ char* strdup(const char* str) { char* newstr = (char*)malloc(strlen(str) + 1); assert(newstr); strcpy(newstr, str); retrun newstr; }
不要把錯誤標誌和有效數據混雜在一塊兒返回
int resize_memory(void** ptr, size_t newsize) { unsigned char** p = (unsigned char**)ptr; unsigned char* presize = (unsigned char*)realloc(*p, newsize); if (presize != NULL) *p = presize; return (presize != NULL); }
一個函數只幹一件事,編寫功能單一的函數,而不是多功能集一身的函數
反面教材: void* realloc(void** ptr, size_t size);
該函數改變先前已分配的內存塊大小:
a.若是新請求大小小於原來長度,realloc釋放該塊尾部多餘的內存空間,返回的ptr不變
b.若是新請求大小大於原來長度,擴大後的內存塊有可能被分配到新地址處,該塊的原有內容被拷貝到新的位置,返回的指針指向擴大後的內存首地址,而且新內存塊擴大部分未經初始化
c.若是知足不了擴大內存塊的請求,realloc返回NULL,當縮小內存塊時,老是成功的
d.若是ptr == NULL 則realloc的做用至關於調用 malloc(size);
e.若是ptr != NULL 且 size == 0 則realloc的做用至關於調用 free(ptr);
f.若是ptr == NULL 且 size == 0 則realloc結果未定義
在容許大小爲0的參數時要特別當心,一開始就要爲函數的輸入選擇嚴格的定義,並最大限度地利用斷言
爲了程序的易讀性和擴充性,不要使用布爾類型做爲函數的參數類型
6.風險事業
ANSI並無標準化 char,int,long 這樣的基本數據類型
ANSI沒有標準化 基本數據類型的緣由:C語言產生於70年代,等到標準化時已經有了20多年寫出來的代碼基,定義嚴格的標準將會使大量現存代碼無效
char* strcpy(char* pto, const char* pfrom) { char* ptr = pto; while ((*pto ++ = *pfrom++) != '\0') NULL; return ptr; }
上述代碼在任何編譯系統上均可以正確工做
int strcmp(const char* left, const char* right) { for (NULL; *left = *right; left++, right++) { if (left == '\0') return 0; } return ((*left < *right) ? -1 : 1); }
上述代碼因爲最後一行的比較操做而失去了可移植性。修改strcmp,只需聲明 left 和 right 爲 unsigned char 指針,或者直接在比較中先使用強制轉型
(*(unsigned char*)left < *(unsigned char*)right)
for (unsigned char ch = 0; ch <= UCHAR_MAX; ch++) array[ch] = ch;
若是 ch = UCHAR_MAX 時,執行最後一次循環,循環以後,ch增長爲 UCHAR_MAX + 1, 這將引發ch上溢爲0,所以該循環變成了無限循環
if (n < 0) n = -n;
這段代碼可能會出現bug. 在二進制補碼系統中,數據類型的表達範圍不是對稱的,例如 char [-128, 127) 若是n正好爲最小負數,則 n = -n 則會上溢
7.編碼中的假象
不要引用不屬於你的未知存儲區,"引用"意味着不只讀並且要寫,這樣可能會和別的進程產生難以想象的相互做用
/* unsign_to_str: 將無符號數轉換爲字符串 */ void unsign_to_str(unsigned u, char* str) { char* start = str; while (u > 0) { *str++ = (u % 10) + '\0'; u /= 10; } *str = '\0'; reverse_string(start); }
上述代碼 是反向順序導出數字,確正向順序創建字符串,因此須要 reverse_string 來重排數字順序
void unsign_to_str(unsigned u, char* str) { assert(u < UMAX); /* 將每一位數字從後往前存儲, 字符串足夠大以便能存儲 u 的最大可能值 */ char* ptr = &str[5]; /* 假設 u <= 65536 */ *ptr = '\0'; while (u > 0) { *(--ptr) = (u % 10) + '\0'; u /= 10; } strcpy(str, ptr); }
函數能正確工做是不夠的,還必須可以防範程序員產生明顯的錯誤
儘可能慎用靜態(或全局)存儲區傳遞數據
緊湊的C代碼並不能保證獲得高效的機器代碼,首先應該考慮的是代碼的正確性和可讀性
8.剩下來的就是態度問題