C++防止標頭檔案被重複引入的3種方法(詳解版)

2020-07-16 10:05:23
《防止C語言標頭檔案被重複包含》一節中,詳細介紹了 C 語言中如何使用宏定義(#ifndef / #define / #endif)來有效避免標頭檔案被重複 #include,此方式在 C++ 多檔案程式設計中也很常用。

舉個例子,如下是一個 C++ 專案,其內部含有 school.h 和 student.h 這 2 個標頭檔案以及 main.cpp 原始檔,其各自包含的程式碼為:
//student.h
class Student {
    //......
};

//school.h
#include "student.h"
class School {
    //......
private:
    Student stu[50];
};

//main.cpp
#include "student.h"
#include "school.h"
int main() {
    //......
    return 0;
}
執行此專案讀者會發現,編譯器報“Student 型別重定義”錯誤。這是因為在 school.h 檔案中已經 #include 了一次 "student.h",而在 main.cpp 主程式又同時 #include 了 "school.h" 和 "student.h",即 Student 類的定義被引入了 2 次,C++不允許同一個類被重複定義。

有讀者可能想到,既然 School.h 檔案中已經引入了 Student 類,那去掉 main.cpp 主程式引入的 student.h 檔案不就可以了嗎?這樣確實可以避免重複引入 Student 類,但此方式並不適用於所有“重複引入”的場景。

C++ 多檔案程式設計中,處理“多次 #include 導致重複引入”問題的方式有以下 3 種。

1) 使用宏定義避免重複引入

在實際多檔案開發中,我們往往使用如下的宏定義來避免發生重複引入:
#ifndef _NAME_H
#define _NAME_H
//標頭檔案內容
#endif
其中,_NAME_H 是宏的名稱。需要注意的是,這裡設定的宏名必須是獨一無二的,不要和專案中其他宏的名稱相同。

當程式中第一次 #include 該檔案時,由於 _NAME_H 尚未定義,所以會定義 _NAME_H 並執行“標頭檔案內容”部分的程式碼;當發生多次 #include 時,因為前面已經定義了 _NAME_H,所以不會再重複執行“標頭檔案內容”部分的程式碼。

也就是說,我們可以將前面專案中的 student.h 檔案做如下修改:
#ifndef _STUDENT_H
#define _STUDENT_H
class Student {
    //......
};
#endif
雖然該專案 main.cpp 檔案中仍 #include 了 2 次 "student.h",但鑑於 _STUDENT_H 宏只能定義一次,所以 Student 類也僅會定義一次。再次執行該專案會發現,其可以正常執行。

2) 使用#pragma once避免重複引入

除了前面第一種最常用的方式之外,還可以使用 #pragma one 指令,將其附加到指定檔案的最開頭位置,則該檔案就只會被 #include 一次。

我們知道,#ifndef 是通過定義獨一無二的宏來避免重複引入的,這意味著每次引入標頭檔案都要進行識別,所以效率不高。但考慮到 C 和 C++ 都支援宏定義,所以專案中使用 #ifndef 規避可能出現的“標頭檔案重複引入”問題,不會影響專案的可移植性。

和 ifndef 相比,#pragma once 不涉及宏定義,當編譯器遇到它時就會立刻知道當前檔案只引入一次,所以效率很高。但值得一提的是,並不是每個版本的編譯器都能識別 #pragma once 指令,一些較老版本的編譯器就不支援該指令(執行時會發出警告,但編譯會繼續進行),即 #pragma once 指令的相容性不是很好。

目前,幾乎所有常見的編譯器都支援 #pragma once 指令,甚至於 Visual Studio 2017 新建標頭檔案時就會自帶該指令。可以這麼說,在 C/C++ 中,#pragma once 是一個非標準但卻逐漸被很多編譯器支援的指令。

除此之外,#pragma once 只能作用於某個具體的檔案,而無法向 #ifndef 那樣僅作用於指定的一段程式碼。

這裡仍以前面的 "student.h" 檔案為例,將其內容修改為:
#pragma once
class Student {
    //......
};
再次執行專案,同樣可以正常執行。

3) 使用_Pragma操作符

C99 標準中新增加了一個和 #pragma 指令類似的 _Pragma 操作符,其可以看做是 #pragma 的增強版,不僅可以實現 #pragma 所有的功能,更重要的是,_Pragma 還能和宏搭配使用。

有關 _Pragma 操作符更多的功能和用法,本節不做詳細講解,這裡僅介紹如何用 _Pragma 操作符避免標頭檔案重複引入。

當處理標頭檔案重複引入問題時,可以將如下語句新增到相應檔案的開頭:

_Pragma("once")

比如,將該語句新增到前面專案中 student.h 檔案中的開頭位置,再次執行專案,其可以正常執行。

事實上,無論是 C 語言還是 C++,為防止使用者重複引入系統庫檔案,幾乎所有庫檔案中都採用了以上 3 種結構中的一種,這也是為什麼重複引入系統庫檔案編譯器也不會報錯的原因。

總結

本節介紹了 3 種避免標頭檔案被重複引入的方法,其中 #pragma once 和 _Pragma("once") 可算作一類,其特點是編譯效率高,但可移植性差(編譯器不支援,會發出警告,但不會中斷程式的執行);而 #ifndef 的特點是可移植性高,編譯效率差。讀者可根據實際情況,挑選最符合實際需要的解決方案。

除非對專案的編譯效率有嚴格的要求,強烈推薦讀者選用第一種解決方案,即採用 #ifndef / #define / #endif 組合解決標頭檔案被重複引入。

另外在某些場景中,考慮到編譯效率和可移植性,#pragma once 和 #ifndef 經常被結合使用來避免標頭檔案被重複引入。比如說:
#pragma once
#ifndef _STUDENT_H
#define _STUDENT_H
class Student {
    //......
};
#endif
當編譯器可以識別 #pragma once 時,則整個檔案僅被編譯一次;反之,即便編譯器不識別 #pragma once 指令,此時仍有 #ifndef 在發揮作用。