一、基础知识 —— CMake 基础
文章目录
- 一、什么是 CMake
- 1. CMake 是怎么出现的
- 二、C++ 为什么会有头文件 (.h 文件)
- 1. 头文件 (.h 文件)
- 2. 源文件 (.cpp 文件)
- 3. 使用方法一:两者配合使用
- 4. 使用方法二: 声明类/函数模板(必须放在头文件)
- 5. 使用方法三:头文件内实现 (Header-only Design)
- 6. 使用方法四:头文件内定义普通类 + 部分内联实现(inline)
- 7. 使用方法五:有时 A.h 对应的 A.cpp 实现集中放在一个大 B.cpp 中
- 8. 使用方法六:forward declaration(前向声明)
- 9. 使用方法七:公共接口封装(模块聚合)
- 三、C++程序的编译流程
一、什么是 CMake
都说 CMake 是一个跨平台的编译工具,首先需要更深入地理解什么是跨平台的编译工具?这个和使用 visual studio 直接运行有什么区别?
1. CMake 是怎么出现的
在做一个较为复杂的项目的时候,会用到很多 第三方库,有的时候也会把 自己的代码 打包成库,或者 修改别人的库 然后打包。如果使用 “visual studio 运行” 那一套,没有 CMake 肯定也能编译,但是编译的复杂程度会远超你的想象。
CMake 的出现是为了提高生产力。简单的理解就是,将别人做过的复杂的编译过程,打包起来,下次再进行这个复杂过程的时候,便可以用 CMake 代替他做这个过程。
二、C++ 为什么会有头文件 (.h 文件)
#include
命令实际上只是将对应hpp中的文件原封不动的粘贴到 #include
所在位置。接下来详细讨论一下 .h
和 .cpp
文件的概念、关系和使用方法。
1. 头文件 (.h 文件)
头文件中包含的内容主要是 函数、类、变量、常量 的 声明,通常 不包含实现代码。不会包含具体实现,只是 “告诉别人有哪些东西”。它定义了 接口 和 数据结构的规范。
2. 源文件 (.cpp 文件)
源文件 实现内容,编写类、函数等的 具体功能。对对应 .h
文件中声明的 函数 进行 实际定义。
3. 使用方法一:两者配合使用
简单来说,.h
文件是 “说明书” 或 “接口”。.cpp
文件是 “具体实现”。其他程序文件通过 include xxx.h
文件来使用功能。
示例 (STUDENT.h + STUDENT.cpp + main.cpp):
- 首先是
.h
文件
#ifndef STUDENT_H
//这是一个 预处理指令,全称是 "If Not Defined"(如果没有定义 STUDENT_H)。
//它的作用是:防止头文件被重复包含(multiple inclusion)。
//如果 STUDENT_H 没有被定义,就继续执行下面的代码。
#define STUDENT_H
//定义一个宏 MYCLASS_H,告诉编译器:这个头文件已经被包含过了。
//#ifndef 和 #define 搭配使用,这种写法叫做 头文件保护(Include Guard)。
#include <string>
class Student {
public:
Student(std::string name, int age);
void printInfo();
static const int MAX_AGE = 150;
private:
std::string name;
int age;
};
void printWelcome();
#endif
//与前面的 #ifndef 配套使用,表示 如果上面条件成立,这里是结束的地方。
- 然后是
.cpp
文件
#include <iostream>
#include "Student.h"
// 构造函数实现(按值传递)
Student::Student(std::string name, int age) {
this->name = name;
this->age = age;
}
// 成员函数实现(不加 const 修饰)
void Student::printInfo() {
std::cout << "Student Name: " << name << std::endl;
std::cout << "Student Age: " << age << std::endl;
std::cout << "Max Allowed Age: " << MAX_AGE << std::endl;
}
// 全局函数实现(不变)
void printWelcome() {
std::cout << "Welcome to the Student Management System!" << std::endl;
}
- 最后是
main.cpp
文件
#include "Student.h"
int main() {
printWelcome();
Student stu("Alice", 20); // 构造函数按值传参
stu.printInfo(); // 调用成员函数(非 const)
return 0;
}
4. 使用方法二: 声明类/函数模板(必须放在头文件)
首先什么是模板,模板是 C++ 支持的一种 泛型编程机制,允许 编写 与数据类型无关的 代码。分为两类:函数模板 (Function Template)、类模板 (Class Template)。
下面结合代码简单讲一下什么是函数模板,什么是类模板。模板可以理解为可以复用的代码,简单理解就是将一个代码中某些数据的数据类型挖出来,可以更具需求往中间填入合适的数据类型。
例如某个 xxx.h
文件中写入如下内容:
// 函数模板
template<typename T>
T add(T a, T b) {
return a + b;
}
//类模板
template<typename T>
class Box {
public:
Box(T value) {
val = value;
}
T getValue() { return val; }
private:
T val;
};
那么之后在写了 include "xxx.h"
的 .cpp
文件中就可以按照如下的方法调用函数模板:
int x = add<int>(3, 4);
double y = add<double>(2.5, 1.3);
Box<int> b1(10);
Box<std::string> b2("hello");
5. 使用方法三:头文件内实现 (Header-only Design)
这种方法将所有代码写在 .h
中,不再需要 .cpp
。
6. 使用方法四:头文件内定义普通类 + 部分内联实现(inline)
在 类的定义 中就直接写上了 函数实现内容,这种做法叫做:类内定义函数 (Inline Member Functions)。在 C++ 中,如果函数是在类内写出函数体的,那么它默认是 inline 的。
inline std::streamoff save(...)
{
return pk_.save(...);
}
一般来说,将短小、频繁调用、性能敏感的函数放在 .h
中,作为 Inline
实现。而对于复杂函数、调用较少、逻辑长函数,放在 .cpp
中实现,.h
中只做声明。而模板类则必须全写在 .h
。
7. 使用方法五:有时 A.h 对应的 A.cpp 实现集中放在一个大 B.cpp 中
文件 | 内容 |
---|---|
A.h | 声明 class A 的接口 |
B.cpp | 实现 class A 中的函数 (同时实现其他类) |
没有 A.cpp | 因为 A.cpp 的内容被放到了 B.cpp 中统一管理 |
举个例子:
//A.h
#ifndef A_H
#define A_H
#include <string>
class A {
public:
A();
void sayHello();
private:
std::string name_;
};
#endif
//B.cpp
#include <iostream>
#include "A.h"
// 实现 A 的构造函数
A::A() : name_("Alice") {}
// 实现 A 的成员函数
void A::sayHello() {
std::cout << "Hello, my name is " << name_ << std::endl;
}
//main.cpp
#include "A.h"
int main() {
A a;
a.sayHello();
return 0;
}
8. 使用方法六:forward declaration(前向声明)
基本工作逻辑是:告诉编译器某个类/结构/函数的存在,但不引入其完整定义。
此时编译器知道:有一个类叫 MyClass
;但不知道它有什么成员变量/函数;只能用于 指针、引用、声明 friend、声明函数参数等 不需要知道完整类型大小 的场景。例如:
// 不使用 forward declaration 的情况,需要在用到某个类之前先引入完整定义 (声明+实现)
// A.h
#include "B.h" // 引入整个 B 的定义(即使只是用 B*)
class A {
B* b_ptr; // 只是用个指针
};
//使用 forward declaration 的情况
// A.h
class B; // 前向声明即可
class A {
B* b_ptr;
};
基本的做法是,“只指不取,forward;一取必须 include”。
9. 使用方法七:公共接口封装(模块聚合)
常用于大型库,将多个子模块头文件 封装成 一个主接口。例如:
// seal.h
#include "ciphertext.h"
#include "encoder.h"
#include "ckks.h"
#include "batchencoder.h"
三、C++程序的编译流程
编译流程分为四个阶段:预处理、编译、汇编、链接。以 Linux 系统下的 g++ 编译为例:
基本过程是:
- 预处理:
处理一些#
号定义的命令或语句(如#define
、#include
、#ifdef
等),生成.i
文件。 - 编译:
进行词法分析、语法分析和语义分析等,生成.s
的 汇编文件。 - 汇编:
将对应的汇编指令翻译成机器指令,生成二进制.o
目标文件。 - 链接:
调用链接器对程序需要调用的 库 进行链接。链接分为两种:- 静态链接:
在链接期,将静态链接库中的内容直接装填到可执行程序中。在程序执行时,这些代码都会被装入该进程的虚拟地址空间中。 - 动态链接:
在链接期,只在可执行程序中记录与动态链接库中共享对象的映射信息。在程序执行时,动态链接库的全部内容被映射到该进程的虚拟地址空间。其本质就是 将链接的过程推迟到运行时处理。
Cmake 本质上帮我们做的事情就是 针对于不同的编译器,生成相对应的编译命令。针对 Linux 下的 g++ 就是 Makefile 文件,针对 win 下的 MSVC 就是 .sln 文件。
CMake 相当于在用户和操作系统上的编译器之间做了一层抽象,用户借助于 CMake,不用关心自己的操作系统上用了什么编译器就能直接完成工程的快速编译。
- 静态链接: