一个c++工程可能会涉及到很多的基础库,但是c++不像python一样可以直接import,因此引入了Cmake,将多个库链接起来。
参考:CMake系列讲解 - 总目录(由浅入深,实例讲解)_cmake 项目目录-CSDN博客
【C++】为什么需要CMake?-CSDN博客
1.c++的编译过程
使用g++
等编译工具,从源码生成最终的可执行文件一般有这几步:预处理(Preprocess)、编译(Compile)、汇编(assemble)、链接(link)
预处理: 处理一些#号定义的命令或语句(如#define、#include、#ifdef等),生成.i文件
编译:进行词法分析、语法分析和语义分析等,生成.s的汇编文件
汇编:将对应的汇编指令翻译成机器指令,生成二进制.o目标文件
链接:调用链接器对程序需要调用的库进行链接。链接分为两种
静态链接
在链接期,将静态链接库中的内容直接装填到可执行程序中。
在程序执行时,这些代码都会被装入该进程的虚拟地址空间中。
动态链接
在链接期,只在可执行程序中记录与动态链接库中共享对象的映射信息。
在程序执行时,动态链接库的全部内容被映射到该进程的虚拟地址空间。其本质就是将链接的过程推迟到运行时处理
输入g++ --help
可以看到对应命令:
-E Preprocess only; do not compile, assemble or link.
-S Compile only; do not assemble or link.
-c Compile and assemble, but do not link.
-o <file> Place the output into <file>.
例1: 对于简单的文件,没有引入其他库的,如下程序main.cpp,编译过程如下所示
#include <iostream>
int main() {
std::cout << "Hello World!" << std::endl;
return 0;
}
第一步:预处理 :C++中预处理指令以 #
开头。在预处理阶段,会对#define
进行宏展开,处理#if,#else
等条件编译指令,递归处理#include
。这一步需要我们添加所有头文件的引用路径。
# 将xx.cpp源文件预处理成xx.i文件(文本文件),其中main.cpp , main.i为文件路径
g++ -E main.cpp -o main.i
第二步:编译:检查代码的规范性和语法错误等,检查完毕后把代码翻译成汇编语言文件。
# 将xx.i文件编译为xx.s的汇编文件(文本文件)
g++ -S main.i -o main.s
第三步:汇编:基于汇编语言文件生成二进制格式的目标文件。
# 将xx.s文件汇编成xx.o的二进制目标文件
g++ -c main.s -o main.o
第四步:链接:将目标代码与所依赖的库文件进行关联或者组装,合成一个可执行文件
# 将xx.o二进制文件进行链接,最终生成可执行程序
g++ main.o -o main
最后将生成的可执行文件路径直接输入终端便可以执行
2.Cmake的安装
在ubuntun系统中
方法一:
sudo apt install cmake -y
方法二:指定版本的安装
# 以v3.25.1版本为例
git clone -b v3.25.1 https://github.com/Kitware/CMake.git
cd CMake
# 你使用`--prefix`来指定安装路径,或者去掉`--prefix`,安装在默认路径。
./bootstrap --prefix=<安装路径> && make && sudo make install
# 验证Cmake版本
cmake --version
例2:简单例子,文件中不含有其他库,如例1中的main.cpp,用Cmake来执行,由于main.cpp中没涉及到其他库,因此不用静态链接或动态链接
第一步:创建CMakeLists.txt文件,一般与main.cpp在同一文件夹,文件内容如下
cmake_minimum_required(VERSION 3.10) #Cmake 最低版本要求号
project(
first_camke #项目名称
VERSION 1.0.0 #此项目的版本号
DESCRIPTION "项目描述" #项目描述
LANGUAGES CXX #项目所使用的语言,注意languages是复数加s,CXX代表C++
)
#添加一个可执行程序(生成可执行程序),main2是可执行程序名称,main2.cpp是源文件,相当于g++ main2.cpp -o main2;
add_executable(main2 main2.cpp)
第二步:执行 cmake -S . -B build 指令
cmake -S <source-dir> -B <build-dir>
是 CMake 命令的一种用法,用于指定 CMake 构建系统的源码目录和构建目录。-S <source-dir>:指定 CMakeLists.txt 所在的源码目录。<source-dir> 是包含 CMakeLists.txt 文件的目录路径。CMake 将在该目录中查找和读取 CMakeLists.txt 文件,并基于其中的指令来配置项目的构建过程。
-B <build-dir>:指定生成的构建文件(Makefile、Visual Studio 项目文件等)存放的目录。<build-dir> 是用于生成和存储构建文件的目录路径。在这个目录中执行生成系统的命令(如 make、cmake --build)会根据 CMakeLists.txt 中的指令生成项目的可执行文件或库文件。
运行
cmake -S <source-dir> -B <build-dir>
时,CMake 将会在<source-dir>
中寻找 CMakeLists.txt 文件并解析其中的内容,然后在<build-dir>
中生成相应的构建文件,以便进行项目的构建和编译。
第三步:执行cmake --build build指令
cmake -B <build-dir>
在<build-dir>文件夹中生成构建文件
这个命令的执行结果通常是将项目源代码编译成可执行文件或库文件,具体取决于 CMakeLists.txt 文件中的配置和指令。
第四步:找到上一步<build-dir>文件夹中的生成的可执行文件,在终端输入地址运行
例3:复杂案例,main.cpp中包含Account类的头文件Account.h与源文件Account.cpp
main.cpp,Account.h,Account.cpp三个文件的结构与内容如下
├── account_dir
│ ├── Account.cpp
│ └── Account.h
└── main
└── main.cpp
main.app的内容如下
#include "Account.h"
#include <iostream>
using namespace std;
//argc 代表argument cout, 参数数量
//argc 代表argument vector, 参数列表
int main()
{
Account account1;
cout << "This is main 函数" <<endl;
return 0;
}
Account.h的内容如下
#ifndef Account_H
#define Account_H
class Account
{
private:
/* data */
public:
Account(/* args */);
~Account();
};
#endif //Account_H
Account.cpp的内容如下
#include "Account.h"
#include <iostream>
Account::Account(){
std::cout << "构造函数Account::Account()" << std::endl;
}
Account::~Account(){
std::cout << "析构函数Account::~Account()" << std::endl;
}
Account.h文件中的#ifndef Account_H解释:
#ifndef Account_H:这是一个预处理指令,用于检查名为 Account_H 的宏是否已经被定义。如果 Account_H 没有被定义过(即未定义),则会执行 #ifndef 后面的代码块。
#define Account_H:如果 Account_H 没有被定义过(即上面的条件成立),则会定义 Account_H 这个宏,防止再次包含同一个头文件时,其内容被重复定义。
这两个指令通常与 #endif 配合使用,用于创建头文件的保护包装,代码格式如下:
#ifndef Account_H
#define Account_H// 这里放置头文件的内容
#endif // Account_H
在上面的例子中,当第一次包含 Account.h 头文件时,Account_H 宏未定义,因此 #ifndef Account_H 条件为真,然后 #define Account_H 定义了 Account_H 宏。接下来的代码会被包含在 #ifndef 和 #endif 之间。
当再次包含 Account.h 头文件时,由于 Account_H 宏已经被定义过,因此 #ifndef Account_H 条件为假,预处理器会跳过 #ifndef 和 #endif 之间的代码,防止头文件内容被重复定义。
这种预处理指令的使用可以有效地避免头文件被多次包含,从而防止因重复包含导致的编译错误或定义冲突。