C語言API接口文檔

接口規(guī)定了模塊做什么,僅規(guī)定應(yīng)用程序可能使用的那些標(biāo)識符,而盡可能隱藏不相關(guān)的表示細(xì)節(jié)和算法。在C語言中,接口通過一個(gè)頭文件指定,頭文件的擴(kuò)展名通常為.h。這個(gè)頭文件會(huì)聲明應(yīng)用程序可能使用的宏、類型、數(shù)據(jù)結(jié)構(gòu)、變量和接口聲明。應(yīng)用程序通過預(yù)處理指令#include導(dǎo)入接口。

API接口文檔分為在線或離線兩種,API接口文檔一般遵循這些規(guī)范:

編程風(fēng)格

編程風(fēng)格一致,此外,編程命名函數(shù)和變量時(shí),用英文表述它的具體含義。

原子獨(dú)立

API接口必須是定義明確,功能獨(dú)立的;如果一個(gè)函數(shù)能通過其他函數(shù)組合實(shí)現(xiàn),你就不要提供這個(gè)函數(shù),讓用戶自己去實(shí)現(xiàn)。

接口清楚

接口包括exported 函數(shù)及抽象數(shù)據(jù)類型

函數(shù)

必須顯示地讓調(diào)用者明白接口使用方法及約束:輸入?yún)?shù),輸出參數(shù),錯(cuò)誤處理,線程是否安全,是否support c++等。

頭文件聲明時(shí),判斷調(diào)用者是C++時(shí),加上exern "C",否則連接錯(cuò)誤;

抽象數(shù)據(jù)類型

? ? ? ?一個(gè)抽象數(shù)據(jù)類型是一個(gè)接口,它定義了一個(gè)數(shù)據(jù)類型和對該類型的值所進(jìn)行的操作。一個(gè)數(shù)據(jù)類型是一個(gè)值的集合。在C語言中,內(nèi)建的數(shù)據(jù)類型包括字符、整數(shù)、浮點(diǎn)數(shù)等。而結(jié)構(gòu)本身也能定義新的類型,因而可用于建立更高級類型,如列表、樹、查找表等。

? ? ? ?高級類型是抽象的,因?yàn)槠浣涌陔[藏了相關(guān)的表示細(xì)節(jié),并只規(guī)定了對該類型值的合法操作。理想情況下,這些操作不會(huì)暴露類型的表示細(xì)節(jié),因?yàn)槟菢涌赡苁箲?yīng)用程序隱含地依賴于具體的表示。抽象數(shù)據(jù)類型或ADT的標(biāo)準(zhǔn)范例是棧。

錯(cuò)誤處理機(jī)制

錯(cuò)誤處理返回碼應(yīng)清晰具體,這樣便于排查問題。

接口封閉

必須有頭有尾,比如你提供了void *context_create();,內(nèi)部實(shí)現(xiàn)malloc一塊內(nèi)存,那么對應(yīng)的你需要有相應(yīng)接口釋放這塊內(nèi)存空間,比如oid context_destroy(void *ctx)。

可移植性(如果需要)

頭文件要做到可移植性,至少看起來你的實(shí)現(xiàn)支持windows和linux平臺(tái)。通過宏來實(shí)現(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.


高級進(jìn)階

依賴

特指編譯依賴。若x.h包含了y.h,則稱作x依賴y。依賴關(guān)系會(huì)進(jìn)行傳導(dǎo),如x.h包含y.h,而y.h又包含了z.h,則x通過y依賴了z。依賴將導(dǎo)致編譯時(shí)間的上升。雖然依賴是不可避免的,也是必須的,但是不良的設(shè)計(jì)會(huì)導(dǎo)致整個(gè)系統(tǒng)的依賴關(guān)系無比復(fù)雜,使得任意一個(gè)文件的修改都要重新編譯整個(gè)系統(tǒng),導(dǎo)致編譯時(shí)間巨幅上升。

在一個(gè)設(shè)計(jì)良好的系統(tǒng)中, 修改一個(gè)文件,只需要重新編譯數(shù)個(gè),甚至是一個(gè)文件。

《 google C++ Style Guide》 1.2 頭文件依賴 章節(jié)也給出了類似的闡述:

若包含了頭文件aa.h,則就引入了新的依賴:

一旦aa.h被修改,任何直接和間接包含aa.h代碼都會(huì)被重新編譯。如果aa.h又包含了其他頭文件如bb.h,那么bb.h的任何改變都將導(dǎo)致所有包含了aa.h的代碼被重新編譯,在敏捷開發(fā)方式下,代碼會(huì)被頻繁構(gòu)建,漫長的編譯時(shí)間將極大的阻礙頻繁構(gòu)建。因此,我們傾向于減少包含頭文件,尤其是在頭文件中包含頭文件,以控制改動(dòng)代碼后的編譯時(shí)間。

合理的頭文件劃分體現(xiàn)了系統(tǒng)設(shè)計(jì)的思想,但是從編程規(guī)范的角度看,仍然有一些通用的方法,用來合理規(guī)劃頭文件。本章節(jié)介紹的一些方法,對于合理規(guī)劃頭文件會(huì)有一定的幫助。

原則1.1 頭文件中適合放置接口的聲明,不適合放置實(shí)現(xiàn)。

說明: 頭文件是模塊( Module)或單元( Unit)的對外接口。頭文件中應(yīng)放置對外部的聲明,如對外提供的函數(shù)聲明、宏定義、類型定義等。

內(nèi)部使用的函數(shù)(相當(dāng)于類的私有方法)聲明不應(yīng)放在頭文件中。

內(nèi)部使用的宏、枚舉、結(jié)構(gòu)定義不應(yīng)放入頭文件中。

變量定義不應(yīng)放在頭文件中,應(yīng)放在.c文件中。

變量的聲明盡量不要放在頭文件中,亦即盡量不要使用全局變量作為接口。變量是模塊或單元的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),不應(yīng)通過在頭文件中聲明的方式直接暴露給外部,應(yīng)通過函數(shù)接口的方式進(jìn)行對外暴露。 即使必須使用全局變量,也只應(yīng)當(dāng)在.c中定義全局變量,在.h中僅聲明變量為全局的。

原則1.2 頭文件應(yīng)當(dāng)職責(zé)單一。

說明:頭文件過于復(fù)雜,依賴過于復(fù)雜是導(dǎo)致編譯時(shí)間過長的主要原因。 很多現(xiàn)有代碼中頭文件過大,職責(zé)過多, 再加上循環(huán)依賴的問題,可能導(dǎo)致為了在.c中使用一個(gè)宏,而包含十幾個(gè)頭文件。

某個(gè)頭文件不但定義了基本數(shù)據(jù)類型WORD,還包含了stdio.h syslib.h等等不常用的頭文件。如果工程中有10000個(gè)源文件,而其中100個(gè)源文件使用了stdio.h的printf,由于上述頭文件的職責(zé)過于龐大,而WORD又是每一個(gè)文件必須包含的,從而導(dǎo)致stdio.h/syslib.h等可能被不必要的展開了9900次,大大增加了工程的編譯時(shí)間。

原則1.3 頭文件應(yīng)向穩(wěn)定的方向包含。

說明: 頭文件的包含關(guān)系是一種依賴,一般來說,應(yīng)當(dāng)讓不穩(wěn)定的模塊依賴穩(wěn)定的模塊,從而當(dāng)不穩(wěn)定的模塊發(fā)生變化時(shí),不會(huì)影響(編譯)穩(wěn)定的模塊。

就我們的產(chǎn)品來說,依賴的方向應(yīng)該是: 產(chǎn)品依賴于平臺(tái),平臺(tái)依賴于標(biāo)準(zhǔn)庫。 某產(chǎn)品線平臺(tái)的代碼中已經(jīng)包含了產(chǎn)品的頭文件,導(dǎo)致平臺(tái)無法單獨(dú)編譯、發(fā)布和測試, 是一個(gè)非常糟糕的反例。

除了不穩(wěn)定的模塊依賴于穩(wěn)定的模塊外,更好的方式是兩個(gè)模塊共同依賴于接口,這樣任何一個(gè)模塊的內(nèi)部實(shí)現(xiàn)更改都不需要重新編譯另外一個(gè)模塊。在這里,我們假設(shè)接口本身是最穩(wěn)定的。

規(guī)則1.1 每一個(gè).c文件應(yīng)有一個(gè)同名.h文件,用于聲明需要對外公開的接口。

說明: 如果一個(gè).c文件不需要對外公布任何接口,則其就不應(yīng)當(dāng)存在,除非它是程序的入口,如main函數(shù)所在的文件。

現(xiàn)有某些產(chǎn)品中,習(xí)慣一個(gè).c文件對應(yīng)兩個(gè)頭文件,一個(gè)用于存放對外公開的接口,一個(gè)用于存放內(nèi)部需要用到的定義、聲明等,以控制.c文件的代碼行數(shù)。編者不提倡這種風(fēng)格。這種風(fēng)格的根源在于源文件過大,應(yīng)首先考慮拆分.c文件,使之不至于太大。另外,一旦把私有定義、聲明放到獨(dú)立的頭文件中,就無法從技術(shù)上避免別人include之,難以保證這些定義最后真的只是私有的。

本規(guī)則反過來并不一定成立。有些特別簡單的頭文件,如命令I(lǐng)D定義頭文件,不需要有對應(yīng)的.c存在。

示例:對于如下場景,如在一個(gè).c中存在函數(shù)調(diào)用關(guān)系:

void foo()

{

? ? bar();

}

void bar()

{

? ? Do something;

}


必須在foo之前聲明bar,否則會(huì)導(dǎo)致編譯錯(cuò)誤。

這一類的函數(shù)聲明,應(yīng)當(dā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)致任何一個(gè)頭文件修改,都導(dǎo)致所有包含了a.h/b.h/c.h的代碼全部重新編譯一遍。而如果是單向依賴,如a.h包含b.h, b.h包含c.h,而c.h不包含任何頭文件,則修改a.h不會(huì)導(dǎo)致包含了b.h/c.h的源代碼重新編譯。

規(guī)則1.3 .c/.h文件禁止包含用不到的頭文件。

說明: 很多系統(tǒng)中頭文件包含關(guān)系復(fù)雜,開發(fā)人員為了省事起見,可能不會(huì)去一一鉆研,直接包含一切想到的頭文件,甚至有些產(chǎn)品干脆發(fā)布了一個(gè)god.h,其中包含了所有頭文件,然后發(fā)布給各個(gè)項(xiàng)目組使用,這種只圖一時(shí)省事的做法,導(dǎo)致整個(gè)系統(tǒng)的編譯時(shí)間進(jìn)一步惡化,并對后來人的維護(hù)造成了巨大的麻煩。

規(guī)則1.4 頭文件應(yīng)當(dāng)自包含。

說明: 簡單的說,自包含就是任意一個(gè)頭文件均可獨(dú)立編譯。 如果一個(gè)文件包含某個(gè)頭文件,還要包含另外一個(gè)頭文件才能工作的話,就會(huì)增加交流障礙,給這個(gè)頭文件的用戶增添不必要的負(fù)擔(dān)。

示例:

如果a.h不是自包含的,需要包含b.h才能編譯,會(huì)帶來的危害:

每個(gè)使用a.h頭文件的.c文件,為了讓引入的a.h的內(nèi)容編譯通過,都要包含額外的頭文件b.h。

額外的頭文件b.h必須在a.h之前進(jìn)行包含,這在包含順序上產(chǎn)生了依賴。

注意:該規(guī)則需要與“ .c/.h文件禁止包含用不到的頭文件”規(guī)則一起使用,不能為了讓a.h自包含,而在a.h中包含不必要的頭文件。 a.h要?jiǎng)倓偪梢宰园?,不能在a.h中多包含任何滿足自包含之外的其他頭文件。

規(guī)則1.5 總是編寫內(nèi)部#include保護(hù)符( #define 保護(hù)) 。

說明:多次包含一個(gè)頭文件可以通過認(rèn)真的設(shè)計(jì)來避免。如果不能做到這一點(diǎn),就需要采取阻止頭文件內(nèi)容被包含多于一次的機(jī)制。

通常的手段是為每個(gè)文件配置一個(gè)宏,當(dāng)頭文件第一次被包含時(shí)就定義這個(gè)宏,并在頭文件被再次包含時(shí)使用它以排除文件內(nèi)容。

所有頭文件都應(yīng)當(dāng)使用#define 防止頭文件被多重包含,命名格式為FILENAME_H,為了保證唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H。

注:沒有在宏最前面加上““,即使用FILENAME_H代替_FILENAME_H,是因?yàn)橐话阋浴薄昂汀盻“開頭的標(biāo)識符為系統(tǒng)保留或者標(biāo)準(zhǔn)庫使用,在有些靜態(tài)檢查工具中,若全局可見的標(biāo)識符以”_”開頭會(huì)給出告警。

定義包含保護(hù)符時(shí),應(yīng)該遵守如下規(guī)則:

1)保護(hù)符使用唯一名稱;

2)不要在受保護(hù)部分的前后放置代碼或者注釋。

示例:假定VOS工程的timer模塊的timer.h,其目錄為VOS/include/timer/timer.h,應(yīng)按如下方式保護(hù):

#ifndef VOS_INCLUDE_TIMER_TIMER_H

#define VOS_INCLUDE_TIMER_TIMER_H

...

#endif


也可以使用如下簡單方式保護(hù):

#ifndef TIMER_H

#define TIMER_H

..

#endif


例外情況:頭文件的版權(quán)聲明部分以及頭文件的整體注釋部分(如闡述此頭文件的開發(fā)背景、使用注意事項(xiàng)等)可以放在保護(hù)符(#ifndef XX_H)前面。

規(guī)則1.6 禁止在頭文件中定義變量。

說明: 在頭文件中定義變量,將會(huì)由于頭文件被其他.c文件包含而導(dǎo)致變量重復(fù)定義。

規(guī)則1.7 只能通過包含頭文件的方式使用其他.c提供的接口,禁止在.c中通過extern的方式使用外部函數(shù)接口、變量。

說明:若a.c使用了b.c定義的foo()函數(shù),則應(yīng)當(dāng)在b.h中聲明extern int foo(int input);并在a.c中通過#include 來使用foo。禁止通過在a.c中直接寫extern int foo(int input);來使用foo,后面這種寫法容易在foo改變時(shí)可能導(dǎo)致聲明和定義不一致。

規(guī)則1.8 禁止在extern “C”中包含頭文件。

說明:在extern “C”中包含頭文件, 會(huì)導(dǎo)致extern “C”嵌套, Visual Studio對extern “C”嵌套層次有限制,嵌套層次太多會(huì)編譯錯(cuò)誤。

在extern “C”中包含頭文件,可能會(huì)導(dǎo)致被包含頭文件的原有意圖遭到破壞。例如,存在a.h和b.h兩個(gè)頭文件:

使用C++預(yù)處理器展開b.h,將會(huì)得到

extern "C"

{

? ? void foo(int);

? ? void b();

}


按照a.h作者的本意,函數(shù)foo是一個(gè)C++自由函數(shù),其鏈接規(guī)范為”C++”。但在b.h中,由于#include “a.h”被放到了extern “C” { }的內(nèi)部,函數(shù)foo的鏈接規(guī)范被不正確地更改了。

示例: 錯(cuò)誤的使用方式:

extern “C”

{

? ? #include “xxx.h”

? ? ...

}

正確的使用方式:

#include “xxx.h”

extern “C”

{

? ? ...

}


建議1.1 一個(gè)模塊通常包含多個(gè).c文件,建議放在同一個(gè)目錄下,目錄名即為模塊名。為方便外部使用者,建議每一個(gè)模塊提供一個(gè).h,文件名為目錄名。

說明:需要注意的是,這個(gè).h并不是簡單的包含所有內(nèi)部的.h,它是為了模塊使用者的方便,對外整體提供的模塊接口。

以Google test(簡稱GTest)為例, GTest作為一個(gè)整體對外提供C++單元測試框架,其1.5版本的gtest工程下有6個(gè)源文件和12個(gè)頭文件。但是它對外只提供一個(gè)gtest.h,只要包含gtest.h即可使用GTest提供的所有對外提供的功能,使用者不必關(guān)系GTest內(nèi)部各個(gè)文件的關(guān)系,即使以后GTest的內(nèi)部實(shí)現(xiàn)改變了,比如把一個(gè)源文件c拆成兩個(gè)源文件,使用者也不必關(guān)心,甚至如果對外功能不變,連重新編譯都不需要。

對于有些模塊,其內(nèi)部功能相對松散,可能并不一定需要提供這個(gè).h,而是直接提供各個(gè)子模塊或者.c的頭文件。

比如產(chǎn)品普遍使用的VOS,作為一個(gè)大模塊,其內(nèi)部有很多子模塊,他們之間的關(guān)系相對比較松散,就不適合提供一個(gè)vos.h。而VOS的子模塊,如Memory(僅作舉例說明,與實(shí)際情況可能有所出入),其內(nèi)部實(shí)現(xiàn)高度內(nèi)聚,雖然其內(nèi)部實(shí)現(xiàn)可能有多個(gè).c和.h,但是對外只需要提供一個(gè)Memory.h聲明接口。

建議1.2 如果一個(gè)模塊包含多個(gè)子模塊,則建議每一個(gè)子模塊提供一個(gè)對外的.h,文件名為子模塊名。

說明:降低接口使用者的編寫難度。

建議1.3 頭文件不要使用非習(xí)慣用法的擴(kuò)展名,如.inc。

說明:目前很多產(chǎn)品中使用了.inc作為頭文件擴(kuò)展名,這不符合c語言的習(xí)慣用法。在使用.inc作為頭文件擴(kuò)展名的產(chǎn)品,習(xí)慣上用于標(biāo)識此頭文件為私有頭文件。但是從產(chǎn)品的實(shí)際代碼來看,這一條并沒有被遵守,一個(gè).inc文件被多個(gè).c包含比比皆是。本規(guī)范不提倡將私有定義單獨(dú)放在頭文件中,具體見 規(guī)則1.1。

除此之外,使用.inc還導(dǎo)致source insight、 Visual stduio等IDE工具無法識別其為頭文件,導(dǎo)致很多功能不可用,如“跳轉(zhuǎn)到變量定義處”。雖然可以通過配置,強(qiáng)迫IDE識別.inc為頭文件,但是有些軟件無法配置,如Visual Assist只能識別.h而無法通過配置識別.inc。

建議1.4 同一產(chǎn)品統(tǒng)一包含頭文件排列方式。

說明:常見的包含頭文件排列方式: 功能塊排序、文件名升序、穩(wěn)定度排序。

以穩(wěn)定度排序, 建議將不穩(wěn)定的頭文件放在前面,如把產(chǎn)品的頭文件放在平臺(tái)的頭文件前面,如下:

相對來說, product.h修改的較為頻繁,如果有錯(cuò)誤,不必編譯platform.h就可以發(fā)現(xiàn)product.h的錯(cuò)誤,可以部分減少編譯時(shí)間。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容