欢迎来到繁星的优快云,本期内容主要包括,继承与多态。
一、继承
什么是继承呢?顾名思义,如果A具有B的所有特征,那么我们可以将B的代码复用,并让A继承B,其中,A是子类(派生类),B是父类(基类)。
举个例子,在一个学校里有学生和老师,他们都需要一张身份证明。身份证明上需要姓名,而学生的身份证明上需要学号,老师的身份证明上需要教工号,两者并不相同,但我们可以再从中拿出一个person类,令其记录姓名,再将学生和老师继承person类的姓名,分别使用学号以及教工号,这样我们就不需要写两边姓名了。
实际操作中的代码复用节省的更多,但这里我仍旧以这个看似冗余的例子来继续说明。
1.1继承的用法
class Person {
public:
string name;
};
class Student:public Person {
public:
int number;
};
class Teacher:public Person {
public:
int id;
};
这里让student类和teacher类都继承了person类的name,然后再分别加上了单独的number与id。
1.2继承方式与访问限定符
在之前的学习中,我们认识到访问限定符分为public,private,protected这三种,在当时的说法中我提到private和protected的区别不大,但在继承这里两者之间便出现了区别。
在上述的例子中,student && teacher的继承方式均为public,事实上我们还可以用protected和private来继承。
继承方式与访问限定符的关系如下图所示。
什么是在派生类不可见?其含义是基类中的该private成员仍然会被派生类继承,但是在派生类中不能被其成员函数直接调用,尽管我们在监视窗口里是看的到的。
从上述的两张图片我们不难得知,protected的出现就是为了这种情况,protected既使得类外不能随意地调用类里的成员,还使得派生类可以比较方便地使用基类中继承下来的成员。这并不是一竿子打死,在实际运用中如果需要使用private还是使用private。
class默认的继承方式是private,而struct默认的继承方式是public。尽管如此,笔者推荐各位在写继承方式的时候还是显式地写出public、protected、private。
在实际运用中大部分情况使用public继承,protected和private继承下来的成员仅仅能在派生类的类内使用,扩展性不强。
1.3派生类与基类的类型转换
派生类对象可以赋值给基类的对象/基类的指针/基类的引用。在操作过程中,编译器将不属于基类的那部分成员切割,并将派生类中属于基类的部分留下,并进行赋值,这一形象的操作也称为“切片”或者“切割”。而这种操作可以直接书写,不需要强制类型转换。
但值得注意的是,基类对象不可以赋值给派生类的对象/派生类的指针/派生类的引用。原因很简单,编译器不可以凭空捏造出成员给派生类。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
1.4继承体系中的作用域
继承体系中也存在作用域。基类和派生类实际上仍然是两个类。
我们可以在基类和派生类里使用同名函数,而在派生类中的函数会隐藏基类中的函数,或者称为“重定义”。只要函数名相同,两者就构成隐藏/重定义,在使用派生类对象的函数的时候,如果发生了隐藏,调用时也会调用隐藏后的函数,而非基类的同名函数。
尽管我们可以这么做,但请在非必要的继承体系中,不要定义同名函数。
1.5继承体系中的一些成员说明
我们知道在类中有默认成员函数,而这些函数也会在继承体系中体现其继承。
首先是构造函数,构造派生类对象的时候,程序会先调用基类的构造函数来构造属于基类部分的成员,然后再调用派生类的构造函数来构造不属于基类部分的成员。当然
然后是析构函数,析构派生类对象的时候,程序会先析构派生类部分,再析构基类部分(仍然符合先构造的后析构)。
为了防止其析构两遍,析构函数会被编译器处理为destructor(),这使得未加virtual的情况下派生类的析构函数会隐藏基类的析构函数,从而防止析构两遍基类的部分,同时值得注意的是,我们在书写派生类的析构函数时,编译器禁止我们直接显式地调用基类的析构函数,这也是防止二次析构的措施。
友元关系是不可以被继承的,这一点等同于友元不具有传递性。
基类的静态成员无论被派生多少次,都只有一个实例。
1.6菱形继承
1.6.1菱形继承的出现
class Person {
private:
string name;
};
class Student:public Person {
public:
int number;
};
class Teacher :public Person {
public:
int id;
};
class Assistant :public Student, public Teacher {
public:
int tel;
};
同样还是学生与老师的例子。这里我们引入新的角色——助教,他们既是老师又是学生,同时具备了两种特质,所以继承了两者的内容。
在之前的教学中,笔者省略了多继承的内容,便是为了说明菱形继承的问题。
多继承指的是一个派生类可能有多个基类,这就好比一个人可以有父亲也可以有义父。在多个继承之间用逗号隔开,便可以实现多继承。
多继承在实践中是有必要的,但是其背后的菱形继承却会使这件事变的复杂。
从例子中我们不难发现,assistant这个类继承了student和teacher。别忘了,student和teacher中还各有一份从person中继承得到的name。如果这个助教有曾用名,而student的卡上是曾用名,teacher的卡上是现在的名字,那么应该在程序里用哪个名字呢?
将这份关系图画出来,由于状似菱形,所以我们称为菱形继承。
有人说直接用后者(Teacher)的不就可以了吗?但如果这是在程序中,程序如何得知哪一份信息是需要的呢,我把student和teacher位置换一换,覆盖的信息不就是正确的信息了吗?
所以我们在实践中应避免菱形继承。
1.6.2菱形继承的解决方案
第一种解决方案非常简单,会发生这种问题,那我就尽量避免出现。
重点在第二种方法,如果我认为我是高手,一定要将代码复用表现到极致,那么我该如何解决菱形继承的问题呢?
class Person {
private:
string name;
};
class Student:virtual public Person {
public:
int number;
};
class Teacher :virtual public Person {
public:
int id;
};
class Assistant :public Student, public Teacher {
public:
int tel;
};
我们把这种方案称为虚继承。既在继承的位置加上virtual。
务必注意,我们仅需在会发生菱形继承的位置加virtual。其他继承时请不要加virtual。
那么这种情况下我们应该在哪里加上virtual呢?
答案是B和C,因为B和C是发生的源头。
虚继承可以使得最后继承的类E只存一份来自A的信息,避免了数据冗余和二义性。
1.7继承的额外说明
在使用继承(包括后续说明的多态)时,基类也可以是另一个类的派生类,派生类也可以是别的类的基类,基类与派生类的关系是相当的,而非绝对,这一点务必注意。
二、多态
什么是多态?顾名思义,多态就是输入同样的指令,但是由于输入者不同,该指令得到的结果不同。
class Person {
public:
virtual void Buyticket() {
cout << "Person::全价票"<<endl;
}
string name;
};
class Student:public Person {
public:
virtual void Buyticket() {
cout << "Student::半价票"<<endl;
}
int number=0;
};
class Soldier :public Person {
public:
virtual void Buyticket() {
cout << "Solider::免费" << endl;
}
int id = 1;
};
int main() {
Person* a = new Person;
Person* b = new Student;
Person* c = new Soldier;
a->Buyticket();
b->Buyticket();
c->Buyticket();
return 0;
}
程序的结果如下:
大家可能会疑惑,为什么同样是Person的指针,却能得到不同的结果。按之前继承的逻辑而言,不应该因为类型转换,三个指针调用函数得到的结果完全相同吗?且看我娓娓道来。
2.1多态的实现原理
多态的实现并不复杂,但必须建立在继承体系内。
还记得刚刚在继承的部分提到的“隐藏/重定义”吗?多态其中的一个实现条件比隐藏略复杂一些。多态需要的是重写,即返回值类型、函数名、参数列表完全相同,但值得注意的是,参数列表只需要参数类型和对应个数相同即可,参数的名称可以不同。
讲完重写,便可以和各位阐述多态的实现条件了,实现条件有二:
1、必须通过基类的指针或者引用调用函数。
2、被调用的函数必须是虚函数,且派生类必须对虚函数进行重写。
在例子中,笔者使用了基类的指针调用函数Buyticket,该用基类的引用同样可以得到一样的效果。
而虚函数和之前虚继承的方法相同,即在函数的返回类型前加上virtual。我们在使用中,最好在基类和派生类的对应函数前均加上virtual,尽管派生类的对应函数可以不加virtual达到同样的效果,但这样的写法是不规范的,难以察觉其是否是多态所对应的函数。
2.2多态的检查
对于不熟悉是否构成多态的读者而言,在C++11中,个人建议使用override进行检查。示例如下:
仍然是刚刚的示例,student的函数是符合重写要求的,所以在使用override时并不报错。但Soldier的函数名与基类对应的函数名不同,不符合重写要求,所以在添加override后直接报错了。
因此,在使用多态时,不妨在需要使用多态的对应派生类函数时加一个override,无需运行直接报错,方便且节约时间。
另外,C++中还可以在虚函数后加上final,使得该虚函数不能再被重写
2.3虚函数重写的两个例外
虚函数重写需要满足的条件是返回值类型、函数名、参数列表均相同,但是有两个例外。
1、协变(基类与派生类的返回值类型不同)
基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。关于这一点笔者认为协变仅需要了解即可,实际用处不大。
2、析构函数(基类与派生类函数名不同)
在继承的部分笔者提到析构函数会被处理为destructor(),在重写部分也不例外。如果基类的析构函数加上了virtual关键字,那么派生类的析构函数无论是否加上virtual,它们都构成重写(可以理解为提到过的不规范的写法,实际情况仍建议书写),这是为了防止在销毁对应指针的时候调用错误析构函数。这一部分的规定可以使得在销毁基类的指针时调用基类的析构函数,而调用派生类的指针时调用派生类的析构函数。
2.4抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。