1、多态
在继承中构成多态的必要条件:
- 必须通过基类的指针或者基类的引用调用虚函数;
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
应用:
- 重写 详细解释请点击:重载、重写、重定义链接
在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写 (因为继承后,基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,所以尽量都加virtual关键字 - 指针和引用
满足多态,跟指向的对象有关,传哪个对象就是谁的;
不满足多态,就是普通调用,跟指针的类型有关
2、C++11 override和final
override:
检查派生类的虚函数是否重写了基类中对应的虚函数,如果没有重写,则编译报错!
class Car{
public:
virtual void Drive()
{}
};
class Benz :public Car {
public:
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};
final:
修饰类,表示该类不能被继承。
修饰虚函数,表示该虚函数不能再被继承!
class Car
{
public:
virtual void Drive() final
{}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
3、抽象类
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
4、多态的原理
虚函数 + 动态绑定。
虚函数的重写:
派生类中有一个与基类完全相同的虚函数,即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同,这就是派生类的虚函数重写了基类的虚函数。
两个特例:
特例1:协变
派生类重写基类虚函数时,基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A
{};
class B : public A
{};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
特例2:析构函数的重写
基类与派生类析构函数的名字不同。
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加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;
delete p2;
return 0;
}
运行结果:
注意:
析构函数可以定义成虚函数,并不是必须定义成虚函数,但有一个场景,必须定义成虚函数:父类指针new一个子类,delete父类指针,析构函数没有定义成虚函数,会造成内存泄漏!
基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。
虚函数和纯虚函数:
- 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,抽象类不能实例化对象。
- 虚函数的定义形式:virtual xx xxx{body};纯虚函数的定义形式:virtual xx xxx{ } = 0; 。
在虚函数和纯虚函数的定义中不能有static标识符,因为,被static修饰的函数在编译时候要求前期绑定,然而虚函数却是动态绑定。 - 虚函数和纯虚函数都可以在子类被重载。
- 虚函数必须实现,否则会报错。
- 实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数。
虚函数存在哪?
虚函数和普通函数一样,都是存在代码段的;虚函数指针(地址)放在虚函数表里;虚函数表简称虚表;虚表通常存在vs下代码段,Linux下只读数据段(常量区);虚表指针存在**对象(堆或者栈)**中,不同的平台,该指针放的位置不同,有的放在对象的前面,有的放在对象的后面) 。
虚函数表的存放规则:先定义的函数放在最前面。
同一个类型对象的虚表指针指向同一个虚表,所以同一个类实例化的所有对象的虚表是一样的:
class A
{
public:
virtual void func(void)
{};
};
int main()
{
A a1, a2;
int * pVfptr = (int*)(&a1);
printf("a1 的__vfptr = %x\n", (*pVfptr));
pVfptr = (int*)(&a2);
printf("a2 的__vfptr = %x\n", (*pVfptr));
system("pause");
return 0;
}
虚函数表:
一个含有虚函数的类中至少有一个寻函数表指针_vfptr,(v:virtual f:function)。(不同的平台,该指针放的位置不同,有的放在对象的前面,有的放在对象的后面)
为什么会有虚函数表?
一个含有虚函数的类中至少有一个虚函数表指针_vfptr,因为虚函数的地址要被放到虚函数表中,虚函数表也被简称 虚表。
一个类实例化多个对象的虚函数表是共用的吗?
如果一个类中有虚函数,那么这个类实例化出的多少个对象都是共用同一个虚函数表的。
虚函数表的继承:
(非继承的类)
- 如果一个类中有虚函数,则该类就有一个虚函数表。
- 虚函数表是属于类的,不属于类对象。在编译的时候确定,存放在只读数据段。
- 每一个实例化的类对象都有一个虚函数表指针,指向类的虚函数表。
- 虚函数表指针属于类对象。存放在堆上或者栈上。
(单继承的类)
- 如果基类中有虚函数,派生类实现或没实现,都有虚函数表。
- 基类的虚函数表和派生类的虚函数表不是同一个表。如果派生类重写了基类虚函数,那么派生类虚函数表中存的是重写(覆盖)后的基类虚函数。
- 如果派生类没有重写基类的虚函数,则派生类的虚函数表和基类的虚函数表的内容是一样的。
- 该派生类对象由两部分组成,一部分是基类继承下来的成员,虚表指针存在这里;另一部分是自己的成员。
- 继承下来的不是虚函数,就不会被放进派生类虚函数表中。
- 虚函数表本质是一个存放虚函数指针的指针数组,它的最后面放了一个nullptr。
- 派生类的虚表的生成:
a. 先将基类的虚函数表拷贝到派生类虚函数表中;
b. 如果派生类重写了某个虚函数,则覆盖掉所拷贝来的对应虚函数信息;
c. 在派生类虚函数表后面增加派生类独有的虚函数,增加的顺序就是虚函数在派生类中声明的顺序,存放的顺序就是继承基类的顺序,放到继承的第一个基类虚函数表中。
(多继承的类)
- 一个类可能有多个虚函数表。
含有虚函数的基类有多少个,派生类就有多少个虚函数表指针,派生类有就有多少个虚函数表。 - 派生类有的而基类没有的虚函数,添加在第一个虚函数表中。
- 虚函数表的结果是* 表示还有下一个虚函数表。
- 虚函数表的结果是0 表示是最后一个虚函数表。
虚函数存在哪里?虚函数表存在哪里?对象中存的是什么?
虚函数和普通的函数一样,存在代码段;
指向虚函数的指针存在虚函数表中,所以虚函数表本质是一个指针数组,存的是指针;
vs下,虚函数表存在代码段;Linux下,在gcc编译器中,虚函数表vtable存放在可执行文件的只读数据段.rodata中;
虚函数表指针存在对象(栈或堆)中。
动态绑定与静态绑定(动态联编与静态联编)
静态绑定:前期绑定,早绑定
在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定:后期绑定,晚绑定
在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体函数,也称为动态多态。
5、练习
- inline函数可以是虚函数吗?
答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。 - 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。 - 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。 - 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。场景:父类指针new一个子类,delete父类指针,析构函数没有定义成虚函数,会造成内存泄漏! - 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。 - 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段/只读常量区的。 - 什么是抽象类?抽象类的作用?
答:包含纯虚函数的类时抽象类。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。