14 类与继承
- 在前面我们提到过继承的一些概念,现在我们来回顾一下
打个比方:在CS2中我们把玩家定义为一个类
- class 玩家:
- 血量:100
- 阵营(未分配)
- 服饰(未分配)
- 位置(未分配)
- 武器(未分配)
- 是否允许携带C4(未分配)
- 是否拥有C4(未分配)
- 当对局创建时,会新生成两个类,这两个类继承自"class 玩家"
- 反恐精英
- 血量:100(不变)
- 阵营:反恐精英
- 服饰:反恐精英
- 位置(未分配)
- 武器(未分配)
- 是否允许携带C4:否
- 是否拥有C4:否
- 恐怖分子
- 血量:100(不变)
- 阵营:恐怖分子
- 服饰:恐怖分子
- 位置(未分配)
- 武器(未分配)
- 是否允许携带C4:是
- 是否拥有C4:是
- 当用户oldkingnana加入对局时,系统会生成一个类随机继承自反恐精英或者是恐怖分子(我们假设 类:oldkingnana 继承自了反恐精英)
- oldkingnana
- 血量:100(不变)
- 阵营:反恐精英
- 服饰:反恐精英
- 位置:反恐精英出生点
- 武器:野牛(假设用户买了把野牛)
- 是否允许携带C4:否
- 是否拥有C4:否
-
在以上的例子中我们称反恐精英为玩家的子类,玩家为反恐精英的父类,反恐精英继承自玩家这个父类
-
使用继承这个操作好处多多
- 因为存在继承这个操作,因此我们不需要重新定义反恐精英或者恐怖分子这样的类,两者相同的部分(血量)全都包括在其 父类:玩家 中,能提高开发效率
- 当然不仅仅是属性的部分,部分方法也可以修改自父类,如 恐怖分子 只有安装C4这个方法,而在 反恐精英 中就被替换成了拆除C4这种方法
14.1 类继承的语法粗看
class player
{
public:void move(){cout << "move()" << endl;}protected:int health;int weapon;
};//此处是类的继承
//class [子类名] : [继承优先级] [父类名]
class team1 : public player
{
public:void using_C4(){cout << "using_C4()" << endl;}private:bool C4;
};
- 子类包含父类的所有成员,只是优先级方面需要着重考虑,及重点研究[继承优先级]
以public优先级继承 | 以protected优先级继承 | 以private优先级继承 | |
---|---|---|---|
父类public标签的成员 | 在子类中为public | 在子类中为protected | 在子类中为private |
父类protected标签的成员 | 在子类中为protected | 在子类中为protected | 在子类中为private |
父类private标签的成员 | 在子类中不可访问 | 在子类中不可访问 | 在子类中不可访问 |
- 一般情况下常用的也就标记的三个,一般使用
public
继承就行了,很少会用其他方式继承,况且其他的写多了可读性也会变差,增加debug难度
14.2 简单了解继承在代码中的具体实现方式
14.2.1 普通类作为父类
- 回到上面CS的例子,我们可以简单写一个类出来
class player
{
public:void move(){cout << "move()" << endl;}protected:int health;int weapon;
};//以下就是类"team1",继承自"player"
class team1 : public player
{
public:void using_C4(){cout << "using_C4()" << endl;}private:bool C4;
};int main()
{team1 t1;return 0;
}
- (编译通过)
14.2.2 模板类作为父类
template<class T>
class A
{
public:A():_a(1){cout << "A()" << endl;}void func1(T& val){cout << val << endl;}private:int _a;
};template<class T>
class B : public A<T>
{
public:B():_b(2){}void func2(){//子类使用父类的成员函数的时候需要声明类域,具体原因下面会提到A<T>::func1(_b);//A<T>::func1(1.1f);}private:int _b;
};
- 关于声明类域的原因:
- 模板在实例化的时候只会按需实例化,比方说上面我想实例化类
B
,使用B b;
来实例化,但却只是实例化了其构造函数,func2()
其实并没有被实例化,而实例化B
必定会实例化A
,但实例化A
也只是实例化了构造函数,此时我想在B
的成员函数中调用函数func1()
,但编译器不知道func1()
是A
的成员函数(因为func1()
压根就没有实例化啊),所以找不到 - 如果未来开发中遇到了"继承链"的情况,而每个"祖先节点"似乎都有同名的成员函数,此时调用的时候就不知道应该用哪个成员函数,所以子类调用"祖先类"的成员函数的时候,一定要指定类域
- 模板在实例化的时候只会按需实例化,比方说上面我想实例化类
14.2.3 模板类的继承的应用
- 例如我们可以很简单地就实现一个栈(没写完)
template<class T>
class myStack : public vector<T>
{
public:void push(const T& val){vector<T>::push_back(val);}void pop(){vector<T>::pop_back();}void top(){return vector<T>::back();}
};
14.3 父类和子类对象的赋值兼容转换
- 简单来讲就是子类对象可以被允许赋值给父类对象,包括父类对象类型的引用和指针
class person
{
public:person(): _num(111), _name("zhangsan"), _sex("male"), _age(18){}protected:int _num;string _sex;int _age;public:string _name;
};class student : public person
{
public:student(): _school_id("12345"){}
protected:string _school_id;
};int main()
{student st;//这里复制过去的是属于父类对象的一部分,属于子类对象的就没有赋值过去person p1 = st;//指向子类对象中属于父类对象的那一部分person* p2 = &st;//引用子类对象中属于父类对象的那一部分person& p3 = st;//p3引用的是st的一部分,所以修改p3也会导致st被修改p3._name = "lisi";cout << st._name << endl;//输出"lisi"return 0;
}
- Ps:父类不可以赋值给子类,这会导致子类有成员变量不确定,语法上是不允许的,不过有方法让父类指针/引用赋值给子类指针/引用
14.4 作用域的隐藏规则
- 父类和子类的作用域都是独立的
- 当父类和子类中同时存在同名的成员变量的时候,父类的成员变量将会被隐藏,如果需要调用的话就得指定类域
- 如果父类和子类中存在相同名称的成员函数的时候,父类的成员函数会被隐藏,并不会构成重载
- 最好不要触发作用于的隐藏规则
class person
{
public:person(): _num(111), _name("zhangsan"), _sex("male"), _age(18){}void func(){cout << "person::func()" << endl;}protected:int _num;string _sex;int _age;public:string _name;
};class student : public person
{
public:student(): _school_id("12345"), _num(666), _name("wangwu"){}void func(int x){cout << "student::func()" << endl;}protected:string _school_id;int _num;
public:string _name;
};int main()
{student st;cout << st._name << endl;cout << st.person::_name << endl;st.func(1);st.person::func();//对于隐藏的成员函数的错误使用方式://(因为person的func被隐藏了,所以需要指定类域来调用)//st.func();return 0;
}
- 注意,这里这两个
func()
构成隐藏而不是重载!!!
14.5 继承与其他默认成员函数
- Ps:阅读本小节时,我么们可以将父类当作一个整体看待
14.5.1 关于默认构造函数
class person
{
public:person(int num = 777, string name = "zhangsan", string sex = "male", int age = 18): _num(num), _name(name), _sex(sex), _age(age){}void func(){cout << "person::func()" << endl;}person(const person& p): _num(p._num), _name(p._name), _sex(p._sex), _age(p._age){}protected:int _num;string _sex;int _age;public:string _name;
};class student : public person
{
public:student(string school_id = "12345", int num = 666, string name = "wangwu", int age = 18, string sex = "male"): _school_id(school_id), _num(1), _name(name), person(num, name, sex, age)//子类的默认构造中,需要单独对person进行构造,如果不这么做,person就会调用自己的默认构造函数//如果person没有默认构造函数,就会报错{cout << person::_num << endl;}void func(int x){cout << "student::func()" << endl;}student(const student& st):_school_id(st._school_id), _num(st._num), _name(st._name), person(st){}protected:string _school_id;int _num;
public:string _name;
};int main()
{student st("778899", 444, "oldking");student st1(st);return 0;
}//输出: 444
- 以下是
student
子类的默认构造不单独对父类进行构造的情况
class person
{
public:person(int num = 777, string name = "zhangsan", string sex = "male", int age = 18): _num(num), _name(name), _sex(sex), _age(age){}void func(){cout << "person::func()" << endl;}protected:int _num;string _sex;int _age;public:string _name;
};class student : public person
{
public:student(string school_id = "12345", string name = "wangwu", int age = 18, string sex = "male"): _school_id(school_id), _num(1), _name(name){cout << person::_num << endl;}void func(int x){cout << "student::func()" << endl;}protected:string _school_id;int _num;
public:string _name;
};int main()
{student st("778899", 444, "oldking");return 0;
}//输出: 777
14.5.2 关于拷贝构造
class person
{
public:person(int num = 777, string name = "zhangsan", string sex = "male", int age = 18): _num(num), _name(name), _sex(sex), _age(age){}void func(){cout << "person::func()" << endl;}person(const person& p): _num(p._num), _name(p._name), _sex(p._sex), _age(p._age){}protected:int _num;string _sex;int _age;public:string _name;
};class student : public person
{
public:student(string school_id = "12345", int num = 666, string name = "wangwu", int age = 18, string sex = "male"): _school_id(school_id), _num(1), _name(name), person(num, name, sex, age){}void func(int x){cout << "student::func()" << endl;}student(const student& st):_school_id(st._school_id), _num(st._num), _name(st._name), person(st)//拷贝构造同样需要对person单独进行拷贝/构造,如果是进行拷贝的话,可以像上面这样使用"切片",编译器自动把st中person的部分拷贝过来{cout << person::_num << endl;}protected:string _school_id;int _num;
public:string _name;
};int main()
{student st("778899", 444, "oldking");student st1(st);return 0;
}//输出: 444
- 如果不单独对
person
进行拷贝的话,就会调用person
的默认构造函数
class person
{
public:person(int num = 777, string name = "zhangsan", string sex = "male", int age = 18): _num(num), _name(name), _sex(sex), _age(age){}void func(){cout << "person::func()" << endl;}person(const person& p): _num(p._num), _name(p._name), _sex(p._sex), _age(p._age){}protected:int _num;string _sex;int _age;public:string _name;
};class student : public person
{
public:student(string school_id = "12345", int num = 666, string name = "wangwu", int age = 18, string sex = "male"): _school_id(school_id), _num(1), _name(name), person(num, name, sex, age){}void func(int x){cout << "student::func()" << endl;}student(const student& st):_school_id(st._school_id), _num(st._num), _name(st._name){cout << person::_num << endl;}protected:string _school_id;int _num;
public:string _name;
};int main()
{student st("778899", 444, "oldking");student st1(st);return 0;
}//输出: 777
14.5.3 关于赋值重载
class person
{
public:person(int num = 777, string name = "zhangsan", string sex = "male", int age = 18): _num(num), _name(name), _sex(sex), _age(age){}void func(){cout << "person::func()" << endl;}person(const person& p): _num(p._num), _name(p._name), _sex(p._sex), _age(p._age){}person& operator=(const person& p){if (this != &p){_num = p._num;_sex = p._sex;_age = p._age;_name = p._name;}}protected:int _num;string _sex;int _age;public:string _name;
};class student : public person
{
public:student(string school_id = "12345", int num = 666, string name = "wangwu", int age = 18, string sex = "male"): _school_id(school_id), _num(1), _name(name), person(num, name, sex, age){//cout << person::_num << endl;}void func(int x){cout << "student::func()" << endl;}student(const student& st):_school_id(st._school_id), _num(st._num), _name(st._name), person(st){cout << person::_num << endl;}student& operator=(const student& st){if (this != &st){//这里一定要指定类域,否则会因为构成隐藏而造成递归person::operator=(st);_school_id = st._school_id;_num = st._num;_name = st._name;}}protected:string _school_id;int _num;
public:string _name;
};
14.5.4 关于析构函数
class person
{
public:person(int num = 777, string name = "zhangsan", string sex = "male", int age = 18): _num(num), _name(name), _sex(sex), _age(age){}void func(){cout << "person::func()" << endl;}person(const person& p): _num(p._num), _name(p._name), _sex(p._sex), _age(p._age){}person& operator=(const person& p){if (this != &p){_num = p._num;_sex = p._sex;_age = p._age;_name = p._name;}}~person(){}protected:int _num;string _sex;int _age;public:string _name;
};class student : public person
{
public:student(string school_id = "12345", int num = 666, string name = "wangwu", int age = 18, string sex = "male"): _school_id(school_id), _num(1), _name(name), person(num, name, sex, age){//cout << person::_num << endl;}void func(int x){cout << "student::func()" << endl;}student(const student& st):_school_id(st._school_id), _num(st._num), _name(st._name), person(st){cout << person::_num << endl;}student& operator=(const student& st){if (this != &st){person::operator=(st);_school_id = st._school_id;_num = st._num;_name = st._name;}}//子类的析构会自动调用父类的析构,我们不需要管父类//这是因为父类先被创建,子类后被创建,为了保证先析构子类再析构父类做出的规定//(并且,假设我们在子类的析构显式调用父类的析构,还是需要指定类域,因为编译器默认将析构的名字统一规定成了destructor,两个析构构成隐藏)~student(){}protected:string _school_id;int _num;
public:string _name;
};int main()
{student st("778899", 444, "oldking");student st1(st);return 0;
}
14.6 不能被继承的类
- 如果一个类的默认构造函数被标记为
private
,因为子类实例化的时候需要调用父类的默认构造函数,但因为private
的原因其默认构造函数在子类不可见,所以在实例化的时候会导致报错
class Person
{
protected:string _name;int _age;private:Person():_name("oldking"), _age(18){}
};
- CPP11之后,可能是觉得这样的方式不够明显,且子类定义了不会报错,还要到实例化的时候才会报错,就很麻烦,所以新增了关键字
final
class Person final
{
public:Person():_name("oldking"), _age(18){}protected:string _name;int _age;
};
14.7 继承与友元
- 友元关系不会被继承,只会保留在父类,被标记的友元函数有访问子类中父类的那部分中的私有/保护成员的权限
//声明类型
class Student;class Person
{
public:friend void func(const Person& p, const Student& st);Person():_name("oldking"), _age(18){}protected:string _name;int _age;
};class Student : public Person
{
public:Student():_id_number("8877665544"){}protected:string _id_number;
};void func(const Person& p, const Student& st)
{cout << p._age << endl;cout << st._age << endl;//下面这行使用会报错//cout << st._id_number << endl;
}int main()
{Person p;Student st;func(st, st);return 0;
}//输出:18
// 18
14.8 继承与静态成员
- 在继承中,静态成员实际只存在一份,由子类和父类共享
class Person
{
public:Person():_name("oldking"), _age(18){}static string _sex;protected:string _name;int _age;
};string Person::_sex = "woman";class Student : public Person
{
protected:string _id_number;
};int main()
{Person p;Student st;cout << Person::_sex << endl;cout << Student::_sex << endl;cout << p._sex << endl;cout << st._sex << endl;return 0;
}
14.9 继承模型
14.9.1 单继承
-
简单来说单继承就是只有一个"继承链"的继承方式,类似于链表,每个类的父节点都是唯一的
-
类似于 person->student->monitor(班长)
14.9.2 多继承
- 多继承就相对来讲复杂一些,多继承的出现是为了更好地模拟/描述现实世界,因为现实世界往往是复杂的,各种对象间都存在继承关系,类似于电子产品中的笔记本电脑和手机,它们都属于移动计算机的一种,他们都是移动计算机的子类,但有一个子类同时继承了笔记本电脑和手机,即平板电脑,同时具备有手机和电脑的特性
class Computer
{
protected:string _cpu;string _screen;//...
};class Phone : public Computer
{
protected:string _touch_panel; //触控屏//...
};class Laptop : public Computer
{
protected:string _touch_pad; //触控板//...
};class Pad : public Phone, public Laptop
{
};
-
CPP支持多继承,但事实上多继承其实是CPP的一个大坑,像是Java就直接禁用了多继承
-
这个多继承坑就坑在,他会产生菱形继承,咱来先看看什么是菱形继承
-
比方说,刚刚我们举的平板电脑的例子就是经典的菱形继承
-
我们来分析一下菱形继承会导致什么问题
-
计算机:
- 计算机的特性
-
笔记本电脑:
- 计算机的特性
- 笔记本额外有的特性
-
手机:
- 计算机的特性
- 手机额外有的特性
-
平板电脑:
- 计算机的特性
- 计算机的特性
- 手机额外有的特性
- 笔记本额外有的特性
-
发现了吗,平板电脑因为既继承了笔记本电脑,又继承了手机,所以其中包含了两个计算机的特性,这就产生了数据冗余和二义性的问题
-
数据冗余:多了一份重复的数据,空间上会有浪费
-
二义性,如果我们想访问的数据是重复的那部分数据,编译器就不知道应该访问哪一份的
- 以下是正确使用方式
class Pad : public Phone, public Laptop
{
public:void func(){cout << Phone::_cpu << endl;}
};
- 总的来说,多继承最好别用,非常容易搞混,菱形继承千万别用,弊大于利,极不推荐用,能不用就不用
14.9.3 虚继承
-
回到上面的例子,如果我实在想用菱形继承怎么办,我们可以加上虚继承
-
虚继承可以理解为是一个标记,虚继承会标记当前类中的父类,如果当前类的子类是多继承就会影响到子类,将当前类的子类中重复的当前类的父类部分合二为一,所以说虚继承关系到3个具有父子关系的类
class Computer
{
protected:string _cpu;string _screen;//...
};class Phone : virtual public Computer
{
protected:string _touch_panel; //触控屏//...
};class Laptop : virtual public Computer
{
protected:string _touch_pad; //触控板//...
};class Pad : public Phone, public Laptop
{
public:void func(){cout << &_cpu << endl;cout << &(Phone::_cpu) << endl;cout << &(Laptop::_cpu) << endl;}
};int main()
{Pad p;p.func();return 0;
}
-
可以看到实际在类平板电脑中只创建了一个计算机类
-
实际开发中的思路(可能?)是这样的
- 定义了类计算机,然后从计算机继承下来了手机和笔记本电脑
- 考虑到手机和平板电脑可能会作为多继承的父类
- 思考到如果作为多继承的父类的话可能会造成类计算机重复存在,会产生数据冗余和二义性
- 于是将手机和平板电脑的继承方式改为虚继承
-
当然,报错了再加
virtual
也行 -
假设有以下这种情况
- 此时,重复的数据应该是计算机的这部分数据,所以计算机向笔记本电脑和手机的继承方式应该是虚继承,标记"笔记本电脑"中的"计算机"和"手机"中的"计算机",实际对Windows平板电脑没有影响,影响的是Win掌机
14.9.4 多继承下的子/父类地址关系
- 我们简单看一看以下代码
class A
{
protected:int _a;
};class B
{
protected:int _b;
};class C : public A, public B
{
protected:int _c;
};int main()
{C c;A* pa = &c;B* pb = &c;C* pc = &c;cout << pa << endl;cout << pb << endl;cout << pc << endl;return 0;
}
-
pc
指向的一定是c
的起始地址这毋庸置疑,但pa
和pb
指向哪里这是有讲究的 -
在
c
中,我们知道父类相当于作为一个独立的对象去存放,构造c
真正前,首先要对父类进行构造,于是我们能看到pa
指向的位置和pc
相同,其实是因为A
优先被构造,后面构造的是B
,于是我们能看到B
紧接着就在A
后面
- 所以说,在多继承中,父类会优先被构造且放在当前类的最前面
14.10 继承与组合
14.10.1 黑箱与白箱
- 黑箱:简单来说,黑箱就是封装好各种设计细节,只保留关键接口给使用者,使用者无需关心底层的设计细节,拿来就用,比方说我们设计一个
my_Stack
,调用了stl提供的list
来实现,我们压根就不需要关心这个链表具体是什么类型,只管用就完事了,我们只需要知道,用它提供的接口可以完成my_Stack
的封装,封装完之后依旧仅外露一部分接口,保持黑箱 - 白箱:白箱就是各种底层细节全部展现给使用者,使用者根据自己的使用情况,想怎么调用就可以怎么调用,甚至能对底层细节进行深度修改
14.10.2 黑箱与白箱的优劣
-
黑箱:
- 因为不会暴露底层细节,使用者也不会修改和使用底层细节,所以如果想要修改黑箱其中的细节,使用者无需更改代码,实现的效果依旧不变
-
白箱:
- 白箱因为暴露了底层细节,使用者也可以随意调用,就会导致如果对白箱的底层细节做出了修改,因为使用者使用了底层的内容,导致使用者也需要对代码进行修改,是一件非常得不偿失的事
14.10.3 has-a & is-a
-
两个类的关系可以用
has-a
和is-a
来表达 -
has-a
:即包含关系,我们可以说一台宝马汽车有4个轮子,这四个轮子就可以作为自定义类放进宝马汽车中 -
is-a
:即继承关系,我们可以说宝马汽车是一台车,继承自一台车,但不能说宝马里有个车对吧
14.10.4 继承&组合与黑箱&白箱
-
不难看出,继承应该对应着白箱,因为父类一般不会设计
private
标签,所有的底层全都是开放的,导致子类可以很轻松地使用到父类底层的所有东西 -
而组合就是将一到多个自定义类封装到当前这个大类中,你看不到这几个类的实现细节,只管用就完事了,也就对应着黑箱
-
所以说,一般来说,如果两个类的关系可以用
has-a
来表达,就尽量用has-a
来表达,尽可能用组合描述事物,除非这个事物用继承来描述更加合适