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中输入标题

Week04Task1 获取 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 <iostream>
#include <cmath>

// 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. <algorithm> // std::copy
  5. 移动语义
  6. noexcept 与性能优化
③ 代码详细解读

先看一下老师给的带提示的空白代码

cpp 复制代码
#pragma once

#include <cstddef>

// 模拟管理堆内存的缓冲区类,用于 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 <cstddef>

// 模拟管理堆内存的缓冲区类,用于 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 <algorithm> // 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 <algorithm> // 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时间库全面解析
  2. C++的基准测试

看一下老师给的提示的MathPerfTest.cpp

cpp 复制代码
#include <chrono>
#include <iostream>
#include <vector>

#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 <chrono>
#include <iostream>
#include <vector>

#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<std::chrono::milliseconds>(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<std::chrono::milliseconds>(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<DynamicBuffer> 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<std::chrono::milliseconds>(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的时候标题为Week04Task3 实现 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 <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

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 <iostream>

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 <vector>
#include <memory>

#include "IInputHandler.h"
#include "SceneNode.h"   // 这个可以 include(节点抽象)

class GameLoop {
private:
    std::vector<std::unique_ptr<SceneNode>> nodes_;
    std::unique_ptr<IInputHandler> inputHandler_;

public:
    explicit GameLoop(std::unique_ptr<IInputHandler> handler);

    void AddNode(std::unique_ptr<SceneNode> node);

    void Tick(float dt);

    void HandleInput(int key);
};

GameLoop.cpp

cpp 复制代码
#include "GameLoop.h"

GameLoop::GameLoop(std::unique_ptr<IInputHandler> handler)
    : inputHandler_(std::move(handler)) {}

void GameLoop::AddNode(std::unique_ptr<SceneNode> 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 <gtest/gtest.h>

// 完成 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 <gtest/gtest.h>

// 完成 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 <memory>
#include <string>
#include <vector>

// 测试用 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<TestInputHandler>();
    GameLoop game(std::move(input));

    auto node1 = std::make_unique<TestNode>("node1");
    auto node2 = std::make_unique<TestNode>("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>();
    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<TestInputHandler>();
    GameLoop game(std::move(input));

    auto newNode = std::make_unique<NewNode>("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 <iostream>
#include <memory>

#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<KeyboardInputHandler>();
    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。

  1. 条件分支过多

    • 问题描述:
      UpdateRender 中大量 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)
        高层模块依赖具体实现,而非节点抽象接口。

  1. 硬编码输入逻辑

    • 问题描述:
      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)
        输入处理本应独立于游戏状态管理。

  1. **资源存储耦合 **

    • 问题描述:
      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)
        无法通过多态处理不同资源类型,违反可替换性。

  1. 字符串作为节点标识

    • 问题描述:
      sceneNodes_ 使用 std::string 表示节点类型:

      cpp 复制代码
      std::vector<std::string> sceneNodes_;

      节点类型和逻辑高度耦合,无法通过接口或多态管理。

    • 影响:

      添加新节点类型、扩展节点行为或修改渲染逻辑必须修改 GameManager

    • 对应 SOLID 原则:

      • O -- Open/Closed Principle (OCP)
        对节点类型扩展不开放。
      • D -- Dependency Inversion Principle (DIP)
        高层依赖具体类型字符串,而非抽象节点接口。

  1. 重复输出日志

    • 问题描述:
      UpdateRenderHandleInput 方法中重复 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 报告

相关推荐
handler0118 分钟前
【算法】并查集(普通/扩展/带权)模板与例题
数据结构·c++·笔记·算法·c·图论·查并集
繁星蓝雨1 小时前
C++中对比pragma once和ifndef的使用区别
开发语言·c++·ifndef·头文件·pragma once
.千余1 小时前
【C++】C++手写Vector容器:从底层源码模拟实现
开发语言·c++·经验分享·笔记·学习
a诠释淡然1 小时前
C++ vs Rust:哪个更适合你的下一个项目?
开发语言·c++·rust
小小de风呀1 小时前
de风——【从零开始学C++】(十二):stack和queue的基本使用和模拟实现
开发语言·c++
汉克老师1 小时前
GESP6级C++考试语法知识(五十三、动态规划----背包问题(六、分组背包)
c++·动态规划·背包问题·gesp6级·gesp六级·分组背
雪度娃娃2 小时前
转向现代C++——保证const成员函数的线程安全性
开发语言·c++
坚果派·白晓明2 小时前
[鸿蒙PC三方库移植适配] 使用 AtomCode + Skills 自动完成Protobuf鸿蒙化适配
c语言·c++·华为·harmonyos
原来是猿2 小时前
深入理解 C++ unordered_map 与 unordered_set
开发语言·c++
满天星83035772 小时前
【Qt】信号和槽 (一)(概述和基本使用)
开发语言·c++·qt