画布应持有图形对象的独占所有权,用std::vector管理;shape基类封装坐标与尺寸,子类只实现渲染逻辑;draw()接口应无渲染后端依赖,避免重复计算坐标或耦合具体api。

画布对象该不该持有图形对象的指针
多数初学者会直接在 Canvas 类里用 std::vector<shape></shape> 存圆形和矩形,但这样容易引发悬空指针或内存泄漏。更稳妥的做法是让画布管理所有权:用 std::vector<:unique_ptr>></:unique_ptr>。这样添加图形时用 canvas.add(std::make_unique<circle>(...))</circle>,析构自动清理,也不用担心谁负责 delete。
如果图形需要跨函数共享或被多个画布引用,才考虑 std::shared_ptr,但得同步检查循环引用风险——比如图形内部又存了对画布的 shared_ptr,就可能永远不释放。
Circle 和 Rectangle 怎么共用 draw() 接口而不重复写坐标逻辑
关键不是“怎么画”,而是“谁提供坐标”。把位置、尺寸等公共数据抽到基类 Shape 里,声明纯虚函数 draw(),但让子类只负责“怎么渲染”,不负责“在哪渲染”。例如:
class Shape {
protected:
double x_, y_, width_, height_;
public:
virtual void draw() const = 0;
// 提供 get_bounds() 或 transform() 等辅助接口,供上层统一做平移/缩放
};
这样画布遍历时调用 shape->draw(),而具体实现里(比如 OpenGL 或 SDL)只管调用 glDrawArrays 或 SDL_RenderFillRect,不用每种图形都重算屏幕坐标。
常见错误是把 draw() 写成带参数的,比如 draw(SDL_Renderer*),结果导致基类依赖具体渲染后端——换 DirectX 就得改所有子类。保持接口无依赖,靠构造时注入或画布统一传入上下文更灵活。
要不要为每个图形单独开一个 .cpp 文件
小项目(shape.hpp 包含 Shape、Circle、Rectangle 全部定义和 inline 实现。编译快,修改不牵连多文件。
图形变多或逻辑复杂(比如 Circle 加了贝塞尔插值动画、碰撞检测)再拆分:
-
shape.hpp:只留基类声明 +virtual ~Shape() = default; -
circle.hpp:继承声明 + 公共接口 -
circle.cpp:实现细节,尤其是涉及第三方库(如glm::mat4变换)时避免头文件污染
注意:一旦拆 .cpp,子类构造函数就不能是 inline,且必须确保 Shape 的析构函数是虚函数,否则 delete shape_ptr 会漏掉子类成员析构。
画布 resize 后图形位置错乱怎么办
这不是图形对象的问题,是坐标系没对齐。典型表现:窗口从 800×600 拉到 1600×900,圆心还在 (100,100) 像素处,看起来“跑偏”了。
解决思路分两层:
- 逻辑坐标:所有图形存的是归一化坐标(0.0–1.0)或世界坐标(如米制),由画布统一映射到像素
- 画布维护
view_matrix或scale_x_, scale_y_, offset_x_, offset_y_,draw()调用前先变换
别在 Circle::draw() 里硬编码 SDL_RenderDrawCircle(renderer, x_, y_, r_) —— 这样 resize 时得遍历所有图形改 x_/y_。应该让画布在渲染前统一做一次视口适配,图形只管提供原始几何数据。
最容易被忽略的一点:鼠标点击拾取图形时,也要用同一套逆变换把屏幕坐标转回逻辑坐标,否则点选失效。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











