C陷阱與缺陷--讀書筆記

onephone發表於2017-01-28

原文連結:http://codeshold.me/2017/01/c_trapsandpitfalls.html

FORTAN: formula translator 公式翻譯程式語言
Fibonacci: 斐波那契

詞法陷阱

為什麼n-->的含義是n-- > 0, 而不是n- ->0?
a+++++b的含義是?

  1. 賦值操作符為什麼是=而不是==?

    • 程式中的賦值操作相對於比較操作更多,所以用=來表示
  2. ASCII碼

    • 參考
    • 空格(32/0x20, 對應於^@), 0(48/0x30), A(65/0x41), a(97/0x61)
    • 換行LF(0x0A), 回車CR(0x0D, 對應於^M)
    • vim下的normal模式下ga可檢視ascii碼
  3. 貪心原則?

    • 每個符號(token)應包含儘可能多的字元
    • "如果輸入流截止至某個字元之前都已經被分解為一個個符號,那麼下一個符號將包括從該字元之後可能組成的一個符號的最長字串"
    • y = x/*p, y = x / *p, y = x/(*p)?
    • a---b?
  4. 10是否能表示成010?

    • 一個表示十進位制,一個表示八進位制
  5. 單引號括起來的一個字元表示一個整數,雙引號括起來的一個字元代表一個指標

  6. C語言的定義並不允許巢狀註釋,但C編譯器可以支援

  7. a++的結果不能作為左值(見補充知識點)

  8. C語言允許初始化列表中出現多餘的逗號:能讓自動化的程式設計工具方便地處理很大的初始化列表(每行的格式一樣)

語法陷阱

C語言允許初始化列表中多餘的逗號,例如 int days[] = {1, 2, 3, 4,}; 為什麼有這種特性?

  1. 函式宣告

    • 兩部分組成:型別 + 一組類似表示式的宣告符(declarator), 後者的求值應返回一個給定型別的結果!
    • float f, f; 等同於 float ((f)), (g);
    • float *g(), (*h)() 後者是一個函式指標
    • 型別轉換符:float (*h)()(h是一個指向返回值型別為浮點型別的函式的指標) 宣告對應的型別轉換符為(float (*)())
    • typedef void (*funcptr)(); (*(funcptr)0)(); 等同於 (*(void(*)())0)(); 顯示呼叫開機子例程
    • signal.h 中的typedef void (*HANDLER)(int); HANDLER singal(int, HANDLER)
  2. 運算子優先順序,結合性

    • 任何一個邏輯運算子的優先順序低於任何一個關係運算子(有關係的人排在前面)
    • 移位運算子的優先順序比算術運算子要低,但比關係運算子要高
    • 關係運算子的優先順序並不相同,!===要低於其他的
    • 任何兩個邏輯運算子都有不同的優先順序, 按位運算子比順序運算子要高
    • 結合性為自右向左的有:單目運算;三目運算;assignments
    • 詳細參考,前置和後置++, --的區別見文末補充知識點
  3. switch語句中的case和break(C的特色)

    • 在沒有break的地方加上註釋!
  4. 函式呼叫要包括函式列表

    • 即使函式不帶引數,但呼叫時也得包括函式列表, f()是一個呼叫語句,而f僅計算函式f的地址卻不呼叫該函式
  5. "懸掛"else

    • else 始終與同一對括號內最近的未匹配的if結合(所以if後面加{})
    • 一些程式設計語言在if中使用收尾定界符來顯示地說明,如shell中的if...fi

``` c
if (x == 0)
if (y == 0) error();
else{
z = z + y;
f(&z);
}

```

語義陷阱

寫出memcpy(char *dest, const char *source, int k)函式, 寫一個函式void bufwrite(char *p, int n)來一次轉移一批字元到緩衝區,必要時通過函式flushbuffer()重新整理快取。
程式按一定順序生成一些整數,並將這些整數按列出輸出,程式的輸出可能包括若干頁的整數,每頁包括NCOLS列,每列又包括NROWS個元素,每個元素就是一個待輸出的整數,寫出輸出程式(printnum(), printnl(), printpage()分別是列印對應的內容)

  1. 陣列和指標

    • C語言中只有一維陣列,對於一個陣列只能做兩件事:確定陣列的大小(C99標準允許變長陣列VLA);獲取指向陣列下標為0的元素的指標
  2. 作為引數的陣列宣告

    • C語言中,我們無法將一個陣列作為函式引數直接傳遞
    • C語言會自動地將作為引數的陣列宣告轉換為相應的指標宣告
  3. 避免"提喻法"synecdoche

    • ANSI C 標準中禁止對string literal 做出修改
    • K&R C 中對這一問題的說明是,試圖修改字串常量的行為是為定義的
  4. 空指標不能解除引用

    • 編譯器保證由0轉換而來的指標不等於任何有效的指標,即NULL
    • 當常數0被轉化為指標使用時,其絕不能被解除引用(dereference),即if (p == (char *) 0)是合法的,但if (strcmp(p, (char *)0) == 0)...非法
    • p是空指標,則printf(p)printf("%s", p)的行為均為未定義
  5. 不對稱邊界

    • "off-by-one error" 差一錯誤
    • 左閉右開, for (i = 0; i < 10; i++), 而不寫成for (i = 0; i <= 9 ; i++)
    • 入界點(可用序列中的第一個元素為0),出界點(不可用序列中的第一個元素)為10
    • --n >= 0至少要與等效的n-- >0一樣快或更快,第一個結果先將n減1,再將結果與0比較;第二個表示式則先儲存n,從n中減1,然後比較儲存值與0的大小()
    • 堅持“不對稱原則”
    • 陣列中實際不存在的“溢界”元素的地址位於陣列所佔記憶體之後,這個地址可以用於賦值和比較,但如果引用該元素,則非法
  6. 求值順序

    • 分隔函式引數的逗號並非逗號運算子
    • 涉及求值順序的僅有 && || ?: ,, 其中,特殊,其先丟棄再求值
  7. 整數溢位

    • 無符號運算沒有溢位一說
    • 如果算數運算中一個運算元是有符號整數,另一個無符號整數,則均會轉換為無符號整數
    • 如果兩個都是有符號整數,則溢位結果未定義
    • 正確檢測溢位的方法if ((unsigned)a +(unsigned)b < INT_MAX)if (a < INT_MAX - b), INT_MAX在<limits.h>

連線

  1. 外部物件

    • 外部物件(external object),每個外部代表著機器記憶體中的某個部分,並通過一個外部名稱來識別。
    • extern int a; 顯示的說明a的儲存空間是在程式的其他的地方分配
  2. static 修飾符能夠減少命名衝突

  3. 形參、實參與返回值

    • 任何C函式都含有一個形參列表,該變數在函式呼叫時被初始化
    • 任何C函式都有返回型別,要麼是void,要麼是函式生成結果的型別
    • C語言中的形參和實參匹配的規則有些複雜,ANSI C允許程式在宣告時指定函式的引數型別
    • 如果一個函式沒有float、short、char型別的引數,在其宣告中可以省略引數型別說明
    • 對於類似的宣告double square(),float型別的引數會自動轉化為double型別,short和char型別的引數會自動轉換為int型別
  4. 檢查外部型別

    • char filename[] = "/etc/passwd";, extern char* filename;,前者中filename的型別為“字元陣列”,後者的型別為“字元指標”,這兩個對filename的宣告使用儲存空間的方式不同(圖不一樣)
    • 應修改為char filename[] = "/etc/passwd";, extern char filename[]; 或者 char* filename = "/etc/passwd";(檔案一), extern char* filename;(檔案二)
  5. Endian的意思是“資料在記憶體中的位元組排列順序”, 表示一個字在記憶體中或傳送過程中的位元組順序。(Little endian 將低序位元組儲存在起始地址, Big endian 則相反)

庫函式

當一個程式異常終止時,程式輸出的最後幾行常常會丟失,原因?如何解決這個問題?

  1. 緩衝輸出與記憶體分配

    • setbuf()
  2. 使用errno檢測錯誤

    • 建議使用if(返回的錯誤值){檢查 errno}的方式
    • 並未強制要求庫函式實現errno機制,同時errno的值是前一個執行失敗的庫函式設定的值
  3. signal

    • signal也不總是安全的
    • 讓signal處理函式儘可能的簡單,一般是列印出一條錯誤訊息(設定一個標誌)再返回
  4. getchar(putchar)經常被實現為巨集

前處理器

“表示式” (x) ((x)-1) 能否成為一個合法的C表示式?
使用巨集實現max的一個版本,其中max的引數都是整數,要求在巨集max的定義中這些整型引數只被求值一次

  1. 顯示常量 manifest constant

  2. 巨集定義

    • 巨集這是對程式的文字起作用
    • #define f(x) ((x)-1) 注意空格, #define FOOTYPE struct foo
    • 巨集不是型別定義
    • 巨集不是語句
  3. do {...} while(0)

    • 幫助定義複雜的巨集以避免錯誤,同時使用巨集可新增;,符合程式碼習慣
    • 避免使用goto控制流程(break)
    • 避免由巨集引起的警告, #define EMPTYMICRO do{}while(0)

可移植性缺陷

實現atol

  1. 對應C語言的標準

  2. 識別符號名稱的限制

    • ANSI C 標準所能保證的是, C實現必須能夠區別出前6個字元不同的外部名稱(並未區分大小寫)
  3. 字元是有符號整數還是無符號

    • 錯誤認識:(unsigned)c,其在將字元c轉化為無符號整數時,c將首先被轉化為int型整數,可能會達到非預期的結果
    • 正確的使用: (unsigned char)c
  4. 移位運算

    • 空位用什麼填充?
  5. 大小寫轉化

    • #define toupper(c) ((c)+'A'-'a')#define tolower(c) ((c)+'a'-'A')
  6. 並非所有的C實現在某塊記憶體被釋放後還能較長時間的保留之

  7. "0123456789"[n % 10]

附錄

  1. printf

    • %之後的稱為格式碼(指明瞭格式轉換的型別)
    • 修飾符, %和格式碼之間 %3.1g(寬度修飾符、精度修飾符等)
    • 標誌, %和域寬修飾符之間,如%-14s(左對齊), %+d
    • #對數值的輸出格式進行微調,0%o%#o,針對數值0,其分別列印00和0;%#x%#X列印出的16進位制數前加上0x或0X;#用在浮點數中則其要求小數點必須列印出來(即使小數點後沒有數字),如果用於%g或%G格式項,列印出的數值尾綴的0 將不會被去掉
    • printf允許間接指定域寬,只需用*替換域寬修飾符或精度修飾符或兩者,printf("%*.*s", 12, 5, str);
    • printf("%*%\n", n) 列印出n-1個空白字元,後面再跟一個%符號
    • 新增格式碼:%p 列印出該指標所指向的地址; %n 指出已經列印的字元數,這個數被儲存在對應引數所指向的整數中(一個整型指標),如下int n; printf("hello\n%n", &n)
  2. varargs, stdarg

    • varargs.h 中的 va_alist, va_dcl, va_list, va_start, va_arg, va_end
    • stdarg.h 處理可變引數列表
    • void error(char *, ...);

補充知識點

1. i++ 和 ++i

  1. 區別

    • i++ 返回原來的值;++i 返回加一之後的值
    • i++ 不能當作左值,而i++可以
  2. 左值&右值

    • 左值是對應記憶體中有確定儲存地址的物件的表示式的值,而右值是所有不是左值的表示式的值。
    • 一般的可以和賦值聯絡起來,但左值和右值的根本區別在於是否允許取地址運算子&獲得對應的記憶體地址
  3. 簡單的實現(C++)

``` c++
// 字首形式
int& int::operator++() //返回值是一個引用,就是說函式返回值也可以作為一個左值使用
{//函式本身無參,意味著是在自身空間內增加1的
*this += 1; // 增加
return *this; // 取回值
}

//字尾形式:
const int int::operator++(int) //返回值是一個非左值型的,與字首形式的差別所在
{//函式帶參,說明有另外的空間開闢
int oldValue = *this; // 取回值
++(*this); // 增加
return oldValue; // 返回被取回的值
}
```

相關文章