在上篇文章中,简单的介绍了多态中的概念以及其相关原理。本文将针对多态中其他的概念进一步进行介绍,并且更加深入的介绍关于多态的相关原理。
目录
1. 抽象类:
2. 再谈虚表:
3. 多继承中的虚函数表:
1. 抽象类:
在上篇文章中提到了,如果使用关键字修饰一个成员函数,则这个成员函数被称为虚函数。此处,针对虚函数进行扩展,如果在虚函数的声明后面加上,则这个函数被称为纯虚函数。包含纯虚函数的类又叫抽象类,其特点是不能初始化出对象。即使是子类继承这个类,同样也不能初始化出对象。只有认为对纯虚函数进行重写,才能初始化出一个对象。
给定一个抽象类及其子类如下:
//抽象类
class Person
{
public:virtual void func() = 0{cout << "Person-func()";}
};class Teacher : public Person
{
public:};class Student : public Person
{
public:};
如果向初始化出这三个类的对象,即:
int main()
{Person p;Student s;Teacher t;
}
此时编译器报错如下:
如果对子类中继承父类中的纯虚函数进行重写,即:
class Teacher : public Person
{
public:virtual void func(){cout << "Teacher-func()" << endl;}
};class Student : public Person
{
public:virtual void func(){cout << "Student-func()" << endl;}
};
此时再去分别初始化两个子类的对象,即:
int main()
{Student s;s.func();Teacher t;t.func();
}
代码可以正常运行,且运行结果如下:
2. 再谈虚表:
在之前基础的文章中提到了,在构造函数中,存在初始化列表,初始化列表初始化成员变量的顺序并不是根据初始化列表的顺序,而是根据成员变量声明的顺序。对于虚函数,其也符合这个特性。具体可以用下面的代码进行证明:
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}
通过监视窗口,查看对象中虚表:
可以看到,虚函数在虚表中存放的顺序,正是虚函数在类中声明的顺序。对于这一点,也同样可以在内存窗口中进行查看。
从图中不难发现,对象中的第一个地址,恰好对应了虚表指针的地址。此时再查看虚表中的内容,即:
不难看出,再内存窗口中,第二,第三条地址分别对应了虚表中两个虚函数的地址。
而对于子类,其生成的对象中的内容如下:
对于子类对象的内容,可以分为两个部分,一是从父类中继承的内容,二是子类中自己的成员变量以及函数。在监视窗口中,可以看到子类继承了父类的虚表,并且对其中进行重写的虚函数的地址进行了覆盖。但是需要注意,在子类中,并不存在自己的虚表 。对于子类虚表中的函数指针如下:
在上面给出的图片中可以看出,蓝线连接的两个地址分别是父类、子类中的虚函数,但是因为这个函数在子类中发生了重写,因此,父类,子类中这两个虚函数的地址并不相同。
而对于紫线连接的两个虚函数,由于虚函数并未在子类中发生虚函数的重写,因此,父类,子类中俩个虚函数的地址相同。
如果对于子类,再添加一个虚函数,例如:
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}virtual void Func(){;}
private:int _d = 2;
};
此时,在监视窗口中进行查看,子类对象的虚表中并没有出现新的虚函数的函数指针,但是在内存窗口中,却出现了一条新的地址,对于这个新的地址,一般认为就是子类中新加入的虚函数。至于具体的验证,将在文章后面给出。
(注:为了方便演示,下面的代码在,即位环境下运行)
在之前基础关于内存管理的文章中(C++(9)——内存管理-CSDN博客 )提到了系统根据不同的需求,将内存划分为不同的部分,具体如下:
1.栈:用于存储非全局、非静态的局部变量,函数参数,返回值等等
2.堆:用于程序运行时的内存的动态开辟
3.数据段(静态区):用于存储全局变量和静态变量
4.代码段(常量区):可执行代码\只读常量
在给出了上述概念后,文章将探讨一个 问题,即:虚表指针是存储在什么地方的。
为了方便测试,首先给出上面四个类型变量的地址,即:
int i = 1;//栈int* p = new int;//堆static int j = 0;//数据段(静态区)const char* p2 = "xxxxxxx";//代码段(常量区)printf("栈=%p\n", &i);printf("堆=%p\n", p);printf("静态区=%p\n", &j);printf("常量区=%p\n",p2);
打印结果如下:
对于如何获取虚表指针,本文提供一种方法:由于虚表指针存储在一个类的前四个字节,因此,只需要初始化出一个该类的对象,首先获取这个对象的指针,在将这个指针强转成类型,即可获取虚表指针,具体代码如下:
Base* B = &b;Derive* D = &d;printf("B=%p\n", *(int*)B);printf("D=%p\n", *(int*)D);
打印结果如下:
从上述区段以及两个虚表指针的指针对比来看,虚表指针应该存储在常量区,也就是代码段。
上面给出了如何获取虚表指针的存储地址,下面给出虚表中,如何获取虚表中存储各个虚函数的指针,具体方法如下:
typedef void(*VF_PTR)();
void PrintVF(VF_PTR* vf)
{for (size_t i = 0; vf[i] != nullptr; i++){printf("[%d] :%p", i, vf[i]);}
}
PrintVF((VF_PTR*) * (int*)&d);
打印结果如下:
如果在获取了上述指针后,直接调用这些函数指针,便可知道上述 获取的地址是否是类中的虚函数,即:
typedef void(*VF_PTR)();
void PrintVF(VF_PTR* vf)
{for (size_t i = 0; vf[i] != nullptr; i++){printf("[%d] :%p", i, vf[i]);VF_PTR f = vf[i];f();}
}
打印结果如下:
通过这个例子可以看出,虽然在上面添加新的虚函数时,在子类的虚表中并没有看到这个函数的地址,但是在次数,照样可以通过函数指针调用这个函数,这也间接证明了其实添加到了子类中,只是在监视窗口不可见。
3. 多继承中的虚函数表:
给定代码如下:
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;
};int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;return 0;
}
在上面给出的代码中,存在三个类,其中被集成到了中,由于最先被继承到子类中,因此,可以认为,父类成员在子类的空间中的位置是最靠前的。对于&,表示取对象的首地址,由于父类成员在空间中位置是最靠前的,因此,理论上&。而对于,由于其在后继承,因此相对于是靠后的,因此,在子类中,存在着两张虚表,这两个虚表分别有着自己独立的地址。在监视窗口中,同样可以证明这一点:
而对于中的虚函数,为了验证是存储在哪个虚表中的,可以用下面的代码进行检验:
PrintVF((VF_PTR*)*(int*)p1);
对于中虚表中存储的函数指针打印结果如下:
下面打印中虚表中的函数指针:
由此证明,子类中的虚函数是存储在子类继承并且进行覆盖的中的虚表。