『C/C++』一文初识Makefile与CMake
本文来源于 B 站视频,鄙人只是跟着做一下笔记:
感觉这个老师讲的真的好
,要是杭电也都是这样的的老师就好了(
编译链接的预备知识
在手动编译一些 C/C++ 项目时,你会经常遇到下面的命令
1 | cmake . |
这两个都是用来管理和编译 C/C++ 项目的工具,本文将浅浅地介绍这两个工具的使用
首先引出一个小项目作为例子,项目结构如下
1 | ./ |
目前共有 4 个文件,首先来看 printhello.cpp
1 |
|
这是一个很简单的文件,仅有一个 printhello()
函数,功能也很简单
但请记住这个定义了但没使用 i
变量,后面会出场
接着看 factorial.cpp
,它包含一个简单的递归函数,用于求阶乘
1 |
|
现在来看 function.h
,它将上面两个函数注册其中,供下面的主函数调用
1 |
|
最后来看 main.cpp
,它通过引用头文件,调用了最开始的两个函数
1 |
|
也就是说,我们现在有两个函数分别定义在不同的文件中,并且在主函数中调用了他们
所有的函数定义都写在头文件中,main.cpp
引用了该头文件,就能调用那两个函数
那么现在要编译这个项目,一个最偷懒的方法如下:
1 | g++ main.cpp factorial.cpp printhello.cpp -o main |
那这样的确可以成功运行,但如果源文件很多的话怎么办?文件一多,如果每次都要一起编译,就很浪费时间
一种省时间的方式是不一起来编译,而是分别编译,生成目标文件,最后再手动链接在一起
1 | g++ -c main.cpp |
也就是说,在源文件很多的时候,可以只单独编译修改了的文件,然后再链接在一起,这样就可以省下其他文件的编译时间
那现在又有一个问题了,在文件很多的情况下,每次都手动输入这些命令是不是很麻烦
那这个过程其实完全可以写在一个脚本文件里面,一种通用的格式就叫 Makefile
Makefile
首先在当前目录新建一个 Makefile
文件(就叫这个名字),在里面编写脚本
下面来看看几个脚本版本,由浅入深地理解
VERSION 1
1 | ## VERSION 1 |
这是一个最基本最简单的 Makefile
,hello
指生成的可执行文件叫 hello ,冒号后面的内容是依赖,也就是说 hello 依赖于后面的几个 cpp 文件
而下面一行,首先是一个 Tab(不能是空格) ,然后是生成目标的命令 g++ -o hello main.cpp printhello.cpp factorial.cpp
也就是说
1 | ## VERSION 1 |
那写了 Makefile
之后怎么编译呢?先把之前临时文件都清除,然后使用 make
命令
make
会自动在当前目录下找 Makefile 文件,当然你也可以使用 -f
参数手动指定文件
1 | make -f Makefile |
这样就能自动编译文件了
而如果你再运行一次,你就会发现它会返回下面的内容
这是因为 make
发现 hello
的修改日期比它的依赖都新,也就没有重新生成的必要了
这时给 main.cpp
加个空格修改一下,那么它的修改时间就比 hello
更新了,就需要重新生成,这个命令就会智能地重新运行
这是第一个版本,这个版本的缺点也很明显,就是如果我每多一个文件,就要修改一次 Makefile
,文件一多也不好管理,而且它的生成命令也是最原始最偷懒的版本,所有文件一起编译
下面来看第二个版本
VERSION 2
1 | ## VERSION 2 |
VERSION 2 看上去就更加地专业
首先定义了三个变量 CXX
、TARGET
和 OBJ
,其中 OBJ
包括3个元素
然后是 TARGET
依赖于 OBJ
,被依赖的文件如果没有生成,就会向下查找它依赖的文件,以及是怎么生成的,基本是个递归的过程,最终把每个目标都生成出来
现在把 main.cpp
做一点修改,再调用 make
命令
会发现只会编译你修改的文件,然后就去链接了,节省编译的时间
接下来是第3个版本,又升级了一下
VERSION 3
1 | ## VERSION 3 |
其中,$@
就是 TARGET
,就是冒号前面的东西;$^
就是依赖,就是冒号后面的内容,也就是 OBJ ,也就是少写了一些变量;%<
指的是依赖的第一个,但是这里其实也只有一个,也无所谓(
现在来运行一下,发现出现了警告,这是 -Wall
参数的作用,将所有的编译参数放在一起,可以方便日后修改
而下面的
1 |
|
是为了实现一个 make clean
的功能,也就是清空所有非源码文件
可以这样来理解:我现在有一个 clean
目标,当我 make clean
的时候就会去执行对应的生成指令
当然啦,并没有什么 clean
文件,只是去骗它来执行 rm -f *.o $(TARGET)
这条命令罢了哈哈哈
然后为什么要有一行 .PHONY: clean
捏,这是因为如果当前目录正好有一个 clean
文件,那就不会被骗去执行生成命令了
而又有一个 .PHONY
目标依赖于 clean
,即使当前有一个 clean
,.PHONY
还是不存在的(其实这东西永远也不会存在),那么还是会去调用 clean
的生成命令,也就被骗去执行了那条指令
现在,如果又要加新文件,只要在 OBJ
变量后面加文件名就好了,但是这样还不是最方便的,最方便的应当是自动编译目录下的所有文件
VERSION 4
1 | ## VERSION 4 |
这个版本只变动了一个地方,也就是 SRC
和 OBJ
两个变量那里
首先使用 wildcard
函数来找到当前目录下所有的 .cpp
文件
然后下面使用 patsubst
函数,也就是 pattern substitution
后面跟 3 个参数:要替换的模式,替换的内容,要替换的文本
也就是吧 SRC
中的 .cpp
全部换成 .o
,那么也就是对应的目标文件的路径
这样一来,就能实现上面说的功能了
CMake
Makefile
有一个致命的问题,就是与当前的开发环境强相关,比如到其他系统可能会出现编译器不同、路径不同等等各种问题
CMake
就是解决这种问题的工具(当然也还有很多同类型的工具),让你能够跨平台地生成自动生成 Makefile
文件
还是上面的例子,我们来用 CMake ,先创建一个 CMakeLists.txt
文件
然后很简单,只用三行:
1 | cmake_minimum_required(VERSION 3.0) |
首先指定最低版本,然后说现在有一个 project
叫 hello
,再加上可执行程序 hello
,并且加上依赖的 cpp
就好了
然后使用 cmake .
来自动生成
可以看见生成了不少东西,有一些缓存文件,还有个目录,当然还有个 Makefile
如果你去看这个文件,你会发现非常之长
非常长,不管了,现在来用一下看看
可以的,现在可以实现我们的目标了:跨平台地编译
然后现在发现多了很多相关的文件,如果都需要手动清理,就很麻烦
于是,就有了下面这种操作
1 | mkdir build && cd build |
这样,所有生成的文件就只在这个 build
目录里面,删起来就很方便
这也就是很多软件在 cmake
之前会创建一个目录的原因