2025 冬季 InfiniTensor 训练营|C++ 实践课(三)

第一节课写的程序,数据输入(input)和计算(calculate)混合在一起,第二节课我们把这两块功能拆成了独立的函数,代码一下子清爽了不少。
更关键的一步是引入了 struct(结构体)——把原来散落在各处的 data 和 size 两个变量打包进了一个结构体里。它让数据的组织方式从"散装"变成了"整装",也为今天要讲的面向对象编程埋好了伏笔。
在上节课的基础上继续往前推——先搭一个操作服务,能接收一串数字输入,然后对它们求和。这节课要做的,就是把代码重构得更规范、更接近真实项目中的写法。
VLA 与 const 关键字
我们尝试进行一项操作——将数组初始化为零:
int data[size] = {0};
结果编译器报错:
error: variable "data" may not be initialized
等等,我明明写了 = {0},怎么还说"未初始化"?
问题出在 VLA(变长数组,Variable Length Array) 上。
什么是 VLA?
VLA 是指使用运行时确定的动态大小来创建数组的方式。而上文例子中的 size 虽然使用 size_t 类型初始化为 3,但对编译器来说,它是一个运行时才能确定的变量——它可以是 3、5、10或任何值。
编译器的逻辑很简单:我在编译阶段就得知道你要分配多大的内存,你给我一个运行时才能确定的 size,我怎么分?
解决方案?加 const:
const size_t size = 3;
int data[size] = {0}; // 编译通过
在这个例子中,const size_t size = 3 是编译期常量表达式,因此可以作为数组长度
C99 之后的 C 语言其实支持 VLA,但 C++ 标准不支持。这算是两门语言之间一个细微差别。
动态数组与 capacity
现实开发中,数据量从来不是固定的。今天处理 3 个 5 个,明天可能要处理成百上千个。固定大小的数组显然不够用。
于是我们引入了一个关键概念——capacity(容量)。
struct Vector {
double* data; // 存储数据的指针
int size; // 当前实际存储数据个数
int capacity; // 总容量
};
size ≠ capacity
- size:当前已存储的数据个数,即数组中实际有意义的元素数量(实际用了多少)
- capacity:数组的总容量,即为数据分配的实际内存空间能容纳多少元素(最多能装多少)
例如,你可以把 capacity 设为 100,而 size 根据实际存储数据量动态变化。只要不超过容量,就不用重新分配内存。
为了使代码语义更加清晰,我们将结构体名称由 numbers 重命名为 vector(向量),这是一个动态变化的数据结构。当然,你也可以叫它"动态数组(dynamic array)",本质上一样。
动态内存管理
new 与 delete 操作符
有了 capacity,接下来要解决的就是:内存从哪来?
C++ 的答案是 new 和 delete:
double* data = new double[capacity]; // 分配
delete[] data; // 释放
⚠️ 释放数组内必须用
delete[],不能用delete。
如果用 delete 释放 new[] 分配的数组,会导致未定义行为,可能出现析构不完整、内存泄漏、程序崩溃等问题。
构造函数与析构函数
手动 new / delete 太容易忘、容易出错。于是 C++ 引入了构造函数和析构函数:
class Vector {
public:
Vector() { // 构造函数:对象创建时自动执行,负责初始化
}
~Vector() { // 析构函数:对象销毁时自动执行,负责清理
}
};
- 构造函数(Constructor):名字和类名一样,无返回值,用于初始化。
- 析构函数(Destructor):名字前加 ~,无返回值,用于清理资源。
还有一个更简洁高效的写法——初始化列表,在构造函数参数列表后面使用冒号,然后列出所有成员变量及其初始值,用逗号分隔:
Vector() : data(nullptr), size(0), capacity(0) {
// 构造函数体可为空
}
类的封装与访问控制
public 与 private
类(class) 中的成员(包括变量和函数)可以设置为不同的访问级别,主要有两种:
public(公有):可以被类的外部代码直接访问private(私有):只能被该类的成员函数访问,类的外部代码无法直接访问
封装可以保护数据不被意外破坏,同时可以在成员函数中添加必要的检查和验证逻辑。
class Vector {
private: // 私有:外部代码无法直接访问
double* data;
int size;
int capacity;
public: // 公有:提供对外接口
Vector();
~Vector(); // 向 Vector 中添加元素
void push_back(double value); // 获取指定位置的元素
double get(int index);
};
this 指针
在类的成员函数中,存在一个特殊的指针——this 指针,指向当前对象(即调用该成员函数的对象)。遇到命名冲突时,this-> 可以明确告诉编译器:我要的是成员变量,不是参数。
void Vector::push_back(double value) { // 通过 this-> 明确访问当前对象的成员
this->data[this->size] = value;
this->size++;
}
RAII 模式
把上面这些串起来,就得到了 C++ 里重要的编程惯用法(idiom)之一——RAII(Resource Acquisition Is Initialization,资源获取即初始化)。
核心思想就一句话:
资源的获取绑定构造函数,资源的释放绑定析构函数。
RAII 模式带来三个巨大好处:
- 自动管理资源:不用手写释放代码,忘记释放的概率大幅降低;
- 异常安全:即使程序中途异常,栈上的局部对象也会被正确析构;
- 避免内存泄漏:资源生命周期 = 对象生命周期,对象销毁了,资源就不会泄露
new配delete,malloc配free
成对出现,缺一不可。
模板类(Template)
为什么需要模板?
现在的 Vector 只能存 double。想存 int?再写一遍。想存 string?再写一遍。这显然不可接受。
模板(Template) 就是来解决这个问题的。
定义模板类
使用 template 关键字可以定义模板类,将具体的类型替换为类型参数 T(也可以使用其他名称,如 U、ValueType 等)
template <typename T>
class Vector {
private:
T* data; // T 是占位符,表示任意类型
int size;
int capacity;
public:
void push_back(T value); // 参数类型为 T
T get(int index); // 返回类型为 T
};
typename 关键字用于声明一个类型参数。在模板声明中,class 和 typename 可互换,即 template<class T> 和 template<typename T> 是等价的。
模板像是一个“类型工厂”——你给它一个类型,它给你生成一整套对应的代码。
使用模板类
实例化模板类时,需要在尖括号中指定具体的类型:
Vector<double> doubleVec; // 存储 double 类型
Vector<int> intVec; // 存储 int 类型
Vector<std::string> strVec; // 存储字符串
模板类的声明和实现通常都要放在 头文件(
.h/.hpp) 里。因为编译器在实例化时需要看到完整定义,这与普通类将声明和实现分离不同,是模板机制的特殊要求。
头文件与源文件的分离
传统的 .h 和 .cpp 分离
对于非模板类,通常将类的声明放在头文件(.h/.hpp)中,将实现放在源文件(.cpp)中。这种分离有几个好处:
- 代码组织清晰:声明与实现分离,便于管理
- 编译加速:修改实现文件不需要重新编译使用该类的代码
- 信息隐藏:向使用者隐藏实现细节
模板类的特殊性
由于编译器在实例化模板时需要看到完整的定义,模板类需要特殊处理,声明和实现通常都放在头文件中。
这种做法虽然打破了传统的分离原则,但能确保编译器在实例化模板时能够访问完整的代码。这也是 C++ 模板系统的一个独特之处。
Git 版本控制基础
暂存与提交
在开发过程中,使用 Git 进行版本控制是一个好习惯。我们通常遵循以下流程:
git add:将文件添加到暂存区git commit:将暂存区的修改提交到本地仓库- 每节课结束时提交一次,这样就可以清晰地知道上一节课结束时的代码状态
git stash 命令
git stash 可以暂存当前未提交的修改,以便切换到其他分支或进行其他操作。
git stash 默认会暂存已跟踪文件的修改,包括已暂存和未暂存的修改,但不会包含未跟踪文件。若要同时暂存未跟踪文,可使用 git stash -u 或 git stash --include-untracked 来包含未跟踪的文件。
课后 Q&A
Q1:波浪线(~)在构造函数和析构函数中有什么区别?
构造函数不加波浪线,其名称与类名相同。析构函数在名称前加波浪线(~),表示这是与构造函数对应的析构函数。
Q2:include 与 Python 的 import 类似吗?
不完全类似,但功能相近。
include更接近于在预处理阶段把目标文件内容“复制粘贴”到当前文件中;而 Python 的import是模块加载机制,不是简单的文本插入。。
Q3:模板参数只能是类型吗?
不是。模板参数除了类型参数(如
typename T)外,还可以是非类型的常量参数(如template<int N>)。不过非类型参数有一些限制,实际使用中类型参数更为常见。
更多课程细节查看
本次课程回放可访问 InfiniTensor 官网:https://www.infinitensor.com 查看,或点击 【阅读原文】 跳转至 InfiniTensor 官网。
觉得有用就转发给也在学 C++ 的朋友吧! 新一期训练营也将于近期开启报名,欢迎关注公众号,第一时间获取课程与报名信息📢
关注与交流

