文章目录
1 . 多态原理
c++多态1—基础知识和概念
了解了多态的一些基础知识之后,就能更加深入的理解一下多态的实现原理了。
首先我将用下面这个例子来理解一下构成多态的对象的内存分布情况和实现原理。
class Base
{
public:
virtual void fun() {cout << "Base:fun()" << endl;}
virtual void fun1() {cout << "Base:fun1()" << endl;}
virtual void fun2() {cout << "Base:fun2()" << endl;}
private:
int m_base=1;
};
class A : public Base
{
public:
virtual void fun() {cout << "A:fun()" << endl;}
private:
int m_a=2;
};
int main()
{
Base b1;
cout << sizeof(b1) << endl;
return 0;
}
通过观察测试我们发现b1对象大小是8bytes,因为除了m_base成员,还多一个__vfptr放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关),这个指针就是虚函数表指针(v代表virtual,f代表function),因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。一个含有虚函数的类中都至少都有一个虚函数表指针(单继承只有一个虚函数表指针),
如上图所示,实例化了一个派生类a1对象,再用a1给基类b1对象赋值。
可以观察到:通过子类成员实例化出来的父类成员只能看到父类定义范围类的成员,即b1中无法观察到a1中的m_a成员。
产生的两个对象各有一个地址不同的虚表指针,虚表指针指向一个装有虚函数的虚表中(函数数组),只要是被声明为虚方法的成员都会被记录在虚表中(有的平台会在虚表的结尾放一个nullpter).
则a1的内存模型如下:
首先需要了解的是:只要函数被声明为虚函数就会被记录在虚表中。 当有派生类重写了父类的虚函数,并且通过基类的指针或引用调用了该函数就会形成多态
这里我利用上图中的例子解释一下产生多态的原理,首先构造类对象b1时会形成一张虚表,其中记录了虚函数Base::fun()、Base::fun1()、Base::fun2()。当用派生类对象的指针或者引用给父类对象赋值,发现fun()满足多态的条件,就会在基类的虚表中将对应Base::fun()的函数指针用A::fun()的函数指针覆盖掉。这样该基类对象在调动fun()方法则按之前记录下的偏移位置寻找到Base::fun()的地址,然而Base::fun()的指向已被替换为A::fun()函数的地址,调用的时候自然就运行A::fun()函数了。这样就完成了多态。
总结一下派生类的虚表生成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中 。
- 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
2虚表和虚函数的存储
这小节和下一小节分内容参考文章C++ 虚函数表解析,讲解的比较清晰,方便学习使用。
虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中,虚表也存在代码段。另外对象中存的不是虚表,存的是虚表指针。
通过以下操作可以找到虚表存放地址,并运行虚表中的函数:
class Base {
public:
virtual void f() {
cout << "Base::f" << endl;
}
virtual void g() {
cout << "Base::g" << endl;
}
virtual void h() {
cout << "Base::h" << endl;
}
};
void main()
{
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
printf("对象的地址0x%p\n", &b);
printf("虚函数表地址:0x%p\n", *(int*)(&b)); //__vfptr为四字节指针,需要将对象地址强转为四字节的指针。
printf("虚表中第一个函数地址:0x%p\n", *(int*)*(int*)(&b)); //将虚表指针指向的地址转换为四字节的即得到虚函数表中指向第一个函数的地址,解引用之后则得到虚表中第一个函数地址。
//运行虚表中的三个函数
for (int i = 0; i < 3; ++i) {
pFun = (Fun) * (((int*)*(int*)(&b)) + i);
pFun();
}
}
注意:这里在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“\0”一样,其标志了虚函数表的 结束。这个结束标志的值在不同的编译器下是不同的。
3.单继承和多继承关系的虚函数表
3.1单继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
对于实例:Derive d; 的虚函数表如下:
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
3.2 单继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
3.3 多继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
3.4 多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
4 动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
5 抽象类
- 概念:
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
抽象类的主要作用为给其子类提供操作接口,而具体操作则由子类实现。
eg:
class Car
{
public:
virtual void Drive() = 0; //纯虚函数。
};
class Benz :public Car
{
public:
virtual void Drive(){cout << "Benz-舒适" << endl;}
};
class BMW :public Car
{
public:
virtual void Drive(){cout << "BMW-操控" << endl;}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
- 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。