个人整理学习用,非教材,有错误欢迎指正

头文件

  究竟什么是头文件?
  首先说明一个概念,所谓的文件后缀并不是必须的,在Linux下这种特点尤为明显。对于编译器来说,无论是.c文件 .cpp文件,抑或是.h文件,都是同一种文件形式,区别仅仅是你去执行该文件的方式。

上图是在Linux环境中编译一个.cpp文件 当使用 g++编译器时,a.cpp被视为一个CPP文件,可以正常编译,编译后的文件也能被正常运行。但使用gcc编译器编译时,gcc会把a.cpp视为一个C文件,而很明显,a.cpp并不符合C的语法。也就是说,编译器并不在意该文件的后缀。

  因此,头文件并不是一种天生的类型,而是我们为了后期编写程序便利才引入的。理论上讲,你可以在头文件里编写任何符合CPP/C语法的内容,它在单个文件内都可以被正确识别并执行。但是为了避免多个文件编译时产生错误,我们认为的制定了一些规则:
  1 .h 中仅存放 声明(declaration),而与其对应的 定义(definition)应存放在对应的.cpp文件中
  2 .h 文件头尾要添加 #ifndefine 语句
  这还要从编译器讲起。任何.cpp文件都需要4个步骤:预编译、编译、汇编、链接,才能最终转化为可执行文件。

  1. 预编译阶段:会完成预编译指令,如#define宏的替换,#include头文件的展开。生成对应 .ii文件
  2. 编译阶段:会代码转换为汇编代码,同时为每个文件生成一个地址符号表,记录在文件中出现的每个函数以及全局变量,若是declaration,则地址为0;若是definition,则记录其实际地址,生成对应 .s文件
  3. 汇编阶段:将所生成的汇编代码继续转换为机器代码,并为每个文件生成相对应的.o文件,并以二进制的形式存储
  4. 链接阶段:将所有的.o文件链接为一个可执行文件(g++默认为.out文件),并根据各个文件的地址符号表,生成一张完整的地址符号表,记录了每一个相对应的函数或全局变量相对应的、唯一的地址。若在此阶段出现地址重复或地址仍为0,则链接失败

在头文件中存储定义

=========================a.h=======================
int g;		// 注意,这里 *定义* 了一个全局变量 g
class A{	// *声明* 了 A类
private:
	int a;	// *声明* 了 A类中的一个变量 a
public:		// *声明* 了 A类中的一个函数func
	void func();
};
========================a.cpp======================
#include "a.h"
#include <iostream>
using namespace std;
void A::func(){
	g = 10;
	cout << "g = " << g << endl;
	cout << "a.func()" << endl;
}
int main(){
	A a;
	a.func();
	return 0;
}
========================b.cpp======================
#include "a.h"
#include <iostream>
using namespace std;
void func(){
	cout << "b.func()" << endl;
}

在这里插入图片描述
以上有三个文件,其中 a.cpp与 b.cpp都包含了 a.h头文件
尝试仅编译a.cpp并运行
在这里插入图片描述可以看到,即使 a.h中我们定义了一个全局变量,程序仍能正常执行,这也侧面说明了头文件规则并非规定
  这次我们加入b,将a、b一起编译,他们同时引用了头文件 a.h。我们可以用 --save-temps参数保存所有的中间文件
在这里插入图片描述这次出现了错误,提示了multiple definition of 'g
我们站看文件列表,可以清楚看到 a.o,b.o都已经生成,也就表明直至汇编阶段都成功执行,于是我们可以打开a.ii,也就是#include执行后的文件,看究竟是什么导致了err
在这里插入图片描述
在这里插入图片描述注意上图中 8~14 行代码,就是我们本来写在 a.h中的内容,而在经过预编译后,被直接插入进了 b.ii中。实际上,#include "a.h"在预编译阶段,就是将 a.h 中包含的代码片段插入到了原文件#include的那一行中。a.cpp也会做同样的事,即也定义了一个全局变量g。这就使得在最后的链接阶段的地址符号表中,一个全局变量对应两个地址,因此报错multiple definition of 'g。回头观察可以发现,这个错误是 /usr/bin/ld 发出的,ld是链接器,这也解释了为什么这种错误不会像普通编译错误一样提示你具体哪一行有问题,因为预编译阶段已经完成,而链接器无法分辨此语句在源文件的哪一行中
上述代码的正确版本

=========================a.h=======================
extern int g;	// 注意,这里 *声明* 了一个全局变量 g
class A{		// *声明* 了 A类
private:
	int a;		// *声明* 了 A类中的一个变量 a
public:			// *声明* 了 A类中的一个函数func
	void func();
};
========================a.cpp======================
#include "a.h"
#include <iostream>
using namespace std;
int g;	// 此处才是头文件中所声明 g的定义
void A::func(){
	g = 10;
	cout << "g = " << g << endl;
	cout << "a.func()" << endl;
}
int main(){
	A a;
	a.func();
	return 0;
}
========================b.cpp======================
#include "a.h"
#include <iostream>
using namespace std;
void func(){
	cout << "b.func()" << endl;
}

  而除此之外还有一种情况,当程序调用关系复杂的时候,可能会多次调用同一个头文件。如此时,在b.cpp中再#include "a.cpp"(此时将a.cpp也视为一个头文件,因此假设其中不包含定义与全局变量,但也#include "a.h"),编译 g++ b.cpp后会出现如下错误
在这里插入图片描述  有了上文的铺垫,再加上此处的提示,原因我们也很清楚,就相当于在b.cpp中,#include "a.h"处中定义了一个类A,紧接着#include "a.cpp" 处又定义了一个类A,造成redefinition of ‘class A’。为了让每个头文件仅有效地包含一次,我们需要在 a.h 与 a.cpp 中添加如下字段

========================a.h==========================
#ifndef _A_H_
#define _A_H_
extern int g;	// 注意,这里 *声明* 了一个全局变量 g
class A{		// *声明* 了 A类
private:
	int a;		// *声明* 了 A类中的一个变量 a
public:			// *声明* 了 A类中的一个函数func
	void func();
};
#endif
=======================a.cpp=========================
#ifndef _A_CPP_
#define _A_CPP_
#include "a.h"
void fa(){}
#endif

修改后效果如下
在这里插入图片描述

Logo

更多推荐