头文件、定义与声明
类的定义
- 类的要用两个分离的 .h 文件(头文件)和 .cpp 文件来定义。
- 类的声明以及类内所有函数的原型写在 .h 文件。
- 类的所有函数的具体实现写在 .cpp 文件。
定义和声明
后面几乎所有的定义和声明这两个动词我都加粗强调了,它们的区别很大,也很重要。
- 头文件里只能存在声明。
extern
关键字(之后会说这个关键字)是声明。- 函数的原型是声明。
- 类/结构体的声明。
头文件
何时引用头文件
- 如果一个函数在头文件中被声明,那么在所有要调用或者定义这个函数的地方都要
#include "name.h"
。 - 如果一个类在头文件中被声明,那么在所有调用这个类或者定义这个类的实体的地方都要
#include "name.h"
。
头文件 = 接口
- 头文件是一个类和这个类的使用者之间的联系纽带。
- 编译器强化要求使用所有结构和函数之前必须声明它们。
头文件中不能定义变量
在介绍这些禁忌之前,要先了解编译预处理指令 #include
的工作原理:
#include
把头文件里所有的内容复制进下面的代码前面,形成一个大的文本文件,然后交给编译器编译。
那么问题来了,如果头文件里定义了变量的话!!!(如下代码)
/* in example.h*/
void f();
//函数 f 是一个声明的函数
int global;
//这里的 global 是一个定义的变量
/* in a.cpp */
#include "example.h"
int main()
{
return 0;
}
/* in b.cpp */
#include "example.h"
#include <iostream>
using namespace std;
void f()
{
cout << "Hello, World!" << endl;
}
当我们在终端执行 g++ -o test.exe a.cpp b.cpp
的时候,链接器(不是编译器!)就报错了,报错如下:
-
ld.lld: error: duplicate symbol: global defined at :
-
C:/Users/ZTer/AppData/Local/Temp/a-06a49a.o
-
C:/Users/ZTer/AppData/Local/Temp/b-5a9f35.o
clang-16: error: linker command failed with exit code 1
-
这段话实际上就是指存在一个重复定义的变量(duplicate symble)导致链接器(linker)报错。
究其原因,就像我们上面提到的,预编译命令实际上是把头文件里的东西复制到代码前面,我们在 a.cpp 和 b.cpp 中均预编译了 example.h 这个头文件,然后 example.h 中有一个 global 全局变量,此时 global 便同时成为 a.cpp 和 b.cpp 的一个全局变量。但是编译器编译代码的时候是一份一份编译的,所以编译器并没有对它们中的任何一个报错。直到最终链接器要合并出一个可执行文件 test.exe 的时候,才发现这两份代码里有一个叫做 global 的变量被重复定义了,然后链接器报错,编译终止。
头文件中声明之后必须在别处存在定义
假设我们确有需要在头文件里创造一个变量 global,怎么办呢?
关键词 extern
就派上用场了。它的作用是声明(不是定义!)一个变量。
让我们把上面的代码修改一下:
/* in example.h*/
void f();
//函数 f 是一个声明的函数
extern int global;
//这里的 global 是一个声明的变量
/* in a.cpp */
#include "example.h"
int main()
{
return 0;
}
/* in b.cpp */
#include "example.h"
void f()
{
global ++;
}
结果链接器又报错了,报错信息如下:
- ld.lld: error: undefined symbol: global
referenced by C:/Users/ZTer/AppData/Local/Temp/b-3f78d2.o:(.refptr.global)
clang-16: error: linker command failed with exit code 1
这段话实际上就是说引用了一个未定义的变量 global,导致链接器报错。
这是因为 extern
关键字只是声明了 global,但是没有告诉编译器它到底在哪。
extern
:我有一个变量 global,我现在不知道他在哪,但请你相信我,确实有。
然后编译器就信了。编译器编译完了程序,但是它不知道 global 在哪,于是把它的位置空了出来,转而告诉链接器有一个 global 不知道跑哪里去了(因为它一次只编译一份代码,而 global 的定义可能在别的代码里,编译器就找不到了),要链接器找一下。
链接器翻了一遍所有的代码,都没找到 global,于是跑来报错,说 global 没定义。
-
所以说,只要有声明,必须有定义,但定义不需要声明。
特别的,如果声明了没定义,但后面代码中没用到这个声明了的变量,是没问题的。
我们再次修改一下代码:
/* in example.h*/
void f();
//函数 f 是一个声明的函数
extern int global;
//这里的 global 是一个声明的变量
/* in a.cpp */
#include "example.h"
int main()
{
return 0;
}
/* in b.cpp */
#include "example.h"
int global;
void f()
{
global ++;
}
这次通过编译了。
条件编译指令
-
条件编译指令指
#ifdef, #ifndef, #endif
等等。#ifdef
如果该命令后的宏已经被定义过,那么编译它下面的代码直到#endif
#ifndef
如果该命令后的宏没有被定义过,那么编译它下面的代码直到#endif
-
如果代码目前的宏定义状态不满足
#ifdef, #ifndef
的条件,则从它开始直到#endif
之间的代码都不会被编译。
请看如下代码情景:
/* a.cpp */
#include "example1.h"
#include "example2.h"
int main()
{
return 0;
}
/* example1.h */
class A{
};
/* example2.h */
#include "example1.h"
extern A a;
这时候我们编译 a.cpp,编译器报错,提示类 A 被重复声明了。
为什么?还记得预编译指令的原理吗?它把头文件里的内容复制到引用它的代码之前。
a.cpp 预编译了 example1.h 和 example2.h 两个头文件,而 example2.h 里又预编译了 example1.h 。这导致 example1.h 被复制进 a.cpp 之后又被复制进 example2.h,然后跟着 example2.h 一起被复制到 a.cpp 里。相当于此时 a.cpp 里有两个 example1.h 里的代码,而 example1.h 里声明了一个类 A,类是不可以被重复声明的,所以程序编译错误。
而我们利用条件编译指令,就可以避免这样的错误出现。修改代码如下:
/* a.cpp */
#include "example1.h"
#include "example2.h"
int main()
{
return 0;
}
/* example1.h */
#ifndef EXAMPLE1_H_
#define EXAMPLE1_H_
class A{
};
#endif
/* example2.h */
#ifndef EXAMPLE2_H_
#define EXAMPLE2_H_
#include "example1.h"
extern A a;
#endif
这样的话,a.cpp 在第一次预编译 example.h 的时候就定义了 EXAMPLE_H_ 这个宏,那么在预编译 example2.h 的时候,就不会对 example1.h 再次编译,重复声明的问题得到解决。
总结一下头文件的 Tips
- 一个头文件中最多只能包含一个类的声明,并且头文件中只能有声明。
- 用一个与头文件相同名称的.cpp文件定义头文件中的所有声明的函数和变量。
- 头文件要用标准头文件结构围绕起来(就是“条件编译命令”一节中修复错误的做法)。