標籤:c語言 不完全類型 延遲定義 模組化
一直以為我的C語言學的還可以,雖說不是出神入化,但是至少比較熟悉吧。但是前一段時間看了一篇推文,再百度了一下C語言不完全類型。發現我居然C語言不完全類型和用途甚廣的延遲定義都沒概念。這兩天仔細查閱了相關概念並用代碼實驗了一下。
本文結構如下:
- C語言不完全類型概念介紹
- 一個故事
- 延遲定義的優點
- 思考…
C語言不完全類型
不完全類型也就是不知道變數的所有的類型資訊。比如可以聲明一個數組,但是不給出該數組的長度;聲明一個指標,但是不給出該指標的類型;聲明一個結構體類型,但是不給出完整的結構體定義,只說它是一個結構體。但是最終你必須得給出完整的類型資訊。要不然編譯會報錯的。編譯器在編譯某個單元時,如果遇到一個不完全類型的定義的類型或變數(假設它叫p),它會把這當作正常現象,然後繼續編譯該單元,如果在本單元內找不到p完整的類型資訊,它就去其它編譯單元找。如果把整個編譯過程分為編譯、連結兩個過程。在編譯階段遇到不完全類型是正常的,但是在連結過程中,所有的不完全類型必須存在對應的完整類型資訊,否則報錯。
舉個例子,下面的代碼先聲明了一個不完全類型的變數字元數組str,沒有給出它的長度資訊。然後再定義了一次str數組,這次給出的長度資訊。
char str[];//不完全類型定義
char str[10];//終於遇到了str數組的完整類型資訊,編譯器鬆了一口氣
注意:不完全類型定義不適合局部變數,如果把上面兩行代碼放在一個函數體中,會出現符號重定義錯誤。
不完全類型由於不包含具體的類型資訊,所以不能通過sizeof來獲得其大小。(編譯器君的旁邊:我連它的完整類型都不知道,我怎麼告訴你它的大小。)下面的代碼不能編譯通過。它會報錯 error: invalid application of `sizeof‘ to an incomplete type 不能對不完全類型使用sizeof。
#include<stdio.h>
char str[];
int n =sizeof(str);
char str[10];
int main()
{
printf("%d ",n);
return 0;
}
如果把int n = sizeof(str)放到charstr[10]後面就沒事了。
再舉一個結構體的例子。下面的代碼先聲明了一個不完全類型的結構體s。然後又定義了該結構體。
struct s;
struct s{
int a;
int b;
};
一個故事,不完全類型的應用:延遲定義
(哈哈,終於要講應用了,好激動!)
C語言的不完全類型的一般用作模組化編程中。現在假設你要實現一個儲存字元的棧給你的程式員隊友用。於是你寫出了下面的代碼。
//Stack.h
typedef structNode{
char data;
struct Node* next;
}Node;
typedef struct {
Node* top;//只需要棧頂指標
int num;
}Stack;
/*為了快速返回棧中元素的個數,你用一個int變數來記錄棧中元素的個數,在入棧和出棧時對num進行增減操作,這樣在返回棧中元素的個數的時候,就可以直接返回num變數了。*/
/構造一個空棧S
voidInitStack(Stack **s);
//銷毀棧S
voidDestroyStack(Stack **s);
//判斷棧是否為空白
intStackEmpty(Stack *s);
//返回棧的長度
intStackLength(Stack *s);
//用e返回棧頂元素
int GetTop(Stack*s,char *e);
//入棧
void Push(Stack*s,char e);
//彈出棧頂元素,並用e返回其值
int Pop(Stack*s,char *e);
為了不暴露實現細節,你將實現棧操作的各種實現代碼封裝成到了一個庫檔案中(靜態、動態都行)。然後將這個.h檔案和庫檔案信心滿滿的提交給了隊友。並暗示自己“我果然是個牛x的程式員,小美一定會崇拜我、尊敬我、對我欲罷不能!不行,我要謙虛,當作什麼也沒發生”。
幾個小時候,隊友皺著眉頭過來了。你心想“不對啊,怎麼不是崇拜的眼神”。然後發生了下面的一段對話。
“你封裝的什麼啊?連個棧中元素的個數都統計不對”,隊友很生氣
“怎麼可能?為了快速返回棧中元素的個數,我特意用了一個int變數來儲存棧中元素的個數”你也很生氣,明明用一個單獨變數來儲存元素個數是你的得意之處,居然被人說不對。
“我知道,可就是不對”,隊友堅持說你不對。
然後為了找出問題,你和隊友一起檢查他寫的代碼。你發現他寫了如下代碼。
…
Push(s);
++(s->num);
…
Pop(s,&e);
--(s->num);
原來隊友在壓棧和出棧時都對棧s的num進行了加減操作。
你:“為什麼你要動我的num?”
隊友:“你的num不是用來儲存棧中元素的個數的嗎?我壓棧、出棧當然要對它進行增減啊!”
你:“我在實現代碼中已經這麼做了。”
原來你以為理所當然的事,隊友卻和你想的不一樣。然後你告訴他不要多事,num的大小由你來維持。你們終於又可以一起愉快的玩耍了。
但是你在隊友的代碼裡還是發現像下面一些不爽的代碼,這讓你感到膈應。
e =s->top->data;
int len =s->num;
為了獲得棧頂元素,他沒有用你提供的GetTop函數,而是自作主張的直接通過指標直接存取棧內部的top指標。而且沒用你給的StackLength函數,而是直接存取棧結構體的num成員。雖然你可以和他溝通,讓他不要自己去訪問你的實現細節。但是你不能保證他會完全聽你的。能不能從技術上解決這個問題呢?個人覺得,團隊協作時,能從技術上解決的事,不必從溝通上解決。這樣可以減少團隊溝通成本。扯多了,回到剛才的問題——我們能否從技術上解決這個問題?
問題分析和解決方案
回頭看看我們剛才寫的代碼。隊友會直接存取我們封裝的內部細節,是因為他看到了我們給的.h檔案中結構體的詳細定義。那麼能不能去掉.h檔案中Stack結構體的定義呢?顯然不行,因為我們給出的幾個函數介面都要用到Stack類型的參數,如果使用者看不到Stack的定義,那他怎麼定義Stack類型的變數然後傳遞給這些介面函數呢?答案是,我們給出Stack結構體類型的定義,但是不給出Stack結構體的詳細資料。也就是前面講到的不完全類型定義。改進後的代碼如下。
//Stack.h
typedef structSqStack Stack;
//…各個函數介面定義
//Stack.c
typedef structNode{
char data;
struct Node* next;
}Node;
struct SqStack{
Node* top;
int num;
};
在改進後的代碼中,我們只是定義了一個不完全類型struct SqStack結構體,並用typedef將其和Stack名的類型等價。而將SqStack結構體的詳細定義放到了.c檔案中。這樣將.h檔案和庫檔案提交給隊友後,他再也看不到Stack的詳細資料了。這下,他就沒有直接存取結構體內部的衝動了。(看你丫還怎麼訪問?老老實實用哥提供的函數介面吧!)隊友看不到Stack結構體的內部是怎麼定義的,自然也不知道怎麼訪問,而且任何通過Stack類型指標變數的訪問方式都會讓編譯器報錯。這下,我再也不用擔心隊友的愚蠢了。
上面講到的,在標頭檔中只定義一個結構體的不完全類型,而將結構體的詳細定義延遲到在.c檔案的方式就叫“延遲定義”。
延遲定義帶來的其它好處
在上面的執行個體過程中,雖然我們是為了隱藏實現細節才使用的延遲定義。但是延遲定義也帶來了其它好處。另一個好處就是,我們可以以很小的工作量代價來更改實現。比如我們發現Stack結構的實現有bug需要修正,或者我們想將內部的棧的鏈式儲存改為順序儲存。我都可以直接在.c檔案中修改,然後重新編譯產生新的庫檔案,不需要修改.h檔案,因為我們的介面函數並不需要變。將庫檔案提交給使用者,使用者只需替換原來舊的庫檔案,而不需要修改代碼。如果是動態連結程式庫,使用者只需替換庫檔案就行。如果是靜態庫,使用者只需替換庫後重新連結一下。至始至終,使用者都不需要修改代碼,不需要重新編譯自己的用戶端代碼。
如果沒有用延遲定義,將內部細節放在.h檔案中,那修改細節後,使用者的用戶端代碼也需要重新編譯。
延遲定義的優點
我們來總結一下延遲定義的優點:
1. 隱藏了內部實現細節,強制使用者按介面規則訪問。減少溝通成本。
2. 便於修改。
上面兩點都是實現模組化編程所必須的。而且個人認為,站在客戶的角度,知道的細節越少越好,知道的越多,要記憶和思考的東西也越多。就像電視劇中的經典台詞:“有時候知道的太多並不是好事”。角色被人幹掉的理由也是“你知道的太多了”,或者“你知道了你不該知道的事情”。我只按照既定的規則來訪問別人提供的代碼,有什麼問題直接問別人,而不是看實現代碼。按照既定規則來訪問也便於職責劃分。如果非要總結延遲定義帶來的第3個好處,個人認為是:便於職責劃分,清晰職責邊界。
思考…
Q:既然延遲定義便於模組化編程,那C++、Java等物件導向的語言中有用到延遲定義類似的技術嗎?
A:當然有,只是物件導向語言中不叫“延遲定義”。而是叫封裝、存取權限、介面和多態。類的封裝和存取權限可以阻止客戶訪問類的實現細節,而介面和多態可以隱藏實現細節。當然它們的好處不只這些。