编译器,是一个根据源代码生成机器码的程序。
g++ main.cpp -o a.out
该命令会调用编译器程序 g++,让他读取 main.cpp 中的源码,并根据 C++标准生成相应的机器指令码,输出到 a.out 这个文件中,称为可执行文件。
./a.out
之后执行该命令,操作系统会读取刚刚生成的可执行文件,从而执行其中编译器成机器码,调用系统提供的printf函数,并在终端显示出Hello, world.
单文件编译虽然方便,但也有如下缺点:
因此,我们提出多文件编译的概念,文件之间通过符号声明相互引用。
g++ -c hello.cpp -o hello.o g++ -c main.cpp -o main.o
其中使用 -c 选项指定生成临时的对象文件 main.o,之后再根据一系列对象文件进行链接,得到最终的a.out:
g++ hello.o main.o -o a.out
文件越来越多时,一个个调用g++编译链接会变得很麻烦。
于是,发明了 make 这个程序,你只需写出不同文件之间的依赖关系,和生成各文件的规则。
make a.out
敲下这个命令,就可以构建出 a.out 这个可执行文件了。
和直接用一个脚本写出完整的构建过程相比,make 指明依赖关系的好处:
但坏处也很明显:
为了解决 make 的以上问题,跨平台的 CMake 应运而生!
读取当前目录的 CMakeLists.txt,并在 build 文件夹下生成 build/Makefile
cmake -B build
让make读取build/Makefile,并开始构建 a.out
make -C build
以下命令和上一个等价,但更跨平台:
cmake --build build
执行生成的 a.out
build/a.out
有时候我们会有多个可执行文件,他们之间用到的某些功能是相同的,我们想把这些公用的功能做成一个库,方便大家一起共享。
CMake除了 add_execute 可以生成可执行文件外,还可以通过 add_library 生成库文件。
add_library 的语法与 add_execute 大致相同,除了他需要指定是动态库还是静态库:
add_library(test STATIC source1.cpp source2.cpp)
# 生成静态库 libtest.a
add_library(test SHARED source1.cpp source2.cpp)
# 生成动态库 libtest.so动态库有很多坑,特别是Windows环境下,初学者自己创建库时,建议使用静态库。
但是他人提供的库,大多是作为动态库的,我们之后会讨论如何使用他人的库。
创建库以后,要在某个可执行文件中使用该库,只需要:
target_link_libraries(myexec PUBLIC test)
# 为 myexec 链接刚刚制作的库 libtest.a在多文件编译章中,说到了需要在 main.cpp 声明 hello() 才能引用。为什么?
其实,C++是一中强烈依赖上下文信息的编程语言。举个例子:
vector<Myclass> a; // 声明一个由MyClass组成的数组
如果编译器不知道 vector 是个模板类,那他完全可以把 vector 看做一个变量名,把 < 解释成小于号,从而理解成判断 vector 这个变量的值是否小于 MyClass 这个变量的值。
正因如此,我们常常可以在C++代码中看到这样的写法:
typename decay<T>::type
因为T是不确定的,导致编译器无法确定 decay<T> 的 type 是一个类型,还是一个值。因此用 typename 修饰来让编译器确信这是一个类型名。
没错,C 语言的前辈们也想到了,他们说,既然每个 .cpp 文件的这个部分是一模一样的,不如我把 hello() 的声明放到单独一个文件 hello.h 里,然后在需要用到 hello() 这个声明的地方,打上一个记号,#include "hello.h",然后用一个小程序,自动在编译前把这个引号内的文件名 hello.h 的内容插入到记号所在的位置,这样不就只用编辑 hello.h 一次了嘛!
实际上 cstdio 也无非是提供了 printf 等一系列函数声明的头文件而已,实际的实现是在 libc.so 这个动态库里。其中 <cstdio> 这种形式表示不要在当前目录下搜索,只在系统目录里搜索,"hello.h"这种形式则优先搜索当前目录下有没有这个文件,找不到再搜索系统目录。
此外,在实现的文件 hello.cpp 中也导入声明的文件 hello.h 是个好习惯。
在C++中常常用到很多的类,和函数一样,类的声明也会被放到头文件中。
有时候我们的函数声明需要使用到某些类,就需要用到声明了该类的头文件,像这样递归地 # include 即可:
但是这样造成一个问题,就是如果多个头文件都引用了 MyClass.h ,那么 MyClass 会被重复定义两遍,解决方案:在头文件前面加上一行:# pragma once
这样当预处理器第二次读到同一个文件时,就会自动跳过。通常头文件都不想被重复导入,因此建议在每个头文件前加上这句话。
复杂的工程中,我们需要划分子模块,通常一个库一个目录。这里我们把 hellolib 库的东西移到 hellolib 文件夹下了,里面的 CMakeLists.txt 定义了 hellolib 的生成规则。要在根目录使用他,可以用 CMake 的 add_subdirectory 添加子目录,子目录也包含一个CMakeLists.txt,其中定义的库在 add_subdirectory 之后就可以在外面使用。
子目录的 CMakeLists.txt 里路径名(比如 hello.cpp)都是相对路径,这也是很方便的一点。
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_subdirectory(hellolib) -> add_library(hellolib STATIC hello.cpp)
add_executable(a.out main.cpp)
target_link_libraries(a.out PUBLIC hellolib)因为 hello.h 被移到了 hellolib 子文件夹里,因此 main.cpp 里也要改成# include "hellolib/hello.h",如果要避免修改代码,我们可以通过 target_include_directories 指定 a.out 的头文件搜索目录:(其中第一个 hellolib 是库名,第二个是目录)
add_executable(a.out main.cpp)
target_link_libraries(a.out PUBLIC hellolib)
target_include_directories(a.out PUBLIC hellolib)这样甚至可以用 <hello.h> 来引用这个头文件了,因为通过 target_include_directories 指定的路径会被视为与系统路径等价:# include <hello.h>
但是这样如果另一个 b.out 也需要用 hellolib 这个库,难道也得再指定一遍搜索路径吗?
不需要,其实我们只需要定义 hellolib 的头文件搜索路径,引用他的可执行文件 CMake 会自动添加这个路径:
add_library(hellolib STATIC hello.cpp)
target_include_directories(hellolib PUBLIC .)这里用了 . 表示当前路径,因为子目录里的路径是相对路径,类似还有 .. 表示上一层目录。此外,如果不希望让引用 hellolib 的可执行文件自动添加这个路径,把 PUBLIC 改成 PRIVATE 即可。这就是他们的用途:决定一个属性要不要在被 link 的时候传播。
除了头文件搜索目录外,还有这些选项,PUBLIC 和 PRIVATE 对他们同理。
target_include_directories(myapp PUBLIC /usr/include/eigen3)
# 添加头文件搜索目录
target_link_libraries(myapp PUBLIC hellolib)
# 添加要链接的库
target_add_definitions(myapp PUBLIC MY_MACRO=1)
# 添加一个宏定义
target_add_definitions(myapp PUBLIC -DMY_MACRO=1)
# 与 MY_MACRO=1 等价
target_complie_options(myapp PUBLIC -fopenmp)
# 添加编译器命令行选项
target_sources(myapp PUBLIC hello.cpp other.cpp)
# 添加要编译的源文件以及可以通过下列指令(不推荐使用),把选项加到所有接下来的目标去:
include_directories(/opt/cuda/include)
# 添加头文件搜索目录
link_directories(/opt/cuda)
# 添加库文件的搜索路径
add_definitions(MY_MACRO=1)
# 添加一个宏定义
add_compile_options(-fopenmp)
# 添加编译器命令行选项有时候我们不满足于 C++ 标准库的功能,难免会用到一些第三方库。
最友好的一类库莫过于纯头文件库了,这里是一些好用的 header-only 库:
第二友好的方式则是作为 CMake 子模块引入,也就是通过 add_subdirectory
方法就是把那个项目(以fmt为例)的源码放到你工程的根目录:
这些库能够很好的支持作为子模块引入:
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_subdirectory(fmt)
add_executable(a.out main.cpp)
target_link_libraries(a.out PUBLIC fmt)cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_executable(a.out main.cpp)
target_link_libraries(a.out PUBLIC glm/include)build CMakeLists.txt glm main.cpp run.sh
# include <glm/vec3.hpp>
# include <iostream>
inline std::ostream &operator<<(std::ostream &os, glm::vec3 const &v){
return os << v.x << ' ' << v.y << ' ' << v.z;
}
int main() {
glm::vec3 v(1,2,3);
v += 1;
std::cout << v << std::endl;
return 0;
}fmt::format 的用法和 Python 的 str.format 大致相似:
# include <fmt/core.h>
# include <iostream>
int main() {
std::string msg = fmt::format("The answer is {}.\n", 42);
std::cout << msg << std::endl;
return 0;
}可以通过 find_package 命令寻找系统中的包/库:
为什么是 fmt::fmt 而不是简单的 fmt?
你可以指定要用哪几个组件:
不同的包之间常常有着依赖关系,而包管理器的作者为 find_package 编写的脚本(例如 /usr/lib/cmake/TBB/TBBConfig.cmake)能够自动查找所有依赖,并利用刚刚提到的 PUBLIC PRIVATE 正确处理依赖项,比如如果你引用了 OpenVDB::openvdb 那么 TBB::tbb 也会被自动引用。其他包的引用格式和文档参考:
https://cmake.org/cmake/help/latest/module/FindBLAS.html
Linux 可以用系统自带的包管理器(如 apt)安装 C++ 包。
pacman -S fmt
Windows 则没有自带的包管理器,因此可以用跨平台的 vcpkg:
https://github.com/microsoft/vcpkg
使用方法:下载 vcpkg 的源码,放到你的项目根目录,像这样:
cd vcpkg
.\bootstrap-vcpkg.bat
.\vcpkg integrate install
.\vcpkg install fmt:x64-windows
cd ..
cmake -B build -DCMAKE_TOOLCHAIN_FILE="%CD%/vcpkg/scripts/buildsystems/vcpkg.cmake"