在上篇文章中,简单的介绍了多态中的概念以及其相关原理。本文将针对多态中其他的概念进一步进行介绍,并且更加深入的介绍关于多态的相关原理。
目录
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);
对于中虚表中存储的函数指针打印结果如下:
下面打印中虚表中的函数指针:
由此证明,子类中的虚函数是存储在子类继承并且进行覆盖的
中的虚表。