BIT-TPS:关于实验4数学库完善与设计重构的详细解读

旋风ppt看的稀里糊涂的,简单的事务复杂化,这样,我们先提取知识点,然后慢慢一步步深入每个知识点

0 旋风ppt拜读/实验前需要具备的基础知识点

这份资料是关于C++面向对象技术的高级课程讲义,内容涵盖运算符重载移动语义SOLID原则三大核心主题。

一、运算符重载

核心思想

让自定义类的对象用起来像内置类型一样自然、直观。例如:a + ba.Add(b) 更符合数学表达习惯。

成员函数 vs 友元函数的选择
情况 推荐形式 原因
修改自身的运算符(如 += 成员函数 约定俗成,必须修改 this
赋值运算符(= 成员函数 语言规定,必须为成员
非修改运算符(如 + 友元函数 确保对称性,如 v*2.0f2.0f*v 都能编译
左操作数是其他类型(如 ostream << obj 友元函数 无法给 ostream 添加成员函数
关键规则
  1. 复合赋值运算符 (如 +=)返回 *this 的引用,支持链式操作。
  2. 二元运算符 (如 +)通常通过复合赋值实现:
    Vector3D operator+(Vector3D lhs, const Vector3D& rhs) { return lhs += rhs; }
    左操作数按值传递,避免逻辑重复,且可能触发NRVO。
  3. 比较运算符==!= 通常用友元实现对称性,!= 应该复用 ==
  4. 输出运算符<<)必须是友元函数。
  5. 禁止重载的运算符&&||、逗号、取地址(会改变短路求值等固有行为)。
  6. 黄金法则 :重载运算符必须保持直观语义。例如 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(五法则)

如果手动定义了以下任何一个,通常需要定义全部五个:

  1. 析构函数
  2. 拷贝构造函数
  3. 拷贝赋值运算符
  4. 移动构造函数
  5. 移动赋值运算符
    每个成员职责:
  • 析构:释放资源。
  • 拷贝构造:深拷贝。
  • 拷贝赋值:释放旧资源+深拷贝。
  • 移动构造:窃取资源+源置空。
  • 移动赋值:释放旧资源+窃取+源置空。
Rule of Zero(零法则)

优先使用智能指针和STL容器,让编译器自动生成特殊成员函数,避免手动管理资源。

RVO/NRVO(返回值优化)
  • RVO:函数返回无名对象,编译器直接在调用者栈上构造,零拷贝。
  • NRVO:函数返回命名对象,编译器省略拷贝或移动。
  • 反模式 :不要写 return std::move(x);,这会阻止NRVO。

三、SOLID原则

SRP:单一职责原则
  • 一个类应该只有一个改变的理由。
  • 违反示例:GameEngine 类同时处理渲染、物理、输入、音效、存档等。
  • 修复:分解为 RendererPhysicsSystemInputHandler 等独立类。
OCP:开闭原则
  • 对扩展开放,对修改关闭。
  • 违反示例:通过 if/else 判断类型,新增类型时需修改现有代码。
  • 修复:使用多态,基类定义虚接口,派生类实现具体行为。
LSP:里氏替换原则
  • 子类必须能够替换基类而不影响程序正确性。
  • 违反示例:Square 继承 Rectangle,但修改 SetWidth 同时改变 height,导致 Rectangle 的不变式被破坏。
  • 修复:重新考虑继承关系,或使用组合。
ISP:接口隔离原则
  • 接口应该小而专注,不应强迫类实现不需要的方法。
  • 违反示例:ISceneNode 接口要求所有派生类实现 UpdateRenderHandleInputPlayAudio,但 LightNode 可能不需要输入和音频。
  • 修复:将接口拆分为 IUpdatableIRenderableIInputHandlerISerializable 等。
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。


六、实践建议

  1. 运算符重载 :先实现复合赋值,再基于它实现二元运算符;对称运算符用友元;!= 复用 ==
  2. 移动语义 :确保移动操作标记 noexcept;遵循Rule of Five完整实现;优先用Rule of Zero。
  3. SOLID重构:识别上帝类,按职责分解;用多态替代类型判断;将臃肿接口拆分为小接口;高层模块依赖抽象。
  4. 性能与可维护性:业务逻辑优先可维护性(抽象、SOLID);热点代码(如游戏循环)再考虑性能优化;永远在测量后优化。

七、里程碑任务(Milestone 1)

根据资料,你需要完成以下三项:

  1. 运算符重载:为你的类实现复合赋值、二元运算符、比较和输出运算符,注意成员/友元选择。
  2. Rule of Five :为 DynamicMesh 或你的资源类实现析构、拷贝/移动构造和赋值,移动操作标记 noexcept,用 Valgrind 检查内存泄漏。
  3. SOLID重构:重构场景图,识别至少3个SOLID违反,应用SRP分解和OCP多态,绘制重构前后UML类图,撰写简短设计报告。

八、旋风练习题



答案ppt都有

学习完基础知识,让我们来看看本次实验旋风要我们干什么

1Task 1:获取骨架并闻出坏味道

MiniEngine-04骨架提供的文件导入文件,这个不多赘述

  1. apps/week04/miain.cpp 符合本次实验(实验四)的单元测试
  2. include/Vector3D.h
    include/DynamicBuffer.h src/DynamicBuffer.cpp
  3. refactor/GameManager
  4. 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 原则:

  • S -- Single Responsibility Principle (SRP)

    • 每个类应仅有一个职责。
    • GameManager 违反 SRP,因为它管理资源、场景节点、渲染和输入,多种职责混在一起。

2 条件分支过多 / 开放封闭违背

坏味道描述:
UpdateRender 函数中大量 if-elseelse if 来判断节点类型("Mesh"、"Camera"、"Light"),硬编码逻辑。

cpp 复制代码
if (node == "Mesh") { ... }
else if (node == "Camera") { ... }
else if (node == "Light") { ... }

问题:

  • 每增加一种节点类型都必须修改 GameManager,违反了可扩展性。
  • 节点行为和类型耦合。

对应 SOLID 原则:

  • O -- Open/Closed Principle (OCP)

    • 软件实体应对扩展开放,对修改关闭。
    • 这里每增加节点类型就要改 Update / Render,违反 OCP。
  • D -- Dependency Inversion Principle (DIP)(间接相关)

    • 高层模块依赖具体实现,而非抽象。
    • GameManager 硬编码具体节点类型,而不是依赖抽象接口。

3 硬编码输入逻辑 / 开放封闭违背

坏味道描述:
HandleInput 使用硬编码的字符判断操作:

cpp 复制代码
if (key == 'W') { ... }
else if (key == 'S') { ... }

问题:

  • 新增操作或修改按键必须修改 GameManager
  • 输入处理与游戏逻辑耦合。

对应 SOLID 原则:

  • O -- Open/Closed Principle (OCP)

    • 对扩展不开放:添加新按键要改类。
  • S -- Single Responsibility Principle (SRP)

    • 输入处理和游戏状态更新本应由不同模块管理。

4 资源存储耦合 / 高依赖具体类型

坏味道描述:
Resource 直接存储为字符串 dataGameManager 管理所有资源类型。

cpp 复制代码
struct Resource {
    std::string name;
    std::string type;
    std::string data;
};

问题:

  • 对不同资源类型(纹理、模型、声音等)缺乏抽象。
  • 添加新资源类型需要修改 GameManager

对应 SOLID 原则:

  • O -- Open/Closed Principle (OCP)

  • L -- Liskov Substitution Principle (LSP)

    • 不能通过多态来处理不同资源类型,违反 LSP 的可替换性。

总结表格

坏味道 位置 / 示例 违反 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 运算符重载 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/dfeb78efacf0438ba379c971d52a613b.png) 根据UML设计,在已有的 include/Vector3D.h 中补全,需要提交include/Vector3D.h src/Vector3D.cpp apps/week04/main.cpp ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/bf708d906f924d36bd0faa6acc2c107a.png) 先看一下老师给的代码 `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 (向量叉乘)的情况,②便于链式复用复合赋值 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/dc05abbe9a374076be7f2c2baf2327c0.png) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/81d6b4e761994dae9d4574e17f8353ce.png) #### (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 头文件!!!(输出流没办法奏效) 然后输出是下面这样就代表成功完成了 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/236f54416fbe49ef8b67751131f57bb1.png) 最后是经典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设计 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/52a487d50d2c4707a640f63b0553b8c1.png) 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\* ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c15398f5bad8478c981088da201d217a.png) 1. **深拷贝**解决重复释放 2. 移动语义必须noexcept,因为std::vector 扩容时,如果移动可能抛异常,就不用移动 3. 防自赋值 4. 赋值顺序 .![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/da5a93454dd64cfb9e4d9f0908733f2e.png) ##### ② 知识点整理 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. ``` 最关键的部分(截图一) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f0e135fcc52c45849c40df7273b17d31.png) 这个是Google Test(GTest)运行测试的完整报告,表示Vector3D 类和 DynamicBuffer 类的构造、移动、赋值、操作符重载等功能都正确实现了。 #### (2) 性能测试规格 完善MathPerfTest.cpp ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/5f72d05af4dd42ce87e034a46e89facf.png) 先理解一下要干什么,就是删去DynamicBuffer里面的noexcept,运行用于性能测试的MathPerfTest.cpp,并把两次结果截图,尤其对比Bench2 解释: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/3be1315615d64567ae706e8f7daf2e23.png) 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 ``` 预期输出 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/4fe26018c17847c9b23882f5f564780b.png) ###### 无noexcept 然后我们删去DynamicBuffer.h和.cpp里面移动构造和移动赋值的noexcept,再同样测试一遍,预期输出如下: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b25859f5906d422d978282b068621f36.png) #### (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 ##### ①重构前 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/e42d5b235fc24bedafb70ca7648728cb.png) 也就是说这个类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** (从此图出发创建所有新文件) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/2da5f384d9334d1eab30a6f72e237a47.png) **关键调用序列图** ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ff2c962cf7ff4450807062532ea49374.png) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0870016fccb5418686fb94b6da56d73b.png) 看完了,还是很懵 这个任务本质是在做一次**面向对象重构(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 ``` 预期类似结果输出: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/4adb3faf42f542469fd36e74c6943314.png) 测试 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 ``` 预期输出如下: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0dc7115f166b47d6a74afa59c375279c.png) #### (2)完成 里程碑 1 报告 docs/milestone1-report.md 要求如下: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/96d77171da264eb6b4dba528cffe4b57.png) ##### ①坏味道列表 详细见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 类图(重构前后对比) 可使用上方两张图 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/82adb433c54048b082ac2a054bf2de37.png) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/03d95cec2f4a4e1da3e9f2f92f662911.png) ##### ③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倍左右 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0884195be1c144b59dfecd20f8b1eeda.png) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b25859f5906d422d978282b068621f36.png) ##### ⑤ 重构收益说明 新增输入方式 / 新增节点类型各需修改哪些文件 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/79f26cd577ac4ce69243016b2c416e1e.png) #### (3)MR 提交的时候 标题为\[Week4\]\[\[Task4\]完成 GameManager的重构和里程碑 1 报告 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0019dfb09dd1457ca4ca48d7fa311ff4.png)

相关推荐
cpp_25014 分钟前
P2871 [USACO07DEC] Charm Bracelet S
数据结构·c++·算法·动态规划·题解·洛谷·背包dp
郝学胜-神的一滴6 分钟前
深入epoll反应堆模型:从libevent源码看高性能IO设计精髓
linux·服务器·开发语言·c++·网络协议·unix·信息与通信
_F_y9 分钟前
C++11 异步操作实现线程池
java·jvm·c++
!停13 分钟前
C++入门STL容器Vector使用基础,深挖 Vector替代 C 语言繁琐容器的利器
开发语言·c++
tankeven40 分钟前
C++ 学习杂记06:std::unordered_map
c++
t***54443 分钟前
如何在 Dev-C++ 中设置 MinGW 和 Clang 的路径
java·前端·c++
cpp_25011 小时前
P2722 [USACO3.1] 总分 Score Inflation
数据结构·c++·算法·动态规划·题解·洛谷·背包dp
t***5441 小时前
如何在 Dev-C++ 中配置 Clang 编译器集
开发语言·c++
王老师青少年编程1 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【删数问题】:删数问题
c++·算法·贪心·csp·信奥赛
qq_254617772 小时前
attribute((constructor)) 在C/C++中的应用
开发语言·c++