1. 程式人生 > >深入淺出 C++:#include Directive PART 1

深入淺出 C++:#include Directive PART 1

除了基本語法外,使用 C++ 提供的標準庫、型別定義等,都需要使用 #include 引入 header file,寫法如下:

#include <iostream>
#include <vector>
#include <string>

#include 在 C++ 屬於 preprocessing directive,他不算是程式執行指令的一部分,其功能是對 compile 過程、第一步的 preprocessor 下指令。當 preprocessor 看到 #include,他會將該行置換為 #include 所欲包含的檔案內容。若該檔案內又有 #include,則會層層展開,引入的檔案越多,程式碼越大,compile 的時間就越長。

為何需要 Header File

為了讓程式更模組化、提高可讀性、加強複用性等,除了單一函式程式碼不要太長、通常也會避免單一檔案的程式碼太多。將一個大功能,拆成好幾個檔案,每個檔案的取名就能看出是對應其中的哪個子功能,這是常規操作。

為了使用其他 cpp 檔案裡定義的函式,在使用之前,必須先宣告他的存在。下面範例中,main.cpp 宣告了 Factorial() 與 SuperFactorial() 兩個函式,而他們的程式碼,實際上是寫在 factorial.cpp:

// main.cpp
int Factorial(int n);      // 函式宣告
int SuperFactorial(int
n); // 函式宣告 int main() { // 使用 Factorial( 與 SuperFactorial() }
// factorial.cpp
int Factorial(int n)
{
  // 程式碼實現
}

int SuperFactorial(int n)
{
  // 程式碼實現
}

將兩個 cpp 檔案,compile 成一個可執行檔案的指令如下:

[email protected]:~/cpp/c2$ clang++ -std=c++17 -stdlib=libc++ --pedantic-errors -o factorial main.cpp factorial.cpp

C++ compiler 首先根據每個 cpp file,轉換成平臺對應的指令、進行優化,產生 object file。當 compile main.cpp 時,看到了兩個函式的宣告、但沒找到定義,先以 undefined symbol 來表示他們。當兩個 cpp file 各自產生 object file 後,linker 會將其合成最後的可執行檔案,過程中就會嘗試尋找 undefined symbol 定義在哪個 object file,如果找到了,就將其替換為實際函式的地址,這樣執行時,就能呼叫到該函式。

透過這種將程式碼分類到不同 cpp 的方式,可以提升模組化,通用的程式碼就不用每個 cpp 都重寫一次,增加程式碼體積、造成維護困難。然而,如果我們現在開發較大型的程式,可複用的函式一定很多,難不成每個 cpp 檔案,都得寫一大串的函式宣告?這想想也挺麻煩的,如果這些大串的函式宣告,也能寫一次、共用到各個 cpp 檔案,豈不美哉?有鑑於此,C/C++ 引入了 header file 的概念,用以存放共通的函式、型別宣告。

針對上面範例,一般是寫一個如下的 header file:

#ifndef FACTORIAL_EXAMPLE_H
#define FACTORIAL_EXAMPLE_H

int Factorial(int n);
int SuperFactorial(int n);

#endif

然後 main.cpp 裡只要 #include 它就行了。

Include Guard

開發大型程式時,由於 #include 檔案相當多,且 header file 也可以再 #include 其他 header file,層層展開下,不免導致程式碼重複。函式宣告重複出現就算了,型別宣告可不能重複。為了更細緻的理解,我們用下面程式碼實驗,假設他們就是多層 #include,被 preprocess 展開的結果:

class MyClass { /* ... */ };
class MyClass { /* ... */ };
enum MyEnum { /* ... */ };
enum MyEnum { /* ... */ };
typedef int size;
typedef int size;
typedef float length;
typedef double length;


#define SIZE(m) m->size
#define SIZE(m) m->size
#define SIZE(m) m->computeSize()

int function1();
int function1();
int function1(int parameter);
int function1(double parameter);

int main()
{
}

從下面的報錯,可看到,class、enum 重複定義其內容是不允許的。而 typedef、#define 只要內容還是相同,可以允許重複定義,但若內容不同,還是會報錯。同樣的函式宣告,是可以重複定義的,即使引數變了也允許,因為 C++ 有 function overloading 機制,函式名稱相同、但引數不同,compiler 會視為不同的函式。

[email protected]:~/cpp/c3$ clang++ -std=c++17 -stdlib=libc++ --pedantic-errors -o test test.cpp
test.cpp:2:7: error: redefinition of 'MyClass'
class MyClass { /* ... */ };
      ^
test.cpp:1:7: note: previous definition is here
class MyClass { /* ... */ };
      ^
test.cpp:4:6: error: redefinition of 'MyEnum'
enum MyEnum { /* ... */ };
     ^
test.cpp:3:6: note: previous definition is here
enum MyEnum { /* ... */ };
     ^
test.cpp:8:16: error: typedef redefinition with different types
      ('double' vs 'float')
typedef double length;
               ^
test.cpp:7:15: note: previous definition is here
typedef float length;
              ^
test.cpp:13:9: error: 'SIZE' macro redefined [-Werror,-Wmacro-redefined]
\#define SIZE(m) m->computeSize()
        ^
test.cpp:12:9: note: previous definition is here
\#define SIZE(m) m->size
        ^
4 errors generated.

為解決大型程式可能遇到的、在多個 #include 內容導致重複定義的問題,常用的技巧是在 header file 的開始與結束加入下面 #ifdef、#define、#endif,第一次引入這個 header file,就 #define 了 FACTORIAL_EXAMPLE_H 這個 macro,這樣在後續重複引入時,preprocessor 發現 FACTORIAL_EXAMPLE_H 已經被定義過了,所以就不再展開 #ifndef 到 #endif 之間的部分了。此技巧是 C/C++ 的常規操作,稱為 include guard

#ifndef FACTORIAL_EXAMPLE_H
#define FACTORIAL_EXAMPLE_H

int Factorial(int n);
int SuperFactorial(int n);

#endif

Include guard 定義 macro 名稱時,最好選擇有點複雜度的名字,例如內容包含公司、專案等名稱,在大型程式引用多種庫時,才不至於跟其他的 header file 使用同樣的 macro 名稱,導致該展開的程式碼沒展開。

此外,有些程式設計師會在 include guard 的 macro,選擇單底線或雙底線開頭的名字,例如:

#ifndef __LIBCPP_ISTREAM
#define __LIBCPP_ISTREAM
// ...
#endif

筆者第一次看到這種寫法時,覺得很特別,有段時間 include guard 就這麼依樣畫葫蘆耍帥,後來才知道這不是允許的寫法,只是 compiler 沒檢查這項。C++ 標準規定,雙底線開頭、或單底線開頭加上首字母大寫的取名方式,是保留給各平臺實現 C++ 標註庫用的,不是給一般應用開發者用的。咱們看下原文吧,這個規則到目前 C++17 仍是成立:

— Each identifier that contains a double underscore __ or begins with an underscore followed by an
uppercase letter is reserved to the implementation for any use.

— Each identifier that begins with an underscore is reserved to the implementation for use as a name in
the global namespace.

Preprocessor 如何處理 #include directive

前面提到,preprocessor 無非就是將引入的內容展開。我們以下面的程式來說明。在這個例子中,factorial.h include 了 BigNumber.h,而 main.cpp 除了 include factorial.h,又將 BigNumber.h 引入了一次,這是常見的情況,引入別的庫的 header file,通常不會管他裡面已經引入了什麼:

// BigNumber.h
#ifndef BIG_NUMBER_H
#define BIG_NUMBER_H

class BigNumber final
{
public:
  BigNumber();
  BigNumber(const BigNumber& rhs);
  ~BigNumber();

  BigNumber& operator = (const BigNumber& rhs);
private:
  char* value_;
};

BigNumber operator + (const BigNumber& lhs, const BigNumber& rhs);
BigNumber operator - (const BigNumber& lhs, const BigNumber& rhs);
BigNumber operator * (const BigNumber& lhs, const BigNumber& rhs);
#endif
// factorial.h
#ifndef FACTORIAL_H
#define FACTORIAL_H

#include "BigNumber.h"

BigNumber Factorial(int n);
BigNumber SuperFactorial(int n);

#endif
#include "factorial.h"
#include "BigNumber.h"

int main()
{
}

我們可以用 clang 的 -E 選項,只執行 preprocessor,就能觀察如何展開了:

[email protected]:~/cpp/c3$ clang++ -std=c++17 -E -o factorial.txt main.cpp

本例中,產生的 factorial.txt 內容如下:

# 1 "main.cpp"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 402 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.cpp" 2
# 1 "./factorial.h" 1



# 1 "./BigNumber.h" 1



class BigNumber final
{
public:
  BigNumber();
  BigNumber(const BigNumber& rhs);
  ~BigNumber();

  BigNumber& operator = (const BigNumber& rhs);
private:
  char* value_;
};

BigNumber operator + (const BigNumber& lhs, const BigNumber& rhs);
BigNumber operator - (const BigNumber& lhs, const BigNumber& rhs);
BigNumber operator * (const BigNumber& lhs, const BigNumber& rhs);
# 5 "./factorial.h" 2

BigNumber Factorial(int n);
BigNumber SuperFactorial(int n);
# 2 "main.cpp" 2


int main()
{
}

__has_include (C++17 新增)

開發大型程式時,可能會需要切換不同的庫引入、也可能需要引入不同版本,此時就需要檢查 header file 是否存在。C++ 17 引入了 __has_include 語法,協助在 macro 中判斷,格式如下:

__has_include {"filename"}
__has_include {<filename>}

以下是 C++ 標準給出的使用範例:

#if __has_include(<optional>)
  # include <optional>
  # define have_optional 1
#elif __has_include(<experimental/optional>)
  # include <experimental/optional>
  # define have_optional 1
  # define experimental_optional 1
#else
  # define have_optional 0
#endif

__has_include 就只是檢查檔案存不存在,並不檢查是否引入後有任何語法或 compile 錯誤。

#pragma once

許多 C++ compiler,如 GCC、Clang、Microsoft Visual C++、Intel C++ Compiler、Comeau C/C++ 等,都提供這個非標準、但廣泛使用的 directive,一般寫在 header file 第一行,讓這個 header file 在一個 object file 中只引入一次,目的類似 include guard:

// factorial.h
#pragma once

int Factorial(int n);
int SuperFactorial(int n);

而原理不同之處,在於 #pragma once 是直接比對檔案路徑來避免重複引入;include guard 則是檢查 macro 是否重複定義,來決定是否引入程式碼。

但 #pragma once 使用上也是有風險的。在開發大型程式時,有可能檔案因為使用 symbolic link、hard link 等機制,導致看似不同路徑、不同名稱的檔案,實際上是同一份內容,這對 compiler 的實現是一種挑戰。

#pragma once 畢竟不是標準支援的寫法。如果考慮到程式碼需跨平臺允許,還是乖乖使用 include guard 為佳。

相關推薦

深入淺出 C++#include Directive PART 1

除了基本語法外,使用 C++ 提供的標準庫、型別定義等,都需要使用 #include 引入 header file,寫法如下: #include <iostream> #include <vector> #include <s

深入淺出 C++與程式終止相關的函式 PART 1

C/C++ 程式,一般是藉由 main() 的返回值呼叫 exit() 函式以正常結束程式。除了程式崩潰、或使用者強制結束程式外,C++ 亦提供數個函式,允許呼叫以立即終止程式,本文將一一介紹這些函式。 不過,在進入主題前,需提醒讀者:撰寫程式時,儘可能使程式

深入淺出 C++與程式終止相關的函式 PART 3

Markdown 編輯器真是不好用,這個文章裡,好幾個程式輸出的地方,# 開頭的都被識別成標題了。如果在 # 前面加上 \,看起來似乎能解決,但好幾行一改,又變成能在文章內看到 \ # 開頭了。哎,試了半個小時,懶得再試了,客官們擔待些,反正對理解正文沒影響便是

深入淺出 C++與程式終止相關的函式 PART 2

quick_exit() 與 at_quick_exit() (C++11新增) [[noreturn]] void quick_exit(int status) noexcept; quick_exit() 為 C++11 引入的函式,如果程式有特殊理

C++構造函數1——普通構造函數

創建 c++編譯 clu namespace 我們 這一 () 一次 ret 前言:構造函數是C+中很重要的一個概念,這裏對其知識進行一個簡單的總結 一、構造函數的定義 1.類中的構造函數名與類名必須相同 2.構造函數沒有函數的返回類值型說明符 [特別註意]: a.構造函數

C#執行緒(1什麼是執行緒?我們為什麼要使用執行緒?

最近在看公司上一個專案的原始碼,讓我感覺非常困惑的是,原始碼中使用了很多多執行緒的內容,所以給我的感覺是執行緒一直跳來跳去的,讓我感覺到很困惑。於是我就寫了這篇部落格,希望能夠更好的理解執行緒有關的內容。 一:什麼是執行緒 執行緒是和程序經常放在一起比較的兩個概念。按照我的理解,執行緒和程序

深入淺出 C++main()

main() 是 C/C++ 程式執行的進入點,作業系統執行程式時,首先會執行 Runtime Library 內的函式進行必要的初始化,接著才呼叫 main() 轉移控制權,當 main() 返回時,再根據 main() 的返回值呼叫 exit() 結束程式。

C語言代碼編程題匯總顯示表達式1*2+3*4+...+99*100的表示形式(采取交互的形式)

stdio.h tdi input 字符型 6.0 tro vc++6.0 text class 顯示表達式1*2+3*4+...+99*100的表示形式(采取交互的形式) 程序源代碼如下: 1 /* 2 2017年6月8日08:03:38 3 功能

C語言代碼編程題匯總顯示表達式1*2+3*4+...+9*10的表示形式

clas ron urn ++ class align int c語言代碼 程序 顯示表達式1*2+3*4+...+9*10的表示形式 源程序代碼如下: 1 /* 2 2017年6月7日22:54:51 3 功能:實現1*2+3*4+...+9*10

程序發布出現 服務器無法處理請求--->無法生成臨時類(result = 1)。 錯誤CS2001未能找到源文件“C Windows TEMP lph54vwf.0.cs”

win 臨時 生成 color 無法 添加 權限 web windows 服務器上發布的web服務程序出錯: 服務器無法處理請求--->無法生成臨時類(result = 1)。錯誤CS2001:未能找到源文件“C:\ Windows \ TEMP \ l

C++ and OO Num. Comp. Sci. Eng. - Part 1.

nim num 內容 general -o 編譯時間 增加 radi gpo 本文參考自 《C++ and Object-Oriented Numeric Computing for Scientists and Engineers》。 序言 書中主要討論的問題是面向對象的

2017.2.8-9 “PL part COOP”

。。 eth 發現 就是 moment 成了 program blog log 雖然以前在python中也接觸過OOP,但是不系統,而且自己寫python肯定也是不會寫成OOP風格的。 現在相對系統的學習OOP的概念,感覺。。。很難受! 有點像一開始學ML時候的感覺,就是接

持續集成與持續部署寶典Part 1將構建環境容器化

成熟 curl命令 設置 doc 包括 探討 完成 2.7 mage 介 紹隨著Docker項目及其相關生態系統逐漸成熟,容器已經開始被更多企業用在了更大規模的項目中。因此,我們需要一套連貫的工作流程和流水線來簡化大規模項目的部署。在本指南中,我們將從代碼開發、持續集成

Django 教程 Part 1請求與響應

arm pattern 處理 指導 一個 接收 網絡通信 生成 star 版本說明: 因為在撰寫本教程的時候,正逢Django從1.11向2.0轉變的時期,而教程的編寫是從17年8月開始的,前後共花了5個月左右的時間,所以使用的是1.11版本,局面非常尷尬。 實際上Djan

安卓Could not read cache value from'C:\Users\Username\.gradle\daemon\1.12\registry.bin'

android studio在載入專案的時候報錯: Error:Could not read cache value from'C:\Users\Username\.gradle\daemon\1.12\registry.bin' 參考stack overflow上的一個解決方法,刪除

C#執行緒系列講座(1)BeginInvoke和EndInvoke方法

開發語言:C#3.0 IDE:Visual Studio 2008 本系列教程主要包括如下內容:1.  BeginInvoke和EndInvoke方法 2.  Thread類 3. 執行緒池 4. 執行緒同步基礎 5. 死鎖 6. 執行

PAT1005 繼續(3n+1)猜想(25 分)C語言

PAT 1005 繼續(3n+1)猜想(25 分) C語言 #include<stdio.h> int main() { int n; scanf("%d", &n); //輸入n個整數 int zs[n]; for(in

(1) C++過載、覆蓋與隱藏

  C++之中的過載、覆蓋、隱藏   過載 覆蓋 過載與覆蓋的區別 相關程式碼 隱藏   過載 過載是指函式不同的引數表,對同名函式的名稱做修飾,然後這些同名函式就成了不同的函式。在同一可訪問區域內被宣告的幾

C++ 錯誤提示無法將引數1從const char [8] 轉換為char *

#include <iostream> using namespace std; void test(char * p) { cout << p << endl; } int main(void) { test("geerniya")

深入淺出etcd系列Part 1 – etcd架構和程式碼框架

1、緒論 etcd作為華為雲PaaS的核心部件,實現了PaaS大多陣列件的資料持久化、叢集選舉、狀態同步等功能。如此重要的一個部件,我們只有深入地理解其架構設計和內部工作機制,才能更好地學習華為雲Kubernetes容器技術,笑傲雲原生的“江湖”。本系列將從整體框架再細化到內部流程,對etcd的程式碼和設計