天天看點

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++标準的一部分,是以并不是所有的編譯器都支援該種方式,是以使用不能普及。

總結:

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

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

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