旋风ppt看的稀里糊涂的,简单的事务复杂化,这样,我们先提取知识点,然后慢慢一步步深入每个知识点
0 旋风ppt拜读/实验前需要具备的基础知识点
这份资料是关于C++面向对象技术的高级课程讲义,内容涵盖运算符重载 、移动语义 和SOLID原则三大核心主题。
一、运算符重载
核心思想
让自定义类的对象用起来像内置类型一样自然、直观。例如:a + b 比 a.Add(b) 更符合数学表达习惯。
成员函数 vs 友元函数的选择
| 情况 |
推荐形式 |
原因 |
修改自身的运算符(如 +=) |
成员函数 |
约定俗成,必须修改 this |
赋值运算符(=) |
成员函数 |
语言规定,必须为成员 |
非修改运算符(如 +) |
友元函数 |
确保对称性,如 v*2.0f 和 2.0f*v 都能编译 |
左操作数是其他类型(如 ostream << obj) |
友元函数 |
无法给 ostream 添加成员函数 |
关键规则
- 复合赋值运算符 (如
+=)返回 *this 的引用,支持链式操作。
- 二元运算符 (如
+)通常通过复合赋值实现:
Vector3D operator+(Vector3D lhs, const Vector3D& rhs) { return lhs += rhs; }
左操作数按值传递,避免逻辑重复,且可能触发NRVO。
- 比较运算符 :
== 和 != 通常用友元实现对称性,!= 应该复用 ==。
- 输出运算符 (
<<)必须是友元函数。
- 禁止重载的运算符 :
&&、||、逗号、取地址(会改变短路求值等固有行为)。
- 黄金法则 :重载运算符必须保持直观语义。例如
string + string 合理,但 window + widget 没有意义。
常见错误
- 赋值运算符写成友元函数(编译错误)。
- 对称运算符(如标量乘法)只提供一个方向,导致
2.0f * v 无法编译。
- 重载了不应该重载的运算符,破坏了直觉语义。
二、移动语义
为什么需要移动语义?
深拷贝在临时对象场景下代价昂贵(多次内存分配和复制)。移动语义通过转移所有权实现零开销资源传递。
左值 vs 右值
- lvalue:有名字、有地址,可出现在赋值号左边。
- rvalue:临时、无名,如函数返回的临时对象、字面量。
- xvalue :即将过期的对象,资源可被转移,如
std::move(x)。
移动构造函数
cpp
复制代码
DynamicMesh(DynamicMesh&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 源对象置空,防止双重释放
other.size_ = 0;
}
- 步骤:窃取指针 → 源置空 → 标记
noexcept。
移动赋值运算符
cpp
复制代码
DynamicMesh& operator=(DynamicMesh&& other) noexcept {
if (this != &other) {
delete[] data_; // 1. 释放旧资源
data_ = other.data_; // 2. 窃取指针
size_ = other.size_;
other.data_ = nullptr; // 3. 源置空
other.size_ = 0;
}
return *this;
}
- 关键点:自赋值检查、释放旧资源、窃取新资源、源置空。
noexcept 的重要性
- 容器(如
std::vector)在重新分配时,如果元素的移动操作标记了 noexcept,则使用移动;否则回退到拷贝以确保强异常安全保证。
- 移动操作应始终标记为
noexcept,除非确实可能抛出异常。
Rule of Five(五法则)
如果手动定义了以下任何一个,通常需要定义全部五个:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
每个成员职责:
- 析构:释放资源。
- 拷贝构造:深拷贝。
- 拷贝赋值:释放旧资源+深拷贝。
- 移动构造:窃取资源+源置空。
- 移动赋值:释放旧资源+窃取+源置空。
Rule of Zero(零法则)
优先使用智能指针和STL容器,让编译器自动生成特殊成员函数,避免手动管理资源。
RVO/NRVO(返回值优化)
- RVO:函数返回无名对象,编译器直接在调用者栈上构造,零拷贝。
- NRVO:函数返回命名对象,编译器省略拷贝或移动。
- 反模式 :不要写
return std::move(x);,这会阻止NRVO。
三、SOLID原则
SRP:单一职责原则
- 一个类应该只有一个改变的理由。
- 违反示例:
GameEngine 类同时处理渲染、物理、输入、音效、存档等。
- 修复:分解为
Renderer、PhysicsSystem、InputHandler 等独立类。
OCP:开闭原则
- 对扩展开放,对修改关闭。
- 违反示例:通过
if/else 判断类型,新增类型时需修改现有代码。
- 修复:使用多态,基类定义虚接口,派生类实现具体行为。
LSP:里氏替换原则
- 子类必须能够替换基类而不影响程序正确性。
- 违反示例:
Square 继承 Rectangle,但修改 SetWidth 同时改变 height,导致 Rectangle 的不变式被破坏。
- 修复:重新考虑继承关系,或使用组合。
ISP:接口隔离原则
- 接口应该小而专注,不应强迫类实现不需要的方法。
- 违反示例:
ISceneNode 接口要求所有派生类实现 Update、Render、HandleInput、PlayAudio,但 LightNode 可能不需要输入和音频。
- 修复:将接口拆分为
IUpdatable、IRenderable、IInputHandler、ISerializable 等。
DIP:依赖倒置原则
- 高层模块不应依赖低层模块,二者都应依赖抽象。
- 违反示例:
PhysicsSystem 直接依赖具体的 BulletPhysicsEngine。
- 修复:定义
IPhysicsBackend 接口,PhysicsSystem 通过构造函数注入接口的实现。
常见代码异味与修复
| 异味 |
症状 |
违反原则 |
修复方法 |
| 上帝类 |
一个类做太多事 |
SRP |
按职责分解 |
| 霰弹式修改 |
一个变化需改多个文件 |
SRP |
合并相关逻辑 |
| 僵化if/switch |
多处类型判断 |
OCP |
使用多态 |
| 深层继承 |
子类破坏基类契约 |
LSP |
倾向组合 |
| 臃肿接口 |
类被迫实现空方法 |
ISP |
拆分接口 |
| 循环依赖 |
A依赖B,B又依赖A |
DIP |
引入抽象层 |
四、UML序列图
序列图展示运行时对象交互的动态视图。
- 生命线:对象存在的时间(虚线)。
- 激活框:对象正在处理消息的时间段(实线矩形)。
- 同步消息:实线箭头,调用者等待返回。
- 返回消息:虚线箭头。
- 创建/销毁 :虚线箭头,销毁用
X 标记。
阅读顺序:从上到下(时间流逝),从左到右(对象交互)。
五、关键概念问答
Q1: 为什么移动构造函数中要将源对象指针置空?
A: 防止析构函数对同一块内存双重释放(因为移动后资源所有权已转移)。
Q2: std::move 之后还能使用原对象吗?
A: 语法上可以,但处于"有效但未指定状态",只能安全地赋新值或析构。
Q3: 为什么 const int& r = 42 合法?
A: const 引用可以绑定到临时值,并延长其生命周期。
Q4: 返回 std::move(x) 为什么不好?
A: 会阻止RVO/NRVO优化,强制调用移动构造函数,可能产生不必要的开销。
Q5: 何时遵循Rule of Five,何时遵循Rule of Zero?
A: 手动管理资源(如裸指针、文件句柄)时用Rule of Five;使用智能指针或STL容器时用Rule of Zero。
六、实践建议
- 运算符重载 :先实现复合赋值,再基于它实现二元运算符;对称运算符用友元;
!= 复用 ==。
- 移动语义 :确保移动操作标记
noexcept;遵循Rule of Five完整实现;优先用Rule of Zero。
- SOLID重构:识别上帝类,按职责分解;用多态替代类型判断;将臃肿接口拆分为小接口;高层模块依赖抽象。
- 性能与可维护性:业务逻辑优先可维护性(抽象、SOLID);热点代码(如游戏循环)再考虑性能优化;永远在测量后优化。
七、里程碑任务(Milestone 1)
根据资料,你需要完成以下三项:
- 运算符重载:为你的类实现复合赋值、二元运算符、比较和输出运算符,注意成员/友元选择。
- Rule of Five :为
DynamicMesh 或你的资源类实现析构、拷贝/移动构造和赋值,移动操作标记 noexcept,用 Valgrind 检查内存泄漏。
- SOLID重构:重构场景图,识别至少3个SOLID违反,应用SRP分解和OCP多态,绘制重构前后UML类图,撰写简短设计报告。
八、旋风练习题



答案ppt都有
学习完基础知识,让我们来看看本次实验旋风要我们干什么
1Task 1:获取骨架并闻出坏味道
MiniEngine-04骨架提供的文件导入文件,这个不多赘述
- apps/week04/miain.cpp 符合本次实验(实验四)的单元测试
- include/Vector3D.h
include/DynamicBuffer.h src/DynamicBuffer.cpp
- refactor/GameManager
- test
阅读 refactor/GameManager.cpp,列出至少 3 个坏味道并对应 SOLID 原则 ,写入 Task 1 的 MR 描述。
让我们来看看这个类
GameManager.h
kotlin
复制代码
#pragma once
#include <iostream>
#include <string>
#include <vector>
// ============================================================
// 这是一段有意编写的"坏味道"代码,用于 Task 4 重构练习。
// 请勿修改本文件,请新建文件完成重构。
// ============================================================
class GameManager {
public:
void LoadResource(const std::string& name, const std::string& type);
void Update(float dt);
void Render();
void HandleInput(int key);
void AddNode(const std::string& nodeType);
private:
struct Resource {
std::string name;
std::string type;
std::string data;
};
std::vector<Resource> resources_;
std::vector<std::string> sceneNodes_;
};
GameManager.cpp
kotlin
复制代码
#include "GameManager.h"
void GameManager::LoadResource(const std::string& name, const std::string& type) {
Resource r;
r.name = name;
r.type = type;
r.data = "[data of " + name + "]";
resources_.push_back(r);
std::cout << "[GameManager] Loaded " << type << ": " << name << "\n";
}
void GameManager::Update(float dt) {
for (const auto& node : sceneNodes_) {
if (node == "Mesh") {
std::cout << "[GameManager] Updating Mesh node, dt=" << dt << "\n";
} else if (node == "Camera") {
std::cout << "[GameManager] Updating Camera node, dt=" << dt << "\n";
} else if (node == "Light") {
std::cout << "[GameManager] Updating Light node, dt=" << dt << "\n";
}
}
}
void GameManager::Render() {
for (const auto& node : sceneNodes_) {
if (node == "Mesh") {
std::cout << "[GameManager] Rendering Mesh\n";
} else if (node == "Camera") {
std::cout << "[GameManager] Rendering Camera view\n";
} else if (node == "Light") {
std::cout << "[GameManager] Rendering Light\n";
}
}
}
void GameManager::HandleInput(int key) {
if (key == 'W') {
std::cout << "[GameManager] Moving forward\n";
} else if (key == 'S') {
std::cout << "[GameManager] Moving backward\n";
} else if (key == 'A') {
std::cout << "[GameManager] Moving left\n";
} else if (key == 'D') {
std::cout << "[GameManager] Moving right\n";
} else if (key == 'Q') {
std::cout << "[GameManager] Quit\n";
}
}
void GameManager::AddNode(const std::string& nodeType) {
sceneNodes_.push_back(nodeType);
}
1 上帝类(God Class)/ 单一职责违背
坏味道描述:
GameManager 类同时负责以下功能:
- 资源加载 (
LoadResource)
- 场景节点管理 (
AddNode)
- 更新逻辑 (
Update)
- 渲染逻辑 (
Render)
- 输入处理 (
HandleInput)
这导致类过于庞大,职责不单一。
对应 SOLID 原则:
2 条件分支过多 / 开放封闭违背
坏味道描述:
Update 和 Render 函数中大量 if-else 或 else if 来判断节点类型("Mesh"、"Camera"、"Light"),硬编码逻辑。
cpp
复制代码
if (node == "Mesh") { ... }
else if (node == "Camera") { ... }
else if (node == "Light") { ... }
问题:
- 每增加一种节点类型都必须修改
GameManager,违反了可扩展性。
- 节点行为和类型耦合。
对应 SOLID 原则:
3 硬编码输入逻辑 / 开放封闭违背
坏味道描述:
HandleInput 使用硬编码的字符判断操作:
cpp
复制代码
if (key == 'W') { ... }
else if (key == 'S') { ... }
问题:
- 新增操作或修改按键必须修改
GameManager。
- 输入处理与游戏逻辑耦合。
对应 SOLID 原则:
4 资源存储耦合 / 高依赖具体类型
坏味道描述:
Resource 直接存储为字符串 data,GameManager 管理所有资源类型。
cpp
复制代码
struct Resource {
std::string name;
std::string type;
std::string data;
};
问题:
- 对不同资源类型(纹理、模型、声音等)缺乏抽象。
- 添加新资源类型需要修改
GameManager。
对应 SOLID 原则:
总结表格
| 坏味道 |
位置 / 示例 |
违反 SOLID 原则 |
| 上帝类 / 单一职责违背 |
GameManager 整体 |
SRP |
| 条件分支过多 / 节点硬编码 |
Update / Render |
OCP, DIP |
| 硬编码输入逻辑 |
HandleInput |
OCP, SRP |
| 资源类型耦合 / 无多态 |
Resource struct + LoadResource |
OCP, LSP |
git指令
bash
复制代码
git checkout main
git checkout -b lab/week04-task1
cmake -S . -B build -G "MinGW Makefiles"
cmake --build build --target week03_app # 回归确认第3周
git add .
git commit -m "init: 导入 MiniEngine 第4周最小工程骨架"
git push -u origin lab/week04-task1
在MR中输入标题
Week04\]\[Task1\] 获取 MiniEngine 第4周骨架并阅读GameManager
在描述中输入
1. 单一职责违背 GameManager 同时负责资源加载、场景节点管理、更新逻辑、渲染逻辑和输入处理,类过于庞大,职责不单一,违反
SOLID的SRP原则
2. 条件分支过多 / 节点类型硬编码 Update 和 Render 中使用大量 if-else
判断节点类型("Mesh"、"Camera"、"Light"),新增节点类型需要修改类。违反OCP和DIP原则
3. 硬编码输入逻辑 HandleInput 使用硬编码字符判断操作,例如 'W'、'S',新增按键或操作必须修改类,输入逻辑与游戏逻辑耦合。违反OCP
4. 资源存储耦合 / 缺乏多态 Resource 直接存储为字符串 data,GameManager
管理所有资源类型,不同资源类型缺乏抽象。 违反OCP和LSP原则
### 2 Task 2:Vector3D 运算符重载

根据UML设计,在已有的 include/Vector3D.h 中补全,需要提交include/Vector3D.h src/Vector3D.cpp apps/week04/main.cpp

先看一下老师给的代码
`Vector3D.h`
```cpp
#pragma once
class Vector3D {
private:
double x_;
double y_;
double z_;
public:
explicit Vector3D(double x = 0.0, double y = 0.0, double z = 0.0);
[[nodiscard]] double getX() const;
[[nodiscard]] double getY() const;
[[nodiscard]] double getZ() const;
Vector3D& setX(double x);
Vector3D& setY(double y);
Vector3D& setZ(double z);
[[nodiscard]] double length() const;
void print() const;
// TODO: 在 Vector3D.cpp 中实现以下运算符
// 复合赋值(成员函数,赋值类运算符必须为成员)
/* Vector3D& operator+=(const Vector3D& rhs);
Vector3D& operator-=(const Vector3D& rhs);
Vector3D& operator*=(float s); */
// 一元取反(成员函数)
// Vector3D operator-() const;
// 二元运算符(友元函数,左操作数值传递以便复用复合赋值)
/* friend Vector3D operator+(Vector3D lhs, const Vector3D& rhs);
friend Vector3D operator-(Vector3D lhs, const Vector3D& rhs);
friend Vector3D operator*(Vector3D v, float s);
friend Vector3D operator*(float s, Vector3D v); // s*v 对称版本 */
// 比较运算符(友元函数,!= 必须复用 ==)
/* friend bool operator==(const Vector3D& a, const Vector3D& b);
friend bool operator!=(const Vector3D& a, const Vector3D& b); */
// 流输出(友元函数,左操作数为 ostream)
// friend std::ostream& operator<<(std::ostream& os, const Vector3D& v);
};
```
没有注释掉的部分是实验一写的一些很基础的设计,老师让写的是注释部分,已经声明了,我们只需要实现
`Vector3D.h`
```cpp
#pragma once
class Vector3D {
private:
double x_;
double y_;
double z_;
public:
explicit Vector3D(double x = 0.0, double y = 0.0, double z = 0.0);
[[nodiscard]] double getX() const;
[[nodiscard]] double getY() const;
[[nodiscard]] double getZ() const;
Vector3D& setX(double x);
Vector3D& setY(double y);
Vector3D& setZ(double z);
[[nodiscard]] double length() const;
void print() const;
// TODO: 在 Vector3D.cpp 中实现以下运算符
// 复合赋值(成员函数,赋值类运算符必须为成员)
Vector3D& operator+=(const Vector3D& rhs);
Vector3D& operator-=(const Vector3D& rhs);
Vector3D& operator*=(float s);
// 一元取反(成员函数)
Vector3D operator-() const;
// 二元运算符(友元函数,左操作数值传递以便复用复合赋值)
friend Vector3D operator+(Vector3D lhs, const Vector3D& rhs);
friend Vector3D operator-(Vector3D lhs, const Vector3D& rhs);
friend Vector3D operator*(Vector3D v, float s);
friend Vector3D operator*(float s, Vector3D v); // s*v 对称版本 */
// 比较运算符(友元函数,!= 必须复用 ==)
friend bool operator==(const Vector3D& a, const Vector3D& b);
friend bool operator!=(const Vector3D& a, const Vector3D& b);
// 流输出(友元函数,左操作数为 ostream)
friend std::ostream& operator<<(std::ostream& os, const Vector3D& v);
};
```
这段代码本质上是在实现一个三维向量类(Vector3D)的完整**数学**行为,目标是让它在 C++ 里用起来像"内置类型一样自然"。
#### (1)代码整体干了什么
这个 Vector3D 类实现了三件事:
1. 数据封装一个三位变量(x, y, z)
2. 基础操作(像数学一样用):获取 / 修改;向量长度;打印
3. **运算符重载**,可以让我们
```cpp
Vector3D a(1,2,3), b(4,5,6);
Vector3D c = a + b;
Vector3D d = a - b;
Vector3D e = a * 2;
if (a == b) { ... }
std::cout << a;
```
可以这样写,而非
```cpp
a.add(b);
a.multiply(2);
```
#### (2)为什么这么写
1. 核心思想:让类"像基础类型一样好用" 运算符重载 = 语法糖 + 可读性
2. 为什么这些二元运算符和输出流要用 friend,而一元取反用成员函数:
①一元运算符只有一个操作数(this)② 对象自己就能访问自己的私有成员③ 语义简单,不需要外部访问权限
①无法支持 左操作数不是 Vector3D (向量叉乘)的情况,②便于链式复用复合赋值


#### (3)代码详解
`Vector3D.cpp`
```cpp
#include "../include/Vector3D.h"
#include
#include
// TODO 1: 实现构造函数
Vector3D::Vector3D(double x, double y, double z) : x_(x), y_(y), z_(z) {}
// TODO 2: 实现 Getter
double Vector3D::getX() const {
return x_;
}
double Vector3D::getY() const {
return y_;
}
double Vector3D::getZ() const {
return z_;
}
// TODO 3: 实现 Setter
Vector3D& Vector3D::setX(double x) {
x_ = x;
return *this;
}
Vector3D& Vector3D::setY(double y) {
y_ = y;
return *this;
}
Vector3D& Vector3D::setZ(double z) {
z_ = z;
return *this;
}
double Vector3D::length() const {
return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_);
}
void Vector3D::print() const {
std::cout << "Vector3D(x=" << x_ << ", y=" << y_ << ", z=" << z_ << ")" << std::endl;
}
//运算符重载,一元
Vector3D& Vector3D::operator+=(const Vector3D& rhs) {
x_ += rhs.x_;
y_ += rhs.y_;
z_ += rhs.z_;
return *this;
}
Vector3D& Vector3D::operator-=(const Vector3D& rhs) {
x_ -= rhs.x_;
y_ -= rhs.y_;
z_ -= rhs.z_;
return *this;
}
Vector3D& Vector3D::operator*=(float s) {
x_ *= s;
y_ *= s;
z_ *= s;
return *this;
}
//一元取反
Vector3D Vector3D::operator-() const {
return Vector3D(-x_, -y_, -z_);
}
//二元运算符
Vector3D operator+(Vector3D lhs, const Vector3D& rhs) {
lhs += rhs; // 复用 operator+=
return lhs;
}
Vector3D operator-(Vector3D lhs, const Vector3D& rhs) {
lhs -= rhs; // 复用 operator-=
return lhs;
}
Vector3D operator*(Vector3D v, float s) {
v *= s; // 复用 operator*=
return v;
}
Vector3D operator*(float s, Vector3D v) {
v *= s; // 复用 operator*=,s*v 对称版本
return v;
}
//比较运算符
bool operator==(const Vector3D& a, const Vector3D& b) {
return a.x_ == b.x_ && a.y_ == b.y_ && a.z_ == b.z_;
}
bool operator!=(const Vector3D& a, const Vector3D& b) {
return !(a == b); // 复用 operator==,不重复逻辑
}
//输出流,运算符必须是非成员函数,必须写成全局函数
std::ostream& operator<<(std::ostream& os, const Vector3D& v) {
os << "(" << v.x_ << ", " << v.y_ << ", " << v.z_ << ")";
return os; // 返回 os 支持链式调用
}
```
1.**Setter返回Vector3D\&**,返回引用以便链式调用,比如
```cpp
v.setX(1).setY(2).setZ(3);
```
2.**单目运算符重载**:
* 为什么用 const \& ✔ 不拷贝✔ 直接引用原对象✔ 加 const 防止修改
* 为什么\*=是float 向量 × 标量,前面是向量 × 向量,也可以写成double
3.**一元取反** 支持 Vector3D b = -a;写法,不修改自身,返回一个新对象(函数类型是Vector3D)(返回的是"临时对象",不能用引用)
4.**二元运算符重载** 返回一个新对象(函数类型是Vector3D),lhs 是左操作数的副本(值传递,引用会直接修改左操作数),右操作数不会被修改,所以加上 const \&(避免拷贝,提高性能)
5.**ostream** 是 C++ 标准库里的输出流类
负责"把数据写到输出设备",比如:std::cout(控制台) std::ofstream(文件)
"ostream" 就是 output stream(输出流)
搞好之后测试一下,命令行输入:
```bash
cmake -S . -B build -G "MinGW Makefiles"
cmake --build build --target week04_app
./build/apps/week04_app
```
请注意检查.h里面的头文件,;楼主只顾着写.cpp了,发现.h里面没有重要#include 头文件!!!(输出流没办法奏效)
然后输出是下面这样就代表成功完成了

最后是经典git
```bash
git checkout -b lab/week04-task2
git add include/Vector3D.h src/Vector3D.cpp apps/week04/main.cpp
git commit -m "feat: 为 Vector3D 实现完整运算符重载"
git push -u origin lab/week04-task2
```
MR的时候记得提交自己的成功输出
### 3 Task 3:DynamicBuffer Rule of Five + 性能测试
先看看要交什么
include/DynamicBuffer.h
src/DynamicBuffer.cpp
tests/MathPerfTest.cpp
#### (1) 根据UML完善DynamicBuffer类
##### ①UML设计图理解
先看一下UML设计

DynamicBuffer类本质是在让你实现一个"自己管理内存的数组类",就像 std::vector 的超级简化版,一个动态(dynamic)缓存区(buffer),(自己申请、自己释放)
**私有数据成员**
1. int\* data_ 指向一块数组
2. size_t size_ 数组长度
**构造 \& 析构函数**
+DynamicBuffer(size: size_t) 构造函数:申请一块数组
+\~DynamicBuffer() 析构函数:释放数组
**拷贝(复制)**
+DynamicBuffer(other: const DynamicBuffer\&) 拷贝构造
+operator=(other: const DynamicBuffer\&) DynamicBuffer\& 拷贝赋值
**移动(偷资源)**
+DynamicBuffer(other: DynamicBuffer\&\&) noexcept
+operator=(other: DynamicBuffer\&\&) DynamicBuffer\& noexcept
**访问接口**
+Size() const size_t 拿长度
+Data() int\* 拿指针
+Data() const int\*

1. **深拷贝**解决重复释放
2. 移动语义必须noexcept,因为std::vector 扩容时,如果移动可能抛异常,就不用移动
3. 防自赋值
4. 赋值顺序
.
##### ② 知识点整理
1. RAII(资源管理)
2. Rule of Five
3. 深拷贝 vs 浅拷贝
4. [库` // std::copy`](https://cppreference.cn/w/cpp/algorithm/copy)
5. 移动语义
6. noexcept 与性能优化
##### ③ 代码详细解读
先看一下老师给的带提示的空白代码
```cpp
#pragma once
#include
// 模拟管理堆内存的缓冲区类,用于 Rule of Five 练习。
// 骨架已提供构造函数和析构函数,请补全其余四个特殊成员函数。
class DynamicBuffer {
public:
explicit DynamicBuffer(std::size_t size);
~DynamicBuffer();
// TODO: 补全 Rule of Five 剩余四个特殊成员函数
// 1. 拷贝构造函数(深拷贝)
// 2. 拷贝赋值运算符(防自赋值;先释放旧资源,再深拷贝)
// 3. 移动构造函数(接管资源,置空源对象;必须 noexcept)
// 4. 移动赋值运算符(防自赋值;先释放旧资源,再接管;必须 noexcept)
std::size_t Size() const { return size_; }
int* Data() { return data_; }
const int* Data() const { return data_; }
private:
int* data_;
std::size_t size_;
};
```
根据.cpp的提示,完全可以补全.h里面的内容,直接写了,很简单
```cpp
#pragma once
#include
// 模拟管理堆内存的缓冲区类,用于 Rule of Five 练习。
// 骨架已提供构造函数和析构函数,请补全其余四个特殊成员函数。
class DynamicBuffer {
public:
explicit DynamicBuffer(std::size_t size);
~DynamicBuffer();
// 拷贝构造
DynamicBuffer(const DynamicBuffer& other);
//拷贝赋值
DynamicBuffer& operator=(const DynamicBuffer& other);
// 移动构造
DynamicBuffer(DynamicBuffer&& other) noexcept;
// 移动赋值
DynamicBuffer& operator=(DynamicBuffer&& other) noexcept;
std::size_t Size() const { return size_; }
int* Data() { return data_; }
const int* Data() const { return data_; }
private:
int* data_;
std::size_t size_;
};
```
这里不多解释
下面是重点,类的实现
先看一下空白
```cpp
#include "DynamicBuffer.h"
DynamicBuffer::DynamicBuffer(std::size_t size)
: data_(new int[size]()), size_(size) {}
DynamicBuffer::~DynamicBuffer() {
delete[] data_;
}
// TODO: 在此实现拷贝构造函数
// DynamicBuffer::DynamicBuffer(const DynamicBuffer& other) { ... }
// TODO: 在此实现拷贝赋值运算符
// DynamicBuffer& DynamicBuffer::operator=(const DynamicBuffer& other) { ... }
// TODO: 在此实现移动构造函数(noexcept)
// DynamicBuffer::DynamicBuffer(DynamicBuffer&& other) noexcept { ... }
// TODO: 在此实现移动赋值运算符(noexcept)
// DynamicBuffer& DynamicBuffer::operator=(DynamicBuffer&& other) noexcept { ... }
```
先引入库
```cpp
#include // std::copy
```
```cpp
// TODO: 在此实现拷贝构造函数
DynamicBuffer::DynamicBuffer(const DynamicBuffer& other)
: data_(new int[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + other.size_, data_);
}
```
```cpp
DynamicBuffer& DynamicBuffer::operator=(const DynamicBuffer& other) {
if (this == &other) {
return *this; // 防自赋值
}
// 先分配新资源(更安全,防止异常导致对象损坏)
int* newData = new int[other.size_];
std::copy(other.data_, other.data_ + other.size_, newData);
// 再释放旧资源
delete[] data_;
// 接管新资源
data_ = newData;
size_ = other.size_;
return *this;
}
```
下面是完整代码
```cpp
#include "DynamicBuffer.h"
#include // std::copy
DynamicBuffer::DynamicBuffer(std::size_t size)
: data_(new int[size]()), size_(size) {}
DynamicBuffer::~DynamicBuffer() {
delete[] data_;
}
// TODO: 在此实现拷贝构造函数
DynamicBuffer::DynamicBuffer(const DynamicBuffer& other)
: data_(new int[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + other.size_, data_);
}
// TODO: 在此实现拷贝赋值运算符
DynamicBuffer& DynamicBuffer::operator=(const DynamicBuffer& other) {
if (this == &other) {
return *this; // 防自赋值
}
// 先分配新资源(更安全,防止异常导致对象损坏)
int* newData = new int[other.size_];
std::copy(other.data_, other.data_ + other.size_, newData);
// 再释放旧资源
delete[] data_;
// 接管新资源
data_ = newData;
size_ = other.size_;
return *this;
}
// TODO: 在此实现移动构造函数(noexcept)
DynamicBuffer::DynamicBuffer(DynamicBuffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
// 置空源对象
other.data_ = nullptr;
other.size_ = 0;
}
// TODO: 在此实现移动赋值运算符(noexcept)
DynamicBuffer& DynamicBuffer::operator=(DynamicBuffer&& other) noexcept {
if (this == &other) {
return *this; // 防自赋值
}
// 先释放旧资源
delete[] data_;
// 接管资源
data_ = other.data_;
size_ = other.size_;
// 置空源对象
other.data_ = nullptr;
other.size_ = 0;
return *this;
}
```
然后写完代码的单元检测:
执行以下命令进行单元测试(MathLibTest)
```bash
cmake -S . -B build -G "MinGW Makefiles"
cmake --build build --target MathLibTest
./build/tests/MathLibTest
```
**注意**,如果这里报错显示
```bash
'class Vector3D' has no member named 'x'; did you mean 'double Vector3D::x_'? (not accessible from this context)
```
那是因为这个🐖旋风给的MathLibTest.cpp里面,不知道为啥直接访问私有成员(x,y,z),应该用getter方法(明明有写getX,getY,getZ)
```bash
Running main() from D:\001\da2\C++object_oriented\courses\week4\1120243254\build\_deps\googletest-src\googletest\src\gtest_main.cc
[==========] Running 18 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 11 tests from Vector3DTest
[ RUN ] Vector3DTest.AdditionBasic
[ OK ] Vector3DTest.AdditionBasic (0 ms)
[ RUN ] Vector3DTest.SubtractionBasic
[ OK ] Vector3DTest.SubtractionBasic (0 ms)
[ RUN ] Vector3DTest.ScalarMultiplyRHS
[ OK ] Vector3DTest.ScalarMultiplyRHS (0 ms)
[ RUN ] Vector3DTest.ScalarMultiplyLHS
[ OK ] Vector3DTest.ScalarMultiplyLHS (0 ms)
[ RUN ] Vector3DTest.CompoundAssignAdd
[ OK ] Vector3DTest.CompoundAssignAdd (0 ms)
[ RUN ] Vector3DTest.CompoundAssignScale
[ OK ] Vector3DTest.CompoundAssignScale (0 ms)
[ RUN ] Vector3DTest.UnaryNegation
[ OK ] Vector3DTest.UnaryNegation (0 ms)
[ RUN ] Vector3DTest.EqualityOperator
[ OK ] Vector3DTest.EqualityOperator (0 ms)
[ RUN ] Vector3DTest.InequalityOperator
[ OK ] Vector3DTest.InequalityOperator (0 ms)
[ RUN ] Vector3DTest.StreamOutput
[ OK ] Vector3DTest.StreamOutput (0 ms)
[ RUN ] Vector3DTest.ChainedCompoundAssign
[ OK ] Vector3DTest.ChainedCompoundAssign (0 ms)
[----------] 11 tests from Vector3DTest (27 ms total)
[----------] 7 tests from DynamicBufferTest
[ RUN ] DynamicBufferTest.CopyConstructorDeepCopy
[ OK ] DynamicBufferTest.CopyConstructorDeepCopy (0 ms)
[ RUN ] DynamicBufferTest.MoveConstructorTransfersOwnership
[ OK ] DynamicBufferTest.MoveConstructorTransfersOwnership (0 ms)
[ RUN ] Vector3DTest.InequalityOperator
[ OK ] Vector3DTest.InequalityOperator (0 ms)
[ RUN ] Vector3DTest.StreamOutput
[ OK ] Vector3DTest.StreamOutput (0 ms)
[ RUN ] Vector3DTest.ChainedCompoundAssign
[ OK ] Vector3DTest.ChainedCompoundAssign (0 ms)
[----------] 11 tests from Vector3DTest (27 ms total)
[----------] 7 tests from DynamicBufferTest
[ RUN ] DynamicBufferTest.CopyConstructorDeepCopy
[ OK ] DynamicBufferTest.CopyConstructorDeepCopy (0 ms)
[ RUN ] DynamicBufferTest.MoveConstructorTransfersOwnership
[ OK ] DynamicBufferTest.MoveConstructorTransfersOwnership (0 ms)
[ OK ] Vector3DTest.InequalityOperator (0 ms)
[ RUN ] Vector3DTest.StreamOutput
[ OK ] Vector3DTest.StreamOutput (0 ms)
[ RUN ] Vector3DTest.ChainedCompoundAssign
[ OK ] Vector3DTest.ChainedCompoundAssign (0 ms)
[----------] 11 tests from Vector3DTest (27 ms total)
[----------] 7 tests from DynamicBufferTest
[ RUN ] DynamicBufferTest.CopyConstructorDeepCopy
[ OK ] DynamicBufferTest.CopyConstructorDeepCopy (0 ms)
[ RUN ] DynamicBufferTest.MoveConstructorTransfersOwnership
[ OK ] DynamicBufferTest.MoveConstructorTransfersOwnership (0 ms)
[ RUN ] Vector3DTest.StreamOutput
[ OK ] Vector3DTest.StreamOutput (0 ms)
[ RUN ] Vector3DTest.ChainedCompoundAssign
[ OK ] Vector3DTest.ChainedCompoundAssign (0 ms)
[----------] 11 tests from Vector3DTest (27 ms total)
[----------] 7 tests from DynamicBufferTest
[ RUN ] DynamicBufferTest.CopyConstructorDeepCopy
[ OK ] DynamicBufferTest.CopyConstructorDeepCopy (0 ms)
[ RUN ] DynamicBufferTest.MoveConstructorTransfersOwnership
[ OK ] DynamicBufferTest.MoveConstructorTransfersOwnership (0 ms)
[ RUN ] Vector3DTest.ChainedCompoundAssign
[ OK ] Vector3DTest.ChainedCompoundAssign (0 ms)
[----------] 11 tests from Vector3DTest (27 ms total)
[----------] 7 tests from DynamicBufferTest
[ RUN ] DynamicBufferTest.CopyConstructorDeepCopy
[ OK ] DynamicBufferTest.CopyConstructorDeepCopy (0 ms)
[ RUN ] DynamicBufferTest.MoveConstructorTransfersOwnership
[ OK ] DynamicBufferTest.MoveConstructorTransfersOwnership (0 ms)
[----------] 11 tests from Vector3DTest (27 ms total)
[----------] 7 tests from DynamicBufferTest
[ RUN ] DynamicBufferTest.CopyConstructorDeepCopy
[ OK ] DynamicBufferTest.CopyConstructorDeepCopy (0 ms)
[ RUN ] DynamicBufferTest.MoveConstructorTransfersOwnership
[ OK ] DynamicBufferTest.MoveConstructorTransfersOwnership (0 ms)
[ RUN ] DynamicBufferTest.CopyConstructorDeepCopy
[ OK ] DynamicBufferTest.CopyConstructorDeepCopy (0 ms)
[ RUN ] DynamicBufferTest.MoveConstructorTransfersOwnership
[ OK ] DynamicBufferTest.MoveConstructorTransfersOwnership (0 ms)
[ OK ] DynamicBufferTest.CopyConstructorDeepCopy (0 ms)
[ RUN ] DynamicBufferTest.MoveConstructorTransfersOwnership
[ OK ] DynamicBufferTest.MoveConstructorTransfersOwnership (0 ms)
[ OK ] DynamicBufferTest.MoveConstructorTransfersOwnership (0 ms)
[ RUN ] DynamicBufferTest.MoveConstructorSizeReset
[ OK ] DynamicBufferTest.MoveConstructorSizeReset (0 ms)
[ OK ] DynamicBufferTest.MoveConstructorSizeReset (0 ms)
[ RUN ] DynamicBufferTest.CopyAssignmentDeepCopy
[ OK ] DynamicBufferTest.CopyAssignmentDeepCopy (0 ms)
[ RUN ] DynamicBufferTest.MoveAssignmentTransfersOwnership
[ OK ] DynamicBufferTest.CopyAssignmentDeepCopy (0 ms)
[ RUN ] DynamicBufferTest.MoveAssignmentTransfersOwnership
[ OK ] DynamicBufferTest.MoveAssignmentTransfersOwnership (0 ms)
[ OK ] DynamicBufferTest.MoveAssignmentTransfersOwnership (0 ms)
[ RUN ] DynamicBufferTest.SelfCopyAssignment
[ OK ] DynamicBufferTest.SelfCopyAssignment (0 ms)
[ RUN ] DynamicBufferTest.SelfMoveAssignment
[ OK ] DynamicBufferTest.SelfMoveAssignment (0 ms)
[----------] 7 tests from DynamicBufferTest (18 ms total)
[----------] Global test environment tear-down
[==========] 18 tests from 2 test suites ran. (52 ms total)
[ PASSED ] 18 tests.
```
最关键的部分(截图一)

这个是Google Test(GTest)运行测试的完整报告,表示Vector3D 类和 DynamicBuffer 类的构造、移动、赋值、操作符重载等功能都正确实现了。
#### (2) 性能测试规格 完善MathPerfTest.cpp

先理解一下要干什么,就是删去DynamicBuffer里面的noexcept,运行用于性能测试的MathPerfTest.cpp,并把两次结果截图,尤其对比Bench2
解释:

ok,明白要干什么,我们来看看MathPerfTest.cpp,老师要求我们用 实现这两个独立可执行的基准
先补充一下知识点:
1. [C++ stdchrono时间库全面解析](https://zhuanlan.zhihu.com/p/662738124)
2. [C++的基准测试](https://www.nagi.fun/Cherno-CPP-Notes/51-100/74%20BENCHMARKING%20in%20C++%20%28how%20to%20measure%20performance%29/)
看一下老师给的提示的`MathPerfTest.cpp`
```cpp
#include
#include
#include
#include "DynamicBuffer.h"
// ============================================================
// 性能基准测试------代码自行实现
//
// 须包含以下两个基准:
//
// Bench 1 --- 拷贝 vs 移动耗时对比
// - DynamicBuffer 大小:>= 100 万个 int(约 4 MB)
// - 拷贝和移动各重复 >= 50 次
// - 使用 std::chrono::high_resolution_clock 计时
// - 预期:移动耗时接近 0 ms(仅指针赋值),
// 拷贝耗时与数据量线性相关,差距通常在 10x 以上
//
// Bench 2 --- noexcept 对 std::vector 扩容策略的影响
// - 从 reserve(1) 开始 push_back N 次,强制多次扩容
// - 先运行"有 noexcept"版本,记录耗时
// - 再注释掉移动构造的 noexcept,重新编译运行,对比耗时
// - 预期:删除 noexcept 后,扩容退回深拷贝,耗时显著增加
//
// 两次运行结果截图作为里程碑 1 报告的必交内容。
// ============================================================
// TODO: 实现 BenchCopyVsMove()
// TODO: 实现 BenchVectorGrowth()
int main() {
std::cout << "=== DynamicBuffer Performance Benchmarks ===" << std::endl;
// TODO: 调用 BenchCopyVsMove() 并输出结果
// TODO: 调用 BenchVectorGrowth() 并输出结果
return 0;
}
```
完全不知道怎么写呢:)
直接上答案
```cpp
#include
#include
#include
#include "DynamicBuffer.h"
// ============================================================
// 性能基准测试------代码自行实现
//
// 须包含以下两个基准:
//
// Bench 1 --- 拷贝 vs 移动耗时对比
// - DynamicBuffer 大小:>= 100 万个 int(约 4 MB)
// - 拷贝和移动各重复 >= 50 次
// - 使用 std::chrono::high_resolution_clock 计时
// - 预期:移动耗时接近 0 ms(仅指针赋值),
// 拷贝耗时与数据量线性相关,差距通常在 10x 以上
//
// Bench 2 --- noexcept 对 std::vector 扩容策略的影响
// - 从 reserve(1) 开始 push_back N 次,强制多次扩容
// - 先运行"有 noexcept"版本,记录耗时
// - 再注释掉移动构造的 noexcept,重新编译运行,对比耗时
// - 预期:删除 noexcept 后,扩容退回深拷贝,耗时显著增加
//
// 两次运行结果截图作为里程碑 1 报告的必交内容。
// ============================================================
// TODO: 实现 BenchCopyVsMove()
void BenchCopyVsMove() {
constexpr size_t N = 1'000'000; // 100 万 int
constexpr int repeat = 50;
DynamicBuffer original(N);
// 拷贝耗时
auto start_copy = std::chrono::high_resolution_clock::now();
for (int i = 0; i < repeat; ++i) {
DynamicBuffer copy = original; // 调用拷贝构造
}
auto end_copy = std::chrono::high_resolution_clock::now();
auto copy_ms = std::chrono::duration_cast(end_copy - start_copy).count();
// 移动耗时
auto start_move = std::chrono::high_resolution_clock::now();
for (int i = 0; i < repeat; ++i) {
DynamicBuffer temp(N);
DynamicBuffer moved = std::move(temp); // 调用移动构造
}
auto end_move = std::chrono::high_resolution_clock::now();
auto move_ms = std::chrono::duration_cast(end_move - start_move).count();
std::cout << "[Bench 1] Copy vs Move:" << std::endl;
std::cout << "Copy 50x: " << copy_ms << " ms" << std::endl;
std::cout << "Move 50x: " << move_ms << " ms" << std::endl;
}
// TODO: 实现 BenchVectorGrowth()
void BenchVectorGrowth() {
constexpr int push_count = 5000; // 强制多次扩容
constexpr size_t buffer_size = 10'000; // 每个 buffer 1 万 int
// vector 默认 (移动构造 noexcept)
auto start_noexcept = std::chrono::high_resolution_clock::now();
std::vector vec;
vec.reserve(1); // 从小容量开始
for (int i = 0; i < push_count; ++i) {
vec.push_back(DynamicBuffer(buffer_size));
}
auto end_noexcept = std::chrono::high_resolution_clock::now();
auto noexcept_ms = std::chrono::duration_cast(end_noexcept - start_noexcept).count();
std::cout << "[Bench 2] with noexcept:" << std::endl;
std::cout << "Time: " << noexcept_ms << " ms" << std::endl;
}
int main() {
std::cout << "=== DynamicBuffer Performance Benchmarks ===" << std::endl;
// TODO: 调用 BenchCopyVsMove() 并输出结果
BenchCopyVsMove();
// TODO: 调用 BenchVectorGrowth() 并输出结果
BenchVectorGrowth();
return 0;
}
```
##### **预期输出**
第一个bench:赋值和移动差距在100左右,移动更快
第二个bench:删去noexcept前后显著差距,8 9倍左右
###### ①有noexcept
先在/test/CMakeList.txt里面注册测试文件
```bash
add_gtest(MathPerfTest MathPerfTest.cpp)
```
然后再
```bash
cmake -S . -B build -G "MinGW Makefiles"
cmake --build build --target MathPerfTest
./build/tests/MathPerfTest
```
预期输出

###### 无noexcept
然后我们删去DynamicBuffer.h和.cpp里面移动构造和移动赋值的noexcept,再同样测试一遍,预期输出如下:

#### (3) git和MR
```bash
git checkout -b lab/week04-task3
git add include/DynamicBuffer.h src/DynamicBuffer.cpp tests/MathPerfTest.cpp
git commit -m "feat: 实现 DynamicBuffer,新增性能基准测试"
git push -u origin lab/week04-task3
```
MR的时候标题为\[Week04\]\[Task3\] 实现 DynamicBuffer并且完成性能基准测试,记得放上截图就可以了
### 4 Task 4:重构 GameManager + 里程碑 1 报告
老规矩,先看看需要交什么
1. 代码提交:tests/RefactorTest.cpp include/ src/ apps/week04/
2. 文档提交:docs/milestone1-report.md
#### (1)重构 GameManager
##### ①重构前

也就是说这个类GameManager很有问题,有坏味道
`GameManager.h`
```cpp
#pragma once
#include
#include
#include
// ============================================================
// 这是一段有意编写的"坏味道"代码,用于 Task 4 重构练习。
// 请勿修改本文件,请新建文件完成重构。
// ============================================================
class GameManager {
public:
void LoadResource(const std::string& name, const std::string& type);
void Update(float dt);
void Render();
void HandleInput(int key);
void AddNode(const std::string& nodeType);
private:
struct Resource {
std::string name;
std::string type;
std::string data;
};
std::vector resources_;
std::vector sceneNodes_;
};
```
`GameManager.cpp`
```cpp
#include "GameManager.h"
void GameManager::LoadResource(const std::string& name, const std::string& type) {
Resource r;
r.name = name;
r.type = type;
r.data = "[data of " + name + "]";
resources_.push_back(r);
std::cout << "[GameManager] Loaded " << type << ": " << name << "\n";
}
void GameManager::Update(float dt) {
for (const auto& node : sceneNodes_) {
if (node == "Mesh") {
std::cout << "[GameManager] Updating Mesh node, dt=" << dt << "\n";
} else if (node == "Camera") {
std::cout << "[GameManager] Updating Camera node, dt=" << dt << "\n";
} else if (node == "Light") {
std::cout << "[GameManager] Updating Light node, dt=" << dt << "\n";
}
}
}
void GameManager::Render() {
for (const auto& node : sceneNodes_) {
if (node == "Mesh") {
std::cout << "[GameManager] Rendering Mesh\n";
} else if (node == "Camera") {
std::cout << "[GameManager] Rendering Camera view\n";
} else if (node == "Light") {
std::cout << "[GameManager] Rendering Light\n";
}
}
}
void GameManager::HandleInput(int key) {
if (key == 'W') {
std::cout << "[GameManager] Moving forward\n";
} else if (key == 'S') {
std::cout << "[GameManager] Moving backward\n";
} else if (key == 'A') {
std::cout << "[GameManager] Moving left\n";
} else if (key == 'D') {
std::cout << "[GameManager] Moving right\n";
} else if (key == 'Q') {
std::cout << "[GameManager] Quit\n";
}
}
void GameManager::AddNode(const std::string& nodeType) {
sceneNodes_.push_back(nodeType);
}
```
然后老师的意思是我们要重新构建一系统(可能)的类来完成GameManager的作用
##### ①重构后
重构**目标 UML** (从此图出发创建所有新文件)

**关键调用序列图**


看完了,还是很懵
这个任务本质是在做一次**面向对象重构(OO Refactoring)+ 设计原则落地(SOLID)**
| 设计问题 | 现在的解决方式 |
|------|--------------|
| 上帝类 | GameLoop 拆职责 |
| 类型判断 | 多态 |
| 输入耦合 | 接口(DIP) |
| 扩展困难 | OCP |
*** ** * ** ***
###### 需要创建哪些文件?要干什么?
**根据UML必须新增的 3 个核心模块**,需要从 0 创建:
**1. 输入抽象层(DIP 核心)**
include/refactor/IInputHandler.h
**2. 具体输入实现**
src/KeyboardInputHandler.cpp
include/refactor/KeyboardInputHandler.h
**3. 游戏主循环(替代 GameManager)**
include/refactor/GameLoop.h
src/GameLoop.cpp
*** ** * ** ***
**4.实际在做什么**
把原来的 **GameManager 上帝类拆成 3 层职责**
| 原功能 | 重构后归属 |
|---------|--------------------------------------|
| 输入处理 | IInputHandler / KeyboardInputHandler |
| 节点更新/渲染 | SceneNode(已有) |
| 调度逻辑 | GameLoop |
*** ** * ** ***
###### 每个文件具体要写什么
###### 1️⃣ IInputHandler.h 接口层 最关键
作用:**解耦输入设备(键盘 / 手柄 / AI)**
```cpp
#pragma once
class IInputHandler {
public:
virtual ~IInputHandler() = default;
virtual void HandleInput(int key) = 0;
};
```
*** ** * ** ***
###### 2️⃣ KeyboardInputHandler
**.h**
```cpp
#pragma once
#include "IInputHandler.h"
#include
class KeyboardInputHandler : public IInputHandler {
public:
void HandleInput(int key) override;
};
```
**.cpp**
```cpp
#include "KeyboardInputHandler.h"
void KeyboardInputHandler::HandleInput(int key) {
std::cout << "[Keyboard] key = " << key << std::endl;
}
```
*** ** * ** ***
###### 3️⃣ GameLoop 核心重构对象
**GameLoop.h**
> 注意:只能 include IInputHandler(题目硬性要求)
```cpp
#pragma once
#include
#include
#include "IInputHandler.h"
#include "SceneNode.h" // 这个可以 include(节点抽象)
class GameLoop {
private:
std::vector> nodes_;
std::unique_ptr inputHandler_;
public:
explicit GameLoop(std::unique_ptr handler);
void AddNode(std::unique_ptr node);
void Tick(float dt);
void HandleInput(int key);
};
```
*** ** * ** ***
**GameLoop.cpp**
```cpp
#include "GameLoop.h"
GameLoop::GameLoop(std::unique_ptr handler)
: inputHandler_(std::move(handler)) {}
void GameLoop::AddNode(std::unique_ptr node) {
nodes_.push_back(std::move(node));
}
void GameLoop::Tick(float dt) {
for (auto& node : nodes_) {
node->Update(); // ❗必须是多态调用
node->Render(); // ❗禁止 if(type)
}
}
void GameLoop::HandleInput(int key) {
if (inputHandler_) {
inputHandler_->HandleInput(key); // ❗多态分派
}
}
```
*** ** * ** ***
在SceneNode.h增加了纯虚函数 virtual void Render() = 0;
在MeshNode CameraNode EmptyNod都实现 void Render() override;
*** ** * ** ***
测试
test/RefactorTest.cpp
先检查一下CMakeList有没有添加相关target
```bash
# 完成重构后取消注释
add_gtest(RefactorTest RefactorTest.cpp)
```
然后再看RefactorTest.cpp,按要求恢复测试文件
```cpp
#include
// 完成 Task 4 重构后,在此补全测试用例,并在 CMakeLists.txt 中取消注释
// add_gtest(RefactorTest RefactorTest.cpp)
//
// 建议测试:
// 1. GameLoop::Tick() 正确触发各节点的 Update() 和 Render()(多态)
// 2. GameLoop::HandleInput() 通过 IInputHandler 路由到具体实现
// 3. 新增节点类型后,Tick() 无需修改仍能正常工作(OCP 验证)
//
// TODO: 补全头文件引用
// #include "GameLoop.h"
// #include "KeyboardInputHandler.h"
// #include "NodeFactory.h"
// TODO: 在此实现测试用例
```
按要求补全如下:
```cpp
#include
// 完成 Task 4 重构后,在此补全测试用例,并在 CMakeLists.txt 中取消注释
// add_gtest(RefactorTest RefactorTest.cpp)
//
// 建议测试:
// 1. GameLoop::Tick() 正确触发各节点的 Update() 和 Render()(多态)
// 2. GameLoop::HandleInput() 通过 IInputHandler 路由到具体实现
// 3. 新增节点类型后,Tick() 无需修改仍能正常工作(OCP 验证)
//
// TODO: 补全头文件引用
// #include "GameLoop.h"
// #include "KeyboardInputHandler.h"
// #include "NodeFactory.h"
// TODO: 在此实现测试用例
#include "GameLoop.h"
#include "KeyboardInputHandler.h"
#include "NodeFactory.h"
#include "SceneNode.h"
#include
#include
#include
// 测试用 Mock 节点(用于验证多态调用)
class TestNode : public SceneNode {
public:
bool updated = false;
bool rendered = false;
explicit TestNode(const std::string& name)
: SceneNode(name) {}
void Update() override {
updated = true;
}
void Render() override {
rendered = true;
}
};
// 测试用 Mock 输入处理器(验证 DIP)
class TestInputHandler : public IInputHandler {
public:
bool called = false;
int lastKey = -1;
void HandleInput(int key) override {
called = true;
lastKey = key;
}
};
// 1️Tick() 是否正确触发多态 Update + Render
TEST(RefactorTest, TickCallsUpdateAndRender) {
auto input = std::make_unique();
GameLoop game(std::move(input));
auto node1 = std::make_unique("node1");
auto node2 = std::make_unique("node2");
TestNode* ptr1 = node1.get();
TestNode* ptr2 = node2.get();
game.AddNode(std::move(node1));
game.AddNode(std::move(node2));
game.Tick(0.016f);
EXPECT_TRUE(ptr1->updated);
EXPECT_TRUE(ptr1->rendered);
EXPECT_TRUE(ptr2->updated);
EXPECT_TRUE(ptr2->rendered);
}
// 输入是否通过接口分发(DIP 验证)
TEST(RefactorTest, HandleInputDelegatesToHandler) {
auto handler = std::make_unique();
TestInputHandler* raw = handler.get();
GameLoop game(std::move(handler));
game.HandleInput(42);
EXPECT_TRUE(raw->called);
EXPECT_EQ(raw->lastKey, 42);
}
// OCP 验证:新增节点类型无需修改 GameLoop
class NewNode : public SceneNode {
public:
bool updated = false;
bool rendered = false;
explicit NewNode(const std::string& name)
: SceneNode(name) {}
void Update() override {
updated = true;
}
void Render() override {
rendered = true;
}
};
TEST(RefactorTest, OCP_NewNodeWorksWithoutModifyingGameLoop) {
auto input = std::make_unique();
GameLoop game(std::move(input));
auto newNode = std::make_unique("new");
NewNode* ptr = newNode.get();
game.AddNode(std::move(newNode));
game.Tick(0.016f);
EXPECT_TRUE(ptr->updated);
EXPECT_TRUE(ptr->rendered);
}
```
然后命令行输入:
```bash
cmake -S . -B build -G "MinGW Makefiles"
cmake --build build --target RefactorTest
./build/tests/RefactorTest
```
预期类似结果输出:

测试
week04/main.cpp
```cpp
#include
#include
#include "DynamicBuffer.h"
#include "NodeFactory.h"
#include "Vector3D.h"
#include "GameLoop.h"
#include "KeyboardInputHandler.h"
int main() {
// ── Task 2 验证:Vector3D 运算符重载 ─────────────────────
std::cout << "=== Vector3D Operator Test ===" << std::endl;
Vector3D a{1.0f, 2.0f, 3.0f};
Vector3D b{4.0f, 5.0f, 6.0f};
// TODO: 实现运算符后,取消以下注释进行验证
std::cout << "a + b = " << (a + b) << std::endl; // expected: (5, 7, 9)
std::cout << "2*a = " << (2.0f * a) << std::endl; // expected: (2, 4, 6)
std::cout << "a*2 = " << (a * 2.0f) << std::endl; // expected: (2, 4, 6)
std::cout << "a==a: " << (a == a) << std::endl; // expected: 1
std::cout << "a==b: " << (a == b) << std::endl; // expected: 0
// ── Task 3 验证:DynamicBuffer 移动语义 ──────────────────
std::cout << "\n=== DynamicBuffer Move Test ===" << std::endl;
DynamicBuffer buf(10);
buf.Data()[0] = 42;
std::cout << "Before move: buf.Data() = " << buf.Data() << std::endl;
// TODO: 实现移动构造函数后,取消以下注释进行验证
DynamicBuffer moved = std::move(buf);
std::cout << "After move: moved.Data() = " << moved.Data() << std::endl;
std::cout << "After move: buf.Data() = " << buf.Data()
<< " (expected: 0/nullptr)" << std::endl;
// ── Task 4 验证:GameLoop 重构(完成 Task 4 后取消注释)─────
std::cout << "\n=== GameLoop Test ===" << std::endl;
auto handler = std::make_unique();
GameLoop loop(std::move(handler));
loop.AddNode(NodeFactory::CreateNode("Mesh", "Hero"));
loop.AddNode(NodeFactory::CreateNode("Camera", "FollowCam"));
loop.Tick(0.016f);
loop.HandleInput('W');
std::cout << "\nMiniEngine Week04 skeleton ready." << std::endl;
return 0;
}
```
然后命令行运行
```bash
cmake -S . -B build -G "MinGW Makefiles"
cmake --build build --target week04_app
build\week04_app.exe
```
预期输出如下:

#### (2)完成 里程碑 1 报告
docs/milestone1-report.md
要求如下:

##### ①坏味道列表
详细见Task1
表格如下:
| 坏味道 | 位置 / 示例 | 违反 SOLID 原则 |
|----------------|------------------------------------|-------------|
| 上帝类 / 单一职责违背 | `GameManager` 整体 | SRP |
| 条件分支过多 / 节点硬编码 | `Update` / `Render` | OCP, DIP |
| 硬编码输入逻辑 | `HandleInput` | OCP, SRP |
| 资源类型耦合 / 无多态 | `Resource` struct + `LoadResource` | OCP, LSP |
列表如下:
**坏味道列表**
1. **上帝类(God Class)**
* **问题描述:**
`GameManager` 类同时负责资源加载 (`LoadResource`)、场景节点管理 (`AddNode`)、更新逻辑 (`Update`)、渲染逻辑 (`Render`) 和输入处理 (`HandleInput`)。类功能过于庞杂,职责不单一,代码耦合度高。
* **影响:**
难以维护和扩展,修改一处可能影响多处逻辑。
* **对应 SOLID 原则:**
* **S -- Single Responsibility Principle (SRP)**
每个类应仅有一个职责。`GameManager` 违反 SRP。
*** ** * ** ***
2. **条件分支过多**
* **问题描述:**
`Update` 和 `Render` 中大量 `if-else` 判断节点类型("Mesh"、"Camera"、"Light"):
```cpp
if (node == "Mesh") { ... }
else if (node == "Camera") { ... }
else if (node == "Light") { ... }
```
每增加一个新节点类型都必须修改这些方法。
* **影响:**
节点逻辑与类型耦合,增加新节点类型需要修改高层类。
* **对应 SOLID 原则:**
* **O -- Open/Closed Principle (OCP)**
类应对扩展开放,对修改关闭。这里违反 OCP。
* **D -- Dependency Inversion Principle (DIP)**
高层模块依赖具体实现,而非节点抽象接口。
*** ** * ** ***
3. **硬编码输入逻辑**
* **问题描述:**
`HandleInput` 方法中按键操作硬编码:
```cpp
if (key == 'W') { ... }
else if (key == 'S') { ... }
else if (key == 'A') { ... }
else if (key == 'D') { ... }
else if (key == 'Q') { ... }
```
新增按键或修改操作必须改类。
* **影响:**
输入处理和游戏逻辑耦合,扩展性差。
* **对应 SOLID 原则:**
* **O -- Open/Closed Principle (OCP)**
类对扩展不开放:添加新按键要修改类。
* **S -- Single Responsibility Principle (SRP)**
输入处理本应独立于游戏状态管理。
*** ** * ** ***
4. \*\*资源存储耦合 \*\*
* **问题描述:**
`Resource` 结构体直接使用字符串存储所有资源:
```cpp
struct Resource {
std::string name;
std::string type;
std::string data;
};
```
对不同资源类型(纹理、模型、声音等)**缺乏抽象**。
* **影响:**
添加新资源类型或处理特定资源需要修改 `GameManager`。
* **对应 SOLID 原则:**
* **O -- Open/Closed Principle (OCP)**
类对新资源类型扩展不开放。
* **L -- Liskov Substitution Principle (LSP)**
无法通过多态处理不同资源类型,违反可替换性。
*** ** * ** ***
5. **字符串作为节点标识**
* **问题描述:**
`sceneNodes_` 使用 `std::string` 表示节点类型:
```cpp
std::vector sceneNodes_;
```
节点类型和逻辑高度耦合,无法通过接口或多态管理。
* **影响:**
添加新节点类型、扩展节点行为或修改渲染逻辑必须修改 `GameManager`。
* **对应 SOLID 原则:**
* **O -- Open/Closed Principle (OCP)**
对节点类型扩展不开放。
* **D -- Dependency Inversion Principle (DIP)**
高层依赖具体类型字符串,而非抽象节点接口。
*** ** * ** ***
6. **重复输出日志**
* **问题描述:**
`Update`、`Render`、`HandleInput` 方法中重复 `std::cout` 打印日志。打印逻辑和核心功能混杂。
* **影响:**
打印逻辑耦合到业务逻辑,无法单独替换或复用。
* **对应 SOLID 原则:**
* **S -- Single Responsibility Principle (SRP)**
日志输出应由独立模块处理。
##### ②UML 类图(重构前后对比)
可使用上方两张图


##### ③UML 序列图
展示 vtable 分派(多态触发)的位置
1. 节点更新/渲染分派:GameLoop::Tick()中通过node-\>Update()和node-\>Render()调用,运行时根据实际对象类型(MeshNode/CameraNode)通过vtable分派到具体实现
2.输入处理分派:GameLoop::HandleInput()中通过inputHandler_-\>HandleInput(key)调用,运行时根据实际输入处理器类型(KeyboardInputHandler等)通过vtable分派到具体实现
##### ④ 性能测试截图
Bench 1 拷贝/移动耗时,大概在几百到几千;
Bench 2 有无 noexcept 扩容对比,大概相差10倍左右


##### ⑤ 重构收益说明
新增输入方式 / 新增节点类型各需修改哪些文件

#### (3)MR
提交的时候
标题为\[Week4\]\[\[Task4\]完成 GameManager的重构和里程碑 1 报告
