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:當然有,只是面嚮對象語言中不叫“延遲定義”。而是叫封裝、訪問權限、接口和多態。類的封裝和訪問權限可以阻止客戶訪問類的實現細節,而接口和多態可以隱藏實現細節。當然它們的好處不只這些。

發佈了134 篇原創文章 · 獲贊 243 · 訪問量 54萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章