多态的构成条件:
多态是指存在继承关系的不同类对象中,在调用同一个函数时所表现的不同行为。
继承中多态的构成还需要两个条件:
1、必须通过基类的指针和引用来调用虚函数。
2、被调用的函数必须是虚函数,派生类需要对虚函数进行重写。
重写的条件是虚函数+三同(函数名,参数,返回值)
虚函数:
虚函数必须要在类作用域中才存在,需要关键字virtual。
class Person
{
public:
//虚函数
virtual void fun()
{
};
};
不同的对象当成参数传来就可以当成多态。
一些细节:
重写的条件本来是虚函数+三同,但有一些例外。
1、派生类的重写虚函数可以不加virtual,但建议加上。
2、存在协变的特殊情况,返回值可以不同,但必须是父子关系的指针或引用。
3、对于析构函数而言,要想满足虚函数的重写,在编译器进行编译时需要将析构函数都处理成destructor这一个统一的名字。而析构函数的重写,是为了不发生内存泄漏。
Person* p = new person;
delete p;//这个场景没问题
//在不满足多态的情况下就会出现下面的场景
p = new Student;
delete p;//这里相当于p->destructor() + operator delete(p)
//由于是Person*类型的去调用析构函数,所以其不能直接去调用Student类的析构函数,造成内存泄漏
//只要析构函数形成多态,那上面的代码在编译时会自动调用派生类的析构函数
//不再受限于调用者(p),编译器根据多态进行析构
重写失效:
这里需要认识关键字final。
override:
该关键字是用来检查函数是否完成了重写。
设计一个不被继承的类:
方法一:
对构造函数进行私有化。
可以在类中写另外的函数来访问构造函数。
不过需要静态化来调取未形成对象的类中函数。
方法二:
基类加上关键字final来限制。
重载、重写、重定义的区别:
重载:
1、函数在同一个作用域中。
2、函数名/参数不同。
重写(覆盖):
1、两个函数分别在基类和派生类的作用域。
2、两个函数必须是虚函数+三同(协变在返回值上是父子关系的指针或引用)。
重定义(隐藏):
1、两个函数分别在基类和派生类的作用域。
2、函数名相同。
简单地说 两个分别在基类和派生类的同名函数不构成重写那就是重定义。
虚函数表:
虚函数本质是放在代码段中,但对象里含有虚函数表来存放虚函数的地址。
不符合多态时,编译器在进行编译就可以确定函数地址。
符合多态时,运行时会找到指向对象的虚函数表寻求地址。
基类和派生类的虚函数表是可以通过切片来统一做成基类的格式以便在调用时找到虚函数表。
这也是为什么可以基类对象调用基类对象的虚函数表,派生类对象调用派生类对象的虚函数表。
为什么在多态的实现中不能是基类对象?
在涉及虚函数的概念后,回过头来思考多态的条件我们不免发出这样的疑问。
首先要明确的一点是,在多态的实现中需要将虚函数表进行拷贝,然后通过虚函数的重写覆盖了旧虚函数地址,换成派生类虚函数地址或者是新地址。
如果是使用基类对象,派生类对象赋值给基类对象的过程中,不会拷贝虚表。
多继承中的虚函数表:
使用sizeof可以知道,创一个Derive对象所需的内存大小是20。
从何而来?打开监视窗口看发现是继承了两个基类的虚函数表(实际是指针),还有自己的成员。
虽然是继承了基类的两个虚函数表,派生类自己的虚函数就会被放在两个虚函数表中的一个。
仔细一看,发现虽然通过两个虚函数表分别能调用func1函数,但地址不一样,这涉及到汇编语言,暂时不介绍。
抽象类:
纯虚函数:
在虚函数后面加上 = 0 就是纯虚函数。
class Book
{
public:
virtual void Print(char* name) = 0;
private:
char* _name;
};
抽象类的定义:
包含纯虚函数的类叫做抽象类,抽象类不能实例化对象。
派生类继承了抽象类后也不能实例化对象,只有重写了纯虚函数,派生类才能实例化对象。
class Ficton:public Book
{
public:
virtual void Print();
//这样不能实例化
virtual void Print(char* name)
{
printf("%s\n", name);
}
};
接口继承:
对抽象类的继承也称为接口继承,使用抽象类,就是强制对虚函数进行重写使不同的类实现不同的功能,可以认为是接口。
总结:
1、什么是多态?
多态可以分为静态多态和动态多态。
静态多态:普通函数重载。例如使用cout时我们不需要像printf函数那样需要特定的格式输出符,事实上cout根据不同类型参数实现了函数的重载。
动态多态:继承中虚函数的重写 + 父类指针调用。父类指针可以指向父类对象,也可以指向子类对象,从而十分灵活地调用虚函数表,获取指向对象的虚函数。
实现原理:静态多态通过函数名修饰规则,动态多态通过虚函数表实现。
2、内联函数可以是虚函数吗?
可以,编译是通过的,但是写上inline并不代表函数就是内联函数,因为虚函数需要有地址存放在虚函数表中,而内联函数类似于宏那般,在编译阶段就写入代码中了,没有地址。
3、静态成员函数可以是虚函数吗?
不行。静态成员函数不和其它成员函数一样拥有this指针,无法通过父类指针方式访问虚函数表,因此无法实现多态。
4、构造函数不能是虚函数。
虚函数表在编译中就已经生成,但虚函数指针是类实例化时,通过构造函数的初始化列表对虚函数表指针进行初始化。
5、析构函数最好是虚函数。
这种情况发生在具有继承关系的类之间,因为使用delete对数据进行清除时难免会根据不同的类进行处理,使用虚函数是为了重写析构函数,实现多态。
6、效率问题:使用普通函数更快还是虚函数更快?
如果是使用普通对象,那二者无区别;如果使用指针(引用)对象,因为虚函数要调用虚函数表,在效率上更低。