C/C++頭文件以及避免頭文件包含造成的重定義方法


頭文件

頭文件是擴展名為 .h 的文件,包含了 C 函數聲明和宏定義,被多個源文件中引用共享。有兩種類型的頭文件:程序員編寫的頭文件和編譯器自帶的頭文件。

在程序中要使用頭文件,需要使用 C 預處理指令 #include 來引用它。前面我們已經看過 stdio.h 頭文件,它是編譯器自帶的頭文件。

引用頭文件相當於復制頭文件的內容,但是我們不會直接在源文件中復制頭文件的內容,因為這么做很容易出錯,特別在程序是由多個源文件組成的時候。

A simple practice in C 或 C++ 程序中,建議把所有的常量、宏、系統全局變量和函數原型寫在頭文件中,在需要的時候隨時引用這些頭文件。

引用頭文件的語法

使用預處理指令 #include 可以引用用戶和系統頭文件。它的形式有以下兩種:

#include <file>

這種形式用於引用系統頭文件。它在系統目錄的標准列表中搜索名為 file 的文件。在編譯源代碼時,您可以通過 -I 選項把目錄前置在該列表前。

#include "file"

這種形式用於引用用戶頭文件。它在包含當前文件的目錄中搜索名為 file 的文件。在編譯源代碼時,您可以通過 -I 選項把目錄前置在該列表前。

引用頭文件的操作

#include 指令會指示 C 預處理器瀏覽指定的文件作為輸入。預處理器的輸出包含了已經生成的輸出,被引用文件生成的輸出以及 #include 指令之后的文本輸出。例如,如果您有一個頭文件 header.h,如下:

char *test (void);

 

和一個使用了頭文件的主程序 program.c,如下:

1 int x; 2 #include "header.h"
3 
4 int main (void) 5 { 6  puts (test ()); 7 }

編譯器會看到如下的代碼信息:

1 int x; 2 char *test (void); 3 
4 int main (void) 5 { 6  puts (test ()); 7 }

只引用一次頭文件

如果一個頭文件被引用兩次,編譯器會處理兩次頭文件的內容,這將產生錯誤。為了防止這種情況,標准的做法是把文件的整個內容放在條件編譯語句中,如下:

1 #ifndef HEADER_FILE 2 #define HEADER_FILE
3 
4 the entire header file file 5 
6 #endif

這種結構就是通常所說的包裝器 #ifndef。當再次引用頭文件時,條件為假,因為 HEADER_FILE 已定義。此時,預處理器會跳過文件的整個內容,編譯器會忽略它。

有條件引用

有時需要從多個不同的頭文件中選擇一個引用到程序中。例如,需要指定在不同的操作系統上使用的配置參數。您可以通過一系列條件來實現這點,如下:

1 #if SYSTEM_1
2    # include "system_1.h"
3 #elif SYSTEM_2
4    # include "system_2.h"
5 #elif SYSTEM_3
6  ... 7 #endif

但是如果頭文件比較多的時候,這么做是很不妥當的,預處理器使用宏來定義頭文件的名稱。這就是所謂的有條件引用。它不是用頭文件的名稱作為 #include 的直接參數,您只需要使用宏名稱代替即可:

1  #define SYSTEM_H "system_1.h"
2  ... 3  #include SYSTEM_H

SYSTEM_H 會擴展,預處理器會查找 system_1.h,就像 #include 最初編寫的那樣。SYSTEM_H 可通過 -D 選項被您的 Makefile 定義。

在一段時間的編程中,時常會遇到重定義(redefinition)問題。一般都是#include在包含頭.h文件時出現了重復包含的關系。運氣好的話可以比較容易的發現問題,運氣不好的話只好列出所有的頭文件.h中的包含關系,挨個檢查是哪里出了問題。最近發現如果遵循“在頭文件.h中不再包含頭文件.h”的原則,可以從根本上避免這個問題。雖然這樣做會使得在代碼文件.c或.cpp中必須各自包含進來所需的頭文件.h,還要注意在包含時可能會存在順序的問題,但這比起查找何處進行了重定義來說簡單了許多,也使包含關系更加清晰。

對原來的項目中的所有文件按上述原則進行了修改,暫未發現不良影響,感覺還不錯。

 

加上一句,也可以在預編譯區加上 #pragma once來防止重定義,

注意#ifndef...  #define... #end if  以及 extern 的用法

#ifndef和#pragma once兩者的使用方式有何區別?

示例代碼如下:

 方式一:

 #ifndef  __SOMEFILE_H__

#define   __SOMEFILE_H__

 ... ... // 聲明、定義語句

#endif

   

方式二:

#pragma once

 ... ... // 聲明、定義語句

兩者各有何特點?

(1)#ifndef

  #ifndef的方式受C/C++語言標准支持。它不僅可以保證同一個文件不會被包含多次,也能保證內容完全相同的兩個文件(或者代碼片段)不會被不小心同時包含。

  當然,缺點就是如果不同頭文件中的宏名不小心“撞車”,可能就會導致你看到頭文件明明存在,但編譯器卻硬說找不到聲明的狀況——這種情況有時非常讓人郁悶。

  由於編譯器每次都需要打開頭文件才能判定是否有重復定義,因此在編譯大型項目時,ifndef會使得編譯時間相對較長,因此一些編譯器逐漸開始支持#pragma once的方式。

(2)#pragma once

  #pragma once 一般由編譯器提供保證:同一個文件不會被包含多次。注意這里所說的“同一個文件”是指物理上的一個文件,而不是指內容相同的兩個文件。

  你無法對一個頭文件中的一段代碼作pragma once聲明,而只能針對文件。

  其好處是,你不必再擔心宏名沖突了,當然也就不會出現宏名沖突引發的奇怪問題。大型項目的編譯速度也因此提高了一些。

  對應的缺點就是如果某個頭文件有多份拷貝,本方法不能保證他們不被重復包含。當然,相比宏名沖突引發的“找不到聲明”的問題,這種重復包含很容易被發現並修正。

  另外,這種方式不支持跨平台!

兩者之間有什么聯系?

 #pragma once 方式產生於#ifndef之后,因此很多人可能甚至沒有聽說過。目前看來#ifndef更受到推崇。因為#ifndef受C/C++語言標准的支持,不受編譯器的任何限制;

 而#pragma once方式卻不受一些較老版本的編譯器支持,一些支持了的編譯器又打算去掉它,所以它的兼容性可能不夠好。

 一般而言,當程序員聽到這樣的話,都會選擇#ifndef方式,為了努力使得自己的代碼“存活”時間更久,通常寧願降低一些編譯性能,這是程序員的個性,當然這是題外話啦。

 還看到一種用法是把兩者放在一起的:

     #pragma once

   #ifndef __SOMEFILE_H__

   #define __SOMEFILE_H__

   ... ... // 聲明、定義語句

   #endif

總結:

        看起來似乎是想兼有兩者的優點。不過只要使用了#ifndef就會有宏名沖突的危險,也無法避免不支持#pragma once的編譯器報錯,所以混用兩種方法似乎不能帶來更多的好處,倒是會讓一些不熟悉的人感到困惑。

        選擇哪種方式,應該在了解兩種方式的情況下,視具體情況而定。只要有一個合理的約定來避開缺點,我認為哪種方式都是可以接受的。而這個已經不是標准或者編譯器的責任了,應當由程序員自己或者小范圍內的開發規范來搞定。

為了避免同一個文件被include多次

1   #ifndef方式
2   #pragma once方式

在能夠支持這兩種方式的編譯器上,二者並沒有太大的區別,但是兩者仍然還是有一些細微的區別。
    方式一:

    #ifndef __SOMEFILE_H__
    #define __SOMEFILE_H__
    ... ... // 一些聲明語句
    #endif

    方式二:

    #pragma once
    ... ... // 一些聲明語句


    #ifndef的方式依賴於宏名字不能沖突,這不光可以保證同一個文件不會被包含多次,也能保證內容完全相同的兩個文件不會被不小心同時包含。當然,缺點就是如果不同頭文件的宏名不小心“撞車”,可能就會導致頭文件明明存在,編譯器卻硬說找不到聲明的狀況

    #pragma once則由編譯器提供保證:同一個文件不會被包含多次。注意這里所說的“同一個文件”是指物理上的一個文件,而不是指內容相同的兩個文件。帶來的好處是,你不必再費勁想個宏名了,當然也就不會出現宏名碰撞引發的奇怪問題。對應的缺點就是如果某個頭文件有多份拷貝,本方法不能保證他們不被重復包含。當然,相比宏名碰撞引發的“找不到聲明”的問題,重復包含更容易被發現並修正。

   方式一由語言支持所以移植性好,方式二 可以避免名字沖突

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM