www.久久久久|狼友网站av天堂|精品国产无码a片|一级av色欲av|91在线播放视频|亚洲无码主播在线|国产精品草久在线|明星AV网站在线|污污内射久久一区|婷婷综合视频网站

當(dāng)前位置:首頁 > 公眾號精選 > C語言與CPP編程
[導(dǎo)讀]很多事不深入以為自己懂了,但真正用到項目上,才發(fā)現(xiàn)了問題。曾以為自己寫C語言已經(jīng)輕車熟路了,特別是對軟件文件的工程管理上,因為心里對自己的代碼編寫風(fēng)格還是有自信的。(畢竟剛畢業(yè)時老大對我最初的訓(xùn)練就是編碼格式的規(guī)范化處理)曾以為,一個.c文件對應(yīng)一個.h文件,.c文件只包含它自身...

很多事不深入以為自己懂了,但真正用到項目上,才發(fā)現(xiàn)了問題。曾以為自己寫C語言已經(jīng)輕車熟路了,特別是對軟件文件的工程管理上,因為心里對自己的代碼編寫風(fēng)格還是有自信的。(畢竟剛畢業(yè)時老大對我最初的訓(xùn)練就是編碼格式的規(guī)范化處理)

曾以為,一個 .c 文件對應(yīng)一個 .h 文件,.c 文件只包含它自身的 .h 文件就好,若 .c 文件中用到其他文件中的內(nèi)容,則 .h 文件把用到的頭文件包含進來就可以了。

自己貌似一直秉承這個理念在進行代碼編寫(好可怕)。工程文件數(shù)量小時,這種理念貌似看不出問題,但隨著工程文件數(shù)量越來越多,我發(fā)現(xiàn)自己這種思路有了弊端:頭文件互相包含,導(dǎo)致編譯時自以為有些宏變量聲明了,它就能起作用,但實際測試發(fā)現(xiàn)這種方式編碼后,有些聲明的宏沒能起到作用。

經(jīng)過領(lǐng)導(dǎo)及同事的指正,自己才明白原有的代碼編寫習(xí)慣不正確。應(yīng)該秉承 .c 文件對應(yīng)的 .h 文件只包含頭文件里用到的其它文件的頭文件,任何非必須的 .h 文件不要包含;而 .c 文件里面要包含用到的所有 .h 文件。這樣寫即使存在 .c 文件內(nèi)頭文件重復(fù)包含也不傷大雅。

語言描述有時太抽象,還是符號舉例說明下:假如有兩個 .c 文件分別為 A.c 和 B.c,自然它們都有各自的 A.h 和 B.h 文件。

原有的思路:

A.c 里面只有一個#include "A.h",而 A.h 所包含的就是一大堆如 B.h,C.h,D.h..... 文件,因為 A.c 文件里面要用到 B.h,C.h,D.h 里面的內(nèi)容。如下圖所示。

新思路:

A.h 里面只包含 A.h 所寫內(nèi)容要用到的 .h 文件,很多時候 A.h 里面無需任何 .h 文件,而在 A.c 文件內(nèi)就要寫成 ?#include "B.h" ?#include "C.h" ?#include "D.h"。而且兩個文件的 .c 文件在頭文件包含上可以互相包含。如下圖所示。

項目中遇到的這個頭文件包含問題導(dǎo)致我重新搜索資料進行該問題的深入了解,故下文是通過網(wǎng)絡(luò)資源的搜查及加上自己對它的理解,進行了相關(guān)內(nèi)容的整理,希望對感興趣的小伙伴有所幫助。

背景

對于 C 語言來說,頭文件的設(shè)計體現(xiàn)了大部分的系統(tǒng)設(shè)計。不合理的頭文件布局是編譯時間過長的根因,不合理的頭文件實際上不合理的設(shè)計。

依賴

特指編譯依賴。若 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ù)個,甚至是一個文件。

某產(chǎn)品曾經(jīng)做過一個實驗,把所有函數(shù)的實現(xiàn)通過工具注釋掉,其編譯時間只減少了不到 10%,究其原因,在于 A 包含 B, B 包含 C, C 包含 D,最終幾乎每一個源文件都包含了項目組所有的頭文件,從而導(dǎo)致絕大部分編譯時間都花在解析頭文件上。

某產(chǎn)品更有一個“優(yōu)秀實踐”,用于將 .c 文件通過工具合并成一個比較大的 .c 文件,從而大幅度提高編譯效率。

其根本原因還是在于通過合并 .c 文件減少了頭文件解析次數(shù)。但是,這樣的“優(yōu)秀實踐”是對合理劃分 .c 文件的一種破壞。

大部分產(chǎn)品修改一處代碼,都得需要編譯整個工程,對于 TDD 之類的實踐,要求對于模塊級別的編譯時間控制在秒級,即使使用分布式編譯也難以實現(xiàn),最終仍然需要合理的劃分頭文件、以及頭文件之間的包含關(guān)系, 從根本上降低編譯時間。

《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:頭文件中適合放置接口的聲明,不適合放置實現(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)部實現(xiàn)細(xì)節(jié),不應(yīng)通過在頭文件中聲明的方式直接暴露給外部,應(yīng)通過函數(shù)接口的方式進行對外暴露。
延伸閱讀材料:《 C語言接口與實現(xiàn)》

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

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

某個頭文件不但定義了基本數(shù)據(jù)類型 WORD,還包含了stdio.h syslib.h等等不常用的頭文件。

如果工程中有 10000 個源文件,而其中 100 個源文件使用了stdio.h的 printf,由于上述頭文件的職責(zé)過于龐大,而 WORD 又是每一個文件必須包含的,從而導(dǎo)致stdio.h/syslib.h等可能被不必要的展開了 9900 次,大大增加了工程的編譯時間。

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

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

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

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

延伸閱讀材料:編者推薦開發(fā)人員使用“依賴倒置”原則,即由使用者制定接口,服務(wù)提供者實現(xiàn)接口,更具體的描述可以參見《 敏捷軟件開發(fā):原則、模式與實踐》 ( Robert C.Martin 著 鄧輝 譯 清華大學(xué)出版社 2003 年 9 月) 的第二部分“敏捷設(shè)計”章節(jié)。

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

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

現(xiàn)有某些產(chǎn)品中,習(xí)慣一個 .c 文件對應(yīng)兩個頭文件,一個用于存放對外公開的接口,一個用于存放內(nèi)部需要用到的定義、聲明等,以控制 .c 文件的代碼行數(shù)。編者不提倡這種風(fēng)格。

這種風(fēng)格的根源在于源文件過大,應(yīng)首先考慮拆分 .c 文件,使之不至于太大。另外,一旦把私有定義、聲明放到獨立的頭文件中,就無法從技術(shù)上避免別人 include 之,難以保證這些定義最后真的只是私有的。

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

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

void?foo()
{
?bar();
}

void?bar()
{
?Do?something;
}
必須在 foo 之前聲明 bar,否則會導(dǎo)致編譯錯誤。

這一類的函數(shù)聲明,應(yīng)當(dāng)在 .c 的頭部聲明,并聲明為 static 的,如下:

static?void?bar();

void?foo()
{
?bar();
}

void?bar()
{
?Do?something;
}

規(guī)則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ī)則3:.c/.h 文件禁止包含用不到的頭文件。

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

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

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

示例:如果 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ī)則5:總是編寫內(nèi)部 #include 保護符( #define 保護)。

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

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

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

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

定義包含保護符時,應(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ī)則6:禁止在頭文件中定義變量。

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

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

說明:若 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 改變時可能導(dǎo)致聲明和定義不一致 [a4] 。

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

說明:在extern “C”中包含頭文件, 會導(dǎo)致extern “C”嵌套, Visual Studioextern “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:一個模塊通常包含多個 .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 聲明接口。

建議2:如果一個模塊包含多個子模塊,則建議每一個子模塊提供一個對外的 .h,文件名為子模塊名。

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

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

說明:目前很多產(chǎn)品中使用了 .inc 作為頭文件擴展名,這不符合 c語言的習(xí)慣用法。在使用 .inc 作為頭文件擴展名的產(chǎn)品,習(xí)慣上用于標(biāo)識此頭文件為私有頭文件。

但是從產(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。

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

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

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

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

[a1] 例如一些屏驅(qū)動的地址文件,一些協(xié)議的格式定義文件.只存在 .c 或者 .h 即可,不一定兩者都要有。

[a2] 我對自包含沒有太理解,只是明白在 .h 文件里盡量不包含沒有必要的頭文件,某些情況下不得已才進行包含其它頭文件的操作。

[a3] 這種做法我寫代碼常用,但后面應(yīng)該盡量避免,而是通過調(diào)用頭文件的方式來使用該函數(shù)。

[a4] 對,我就遇到過。因為隨著工程量的增大,后面某個細(xì)節(jié)調(diào)整了 foo 函數(shù),但其它 extern 調(diào)用它的地方?jīng)]有及時改正,而 KEIL 編譯器又沒有報錯,導(dǎo)致 bug 出現(xiàn),而且不易查找。

文章鏈接:https://blog.csdn.net/fengcq126/article/details/103016917

本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
關(guān)閉
關(guān)閉