多态的概念
什么是多态
多态就是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
实现多态的条件
- 动态绑定多态(在运行时才知道函数的地址):
调用函数的对象是指针或引用。
被调用函数必须是虚函数,并且完成了虚函数的重写。
- 静态绑定多态(在编译时确定函数的地址):
函数重载
多态的作用
封装是为了代码模块化,继承是扩展已存在的代码,他们的目的都是为了 实现代码的复用,但是多态的目的是为了实现接口的重用,也就是多不管传递过来的是哪个类对象,函数都能狗狗通过这个接口调用到适应自己对象的实现方法。
虚函数重写:子类重写父类的函数(协变:子类中的返回值和父类中的返回值是父子关系,并且是引用或指针)。如果只在父类中添加virtual关键字,子类中的重写函数也会称为虚函数,童谣可以构成多态,否则为重定义。
函数重载、重定义、重写的区别:
- 函数的重载:在相同的作用域中,两个函数的参数不同,函数名相同,构成函数重载。
- 函数重定义:子类中有父类的同名函数,继承自父类的函数被隐藏,子类调用重写后的。
- 函数的重写:子类与父类的虚函数函数名、参数、返回值相同的函数,构成函数重写。
虚函数和纯虚函数
虚函数就是在类的成员函数钱钱添加了virtual关键字。主要是为了实现多态,通过一张虚函数表来实现。它允许子类重写来自父类的成员函数。
纯虚函数就是在基类中只对对应的虚函数进行声明,在最后加=0,定义纯虚函数的类是一个抽象类,抽象类不能被实例化,它体现的是接口继承,子类只是继承了这个接口的形式,不需要使用他里面的功能,要实现自己的功能。在子类中必须实现父类的纯虚函数,如果不实现父类的纯虚函数,这个子类也是一个抽象类,同样不能区实例化对象。
那么什么是抽象类呢?抽象类就是包含纯虚函数的类,这个类不能被实例化,因为类定义的不完整,成员函数都没有实现,这种类只是为了接口继承的实现,继承他的子类并不想去用它,子类要实现自己的功能。
无论是虚函数还是纯虚函数,都是在基类中为派生类提供编程接口,面向对象最核心的思想就是对接口编程,而不是对实现编程,在C++中,就是使用继承和多态来事项这种思想。如果想让基类为派生类提供缺省的处理方法,那么就将这个函数设为虚函数,如果是想让派生类必须重写该虚函数,就将这个函数设为纯虚函数。
1、虚函数和纯虚函数可以定义在同一个类中,一旦某个类包含了纯虚函数,这个类就是一个抽象类。
2、虚函数可以直接被使用,但是纯虚函数必须要在派生类中实现之后才可以使用,一i那位纯虚函数在积累中只声明没有定义,所以无法直接使用。
3、虚函数和纯虚函数都可以在子了中被重写,一堕胎的形式被调用。
4、虚函数和纯虚函数都是为了实现接口继承而出现。
5、虚函数的定义:virtual + 函数
纯虚函数定义:virtual + 函数 + =0;
6、虚函数和纯虚函数都不能设为static,因为static在编译的时候就要被绑定,但是虚函数和纯虚函数实在运行时在确定的。
虚函数表
虚函数表是一个函数指针数组,在末尾存放的时一个空指针nullptr,在VS中存放在代码段,在虚函数表中只存放虚函数执政,他不存放普通函数的指针,将新创建的虚函数指针存放在虚函数表末尾。
虚标指针:存放在对象模型中,在32位机上,存放在对象模型的头4个字节中。
单继承中的虚表
在单继承中所有的虚函数都存放在虚表中,如果有被重写的,直接将虚表中的虚函数指针换成重写后的虚函数指针即可,如果在子类中的添加了新的虚函数,按照新的虚函数的声明顺序将其添加到虚表中。在虚表中,先添加父类的虚函数指针,再添加子类的虚函数指针。
多继承中的虚表
#include <iostream>
using namespace std;
class base1
{
public:virtual void func1(){cout << "base1::func1" << endl;}virtual void func2(){cout << "base1::func2" << endl;}
private:int b1;
};
class base2
{
public:virtual void func1(){cout << "base2::func1" << endl;}virtual void func2(){cout << "base2::func2" << endl;}
private:int b2;
};
class derive:public base1, public base2
{
public:virtual void func1(){cout << "derive::func1" << endl;}virtual void func3(){cout << "derive::func3" << endl;}
private:int d1;
};
typedef void(*VFPTR)(); //函数指针
void printFunc(VFPTR* vftable)
{cout << "虚表地址" << vftable << endl;for (int i = 0; (*vftable) != nullptr; ++i){cout << "第" << i << "个函数地址" << *vftable << "--->";(*vftable)();//调这个函数++vftable;//指针向后走,打印下一个函数地址}
}int main()
{derive d;//取虚标地址的方法://先取到d的地址,由于虚标指针存放在对象的头四个字节中,所以要将他转为int*类型(int* 位四个字节),这样就可以得到头四个字节的内容,对这个已经得到的四个字节解引用就可以得到它的值,但现在他是整型的,我们要的是函数指针,所以要将他强转位函数指针类型的指针就可以得到虚表的地址(VFPTR*)*((int*)&b);//如果要调用虚表中的第一个函数,对他向后偏移1个单位再解引用就可以调用虚表中的第一个函数(*(((VFPTR*)*((int*)&b))+1))();//打印第二个虚标地址//方法一://给出一个base2的指针,让他存放d的地址,这时候就发生了一个天然的转换,这个指针中存放的就是第二个虚表的地址base2* pd = &d;//方法二//让第一个虚表的地址向后偏移base1的大小(VFPTR*)*((int*)((char*)&b+sizeof(base1)))return 0;
}
接口继承和实现继承
普通函数的继承就是一种实现继承,派生类继承了基类函数,继承的是函数的实现。
虚函数的继承是接口继承,派生类继承的是基类函数的接口,目的是为了重写,从而达成多态,继承的是接口。如果不是为了实现多态,不要把函数设为虚函数。