1.9头文件

头文件与其存在的意义

随着程序规模的进一步增长,分布在不同文件中的所有函数都需要添加前置声明会是一件很乏味的事,如果可以把所有的前置声明都放在同一个地方,岂不是很方便?

C++代码文件(有.cpp后缀的)并非C++程序中唯一常见的文件,另一种文件类型被称为头文件,有时也称包含文件(include file)。头文件通常有.h后缀,但有时也会有.hpp后缀或者根本没有任何后缀名,头文件的作用是存放声明以供其他文件使用。

使用标准库中的头文件

考虑下面的程序:

#include <iostream>
int main()
{
    using namespace std;
    cout << "Hello, world!" << endl;
    return 0;
}

程序的功能不再多说,但是我们会发现,程序从未定义过cout,那么编译器是如何知道cout是什么的呢?,答案是cout已经在一个被称为“iostream”的头文件被定义,当我们使用#include <iostream>时,就是在将“iostream”中的所有内容拷贝到现有的程序中,所以头文件中的所有内容就可使用了。

需要注意的是头文件通常只包含声明,而不定义某种功能是如何实现的。那么又要问了,既然cout在iostream仅仅是被声明了,那他又是在何处被真正定义的呢?答案是它是在C++运行时支持库(在链接阶段被自动链接到你的程序中)中被实现的。C++头文件原理

考虑一下如果iostream头文件不存在会怎样。每当你使用std::cout,你都会手动地将所有与std::cout相关的声明拷贝到其所在文件的顶部,这就需要知道哪些是相关的哪些是无关的,所以使用#include <iostream>会简单很多。

写自己的头文件

现在我们再来看一下上节课所用的例子,最后我们用了两个文件,add.cpp和main.cpp,如下:

add.cpp:

int add(int x, int y)
{
    return x + y;
}

main.cpp:

#include <iostream>
 
int add(int x, int y); // forward declaration using function prototype
 
int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is " << add(3, 4) << endl;
    return 0;
}

此处我们需要使用一个前置声明以使编译器在编译main.cpp时能够知道add是什么,正如前面所提,在每个使用到相关函数的文件中都写一遍前置声明会很麻烦,头文件就可以帮助我们摆脱这个烦恼,,头文件只需写一次,就可以在需要的地方被多次使用,这同样有利于程序维护,如果程序原型发生了变化(例如添加一个新的参数),只需修改相关头文件即可,大大减少了需要修改的数量。

写头文件其实非常简单,头文件包含两个部分。

第一部分是头文件保护符(header guard,有多种译法),将在下节课(预处理器)讨论。头文件保护符可以防止一个头文件在同一文件中被多次include。

第二部分是头文件真正的内容,应当包含我们希望其他文件可以看到的所有函数的声明。此处我们支持所有的头文件都使用.h后缀名,所以我们将这个新的头文件命名为add.h。

add.h:

// This is start of the header guard.  ADD_H can be any unique name.  By convention, we use the name of the header file.
#ifndef ADD_H
#define ADD_H
 
// This is the content of the .h file, which is where the declarations go
int add(int x, int y); // function prototype for add.h -- don't forget the semicolon!
 
// This is the end of the header guard
#endif

为了能够在main.cpp中使用这个头文件,我们使用#include “add.h”,main.cpp改为:

#include <iostream>
#include "add.h"
 
int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is " << add(3, 4) << endl;
    return 0;
}

add.cpp保持不变。

当编译器编译#include “add.h”时,会将add.h中的内容拷贝到此处,因为add.h中包含add()的函数原型,这个函数原型现在被当作add()的前置声明使用了。

最终结果,程序可以正常编译运行。

Note:当我们#include一个文件时,文件中的所有内容都会被插入到该语句处。C++头文件原理

如果发生编译错误:未找到add.h,很可能是你的文件名错了,它可能被命名add(无后缀名)、add.h.txt或者add.hpp。

如果发生链接错误:add()未定义,确保add.h被添加到你的项目中了。

角括号与双引号

你也许会好奇,为什么iostream我们使用角括号<>,而add.h使用双引号。答案是双引号告诉编译器我们使用了编译器自带的头文件,而双引号告诉编译器这是我们自己提供的头文件,所以编译器会首先在当前源代码所在目录下寻找这个头文件,如果没有找到,编译器会检查所有其余的包含路径(编译器/IDE中设置的)。如果失败,编译器则会返回检查系统路径。

为什么iostream没有.h后缀

另一个常见的问题是为什么iostream(或者其他一些标准库头文件)没有.h后缀呢?答案是,iostream.h与iostream是两个不同的头文件,解释这个问题我们需要上一节小历史课。

C++初创时期,标准运行时库中的所有文件都以.h结尾,看起来和谐一致挺不错的。cout和cin的原始版本存在于iostream.h中,当这门语言被ANSI委员会标准化时,他们决定将运行时库中的所有函数都移动到std名称空间中(通常认为这是一个好想法)。但是,这也造成了一个问题:如果将所有的函数都移动到std名称空间中,那么以前的所有程序都无法正常工作了。

为了绕过这个问题,并未以前的程序提供向后兼容,而引入了一组新的头文件,这组新的头文件拥有与以前相同的名称,但没有.h后缀名。新的头文件将所有的功能组件都放在std名称空间中。这样的话,以前包含#include <iostream.h>的程序就不必重写,新程序可以使用#include <iostream>。

当使用标准库中的头文件时,应当使用不带.h的版本(如果存在的话),否则你就是在使用一个不再被支持的旧版本。

另外,标准库中的一些头文件没有不带.h的版本,只有带.h的版本,对于这些文件,当然是要添加.h后缀的。这些库许多与标准C向后兼容,C语言是不支持名称空间的。结果是,这些库的功能无法通过std名称空间访问。当你写自己的头文件时,总是应当添加.h后缀,因为你无法将你的代码放进std名称空间中。

Rule使用不带.h的版本(如果存在的话)并通过std访问其功能。如果不存在不带.h的版本或者是自建的头文件,则使用带.h的版本。

引入其他目录下的头文件

还有一个常被问及的问题是如何引入其他目录下的头文件。

一个方法是使用相对路径(并非好方法),例如:

#include "headers/myHeader.h"

#include "../moreHeaders/myOtherHeader.h"

这种方法的缺陷是你需要反映出代码的目录结构,如果你更新了你的目录,代码就无法正常工作了。

更好的方法是告诉的编译器/IDE你在某个位置放了一堆头文件,以便当编译器无法在当前目录找到头文件时可以去那里寻找。可以通过在IDE项目中设置“搜索目录”或“包含目录”实现。

在Visual Studio中,可以右击解决方案资源管理器中的项目名,选择“属性”,选择“VC++目录”,可以看到“包含目录“,在此添加目录即可。Visual Studio修改项目包含目录

Code::Blocks中,在项目菜单下选择“生成选项”->“搜索目录”,添加目录即可。

如果使用g++编译,可以使用-I指定一个可替代的包含目录。

g++ -o main -I /source/includes main.cpp

这种方法的优点是,如果你更改了目录结构,只需要修改编译器/IDE相关设置即可,而无需修改所有的代码。

可以将函数定义放在头文件中吗?

如果你这样做,C++并不会排斥,但通常来说,不应当如此。

如前面所提,如果你#include一个文件,那么这个文件的所有内容都会被插入到此处,这意味着你放在头文件中的所有定义会被拷贝到每一个引用此头文件的文件中。

对于小项目,这似乎不是个大问题,但对于更大的项目来说,这会使得编译效率大大下降(因为同样的代码被多次编译)并且增大最终生成的可执行文件的大小。如果你更改了一个代码文件中的定义,只有这个.cpp文件需要被重新编译;如果你更改了一个头文件中定义,那么所有包含此头文件的代码文件都需要重新编译。一个小的改动会让你不得不重新编译整个项目。

对于一些不重要的函数(不可能改动)可能例外(比如函数定义只有一行)。

头文件的最佳做法

下面是几个创建自己头文件时的最佳做法:

  • 始终添加头文件保护符。
  • 不要在头文件中定义变量(除非是常数),头文件通常只用来存放声明。
  • 不要再头文件中定义函数。
  • 每个头文件都应有特定功能,并且应当尽可能独立。比如,你可以将所有与功能A相关的声明放在h中,所有与功能B相关的声明放在B.h中。这样的话如果你以后只关注A,可以仅include A.h,而无须理会任何与B相关的东西。
  • 头文件名尽量与其相关源文件相关,例如h与grade.cpp。
  • 尽可能减少头文件中#include的文件数目,只#include必要的文件。
  • 不要#include .cpp文件。

转载请注明本文出处:http://www.icoder.top/blog/

关于 “1.9头文件” 的 1 个意见

发表评论

电子邮件地址不会被公开。