0 . CMake入门--程序构建和项目结构

参考:
https://subingwen.cn/CMake/CMake-primer/?highlight=CMake
《modern Cmake for C++》
《Cmake构建实战-项目开发卷》

1 程序构建和 Makefile

1.1 程序构建

程序构建的流程

编译器是一套工具链, 分为4部分:

  • 预处理器
    • 输入: 代码源文件
    • 输出: 处理后的源文件
    • 功能: 头文件展开, 宏替换, 去除注释等
  • 编译器
    • 如常见的 gcc
    • 输入: 处理后的源文件
    • 输出: 汇编文件
    • 功能: 将源文件编译为汇编文件
  • 汇编器
    • 输入: 汇编文件
    • 输出: 二进制文件, 如 .obj, .o
    • 功能: 将源文件编译为汇编文件
  • 链接器
    • 输入: 二进制的 .obj
    • 输出: 打包后的可执行程序, 二进制的
    • 功能: 把这些⽬标⽂件链接在⼀起, 解析其中未定义的符号引⽤

例子

g++ -c main.cpp head.cpp

-c 表示编译, 将生成两个独立的 obj 文件

g++ main.o slow.o

这是链接过程, gcc 程序实际上调用了链接器 ld.exe (这里是 windows 系统下的后缀名), 最终生成可执行文件

按需编译

在上面例子中

  • main.o slow.o 是两个独立的编译后文件
  • 当一个源文件, 比如 main.cpp 改变时, 只需要编译 main.cpp 即可
  • 然后再重新链接

步骤

g++ -c main.cpp

g++ main.o slow.o

1.2 make 程序和 Makefile

Makefile 的作用

当源文件数量少的时候, 直接在 终端 中输入编译命令即可
但当文件数量非常多, 直接用命令则需要特别长的命令, 此时就需要 Makefile 脚本的帮助.

  • Makefile 文件的文件名就是 makefileMakefile
  • Makefile 是脚本, 写了一些指令
  • 在终端中使用命令 make, 它就会用 make 程序 执行 Makefile 脚本. 从而实现编译
不同的编译环境,这个make.exe的名称可能不同
在windows下,用msys2安装的clang64环境下的make工具,它的全称为`mingw32-make.exe`

Makefile 例子

main: main.o slow.o
    g++ -o main main.o slow.o

main.o: main.cpp
    g++ -c main.cpp -o main.o

slow.o: slow.cpp
    g++ -c slow.cpp -o slow.o

这里有三条指令

  • 冒号后面, 是构建的源文件 比如 main.o slow.o
  • 冒号前面, 是目标 比如 main
  • 缩进的行是对应的指令
make 程序 实现按需编译
  • 在 Makefile 文件中编写 编译和链接的规则
  • 使用 make 程序将自动执行 Makefile 中的规则
  • 它能自动按需编译
make如何实现按需编译?
- 它会检查源文件的**更新时间**, 以及已存在的目标的更新时间
- 如果源文件 比 目标文件的构建时间 更晚, 那么就会执行那条命令

2 项目的结构

项目文件

项目一般至少含有两类文件:

  • 源代码 (src): 包含了项目的核心逻辑,用编程语言编写,如 C++、Python、Java 等。
  • 编译生成文件 (build): 由编译器生成的中间文件或最终的可执行文件、库文件等。
    更多的情况:
文件类型 作用描述 示例
库文件 (lib) 提供可重用的代码和功能,分为静态库和动态库 C 标准库(如stdio库等),自定义的静态库mylib.a、动态库mylib.so
资源文件 (res) 包含项目运行所需的其他资源,如图片、音频、视频、字体、配置文件、数据文件等 游戏中的角色图片、背景音乐文件,软件的配置ini文件、存储用户信息的数据文件等
配置文件 (config) 用于配置项目的各种设置,包括编译选项、运行参数、环境变量等 CMakeLists.txt, 环境参数, IDE 配置, git 配置
项目文档 (doc) 描述项目结构、功能、使用方法等 README.md 文件,Java 项目生成的 API 文档等
测试文件 (test) 包含单元测试、集成测试等,用于验证代码的正确性 Python 项目中使用unittest框架编写的测试文件,Java 项目中基于JUnit的测试代码文件等

示例项目结构:

my_project/
├── src/
│   ├── main.cpp
│   └── utils.h
├── build/
│   ├── CMakeFiles
│   └── my_project
├── lib/
│   ├── my_lib.a
│   └── third_party_lib.so
├── res/
│   ├── images/
│   ├── sounds/
│   └── data/
├── config/
│   ├── CMakeLists.txt
│   └── app.config
├── doc/
│   ├── README.md
│   └── design.pdf
└── test/
    ├── unit_tests.cpp
    └── integration_tests.py
库文件, 源文件, 头文件
  • 源文件(.c/.cpp)
    • 包含了程序的源代码,是程序的实现部分。
    • 其中定义了函数、变量、类等。
    • 经过编译后生成目标文件(.o/.obj)。
  • 头文件(.h)
    • 包含函数、变量、类的声明,但不包含具体的实现。
    • 用于提供接口,让其他文件可以引用这些声明。
    • 通常被多个源文件包含。
  • 库文件(.lib/.a/.so/.dll)
    • 是由编译好的目标文件链接而成的。
    • 提供可重用的代码和功能。
    • 可以是静态库(.lib/.a)或动态库(.so/.dll)。

- **头文件和源文件的关系:**
    - 头文件中的声明对应源文件中的实现。
    - 源文件通常会包含对应的头文件,以便使用头文件中的声明。
    - 比如a.cpp 和 a.h
- **源文件和库文件的关系:**
    - 多个源文件编译后链接在一起,形成库文件。
    - 库文件中的函数可以在其他程序中被调用。
- **头文件和库文件的关系:**
    - *头文件提供了库的接口,告诉用户如何使用库中的函数*。
    - 库文件提供了函数的具体实现。
库文件比源文件的优势
  • 一些"库文件"不是以 libso 等形式的, 而是直接就是代码, 比如 C++的STL 库
  • 编译可以生成库文件 或 可执行程序
  • 相比于直接给出源代码, 在网络上分发 库文件有一些好处
    • 保密性: 隐藏了原始代码, 防止剽窃
    • 便于管理和使用: 如果源文件数量多, 则不便管理, 而将它们编译为一个 lib 则更方便分发和管理
静态库和动态库的概念

静态库 (Static Library)

  • 链接方式: 在编译时,静态库的代码会被复制到使用它的程序中,成为程序的一部分。
  • 扩展名: .lib (Windows) 或 .a (Linux/Unix)
  • 优点:
    • 程序运行时不需要依赖外部库文件,因为代码已经包含在程序内部。
    • 程序运行速度更快,因为不需要在运行时进行链接操作。
  • 缺点:
    • 编译后的程序体积较大,因为包含了库代码的副本。
    • 如果库更新,需要重新编译所有使用该库的程序。

动态链接库

  • 链接方式: 在编译时,动态库的代码不会被复制到程序中,而是在程序运行时才被加载。
  • 扩展名: .dll (Windows) 或 .so (Linux/Unix)
  • 优点:
    • 编译后的程序体积较小,因为不包含库代码。
    • 多个程序可以共享同一个动态库文件,节省内存和磁盘空间。
    • 动态库可以独立更新,而不需要重新编译使用它的程序。
  • 缺点:
    • 程序运行时需要依赖外部库文件,如果找不到库文件,程序将无法运行。
    • 程序运行速度稍慢,因为需要在运行时进行链接操作。

动态链接的原理
- 启动进程时,Windows 操作系统会*装载*进程所需的动态链接库, 并调⽤动态链接库的*⼊⼝函数*
- Windows 操作系统默认启⽤*地址空间布局随机化*, 因此装载所在的内存位置是随机的
如何使用静态库
  • 确保项目树中有对应的头文件, 在源码中使用 include 包含, 然后使用它的 API
  • 编译时,通过 -L 选项指定静态库文件所在的文件夹路径 (注意是 lib 文件的路径, 而不是库文件对应的头文件的路径)
  • 链接时, 使用 -l 选项指定要链接的库文件名(去掉前缀 lib 和文件后缀

例子
假设我们有一个库文件 libmymath.so,它位于 /usr/local/lib 目录下。要链接这个库,我们可以使用以下命令:

g++ main.cpp -L/usr/local/lib -lmymath
  • -L/usr/local/lib:告诉编译器去 /usr/local/lib 目录下寻找库文件。
  • -lmymath:告诉编译器要链接名为 mymath 的库。
为什么编译时 `-L` 指定了 .lib路径, 链接时还要 `-l` 指定链接的具体文件名
- `-L` 指定搜索路径,扩大编译器寻找库文件的范围
- `-l` 指定库名, 在找到库文件夹后,需要根据库名来确定要链接哪个库
如何使用动态库
  1. 准备动态库文件和对应的头文件, 并在代码中使用 #include
  2. 链接动态库, 有两种方法
    • 隐式链接: 和使用静态库类似
      • 在编译链接时,使用 -l 选项指定动态库名,使用 -L 选项指定动态库的搜索路径。
      • 编译器会在运行时自动加载动态库。
      • 运行隐式链接的程序必须保证: 程序运行时可以找到库文件 (需要加入环境变量 path 或者放在同一目录下)
    • 显式链接
      • 在程序运行时,使用系统提供的 API 函数 动态加载动态库
        • 如 Windows 的 LoadLibrary 打开一个 .dll 文件
        • Linux 系统下 dlopen 打开 .so 文件
      • 这种方式更灵活,但实现起来也相对复杂。
动态库的隐式链接和 使用静态库很相似但:
- 在链接静态库时,链接器会将静态库中的代码和数据*复制到最终的可执行文件*中
- 在隐式链接动态库时,编译器只是在可执行文件中记录了需要加载动态库的信息(如库的名称、路径等),并没有将库中的代码复制到可执行文件中

显式链接的例子
Linux 系统下 使用 dlopen 函数打开动态库,获取一个库句柄
再通过 dlsym 函数从库句柄中获取要使用的函数指针
例子中假设 动态库中有一个 add 函数,函数原型为 int add(int, int)

void *handle = dlopen("./libs/libmydynamic.so", RTLD_LAZY);
if (!handle) {
    // 处理打开失败的情况
}
else {
	int (*add)(int, int) = (int(*)(int, int))dlsym(handle, "add"); 
	
	if (add == NULL) { 
		// 处理获取函数指针失败的情况 
	}
}
posted @ 2025-01-04 19:55  Ace233  阅读(191)  评论(0)    收藏  举报