什么是多态?
对于同一行为(函数),不同的对象调用,会有不同的反响。比如售票系统的售票行为(函数)对于学生、残疾人、老人、成人(对象)有不同的反响(执行过程及结果)。
多态:使用基类的指针或引用调用重写的虚函数时,如果父类去调用这个函数,那么自动识别并去调用父类的虚函数;如果是子类去调用这个函数,那么自动识别并去调用子类的虚函数。
怎么实现多态?
实现多态的两个条件:1)调用函数的对象必须是指针或者引用。2)被调用的函数必须是虚函数,并且完成了函数重写。
虚函数
什么是虚函数?在函数前加virtual关键字类的成员函数叫作虚函数。
class Person
{
public:
virtual void BuyTicket() { cout << "Full price ticket" << endl;}
};
子类有一个跟父类的函数名、参数、返回值都完全相同的虚函数。 这种现象称为子类重写了父类的虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "Full price ticket" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "Half price ticket" << endl; }
};
多态调用和对象调用
#include<iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "Full price ticket" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "Half price ticket" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
p.BuyTicket(); //对象调用
s.BuyTicket(); //对象调用
Func(p); //多态调用
Func(s); //多态调用
system("pause");
return 0;
}
我们知道继承中的切片行为,这个Func函数的参数是父类的引用,当函数参数是父类对象时候,这个函数会去调用这个父类的虚函数,当函数参数是子类对象时候,自动发生切片行为,然后去调用子类的虚函数。这就实现了多态,达到了“一种代码,不同对象,不同形态”。
容易被忽视的虚函数
1)父类虚函数前有virtual关键字,子类虚函数前的virtual关键字可以省略。
2)子类的析构函数默认是对父类析构函数的重写。因为编译器会把析构函数都变成destructor
函数去调用。
虚函数表和虚函数表指针
虚函数表是通过一块连续内存来存储虚函数的地址的指针数组,以nullptr结尾。 这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在一张虚函数表,虚函数表就像一张地图,指明了实际应该调用的虚函数。
如果一个类有虚函数,就有对应的虚函数表,类中头四个字节就会存储下虚函数表的地址,这四个字节就是虚函数指针。 证明一下这个虚函数表指针的存在:
#include<iostream>
using namespace std;
class A{
public:
virtual void func()
{
}
};
int main()
{
A a;
cout << sizeof(a) << endl;
system("pause");
return 0;
}
虚函数表在编译阶段初始化
在调用构造函数时,初始化虚函数指针,在调用了Father构造函数之后。我们明显看到虚函数指针指向了唯一的虚函数fun()。
多态就是通过虚函数来实现的,当通过父类指针调用多态函数时候,如果调用者是父类对象,会去父类虚函数表找对应的虚函数,如果是子类对象会去子类虚函数表找对应的虚函数。
虚函数总结
-
最好把基类的析构函数定义声明为虚函数,因为子类和父类的析构函数被编译器处理成destructor函数,所以子类的析构对父类构成覆盖(即重写) ,定义成虚函数即可实现多态。
-
只有类的成员函数才能定义为虚函数,在成员函数前面加virtual关键字即可实现将这个成员函数定义为虚函数。
-
派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值(协变例外)完全相同。
-
基类定义了虚函数,在派生类中该函数始终保持虚函数特性。(即使重写该虚函数没有virtual,也保持有虚函数特性,这个容易造成混淆,属于C++缺陷。)。
-
虚函数表初始化在编译阶段,同一个类拥有相同的一个虚基表,存储在代码段。
-
虚函数的继承不同于普通实现继承,它属于接口继承,我继承了父类的虚函数,就要重写虚函数,否则没有实现多态的意义。
不能定义为虚函数的成员 | 原因 |
---|---|
静态成员函数 | 静态成员没有this指针,没有虚基表 |
构造函数 | 对象不完成,构造函数初始化虚基表指针 |
内联函数 | 内联函数没有函数地址 |
不要在构造/析构函数里调用虚函数 | 对象不完整,容易发生未定义的行为。 |
不把析构函数定义成虚函数,可能出现内存泄漏问题
#include<iostream>
using namespace std;
class Father{
public:
~Father(){}
private:
int a;
};
class Son :public Father
{
public:
~Son(){}
private:
int b;
};
int main()
{
Son s;
Father *f = &s;//父类指针可以指向子类对象,子类对象发生切片行为
delete f;//此时对p调用析构函数,如果析构函数不是虚函数,则调用的是父类的析构,没有释放全部空间
//倘若定义成虚函数,则调用的是子类的析构函数,空间全部释放
system("pause");
return 0;
}
纯虚函数和抽象类
虚函数之后=0,那么这个虚函数就是纯虚函数。纯虚函数不能实例化出对象,子类继承了纯虚函数,必须对纯虚函数重写之后,才能实例化出对象。含有纯虚函数的类称为抽象类,抽象类更能体现出接口继承的特点。
一般而言纯虚函数的函数体是缺省的,但是也可以给出纯虚函数的函数体(此时纯虚函数仍然为纯虚函数,对应的类仍然为抽象类,还是不能实例化对象)调用纯虚函数的方法为:抽象类类名::纯虚函数名(实参表)
因为对于纯虚函数的重写非常有必要,而实际使用中,虚函数重写又有很多限制,比如函数名和返回值、形参类型等必须完全相同。在使用中一不小心将原本需要重写的函数,忘记重写了。那么子类仍然使用父类的虚函数,不符合本应该实现多态的思路,为了防止万一忘记重写。C++11强制子类对父类进行重写,增加了override关键字,翻译成英文就是覆盖,就是标记了我这函数必须重写。override是用来标记子类需要重写的虚函数的。
class Car{
public:
virtual void Drive(){}
};
// 2.override 修饰派生类虚函数强制完成重写,如果没有重写会编译报错
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
1)final 修饰基类的虚函数不能被派生类重写
2 )协变允许重写的虚函数返回值不同的情况。
#include<iostream>
using namespace std;
class Person {
public:
virtual Person& BuyTicket() { cout << "Full price ticket" << endl; return *this; }
};
class Student : public Person {
public:
virtual Student& BuyTicket() { cout << "Half price ticket" << endl; return *this; }
};
void Func(Person& p) //多态通过这个函数实现
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Func(p); //多态调用
Func(s); //多态调用
system("pause");
return 0;
}