Parallel101 笔记 + 课后作业,课程传送门:
Make,构建系统
构建系统是为了简化多文件编译:
- 在多文件编译时,每有一个文件改动就要重新编译所有文件,解决办法就是将每个文件单独编译成.o 文件,再链接这些.o 文件为可执行文件,若改动了某个文件,那么只需要重新将那个文件编译成.o 文件即可 优点:
- 这样我们改几个文件还得自己重输那几个文件的编译命令,太麻烦,所以使用构建系统来帮我们监控哪些文件被改了,我们只需要写一份 makefile 就可以用 make 命令来编译好所有应该编译的文件 缺点:
- 需要编写各个文件之间的依赖关系,有头文件时特别头疼,如果我改了一个文件的头文件 include,重新写 makefile
- 语法太简单,没有条件判断
- 不能跨平台
- 不能跨编译器,为 g++编写的 makefile 中有 g++的参数,不能用于 clang++
a.out: hello.o main.o
g++ hello.o main.o -o a.out
hello.o: hello.cpp
g++ -c hello.cpp -o hello.o
main.o: main.cpp
g++ -c main.cpp -o main.o
CMake,构建系统的构建系统
只需要写一份 CMakeLists.txt
优点:
- 自动检测源文件和头文件的依赖关系,导出到 Makefile 里
- 相对高级的语法,内置的函数有 configure、install 等
- 跨平台,linux 上生成 makefile, windows 上生成 vsproj 等
- 通过参数指定要使用的编译器,Cpp 版本等
cmake -B build
库
- 有时候多个可执行文件,他们之间的某些部分是相同的,这些功能就可以做成一个库,让大家一起用
- 库中的函数可以被可执行文件调用,可以被其他库调用
静态链接库 (.a/.lib,Linux/Windows)
静态库相当于多个.o 文件的打包,在编译时将对应的代码插入可执行文件,可执行文件的体积会变大,编译完成后可以删除静态库
动态链接库 (.so/.dll,Linux/Windows)
运行时调用,编译时只在可执行文件中生成"插桩函数",当可执行文件被加载时会读取指定目录的动态链接库文件,加载到内存中的空闲位置,并且替换相应的"插桩"指向的地址为加载后的地址,这个过程被称为重定向。这样以后函数被调用就会跳转到动态加载的地址去。
指定目录:
- Windows:可执行文件同目录,其次是环境变量%PATH%
- Linux:ELF 格式可执行文件的 RPATH,其次是/usr/lib 等
windows/linux 下动态链接库的实现区别:
- windows:引用.dll 也需要一个配套的.lib,这个.lib 的作用就是作为插桩函数在编译的时候插入可执行文件中,相当于.dll 就是把.lib 中函数的实现给抽离的出来,从而节省了可执行文件的大小
- Linux:自动生成插桩而不需要.a 文件来生成插桩
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
//生成静态库
//add_library(hellolib STATIC hello.cpp)
//生成动态库
add_library(hellolib SHARED hello.cpp)
//生成可执行文件
add_executable(a.out main.cpp)
//为可执行文件链接库hellolib
target_link_libraries(a.out PUBLIC hellolib)
Cpp 为什么需要声明?
Cpp 是一种强烈依赖上下文的语言
多文件编译,main.cpp 要使用 hello.cpp 的函数,必须要要在 main.cpp 中声明这个函数
因为编译器是一个文件一个文件编译,然后链接,所以 main.cpp 在使用别的文件的函数时需要知道函数的参数和返回值类型:这样才能支持重载,隐式类型转换等特性。
void show(float x); //编译器就知道要把 3 转化成 3.0f show(3);
让编译器知道 show 是一个函数名而不是变量或类名,如果没有声明,编译器可以把他当作创建了一个名为 show 的临时对象
为什么需要头文件?
上面说到,如果一个文件需要用另一个文件里面实现的函数,那么就得声明这个函数,但如果有 100 个文件都要用到这个函数,那岂不是这 100 个文件都得写这个声明?
一个函数还好,如果 100 个文件每个文件都要用到某个文件里实现的 100 个函数,那不是声明得写得老长了?而且要改声明呢?
所以有了头文件,我们把这些声明写进头文件 hello.h 中,只需要#include "hello.h"
就好了
Cpp 中以#开头的都是预处理指令,由预处理器在编译前执行,#include "hello.h"
会把 hello.h 的内容替换到当前位置
这样每当我们更改了 hello.cpp 实现中的函数参数,只需要更改 hello.h 中的内容就行,而不用改 100 个文件的声明了
tips:
- 推荐在 hello.cpp 也就是函数实现所在的文件中插入
#include "hello.h"
这样可以避免沉默的错误,这样的话,当函数实现的更改时,编译器就会提示声明与定义不一样,不然的话,这个错误就只能在链接的时候发现了 - 使用 C 语言代码要写在
extern "C"{}
中,编译器知道这是 C 代码就不会用重载等特性了
还有一些就不赘述了,如:
<cstdio>
<stdio.h>
"stdio.h" 三者的区别- 头文件会递归引用,如何避免递归引用会产生的重复引用
CMake 的子模块
复杂的工程需要划分子模块,通常一个库一个目录
- 根目录要使用子目录的库,可以用
add_subdirectory
添加子目录,然后在子目录也写一个 CMakeLists.txt, 其中定义的库在add_subdirectory
后就可以在外面使用。 - 子目录里面的路径都是相对路径(相对于子目录),就会更方便一点
如果根目录的 main.cpp 要 include 子目录的.h 就需要写相对于根目录的.h 路径,这就有点麻烦了,我们可以将子目录添加到头文件搜索路径来解决这个问题:
target_include_directories(hellolib PUBLIC 子目录)
这时,头文件甚至可以通过尖括号引入,因为上面那一行相当于把子目录当做了系统文件夹路径

CMake 关于目标的其他的一些指令

如何使用第三方库





作业 01
题目:
解答:
A 文件要用 B 文件实现的函数,需要在 A 文件中对函数进行声明 所以为了方便 B 文件实现的函数被多个别的文件使用,可以将函数的声明和实现分离 需要用到的文件直接 include 函数的声明即可
stb 下的库都是单文件库 为了让声明和实现分离,方便多个文件使用,stb 下的库都会约定一个宏#if define XXXXX 当这个宏被定义时,后面的函数实现部分才会被编译
题目要求定义 stbiw 这个库,那么就需要函数的实现,在 stdiw 文件下新建一个源文件,在 include stb_image_write 之前 define 其约定的宏,该源文件就可以在编译时包含声明 + 实现,从而用该文件生成所需要的库
为了 stb_image_write.h 能被<>
找到,在子目录的 cmakelist 添加到头文件搜索路径并设为 PUBLIC 即可,这样子目录和主目录的文件都可以通过<>
include 到 stb_image_write.h