C++头文件和编译
HPP头文件
.hpp
,其实质就是将 .cpp
的实现代码混入
.h
头文件当中,定义与实现都包含在同一文件,则该类的使用只需要调用
#include<xxx.hpp>
以引用该文件即可,无需再将
.cpp
加入到project中进行编译。
而实现代码将直接编译到调用者的 .obj
文件中,不再生成单独的 .obj
,采用 .hpp
将大幅度减少调用 project中的 .cpp
文件数与编译次数,也不用再发布烦人的 .lib
与
.dll
文件,因此非常适合用来编写公用的开源库。
使用注意
.hpp
头文件的优点不少,但是编写中有以下几点要注意:
与
.h
类似,但.hpp
是 Header Plus Plus 的简写,是 C++程序头文件 。是VCL专用的头文件,已预编译。
是一般 模板类 的头文件。
一般来说,
.h
里面只有声明,没有实现,而.hpp
里声明实现都有,后者可以减少.cpp
的数量。.h
里面可以有using namespace std;
,而.hpp
里则无。不可包含 全局对象 和 全局函数 。由于
.hpp
本质上是作为.h
被调用者所include,所以当.hpp
文件中存在全局对象或者全局函数,而该.hpp
被多个调用者include时,将在链接时导致符号重定义错误。要避免这种情况,需要去除全局对象,将全局函数封装为类的静态方法。类之间不可循环调用
在.h和.cpp的场景中,当两个类或者多个类之间有循环调用关系时,只要预先在头文件做被调用类的声明即可,如下:
1 | class B; |
在 .hpp
场景中,由于定义与实现都已经存在于一个文件,调用者必需明确知道被调用者的所有定义,而不能等到
.cpp
中去编译。因此hpp中必须整理类之间调用关系,不可产生循环调用。同理,对于当两个类A和B分别定义在各自的
.hpp
文件中,形如以下的循环调用也将导致编译错误:
1 | //a.hpp |
- 不可使用静态成员
静态成员的使用限制在于如果类含有静态成员,则在 .hpp
中必需加入静态成员初始化代码,当该 .hpp
被多个文档include时,将产生符号重定义错误。唯一的例外是
const static
整型成员,因为在vs2003中,该类型允许在定义时初始化,如:
1 | class A{ |
由于静态成员的使用是很常见的场景,无法强制清除,因此可以考虑以下几种方式(以下示例均为同一类中方法)
- 类中仅有一个静态成员时,且仅有一个调用者时,可以通过 局域静态变量模拟
1 | //方法模拟获取静态成员 |
- 类中有多个方法需要调用静态成员,而且可能存在多个静态成员时,可以将每个静态成员封装一个模拟方法,供其他方法调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19someType 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();
};
- 第二种方法对于大部分情况是通用的,但是当所需的静态成员过多时,编写封装方法的工作量将非常巨大,在此种情况下,建议使用 Singleton模式,将被调用类定义成普通类,然后使用Singleton将其变为全局唯一的对象进行调用。
如原 .h
和 .cpp
中的定义如下:
1 | class A{ |
采用singleton方式,实现代码可能如下(singleton实现请自行查阅相关文档)
1 | //实际实现类 |
分离式编译
在介绍分离式编译之前需要先介绍一下
分离式代码,在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.cpp
和
xxx2.cpp
一同编译,然后生成一个个对象文件( .o
或 .obj
)。上面这种方法会生成三个文件 main.o
、xxx1.o
和 xxx2.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 格式
- targets:规则的目标,可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签;
- prerequisites:是我们的依赖文件,要生成 targets 需要的文件或者是目标。可以是多个,也可以是没有;
- command:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。
注意:我们的 目标(target) 和 依赖文件(prerequisite) 之间要使用 冒号
:
分隔开,命令的开始(before the command) 一定要使用Tab
键。