一篇特別長的總結(C專家程式設計)

onephone發表於2017-02-17

部落格連結: http://codeshold.me/2017/02/expert_c_programming.html

讀一本書必輸出一篇筆記或者總結!!!
《C專家程式設計》這本書很早看完了,但整理筆記卻斷斷續續的花了三天時間,這從側面更說明了這本書的經典了(儘管不到300頁)!
至此C經典著作《C Traps and Pitfalls》《Expert C Programming》《POINTER ON C》已經算完整的看完了……

典型

  1. typedef struct bar {int bar;} bar;bar的含義
  2. 為什麼extern char *p和另一個檔案中的char p[100]不能匹配?
  3. 什是匯流排錯誤(bus error)?什麼是段違規/段錯誤(segmentation violation)?
  4. char *foo[]char (*foo)[]有何不同?

迷霧

  1. 計算機日期

    • UNIX的系統時間是從1970年1月1日(UTC)起按秒算的
    • timestamp是從1970年開始的
    • 先有UNIX,再有C
    • 關於time_t什麼時候會重新回到開始?即達到盡頭,執行如下程式碼可查詢(2038年)

      #include <stdio.h>
      #include <time.h>
      
      
      int main(){
          // time_t 是 long 的typedef形式(有符號)
          time_t biggest = 0x7FFFFFFF;
          // ctime 把時間轉化為當地時間(含時區)
          printf("biggest = %s \n", ctime(&biggest));
          // gmtime 獲取對應的UTC時間,但返回的不是一個可列印的字串,故使用asctime
          printf("biggest = %s \n", asctime(gmtime(&biggest)));
          return 0;
      }
      
  2. C語言排斥強型別,即其是弱型別

  3. C語言的許多特性是為了方便編譯器設計者而建立的:

    • 陣列下表從0開始
    • 基本資料型別直接和底層硬體相關
    • auto 關鍵字是擺設(它是預設的記憶體分配模式,其只對建立符號表入口的編譯器設計者有用)
    • float被自動擴充套件為double(但在ANSI C中不在這樣)
    • 不允許巢狀,即函式內部包含另外一個函式
  4. C和shell

    • Steve Bourne編寫的UNIX shell時,創立了一個C語言的變型, 其使用了很多“顯示的結束語句”,如if...fi,且shell不用malloc,而使用sbrk自行負責堆儲存管理,提高字串處理效率。
    • Broune 事實上促成了國際C語言混亂程式碼大賽(The International Obfuscated C Code Competition)
  5. 應使用ANSI C 而不是 K&R C

  6. ANSI C 對編譯器的部分要求如下

    • 在函式定義中形參個數的上限至少可以達到31個
    • 在函式呼叫中實參個數的上限至少可以達到31個
  7. ANSI C 與 K&R C 的不同

    1. 新的、非常不同,且重要的(僅一個)
      • ANSI C 把函式原型作為函式宣告的一部分,原型的形式,其兩者也有了很大的變化
    2. 新增的關鍵字
      • ANSI C 增加了 enum, const, volatile, signed, void等關鍵字
      • 棄掉了K&R C中的entry等關鍵字
    3. “安靜的改變”

      • 相鄰字串的面值會被自動連線在一起
      • 尋常算數轉換(usual arithmetic conversation)
      • K&R C 採用的是無符號保留(unsigned preserving)原則,即當一個無符號型別與int或更小的整型混合使用時,結果型別是無符號型別。
      • ANSI C 採用的是值保留(value preserving)原則,即當執行算數運算時,如果型別不同,就會發生轉換。資料型別朝著浮點精度更高、長度更長的方向轉換,整形數如果轉換為signed不會丟失資訊,就轉換為signed,否則轉換為unsigned —— 即包括整型升級和尋常算數轉換

        main(){
            if(-1 < (unsigned char)1)
                printf("-1 is less than (unsigned char)1: ANSI semantics");
            else
                printf("-1 NOT less than (unsigned char)1: K&R C semantics");
        }
        
    4. 除上面之外的其他區別

      • 符號貼上(token-pasting)
      • 三字母詞(trigraph),即用3個字元表示一個單獨的字元,如兩字母詞\t表示“tab”, 三字母詞??<表示“開放的花括號”
  8. 實參、形參的匹配

    • 如下程式碼會報一條warning,“argument #1 is imcompatible with prototype...”,為什麼?

      foo(const char **p) {}
      
      
      main(int argc, char **argv) {
          foo(argv);
      }
      
    • 原因分析(摘自ANSI C標準):

    • 每個實參都應該具有自己的型別,這樣它的值就可以複製給與它所對應的形參型別的物件(該物件的型別不能含有限定符)
    • 要使得上述賦值形式合法,必須滿足下列條件之一:
      1. 兩個運算元都是指向有限定符或無限定符的相容型別指標
      2. 左邊指標所指向的型別必須具有右邊指標所指向型別的全部限定符
    • 基於上述描述故實參char *能和型參const char*匹配
    • const float *型別並不是一個有限定符的指標型別,它的型別是“指向一個具有const限定符的float型別的指標,即const修飾的是指標指向的型別而不是指標本身
    • 基於上條描述故char ** 和 const char ** 都是沒有限定符的指標型別,但它們所指向的型別不一樣,進而不相容, 所以報錯
  9. const通常只用於陣列形式的引數中模擬傳值呼叫!

  10. #pragma用於向編譯器提示一些資訊,諸如希望把某個特定函式擴充套件為內斂函式,或者取消邊界的檢查。

  11. 一個‘L’的 NUL 用於結束一個ACSII 字串;兩個‘LL’的 NULL 用於表示什麼也不指向(空指標)

特性

  1. 一個遵循標準的C編譯器至少允許一條switch語句中有257個case標籤

  2. 使用switch... case...break...時,養成新增/* fall through */的習慣

  3. 字串陣列初始化(列舉宣告、單行多變數宣告等),最後一個尾巴,,ANSI C rationel對其的解釋是:它使得C語言在自動生成時更容易些!

  4. 幾乎沒有人習慣在函式名前新增儲存型別說明符,所以絕大多數函式都是全域性可見

  5. C語言的符號過載

    • static 在函式內部時,表示該變數的值在各個呼叫間一直保持著連續性;……
    • void 位於引數列表中,表示沒有引數;……
    • () 呼叫一個函式;定義帶引數的巨集;包圍sizeof操作符的運算元(如果它是型別名)
    • 當sizeof運算元是個型別時,其必須加上(),若是變數則不必加括號(建議加)
  6. i = 1, 2; 中 i 的結果是?(1)

    • 優先順序
    • 結合性
  7. /可對一些字元轉義,包括newline(即Enter鍵,表示連線)

  8. 不充分的引數解析,shell引數解析

    • 找出目錄中的連結檔案
      • ls -l | grep ->ls -l | grep "->" 均不行
      • ls -AF | grep "@" 或者 file -h | grep link
  9. ratio = *x/*y; 會報錯??

  10. 錯誤檢查程式,lint程式

宣告

  1. 儲存型別說明符(storage-class):extern static register auto typedef

  2. 型別限定符(type-qualifier): const volatile

  3. “在函式呼叫時,引數按照從右到左的次序壓到堆疊裡”這種說法過於簡單,引數在傳遞時首先儘可能地存放到暫存器中(追求速度)

  4. 結構體

    • 一般形式

      struct 結構標籤 (可選) {
          型別1 標誌符1;
          型別2 標誌符2;
          ……
      } 變數定義(可選);
      
    • 結構中允許存在位段、無名欄位以及字對齊所需的填充欄位

      struct pid_tag {
          unsigned int inactive : 1;
          unsigned int : 1; // 1個位的填充
          unsigned int refcount : 6;
          unsigned int : 0; //填充到下一個字邊界
          short pid_id;
          struct pid_tag *link;
      }
      
    • 位段的型別必須是int,unsigned int 或 signed int(或加上限定詞)

  5. 聯合

    • 一般形式

      union 結構標籤 (可選) {
          型別1 標誌符1;
          型別2 標誌符2;
          ……
      } 變數定義(可選);
      
    • 節省儲存空間 && 提取單獨的位元組欄位(聯合不需要額外的賦值和強制型別轉換,同一個資料可解釋為兩個不一樣的東西)

    • 如下value.byte.c0

      union bits32_tag {
          int whole;  /* 一個32位的值 */
          struct {char c0, c1, c2, c3;} byte;  /* 4個8位的位元組 */
      } value;
      
  6. 列舉

    • 把一串名字和一串整型值聯絡在一起
  7. 如果const(或)volatile關鍵字的後面緊跟著型別說明符(如int,long等),它作用於型別說明符。在其他情況下,const和(或)volatil 關鍵字作用於它左邊緊鄰的指標星號

  8. 分析以下宣告:

    • char * const *(*next)();
    • char *(* c[10])(int **p);
  9. typedef關鍵字並不是建立一個變數,而是宣稱“這個名字是指定型別的同義詞”

  10. typedef struct foo{...foo;}的含義

    • C語言中存在多種名字空間:
    • 標籤名(label name)
    • 標籤(tag)
    • 成員名
    • 其他
    • 對於typedef struct baz {int baz;} baz; 即相當於 typedef struct baz {int baz;} baz_type;
    • typedef宣告引入了baz_type作為struct baz {int baz;}的簡寫形式
    • struct baz xxxxx;使用的是結構標籤
    • baz yyyyy; 使用的是結構型別
  11. 編寫C語言宣告解釋程式cdecl

指標和陣列

1. 不同的

  1. 陣列和指標並不相同

    • 對編譯器而言,一個陣列就是一個地址,一個指標就是一個地址的地址。
    • char *p = "abcdefgh"; ...p[3] 先取符號表中p的地址;提取儲存於此處的指標;把偏移量和指標相加,產生一個地址;訪問這個地址,取得內容
    • char a[] = "abcdefgh"; ....a[3] 先取符號表中a的地址;把偏移量和這個地址相加,產生一個地址;訪問這個地址,取得內容
    • 初始化指標時所建立的字串常量被定義為只讀!
  2. extern int *x;extern int x[] 區別?

  3. 宣告和定義

    • 宣告相當於普通的宣告:它所說明的並非本身,而是描述其他地方的建立的物件
    • 定義相當於特殊的宣告:它為物件分配記憶體
    • 定義是宣告的特殊情況,它分配記憶體空間,並可能提供一個初始值
  4. 左值:可修改的左值(允許出現在複製語句左邊)和不可修改的左值

    • 陣列名是左值,但不能作為賦值的物件(左值即為可取地址的值),編譯器為每隔物件分配一個地址(左值) 迴文!

2. 相同的

  1. 表示式中陣列名(與宣告不同)被編譯器當作一個指向該陣列第一個元素的指標

    • “表示式中的陣列”就是指標
    • a[6] = ...; 6[a] = ...; 兩種形式都正確

      fun1(int arr[]) {
          int tmp[] = {1, 2, 3};
      
      
      
      printf("%#x\n", &amp;arr);
      printf("%#x\n", arr);
      printf("%#x\n", &amp;(arr[0]));
      
      
      printf("%#x\n", &amp;tmp);
      printf("%#x\n", tmp);
      printf("%#x\n", &amp;(tmp[0]));
      
      }
  2. 下標總是與指標的偏移量相同

    • C語言把陣列下標作為指標的偏移量
    • 處理以一維陣列時,指標並不見的比陣列快
  3. 在函式引數的宣告中,陣列名被編譯器當作指向該陣列第一個元素的指標

    • “作為函式引數的陣列名”等同於指標
    • 下面程式碼執行正常

      fun2(int arr[]) {
          arr[1] = 3;
          *arr = 3;
          arr = array2;
      }
      

3. 其他的

  1. 在C語言中,所有非陣列形式的資料均以傳值形式呼叫

  2. 指標就是指標,只是可以通過下表的形式對其進行訪問

  3. 用a[i]這樣的形式對陣列進行訪問總是被編譯器“改寫”或解釋為像*(a+1)這樣的指標訪問

  4. 多維陣列初始化時,可省略最左邊下標的長度(也只能是最左邊),如int rhubarb[][3] = { {0, 0, 0}, {1, 1, 1},};

  5. sizeof(陣列名)返回的是陣列總的位元組數

    • 下面程式碼,運算結果如下 sizeof(str):15 func sizeof(str):8

      #include <stdio.h>
      
      
      int func(char str[]) {
          return sizeof(str);
      }
      int main(){
          char str[] = "abcdefghijklmn";
      
      
      
      printf("sizeof(str):%lu\n", sizeof(str));
      printf("func sizeof(str):%d\n", func(str));
      
      }
  6. 指標陣列就是Iliffe向量, char *pea[4]

  7. 指標陣列必須用指向為字串而分配的記憶體的指標進行初始化
  8. “陣列名被改寫成一個指標引數”規則並不是遞迴定義的。陣列的陣列會被改寫成為“陣列的指標”,而不是“指標的指標”

    • 對應列表

      |實參|所匹配的形式引數| |:-----:|-------:| |陣列的陣列char c[8][10];| char (*)[10] 陣列指標| |指標陣列char *c[15]| char **c 指標的指標| |陣列指標(行指標)char (*c)[64]| char (*c)[64] 不改變| |指標的指標 char **c| char **c 不改變|

    • 程式碼

      func1(int fruit[2][3][4]) { ; }
      func2(int fruit[][3][4]) { ; }
      func3(int (*fruit)[3][4]) { ; }
      
  9. 向函式傳遞一個一位陣列:增加一個額外的引數或者賦予陣列最後一個元素一個特殊的值

  10. 向函式傳遞一個普通的多維陣列:必須提供除了最左邊一維以外多有維的長度。即多維陣列最主要的一維長度不必顯式書寫。

連結

  1. strings實用程式可幫助從二進位制檔案內部檢視程式可能產生的錯誤。

  2. cc -S -Xc banana.c, -S選項使編譯器停在彙編階段,-Xc選項告訴編譯器拒絕任何不符合ANSI C的程式碼結構 連結

  3. 連結器(linker)

    • 編譯器中單獨分離出來的程式包括:前處理器(preprocessor)、語法和語義檢查器(syntactic and semantic checker)、程式碼生成器(code generator)、彙編程式(assembler)、優化器(optimizer)、連結器(linker)等。
    • -#選項檢視編譯過程的各個獨立階段
    • 通過給編譯器驅動器一個特殊的-W選項(表示傳遞這個選項到那個階段)向各個階段傳遞選項資訊,如cc -W1, -m mainc > main.linker.map,其中-m選項是傳遞給連結-載入器的,要求其產生聯結器映像
  4. 動態連結的主要目的就是把程式與它們使用的特定函式庫版本中分離出來。這種介於應用程式和函式庫二進位制可執行檔案所提供的服務之間的介面,稱之為二進位制介面(Application Binary Interface, ABI)

    • 動態連結庫,可由ld建立,字尾名約定以.so結尾,表示shared object,簡單的可以通過cc的-G選項來建立
    • 動態連結必須保證4個特定的函式庫:libc(C執行時庫), libsys(其他系統函式), libX(X Windowing), libnsl(網路服務)
  5. 靜態庫稱作為archive,通過ar來建立和更新,字尾名約定以.a結尾

    • 生成示例 cc -o libfruit.so -G tomoto.c
    • 使用示例 cc test.c -L/home/swf -R/home/swf -lfruit, -L, -R 分別告訴連結器在連結和執行時從哪個目錄找需要連結的函式庫
  6. -lthread選項告訴編譯連結到libthread.so,即libname.so對應於-lname

  7. 編譯器希望在確定的目錄下找到庫,連結時一般使用-Lpathname-Rpathname,預設讀取系統變數LD_LIBRARY_PATHLD_RUN_PATH

  8. 檔名通常不與其所對應的函式庫名相似

    |#include檔名|庫路徑名|所用的編譯器選項| |:---------------:|------:|--------------:| |math.h|/usr/lib/libm.so|-lm| |math.h|/usr/lib/libm.a|-dn -lm| |stdio.oh|/usr/lib/libc.so|自動連結| |/usr/openwin/include/X11.h|/usr/openwin/lib/libX11.so|-L/usr/openwin/lib -lX11| |thread.h|/usr/lib/libthread.so|-lthread| |curses.h|/usr/lib/curses.a|-lcurses| |sys/socker.h|/usr/lib/libsocket.so|-lsocket|

  9. nm工具可列出函式庫中包含的函式, nm libc.so | grep xdr_reference

  10. 始終將-l函式庫選項放在編譯命令的最右邊,很多人習慣<命令><選項><檔案>,但連結器採用這個容易引起混淆

  11. Interpositioning就是通過編寫與庫函式同名的函式來取代該庫函式的行為。

  12. 準則:不要讓程式中的任何符號成為全域性的,除非有意將其作為程式的介面之一。很多標頭檔案中的函式有儲存型別符static

  13. 避免使用的識別符號(P104)

  14. ANSI C 標準規定,對於外部識別符號,編譯器可以自行定義,使其不區分字母大小寫。同時,外部識別符號的前六個字元必須與其他識別符號不同。

  15. a.out 是 assemble output (彙編程式輸出)的縮寫

  16. UNIX中可執行檔案是以一種特殊的方式加上標籤的,這樣系統就能夠確認它的屬性。

    • 為重要的數字定義標籤,用獨特的數字唯一地標識資料,是一種普遍採用的程式設計技。
    • 標籤所定義的數字通常被稱為“神奇”數字
  17. ELF (Executable and Linking Format)可執行檔案和連結格式。UNIX中可man a.out 檢視有關UNIX系統所使用的格式的資訊。

執行

1. 概念

  1. 段 segments

    • unix中,段表示一個二進位制檔案相關的內容塊
    • Intel x86的記憶體模型中,段表示一個設計的結果,其中地址空間並非一個整體,而是分成了一些64K大小的區域,稱之為段。
  2. size a.out 可檢視可執行檔案中的三個段(文字段、資料段、bss段)

  3. 檢視可執行檔案的內容,nmdump工具也可以

  4. BSS段這個名字是“Block Started by Symble” 由符號開始的塊的縮寫,其不儲存在目標檔案中(除了記錄BSS段在執行時所需要的大小)。

  5. a.out

    • 資料段儲存在目標檔案中,儲存初始化的全域性和靜態變數以及它們的值
    • BSS段不儲存在目標檔案中(除了記錄BSS段在執行時所需要的大小)
    • 文字段是最容易受優化措施影響的段,儲存可執行檔案的指令
    • a.out檔案的大小受除錯狀態下編譯的影響,但段不受影響
  6. 在作業系統中段就是一片連續的虛擬地址

  7. 函式呼叫:過程活動記錄 (可參考CSAPP)

    • 標頭檔案/usr/include/sys/frame.h描述了過程活動記錄在unix系統中的樣子
  8. 懸掛指標 dangling pointer

  9. 儲存型別關鍵字:

    • static 可保證資料存在資料段中而不是堆疊中
    • auto 通常由編譯器設計者使用,用於標記符號表的條目——它表示“在進入該塊之後,自動分配儲存”(與編譯時靜態分配或在堆上動態分配不同)

      register int filbert;
      auto     int almond;
      static   int hazel;
      
  10. 控制

    • setjmp()longjmp()是通過操作過程活動記錄來完成的,其在C++中變異為更普通的異常處理機制catchthrow (P128)
    • goto語言不能跳出C語言當前的函式
    • longjmp()可以跳回到曾經到過的地方
    • setjmp()/longjmp()最大的用途是錯誤恢復

      #inlcude <setjmp.h>
      jump_buf buf;
      
      
      banna(){
          printf("in banna() \n");
          longjmp(buf, 1);
          /*以下程式碼不會被執行*/
          printf("you'll never see this, because i longjmp'd ");
      }
      
      
      main()
      {
          if(setjmp(buf)) 
              printf("back in main\n");
          else {
              printf("first time through\n");
              banana();
          }
      }
      
  11. 有用的C語言工具

    • 檢查原始碼的工具:indent(C程式美化器,和cb類似), cflow(列印程式中呼叫者/被呼叫著的關係) cscope(一個基於ASCII碼C程式的互動式瀏覽器), ctags(建立一個標籤檔案), lint(C程式檢查器), vgrind(格式器,用於列印漂亮的C列表)
    • 檢查可執行檔案的工具:dis(目的碼反彙編工具), dump -Lv(列印動態連結資訊), ldd(列印檔案所需的動態), nm(列印目標檔案的符號列表), strings(檢視嵌入二進位制檔案中的字串), sum(列印檔案的校驗和與程式塊計數)
    • 幫助除錯工具:truss(列印可執行檔案所進行的系統呼叫), ps, ctrace(修改你的原始檔,檔案執行時按行列印), debugger(互動式偵錯程式), file
    • 效能優化工具:tcov(顯示每條語句執行次數的計數), time(實際時間和CPU時間), prof(每隔程式所消耗時間的百分比), gprof(呼叫圖配置資料)
  12. 標準的程式碼優化技巧:消除迴圈;函式程式碼就地擴充套件;公共子表示式消除、改進暫存器分配、省略執行時對陣列邊界的檢查、迴圈不變數程式碼移動(loop-invariant code motion)、操作符長度削減(指標操作符轉變為乘法操作,把乘法操作轉變為位移操作或假髮操作)

  13. 8086中有程式碼暫存器CS,資料暫存器DS,堆疊暫存器SS

  14. 磁碟製造商都是採用十進位制數而不是二進位制數來表示磁碟的容量

  15. billion和trillion在美語和英語中的意義不一樣,美語中分別是十億和一萬億,英語中是一萬億和100億億

  16. /usr/ucb/pagesize可檢視系統中頁面大小,頁就是作業系統在磁碟和記憶體之間移來移去或進行保護的單位。

  17. 用於管理記憶體的呼叫是:

    • malloc 和 free —— 從堆中獲取記憶體以及把記憶體返回給堆
    • brk 和 sbrk —— 呼叫資料段的大小至一個絕對值

2. 執行錯誤

  1. 堆經常出現的問題:

    • 釋放或改寫仍在使用的記憶體(稱為“記憶體損壞”)
    • 未釋放不再使用的記憶體(稱為“記憶體洩漏”)
  2. 檢測記憶體洩漏:

    • netstat, vmstat檢視
    • swap -s檢視交換空間大小
    • ps -lu 使用者名稱 顯示所有程式大小,其中SZ表示的是程式頁面數
    • 使用第三方的malloc庫
  3. 程式執行時的常見錯誤:

    • bus error (core dumped) 匯流排錯誤(資訊已轉儲)
    • 匯流排錯誤基本是由於未對齊的讀或寫操作引起的。(出現未對齊的記憶體訪問請求時,被堵塞的元件是地址匯流排)
    • 對齊, alignment, 資料項只能儲存在地址是資料項大小的整數倍的記憶體位置上
    • 只要對齊了,就能保證一個原子資料想不會跨越一個頁或Cache塊的邊界

          union { char a[10];
                  int i;
              } u;
          int *p = (int *)&(u.a[1]);
          *p = 17; // p中未對齊的地址會引起一個匯流排錯誤!
      
    • segmentation fault (coure dumped) 段錯誤

    • 段錯誤或段違規(segmentation violation)是由於記憶體管理單元的異常導致,而該異常通常是由於解除引用一個未初始化或非法值的指標引起的
    • 如果未初始化的指標恰好有未對齊的值,它將產生匯流排錯誤而不是段錯誤
    • 導致段錯誤的直接原因
      1. 解除引用一個包含非法值的指標
      2. 解除引用一個空指標
      3. 未得到正確的許可權時進行訪問。如往只讀的文字段儲存值就會引起段錯誤
      4. 用完了堆疊或棧空間
    • 導致段錯誤的常見程式設計錯誤
      1. 壞指標值錯誤(指標賦值前就被引用了;向庫函式傳遞一個壞指標;對指標進行釋放後再訪問其內容),free(p); p = NULL; 這樣,在指標釋放後繼續使用該指標,至少程式能在終止前進行core dump
      2. 改寫錯誤(overwrite) :越過陣列邊界謝姑資料,在動態分配的記憶體兩端寫入資料,改寫一些堆管理資料結構
      3. 指標釋放引起的錯誤:釋放一個記憶體塊兩次,釋放一塊未曾使用malloc分配的記憶體,釋放仍在使用的記憶體; 迴圈釋放連結串列就容易出現這種情況
  4. 預設情況下,會進行資訊轉儲,當然也可以這是特定的訊號處理程式(signal handler)

    • 系統不支援在訊號處理程式內部呼叫庫函式(除非嚴格符合標準所限制的條件)
  5. “core dump” 來源於,以前所有的記憶體是用鐵氧化物圓環(也就是core,指磁芯)製造的。

  6. limit stacksize 10 可在C-shell中調整堆疊的大小

  7. dbx工具可用來檢視段錯誤等資訊

3. 其他

  1. 根據位模式構築圖形 一個優雅的#define定義

    #define X )*2+1
    #define _ )*2
    #define s ((((((((((((((((0 /*用於構建16位寬的圖形*/
    
    
    static unsigned short stopwatch[] = 
    {
        s _ _ _ _ X X X X X X _ _ _ X X _,
        s _ _ _ X X X X X X X X X _ X X X,
        ....
        ....
    }
    
  2. 型別提升:char, bit-filed, enum , unsigned char, short, unsigned short 在表示式中,其會提升為int(前提是int能完整地容納原先的資料);引數也會被提升

  3. 如果使用了原型,預設引數提升就不會發生;如果引數宣告為char,則實際傳入的也是char

    • ANSI C 函式原型的目的就是使C語言稱為一種更加可靠的語言,消除型參和實參之間型別不匹配的問題。(但其並沒有排他的使用K&R C 函式原型)
    • 引數提升是為了簡化編譯器,老式的編譯器僅接受int, double 和指標型別的引數
  4. 不需要按回車就能得到一個字元

    • C編譯器中的getch(), getche()
    • 作業系統傳送“字元已經就緒”的訊號後,需要捕捉的訊號是SIGPOLL
  5. 呼叫庫函式之後檢查errno是個好的習慣

C 語言實現有限狀態機 FSM

祕密題

  1. 不使用臨時變數交換兩個值(兩種方法)

    a ^= b;
    b ^= a;
    a ^= b;
    
  2. 怎樣檢測到連結串列中存在迴圈

  3. C語言中不同增值語句的區別(考慮變數和指標等多種情況)

    x = x + 1;
    ++x;
    x++;
    x += 1;
    
  4. 庫函式呼叫和系統呼叫的區別

    • 典型的C函式庫:system, fprintf, malloc
    • 典型的系統呼叫:fork, chdir, write, brk
    • 函式庫呼叫:在所有ANSI C編譯器版本中,C函式庫是相同的;語言或應用程式的一部分;使用者地址空間執行;呼叫函式庫中的一個程式;執行時間屬於使用者時間;屬於過程呼叫,開銷較小;C函式庫libc中大約300個程式;記錄於unix手冊第三節
    • 系統呼叫:各個作業系統的系統呼叫不相同;作業系統的一部分;核心地址空間執行;呼叫系統核心的服務;執行時間屬於系統時間;需要核心上下文切換,開銷較大;在UNIX中大約有90個系統呼叫
  5. 檔案描述符和檔案指標有何不同

    • 檔案描述符(用於索引開放檔案的每個程式表 per-process table-of-open-files)就是開放檔案的每個程式表的一個偏移量(如3),其用於unix系統呼叫中,用於標識檔案
    • FILE 指標儲存了一個FILE結構的地址。FILE結構用於表示開放的I/O流。它用於ANSI C標準I/O庫中,用於標識檔案 (stdio.h),結構內容依平臺而不一樣,在unix中通常是開放檔案的每個程式表的一個條目!
  6. 編寫程式碼,確定一個變數是有符號數還是無符號數

    • 如下程式碼能適用於K&R C,但由於型別提升無法適用於ANSI C

      #define ISUNSIGNED(a) (a >= 0 && ~a >= 0)
      或
      #define ISUNSIGNED(type) ((type)0 - 1 > 0)
      
  7. 列印一顆二叉樹的時間複雜度是多少?

    • 如果execvc 系統呼叫成功,它將返回什麼?(失敗才會返回)
  8. 從檔案中隨機提取一個字串

    • 對字串進行計數,並記錄每個字串的偏移位置
    • 如果只能順序遍歷檔案一次,且不能實用表格來儲存字串的偏移位置,怎麼計算?(提示:應用概率)
  9. 如何用氣壓計測量建築物的高度

你懂C,所以C++不在話下

相關文章