头文件、定义与声明

类的定义

  • 类的要用两个分离的 .h 文件(头文件)和 .cpp 文件来定义。
  • 类的声明以及类内所有函数的原型写在 .h 文件。
  • 类的所有函数的具体实现写在 .cpp 文件。

定义和声明

后面几乎所有的定义和声明这两个动词我都加粗强调了,它们的区别很大,也很重要。

  • 头文件里只能存在声明。
    1. extern 关键字(之后会说这个关键字)是声明。
    2. 函数的原型是声明。
    3. 类/结构体的声明。

头文件

何时引用头文件

  • 如果一个函数在头文件中被声明,那么在所有要调用或者定义这个函数的地方都要 #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 :

    1. C:/Users/ZTer/AppData/Local/Temp/a-06a49a.o

    2. 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 等等。

    1. #ifdef 如果该命令后的宏已经被定义过,那么编译它下面的代码直到 #endif
    2. #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文件定义头文件中的所有声明的函数和变量。
  • 头文件要用标准头文件结构围绕起来(就是“条件编译命令”一节中修复错误的做法)。
posted @ 2023-10-26 16:59  ZTer  阅读(311)  评论(0编辑  收藏  举报