前言
文章目录
1. 多态的理解
简单的说:多态就是根据你传入的不同的对象,去做同一样的事情,会有不同的结果和行为的表现产生;
在C++中,传入不同的对象意思:给父类传入不同的子类;
去做同一样的事意思是:父类指针或者引用调用了同一样的虚函数;
产生不同的结果意思是:调用同样的虚函数,但是结果却不一样;
2. 构成多态的条件
继承中要构成多态还有两个条件:
1. 必须通过基类的指针或者引用调用虚函数;
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写;
3. 虚函数
虚函数:即被virtual修饰的类非静态成员函数称为虚函数。(静态成员不可以作为虚函数,全局函数也不行)
虚函数的重写;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
4. 虚函数的重写(覆盖)
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
5.多态调用虚函数的规则
构成多态,跟父类的指针或者引用的类型没有关系,传的哪个类型的对象,调用的就是这个类型的虚函数 – 跟对象有关;
不构成多态,调用就是父类指针或者引用类型的函数 – 跟类型有关;
#include<iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void test1(Person& p)
{
p.BuyTicket();
}
void test2(Person* p)
{
p->BuyTicket();
}
int main()
{
Person ps;
Student st;
//父类引用调用虚函数,子类重写虚函数,构成多态;那么调用函数就传入的对象有关
//这里传入的对象ps的类型为 Person 所以调用Person的虚函数;
Person& p1 = ps;
p1.BuyTicket();
//父类引用调用虚函数,且子类重写父类虚函数,构成多态,那么调用函数就和传入的对象有关;
//这里传入的对象是 Student,所以调用的是Student的虚函数;
Person& p2 = st;
p2.BuyTicket();
//指针的分析和引用一致
Person* p3 = &ps; //构成多态
p3->BuyTicket();
Person* p4 = &st; //构成多态
p4->BuyTicket();
getchar();
return 0;
}
符合预期:
测试:假如不是父类指针或者引用调用虚函数会发生什么;
去除条件1,不是父类指针或引用而是父类对象去调用虚函数,是否会发生多态?
- 假如 子类对象赋值给父类对象会不会发生多态?肯定不会?所以父类对象调用虚函数,只看父类的类型;
//子类对象赋值给父类对象,没有父类指针或引用指向子类对象,这不会构成多态,所以调用虚函数
//只看p5的类型;
Person p5 = st;
p5.BuyTicket(); //调用的是父类的虚函数,不构成多态
- 即使有父类的指针或引用调用了虚函数,假如子类没有重写虚函数,依旧不会发生多态,没有多态那么,父类指针或引用去调用虚函数时候,都是看父类的类型的;
(ps:我们可以修改父类和子类虚函数,只要不构成重写,就可以验证我上面的结论)
6. 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成
//多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1; // 编译器默认转化了调用虚构函数的形式 p1->destructor()
delete p2; // 编译器默认转化了调用虚构函数的形式 p2->destructor(),
//由于构成多态,所以会正确的嗲用传入对象是子类的虚析构函数
return 0;
}
构成多态,正确的调用了子类的析构函数;
这也是唯一的场景:是析构函数,需要写成虚析构的形式;
没有发生多态时候,不管你的父类还是子类是否为虚析构,都可以正确的调用析构函数
非虚析构函数,依旧可以正确释放
上面两种原因很简单:继承体系中,子类对象指向析构时候,本身就会现执行子类再执行父类,和你的析构函数是否为虚函数没关系;
所以说析构函数是否需要写成虚析构函数,首先得发生多态行为,才需要父类析构写成虚析构,这也才可以保证delete 父类指针时候,能够去调用子类的析构函数;
本质我们 delete 父类指针,就是为了析构子类对象的,因为父类指针指向了子类对象,我们希望的就是做这样的事;delete父类指针,为了析构子类对象,那么就需要发生多态,这个行为才会产生;有多态,就必须,子类重写父类的虚析构函数;
7. 设计一个不能被继承的基类
在C++98时候,有一种方式可以设计不被继承:那就是设计基类的构造函数为私有的;
#include<iostream>
using namespace std;
class Base{
private:
Base(){
cout << "Base::Base()的构造函数调用" << endl;
}
private:
int _b;
};
class Derive :Base{
private:
int _d;
};
int main(){
Derive son;
return 0;
}
这种方式:当我屏蔽了:Derive son
这句代码,也就是,不创建子类对象时候,上面的代码编译是可以通过的;
但是当我写上了Derive son
也就是创建子类对象时候,那么就会编译报错:原因就是基类的构造函数为私有,派生类创建对象时候,也是执行Derive son
代码时候,需要先调用父类的构造函数,但是由于父类的构造函数时私有的,不可以被调用,所以就会编译报错了;
设计基类的构造函数是私有的:这也就解决了一个类不可以被继承的情况;
8. final关键字和override 关键字
C++11通过新增一个关键字final:解决了基类不可以被继承问题, 也就是不需要设计基类的构造函数为私有的;
具体操作就是:在声明基类的后面加多一个关键字 final就可以达到效果;
class Base final{//final关键字修饰,使得该类不可以被继承
public:
Base(){
cout << "Base::Base()的构造函数调用" << endl;
}
private:
int _b;
};
class Derive :public Base{
private:
int _d;
};
int main(){
Derive son;
return 0;
}
final关键还可以修饰基类的虚函数,目的就是为了使得基类的虚函数不被子类重写;
class Base {
public:
Base(){
cout << "Base::Base()的构造函数调用" << endl;
}
virtual void fun() final{ //使得该虚函数不能够被重写
cout << "Base::fun()被调用" << endl;
}
private:
int _b;
};
class Derive :public Base{
public
virtual void fun(){ //由于基类这个函数fun被final修饰,所以派生类不可以重写
cout << "Derive::fun()被调用" << endl;
}
private:
int _d;
};
int main(){
Derive son;
return 0;
}
c++11还有一个关键字override,它是修饰派生类的虚函数,目的为的是:派生类必须重写基类虚函数,如果基类没有该虚函数,那么就会报错,或者派生类重写错乱基类虚函数,那么就会报错;
反正就是在基类中找不到你要重写的虚函数,那么就会直接报错;
所以说:override 有检查机制,帮你检查你的派生类是否正确的重写了基类的虚函数;
class Base {
public:
Base(){
cout << "Base::Base()的构造函数调用" << endl;
}
virtual void fun() {
cout << "Base::fun()被调用" << endl;
}
private:
int _b;
};
class Derive :public Base{
public:
virtual void fun1() override{
cout << "Derive::fun()被调用" << endl;
}
private:
int _d;
};
int main(){
Derive son;
return 0;
}
9. 抽象类
3.1 概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
由于抽象类不能实例化对象,所以虚函数实现也是没有任何意义的,所以不会有人实现纯虚函数;
纯虚函数是可以实现的,但是就是没有意义罢了;
class Car
{
public:
virtual void Drive() = 0; //纯虚函数
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
有纯虚函数,只能用父类指针或引用指向子类对象了,不可能再指向父类对象,因为父类对象是不可以被创建的;
10. 接口继承和实现继承区别
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。