接口規(guī)定了模塊做什么,僅規(guī)定應(yīng)用程序可能使用的那些標識符,而盡可能隱藏不相關(guān)的表示細節(jié)和算法违寿。在C語言中,接口通過一個頭文件指定熟空,頭文件的擴展名通常為.h藤巢。這個頭文件會聲明應(yīng)用程序可能使用的宏、類型息罗、數(shù)據(jù)結(jié)構(gòu)掂咒、變量和接口聲明。應(yīng)用程序通過預(yù)處理指令#include導(dǎo)入接口迈喉。
API接口文檔分為在線或離線兩種绍刮,API接口文檔一般遵循這些規(guī)范:
編程風格
編程風格一致,此外挨摸,編程命名函數(shù)和變量時孩革,用英文表述它的具體含義。
原子獨立
API接口必須是定義明確得运,功能獨立的膝蜈;如果一個函數(shù)能通過其他函數(shù)組合實現(xiàn)锅移,你就不要提供這個函數(shù),讓用戶自己去實現(xiàn)饱搏。
接口清楚
接口包括exported 函數(shù)及抽象數(shù)據(jù)類型
函數(shù)
必須顯示地讓調(diào)用者明白接口使用方法及約束:輸入?yún)?shù)非剃,輸出參數(shù),錯誤處理推沸,線程是否安全备绽,是否support c++等。
頭文件聲明時鬓催,判斷調(diào)用者是C++時肺素,加上exern "C",否則連接錯誤宇驾;
抽象數(shù)據(jù)類型
? ? ? ?一個抽象數(shù)據(jù)類型是一個接口压怠,它定義了一個數(shù)據(jù)類型和對該類型的值所進行的操作。一個數(shù)據(jù)類型是一個值的集合飞苇。在C語言中,內(nèi)建的數(shù)據(jù)類型包括字符蜗顽、整數(shù)布卡、浮點數(shù)等。而結(jié)構(gòu)本身也能定義新的類型雇盖,因而可用于建立更高級類型忿等,如列表、樹崔挖、查找表等贸街。
? ? ? ?高級類型是抽象的,因為其接口隱藏了相關(guān)的表示細節(jié)狸相,并只規(guī)定了對該類型值的合法操作薛匪。理想情況下,這些操作不會暴露類型的表示細節(jié)脓鹃,因為那樣可能使應(yīng)用程序隱含地依賴于具體的表示逸尖。抽象數(shù)據(jù)類型或ADT的標準范例是棧。
錯誤處理機制
錯誤處理返回碼應(yīng)清晰具體瘸右,這樣便于排查問題娇跟。
接口封閉
必須有頭有尾,比如你提供了void *context_create();太颤,內(nèi)部實現(xiàn)malloc一塊內(nèi)存苞俘,那么對應(yīng)的你需要有相應(yīng)接口釋放這塊內(nèi)存空間,比如oid context_destroy(void *ctx)龄章。
可移植性(如果需要)
頭文件要做到可移植性吃谣,至少看起來你的實現(xiàn)支持windows和linux平臺乞封。通過宏來實現(xiàn)。
測試
需要有單元測試基协,功能測試歌亲,穩(wěn)定性測試及性能測試結(jié)果,來保證使用者放心使用澜驮。
示例
頭文件示例:
#ifndef _NAPI_H_
#define _NAPI_H_
#include <stdint.h>
#define ERR_API_BASE 0x00000100
#define ERR_API_INPUT (ERR_API_BASE+0x01)
#if defined(VSAPICALLMCRO) && defined(_WIN32)
#define APICALL __declspec(dllexport)
#else
#define APICALL
#endif
#ifdef__cplusplus
extern"C" {
#endif
APICALL void loglevel_set(uint32_t level);
APICALL void *context_create();
APICALL void context_destroy(void *ctx);
#ifdef__cplusplus
}
#endif
#endif
接口示例:
NAME
?? ? rand, rand_r -- random number generator
SYNOPSIS
?? ? #include <stdlib.h>
?? ? int rand(void);
?? ? int rand_r(unsigned *seed);
DESCRIPTION
?? ? These interfaces are obsoleted by arc4random(3).
?? ? The rand() function computes a sequence of pseudo-random integers in the range of 0 to RAND_MAX (as defined by the header file <stdlib.h>).
?? ? The rand_r() function provides the same functionality as rand().? A pointer to the context value seed must be supplied by the caller.
高級進階
依賴
特指編譯依賴陷揪。若x.h包含了y.h,則稱作x依賴y杂穷。依賴關(guān)系會進行傳導(dǎo)悍缠,如x.h包含y.h,而y.h又包含了z.h耐量,則x通過y依賴了z飞蚓。依賴將導(dǎo)致編譯時間的上升。雖然依賴是不可避免的廊蜒,也是必須的趴拧,但是不良的設(shè)計會導(dǎo)致整個系統(tǒng)的依賴關(guān)系無比復(fù)雜,使得任意一個文件的修改都要重新編譯整個系統(tǒng)山叮,導(dǎo)致編譯時間巨幅上升著榴。
在一個設(shè)計良好的系統(tǒng)中, 修改一個文件屁倔,只需要重新編譯數(shù)個脑又,甚至是一個文件。
《 google C++ Style Guide》 1.2 頭文件依賴 章節(jié)也給出了類似的闡述:
若包含了頭文件aa.h锐借,則就引入了新的依賴:
一旦aa.h被修改问麸,任何直接和間接包含aa.h代碼都會被重新編譯。如果aa.h又包含了其他頭文件如bb.h钞翔,那么bb.h的任何改變都將導(dǎo)致所有包含了aa.h的代碼被重新編譯严卖,在敏捷開發(fā)方式下,代碼會被頻繁構(gòu)建布轿,漫長的編譯時間將極大的阻礙頻繁構(gòu)建妄田。因此,我們傾向于減少包含頭文件驮捍,尤其是在頭文件中包含頭文件疟呐,以控制改動代碼后的編譯時間。
合理的頭文件劃分體現(xiàn)了系統(tǒng)設(shè)計的思想东且,但是從編程規(guī)范的角度看启具,仍然有一些通用的方法,用來合理規(guī)劃頭文件珊泳。本章節(jié)介紹的一些方法鲁冯,對于合理規(guī)劃頭文件會有一定的幫助拷沸。
原則1.1 頭文件中適合放置接口的聲明,不適合放置實現(xiàn)薯演。
說明: 頭文件是模塊( Module)或單元( Unit)的對外接口撞芍。頭文件中應(yīng)放置對外部的聲明,如對外提供的函數(shù)聲明跨扮、宏定義序无、類型定義等。
內(nèi)部使用的函數(shù)(相當于類的私有方法)聲明不應(yīng)放在頭文件中衡创。
內(nèi)部使用的宏帝嗡、枚舉、結(jié)構(gòu)定義不應(yīng)放入頭文件中璃氢。
變量定義不應(yīng)放在頭文件中哟玷,應(yīng)放在.c文件中。
變量的聲明盡量不要放在頭文件中一也,亦即盡量不要使用全局變量作為接口巢寡。變量是模塊或單元的內(nèi)部實現(xiàn)細節(jié),不應(yīng)通過在頭文件中聲明的方式直接暴露給外部椰苟,應(yīng)通過函數(shù)接口的方式進行對外暴露抑月。 即使必須使用全局變量,也只應(yīng)當在.c中定義全局變量尊剔,在.h中僅聲明變量為全局的。
原則1.2 頭文件應(yīng)當職責單一菱皆。
說明:頭文件過于復(fù)雜须误,依賴過于復(fù)雜是導(dǎo)致編譯時間過長的主要原因。 很多現(xiàn)有代碼中頭文件過大仇轻,職責過多京痢, 再加上循環(huán)依賴的問題,可能導(dǎo)致為了在.c中使用一個宏篷店,而包含十幾個頭文件祭椰。
某個頭文件不但定義了基本數(shù)據(jù)類型WORD,還包含了stdio.h syslib.h等等不常用的頭文件疲陕。如果工程中有10000個源文件方淤,而其中100個源文件使用了stdio.h的printf,由于上述頭文件的職責過于龐大蹄殃,而WORD又是每一個文件必須包含的携茂,從而導(dǎo)致stdio.h/syslib.h等可能被不必要的展開了9900次,大大增加了工程的編譯時間诅岩。
原則1.3 頭文件應(yīng)向穩(wěn)定的方向包含讳苦。
說明: 頭文件的包含關(guān)系是一種依賴带膜,一般來說,應(yīng)當讓不穩(wěn)定的模塊依賴穩(wěn)定的模塊鸳谜,從而當不穩(wěn)定的模塊發(fā)生變化時膝藕,不會影響(編譯)穩(wěn)定的模塊。
就我們的產(chǎn)品來說咐扭,依賴的方向應(yīng)該是: 產(chǎn)品依賴于平臺芭挽,平臺依賴于標準庫。 某產(chǎn)品線平臺的代碼中已經(jīng)包含了產(chǎn)品的頭文件草描,導(dǎo)致平臺無法單獨編譯览绿、發(fā)布和測試, 是一個非常糟糕的反例穗慕。
除了不穩(wěn)定的模塊依賴于穩(wěn)定的模塊外饿敲,更好的方式是兩個模塊共同依賴于接口,這樣任何一個模塊的內(nèi)部實現(xiàn)更改都不需要重新編譯另外一個模塊逛绵。在這里怀各,我們假設(shè)接口本身是最穩(wěn)定的。
規(guī)則1.1 每一個.c文件應(yīng)有一個同名.h文件术浪,用于聲明需要對外公開的接口瓢对。
說明: 如果一個.c文件不需要對外公布任何接口,則其就不應(yīng)當存在胰苏,除非它是程序的入口硕蛹,如main函數(shù)所在的文件。
現(xiàn)有某些產(chǎn)品中硕并,習慣一個.c文件對應(yīng)兩個頭文件法焰,一個用于存放對外公開的接口,一個用于存放內(nèi)部需要用到的定義倔毙、聲明等埃仪,以控制.c文件的代碼行數(shù)。編者不提倡這種風格陕赃。這種風格的根源在于源文件過大卵蛉,應(yīng)首先考慮拆分.c文件,使之不至于太大么库。另外傻丝,一旦把私有定義、聲明放到獨立的頭文件中诉儒,就無法從技術(shù)上避免別人include之桑滩,難以保證這些定義最后真的只是私有的。
本規(guī)則反過來并不一定成立。有些特別簡單的頭文件运准,如命令I(lǐng)D定義頭文件幌氮,不需要有對應(yīng)的.c存在。
示例:對于如下場景胁澳,如在一個.c中存在函數(shù)調(diào)用關(guān)系:
void foo()
{
? ? bar();
}
void bar()
{
? ? Do something;
}
必須在foo之前聲明bar该互,否則會導(dǎo)致編譯錯誤。
這一類的函數(shù)聲明韭畸,應(yīng)當在.c的頭部聲明宇智,并聲明為static的,如下:
static void bar();
void foo()
{
? ? bar();
}
void bar()
{
? ? Do something;
}
規(guī)則1.2 禁止頭文件循環(huán)依賴胰丁。
說明: 頭文件循環(huán)依賴随橘,指a.h包含b.h, b.h包含c.h锦庸, c.h包含a.h之類導(dǎo)致任何一個頭文件修改机蔗,都導(dǎo)致所有包含了a.h/b.h/c.h的代碼全部重新編譯一遍。而如果是單向依賴甘萧,如a.h包含b.h萝嘁, b.h包含c.h,而c.h不包含任何頭文件扬卷,則修改a.h不會導(dǎo)致包含了b.h/c.h的源代碼重新編譯牙言。
規(guī)則1.3 .c/.h文件禁止包含用不到的頭文件。
說明: 很多系統(tǒng)中頭文件包含關(guān)系復(fù)雜怪得,開發(fā)人員為了省事起見咱枉,可能不會去一一鉆研,直接包含一切想到的頭文件徒恋,甚至有些產(chǎn)品干脆發(fā)布了一個god.h蚕断,其中包含了所有頭文件,然后發(fā)布給各個項目組使用因谎,這種只圖一時省事的做法基括,導(dǎo)致整個系統(tǒng)的編譯時間進一步惡化颜懊,并對后來人的維護造成了巨大的麻煩财岔。
規(guī)則1.4 頭文件應(yīng)當自包含。
說明: 簡單的說河爹,自包含就是任意一個頭文件均可獨立編譯匠璧。 如果一個文件包含某個頭文件,還要包含另外一個頭文件才能工作的話咸这,就會增加交流障礙夷恍,給這個頭文件的用戶增添不必要的負擔。
示例:
如果a.h不是自包含的媳维,需要包含b.h才能編譯酿雪,會帶來的危害:
每個使用a.h頭文件的.c文件遏暴,為了讓引入的a.h的內(nèi)容編譯通過,都要包含額外的頭文件b.h指黎。
額外的頭文件b.h必須在a.h之前進行包含朋凉,這在包含順序上產(chǎn)生了依賴。
注意:該規(guī)則需要與“ .c/.h文件禁止包含用不到的頭文件”規(guī)則一起使用醋安,不能為了讓a.h自包含杂彭,而在a.h中包含不必要的頭文件。 a.h要剛剛可以自包含吓揪,不能在a.h中多包含任何滿足自包含之外的其他頭文件亲怠。
規(guī)則1.5 總是編寫內(nèi)部#include保護符( #define 保護) 。
說明:多次包含一個頭文件可以通過認真的設(shè)計來避免柠辞。如果不能做到這一點团秽,就需要采取阻止頭文件內(nèi)容被包含多于一次的機制。
通常的手段是為每個文件配置一個宏钾腺,當頭文件第一次被包含時就定義這個宏徙垫,并在頭文件被再次包含時使用它以排除文件內(nèi)容。
所有頭文件都應(yīng)當使用#define 防止頭文件被多重包含放棒,命名格式為FILENAME_H姻报,為了保證唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H间螟。
注:沒有在宏最前面加上““吴旋,即使用FILENAME_H代替_FILENAME_H,是因為一般以”“和”_“開頭的標識符為系統(tǒng)保留或者標準庫使用厢破,在有些靜態(tài)檢查工具中荣瑟,若全局可見的標識符以”_”開頭會給出告警。
定義包含保護符時摩泪,應(yīng)該遵守如下規(guī)則:
1)保護符使用唯一名稱笆焰;
2)不要在受保護部分的前后放置代碼或者注釋。
示例:假定VOS工程的timer模塊的timer.h见坑,其目錄為VOS/include/timer/timer.h,應(yīng)按如下方式保護:
#ifndef VOS_INCLUDE_TIMER_TIMER_H
#define VOS_INCLUDE_TIMER_TIMER_H
...
#endif
也可以使用如下簡單方式保護:
#ifndef TIMER_H
#define TIMER_H
..
#endif
例外情況:頭文件的版權(quán)聲明部分以及頭文件的整體注釋部分(如闡述此頭文件的開發(fā)背景嚷掠、使用注意事項等)可以放在保護符(#ifndef XX_H)前面。
規(guī)則1.6 禁止在頭文件中定義變量荞驴。
說明: 在頭文件中定義變量不皆,將會由于頭文件被其他.c文件包含而導(dǎo)致變量重復(fù)定義。
規(guī)則1.7 只能通過包含頭文件的方式使用其他.c提供的接口熊楼,禁止在.c中通過extern的方式使用外部函數(shù)接口霹娄、變量。
說明:若a.c使用了b.c定義的foo()函數(shù),則應(yīng)當在b.h中聲明extern int foo(int input)犬耻;并在a.c中通過#include 來使用foo踩晶。禁止通過在a.c中直接寫extern int foo(int input);來使用foo,后面這種寫法容易在foo改變時可能導(dǎo)致聲明和定義不一致枕磁。
規(guī)則1.8 禁止在extern “C”中包含頭文件合瓢。
說明:在extern “C”中包含頭文件, 會導(dǎo)致extern “C”嵌套透典, Visual Studio對extern “C”嵌套層次有限制晴楔,嵌套層次太多會編譯錯誤。
在extern “C”中包含頭文件峭咒,可能會導(dǎo)致被包含頭文件的原有意圖遭到破壞税弃。例如,存在a.h和b.h兩個頭文件:
使用C++預(yù)處理器展開b.h凑队,將會得到
extern "C"
{
? ? void foo(int);
? ? void b();
}
按照a.h作者的本意则果,函數(shù)foo是一個C++自由函數(shù),其鏈接規(guī)范為”C++”漩氨。但在b.h中西壮,由于#include “a.h”被放到了extern “C” { }的內(nèi)部,函數(shù)foo的鏈接規(guī)范被不正確地更改了叫惊。
示例: 錯誤的使用方式:
extern “C”
{
? ? #include “xxx.h”
? ? ...
}
正確的使用方式:
#include “xxx.h”
extern “C”
{
? ? ...
}
建議1.1 一個模塊通常包含多個.c文件款青,建議放在同一個目錄下,目錄名即為模塊名霍狰。為方便外部使用者抡草,建議每一個模塊提供一個.h,文件名為目錄名蔗坯。
說明:需要注意的是康震,這個.h并不是簡單的包含所有內(nèi)部的.h,它是為了模塊使用者的方便宾濒,對外整體提供的模塊接口腿短。
以Google test(簡稱GTest)為例, GTest作為一個整體對外提供C++單元測試框架绘梦,其1.5版本的gtest工程下有6個源文件和12個頭文件橘忱。但是它對外只提供一個gtest.h,只要包含gtest.h即可使用GTest提供的所有對外提供的功能谚咬,使用者不必關(guān)系GTest內(nèi)部各個文件的關(guān)系鹦付,即使以后GTest的內(nèi)部實現(xiàn)改變了尚粘,比如把一個源文件c拆成兩個源文件择卦,使用者也不必關(guān)心,甚至如果對外功能不變,連重新編譯都不需要秉继。
對于有些模塊祈噪,其內(nèi)部功能相對松散,可能并不一定需要提供這個.h尚辑,而是直接提供各個子模塊或者.c的頭文件辑鲤。
比如產(chǎn)品普遍使用的VOS,作為一個大模塊杠茬,其內(nèi)部有很多子模塊月褥,他們之間的關(guān)系相對比較松散,就不適合提供一個vos.h瓢喉。而VOS的子模塊宁赤,如Memory(僅作舉例說明,與實際情況可能有所出入)栓票,其內(nèi)部實現(xiàn)高度內(nèi)聚决左,雖然其內(nèi)部實現(xiàn)可能有多個.c和.h,但是對外只需要提供一個Memory.h聲明接口走贪。
建議1.2 如果一個模塊包含多個子模塊佛猛,則建議每一個子模塊提供一個對外的.h,文件名為子模塊名坠狡。
說明:降低接口使用者的編寫難度继找。
建議1.3 頭文件不要使用非習慣用法的擴展名,如.inc逃沿。
說明:目前很多產(chǎn)品中使用了.inc作為頭文件擴展名码荔,這不符合c語言的習慣用法。在使用.inc作為頭文件擴展名的產(chǎn)品感挥,習慣上用于標識此頭文件為私有頭文件缩搅。但是從產(chǎn)品的實際代碼來看,這一條并沒有被遵守触幼,一個.inc文件被多個.c包含比比皆是硼瓣。本規(guī)范不提倡將私有定義單獨放在頭文件中,具體見 規(guī)則1.1置谦。
除此之外堂鲤,使用.inc還導(dǎo)致source insight、 Visual stduio等IDE工具無法識別其為頭文件媒峡,導(dǎo)致很多功能不可用瘟栖,如“跳轉(zhuǎn)到變量定義處”。雖然可以通過配置谅阿,強迫IDE識別.inc為頭文件半哟,但是有些軟件無法配置酬滤,如Visual Assist只能識別.h而無法通過配置識別.inc。
建議1.4 同一產(chǎn)品統(tǒng)一包含頭文件排列方式寓涨。
說明:常見的包含頭文件排列方式: 功能塊排序盯串、文件名升序、穩(wěn)定度排序戒良。
以穩(wěn)定度排序体捏, 建議將不穩(wěn)定的頭文件放在前面,如把產(chǎn)品的頭文件放在平臺的頭文件前面,如下:
相對來說, product.h修改的較為頻繁茶行,如果有錯誤,不必編譯platform.h就可以發(fā)現(xiàn)product.h的錯誤奏司,可以部分減少編譯時間。