為什麼C/C++的預處理指令#include不自動讓所包含的文件只包含一次?

在C/C++中#include所包含的頭文件裡面必須顯式聲明

#ifndef __HEADER_H_NAME__
#define __HEADER_H_NAME__

#endif

#pragma once

來保證頭文件不會被重複包含兩次以上。

但是為何不直接讓#include指令實現這一需求?難道有需要重複導入兩次同一文件的情況?


因為可以這樣實現template:

#define LIST_TYPE IntList

#define LIST_ELEMENT_TYPE int

#include "List.h"

#undef LIST_ELEMENT_TYPE

#undef LIST_TYPE

#define LIST_TYPE DoubleList

#define LIST_ELEMENT_TYPE double

#include "List.h"

#undef LIST_ELEMENT_TYPE

#undef LIST_TYPE

int i, sum;

IntList* list;

list = IntList_Create();

for(i=0;i&<10;i++)

IntList_Add(list, i);

sum=0;

for(i=0;i&<10;i++)

sum+=IntList_Get(list, i);

IntList_Destroy(list);


yu xia 說的對.

舉個例子, 你需要在多個函數中使用一個點陣字, 而又不想把它放到全局變數那裡, 假設這個點陣字是這樣的

// 漢

0x20,0x00,

0x10,0x00,

0x17,0xFC,

0x02,0x08,

0x82,0x08,

0x49,0x10,

0x49,0x10,

0x11,0x10,

0x10,0xA0,

0x20,0xA0,

0xE0,0x40,

0x20,0xA0,

0x21,0x18,

0x26,0x0E,

0x28,0x04,

0x00,0x00,

可以把上面這個點陣字保存在 han.data 裡面, 在需要使用這個點陣字的函數裡面, 這樣使用:

char han[] = {

#include "han.data"

};

這樣, 你在一個文件裡面就可能需要多次 include 這個 "han.data" 文件.

要是你讓預處理器自動處理使得一個文件只包含另一個文件一次, 喔哦~ 為什麼要那樣做呢?


我又來務虛了,內行注意躲避。

  • 原因一:

C/C++ 不由商業公司主導,在語言層面(我把預編譯也算進去了)都是比較保守的語言,基本上秉承著「如無必要勿增實體」的原則。也就是說能用技巧實現的,盡量不要做成語言特性。相比一些工程針對性更強的語言,C/C++ 傾向於假設使用者:

  • 理解力特別好,記憶力特別差。
  • 特別不容易犯錯誤。

你如果站在現在的角度看 C/C++,其實更像是一個「半成品」語言,各種庫和技巧的使用,某種程度上是對語言的「重建」(回憶一下各種 C 語言實現面向對象編程的技巧,以及 Boost 裡面各種不可理喻的模版庫)。

這樣做的好處嘛,自然是有的,保持語言最小化的同時,保留最大的靈活性。比如你說例子,假如將「include一次」做進預處理器里,那我就非得想 include 兩次(例子請參見「原因二」),怎麼辦呢?如果保留這種變通性同時把功能做進預處理器里,可能就得實現成這樣:

#include_once &
#include &

我們在這裡引入了一個新語句——include_once(#pragma once 並不存在於標準中),從日常使用的角度講,可能這樣是有用的,反對者的理由,往往是:

  • 「破壞了語言的原有架構(為什麼說是破壞,請參見下面的「原因二」以及@昝東 的答案)」。
  • 「增加了學習成本」。

  • 「增加了複雜性」。

  • 「就算要增加,也得是 include_no_more_than(N)」。

  • ……

而實際上——我個人的看法——這都是表面,真正的原因是「盡量不要為了使用方便增加語言特性」乃是這兩門語言發明者及社區的信仰(並不是說他們缺乏智慧)。保持穩定、少升級、強調兼容、保持語言最小化,在工程上當然是有價值的,但是你問 10 個 C/C++ 使用者,我猜至少有 9 個會不假思索否定你的提議,他們都仔細思考過工程上的利弊嗎?未必,瞬間否定你的原因其實是,這樣不優雅,不酷,不美。

你要知道這兩門語言背後的力量是偏學術的,不是商業公司做一個盡量好用強大的產品讓你掏錢,而是做盡量少但是最盡量牛的工作,驅動最大的可能性。你看 C++ 11 的升級,加入的凈是些 λ 表達式、右值引用這類增加語言可能性的東西。

  • 原因二:

其實不能叫原因二,應該叫第二個角度。include 多次的情況@port gle 已經舉例了,從這個例子你能看到:include 這條命令的語義並不是「引入一個頭文件」,而是告訴預編譯器「在我出現的地方,插入我要求的文本」,至於插入的是什麼,預編譯器並不關心——預編譯器是不理解語言的,C/C++ 的預編譯宏,被設計成和 C/C++ 本身完全無關。而「頭文件」,也並不是「聲明或定義了一些東西的文件」,而僅僅就是一個文件而已。

如果你願意的話,你可以這麼用頭文件:

namespace_xxx.h:

#ifdef USE_NAMESPACE
#if !defined(IN_NAMESPACE_XXX)
#define IN_NAMESPACE_XXX
namespace xxx {
#else
#undef IN_NAMESPACE_XXX
}
#endif
#endif

some.cpp:

#include "namespace_xxx.h"
class some {
// ...
};
#include "namespace_xxx.h"

我們不知道自己什麼時候需要這種寫法,但我們也不知道我們永遠用不到它。


因為當初內存昂貴,設備通常內存不大,c語言編譯器設計成按每次編譯只處理一個源文件,等所有文件都是編譯成目標文件之後再鏈接。所以編譯無法一次性預先讀入所有源文件及頭文件,它就無法主動分析出哪些頭文件是重複的。後來的c#或java都採用一次預讀待編譯的所有文件,所以可以做到自動避免重複import.。當然它們沒有採用聲明與定義分離,帶頭文件的結構,這也逼著它們必須能夠解決這個問題。


有需要重複include同一個文件多次的情況,完全取決於你的需要。簡單的說,為了實現只include一個文件一次,你增加了預處理的實現邏輯反而減少了功能,為什麼要這麼做呢?


#include 沒任何問題, 問題的是沒有 #import, 你只能寫守衛了. 慢慢爽吧.


原因只有一個,在make file執行之前,沒有辦法確定依賴關係是否正確。


目前include的功能很簡單,就是在該位置將文件替換展開。如果要實現include_once需要做什麼呢?需要維護一個列表,每展開一個新文件,就要去查一遍這個列表。這對於預處理器來說有點太難了——預處理命令都是一些很弱的功能,做做簡單的文本替換,它們本來就是被設計成了不理解程序更不理解工程的,否則會對語言本身形成干涉。(——當然後來大家大量用宏替換來實現功能,我只能說呵呵了。)


堅定不移地用#import

讓他喵的所有版本都支持,

然後就不用考慮這個問題了


include僅僅是將文件展開,最初的設計想法估計沒人知道,不過可以include多次在有些情況是很方便的,比如下面這個代碼:

http://git.savannah.gnu.org/cgit/lwip.git/tree/src/include/lwip/memp_std.h

http://git.savannah.gnu.org/cgit/lwip.git/tree/src/core/memp.c

lwip將所有內存池定義放在了memp_std.h里,然後在memp.c里多次包含此頭文件,方便針對不同配置做不同的定義。這種寫法是大大提高了代碼維護性。例如:

#if MEMP_SEPARATE_POOLS

/** This creates each memory pool. These are named memp_memory_XXX_base (where
* XXX is the name of the pool defined in memp_std.h).
* To relocate a pool, declare it as extern in cc.h. Example for GCC:
* extern u8_t __attribute__((section(".onchip_mem"))) memp_memory_UDP_PCB_base[];
*/
#define LWIP_MEMPOOL(name,num,size,desc) u8_t memp_memory_ ## name ## _base
[((num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size)))];
#include "lwip/memp_std.h"

/** This array holds the base of each memory pool. */
static u8_t *const memp_bases[] = {
#define LWIP_MEMPOOL(name,num,size,desc) memp_memory_ ## name ## _base,
#include "lwip/memp_std.h"
};

#else /* MEMP_SEPARATE_POOLS */

/** This is the actual memory used by the pools (all pools in one big block). */
static u8_t memp_memory[MEM_ALIGNMENT - 1
#define LWIP_MEMPOOL(name,num,size,desc) + ( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ) )
#include "lwip/memp_std.h"
];

#endif /* MEMP_SEPARATE_POOLS */

memp_std.h所有的對內存池的定義都放在了相關配置宏里並以

LWIP_MEMPOOL(name,num,size,desc)

的格式定義。文件末尾有#undef LWIP_MEMPOOL ,所以每次include之前都會有個不同的#define LWIP_MEMPOOL


因為編譯的時候實際上只是將#include語句直接替換成文件的內容,並沒有做其他處理,C/C艹都是這樣


歷史遺留問題。

可以認為include的語句被替換成對應文件里的內容了


推薦閱讀:

gcc編譯大文件非常慢,是有什麼限制嗎?
為何C++/Rust都不允許靜態函數是虛的?
如何優化一個讀取命令並執行的程序?
C++的完美轉發只能針對形如T &&的形參嗎?
現在C++開發是不是都遵守C++11標準,Linux下的多線程編程是優先考慮C++11的線程庫,還是用系統線程API封裝?

TAG:編程語言 | 計算機 | C編程語言 | C | CC |