1.10a头文件保护符

《1.7前置声明与定义》中,一个标识符只能被定义一次,如果重复定义,就会产生编译错误。

int main()
{
    int x;
    int x; // 编译错误:重复定义
    return 0;
}

同样,一个函数如果被重复定义也会产生编译错误。

#include <iostream>
 
int foo()
{
return 5;
}
 
int foo() // 编译错误:重复定义
{
return 5;
}
 
int main()
{
    std::cout << foo();
    return 0;
}

上述问题是很容易解决的,只需要删除重复定义即可。在使用头文件时,一个头文件中的定义被多次包含是极易发生的事情,比如一个头文件中#include了另一个头文件(很常见的)。比如下面这个例子:

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 #include了”math.h”,即将函数getSquareSides的定义拷贝到main.cpp中。然后main.cpp又#include了”geometry.h”,由于”geometry.h” #include了”math.h”,于是函数getSquareSides的定义被拷贝到”geometry.h”中,进而又被拷贝到”main.cpp”中。

处理完所有的#include后,main.cpp就变成了这个样子:

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

于是就产生了重复定义的编译错误,上面的文件,每一个单独来看,都是没有问题的,但组合在一起就产生了错误。如果geometry.h确实需要#include “math.h”,”main.cpp”也确实必须要#include 这两个头文件,那如何才能避免错误发生呢?

注意并非只有函数定义会造成这个问题——任何定义都会,如果math.h中包含变量定义,用户自定义类型,都会产生同样的错误。

头文件保护符

好消息是这个问题很容易解决——通过一个称为头文件保护符(header guard)(也称包含保护符include guard)的机制。头文件是具有如下形式的额外编译指令:

#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE
// 你编写的声明与定义
#endif

当这样的头文件被include时,编译器会首先检查SOME_UNIQUE_NAME_HERE是否被定义过,如果这是我们第一次包含这个头文件,SOME_UNIQUE_NAME_HERE肯定是没有被定义过的,于是接下来SOME_UNIQUE_NAME_HERE就会被定义并且文件中的内容会被包含。如果我们之前已经包含过这个头文件了,SOME_UNIQUE_NAME_HERE就会在之前包含的那次被定义过,于是这个头文件就会被编译器无视。(参看前一节中的条件编译指令

你使用的所有头文件都应当有头文件保护符,SOME_UNIQUE_NAME_HERE可以是你随便定义的名字,但通常会被命名为头文件名加_H,例如,math.h应当有这样的头文件保护符。

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

即使标准库中的文件也会使用头文件保护符,如果你用Visual Studio看一下iostream文件,就会看到:

#ifndef _IOSTREAM_
#define _IOSTREAM_

// content here

#endif

使用头文件保护符更新前面的例子

现在我们回到之前的例子,使用头文件保护符更新之前的例子。

math.h:

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

geometry.h:

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

main.cpp:

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

现在,当main.cpp #includes “math.h”时,预处理器会看到MATH_H 尚未被定义,math.h的内容将会被拷贝到main.cpp中,此时MATH_H已被定义,接下来main.cpp #includes “geometry.h”,预处理器就会发现MATH_H已被定义(因为#includes了 “math.h”),于是头文件保护符之间的内容就会被忽略。

通过使用头文件保护符,我们就能够保证math.h的内容在main.cpp中只会被包含一次。

为了格式的统一,我们通常也会给geometry.h添加头文件保护符(虽然不必要)。

头文件保护符并不会让头文件在几个不同代码文件中只被包含一次(有点绕口)

头文件保护符的作用是防止一个头文件在一个文件中被拷贝多次,但有意思的是,保护符并不会使得头文件在不同代码文件间只被包含一次,这有时候会造成意想不到的问题。考虑下面的例子(多文件项目):

square.h:

#ifndef SQUARE_H
#define SQUARE_H
 
int getSquareSides()
{
    return 4;
}
 
int getSquarePerimeter(int sideLength); // getSquarePerimeter的前置声明
 
#endif

square.cpp:

#include "square.h"  // square.h在这里被include一次
 
int getSquarePerimeter(int sideLength)
{
    return sideLength * getSquareSides();
}

main.cpp:

#include "square.h" // square.h 在这里也被include一次
 
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;
}

可以看到,虽然square.h有头文件保护符,square.h还是在square.cpp和main.cpp分别被拷贝了一次。

让我们详细地解释一下,当square.h被square.cpp时,SQUARE_H将会被定义直到square.cpp末尾。此定义将会防止square.h在square.cpp中被拷贝两次(这也是保护符的真正目的)。但是,当square.cpp结束后,SQUARE_H将不再处于已被定义的状态,这意味着当预处理器处理main.cpp时,是认为SQUARE_H尚未被定义的。

最终的结果是,square.cpp和main.cpp中都有一份getSquareSides()的定义,程序虽然可以正常编译,但连接器确会提示标识符getSquareSides被重复定义了!

解决这个问题的方法有很多,其中一个方法是将函数定义放在其中一个.cpp文件中,使得头文件中只包含前置声明(这也是我们之前推荐的做法):

square.h:

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

square.cpp:

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

main.cpp:

#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;
}

这样函数getSquareSides()的定义只会出现在square.cpp中,连接器就不会再抱怨了。而且main.cpp还是可以调用这个函数的,因为它包含了square.h,而square.h中有函数的前置声明,连接器会将main.cpp中的函数调用与square.cpp中的函数定义相联系(参看1.8多文件程序)。

以后的课程中我们会探索更多解决这个问题的方法。

#pragma once

现在许多编译器都支持一种更简单的,可替代头文件保护符的方法,使用#pragma指令:

#pragma once

// 此处放代码

#pragma once 与头文件保护符有相同的作用,并且更简短、更不易出错。Visual Studio包含在项目中的stdafx.h文件就用它代替了头文件保护符。

但是, #pragma once并非标准C++的一部分,而且并不是所有的编译器都支持(尽管绝大部分现代编译器都支持)。

为了程序的兼容性,我们还是推荐使用头文件保护符。

总结

头文件保护符的目的是防止一个头文件在同一文件中被多次拷贝,以避免重复定义。我们推荐在所有的头文件中都添加头文件保护符,即使有一些不是非加不可。

关于 “1.10a头文件保护符” 的 1 个意见

评论关闭。