去掉視圖中的顯示段落標記可讓文檔更乾淨些html
軟件開發的困難在哪裏?對於這個問題,不一樣的人有不一樣的答案,同一我的在不一樣職業階段linux
也會有不一樣的答案。做爲一個系統程序員來講,我認爲軟件開發有兩大難點:程序員
一是控制軟件的複雜度。軟件的複雜度愈來愈高,而人類的智力基本保持不變,如何以有限 的智力去控制無限膨脹的複雜度?我經歷過幾個大型項目,也分析 過很多現有的開源軟件, 我得出一個結論:沒有單個難題和技術細節是咱們沒法搞定的,而全部這些問題出如今一個 項目中時,其呈指數增加的複雜度每每讓咱們束 手無策。算法
二是隔離變化。用戶需求在變化,應用環境在變化,新技術不斷涌現,全部這些都要求軟件 開發可以射中移動的目標。即便是開發基礎平臺軟件,在超過幾年 時間的開發週期以後, 需求的變化也是至關驚人的。需求變化並不可怕,關鍵在於變化對系統的影響,若是牽一髮 而動全身,一點小小的變化可能對系統形成致命的 影響。編程
培訓能夠製造合格的程序員,卻沒法造就一流的高手。培訓是一個被 動的過程,咱們要變被動爲主動。小程序
make的改進版 automake,如今你能 寫出下面這種簡單的 Makefile 就好了:bash
all:數據結構
gcc -g test.c -o test多線程
clean:併發
rm -f test
在這裏,你能夠把 all 看做一個函數名,gcc -g test.c -o test 是函數體(前面加 tab),它的功能 是編譯 test.c 成 test,在命令行運行 make all 就至關於調用這個函數。clean 是另一個函數, 它的功能是刪除test。
用 C 語言編寫一個雙向鏈表。
專業程 序員與業餘程序員之分主要在於一種態度,若是缺少這種態度,擁有十年工做經驗也仍是業餘的。專業的程序員是很 注重本身的形象的,當 然程序員的形象不是表如今衣着和言談上,而是表如今代碼風格上,代碼就是程序員的社交 工具,代碼風格但是攸關形象的大事。
有人說過,傻瓜均可以寫出機器能讀懂的代碼,但只有專業程序員才能寫出人能讀懂的代碼。 做爲專業程序員,每當寫下一行代碼時,要記得程序首先是給人 讀的,其次纔是給機器讀 的。你要從一個業餘程序員轉向專業程序員,就要先從代碼風格開始,並今後養成一種嚴謹 的工做態度,生活上的不拘小節可不能帶到編程 中來。
專業程序員要有精益求精的精神。至於要精到什麼程度,與 具體需求有關,若是隻 是寫個小程序驗證一下某個想法,那完成須要的功能就好了,若是是開發一個基礎程序庫, 那就要考慮更多了。侯捷先生說過,學從難處學, 用從易處用。這裏咱們是學習,就要精 得不能再精爲止,精到鑽牛角尖爲止。
請讀者思考下面幾個問題:
1. 什麼是封裝?
2. 爲何要封裝?
3. 如何實現封裝?
1.什麼封裝?
人有隱私,程序也有隱私。有隱私不是什麼壞事,沒有隱私人就不是人了,程序也不成其爲 程序了。問題是隱私不該該讓別人知道,不然傷害的不只僅是自 己,相關人物也會跟着倒 黴,「豔照門」就是個典型的例子。程序隱私的暴露,形成的傷害不必定有「豔照門」大, 也不必定比它小,反正不要小看它就好了。封裝 就是要保護好程序的隱私,不應讓調用者 知道的事,就堅定不要暴露出來。
2.爲何要封裝? 整體來講,封裝主要有如下兩大好處(具體影響後面再說):
隔 離 變 化 。 程序的隱私一般是程序最容易變化的部分,好比內部數據結構,內部使用的函 數和全局變量等等,把這些代碼封裝起來,它們的變化不會影響系統的其它部分。
降 低 復 雜 度 。 接口最小化是軟件設計的基本原則之一,最小化接口容易被理解和使用。封 裝內部實現細節,只暴露最小的接口,會讓系統變得簡單明瞭,在必定程度上下降了系統的
複雜度。 3.如何封裝? 隱藏數據結構
暴露內部數據結構,會使頭文件看起來雜亂無章,讓調用者發矇。其次是若是調用者圖方便, 直接訪問這些數據結構的成員,會形成模塊之間緊密耦合,給之後的修改帶來困難。隱藏數據結構的方法很簡單,若是是內部數據結構,外面徹底不會引用,則直接放在C 文件中就 好了,千萬不要放在頭文件裏。若是該數據結構 在內外都要使用,則能夠對外暴露結構的 名字。
隱藏內部函數
內部函數一般實現一些特定的算法(若是具備通用性,應該放到一個公共函數庫裏),對調用 者沒有多大用處,但它的暴露會干擾調用者的思路,讓系統看起 來比實際的複雜。函數名 也會污染全局名字空間,形成重名問題。它還會誘導調用者繞過正規接口走捷徑,形成沒必要 要的耦合。
隱藏內部函數的作法很簡單:
在頭文件中,只放最小接口函數的聲明。 在 C 文件上,全部內部函數都加上 static 關鍵字。
禁止全局變量
除了爲使用單件模式(只容許一個實例存在)的狀況外,任什麼時候候都要禁止使用全局變量。這 一點我反覆的強調,但發現初學者仍是屢禁不止,爲了貪圖方便而使用全局變量。請讀者從 如今開始就記住這一準則。
全局變量始終都會佔用內存空間,共享庫的全局變量是按頁分配的,那怕只有一個字節的全 局變量也佔用一個page,因此這會形成沒必要要空間浪費。全局 變量也會給程序併發形成困 難,想把程序從單線程改成多線程將會遇到麻煩。重要的是,若是調用者直接訪問這些全局 變量,會形成調用者和實現者之間的耦合。
關於對象:對象就是某一具體的事物,好比一個蘋果, 一臺電腦都是一個對象。每一個對象都 是惟一的實例,兩個蘋果,不管它們的外觀有多麼相像,內部成分有多麼類似,兩個蘋果畢
竟是兩個蘋果,它們是兩個不一樣的對 象。對象能夠是一個實物,也能夠是一個概念,好比 一個蘋果對象是實物,而一項政策就是一個概念。在軟件中,對象是一個運行時概念,它只 存在於運行環境中, 好比:代碼中並不存在窗口對象這樣的東西,要建立一個窗口對象一 定要運行起來才行。
關 於 類 : 對象多是一個無窮的集合,用枚舉的方式來表示對象集合不太現實。抽象出對 象的特徵和功能,按此標準將對象進行分類,這就引入類的概念。類就是一類事物的統稱, 類實際上就是一個分類的標準,符合這個分類標準的對象都屬於這個類。固然,爲了方便起見,一般只 須要抽取那些對當前應用來講是有用的特徵和功能。在軟件中,類是一個設計時概念,它只存在於代碼中,運行時並不存在某個類和某個類之間的交互。咱們說,編寫一個雙向鏈表,實際上指的是雙向鏈表這個類。
需求簡述
Write Once, Debug Everywhere。聽說這是流傳於 JAVA 程序員中間的一句笑話,Sun 公司用 來形容 JAVA 的跨平臺性的原話是 Write once, run anywhere(WORA) 。後者是理想的,前者 纔是現實。若是咱們的雙向鏈表能夠處處運行,那就太好了。Write once, run anywhere(WORA)是咱們的目標。
列問題:
1.專用雙向鏈表和通用雙向鏈表各自的特色與適用範圍。
2.如何編寫一個通用的雙向鏈表?
typedef int Type;
typedef struct _DListNode
{
struct _DListNode* prev;
struct _DListNode* next;
Type data;
}DListNode;
這樣的鏈表算不上是通用的,由於你存放整數時編譯一次,存放字符串時,重義 Type 再編 譯一次,存放其它類型一樣要重複這個過程。麻煩不說,關鍵是 沒有辦法同時使用多個數 據類型。
爲了讓 C 語言實現的函數在 C++中能夠調用,須要在頭 文件中加點東西才行:
#ifdef __cplusplus
extern "C" {
#endif
...
#ifdef __cplusplus
}
#endif
c語言如何打印出當前源文件的文件名以及源文件的當前行號?
打印文件,函數,行號
printf("file=%s,func=%s,line=%d\n",__FILE__,__FUNCTION__,__LINE__);
在專用雙向鏈表中,dlist_printf 的實現很是簡單,若是裏面存放的是整數,用」%d」打印, 存放的字符串,用」%s」打印。如今的麻煩在於雙向鏈表是通用的,咱們沒法預知其中存在 的數據類型,也就是咱們要面對數據類型的變化。怎麼辦呢?
dlist_print 的大致框架爲:
DListNode* iter = thiz->first;
while(iter != NULL)
{
print(iter->data);
iter = iter->next;
}
在上面代碼中,咱們主要是不知道如何實現 print(iter->data);這行代碼。但是誰知道呢?很明 顯,調用者知道,由於調用者知道 裏面存放的數據類型。OK,那讓調用者來作好了,調用 者調用dlist_print時提供一個函數給dlist_print調用,這種回調調用者提供的函 數的方法, 咱們能夠稱它爲回調函數法。
調用者如何提供函數給 dlist_print 呢?固然是經過函數指針了。變量指針指向的是一塊數據, 指針指向不一樣的變量,則取到的是不一樣的數據。函 數指針指向的是一段代碼(即函數), 指針指向不一樣的函數,則具備不一樣的行爲。函數指針是實現多態的手段,多態就是隔離變化的祕訣.
回到正題上,咱們看如何實現 dlist_print: 定義函數指針類型:
typedef DListRet (*DListDataPrintFunc)(void* data);
聲明 dlist_print 函數: DListRet dlist_print(DList* thiz, DListDataPrintFunc print);
實現 dlist_print 函數:
DListRet dlist_print(DList* thiz, DListDataPrintFunc print) {
DListRet ret = DLIST_RET_OK;
DListNode* iter = thiz->first;
while(iter != NULL)
{
print(iter->data);
iter = iter->next;
}
return ret;
}
調用方法
static DListRet print_int(void* data)
{
printf("%d ", (int)data);
return DLIST_RET_OK;
}
...
dlist_print(dlist, print_int);
需求簡述
這裏咱們請讀者實現下列功能:
對一個存放整數的雙向鏈表,找出鏈表中的最大值。
對一個存放整數的雙向鏈表,累加鏈表中全部整數。
int main(int argc, char* argv[])
{
int i = 0;
int n = 100;
long long sum = 0;
MaxCtx max_ctx = {.is_first = 1, 0};
DList* dlist = dlist_create();
for(i = 0; i < n; i++)
{
assert(dlist_append(dlist, (void*)i) == DLIST_RET_OK);
}
dlist_foreach(dlist, print_int, NULL);
dlist_foreach(dlist, max_cb, &max_ctx);
dlist_foreach(dlist, sum_cb, &sum);
printf("\nsum=%lld max=%d\n", sum, max_ctx.max);
dlist_destroy(dlist);
return 0;
}
static DListRet sum_cb(void* ctx, void* data)
{
long long* result = ctx;
*result += (int)data;
return DLIST_RET_OK;
}
typedef struct _MaxCtx
{
int is_first;
int max;
}MaxCtx;
static DListRet max_cb(void* ctx, void* data)
{
MaxCtx* max_ctx = ctx;
if(max_ctx->is_first)
{
max_ctx->is_first = 0;
max_ctx->max = (int)data;
}
else if(max_ctx->max < (int)data)
{
max_ctx->max = (int)data;
}
return DLIST_RET_OK;
}
static DListRet print_int(void* ctx, void* data)
{
printf("%d ", (int)data);
return DLIST_RET_OK;
}
DListRet dlist_foreach(DList* thiz, DListDataVisitFunc visit, void* ctx)
{
DListRet ret = DLIST_RET_OK;
DListNode* iter = thiz->first;
while(iter != NULL && ret != DLIST_RET_STOP)
{
ret = visit(ctx, iter->data);
iter = iter->next;
}
return ret;
}
這兩個函數沒有什麼實用價值,可是經過它們咱們能夠學習幾點:
1.不要編寫重複的代碼
按傳統的方法寫出 dlist_find_max 以後,每一個人都知道這個函數與 dlist_print 很相似,在寫出 dlist_sum以後,那種感 覺就更明顯了。在這個時候,不該該停下來,而是要想辦法把這些 重複的代碼抽出來。即便由於經驗所限,也要極力去想思考和查資料。
寫重複的代碼很簡單,甚至憑本能均可以寫出來。但要想成爲優秀的程序員,你必定要克服
本身的惰情,由於重複的代碼形成不少問題:
重複的代碼更容易出錯。在寫相似代碼的時候,幾乎全部人(包括我)都會選擇 Copy&Paste 的 方法,這種方法很容易犯一些細節上的錯誤,若是某個地方修改不完整,那就留下了」不定 時」的炸彈,說不定何時會暴露出來。
重複的代碼經不起變化。不管是修改 BUG,仍是增長新特性,每每你要修改不少地方,如 果忘掉其中之一,你一樣得爲此付出代價。請記住古惑仔的話,出來混早晚是要還的。大師 們說過,在軟件中欠下的 BUG,你會爲此還得更多。
去除重複代碼每每不是件簡單的事情,須要更多思考和更多精力,不過事實證實這是最值得
的投資。
2.任何回調函數都要有上下文
大部分初學者都選擇了回調函數法,不過都無一例外的選擇了用全局變量來保存中間數據,
這裏我不想再強調全局變量的壞處了,記性很差的讀者能夠看看前面的內容。咱們要說的是,
在這種狀況下,如何避免使用全局變量。
很簡單,給回調函數傳遞額外的參數就好了。這個參數咱們稱爲回調函數的上下文,變量名 用 ctx(context 的縮寫)。
下面咱們看看怎麼實現這個 dlist_foreach:
DListRet dlist_foreach(DList* thiz, DListVisitFunc visit, void* ctx)
{
DListRet ret = DLIST_RET_OK; DListNode* iter = thiz->first; while(iter != NULL && ret != DLIST_RET_STOP) {
ret = visit(ctx, iter->data);
iter = iter->next;
}
return ret;
}
3.只作分內的事
我見到很多不辭辛苦的程序員,別人讓他作什麼他就作什麼,無論是否是分內的事,無論是 上司要求的仍是同事要求的,都來者不拒。別人說須要一個 XXX 功能的函數,他就寫一個
函數在他的模塊裏,日積月累後,他的模塊變得亂七八糟的,成了大雜燴。我親眼見過在系 統設置和桌面兩個模塊裏,提供不少絕不相干的 函數,這些函數形成沒必要要的耦合和複雜 度。
在這裏也是同樣的,求和和求最大值不是 dlist 應該提供的功能,放在 dlist 裏面實現是不該 該的。爲了能實現這些功能,咱們提供一種知足這些需求的機制就行了。熱心腸是好的,但必定不能違背原則,不然就費力不討好了。
需求簡述
這裏咱們請讀者實現下列功能:
對一個存放字符串的雙向鏈表,把存放在其中的字符串轉換成大寫字母。
存放時拷貝了數據,但沒有 free 分配的內存。
DList* dlist = dlist_create();
dlist_append(dlist, strdup("It"));
dlist_append(dlist, strdup("is"));
dlist_append(dlist, strdup("OK"));
dlist_append(dlist, strdup("!"));
dlist_foreach(dlist, str_toupper, NULL);
dlist_foreach(dlist, str_print, NULL);
dlist_destroy(dlist);
這裏看起來工做正常了,但存在內存泄露的 BUG。strdup 調用 malloc 分配了內存,但沒有地 方去 free 它們。
strdup()在內部調用了malloc()爲變量分配內存,不須要使用返回的字符串時,須要用free()釋放相應的內存空間,不然會形成內存泄漏。
在程序中,數據存放的位置主要有如下幾個:
1.未初始化的全局變量(.bss 段)
BSS(Block Started by Symbol)
BSS(Block Started by Symbol)一般是指用來存放程序中未初始化的全局變量和靜態變量的一塊內存區域。特色是:可讀寫的,在程序執行以前BSS段會自動清0。因此,未初始的全局變量在程序執行以前已經成0了。
注意和數據段的區別,BSS存放的是未初始化的全局變量和靜態變量,數據段存放的是初始化後的全局變量和靜態變量。
李先靜: bss 段是用來存放那些沒有初始化的和初始化爲 0 的全局變量的。
2.初始化過的全局變量 (.data段)
通俗的說,data 段用來存放那些初始化 爲非零的全局變量。
3.常量數據 (.rodata段)
rodata 的意義一樣明顯,ro 表明 read only,rodata 就是用來存放常量數據的。
關於 rodata 類型的數據,要注意如下幾點:
o 常量不必定就放在 rodata 裏,有的當即數直接和指令編碼在一塊兒,存放在代碼段(.text)中。
o 對於字符串常量,編譯器會自動去掉重複的字符串,保證一個字符串在一個可執行文件 (EXE/SO)中只存在一份拷貝。
o rodata 是在多個進程間是共享的,這樣能夠提升運行空間利用率。 o 在有的嵌入式系統中,rodata 放在 ROM(或者 norflash)裏,運行時直接讀取,無需加載到
RAM 內存中。 o 在嵌入式 linux 系統中,也能夠經過一種叫做 XIP(就地執行)的技術,也能夠直接讀取,
而無需加載到 RAM 內存中。
o 常量是不能修改的,修改常量在 linux 下會出現段錯誤。
因而可知,把在運行過程當中不會改變的數據設爲 rodata 類型的是有好處的:在多個進程間共 享,能夠大大提升空間利用率,甚至不佔用RAM空間。同 時因爲rodata在只讀的內存頁面 (page)中,是受保護的,任何試圖對它的修改都會被及時發現,這能夠提升程序的穩定性。
字符串會被編譯器自動放到 rodata 中,其它數據要放到 rodata 中,只須要加 const 關鍵字修 飾就行了。
4.代碼 (.text段) text 段存放代碼(如函數)和部分整數常量,它與 rodata 段很類似,相同的特性咱們就不重複了,主要不一樣在於這個段是能夠執行的。
5. 棧(stack)
棧用於存放臨時變量和函數參數。
儘管大多數編譯器在優化時,會把經常使用的參數或者局部變量放入寄存器中。但用棧來管理函
數調用時的臨時變量(局部變量和參數)是通用作法,前者只是輔助手段,且只在當前函數
中使用,一旦調用下一層函數,這些值仍然要存入棧中才行。
一般狀況下,棧向下(低地址)增加,每向棧中 PUSH 一個元素,棧頂就向低地址擴展,每從棧中POP一個元素,棧頂就向高地址回退。一個有興趣的問 題:在x86平臺上,棧頂寄 存器爲 ESP,那麼 ESP 的值在是 PUSH 操做以前修改呢,仍是在 PUSH 操做以後修改呢? PUSH ESP 這條指令會向棧中存入什麼數據呢?聽說 x86 系列 CPU 中,除了 286 外,都是先 修改ESP,再壓棧的。因爲286沒有CPUID指令,有的OS用 這種方法檢查286的型號。
要注意的是,存放在棧中的數據只在當前函數及下一層函數中有效,一旦函數返回了,這些
數據也自動釋放了,繼續訪問這些變量會形成意想不到的錯誤。
6.堆(heap) 堆是最靈活的一種內存,它的生命週期徹底由使用者控制。標準 C 提供幾個函數:
malloc 用來分配一塊指定大小的內存。
realloc 用來調整/重分配一塊存在的內存。
free 用來釋放再也不使用的內存。
最後,咱們來看看在 linux 下,程序運行時空間的分配狀況:
每一個區間都有四個屬性:
r 表示能夠讀取。 w 表示能夠修改。 x 表示能夠執行。 p/s 表示是否爲共享內存。
「 快」是指開發效率高,「好」是指軟件質量高。呵呵,寫得又快又好的人就是高手了。 記得這是林銳博士下的定義
UNIX下可以使用size命令查看可執行文件的段大小信息。如size a.out。
fdf:data_store chaixiaohong$ gcc -g bss.c -o bss.exe
fdf:data_store chaixiaohong$ ls
Makefile bss.exe.dSYM dlist.h dlist_toupper_test.dSYM
bss.c data.c dlist_toupper.c heap_error.c
bss.exe dlist.c dlist_toupper_test toupper.c
fdf:data_store chaixiaohong$ ls -l bss.exe
-rwxr-xr-x 1 chaixiaohong staff 4624 1 6 16:18 bss.exe
fdf:data_store chaixiaohong$ objdump -h bss.exe | grep bss
-bash: objdump: command not found
fdf:data_store chaixiaohong$ otool -h /bin/ls
/bin/ls:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x80 2 19 1816 0x00200085
fdf:data_store chaixiaohong$ otool -h bss.exe
bss.exe:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x80 2 16 976 0x00200085
fdf:data_store chaixiaohong$
ls 顯示的時文件大小 5975, 00400020是bss_array的大小