多态到底是什么呢?
字面意思就是同种事物在不同的场景下所表现出不同的形态。
在c++当中,多态分类如下:
在学习多态之前,我们必须得先了解虚函数的概念。
- 虚函数就是在类的成员函数(除构造函数、拷贝构造函数、静态成员函数)前加virtual关键字。
class B
{
public:
virtual void TestFunc()
{
cout << "Base::TestFunc()" << endl;
}
int _b;
};
int main()
{
B b;
cout << sizeof(B) << endl;
return 0;
}
这里打印结果为什么是8不是4呢?
在内存窗口上&B之后,发现它的前4个字节放的类似地址的东西,那么这个地址指向的又是什么呢?
将前四个字节的类似地址的东西放到内存窗口上查看后,发现这里放的还是地址,那么这里的地址是什么呢?
其实这里的地址就是虚函数的地址,在带有虚函数的类中,会多开辟四个字节用来存放指向一张虚表的指针,虚表里放的都是虚函数的地址。要注意的是,虚函数只有在继承体系中才有意义,因为在非继承体系用不到,还多开辟了4字节的空间。带有虚函数的类的对象模型如下:
静态多态在这里不过多介绍,主要学习动态多态
动态多态的条件:
- 基类中必须包含虚函数,并且派生类一定要对基类中的虚函数进行重写
- 重写:
- 要和基类中的虚函数原型相同(返回值、参数列表、函数名均相同)(协变和虚拟析构除外)
- 协变:返回值可以不同,但是基类的虚函数必须返回基类对象的指针(引用);派生类的虚函数必须返回派生类对象的指针(引用)–这里不符合返回值相同,但是也是重写。
- 析构函数:析构函数也可以作为虚函数,并且在继承体系中建议作为虚函数(为什么建议稍后解释)
- 通过基类的指针(引用)调用虚函数。
多态的含义:
- 如果基类的指针/引用指向/引用基类的对象,那么在调用虚函数时调用属于基类的虚函数。
- 如果基类的指针/引用指向/引用派生类的对象(赋值兼容规则),那么在调用虚函数时调用属于派生类的虚函数。
class B
{
public:
virtual void TestFunc()//加virtual关键字,成员函数将作为虚函数
{
cout << "Base::TestFunc()" << endl;
}
};
class D : public B
{
public:
virtual void TestFunc()//派生类中对虚函数进行重写,函数名、返回值、参数列表必须一致,
//在重写时,派生类中的访问限定符不会对虚函数有什么影响,且可以不加virtual关键字
{
cout << "Derived::TestFunc()" << endl;
}
};
void Test(B& b)//通过基类的引用调用虚函数
{
b.TestFunc();
}
int main()
{
D d;
B b;
Test(d);
Test(b);
return 0;
}
那么,单继承中派生类的对象模型是怎样的呢?
带有虚函数单继承对象模型
class B
{
public:
virtual void TestFunc()//加virtual关键字,成员函数将作为虚函数
{
cout << "Base::TestFunc()" << endl;
}
int _b;
};
class D : public B
{
public:
virtual void TestFunc()
{
cout << "Derived::TestFunc()" << endl;
}
int _d;
};
int main()
{
D d;
d._b = 1;
d._d = 2;
cout << sizeof(d) << endl;
return 0;
}
在调试的时候调出内存窗口,&d可以很容易得出带有虚函数的单继承对象模型:
普通成员函数和虚函数的区别:
最大的区别就是调用方式不同,普通成员函数直接调用,而虚函数的调用分为如下几步:
- 从对象前4个字节中取虚表的地址
- 传递this指针
- 从虚表中获取虚函数的地址(虚表地址+虚函数在虚表中的偏移量)
- 调用虚函数
带有虚函数的多继承对象模型
class B1
{
public:
virtual void TestFunc1()
{
cout << "Base1::TestFunc1()" << endl;
}
int _b1;
};
class B2
{
public:
virtual void TestFunc2()
{
cout << "Base2::TestFunc2()" << endl;
}
int _b2;
};
class D: public B1, public B2
{
public:
virtual void TestFunc1()//对B1中的虚函数TestFunc1重写
{
cout << "Derived::TestFunc1()" << endl;
}
virtual void TestFunc2()
{
cout << "Derived::TestFunc2()" << endl;//对B2中的虚函数TestFunc2重写
}
virtual void TestFunc3()//派生类自己特有的虚函数
{
cout << "Derived::TestFunc3()" << endl;
}
int _d;
};
int main()
{
D d;
d._b1 = 1;
d._b2 = 2;
d._d = 3;
cout << sizeof(D) << endl;
return 0;
}
对&d得:
发现这里有两个类似地址得东西,查看得
不难发现,这两个地址分别指向两张虚表,一张为继承B1的、另一张为继承B2的,第一张虚表还会存放派生类中特有的虚函数。所以,对象模型如下:
带有虚函数的菱形继承
#include <iostream>
using namespace std;
class B
{
public:
virtual void TestFunc1()
{
cout << "B::TestFunc1()" << endl;
}
virtual void TestFunc2()
{
cout << "B::TestFunc2()" << endl;
}
virtual void TestFunc3()
{
cout << "B::TestFunc3()" << endl;
}
int _b;
};
class C1 : public B
{
public:
virtual void TestFunc1()
{
cout << "C1::TestFunc1()" << endl;
}
int _c1;
};
class C2 : public B
{
public:
virtual void TestFunc2()
{
cout << "B::TestFunc2()" << endl;
}
int _c2;
};
class D : public C1, public C2
{
public:
virtual void TestFunc1()
{
cout << "D::TestFunc()" << endl;
}
int _d;
};
int main()
{
D d;
cout << sizeof(D) << endl;
return 0;
}
对象模型:
其实从派生类D的大小很容易就能推断出对象的模型如下:
由于菱形继承存在数据二意性的问题,所以就引出了带有虚函数的菱形虚拟继承
带有虚函数的菱形虚拟继承
首先,先看看在单继承中,带有虚函数的虚拟继承的对象模型,这里是为了方便理解带有虚函数的菱形虚拟继承的对象模型,因为在单继承中,虚拟继承是没有什么实际意义的。
class B
{
public:
virtual void TestFunc()
{
cout << "B::TestFunc()" << endl;
}
int _b;
};
class D : virtual public B
{
public:
virtual void TestFunc()
{
cout << "D::TestFunc()" << endl;
}
int _d;
};
int main()
{
D d;
cout << sizeof(D) << endl;
d._b = 1;
d._d = 2;
return 0;
}
在对象d中,除了有一张拷贝B的虚表外,因为是虚拟继承,还有一张保存偏移量的表格,调出内存窗口,取地址,如下:
发现,确实是有两个指针,那么,哪一个是指向虚表的,哪一个又是指向保存偏移量的呢,我们可以再调用一个内存窗口进行查看,因为虚表中保存的是地址,而另一个保存的是偏移量,是整数。
可以发现,上面的地址指向存放偏移量的表格,下面的指向虚表,所以再单继承中,带有虚函数的虚拟继承对象模型如下:
上面的是派生类没有新增自己特有的虚函数的模型,接下来我们用同样的方法看看在派生类中新增虚函数后,对象模型又是什么?
class B
{
public:
virtual void TestFunc1()
{
cout << "B::TestFunc1()" << endl;
}
int _b;
};
class D : virtual public B
{
public:
virtual void TestFunc1()
{
cout << "D::TestFunc1()" << endl;
}
virtual void TestFunc2()
{
cout << "D::TestFunc2()" << endl;
}
int _d;
};
int main()
{
D d;
cout << sizeof(D) << endl;
d._b = 1;
d._d = 2;
return 0;
}
算下来大小比上面的多了4字节。其实就是如果派生类自己新增虚函数,就会多开辟四个字节,指向另一张虚表,这张虚表中存放派生类自己特有的虚函数地址。
接下来,再来看看带有虚函数的菱形虚拟继承,这里为了简单起见,没有在派生类中新增特有的虚函数。
class B
{
public:
virtual void TestFunc1()
{
cout << "B::TestFunc1()" << endl;
}
virtual void TestFunc2()
{
cout << "B::TestFunc2()" << endl;
}
virtual void TestFunc3()
{
cout << "B::TestFunc3()" << endl;
}
int _b;
};
class C1 : virtual public B
{
public:
virtual void TestFunc1()
{
cout << "C1::TestFunc1()" << endl;
}
int _c1;
};
class C2 : virtual public B
{
public:
virtual void TestFunc2()
{
cout << "C2::TestFunc2()" << endl;
}
int _c2;
};
class D : public C1, public C2
{
public:
virtual void TestFunc3()
{
cout << "D::TestFunc3()" << endl;
}
int _d;
};
int main()
{
D d;
cout << sizeof(C1) << endl;
cout << sizeof(D) << endl;
d._b = 1;
d._c1 = 2;
d._c2 = 3;
d._d = 4;
return 0;
}
对象模型如下:
讲到这里,现在我们来想想动态多态的实现原理
动态多态实现原理
- 编译器在带有虚函数的类的背后维护了一张虚表(虚函数的入口地址)
- 虚函数的调用原理(通过基类的指针/引用调用虚函数)
- 从指针所指对象(基类/派生类)前4个字节中取虚表的地址
- 传递参数(this+当前虚函数的参数)
- 根据从对象4个字节取到的虚表的地址取对象的虚函数地址
- 调用虚函数
现在我们再来谈谈为什么静态函数,构造函数,拷贝构造函数为什么不能作为虚函数?
- 最主要的原因就是调用虚函数需要this指针,但是这几个函数当中并没有虚函数(或者还没构造好对象)
那么为什么建议将析构函数作为虚拟函数呢?
答案是因为如果不作为虚拟函数,有可能会造成内存泄露的问题,如下:
class B
{
public:
B()
:_b(1)
{
cout << "B::B()" << endl;
}
~B()
{
cout << "B::~B()" << endl;
}
int _b;
};
class D: public B
{
public:
D()
:_ptr(new char[10])
{
cout << "D::D()" << endl;
}
~D()
{
if (_ptr)
{
delete[] _ptr;
}
cout << "D::~D()" << endl;
}
char* _ptr;
};
void TestFunc()
{
B* pb;
pb = new D;//由于赋值兼容规则,基类指针可以指向派生类对象,这里就会调用D的构造函数申请空间
delete pb;//由于是基类类型的指针,所以delete只会调用基类的析构,所以这里就会造成内存泄漏
}
int main()
{
TestFunc();
return 0;
}
只要我们将析构函数作为虚函数,就不会出现上面的问题了,打印结果如下:
那么,为什么不直接将析构函数默认作为虚函数呢?
- C++不 把虚析构函数直接作为默认值的原因是虚函数表的开销以及和C语言的类型的兼容性。