目录
一、多态的概念
从字面上就可以知道,多态意指多种形态,具体而言,不同的对象在完成某个行为时会产生出不同的形态。
二、多态的定义及实现
1、重写(覆盖)
在派生类中有一个和基类完全一样(即函数名称、参数列表、返回类型都一样)的函数,就称为重写。
1、协变
派生类重写基类的虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A{};
class B:public A{};
class Person{
public:
//返回父类的指针
virtual A* func(){
return new A;
}
};
class Student:public Person{
public:
//返回子类的指针
virtual B* func(){
return new B;
}
};
2、析构函数的重写
析构函数的名称在编译器中会被统一处理为destructor。
当用户自己定义的析构函数不是虚函数时:
class Person{
public:
~Person(){cout<<"~Person()"<<endl;}
};
class Student{
public:
~Student(){cout<<"~Student()"<<endl;}
};
int main(){
Person* p1 = new Person;
Person* p2 = new Student;//new一个子类,用基类指针指向该子类对象
delete p1;
delete p2;
return 0;
}
程序运行结果为:
~Person()
~Person()
本意是让p1调用Person的析构,让p2先调用Person的析构再调用Student的析构,但是输出结果显示,p2并没有调用Student的析构,只调用了Person的析构,原因是:编译器将析构函数 ~Person() 和 ~Student() 都统一处理成destructor,发生了隐藏。
给析构函数加上virtual后:
class Person{
public:
virtual ~Person(){cout<<"~Person()"<<endl;}
};
class Student{
public:
virtual ~Student(){cout<<"~Student()"<<endl;}
};
int main(){
Person* p1 = new Person;
Person* p2 = new Student;//new一个子类,用基类指针指向该子类对象
delete p1;
delete p2;
return 0;
}
程序运行结果为:
~Person()
~Student()
~Person()
☆注意:当在new场景下,必须需要析构函数是虚函数,避免内存泄漏。
2、多态的两个构成条件
- 必须是基类的指针或者引用调用虚函数;
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
class human{
public:
//被调用的函数为虚函数,用关键字virtual修饰
virtual void BuyTicket(){
cout<<"买票-全价"<<endl;
}
};
//继承human
class student:public human(){
public:
//对基类的虚函数进行重写
virtual void BuyTicket(){
cout<<"买票-半价"<<endl;
}
};
//以基类引用的方式调用虚函数BuyTicket()
void func(human& h){
h.BuyTicket();
}
int main(){
human hu;
student st;
func(hu);
func(st);
return 0;
}
程序运行结果为:
买票-全价
买票-半价
3、override和final
在基类虚函数后加override,表示必须要对其进行重写:
class Person{
public:
virtual void Test(){
cout<<"此函数必须被重写"<<endl:
}
};
class Student:public Person{
public:
virtual void Test() override {
cout<<"已经被重写"<<endl;
}
};
int main(){
Person p;
Student st;
p.Test();
st.Test();
return 0;
}
程序运行结果为:
此函数必须被重写
已经被重写
在基类名后加final表示该基类不能被继承;在基类成员函数后加final表示该函数不能被重写。
4、重载、覆盖和隐藏的对比
重载 | 重写(覆盖) | 重定义(隐藏) |
---|---|---|
两个函数在同一作用域 | 两个函数分别在基类和派生类的作用域 | 两个函数分别在基类和派生类的作用域 |
函数名/参数列表相同 | 完全相同(协变除外) | 不构成重写就是重定义 |
5、抽象类(abstract class)
在虚函数后面加上 = 0,则这个函数就为纯虚函数。纯虚函数只有函数声明,没有函数体,最后的=0并不表示函数返回值,只起形式上的作用,告诉编译系统“这是纯虚函数”。包含纯虚函数的类被称为抽象类,无法实例化,即无法创建对象。派生类继承后也不能实例化对象,只有重写纯虚函数,派生类才能实例化对象。纯虚函数规范了派生类必须重写,更体现出了接口继承。
class Person{
public:
virtual void test() = 0;//只有函数声明没有函数体
};
class Student:public Person{
public:
vitual void test(){
cout<<"重写纯虚函数"<<endl;
}
}
int main(){
Person p;//会报错
Student st;//由于在派生类Student中已经对test()进行了重写,所以此时正确,不会报错
return 0;
}
普通函数继的承是一种实现继承。 派生了继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口的继承。 派生类继承的是基类虚函数的接口,目的是对纯虚函数进行重写,达成多态,继承的是接口。
所以,如果不实现多态,就不要把函数定义为虚函数。
三、多态的原理
1、虚函数的定义
在函数前加上关键字virtual,称其为虚函数。
虚函数的核心作用:
- 实现动态联编,在函数运行阶段动态地选择合适的成员函数;
- 在定义了虚函数后,可实现在派生类中对虚函数的重写,从而实现多态。
2、虚函数表
下列代码在×64的环境中运行:
class Person{
public:
virtual void test(){}
private:
int _a = 1;
};
int main(){
Person p;
cout<<sizeof(p)<<endl;
return 0;
}
程序运行结果为:
16
p对象的长度为16bytes,原因是在Person类中,除了_a成员,还有一个 _vfptr的成员,它被叫做虚函数表指针,v表示virtual,f表示function。一个含有虚函数的类中至少含有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称为虚表。