Learning C++ 之1.10a ”頭文件警衛“

在1.7節中,提前定義和聲明。我們提到一個標識符只能定義一次。因此一個標識符定義兩次就會上報編譯錯誤。

int main()
{
    int x; // this is a definition for identifier x
    int x; // compile error: duplicate definition
 
    return 0;
}

類似的,一個程序中的函數如果定義兩次也會上報錯誤。

#include <iostream>
 
int foo()
{
    return 5;
}
 
int foo() // compile error: duplicate definition
{
    return 5;
}
 
int main()
{
    std::cout << foo();
    return 0;
}

雖然這些程序很容易修復(刪掉重複的定義即可),當有頭文件的時候,非常容易出現一個頭文件被多次引用的情況出現問題。看下面的例子:

math.h

int getSquareSides()
{
    return 4;
}

geometry.h

#include "math.h"

main.cpp

#include "math.h"
#include "geometry.h"
 
int main()
{
    return 0;
}

表面上看起來程序編譯會報錯,根本原因是math.h中有一個定義,而我們在main.cpp中引用了兩次math.h。下面是真正發生的情況:首先,#include “math.h” 會把getSquareSides複製到文件中一次。然後#include "geometry.h"中包含了 @include "math.h" ,這回將getSquareSides定義copy到geometry.h中,從而也就copy到了main.cpp中。

因此盡力了這幾個copy後,main函數就變成了如下的樣子:

int getSquareSides()  // from math.h
{
    return 4;
}
 
int getSquareSides() // from geometry.h
{
    return 4;
}
 
int main()
{
    return 0;
}

這就發生了重複定義的錯誤,每一個文件單獨來說都是沒有問題的。因爲main.cpp引用了兩個頭文件,而其中一個又引用了另一個頭文件,我們就碰到問題了。如果geometry.h需要getSquareSides,而main.cpp需要geometry.h和math.h你該怎麼辦呢?

頭文件警衛

好消息是我們可以通過一種叫做頭文件警衛的原理解決這個問題,頭文件警衛就是一個條件編譯指令,格式如下:

#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE
 
// your declarations and definitions here
 
#endif

當該頭文件被引用的時候,如果是第一次引用,那麼肯定沒有SOME_UNIQUE_NAME_HERE的定義,於是就會定義這個宏,並且成功引入該頭文件。因此該文件定義了SOME_UNIQUE_NAME_HERE宏並且把頭文件中的內容copy到引用的文件中。因此在之後如果重複引用了該文件的時候,該宏已經定義了,就不會出現重複引用的情況。

你所寫的每一個頭文件都必須有一個宏警衛,SOME_UNIQUE_NAME_HERE可以使用任意你想用的名字。但是一般都是由頭文件加_H命名。例如math.h使用:

#ifndef MATH_H
#define MATH_H
 
int getSquareSides()
{
    return 4;
}
 
#endif

即使是標準庫的函數頭文件都會使用宏警衛,如果你看過iostream的代碼的話,你會看到:

#ifndef _IOSTREAM_
#define _IOSTREAM_
 
// content here
 
#endif

把我們之前的例子上加上宏警衛:

math.h

#ifndef MATH_H
#define MATH_H
 
int getSquareSides()
{
    return 4;
}
 
#endif

geomretry.h

#ifndef GEOMETRY_H
#define GEOMETRY_H
 
#include "math.h"
 
#endif

main.cpp

#include "math.h"
#include "geometry.h"
 
int main()
{
    return 0;
}

現在當math.h被第一次引用的時候,MATH_H還沒有定義,於是就會定義這個宏,然後引入該文件。但是當引用geometry.h的時候,geometry.h會再次引用math.h從事,MATH_H已經定義了,就忽略掉該文件了。

我們能不能在頭文件中避免定義呢?

我們已經提醒過你不要在頭文件裏有定義,所以你這邊可能會好奇,如果頭文件裏面沒有定義,也就不需要頭文件警衛了啊。

有極少數的情況,我們後面會想你介紹,需要在頭文件裏進行定義。比如,當我們使用用戶自己定義的類的時候.現在我們還沒有講到這種情況,以後會講到。所以即使很多情況下不一定需要頭文件警衛,建議爲了養成好的習慣,大家都寫上。

頭文件警衛不會阻止頭文件被引用到不同的文件中。

注意全局的頭文件警衛是爲了阻止同一個文件中複製多個相同的頭文件。設計上,頭文件警衛並不會阻止同一個頭文件被copy到多個不同的文件中。考慮下面的情況:

squar.h

#ifndef SQUARE_H
#define SQUARE_H
 
int getSquareSides()
{
    return 4;
}
 
int getSquarePerimeter(int sideLength); // forward declaration for getSquarePerimeter
 
#endif

squar.cpp

#include "square.h"  // square.h is included once here
 
int getSquarePerimeter(int sideLength)
{
    return sideLength * getSquareSides();
}

main.cpp

#include <iostream>
#include "square.h" // square.h is also included once here
 
int main()
{
    std::cout << "a square has " << getSquareSides() << " sides" << std::endl;
    std::cout << "a square of length 5 has perimeter length " << getSquarePerimeter(5) << std::endl;
 
    return 0;
}

即使squar.h有頭文件守衛,也可以被引用多次,前提是在不同的文件中。

讓我們從細節上考慮一下這個爲什麼發生,這是因爲宏的定義只在該文件中有效,所以sqare.cpp引用了宏之後,定義SQUAR_H,這個宏的有效期只在squar.cpp中,當squar.cpp完成之後,這個宏就無效了。從而在main.cpp中再次引用的時候,會重新定義一下SQUAR_H這個宏。

 結果是兩個cpp文件都可以引用該文件,編譯沒有什麼問題。但是當鏈接的時候,鏈接器還是會抱怨有重複的函數定義。

 有很多種解決這個問題的方法,其中一個比較好的方式是將getSquareSize挪到suare.cpp中,這樣就不會報錯了。如下:

squar.h

#ifndef SQUARE_H
#define SQUARE_H
 
int getSquareSides(); // forward declaration for getSquareSides
int getSquarePerimeter(int sideLength); // forward declaration for getSquarePerimeter
 
#endif

squar.cpp

// It would be okay to #include square.h here if needed
// This program doesn't need to.
 
int getSquareSides() // actual definition for getSquareSides
{
    return 4;
}
 
int getSquarePerimeter(int sideLength)
{
    return sideLength * getSquareSides();
}

main.cpp

#include <iostream>
#include "square.h" // square.h is also included once here
 
int main()
{
    std::cout << "a square has " << getSquareSides() << "sides" << std::endl;
    std::cout << "a square of length 5 has perimeter length " << getSquarePerimeter(5) << std::endl;
 
    return 0;
} 

現在就可以了,getsquarsize只定義了一次。main.cpp可以正常調用該函數了。在之後的課程中我們還會介紹其他解決該問題的方式。

#pragma once

一些編譯器支持一個簡單的頭文件守衛,#pragma once。

#pragma once
 
// your code here

這個宏提供了一種簡單的頭文件守衛的方式,更加簡潔明瞭,不容易出錯。例如visual stadio的stdafx.h就使用了這種方式。

但是因爲這不是C++標準的一部分,所以並不是所有的編譯器都支持該種方式,所以使用不能普及。

總結:

頭文件守衛的存在就是爲了在同一個文件中不會重複地引用頭文件,從爲避免重複定義地錯誤。

考慮到複製的聲明不會引起任何問題,因爲聲明可以重複定義,但是即使你的頭文件裏面沒有定義只有聲明,建議也同樣需要寫頭文件守衛,這個是一個良好的習慣。

當然,頭文件守衛不會阻止文件在不同的文件中引用,所以這對我們來說是個好消息。因爲我們經常需要在不同的文件中引用相同的文件。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章