2016年3月29日 星期二

Deep C 心得 -- Part 1 (C)

為了增進對C語言的認識,我這幾天看了些成大資工"2016年系統軟體課程"分享的資料
以下是我看Olve Maudal 的投影片:Deep C 的心得
(為了了解內容我有參考wiki跟jserv大的C語言講座)
本文只有提到C的部份,C++的部份會另外寫。
投影片連結:
http://slideshare.net/olvemaudal/deep-c

1. 指出以下程式碼的結果與缺失

  • 需 #include <stdio.h> 以明確宣告printf函式
    • 若使用C++編譯器編譯會失敗,因為C++編譯器需要明確(explicit)宣告使用函式。
    • 一般的C編譯器在函式未宣告的情況下會自動新增implicit宣告函式。
      • 當連結(link)到標準函式庫,編譯器會找到prinf這個函式
  • main為int型態卻沒有回傳值(return 0)
    • C99或更新的版本,main的回傳值為執行的成功(0)與否
    • 但是ANSI C或K&R C,在這種情況下回傳值為未定義(undefined)
      • 有可能是3(因為printf的回傳值為輸出的char數)
  • 標準C應該要用main(void) (void表示不需要額外參數)
  • 標準C表示程式碼必須以一個空行做結。
 2. C語言的變數宣告
  • static變數和global(全域)變數(被宣告在main外面)被宣告時自動被預設為0,非static變數(在{}之內時被自動設為auto)宣告時預設值為undefined,這是因為
    • "預設變數為0"這個動作是要額外消耗CPU的,而C語言是非常在乎程式效能的,所以在不需要的情況下不會刻意去做這種消耗CPU的事。
    • static跟global變數是被存在Data(有初始值)或BSS(無初始值)內,這兩個區塊中的變數都是在編譯期就會固定住變數的位址,所以被預設為0不會影響到執行的效率。此兩種變數的生命期跟程式的執行期一樣長
    • 而auto變數是被儲存在stack裏面,其位址並沒有被固定住。此種變數的生命期只限於宣告位置前後的{}。
  • static變數的影響力只限宣告該變數的檔案,global變數的影響力括及其他的obj檔案。
  • 但是在C++的情況下,static變數是被預設成"某個預設值"(naive的情況就是0)
  • 使用malloc/calloc/realloc宣告的某pointer空間,其生命期截止於free該pointer的時候
3. 預測以下程式碼的結果

  • a預設值為undefined
  • 但在除錯模式的情況下,執行期有可能代為執行memset使得預設值為0
  • 依本程式來看,a宣告的時候有可能都出現在stack內同樣的address中,所以本程式的執行結果可能會顯示出3個連續的數列(ex: 1,2,3 or 0,1,2 or 22,23,24),不過這視編譯最佳化的結果而定。
4.  最佳化參數的影響
預測以下程式碼的結果並解釋原因

  • 沒最佳化的情況會是42
  • 有最佳化的可能情況:
    • 省去bar(),反正沒影響
    • foo會變成main內的inline函式(反正沒其他函式會呼叫,這樣節省函式調用時間)
    • 所以結果可能會變成某個奇怪的數字(undefined)
  • 平常儘可能用最佳化參數,以發現潛在的問題。
5. 關於sequence point(順序點):
順序點是副作用發生與否的分界點,在順序點左邊的表示已經發生,右邊的還未發生。在兩個順序點之間變量只能改變一次,不然結果會是undefined。ex: (i=i++; 在分號前i的值改變兩次,結果會是undefined)。C/C++裡的順序點其實不多,大致如下:
  • &&, ||, 或?(三元運算符)
    ex: (*a++ != 0 && *b++ != 0) -> 會先算出*a++ != 0,再算出*b++ != 0,然後比較&&
  • 完整表達式(用;當然這有點廢話)
  • 以逗號分開的變數初始化
    ex: int a[3] = {b++,c--,d(101)};  int x=a++, y=a++; 一定是從左到右(好像是從C++11開始?)
  • 函式的進入點
    ex: f(i++) + g(j++) + h(k++), 進入函式f/g/h的是增加過後的i/j/k,不過f/g/h哪個函式先開始呼叫並沒有指定(unspecified behavior)。
  • 涉及到I/O
    ex: printf("Bug %n %d", &a, 11);  在printf %d前會先執行%n(類似fprintf出多少char)的動作(這裡有爭議)
6. 關於sizeof
  • 此為size_t變數,在32bit的機器上通常為unsigned int,而在64bit的機器上通常為unsigned long
  • 所以printf的時候有時會出問題,不能都用%u或%lu。C99以後可以用%zu代表size_t的變數
7. 關於word alignment(對齊):變數大小對齊有助於改善效能
預測以下程式碼的結果並說明原因(假設為64bit機器):

  • 若有類似gcc中-fpack-struct的參數去收緊struct大小,結果會是4,1,9(不做alignment)
  • 有word alignment的情況下會是4,1,12
  • 若是在struct X中多加一個char d;
    • 如果char d;在int c;後面,可能會變成4,1,16
    • 如果char d;在char b;後面,可能會維持4,1,12
    • 結論是看最佳化的演算法而定
  •  若是在struct X中多加一個char* d;
    • 以alignment的結果而定,有可能會是4,1,20或4,1,24
  • C/C++編譯器並不會以紀錄struct內變數成員的方法去做最佳化
 8. C語言的精神:
  • 相信coder的能力
  • 語言本身儘可能設計得簡單扼要
  • 只允許單一方法完成一個動作
  • 執行起來愈快愈好,就算要讓程式本身大一點
  • 維護起來簡單
  • 不會阻止coder去做必須要做的事(因為相信coder)


Google Code Prettify