一篇特别长的总结(C专家编程)
博客鏈接: http://codeshold.me/2017/02/expert_c_programming.html
讀一本書必輸出一篇筆記或者總結!!!
《C專家編程》這本書很早看完了,但整理筆記卻斷斷續續的花了三天時間,這從側面更說明了這本書的經典了(盡管不到300頁)!
至此C經典著作《C Traps and Pitfalls》《Expert C Programming》《POINTER ON C》已經算完整的看完了……
典型
迷霧
計算機日期
- 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時間,但返回的不是一個可打印的字符串,故使用asctimeprintf("biggest = %s \n", asctime(gmtime(&biggest)));return 0; }
C語言排斥強類型,即其是弱類型
C語言的許多特性是為了方便編譯器設計者而建立的:
- 數組下表從0開始
- 基本數據類型直接和底層硬件相關
- auto 關鍵字是擺設(它是缺省的內存分配模式,其只對創建符號表入口的編譯器設計者有用)
- float被自動擴展為double(但在ANSI C中不在這樣)
- 不允許嵌套,即函數內部包含另外一個函數
C和shell
- Steve Bourne編寫的UNIX shell時,創立了一個C語言的變型, 其使用了很多“顯示的結束語句”,如if...fi,且shell不用malloc,而使用sbrk自行負責堆存儲管理,提高字符串處理效率。
- Broune 事實上促成了國際C語言混亂代碼大賽(The International Obfuscated C Code Competition)
應使用ANSI C 而不是 K&R C
ANSI C 對編譯器的部分要求如下
- 在函數定義中形參個數的上限至少可以達到31個
- 在函數調用中實參個數的上限至少可以達到31個
ANSI C 與 K&R C 的不同
- 新的、非常不同,且重要的(僅一個)
- ANSI C 把函數原型作為函數聲明的一部分,原型的形式,其兩者也有了很大的變化
- 新增的關鍵字
- ANSI C 增加了 enum, const, volatile, signed, void等關鍵字
- 棄掉了K&R C中的entry等關鍵字
-
“安靜的改變”
- 相鄰字符串的面值會被自動連接在一起
- 尋常算數轉換(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");elseprintf("-1 NOT less than (unsigned char)1: K&R C semantics"); } - 除上面之外的其他區別
- 符號粘貼(token-pasting)
- 三字母詞(trigraph),即用3個字符表示一個單獨的字符,如兩字母詞\t表示“tab”, 三字母詞??<表示“開放的花括號”
實參、形參的匹配
-
如下代碼會報一條warning,“argument #1 is imcompatible with prototype…”,為什么?
foo(const char **p) {}main(int argc, char **argv) {foo(argv); } -
原因分析(摘自ANSI C標準)
- 每個實參都應該具有自己的類型,這樣它的值就可以復制給與它所對應的形參類型的對象(該對象的類型不能含有限定符)
- 要使得上述賦值形式合法,必須滿足下列條件之一:
- 兩個操作數都是指向有限定符或無限定符的相容類型指針
- 左邊指針所指向的類型必須具有右邊指針所指向類型的全部限定符
- 基于上述描述故實參char *能和型參const char*匹配
- const float *類型并不是一個有限定符的指針類型,它的類型是“指向一個具有const限定符的float類型的指針,即const修飾的是指針指向的類型而不是指針本身
- 基于上條描述故char ** 和 const char ** 都是沒有限定符的指針類型,但它們所指向的類型不一樣,進而不相容, 所以報錯
const和*通常只用于數組形式的參數中模擬傳值調用!
#pragma用于向編譯器提示一些信息,諸如希望把某個特定函數擴展為內斂函數,或者取消邊界的檢查。
一個‘L’的 NUL 用于結束一個ACSII 字符串;兩個‘LL’的 NULL 用于表示什么也不指向(空指針)
特性
一個遵循標準的C編譯器至少允許一條switch語句中有257個case標簽
使用switch... case...break...時,養成添加/* fall through */的習慣
字符串數組初始化(枚舉聲明、單行多變量聲明等),最后一個尾巴,,ANSI C rationel對其的解釋是:它使得C語言在自動生成時更容易些!
幾乎沒有人習慣在函數名前添加存儲類型說明符,所以絕大多數函數都是全局可見
C語言的符號重載
- static 在函數內部時,表示該變量的值在各個調用間一直保持著連續性;……
- void 位于參數列表中,表示沒有參數;……
- () 調用一個函數;定義帶參數的宏;包圍sizeof操作符的操作數(如果它是類型名)
- 當sizeof操作數是個類型時,其必須加上(),若是變量則不必加括號(建議加)
i = 1, 2; 中 i 的結果是?(1)
- 優先級
- 結合性
/可對一些字符轉義,包括newline(即回車鍵,表示連接)
不充分的參數解析,shell參數解析
- 找出目錄中的鏈接文件
- ls -l | grep ->,ls -l | grep "->" 均不行
- ls -AF | grep "@" 或者 file -h | grep link
ratio = *x/*y; 會報錯??
錯誤檢查程序,lint程序
聲明
存儲類型說明符(storage-class):extern static register auto typedef
類型限定符(type-qualifier): const volatile
“在函數調用時,參數按照從右到左的次序壓到堆棧里”這種說法過于簡單,參數在傳遞時首先盡可能地存放到寄存器中(追求速度)
結構體
-
一般形式
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(或加上限定詞)
聯合
-
一般形式
union 結構標簽 (可選) {類型1 標志符1;類型2 標志符2;…… } 變量定義(可選); -
節省存儲空間 && 提取單獨的字節字段(聯合不需要額外的賦值和強制類型轉換,同一個數據可解釋為兩個不一樣的東西)
-
如下value.byte.c0
union bits32_tag {int whole; /* 一個32位的值 */struct {char c0, c1, c2, c3;} byte; /* 4個8位的字節 */ } value;
枚舉
- 把一串名字和一串整型值聯系在一起
如果const(或)volatile關鍵字的后面緊跟著類型說明符(如int,long等),它作用于類型說明符。在其他情況下,const和(或)volatil 關鍵字作用于它左邊緊鄰的指針星號
分析以下聲明:
- char * const *(*next)();
- char *(* c[10])(int **p);
typedef關鍵字并不是創建一個變量,而是宣稱“這個名字是指定類型的同義詞”
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; 使用的是結構類型
編寫C語言聲明解釋程序cdecl
指針和數組
1. 不同的
數組和指針并不相同
- 對編譯器而言,一個數組就是一個地址,一個指針就是一個地址的地址。
- char *p = "abcdefgh"; ...p[3] 先取符號表中p的地址;提取存儲于此處的指針;把偏移量和指針相加,產生一個地址;訪問這個地址,取得內容
- char a[] = "abcdefgh"; ....a[3] 先取符號表中a的地址;把偏移量和這個地址相加,產生一個地址;訪問這個地址,取得內容
- 初始化指針時所創建的字符串常量被定義為只讀!
extern int *x;和extern int x[] 區別?
聲明和定義
- 聲明相當于普通的聲明:它所說明的并非本身,而是描述其他地方的創建的對象
- 定義相當于特殊的聲明:它為對象分配內存
- 定義是聲明的特殊情況,它分配內存空間,并可能提供一個初始值
左值:可修改的左值(允許出現在復制語句左邊)和不可修改的左值
- 數組名是左值,但不能作為賦值的對象(左值即為可取地址的值),編譯器為每隔對象分配一個地址(左值)
回文!
2. 相同的
表達式中數組名(與聲明不同)被編譯器當作一個指向該數組第一個元素的指針
- “表達式中的數組”就是指針
-
a[6] = ...; 6[a] = ...; 兩種形式都正確
fun1(int arr[]) {int tmp[] = {1, 2, 3};printf("%#x\n", &arr);printf("%#x\n", arr);printf("%#x\n", &(arr[0]));printf("%#x\n", &tmp);printf("%#x\n", tmp);printf("%#x\n", &(tmp[0])); }
下標總是與指針的偏移量相同
- C語言把數組下標作為指針的偏移量
- 處理以一維數組時,指針并不見的比數組快
在函數參數的聲明中,數組名被編譯器當作指向該數組第一個元素的指針
- “作為函數參數的數組名”等同于指針
-
下面代碼運行正常
fun2(int arr[]) {arr[1] = 3;*arr = 3;arr = array2; }
3. 其他的
在C語言中,所有非數組形式的數據均以傳值形式調用
指針就是指針,只是可以通過下表的形式對其進行訪問
用a[i]這樣的形式對數組進行訪問總是被編譯器“改寫”或解釋為像*(a+1)這樣的指針訪問
多維數組初始化時,可省略最左邊下標的長度(也只能是最左邊),如int rhubarb[][3] = { {0, 0, 0}, {1, 1, 1},};
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)); }
指針數組就是Iliffe向量, char *pea[4]
“數組名被改寫成一個指針參數”規則并不是遞歸定義的。數組的數組會被改寫成為“數組的指針”,而不是“指針的指針”
-
對應列表
實參所匹配的形式參數 數組的數組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]) { ; }
向函數傳遞一個一位數組:增加一個額外的參數或者賦予數組最后一個元素一個特殊的值
向函數傳遞一個普通的多維數組:必須提供除了最左邊一維以外多有維的長度。即多維數組最主要的一維長度不必顯式書寫。
鏈接
strings實用程序可幫助從二進制文件內部查看程序可能產生的錯誤。
cc -S -Xc banana.c, -S選項使編譯器停在匯編階段,-Xc選項告訴編譯器拒絕任何不符合ANSI C的代碼結構
鏈接
鏈接器(linker)
- 編譯器中單獨分離出來的程序包括:預處理器(preprocessor)、語法和語義檢查器(syntactic and semantic checker)、代碼生成器(code generator)、匯編程序(assembler)、優化器(optimizer)、鏈接器(linker)等。
- -#選項查看編譯過程的各個獨立階段
- 通過給編譯器驅動器一個特殊的-W選項(表示傳遞這個選項到那個階段)向各個階段傳遞選項信息,如cc -W1, -m mainc > main.linker.map,其中-m選項是傳遞給鏈接-載入器的,要求其產生連接器映像
動態鏈接的主要目的就是把程序與它們使用的特定函數庫版本中分離出來。這種介于應用程序和函數庫二進制可執行文件所提供的服務之間的接口,稱之為二進制接口(Application Binary Interface, ABI)
- 動態鏈接庫,可由ld創建,后綴名約定以.so結尾,表示shared object,簡單的可以通過cc的-G選項來創建
- 動態鏈接必須保證4個特定的函數庫:libc(C運行時庫), libsys(其他系統函數), libX(X Windowing), libnsl(網絡服務)
靜態庫稱作為archive,通過ar來創建和更新,后綴名約定以.a結尾
- 生成示例
cc -o libfruit.so -G tomoto.c - 使用示例
cc test.c -L/home/swf -R/home/swf -lfruit, -L, -R 分別告訴鏈接器在鏈接和運行時從哪個目錄找需要鏈接的函數庫
-lthread選項告訴編譯鏈接到libthread.so,即libname.so對應于-lname
編譯器希望在確定的目錄下找到庫,鏈接時一般使用-Lpathname,-Rpathname,默認讀取系統變量LD_LIBRARY_PATH和LD_RUN_PATH等
文件名通常不與其所對應的函數庫名相似
| 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 |
nm工具可列出函數庫中包含的函數, nm libc.so | grep xdr_reference
始終將-l函數庫選項放在編譯命令的最右邊,很多人習慣<命令><選項><文件>,但鏈接器采用這個容易引起混淆
Interpositioning就是通過編寫與庫函數同名的函數來取代該庫函數的行為。
準則:不要讓程序中的任何符號成為全局的,除非有意將其作為程序的接口之一。很多頭文件中的函數有存儲類型符static
避免使用的標識符(P104)
ANSI C 標準規定,對于外部標識符,編譯器可以自行定義,使其不區分字母大小寫。同時,外部標識符的前六個字符必須與其他標識符不同。
a.out 是 assemble output (匯編程序輸出)的縮寫
UNIX中可執行文件是以一種特殊的方式加上標簽的,這樣系統就能夠確認它的屬性。
- 為重要的數字定義標簽,用獨特的數字唯一地標識數據,是一種普遍采用的編程技。
- 標簽所定義的數字通常被稱為“神奇”數字
ELF (Executable and Linking Format)可執行文件和鏈接格式。UNIX中可man a.out 查看有關UNIX系統所使用的格式的信息。
運行
1. 概念
段 segments
- unix中,段表示一個二進制文件相關的內容塊
- Intel x86的內存模型中,段表示一個設計的結果,其中地址空間并非一個整體,而是分成了一些64K大小的區域,稱之為段。
size a.out 可查看可執行文件中的三個段(文本段、數據段、bss段)
查看可執行文件的內容,nm和dump工具也可以
BSS段這個名字是“Block Started by Symble” 由符號開始的塊的縮寫,其不保存在目標文件中(除了記錄BSS段在運行時所需要的大小)。
a.out
- 數據段保存在目標文件中,存儲初始化的全局和靜態變量以及它們的值
- BSS段不保存在目標文件中(除了記錄BSS段在運行時所需要的大小)
- 文本段是最容易受優化措施影響的段,存儲可執行文件的指令
- a.out文件的大小受調試狀態下編譯的影響,但段不受影響
在操作系統中段就是一片連續的虛擬地址
函數調用:過程活動記錄 (可參考CSAPP)
- 頭文件/usr/include/sys/frame.h描述了過程活動記錄在unix系統中的樣子
懸掛指針 dangling pointer
存儲類型關鍵字:
- static 可保證數據存在數據段中而不是堆棧中
-
auto 通常由編譯器設計者使用,用于標記符號表的條目——它表示“在進入該塊之后,自動分配存儲”(與編譯時靜態分配或在堆上動態分配不同)
register int filbert; auto int almond; static int hazel;
控制
- setjmp()和longjmp()是通過操作過程活動記錄來完成的,其在C++中變異為更普通的異常處理機制catch和throw (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();} }
有用的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(調用圖配置數據)
標準的代碼優化技巧:消除循環;函數代碼就地擴展;公共子表達式消除、改進寄存器分配、省略運行時對數組邊界的檢查、循環不變量代碼移動(loop-invariant code motion)、操作符長度削減(指針操作符轉變為乘法操作,把乘法操作轉變為位移操作或假發操作)
8086中有代碼寄存器CS,數據寄存器DS,堆棧寄存器SS
磁盤制造商都是采用十進制數而不是二進制數來表示磁盤的容量
billion和trillion在美語和英語中的意義不一樣,美語中分別是十億和一萬億,英語中是一萬億和100億億
/usr/ucb/pagesize可查看系統中頁面大小,頁就是操作系統在磁盤和內存之間移來移去或進行保護的單位。
用于管理內存的調用是:
- malloc 和 free —— 從堆中獲取內存以及把內存返回給堆
- brk 和 sbrk —— 調用數據段的大小至一個絕對值
2. 運行錯誤
堆經常出現的問題:
- 釋放或改寫仍在使用的內存(稱為“內存損壞”)
- 未釋放不再使用的內存(稱為“內存泄漏”)
檢測內存泄漏:
- netstat, vmstat查看
- swap -s查看交換空間大小
- ps -lu 用戶名 顯示所有進程大小,其中SZ表示的是進程頁面數
- 使用第三方的malloc庫
程序運行時的常見錯誤:
- 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)是由于內存管理單元的異常導致,而該異常通常是由于解除引用一個未初始化或非法值的指針引起的
- 如果未初始化的指針恰好有未對齊的值,它將產生總線錯誤而不是段錯誤
- 導致段錯誤的直接原因
- 解除引用一個包含非法值的指針
- 解除引用一個空指針
- 未得到正確的權限時進行訪問。如往只讀的文本段存儲值就會引起段錯誤
- 用完了堆棧或棧空間
缺省情況下,會進行信息轉儲,當然也可以這是特定的信號處理程序(signal handler)
- 系統不支持在信號處理程序內部調用庫函數(除非嚴格符合標準所限制的條件)
“core dump” 來源于,以前所有的內存是用鐵氧化物圓環(也就是core,指磁芯)制造的。
limit stacksize 10 可在C-shell中調整堆棧的大小
dbx工具可用來查看段錯誤等信息
3. 其他
根據位模式構筑圖形
一個優雅的#define定義
類型提升:char, bit-filed, enum , unsigned char, short, unsigned short 在表達式中,其會提升為int(前提是int能完整地容納原先的數據);參數也會被提升
如果使用了原型,缺省參數提升就不會發生;如果參數聲明為char,則實際傳入的也是char
- ANSI C 函數原型的目的就是使C語言稱為一種更加可靠的語言,消除型參和實參之間類型不匹配的問題。(但其并沒有排他的使用K&R C 函數原型)
- 參數提升是為了簡化編譯器,老式的編譯器僅接受int, double 和指針類型的參數
不需要按回車就能得到一個字符
- C編譯器中的getch(), getche()
- 操作系統發送“字符已經就緒”的信號后,需要捕捉的信號是SIGPOLL
調用庫函數之后檢查errno是個好的習慣
C 語言實現有限狀態機 FSM
秘密題
不使用臨時變量交換兩個值(兩種方法)
a ^= b; b ^= a; a ^= b;怎樣檢測到鏈表中存在循環
C語言中不同增值語句的區別(考慮變量和指針等多種情況)
x = x + 1; ++x; x++; x += 1;庫函數調用和系統調用的區別
- 典型的C函數庫:system, fprintf, malloc
- 典型的系統調用:fork, chdir, write, brk
- 函數庫調用:在所有ANSI C編譯器版本中,C函數庫是相同的;語言或應用程序的一部分;用戶地址空間執行;調用函數庫中的一個程序;運行時間屬于用戶時間;屬于過程調用,開銷較小;C函數庫libc中大約300個程序;記錄于unix手冊第三節
- 系統調用:各個操作系統的系統調用不相同;操作系統的一部分;內核地址空間執行;調用系統內核的服務;運行時間屬于系統時間;需要內核上下文切換,開銷較大;在UNIX中大約有90個系統調用
文件描述符和文件指針有何不同
- 文件描述符(用于索引開放文件的每個進程表 per-process table-of-open-files)就是開放文件的每個進程表的一個偏移量(如3),其用于unix系統調用中,用于標識文件
- FILE 指針保存了一個FILE結構的地址。FILE結構用于表示開放的I/O流。它用于ANSI C標準I/O庫中,用于標識文件 (stdio.h),結構內容依平臺而不一樣,在unix中通常是開放文件的每個進程表的一個條目!
編寫代碼,確定一個變量是有符號數還是無符號數
-
如下代碼能適用于K&R C,但由于類型提升無法適用于ANSI C
#define ISUNSIGNED(a) (a >= 0 && ~a >= 0) 或 #define ISUNSIGNED(type) ((type)0 - 1 > 0)
打印一顆二叉樹的時間復雜度是多少?
- 如果execvc 系統調用成功,它將返回什么?(失敗才會返回)
從文件中隨機提取一個字符串
- 對字符串進行計數,并記錄每個字符串的偏移位置
- 如果只能順序遍歷文件一次,且不能實用表格來存儲字符串的偏移位置,怎么計算?(提示:應用概率)
如何用氣壓計測量建筑物的高度
你懂C,所以C++不在話下
總結
以上是生活随笔為你收集整理的一篇特别长的总结(C专家编程)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++中 引用与取地址的区别
- 下一篇: 自己的总结(你必须知道的C 495个问题