跳跃的对象
面向对象
main.cpp
#include <iostream>
#include "mclog.h"
#include "format.h"
// 最基本的动物类
class animal
{
public:
// 公开区域,可让任何作用域访问
// 默认无参数构造函数
animal() {}
// 析构函数
// 因为类中存在虚函数必须要指定析构函数为虚函数
virtual ~animal() {}
// 有参数默构造函数,主要用于给数据赋值
animal(bool male, size_t age)
: _male(male), _age(age) {}
// 纯虚函数,出现纯虚函数之后这个类将无法创建对象
// 指定自己为特定动物类型,需要具体的动物类型子类实现,现在只是占位声明
virtual std::string kind() = 0;
// 这是获取类内数据的公开函数
bool is_male()
{
return _male;
}
size_t get_age()
{
return _age;
}
std::string DNA()
{
return _DNA;
}
// 注意这里如果只有获取没有设置,表明数据一但确定无法更改
// 类内参数只能通过公开函数的接口进行设置
// 提供变性设置
void set_male(bool male)
{
_male = male;
}
protected:
// 保护区域,只有本身和子类可以访问
// 可改变的-这个继承类不一致的内容
bool _male = true;
size_t _age = 0;
private:
// 私有区域,只有本身可以访问
// 动物的底层代码
std::string _DNA = "DNA_ANIMAL_EAT_SLEEP_SEX";
};
// 继承 animal 的子类,需要指定自己的 kind 种类
// 这个类存在 action 虚函数,无法创建对象
// 这个类有自己的 _name 类型,子类可用,与继承 animal 的数据结构不一致
class rabbit : public animal
{
public:
// 继承父类的构造函数,等于自己也拥有了父类的构造函数
using animal::animal;
// 构造函数,自己的带参数构造函数,相同参数下,优先等级比父类高
rabbit(bool male, size_t age, std::string name)
: animal(male, age), _name(name) {}
// 指定析构为虚函数
virtual ~rabbit() {}
// 实现父类的纯虚函数
// 表明自己的类型是兔子
std::string kind() override
{
return "REBBIT";
}
// introduce 本身信息是不完整的,需要使用 _name 和 action()
// action() 是虚函数,需要子类实现才行
std::string introduce()
{
std::string str = "我是 {0} 接下来我要 {1}\n";
replace_str_ref(str, "{0}", _name);
replace_str_ref(str, "{1}", action());
return str;
};
// 给外部获取 name 数据的接口
std::string get_name()
{
return _name;
}
protected:
// 纯虚函数,需要子类实现,而且不能被外部直接调用
virtual std::string action() = 0;
protected:
std::string _name;
};
// 继承 animal 的子类,需要指定自己的 kind 种类
class fox : public animal
{
public:
using animal::animal;
// 指定析构为虚函数
virtual ~fox() {}
std::string kind() override
{
return "FOX";
}
// 这个 show 是虚函数,但不是纯虚函数,可以创建对象
virtual std::string show()
{
return "野生狐狸不会表演";
};
};
// rabbit 的子类,需要实现 action 虚函数
// 存在继承构造函数可在外部使用
// 本身实现构造函数,优先实现本身构造
class JudyHopps : public rabbit
{
public:
// 继承构造函数
using rabbit::rabbit;
// 自己的构造函数
JudyHopps(int age)
: rabbit(false, age, "朱迪") {}
protected:
// 实现虚函数功能
// 指定不同年龄干不一样的事情
std::string action() override
{
if (_age > 18)
{
return "打击动物城犯罪分子";
}
else if (_age > 10)
{
return "帖200张罚单";
}
return "在家种胡萝卜";
}
};
// rabbit 的子类,需要实现 action 虚函数
class BonnieHopps : public rabbit
{
public:
BonnieHopps()
: rabbit(false, 55, "朱迪妈妈") {}
protected:
std::string action() override
{
return "和朱迪的爸爸在家种胡萝卜";
}
};
// rabbit 的子类,需要实现 show 虚函数
class NickWilde : public fox
{
public:
// 只有一个无参数的构造函数
// 没有继承父类构造函数,不能通过构造函数的参数赋值
NickWilde()
{
_male = true;
_age = 25;
}
std::string show() override
{
return "我叫尼克狐,接下来我会让你知道什么叫一口一个兔兔头";
}
};
// 这个函数使用了父类指针 animal,这个父类存在虚函数,这里会发生多态调用
// 获取最顶层父类信息
std::string animal_info(animal *actor)
{
// 根据 is_male 函数的值确认性别
std::string male = "男";
if (actor->is_male() == false)
{
male = "女";
}
// 这里在查看类有关 animal 以及子类的基本信息
std::string str = "演员信息 "
"种类:{0} "
"性别:{1} "
"年龄:{2} "
"原始属性:{3} ";
replace_str_ref(str, "{0}", actor->kind());
replace_str_ref(str, "{1}", male);
replace_str_ref(str, "{2}", std::to_string(actor->get_age()));
replace_str_ref(str, "{3}", actor->DNA());
return str;
}
// 可多态调用 fox 类虚函数与成员函数
// 接收和 fox 类型相关的数据,包括 fox 类型,以及他的子类
// show 函数是虚函数,这里会调用子类的 show 函数
void showtime_fox(fox *actor)
{
std::string str = "{0}\n少废话,请开始你的表演\n{1}\n";
replace_str_ref(str, "{0}", animal_info(actor));
replace_str_ref(str, "{1}", actor->show());
std::cout << str << std::endl;
}
// 可多态调用 rabbit 类虚函数与成员函数
// action 函数是虚函数,子类实现
// 但是这里并不调用 action,而是调用了 introduce 函数来间接调用 action 虚函数
void showtime_rabbit(rabbit *actor)
{
std::string str = "{0}\n这里是兔子胡萝卜节表演大会,请 {1} 进行自我介绍\n{2}";
replace_str_ref(str, "{0}", animal_info(actor));
replace_str_ref(str, "{1}", actor->get_name());
replace_str_ref(str, "{2}", actor->introduce());
std::cout << str << std::endl;
}
// 显示动物虚函数
void show_animal_kind(animal *actor)
{
MCLOG($(actor->kind()));
}
int main(int argc, char **argv)
{
MCLOG("朱迪小时候");
JudyHopps jd5 = JudyHopps(5);
showtime_rabbit(&jd5);
MCLOG("朱迪刚入职");
JudyHopps jd15 = JudyHopps(15);
showtime_rabbit(&jd15);
MCLOG("朱迪组搭档");
JudyHopps jd20 = JudyHopps(20);
showtime_rabbit(&jd20);
MCLOG("朱迪的妈妈");
BonnieHopps jdmm = BonnieHopps();
showtime_rabbit(&jdmm);
MCLOG("尼克的表演");
NickWilde nick = NickWilde();
showtime_fox(&nick);
MCLOG("野生的狐狸");
fox nono = fox(true, 4);
showtime_fox(&nono);
MCLOG("动物虚函数");
JudyHopps judy_kind = JudyHopps();
NickWilde nick_kind = NickWilde();
show_animal_kind(&judy_kind);
show_animal_kind(&nick_kind);
return 0;
}
打印结果
朱迪小时候 [/home/red/open/github/mcpp/example/09/main.cpp:245]
演员信息 种类:REBBIT 性别:女 年龄:5 原始属性:DNA_ANIMAL_EAT_SLEEP_SEX
这里是兔子胡萝卜节表演大会,请 朱迪 进行自我介绍
我是 朱迪 接下来我要 在家种胡萝卜
朱迪刚入职 [/home/red/open/github/mcpp/example/09/main.cpp:249]
演员信息 种类:REBBIT 性别:女 年龄:15 原始属性:DNA_ANIMAL_EAT_SLEEP_SEX
这里是兔子胡萝卜节表演大会,请 朱迪 进行自我介绍
我是 朱迪 接下来我要 帖200张罚单
朱迪组搭档 [/home/red/open/github/mcpp/example/09/main.cpp:253]
演员信息 种类:REBBIT 性别:女 年龄:20 原始属性:DNA_ANIMAL_EAT_SLEEP_SEX
这里是兔子胡萝卜节表演大会,请 朱迪 进行自我介绍
我是 朱迪 接下来我要 打击动物城犯罪分子
朱迪的妈妈 [/home/red/open/github/mcpp/example/09/main.cpp:257]
演员信息 种类:REBBIT 性别:女 年龄:55 原始属性:DNA_ANIMAL_EAT_SLEEP_SEX
这里是兔子胡萝卜节表演大会,请 朱迪妈妈 进行自我介绍
我是 朱迪妈妈 接下来我要 和朱迪的爸爸在家种胡萝卜
尼克的表演 [/home/red/open/github/mcpp/example/09/main.cpp:261]
演员信息 种类:FOX 性别:男 年龄:25 原始属性:DNA_ANIMAL_EAT_SLEEP_SEX
少废话,请开始你的表演
我叫尼克狐,接下来我会让你知道什么叫一口一个兔兔头
野生的狐狸 [/home/red/open/github/mcpp/example/09/main.cpp:265]
演员信息 种类:FOX 性别:男 年龄:4 原始属性:DNA_ANIMAL_EAT_SLEEP_SEX
少废话,请开始你的表演
野生狐狸不会表演
动物虚函数 [/home/red/open/github/mcpp/example/09/main.cpp:269]
[actor->kind(): REBBIT] [/home/red/open/github/mcpp/example/09/main.cpp:240]
[actor->kind(): FOX] [/home/red/open/github/mcpp/example/09/main.cpp:240]
封装继承多态
你终于来到了这一步,面向对象是编程广泛采用的代码设计规范,这种设计可以将复杂的数据结构归类,将无序的行为进行有目的的划分
我在 main.cpp 文件中编写了一份面向对象的代码,希望你能提前预览才能更好理解,在代码中描述了封装继承多态的表现形式,以及以需要注意的问题和如何使用他们
面向对象的设计是最基本的编程设计,是开发者必须要掌握的设计方式
封装继承多态是实现面向对象的必要手段,它们可以将数据和行为都组装并隐藏起来,只暴露出被允许的行为,而且每一种行为背后都可能存在复杂的逻辑,接下来我会简单概括封装继承多态的含义
封装是将数据和处理数据的行为放入到类中,将他们表示为一类东西
继承是子类可以继承父类的所有数据和行为,然后在扩展自身让自己的子类继承,达到一种逐步扩展效果
多态是调用父类的功能时,实际执行的确是子类的功能,也就是在执行一种不是自己的功能,是父类自己定义确是子类去完成的功能,有着借花献佛的效果
讲完封装继承多态的功能你或许还是一头雾水,这是大概率的事情,如果你看过上面的 main.cpp 文件却没搞懂,接下来我会为你一步步拆解,随后再回头看上面的概括便可一目了然
构造函数种类
class animal
{
// 手动创建的任意参数构造函数
animal(int age) { }
animal(bool male) { }
animal(bool male, int age) { }
// 空类默认生成的唯一构造函数
// 构造构造-类被创建时自动调用-类的第一个调用函数
animal() { }
// 析构函数-类被销毁时自动调用-类的最后一个可调用函数
~animal() { }
// 拷贝构造-从同类上复制数据到自身
animal(const animal& other) { }
// 移动构造-从同类上直接转移数据到自身,传入对象会失效
animal(animal&& other) noexcept { }
};
在使用类之前,你要知道类的几个默认构造函数,分别有 构造函数 析构函数 拷贝构造 移动构造 他们都是创建空类时自己默认生成的,但是需要注意的是一旦自己创建构造函数之后,这个自己生成的构造函数就会消失
你可能发现,类的构造函数与普通函数不一样,他们在写法上是没有返回值的,实际上他们是自动调用的,我们不能想函数一样去调用他们,只能由他们自动触发,所以不需要返回值是更符合常识的
拷贝构造和移动构造是存在返回值的,他们都可以从同类型的类上获数据来生成自身,但是需要注意的是,一个类通常我们默认是不能通过拷贝构造来转移数据的,因为你不能确认提供这个类的开发者是否实现了拷贝构造,如果没有实现出出现深浅拷贝问题,相关拷贝构造的知识需要自行学习
深浅拷贝问题简单的说是两个对象引用同一份数据,造成多次释放的问题,所以请你在没有确认是否实现拷贝函数之前,不要通过类对象拷贝创建,可以避免很多问题,因为实际开发中很多类都是随手编写的,不会去实现深拷贝功能
谨慎使用拷贝构造创建对象是 编程规范 的重要一步
构造函数中,带参数的构造玩玩是需要给类内的数据进行赋值,可以看到 main.cpp 文件中 animal(bool male, size_t age) : _male(male), _age(age) {} 这行代码可以给 _male _age 变量直接赋值,而不需要在构造函数类进行赋值,这是初始化列表的一种使用方式,推荐这种写法
这里需要说明的是,我在创建类的成员变量时,采用 _male 首字母下划线的方式来标记这个变量是类成员,这种做法我推荐你也使用,因为通常传参数的变量是同名的,对类内变量进行区分可以减少编写错误,可以避免同名覆盖,同时也可以更快的通过下划线进行代码补全
类与封装
class animal
{
// 默认私有区域
public:
// 公开区域-任何人都可以访问
protected:
// 保护区域-子类可以访问
private:
// 私有区域-自己可以访问
};
先看类与封装的功能,封装的含义就是不让调用者可以随意看到或者使用,调用者想要使用的功能必须是我们给出去的,而不是他们想用就用的
我们要实现封装的效果需要借助 class 类这个功能,它提供了三种作用域 public protected private 他们是有访问权限的
public 是给外部访问的公开内容,是给其他调用者可以随意调用的功能,请记住公开区域是不放数据变量的,只放函数,通常称他们为 方法\成员\行为 函数
protected 是给子类访问的,这里放的数据和函数是给子类扩展功能或者是希望被继承的中间处理函数,总是不是一个完整的行为函数,不是可以直接被外部使用的函数,要知道继承该类的其他类叫子类
private 是只给自己用的,不管是数据还是函数,他们是不给其他人使用的,一旦随意使用就会破坏整个类的行为,所以需要放在私有区域
类的封装是非常重要的,他们可以限制一些数据和函数的使用范围,而不是跟结构体一样全部都是给外部使用的
封装数据是 编程规范 的重要一步
要记住类的封装是面向对象的第一步,对象就是数据,是一组有归类的数据,是可以划分出行为和结构的数据,这一点对新手来讲通常是很难的,但是你必须要学会如何从杂乱的数据中抽出他们的相似点进行封装
以 main.cpp 文件中的 animal 类举例,我们规定了每个动物必须要存在不同的性别和年龄,以及不可改变野性DNA行为,然后从这个动物类中去扩展为狐狸兔子等子类,或者是水牛考拉也罢,你都需要先抽取出他们的共同点
需要注意的是,由一个类来创建的变量,通常被称为对象,而不是称为变量,虽然他们都是内存地址但叫法不同,这一点新手可能会造成疑惑
父类与子类
// 父类
class animal
{
// 构造函数
animal() {}
};
// 子类
class fox : public animal
{
// 继承父类构造函数
using animal::animal;
};
类的继承关系会中,被继承者被称为父类,子类会继承父类,他们通常也被称为 基类\派生类
子类可以继承父类的所有结构数据,包括私有的数据,只是没有权限调用而已,子类实际上是包含了一个完整父类的结构再追加自己的结构,与结构体组合数据的逻辑是一样的,所以子类对象是可以转成父类对象的
一个类中在创建时,会自动的调用构造函数,销毁时自动调用析构函数,这是类的特点之一,所以类中通常会有多个构造函数用于调用,但是子类也可以直接继承父类的构造函数与委托构造函数,相关知识需自行学习
你会注意到 : public animal 这种继承关系,表示是公开继承的,外部可以调用父类成员函数
子类继承父类的方式与类封装一致,有 public protected private 三种,他们用于表示,调用者是否可以通过子类间接访问到父类的函数,通过这种方式等于父类放入了自己的不同类作用域中,来决定外部调用者是否可以使用父类的数据与方法
在 main.cpp 文件中,你会看到所有的类都继承到类 animal 类中,比如朱迪类 JudyHopps : rabbit : animal 通过两层继承关系继承到了 animal 类,所以在他们调用 animal_info 类时,即使是传入的 JudyHopps 子类对象,最好也能找到 animal 的DNA信息
纯虚函数
// 创建父类-虚类不可以创建对象
class animal
{
// 纯虚函数
virtual std::string kind() = 0;
// 存在虚函数,需要指定析构函数为虚函数
virtual ~animal() {}
};
// 创建子类-普通类创建对象
class fox : public animal
{
// 实现父类的纯虚函数,变成普通类
std::string kind() override
{
return "FOX";
}
};
class rabbit : public animal
{
std::string kind() override
{
return "REBBIT";
}
};
// 显示动物虚函数
void show_animal_kind(animal *actor)
{
MCLOG($(actor->kind()));
}
// 调用
int main(int argc, char **argv)
{
MCLOG("动物虚函数");
JudyHopps judy_kind = JudyHopps();
NickWilde nick_kind = NickWilde();
show_animal_kind(&judy_kind);
show_animal_kind(&nick_kind);
}
// 打印
[actor->kind(): REBBIT]
[actor->kind(): FOX]
面向对象最重要的一步就是多态,多态指的是一种类型却却有多种状态,他的状态是运行时才确定的,而不是在编译时确定的,一旦你创建了虚函数,在代码还没有执行时是不知道函数运行的具体指令的,他的指向需要动态的创建
仔细看上面的代码,在 show_animal_kind 函数中,调用了 animal 的 kind 函数,如果这个函数是普通函数,编译器可以指定他的运行逻辑指令,但这个函数如果是虚函数,他们编译器也不知道它到底会运行什么指令,代码必须运行到 show_animal_kind(&judy_kind) 这一行, kind 函数的运行指令才会被确定下来,他会找到 judy_kind 类型对 kind 函数的重新实现并执行那一段代码,但在下一行,由于传入的对象对象变成 nick_kind 那么 kind 函数的运行指令又发生了改变,这就是多态
因为虚函数的多态效果,你会看到传入不同的对象会影响到打印结果,会重定向虚函数的代码执行
多态的好处就是可以使用同一个函数,根据参数执行不一样的代码,这是有利于调用者的,因为调用者无序关系真事的类型,只需要使用一个功能就可以处理多种类型,这种方式通常被称作接口,接口可以让程序按照逻辑设计执行,而不需要被具体的类型干扰,是一种可扩展的高效设计方式
多态的发生必须存在类的父子关系,而且父类需要设计一个虚函数接口,让子类去实现并在调用的传入对应的子类对象
虚函数有 纯虚函数 和 虚函数 两种,函数被标记为 =0 则为 纯虚函数,纯虚函数作为接口使用,本身是需要不实现任何代码的,如果没有标记为 虚函数 需要实现默认行为
一旦一个类出现了 纯虚函数 那这个类就变成 虚类,虚类是无法创建对象的,子类继承并实现父类的 纯虚函数 函数之后,子类就变成普通类,如果没有实现则子类依旧是 虚类
创建虚函数需要关键字 virtual 而实现虚函数的子类需要 override 关键字,override 不是必须的,但是请加个这个关键字来表明是父类虚函数的重新实现
使用关键字约束是 编程规范 的重要一步
你需要注意,一旦你的类中存在虚函数,你需要把类的析构函数也声明为虚函数,声明虚类的析构函数之后,父类指针释放时调用析构会先跳转到子类析构,否则只调用父类分本,导致子类无法正确释放造成内存泄露,内存泄露是C++最危险的地方之一,相关细节你需要自行学习
声明虚类析构是 编程规范 的重要一步
在 main.cpp 文件中,animal rabbit fox 三个类都存在了虚函数,但是 fox 没有 纯虚函数 所以可以创建出 fox 对象的 野生狐狸 类型
你会发现 rabbit 的 action 纯虚函数是被保护起来的,这是因为这个函数是不完整的,它给外部提供 introduce 函数来间接调用 action 虚函数,当然子类需要实现 action ,然后由 showtime_rabbit 函数的 rabbit 指针进行调用就可以分别执行 JudyHopps 和 BonnieHopps 的不同子类行为
虚函数的隐藏表
虚函数之所以能实现多态,是因为父类一旦创建了虚函数后,在类的数据结构中会存在一个虚指针指向一份虚表,虚表中存在虚函数的函数调用地址,当子类继承了父类,也会继承虚表
当子类重新实现父类 纯虚函数 之后,虚表上原本指向父类虚函数指针的被替换成子类重新实现的函数指针,当子类被父类虚函数指针调用,实际上就是调用指向子类重新实现的函数
虚函数有关虚指针和虚表的相关内容请自行学习
对象与指针
在多态的使用上,使用者是使用父类的指针来调用虚函数接口,通过传入子类的对象决定父类虚函数的实际调用,这意味着调用的函数参数往往是指针类型,调用时需要传入子类的对象地址,正因为类对象可以改变具体的功能,所以面向对象的设计通常希望用类的行为改变函数的运行规则
面向对象的设计理念
// 面向对象
class calc
{
virtual int action(int a, int b) = 0;
};
class calc_add : public calc
{
int action(int a, int b) override
{
return a + b;
}
};
class calc_sub : public calc
{
int action(int a, int b) override
{
return a - b;
}
};
int fun_calc(calc *c,int a, int b)
{
return c->action(a,b);
}
// 调用
fun_calc(calc_add(),1,2);
fun_calc(calc_sub(),1,2);
// 面向过程
int fun_sub(int a, int b)
{
return a - b;
}
int fun_add(int a, int b, int c)
{
return a + b + c;
}
// 调用
fun_add(1,2);
fun_sub(1,2,3);
上面着一段伪代码简单的表现了常规的面向过程和面向对象的设计方式,如果你是新手或许你会决定面向对象的设计简直是莫名其妙且难以理解,但不管如何,面向对象都是你需要学习的设计方式和设计理念
面向过程时,代码是散乱的,虽然简单却没有关联,所有的运算都是独立的,参数也是未知的
面向对象的代码是有关联的,他们都是继承 calc 的子类,而且都是通过 fun_calc 函数进行调用,这就是他们的统一接口,他们需要新的计算方法时可以通过创建一个新的子类来扩展功能,这也是面向对象的优势所在,他们可以通过统一的接口来限制复杂的参数,这是对调用者有利的
调用者有利是一件非常重要的事情,因为调用功能代码的人是不确定的,让他们可以轻松调用的代码才是好代码,当然调用者有利也可以通过各种口头约束完成
编写调用者有利代码是 编程规范 的重要一步
当然上面这段在加减计算上使用面向对象的多态设计只是为了体现代码直接的关联性,这不是一个好的例子,因为代码需要保持简单高效,很明显上面的代码例子并不简单
多态设计的作用更擅长建立多个不同的复杂功能之间的连接与统一接口,而不是一些简单功能的接口
在单一功能上,多态设计有利于对单一功能的扩展,这种扩展是被接口统一约束的,这也是多态设计的常见用法
实际上,很多时候并不需要做接口在代码进行约束,只需要遵循部分口头约束的规则,除非你需要多人合作,且无法有效进行交流与建立口头规则才需要在接口上进行约束,从而在代码层面上约束与制定规则
设计理想
高度统一的
面向对象中,可以将代码实现往逻辑设计上靠拢,他们希望将各种复杂的功能都符合逻辑,符合设计,他们习惯将各种功能都抽出成为接口,接口是被规定的,而且不可更改的,一个好的接口设计可以让一切都合乎情理,接口可以统一代码,可以忽视他们在具体功能上的差异,将他们统一到一种类似流水线的逻辑当中,让他们在这一条线上总是保持着一致的运行规律
理想的树枝结构
面向对象的设计理想是希望一切都可以被分类,你需要将一堆数据分类从上到下的金字塔结构,他们的关系又像是树枝一样互不干扰,从一个源头开始发散开来,有层级且互不干扰,这是最理想的一种状态,也是面向对象中,总是希望将大象装入冰箱的原因,以为这一切都是可以分化的,可以井井有条的
抽象的设计
面向对象是反常识的,你可能会问我为什么父类是 animal 动物类而不是其他的类型,你怎么知道我会在未来创造出狐尼克和兔朱迪呢
其实面向对象的设计方式是想假设我有狐尼克和兔朱迪,然后给他们分类出狐狸和兔子,在提炼出动物类,方便以后扩展出水牛警官或者一条蛇家族
所以是不是与很奇怪,面向对象在设计思路是从子类到父类的,代码编写上确是从父类到子类的,所以像 animal 动物类这种高度抽离实际的称为抽象类,得到这个抽象类的过程叫抽象设计
抽象是面向对象的一种设计,他们将一种行为预设成未来可能发生的,抽象是总结可能发生的事情作为父类,然后让发生这个事情的类继承这个类称为子类
当然这种过程并不总是顺利的,设计我定义一种 动物类 存在头部、躯干和四肢,然后衍生了猴子、熊猫等上百个类型,但是有一天你需要引入一条蛇,但是蛇没有四肢,你无法从 动物类 继承,你需要开新建一个 无躯体动物类 然后把原本的 动物类 改为 有躯体动物类 ,猴子继承 有躯体动物类 蛇继承 无躯体动物类 ,然后 动物类 就被删除了,但是删除 动物类 需要改动上百个类型
这就是抽象错误的代价,一套抽象逻辑的代码,在长时间存在之后都会面临类似的问题,你或许可以不引入 无躯体动物类 不做任何修改,而是让蛇直接继承 动物类,然后就会出现带脚的蛇这种奇葩的现象,这是一切不合理代码的源头,屎山代码的开始
面对这种情况,最好的办法就是要定期重新设计的你的代码,这称为重构,这是不可避免的问题,因为没有人能知道未来会怎么样,也不可能提前设计出最完美的结构
项目路径
https://github.com/HellowAmy/mcpp.git