目錄
1、不完全類型的概念
2、不完全類型的用途
3、不完全類型實踐應用
ISO(國際标準化組織(International Standard Organization))将C語言分為三個不同類型集合: 函數類型、對象類型和不完全類型,具體說明如下所示:
函數類型:函數就是C語言的子產品、一系列 C 語句的集合,有較強的獨立性,能完成某個特定的功能,可以互相調用;
對象類型:C語言的對象類型不是說面向對象程式設計,而是在記憶體中建立具有特定長度,有意義的類型,例如char、int、數組、結構體、指針等;
不完全類型:不完全類型是指除了函數類型之外,大小不能被确定的類型。比如,聲明了一個數組,但不給出數組的長度;聲明了一個結構類型,但不給出結構體的定義,隻告訴編譯器這是一個結構體。在最終你還是必須得給出完整的定義,否則編譯器在編譯單元中都找不到不完全類型的完整定義資訊的話就會報錯。
C語言所有資料類型如下圖所示:

在C99标準中對不完全類型描述如下:
The void type comprises an empty set of values; it is an incomplete type that cannot be completed. (C99 6.2.5/19)
An array type of unknown size is an incomplete type. It is completed, for an identifier of that type, by specifying the size in a later declaration (with internal or external linkage). A structure or union type of unknown content (as described in 6.7.2.3) is an incomplete type. It is completed, for all declarations of that type, by declaring the same structure or union tag with its defining content later in the same scope.(C99 6.2.5/22)
總結講,C/C++中不完全類型有三種不同形式:void、未指定長度的數組以及具有非指定内容的結構和聯合。void類型與其他兩種類型不同,因為它是無法完成的不完全類型,并且它用作特殊函數傳回和參數類型。
不完全類型是暫時沒有完全定義好的類型,編譯器不知道這種類型該占幾個位元組的存儲空間,例如以下定義方式:
int str[]; //不完全類型數組str定義
…
int str[10]; //定義str數組完整的類型資訊
再舉個例子,在頭*.h檔案中聲明結構:typedef struct __list *list_t;,最終在*.c檔案中定義如下:
struct __list {
struct __list *prev;
struct __list *next;
viud *data;
};
此時,結構體變量*list_t就屬于不完全類型,不完全類型不包含具體的類型資訊,是以在未完整定義前不能通過sizeof來獲知大小,并且不完全類型定義不适合局部變量。
不完全類型的用途主要為以下三點:
1、提高代碼靈活性。在*.h頭檔案中聲明的數組,不清楚具體使用場景應該需要多大,在*.c中使用數組前再完整定義,就可以很友善的更改數組的大小,也不用再去修改頭檔案。
2、兩個結構體需要互相指向,唯一能夠實作的方式就是不完全結構,如下所示:
struct a { struct b *pb; };
struct b { struct a *pa; };
3、實作抽象模型的封裝,降低程式子產品之間的耦合,防止使用者直接通路結構成員,破壞内部抽象資料類型。這樣可以強制使用者通過接口規則通路,隐藏内部實作細節,降低溝通成本。
舉個例子,項目開發中需要用到環形緩存(一種用于表示一個固定尺寸、頭尾相連的緩沖區的資料結構,适合緩存資料流),于是小夥伴将這個任務交給了你。然後你實作了ring_buffer.c,并在ring_buffer.h頭檔案中定義了實作功能用的資料結構和接口,初次程式設計如下所示:
typedef struct _ring_buffer_type
{
uint8_t *phead;
uint8_t *ptail;
uint8_t *pread;
uint8_t *pwrite;
size_t size;
volatile size_t counts;
}rcb_t;
/* 建構并初始化一個環形緩存 */
err_t ring_buffer_init(uint8_t *pbuffer, size_t size);
/* 向緩存中寫資料 */
err_t ring_buffer_write(rcb_t *const p_rcb, uint8_t *pdata, size_t len);
/* 從緩存中讀資料 */
err_t ring_buffer_read(rcb_t *const p_rcb, uint8_t *pdata, size_t len);
/* 檢查緩存已使用的位元組數 */
err_t ring_buffer_check(rcb_t *const p_rcb, size_t *len);
經過測試,功能實作很好,任務順利完成。為了屏蔽功能實作細節你将子產品封裝成了庫,信心十足的交給了小夥伴使用。但是你的夥伴卻投來了鄙視的目光,說你的實作的功能有問題,于是你們一起檢查他的代碼,你發現他寫了如下代碼:
ring_buffer_write(&buf_rcb, pdata, 10);
buf_rcb. pwrite += 10;
buf_rcb.counts += 10;
于是你不解的質問小夥伴,為什麼要動内部的資料,但小夥伴卻說,往裡面寫入了資料,應該要修改指針啊。你認為的事,小夥伴想的卻不一樣。
然後為了不讓别人動你内部的資料,于是你在頭檔案ring_buffer.h中把結構定義改成了:
typedef struct _ring_buffer_type rcb_t;
并将結構的定義放在了ring_buffer.c中:
struct _ring_buffer_type
{
uint8_t *phead;
uint8_t *ptail;
uint8_t *pread;
uint8_t *pwrite;
size_t size;
volatile size_t counts;
};
從此之後,内部的細節都被隐藏了,封裝成庫之後别人再也不清楚内部的資料結構了,隻能嚴格按照接口的要求進行調用,自然無法修改你的内部資料了。并且,以後修改内部實作也更友善了,甚至外部的接口都不需要做更改。
從使用者的角度,知道的細節越少越好,即減少了記憶的成本,也避免了一些不必要的麻煩。