C++头文件和编译

HPP头文件

.hpp,其实质就是将 .cpp 的实现代码混入 .h 头文件当中,定义与实现都包含在同一文件,则该类的使用只需要调用 #include<xxx.hpp> 以引用该文件即可,无需再将 .cpp 加入到project中进行编译。

而实现代码将直接编译到调用者的 .obj 文件中,不再生成单独的 .obj,采用 .hpp 将大幅度减少调用 project中的 .cpp 文件数与编译次数,也不用再发布烦人的 .lib.dll 文件,因此非常适合用来编写公用的开源库。

使用注意

.hpp 头文件的优点不少,但是编写中有以下几点要注意:

  1. .h 类似,但 .hppHeader Plus Plus 的简写,是 C++程序头文件 。

  2. VCL专用的头文件,已预编译。

  3. 是一般 模板类 的头文件。

  4. 一般来说,.h 里面只有声明,没有实现,而 .hpp 里声明实现都有,后者可以减少 .cpp 的数量。

  5. .h 里面可以有 using namespace std;,而 .hpp 里则无。

  6. 不可包含 全局对象全局函数 。由于 .hpp 本质上是作为 .h 被调用者所include,所以当 .hpp 文件中存在全局对象或者全局函数,而该 .hpp 被多个调用者include时,将在链接时导致符号重定义错误要避免这种情况,需要去除全局对象,将全局函数封装为类的静态方法

  7. 类之间不可循环调用

在.h和.cpp的场景中,当两个类或者多个类之间有循环调用关系时,只要预先在头文件做被调用类的声明即可,如下:

1
2
3
4
5
6
7
8
9
10
11
class B;

class A{
public:
void someMethod(B b);
};

class B{
public :
void someMethod(A a);
};

.hpp 场景中,由于定义与实现都已经存在于一个文件,调用者必需明确知道被调用者的所有定义,而不能等到 .cpp 中去编译。因此hpp中必须整理类之间调用关系,不可产生循环调用。同理,对于当两个类A和B分别定义在各自的 .hpp 文件中,形如以下的循环调用也将导致编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
//a.hpp
#include "b.hpp"
class A{
public :
void someMethod(B b);
};

//b.hpp
#include "a.hpp"
class B{
public :
void someMethod(A a);
};
  1. 不可使用静态成员

静态成员的使用限制在于如果类含有静态成员,则在 .hpp 中必需加入静态成员初始化代码,当该 .hpp 被多个文档include时,将产生符号重定义错误。唯一的例外是 const static 整型成员,因为在vs2003中,该类型允许在定义时初始化,如:

1
2
3
4
class A{
public:
const static int intValue = 123;
};

由于静态成员的使用是很常见的场景,无法强制清除,因此可以考虑以下几种方式(以下示例均为同一类中方法)

  1. 类中仅有一个静态成员时,且仅有一个调用者时,可以通过 局域静态变量模拟
1
2
3
4
5
//方法模拟获取静态成员
someType getMember(){
static someType value(xxx);//作用域内静态变量
return value;
}
  1. 类中有多个方法需要调用静态成员,而且可能存在多个静态成员时,可以将每个静态成员封装一个模拟方法,供其他方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
someType getMemberA() {
static someType value(xxx);//作用域内静态变量
return value;
}

someType getMemberB(){
static someType value(xxx);//作用域内静态变量
return value;
}

void accessMemberA(){
someType member = getMemberA();//获取静态成员
};

//获取两个静态成员
void accessStaticMember(){
someType a = getMemberA();//获取静态成员
someType b = getMemberB();
};

  1. 第二种方法对于大部分情况是通用的,但是当所需的静态成员过多时,编写封装方法的工作量将非常巨大,在此种情况下,建议使用 Singleton模式,将被调用类定义成普通类,然后使用Singleton将其变为全局唯一的对象进行调用。

如原 .h.cpp 中的定义如下:

1
2
3
4
5
6
7
class A{
public :
type getMember(){
return member;
}
static type member;//静态成员
}

采用singleton方式,实现代码可能如下(singleton实现请自行查阅相关文档)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//实际实现类
class Aprovider{
public :
type getMember(){
return member;
}
type member;//变为普通成员
}

//提供给调用者的接口类
class A{
public :
type getMember(){
return Singleton<AProvider >::getInstance()->getMember();
}
}

分离式编译

在介绍分离式编译之前需要先介绍一下 分离式代码,在C++代码中,声明和定义是可以分开写在多个文件中,当然也可以写在同一个文件里面的,如 .hpp

往往是为了逻辑条理的清晰而分开书写,但使用 g++ 和 Terminal 直接编译 main.cpp 时,都是使用的 g++ main.cpp -o main 的命令,也就是只能编译该 main.cpp 一个文件,以生成 main 为名的二进制可运行文件,实际上,书写在其他 .cpp 文件中的代码也需要一同编译,这些书写在其他 .cpp 文件中的代码被称为 分离式代码,只编译 main.cpp 就会出现 undefined reference 之类的错误。

命令行G++

在 Windows / Linux / Mac 的 Terminal 下都可以使用 g++ 命令进行编译,格式如下:

1
g++ -c main.cpp xxx1.cpp xxx2.cpp

使用 -c 选项,将包含 main()main.cpp 与 其他分离式代码文件 xxx1.cppxxx2.cpp 一同编译,然后生成一个个对象文件( .o.obj )。上面这种方法会生成三个文件 main.oxxx1.oxxx2.o ,每个 .o 都是一个对象文件,但不一定可执行(因为缺少 main() 函数),仍需要通过进一步 链接 成可执行文件:

1
g++ -o main main.o xxx1.o xxx2.o

通过上面这一行代码可以生成名为 main 的可执行二进制文件。

以上两句命令也可以通过下面这句命令替代:

1
g++ -o main main.cpp xxx1.cpp xxx2.cpp

Makefile

Makefile 文件描述了 Linux 系统下 C/C++ 工程的编译规则,它用来自动化编译 C/C++ 项目。一旦写编写好 Makefile 文件,只需要一个 make 命令,整个工程就开始自动编译,不再需要手动执行 GCC 命令。

一个中大型 C/C++ 工程的源文件有成百上千个,它们按照功能、模块、类型分别放在不同的目录中,Makefile 文件定义了一系列规则,指明了源文件的编译顺序、依赖关系、是否需要重新编译等。

如果是在 Windows 下作开发的话不需要去考虑这个问题,因为 Windows 下的集成开发环境(IDE)。当然,Windows 下的 Visual Studio Code如果没配置好,也只是个编辑器而已,不算是个IDE。一般的MVS(Microsoft Visual Studio)都已经内置了 Makefile,或者说会自动生成 Makefile,不用去手动编写。

Linux 中却不能这样,需要去手动的完成这项工作。Linux 下可以学习的开发语言有很多,常见的有 C/C++语言、python、java 等等。在 Linux(Unix) 下做开发的话,不了解 Makefile 是一件非常失败的事情。不懂 Makefile,就操作不了多文件编程,就完成不了相对于大的工程项目的操作。Makefile 可以说是必须掌握的一项技能。

Makefile 可以简单的认为是一个工程文件的编译规则,描述了整个工程的 编译链接 等规则。其中包含了那些文件需要编译,那些文件不需要编译,那些文件需要 先编译,那些文件需要 后编译,那些文件需要 重建 等等。编译整个工程需要涉及到的,在 Makefile 中都可以进行描述。换句话说,Makefile 可以使得我们的项目工程的编译变得自动化,不需要每次都手动输入一堆源文件和参数。

Makefile 可以彻底简化编译的操作。把要链接的库文件放在 Makefile 中,制定相应的规则和对应的链接顺序。这样只需要执行 make 命令,工程就会自动编译。每次想要编译工程的时候就执行 make 命令,省略掉手动编译中的参数选项和命令,非常的方便。

Makefile 支持多线程并发操作,会极大的缩短编译时间,并且当修改了源文件之后,编译整个工程的时候,make 命令只会编译修改过的文件,没有修改的文件不用重新编译,也极大的解决了耗费时间的问题。

Makefile 格式

image-20211012134610578
  • targets:规则的目标,可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签;
  • prerequisites:是我们的依赖文件,要生成 targets 需要的文件或者是目标。可以是多个,也可以是没有;
  • command:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。

注意:我们的 目标(target) 和 依赖文件(prerequisite) 之间要使用 冒号 : 分隔开,命令的开始(before the command) 一定要使用 Tab 键。

Reference

  1. hpp.h与.h的区别
  2. Makefile教程:Makefile文件编写1天入门 (biancheng.net)