加载中...
在禁止多重继承的情况下,如何设计“直立智慧猩猩”类?
第1节:代码改善:一个“坑爹”的文字类冒险游戏
第2节:在禁止多重继承的情况下,如何设计“直立智慧猩猩”类?
第3节:C++多线程代码中的“乱序”执行现象
第4节:C++中函数指针有什么作用呢?
第5节:为什么我用c++写的游戏那么简陋?
第6节:多线程读写socket导致的数据混乱的原因是什么?
第7节:WebSocket 是什么原理?为什么可以实现持久连接?
第8节:怎样在c++中实现instanceof?
第9节:一个函数多处 return 是好风格吗?
第10节:C++中虚函数相比非虚函数的优势
第11节:为什么 C::C::C::C::foo() 能编译成功?
第12节:如何静态反射C++枚举的名字
第13节:看C++大叔如何拥 java 妹子入怀……
第14节:坨——理解递归实现“汉诺塔”代码的关键
第15节:C++编译器如何实现 const(常量)?
第16节:C++如何为断言加上消息
第17节:初学C++到什么水平,算是合格的初级开发工程师?
第18节:C++编程要避免使用单例模式吗?
第19节:学习C++要学boost库吗?
第20节:C++的继承就是复制吗?
第21节:C++构造函数失败,如何中止创建对象?
第22节:C++学完多线程后,学什么呢?
第23节:string_view 适合用做函数的返回值类型吗?
第24节:为指针取别名,为何影响const属性?
第25节:std::enable_shared_from_this 的存在意义?
第26节:C++模板可变参数如何一次性解包?
第27节:Linux下的c++开发,平时是怎么调试代码的呢?
课文封面

有 “猩猩”类,然后派生出 “直立猩猩” 和 “智慧猩猩”,现在,想要一个 “直立的智慧猩猩”,所以,就得有一个既 继承 “直立猩猩”,又继承 “智慧猩猩” 的派生类吗?

0. 问题

问:

现有一个名为“猩猩”的基类以及它的两个派生类“直立猩猩”和“智慧猩猩”,需要设计一个既会直立行走又很聪明的“猩猩”类。按照常理,这个类的实例应该既是一种“直立猩猩”又是一种“智慧猩猩”,可是我的工作环境禁止使用多重继承,那么我该如何设计这个类呢?

1. 多重派生与“死亡菱形”

如果使用提问者说的多重继承,即:“直立智慧猩猩” 同时派生自 “直立猩猩” 和 “智慧猩猩”,而后两者又已经各自派生自 “猩猩” ,这就会得到一个 C++ 当中的 “死亡菱形”:

死亡菱形的派生关系

这种派生结构会发现一个方法(C++中更多称为:成员函数)多份实现的问题。比如,假设基类“猩猩” 有一个 “吃” 的方法;“直立猩猩” 和 “智慧猩猩” 类都派生自它,于是各有一份 “吃” 的成员方法,这很好;但是,又来了一个 “直立智慧猩猩” 同时派生自前两者,现在,当需要调用“直立智慧猩猩”的“吃”方法时,如果采用普通的写法,编译器就无法决定,到底是使用来自 “直立猩” 的“吃”,还是来自 “智慧猩” 的 “吃”。

不仅成员函数如此,成员数据也是这样。假设基类 “猩猩” 里有一个叫“嘴”的成员数据,那么在“直立智慧猩猩”那里,就会暗中变成有两张嘴。

我们以 B 代表 “猩猩”,以 Da 代表 “直立猩”,以 Db 代表 “智慧猩”,以 Dab 代表 “直立智慧猩”,则有:

struct B { int a; void eat() {}; }; struct Da : B {}; struct Db : B {}; struct Dab : Da, Db { void test() { std::cout << this->a << std::endl; this->eat(); } };

编译至 test() 时,至少会报两个错:一是分不清this->a,二是分不清this->eat() ,注意这里的“分不清”英文是:“… is ambiguous”。

第一种解决方法是明确指定使用哪个基类的成员,比如:

this->Da::eat(); // 通过 Da:: 明确指定后面的 eat 来自 基类 Da

假设,“直立猩”和“智慧猩”有不同的 “吃” 法,于是各自在基类的基础又实现了自己的一份“吃”方法,那么,“直立智慧猩”可以依据需要,有时候按“直立猩”的吃法,有时候则走“智慧猩”的吃法,那么,这个方法就有点意义;但无论如何,有两张嘴,这就不叫“派生”了,这得叫“基因突变”。所以这种方法,能解决编译上的语法问题,但在语义层面往往说不通。

另一种方法是,是使用“虚继承”,这不是我们这个回答要说的话题,我们只简单说一句:通常也不太推荐。

2. 使用“能力”组合

只要还没有错得太深,那么推荐的方法通常是:推翻原有的继承体系设计,改用组合来实现。其基本思想是: 把“直立”和“智慧”,视为一种“能力”,那么,“猩猩” 就只需要一个“猩猩”类型就可以了,因为什么能力都不拥有的,就是“普通猩”(原来的基类),而拥有 “直立”能力的,就是“直立猩”,拥有“智慧”能力的,就是“智慧猩”,同时拥有二者的,就是“直立智慧猩”。

假设你在设计一种“进化”的猩猩,那么基于能力组合的设计不仅是正确的,恐怕还是唯一正确的设计;因为只有如此设计,才能在程序中实现让一只“普通猩” (可能由于经常爱思考)而进化成“智慧猩”,再加上因为经常跑步而进一步进化为“直立智慧猩”;甚至,可能因为后来成天躺床上玩手机刷短视频,而又退化成“普通猩”……

当然,很多时候,我们并不需要如此强大的灵活性,但纵使如此,我们也应该使用这种能力组合方法,来定义具体的 “直立猩”、“智慧猩”、“直立智慧猩”类。

3. 完整代码

下面是完整代码。代码后有在线运行的链接。为了直观,我们使用了汉字做为类名。大多数新版的C++ 编译器都支持使用汉字做标志符了。当然,不推荐在实际工作代码中使用。

#include <iostream> class 能力 { public: virtual ~能力() = default; virtual std::string 取能() = 0; }; class 直立能力 : public 能力 { public: std::string 取能() override { return "我的一小步,是人类进化的一大步"; } }; class 智慧能力 : public 能力 { public: std::string 取能() override { return "香蕉诚可贵,自由价更高。若为进化故,二者皆可抛"; } }; class 下半身超能力 : public 能力 { public: std::string 取能() override { return "嘿嘿嘿嘿嘿……"; } }; //------------------------------------------------------------- class 猩猩 { public: virtual ~猩猩() = default; virtual void 行走() { std::cout << "爬、爬、爬" << std::endl; } virtual void 思考猩生() { std::cout << "猩生就是吃香蕉" << std::endl; } }; class 直立猩 : public 猩猩 { public: void 行走() override { std::cout << 直立.取能() << std::endl; } protected: 直立能力 直立; }; class 智慧猩 : public 猩猩 { public: void 思考猩生() override { std::cout << 智慧.取能() << std::endl; } protected: 智慧能力 智慧; }; class 直立智慧猩 : public 猩猩 { public: 直立智慧猩() : 下半身(nullptr) {} ~直立智慧猩() override { delete 下半身; } void 设定贤者(bool 是) { if(是) { delete 下半身; 下半身 = nullptr; } else if (!下半身) { 下半身 = new 下半身超能力; } } void 行走() override { std::cout << 进化代价(&直立, "走") << std::endl; } void 思考猩生() override { std::cout << 进化代价(&智慧, "思考猩生") << std::endl; } protected: std::string 进化代价(能力* 原始能, std::string const& action) { if (!下半身) { return 原始能->取能(); } // 下半身思考 : return "我不想" + action + ",我只想" + 下半身->取能() + "\n\t你让我" + 下半身->取能() + ",我才想:" + 原始能->取能(); } protected: 直立能力 直立; 智慧能力 智慧; 下半身超能力* 下半身; }; void 秀(猩猩 *xx, char const* type) { std::cout << "我是一只" << type << "猩"; std::cout << "\n我的行走就是:"; xx->行走(); std::cout << "偶尔我也会思考猩生:"; xx->思考猩生(); std::cout << std::endl; } int main() { 猩猩 xx; 秀(&xx, "普通"); 直立猩 zlx; 秀(&zlx, "直立"); 智慧猩 zhx; 秀(&zhx, "智慧"); 直立智慧猩 zlzhx; 秀(&zlzhx, "直立智慧"); zlzhx.设定贤者(false); 秀(&zlzhx, "下半身超能力直立智慧"); }

在线运行(onlinegdb)

运行输出:

我是一只普通猩 我的行走就是:爬、爬、爬 偶尔我也会思考猩生:猩生就是吃香蕉 我是一只直立猩 我的行走就是:我的一小步,是人类进化的一大步 偶尔我也会思考猩生:猩生就是吃香蕉 我是一只智慧猩 我的行走就是:爬、爬、爬 偶尔我也会思考猩生:香蕉诚可贵,自由价更高。若为进化故,二者皆可抛 我是一只直立智慧猩 我的行走就是:我的一小步,是人类进化的一大步 偶尔我也会思考猩生:香蕉诚可贵,自由价更高。若为进化故,二者皆可抛 我是一只下半身超能力直立智慧猩 我的行走就是:我不想走,我只想嘿嘿嘿嘿嘿…… 你让我嘿嘿嘿嘿嘿……,我才想:我的一小步,是人类进化的一大步 偶尔我也会思考猩生:我不想思考猩生,我只想嘿嘿嘿嘿嘿…… 你让我嘿嘿嘿嘿嘿……,我才想:香蕉诚可贵,自由价更高。若为进化故,二者皆可抛