ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++面向对象实战:从继承多态到智能指针的战斗系统设计

C++面向对象实战:从继承多态到智能指针的战斗系统设计 这篇大作业做到第三部题目叫“开战”说实话看到这个标题我先是松了口气因为前两步的地图和资源系统已经基本定下来了接着又捏了把汗因为战争系统是整个大作业里最容易暴露设计问题的环节也是一道“类设计能力分水岭”。很多人前两步写得很顺一到开战逻辑就变成一长串 if-else 堆出来的脚本表面上“能跑”但程序一复杂就失控。这篇就把我做“魔兽世界三开战”时的完整思路写出来覆盖类层次设计、战斗主循环、多态调用、容器使用和调试经验。无论你是正在赶大作业还是想复习 C 面向对象都能从里面捞到点干货。1. 战斗单元的继承树先定 Unit 这个根再谈别的1.1 为什么必须做继承体系而不是各写各的“魔兽世界”这个题材天然适合用继承来建模步兵、弓箭手、狼骑兵、英雄属性上有大量重叠——都有名字、血量、攻击力、防御力、射程行为上也有重叠——都要能被攻击、会攻击别人、可能放技能。如果每个兵种单独写一个类代码能膨胀到难以维护。当时大作业的前两步里已经出现了类似的重复代码比如步兵和弓箭手各有一份受伤函数函数体几乎一致唯一的差异是减伤逻辑不同。到“开战”这一步必须抽出一个Unit基类把这些公共的部分收编进去。抽出基类的过程其实就是面向对象里“共性抽取”的实践。我在设计时把属性分成两组所有单位都有名称、最大血量、当前血量、攻击力、防御力、射程、速度。部分单位才有技能类型、经验值、武器类型、暴击率。第一组直接放进基类作为protected成员。第二组里如果某个属性只服务于某一个派生类就不放到基类里比如经验值只属于英雄武器类型只属于士兵怪物不需要。这个决定最初有点犹豫因为把经验值放进基类后续加逻辑更方便但那样会让怪物和普通士兵都背上“升级”的概念从建模角度属于“为了省事制造错位概念”。保持基类精简是后面能顺畅扩展的前提。1.2 Unit 基类的接口设计哪些该虚哪些该普通基类的接口决定了整个战斗系统能实现到什么程度。我当时是这样设计的class Unit { protected: std::string m_name; int m_maxHp; int m_hp; int m_attack; int m_defense; int m_range; int m_speed; public: Unit(const std::string name, int hp, int atk, int def, int range, int speed) : m_name(name), m_maxHp(hp), m_hp(hp), m_attack(atk), m_defense(def), m_range(range), m_speed(speed) {} virtual ~Unit() default; bool isAlive() const { return m_hp 0; } const std::string getName() const { return m_name; } int getHp() const { return m_hp; } // 纯虚函数每种单位的攻击行为不同 virtual void attack(Unit target) 0; // 虚函数默认的受击逻辑派生类可覆写 virtual int takeDamage(int damage) { int realDamage std::max(1, damage - m_defense); m_hp std::max(0, m_hp - realDamage); return realDamage; } // 虚函数技能默认什么都不做 virtual void useSkill(BattleContext ctx) {} // 非虚接口留给外部统一调用 void heal(int hp) { m_hp std::min(m_maxHp, m_hp hp); } };这里有个很重要的设计决策attack为什么是纯虚函数而takeDamage只是普通虚函数因为“攻击”这件事在不同兵种之间差异太大步兵就是贴脸砍弓箭手要判断距离英雄可能附带技能效果让基类给一个“默认实现”毫无意义所以用 0强制每个派生类自己实现。而受伤逻辑虽然也有差异但大部分兵种都是“伤害减去防御”这个默认逻辑是成立的个别特殊单位比如圣骑士可以覆写它实现伤害减免。当时犹豫过要不要把useSkill也做成纯虚函数后来想想还是给了一个空的默认实现更方便。因为普通步兵根本没有技能如果做成纯虚每个步兵类都得写一个空函数体纯属噪音。空实现还能让调用方无脑调用unit-useSkill(ctx)不用每次判断“这个单位是不是英雄”。还有一个容易被忽略但非常关键的点析构函数一定要写成虚函数。只要基类指针指向派生类对象并且在删除这个指针时析构函数不是虚函数就属于未定义行为。大作业阶段可能看不出问题但程序跑到释放内存时就会神秘崩溃。养成习惯基类析构函数一律virtual ~Unit() default;。1.3 从 Unit 派生Soldier、Hero、Monster 三类我最终只做了三个派生类不多但足够表达设计意图Soldier普通单位攻击方式是造成攻击力 - 目标防御的伤害。它代表最通用的行为。Hero在Soldier的基础上增加了经验值、等级、技能系统攻击时附带额外的英雄技能效果。Monster野怪可以有特殊的被 attack 行为比如反伤、潜行或者被打死后掉落奖励。这个三级层次在写战斗逻辑时用基类指针就能统一操作。下面是 Soldier 的简单实现class Soldier : public Unit { public: Soldier(const std::string name, int hp, int atk, int def, int range, int speed) : Unit(name, hp, atk, def, range, speed) {} void attack(Unit target) override { target.takeDamage(m_attack); } };就这么简单一个普通兵种的攻击行为已经完整了。弓箭手如果想实现“射程外不能攻击”可以在自己的attack里加距离判断或者覆写一个canAttack函数。Hero 则会这样class Hero : public Soldier { private: int m_level; int m_exp; public: Hero(const std::string name, int hp, int atk, int def, int range, int speed) : Soldier(name, hp, atk, def, range, speed), m_level(1), m_exp(0) {} void useSkill(BattleContext ctx) override { // 英雄特有比如造成范围伤害或治疗 } void gainExp(int exp) { m_exp exp; if (m_exp 100) { m_level; m_exp - 100; m_maxHp 20; m_hp 20; m_attack 5; } } };这里 Hero 从 Soldier 继承是“士兵也能做到的事英雄基本也能做到只是英雄更强一些”的逻辑推演。如果从 Unit 直接派生出 Hero也可以但是从代码复用角度会重复 Soldier 中的通用攻击逻辑。三层继承结构在展示大作业时也能给老师一个信号你理解继承并不只是一层的概念。2. BattleEngine 与伤害结算把回合制战斗的主循环写稳2.1 为什么单独抽一个 BattleEngine而不是把战斗逻辑塞进 Unit战斗逻辑是整个系统里最复杂的一部分它要管理双方的所有单位、判断回合、处理攻击顺序、结算伤害、处理单位死亡、判断胜负。如果把这些代码写进 Unit 类里一个单位将不得不知道整个战场的所有细节这显然违背了单一职责原则。我一开始也犯过这个毛病把“某个单位对另一个单位发起攻击”理解成“这个单位自己执行攻击”结果 Unit 里塞满了对战场状态的引用类之间耦合得一塌糊涂。后来想通了Unit 只负责“单个单位的属性变化和行为”BattleEngine 负责“规则调度”。换句话说Unit 是一个棋子BattleEngine 才是棋盘和裁判。这个划分让双方的关注点完全分离——设计 Unit 时我只需要考虑“一个单位怎么伤害/被伤害”设计 BattleEngine 时我只需要考虑“场面如何按轮次推进”。一个简化的 BattleEngine 骨架如下enum class Camp { Red, Blue }; class BattleEngine { private: std::vectorstd::unique_ptrUnit m_redArmy; std::vectorstd::unique_ptrUnit m_blueArmy; int m_turn; public: BattleEngine() : m_turn(1) {} void addUnit(Camp camp, std::unique_ptrUnit unit) { if (camp Camp::Red) m_redArmy.push_back(std::move(unit)); else m_blueArmy.push_back(std::move(unit)); } void run() { while (!isGameOver()) { executeTurn(); m_turn; } announceWinner(); } private: bool isGameOver() const { return allDead(m_redArmy) || allDead(m_blueArmy); } void executeTurn() { std::cout 第 m_turn 回合 std::endl; // 红方行动 for (auto unit : m_redArmy) { if (!unit-isAlive()) continue; chooseAndAttack(unit, m_blueArmy); } // 蓝方行动 for (auto unit : m_blueArmy) { if (!unit-isAlive()) continue; chooseAndAttack(unit, m_redArmy); } } void chooseAndAttack(std::unique_ptrUnit attacker, std::vectorstd::unique_ptrUnit enemies) { // 找到一个存活目标 for (auto enemy : enemies) { if (enemy-isAlive()) { attacker-attack(*enemy); break; } } } bool allDead(const std::vectorstd::unique_ptrUnit army) const { return std::all_of(army.begin(), army.end(), [](const std::unique_ptrUnit u) { return !u-isAlive(); }); } };这里有个细节executeTurn每一回合先让红方全体行动再让蓝方全体行动。这种“先后手”的判定影响很大后面我会讲它的问题和改进。大作业阶段可以先这样写逻辑清楚演示也不会出大乱子。2.2 攻击速度与行动排序为步兵和弓箭手设计合理的先手权如果每个单位都按照队形依次行动会出现一个奇怪的局面无论是红方还是蓝方处在数组前面的单位永远先攻击与速度无关。这个时候如果有一个速度极快的刺客型单位但被放在数组末尾它反而会成为最后出手的人完全不合理。我做法是引入行动顺序数组在每回合开始时按速度降序排序void executeTurn() { // 构建一个包含所有活着的单位的行动序列 std::vectorUnit* actionOrder; for (auto unit : m_redArmy) { if (unit-isAlive()) actionOrder.push_back(unit.get()); } for (auto unit : m_blueArmy) { if (unit-isAlive()) actionOrder.push_back(unit.get()); } std::sort(actionOrder.begin(), actionOrder.end(), [](const Unit* a, const Unit* b) { return a-getSpeed() b-getSpeed(); }); for (Unit* actor : actionOrder) { if (!actor-isAlive()) continue; // 可能被前面的单位打死了 ... } }这里用到了std::sort和algorithm头文件顺便解决了“快单位后手”的问题。还有一个细节排序后如果前面的单位把后面的单位打死了后面单位在轮到它时应该检测一次存活否则会出现“一个尸体又跳起来打人”的笑话。2.3 伤害结算与“保底伤害”别让防御高到无敌伤害结算公式是max(1, attack - defense)也就是说无论对面防御多高至少也会受到 1 点伤害。这个保底机制在日常游戏中很常见不然会出现“满防御的城墙型单位磨光所有回合也打不死”的死锁局面。但光有保底还不够。我在测试时发现一个问题假设一个单位攻击力 30防御力 28每次只造成 2 点伤害就算有保底战斗也会拖成一场漫长的消耗战。为了让开战节奏看起来“爽”一点我参考了一些常见游戏的做法把伤害公式改成带有“波动比例”的形式比如int realDamage std::max(1, damage - m_defense);这是最基础的版本适合大作业演示。如果想让数值更有层次可以把攻击力拆成“基础攻击 随机浮动”再减去防御。我最终在代码里保留了最简的max(1, damage - defense)因为大作业的评分重点是类和逻辑结构不是数值系统越简单越好解释。2.4 死亡处理与“尸体障碍”坑了我一整个晚上的 bug死亡和非死亡单位的处理点位特别容易出问题。我第一版代码写的是for (auto unit : army) { if (!unit-isAlive()) { // 尝试删除 } unit-attack(...); }在遍历一个vector的同时删除元素这造成迭代器失效整个循环行为变成未定义。很多同学一跑程序发现“随机崩溃”十有八九就是这类问题。我当时采用的解决方法是先标记后清理。executeTurn里只负责让活人打人不删除任何单位等整个回合结束后再统一把死亡单位移除。这样做还有一个额外的好处死亡动画、尸体的“占位”效果如果游戏逻辑需要都不会因为删除太早就丢失。void removeDead(std::vectorstd::unique_ptrUnit army) { army.erase( std::remove_if(army.begin(), army.end(), [](const std::unique_ptrUnit u) { return !u-isAlive(); }), army.end()); }std::remove_if配合erase是 C 里清理容器的标准操作。原因是remove_if会把所有不满足条件的元素移到容器前面然后返回新逻辑末端的迭代器再配合erase把后面那截彻底删掉。这个方法我在项目里用了无数遍大作业里也很值得用上。3. 英雄技能与多态把“差异性”交给虚函数而不是 if-else3.1 技能系统的最初设计一个让人头疼的 switch战斗系统里最容易膨胀的是技能逻辑。最开始我做了一个极冗余的版本战斗里出现“技能释放”时先判断单位类型再根据类型调用不同的处理// 这种做法不推荐只是反面教材 void castSkill(Unit* caster) { if (dynamic_castHero*(caster)) { // 英雄释放雷霆一击 } else if (dynamic_castMonster*(caster)) { // 怪物释放毒液喷射 } }第二种写法的危害是很明显的。每加一个新技能castSkill就得改一次类型判断列表越来越长dynamic_cast本身就说明你没有好好利用多态。更危险的是每次检查都依赖运行时类型信息会拖慢程序速度而且一旦继承层次调整这些代码可能直接编译报错。如果有人问你“什么是面向对象的反模式”这个就是。3.2 用虚函数解放技能系统正确的思路是让每个单位自己知道怎么释放技能外部不需要关心对象的具体类型。在Unit基类里声明virtual void useSkill(BattleContext ctx) {}然后Hero类覆写它class Hero : public Soldier { public: void useSkill(BattleContext ctx) override { // 对敌方全体造成 10 点伤害 } };BattleContext是自定义的战斗上下文结构里面可以包含当前回合数、双方军队的引用、施法者自身等。这样设计之后调用方可以统一处理void BattleEngine::executeHeroSkills(Camp camp) { auto army (camp Camp::Red) ? m_redArmy : m_blueArmy; for (auto unit : army) { unit-useSkill(ctx); } }不管以后新增“死亡骑士”“熊猫酒仙”还是“恶魔猎手”调用方代码一行都不用改这是多态带来的真正收益。写大作业的时候如果能在答辩时讲出这一层“新增扩展点而不用改调用方”的好处得分会比单纯罗列代码高很多。3.3 地形、Buff 与 BattleContext 的边界为了让上下文对象不至于变成“万能口袋”我控制它只包含战斗过程中各单位都需要读的数据比如当前回合、双方兵力引用、一个简单的随机数生成器。这样设计后技能函数在释放过程中如果需要查询敌方血量可以从上下文里拿如果需要造成伤害可以直接操作目标对象。还有一个细节值得提BattleContext里的军队容器是引用还是拷贝必须是引用。如果拷贝一份技能对敌人造成伤害实际上打的是副本正主安然无恙整个战斗逻辑就白写了。struct BattleContext { std::vectorstd::unique_ptrUnit redArmy; std::vectorstd::unique_ptrUnit blueArmy; int turn; BattleContext(std::vectorstd::unique_ptrUnit red, std::vectorstd::unique_ptrUnit blue, int turn) : redArmy(red), blueArmy(blue), turn(turn) {} };这个上下文类的存在让技能系统有了一个干净的数据通道。以后想加“地形让火焰技能伤害翻倍”这类功能只需要扩展BattleContext的字段技能函数内部按需读取即可不需要一路改调用链。4. vectorunique_ptr 的三个坑切片、裸指针、内存管理4.1 对象切片的坑为什么不能用 vector 存兵种用std::vector存对象是个很自然的想法但如果你写std::vectorUnit army;然后把Hero或Soldier往里面放C 会执行对象切片object slicing。也就是派生类对象被“切”掉派生部分只拷贝基类部分进去结果就是英雄被存进去后等级、经验、技能这些属性全部丢失变成了一个“披着英雄外表的普通士兵”。而且由于多态依赖对象的动态类型Unit对象的动态类型就是Unit本身虚函数调用也找不到派生类的实现了。正确做法是存指针让对象留在堆上容器保存指向对象的指针这样派生类信息不会被切掉。4.2 unique_ptr 还是裸指针这里不仅是风格问题裸指针存在内存泄漏的风险因为需要手动delete。大作业虽然规模小但养成手动内存管理的习惯会埋下隐患。使用std::unique_ptr后智能指针在容器销毁时会自动释放所管理的对象异常发生时也会安全释放内存天然具备异常安全性。m_redArmy.push_back(std::make_uniqueHero(阿尔萨斯, 600, 80, 50, 1, 5)); m_redArmy.push_back(std::make_uniqueSoldier(步兵甲, 200, 30, 20, 1, 3)); m_blueArmy.push_back(std::make_uniqueMonster(野狼, 150, 25, 15, 2, 4));std::make_unique在 C14 里可用如果你的课程还停留在老标准可以用std::unique_ptrHero(new Hero(...))。它有几个好处创建对象和分配内存一步完成安全性更高配合容器使用可以完全避免手动 delete。4.3 unique_ptr 的移动语义与容器的配合std::unique_ptr是只可移动不可拷贝的类型。这意味着你不能把一个unique_ptr复制到另一个容器但是可以std::move过去。这个特性在战斗系统中有一个实际好处它强制你明确“所有权”的概念。一个单位在任意时刻只能属于一个容器要么是红方军队要么是蓝方军队不能同时存在于两个地方。用裸指针的话很难保证这点。当初做addUnit时参数我直接传std::unique_ptrUnit调用方必须显式用std::move转移所有权engine.addUnit(Camp::Red, std::make_uniqueHero(...));这看起来比传裸指针麻烦一点但让代码的意图非常明确一旦英雄被addUnit进去外部就不再持有它的裸指针后续要操作它必须通过BattleEngine提供的接口。这对防止“手一抖在某个地方 delete 了一个还在容器里的对象”非常有效。4.4 善用 get()需要裸指针时怎么办unique_ptr虽然方便但有时你必须传裸指针给某些接口比如std::sort里要对unique_ptr解引用。这时候用.get()获取原始指针而不是.release()。release()会放弃对指针的所有权之后容器不再负责 delete非常容易泄漏。我在项目里只在需要传给传统 C 接口且明确生命周期时才用release()其他场景一律.get()。5. 覆盖、隐藏与重载为什么弓箭手的攻击总是“失灵”5.1 一次几乎让我崩溃的“失灵”现场在写完弓箭手这个类之后我遇到一个怪象明明弓箭手在攻击时应该先判断距离、再造成远程伤害但实际战斗里它总是使用基类的“近战攻击”逻辑。原因就是我在派生类里写了这样一个函数class Archer : public Soldier { public: void attack(Unit target) { // 这里没有写 override 关键字而且基类对应函数签名一致 ... } };如果基类里的attack是虚函数派生类中参数完全相同的一个函数也算覆写编译器在大多数情况下还能正常动态绑定。但如果你把基类的attack从虚函数改成了非虚函数那么派生类里的同名函数就变成“隐藏”而不是“覆写”调用时看指针类型决定走哪个版本。当时我的Soldier基类里attack少写了一个virtual于是Archer的所谓“覆写”实际上只是隐藏了基类版本而BattleEngine总是通过Unit*或Soldier*指针调用所以永远调到了基类版本。5.2 override 和 virtual让编译器帮你改 bug为了避免这类问题C11 之后提供了override关键字。在派生类中如果这个函数确实覆写了基类虚函数加上override编译器会检查签名是否一致如果写错就报编译错误如果基类不是虚函数编译器也会报错。这个检查帮我省下了大量调试时间。// 基类需要时 virtual void attack(Unit target); // 派生类中 void attack(Unit target) override; // 编译器强制检查相比之下Soldier类的attack函数我一开始没有加override导致弓箭手类在继承时把基类的攻击逻辑覆盖成了“隐藏”版本。加上override之后编译器立刻提示我签名不一致问题直接暴露。5.3 重载、覆写、隐藏的区别一张表讲清这三类概念经常被搞混。重载是同一个作用域内多个同名函数参数不同覆写是基类虚函数在派生类中重新实现签名一致隐藏则是派生类中定义了与基类同名的函数基类的同名函数被屏蔽参数可以相同也可以不同。概念触发条件调用方式动态绑定常见问题函数重载同作用域同名但参数不同根据实参类型选择否容易被误认为覆写函数覆写基类有 virtual派生类重写同名同参数函数通过基类指针/引用调用是签名不一致导致不是覆写函数隐藏派生类定义同名函数参数的基类函数被遮挡根据指针静态类型调用否调用非虚成员函数时意外隐藏基类版本还有一个坑覆写时基类函数如果是const成员函数派生类也必须加const否则只是重载甚至隐藏。我在给isAlive()加const时发现派生类想覆写一个getHp差点因为const的问题失败编译器检查override字面量才看出来。5.4 推荐在派生类中统一使用 override而不是 virtual在派生类中写不写virtual都不改变它是虚函数的事实。但写override比写virtual更能表达意图。我在整个项目里规定基类用virtual所有派生类使用override。这个习惯让代码可读性提高不少后来老师抽查代码时也夸了一句“这习惯像搬砖多年的老手”。6. 从“能跑”到“能演示”数据平衡、防御性检查和现场防炸6.1 演示数据的重要性开局设置不能太随意大作业最终要演示。我第一版尝试时随便设置了双方兵力结果红方平均攻击力 80蓝方平均防御力 90打了十几个回合双方都死不了。后来调整数据时发现演示的最佳长度是 5 到 8 回合双方你来我往场面热闹但不会拖到观众走神。为了让演示可控我把出兵数据硬编码成一个固定的“表演剧本”不开放随机生成这样每次演示都能预期走向不会突然双方全灭结束。6.2 防御性检查不要相信任何外部数据战斗系统中几乎所有函数的输入都可能出现异常情况攻击目标是空指针、目标已经死亡、越界索引。我一开始认为“自己的代码自己最清楚”结果因为在chooseAndAttack里没检查目标是否存活出现过“攻击尸体”的尴尬。后来在每个关键入口都加上了防御性检查if (target nullptr || !target-isAlive()) { return; }虽然这让代码看起来有些啰嗦但在演示时非常有用程序不容易闪退。答辩现场一旦崩溃印象分直接跌到谷底。6.3 输出战报让战斗过程“看得懂”如果代码只是在后台计算数字演示效果会很差因为评委看不到发生了什么。我加了一个简单的战报输出把每一次攻击都变成一行文本第 3 回合 [红方] 阿尔萨斯 对 [蓝方] 野狼 造成 62 点伤害。 野狼 的生命值从 150 降到 88。 [蓝方] 野狼 对 [红方] 步兵甲 造成 21 点伤害。 步兵甲 的生命值从 200 降到 179。这种类似“战斗日志”的输出代码不复杂但对演示效果提升是肉眼可见的。相比一长串数字表格“阿尔萨斯对野狼造成了 62 点伤害”更有代入感也让评委能快速理解程序在做什么。6.4 环境与编译问题vscode 和 Visual C 的环境排查思路周围不少同学用的是 vscode 配 C/C 插件或者直接用 Visual Studio。环境问题几乎是每一届大作业的必经坑比如编译器版本过低不支持 C14/17导致make_unique、override报错或者缺少运行库报出“Microsoft Visual C 14.0 or greater is required”。这类问题的排查思路其实很统一先看编译器版本再看是否缺少组件。vscode 里可以打开终端执行g --version或cl检查版本如果安装了 VS 但命令行编不过多半是环境变量没配置好。运行库缺失的话去官方下载对应版本的 Redistributable 安装一遍基本能解决。我自己的经验是用 vscode 写大作业需要自己维护tasks.json和launch.json对新手不太友好如果课程要求不苛刻Visual Studio Community 版开箱即用创建空项目直接跑省掉不少环境折腾时间。但如果你是命令行爱好者那 vscode 配好之后也挺顺手关键是编译标准要调到 C17。6.5 代码结构与类拆分让老师一眼看出“面向对象”最后再提一下代码组织。把类拆到独立文件中是大作业的基本要求我在项目里按“一个类一对 .h/.cpp”来组织Unit.h / Unit.cpp Soldier.h / Soldier.cpp Hero.h / Hero.cpp Monster.h / Monster.cpp BattleEngine.h / BattleEngine.cpp main.cpp这么做的好处不只是“好看”更重要的是每个文件不会太长编译时间短改一个类不会导致全项目重编。如果时间充裕还可以把BattleContext单独放一个文件它虽然不是类但作为结构体占一个头文件命名更清晰。6.6 自测清单演示前跑三遍我在最终演示前给自己列了一个自测清单双方英雄能正常释放技能技能名称能正确出现在战报里。胜利条件能正确触发某一方全灭时程序结束并宣布胜者。连续跑三局没有出现一次内存泄漏或崩溃。前两个好检查第三个需要留意输出里是否有“Access violation”之类的异常情况。如果你在写代码时遵循了前文的几个原则智能指针、虚析构、迭代器安全第三项通常不会出问题。从我这次做“开战”的全过程来看最深的体会是战斗系统真正的难点不在于“让两边打起来”而在于“打起来之后代码还能不能保持清晰、能不能继续扩展”。继承、多态、智能指针、容器操作这些知识点单独拎出来都不难但凑到一起特别考验对对象生命周期的理解。如果你在写的过程中发现到处是dynamic_cast、大量 if-else 判断单位类型或者手动 new/delete那大概率是设计上走错了方向。回到基类和派生类的边界上重新想一想往往比硬着头皮继续堆代码更省时间。
返回列表