Parallel101-1.学C++从CMake学起

Parallel101-1.学C++从CMake学起

Parallel101 笔记+课后作业,课程传送门:

Make,构建系统

构建系统是为了简化多文件编译:

  • 在多文件编译时,每有一个文件改动就要重新编译所有文件,解决办法就是将每个文件单独编译成.o 文件,再链接这些.o 文件为可执行文件,若改动了某个文件,那么只需要重新将那个文件编译成.o 文件即可
    优点:
  • 这样我们改几个文件还得自己重输那几个文件的编译命令,太麻烦,所以使用构建系统来帮我们监控哪些文件被改了,我们只需要写一份 makefile 就可以用 make 命令来编译好所有应该编译的文件
    缺点:
  • 需要编写各个文件之间的依赖关系,有头文件时特别头疼,如果我改了一个文件的头文件 include,重新写 makefile
  • 语法太简单,没有条件判断
  • 不能跨平台
  • 不能跨编译器,为 g++编写的 makefile 中有 g++的参数,不能用于 clang++
1
2
3
4
5
6
7
8
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 版本等
1
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 文件来生成插桩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 在使用别的文件的函数时需要知道函数的参数和返回值类型:这样才能支持重载,隐式类型转换等特性。

    1
    2
    3
    4
    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 代码就不会用重载等特性了

还有一些就不赘述了,如:

  • <stdio.h> “stdio.h” 三者的区别
  • 头文件会递归引用, 如何避免递归引用会产生的重复引用

CMake 的子模块

复杂的工程需要划分子模块,通常一个库一个目录

  • 根目录要使用子目录的库,可以用add_subdirectory添加子目录,然后在子目录也写一个 CMakeLists.txt, 其中定义的库在add_subdirectory后就可以在外面使用。
  • 子目录里面的路径都是相对路径(相对于子目录),就会更方便一点

如果根目录的 main.cpp 要 include 子目录的.h 就需要写相对于根目录的.h 路径,这就有点麻烦了,我们可以将子目录添加到头文件搜索路径来解决这个问题:

1
target_include_directories(hellolib PUBLIC 子目录)

这时,头文件甚至可以通过尖括号引入,因为上面那一行相当于把子目录当做了系统文件夹路径

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

如何使用第三方库

作为头文件引入,需要重新编译,而且头文件包含函数实现,所以非常得慢 菱形依赖的问题 解决菱形依赖的问题 pacman安装的库是直接编译好的,而vcpkg安装是先获取源码,然后在本地编译

作业 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

Parallel101-1.学C++从CMake学起

https://fly.meow-2.com/post/cpp/Parallel101-1.html

作者

Meow-2

发布于

2022-02-27

更新于

2022-11-06


评论