cmake之旅(1)

1 构建的过程

我们先写一个简单的的程序:

#include <iostream>
int main()
{
	std::cout << "Hello Cmake" << std::endl;
	return 0;
}

上面程序会在控制台中输出:Hello Cmake

那么如何运行上面的代码呢?你可能会说,在vs(或者其他的IDE)中创建一个项目,创建一个main.cpp,把上面的代码复制进行,然后点击运行就好了呀。是的没错,那么我们思考一下,我们点击运行按钮到控制台输出结果,vs在这个过程中,帮我们“偷偷的”的做了哪些事情?

vs大概帮助我们做了以下内容:预处理、编译、优化、汇编、链接、生成可执行文件、 加载与调试、运行程序、退出与清理不得不感叹这些IDE的方便性,只要点击一个按钮就做了那么多步骤。但是我们现在就是要“化简为繁”手动的做这些事情。亲自感受一下这个过程,这有利于我们后面对cmake的理解。

上面的内容可以分为两个过程:构建过程、运行过程。我们只讲一下构建的过程:

  • 预处理
  1. 宏替换:所有使用 #define 定义的宏在代码中被替换为它们对应的值或代码片段。
  2. 文件包含:#include 指令包含的头文件会被直接插入到代码中。这意味着编译器会将这些头文件的内容复制到使用 #include 指令的地方。
  3. 条件编译:处理 #ifdef、#ifndef、#if 等条件编译指令。这些指令允许根据某些条件编译或忽略代码块。
  4. 去注释:预处理器会删除所有的注释,因为它们在编译阶段是不需要的。
  • 编译
  1. 词法分析:编译器将预处理后的代码转换为一个个“记号”(token)。每个记号表示代码中的基本元素,如关键字、标识符、运算符、数字和符号。
  2. 语法分析:编译器检查代码的语法结构,确保它符合语言的语法规则。语法分析器生成一个抽象语法树(AST),这是源代码结构的层次化表示。
  3. 语义分析:编译器进一步检查代码的语义正确性,例如类型检查、变量和函数的作用域解析、函数参数匹配等。
  4. 中间代码生成:编译器将语法树转换为中间代码,这通常是一种接近机器语言但仍然与具体机器无关的代码形式,如三地址码或简单的指令集。
  • 优化
    中间代码可以被优化,例如移除多余的计算、循环展开、常量折叠、消除冗余代码等。这一步可以在中间代码层进行,也可以在生成机器代码后进行。

  • 汇编

  1. 汇编代码生成(Assembly Code Generation):编译器将中间代码转换为汇编代码,这是一种特定于目标处理器的低级别代码。汇编代码仍然是人类可读的,但每条指令直接对应于机器指令。
  2. 机器代码生成(Machine Code Generation):汇编器将汇编代码转换为二进制的机器代码,这些代码是计算机直接可以执行的指令。这些机器代码通常存储在目标文件(.obj 或 .o 文件)中。
  • 链接
  1. 符号解析(Symbol Resolution):链接器处理所有的外部符号引用,将每个函数调用和变量引用与其实际定义关联起来。比如,一个文件中调用的函数,可能在另一个文件中定义,链接器负责将这些调用和定义匹配。
  2. 重定位(Relocation):链接器调整每个目标文件中的地址和指针,使得它们在最终的可执行文件中是正确的。这一步包括调整代码和数据的内存地址,使得程序可以正确地在运行时访问它们。
  3. 库链接(Library Linking):如果你的程序依赖于静态库或动态库,链接器会将这些库的代码合并到最终的可执行文件中。对于静态库,库的代码被直接包含在可执行文件中;对于动态库,只会在运行时加载库的代码。
  4. 生成可执行文件:最终,链接器将所有的目标文件和库文件组合在一起,生成一个完整的可执行文件(如 .exe 文件),这个文件包含了所有必要的代码和数据。

2 手动构建

2.1 环境

  1. 系统:Ubuntu
  2. 编译器:gcc/g++

2.2 开始编译

  1. 创建文件:touch main.cpp 将上述代码复制到其中并保存
  2. 预处理:g++ -E main.cpp -o main.i
    -E 选项告诉编译器只进行预处理,不进行后续的编译、汇编和链接步骤。
    -o main.i 指定了预处理后的输出文件名为 main.i。如果不使用 -o 选项,预处理结果会输出到标准输出(通常是终端)

根据前面的讲解,我们在代码中加点内容(加一个宏,和注释),来验证上面的说法

#include <iostream>
#define NUM 12
int main()
{
	//注释
	std::cout << "Hello Cmake" << std::endl;
	std::cout << NUM << std::endl;
	return 0;
}

此时,重新运行上面的命令,你可以再文件夹下面看到一个main.i文件,可以打开mian.i,下拉到文件底部,里面生成了那些东西。你会看到头文件被加载了进来,宏被替换了,注释被去掉了

  1. 编译:g++ -S main.i -o main.s
    -S 选项表示将源代码编译为汇编代码。生成的 main.s 文件是汇编语言表示的代码,可以查看其中的低级指令
  2. 汇编:g++ -c main.s -o main.o
    -c 选项表示将汇编代码编译为目标文件而不进行链接。生成的 main.o 是二进制的机器代码,包含了程序的指令,但还没有链接成可执行文件。
  3. 链接:g++ main.o -o main
    链接阶段将目标文件与标准库链接,生成最终的可执行文件。
  4. 运行:./main

经过上面的步骤,应该就可以成功的生成可执行文件,并运行起来。你可能已经发现了,这样很麻烦,而且现在只是一个main.cpp文件,就要那么多步骤,如果文件很多呢,比如增加add.h、add.cpp、de.h,de.cpp:

文件结构如下:

├── add
│   ├── add.cpp
│   └── add.h
├── de
│   ├── de.cpp
│   └── de.h
├── main.cpp
└── Makefile

add.h:

#ifndef _HEAD_ADD_
#define _HEAD_ADD_
int add(int a,int b);
#endif

add.cpp

#include "add.h"
int add(int a,int b)
{
    return a+b;
}

de.h

#ifndef _HEAD_DE_
#define _HEAD_DE_
int de(int a,int b);
#endif

de.cpp

#include "de.h"
int de(int a,int b)
{
    return b-a;
}

main.cpp

#include <iostream>
#include "add.h"
#include "de.h"
#define NUM 12
int main()
{
	//注释
	std::cout << "Hello Cmake" << std::endl;
	std::cout << NUM << std::endl;
	std::cout << add(10,2) << " " << de(2,10) << std::endl;
	return 0;
}

构建过程

g++ -c add/add.cpp -o add/add.o
g++ -c de/de.cpp -o de/de.o
g++ -c main.cpp -I add -I de -o main.o
g++ main.o add/add.o de/de.o -o main
./main

随着文件增多,会越来越麻烦,而且容易出错。

3 使用 Makefile 简化构建

3.1 环境

  1. 系统:Ubuntu
  2. 编译器:gcc/g++
  3. 构建工具:make

3.2 编写Makefile

使用makefile构建,首先要在自己的电脑上安装make(网上有很多方法,不再赘述),然后编写Makefile文件。编写Makefile文件的过程和上面手动构建的过程类似,具体内容有详细注释:

增加Makefile文件

├── add
│   ├── add.cpp
│   └── add.h
├── de
│   ├── de.cpp
│   └── de.h
├── main.cpp
└── Makefile

Makefile文件:

# 定义编译器
CXX = g++

# 定义编译选项
CXXFLAGS = -Wall -g

# 定义目标文件名
TARGET = main

# 自动寻找当前目录及子目录下的所有.cpp文件
SRCS = $(wildcard */*.cpp) $(wildcard *.cpp)

# 自动生成对应的.o文件
OBJS = $(SRCS:.cpp=.o)

# 包含的头文件目录
INCLUDES = -Iadd -Ide

# 默认目标:生成可执行文件
$(TARGET): $(OBJS)
	$(CXX) $(CXXFLAGS) $(INCLUDES) -o $(TARGET) $(OBJS)

# 生成对象文件的规则
%.o: %.cpp
	$(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@

# 清理目标
clean:
	rm -f $(OBJS) $(TARGET)

直接make就可以生成对应的文件了,而且比之前更加的通用一点。

相信你通过这个一步一步走下来,肯定发现了它的不足

  1. 如果项目不断的增大,每个源文件的依赖不同,使用makefile管理这些依赖,就变得很困难;
  2. 后期维护的困难大大的增加(每次都要理清这里面的依赖关系)
  3. 不能跨平台,比如此时想将上面的代码在win上运行,使用makefile很困难
  • 命令和工具差异
    Makefile 中使用的 g++、rm 等命令通常在类 Unix 系统(如 Linux 或 macOS)上可用,而 Windows 上的命令行工具有所不同。
    rm 是 Linux 下的命令,用于删除文件,但在 Windows 上并不存在。Windows 使用 del 或 erase 来删除文件。
  • 路径分隔符
    Makefile 中使用的路径分隔符是正斜杠 /,而在 Windows 上,路径分隔符通常是反斜杠 \。虽然某些工具(如 g++)在 Windows 上也支持正斜杠,但这并不是通用的标准。
  • 构建工具
    make 是 GNU 工具链的一部分,默认在 Windows 上不可用。虽然你可以通过安装 MinGW、Cygwin 或 MSYS2 等工具在 Windows 上使用 make,但这依然需要额外的配置
  • 环境变量
    Windows 上的环境变量设置和类 Unix 系统不同,可能需要调整编译器路径、库路径等。

有没有一个更好方法,既不用繁琐的写着构建过程,还可以自动处理依赖关系,自动寻找第三方的库?

4 使用cmake构建

CMake 是一个跨平台的构建系统生成器,它可以根据简单的配置文件(CMakeLists.txt)自动生成适用于多种构建系统的文件,比如 Unix 系统上的 Makefile,Windows 上的 Visual Studio 项目文件等。

优势:

  • 自动处理依赖关系:CMake 自动管理源文件之间的依赖关系,只重新编译发生变化的部分。
  • 跨平台支持:CMake 可以生成适用于不同操作系统的构建文件,从而实现真正的跨平台构建。
  • 轻松集成第三方库:CMake 可以帮助你自动查找和集成第三方库,而不需要手动配置路径和编译选项。
  • 社区资源丰富:有很丰富的社区和学习资料

4.1 环境

  1. Ubuntu:
    安装:sudo apt-get install cmake
  2. win
    https://cmake.org/download/

自行百度如何安装。

4.2 编写CMakeLists.txt

# 设定最低版本要求
cmake_minimum_required(VERSION 3.10)

# 定义项目名称
project(HelloCMake)

# 设置编译选项
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# 添加源文件
set(SOURCES
    main.cpp
    add/add.cpp
    de/de.cpp
)

# 包含头文件目录
include_directories(add de)

# 生成可执行文件
add_executable(main ${SOURCES})

执行以下命令,就可以生成可执行文件了:

mkdir build
cd build
cmake ..
make

非常的简洁明了,不用我们手动管理依赖,也可以实现跨平台。通过上面的内容,应该对cmake有了基本了解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

m晴朗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值