开始新的学习之前,我们先通过一段涉及继承、多态的 代码来回忆、加深理解。
Animal作为基类,我们要给每种动物实例化出sound()的模块,因为Animal在实际意义上没什么好实例化的,所以设计成抽象类。
class Animal
{
public:
virtual void sound() const = 0;//后面重写的函数都必须用const修饰
};
class Cat : public Animal
{
public:
virtual void sound() const
{
cout << "喵喵" << endl;
}
};
class Dog : public Animal
{
public:
virtual void sound() const
{
cout << "汪汪" << endl;
}
};
void AnimalSound(const Animal& anm)//Animal不能实例化对象,但是可以生成指针或者引用
{
anm.sound();
}
虽然Animal不能实例化出对象,但是可以声明出一个抽象类的引用,来作为施展多态魔法的条件。这里的Animal就是作为一个像工具人一样的类,本身就不打算实例一个Animal,因为必须要指定出具体是什么动物之后,sound()这个函数才有意义。
1. 虚函数表
上一文的最后一个例题提到了虚函数表指针(_vfptr),虚函数表是一个指针数组,虚函数的地址都被放进了虚函数表,虚函数表也被简称为虚表。
关于虚函数、虚函数表、虚函数表指针的关系,我们有以下说明:
虚函数和其他成员函数一样,都是存在代码段的。
可通过比较虚拟函数和一般函数的地址差距:
三个函数的地址:
打印地址可发现,Func1 Func2 Func3的位置还是很接近的,都存在代码段。
而虚表的地址不一定,比如在vs下虚表就是存在常量区的,而虚表的指针才是存在对象中的。
2. 多态原理
再来观察虚函数表的存在:
Func1和Func2作为虚函数,其函数指针会被存进虚函数表;反之编译器也能通过
但是Func3没有在虚函数表中,而是存在符号表中。
Base对象作为基类,设其子类叫Derive,并且在Derive中只重写了Func1 , 没有重写Func2;并且我们发现:
子类的虚表和父类的虚表不是一个虚表,调用函数时只管去对应的虚表找出新的Func1
所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。派生类和基类的Func1地址不一样,而Func2的地址是一样的,因为Func2没有重写。
派生类的虚表中会放没有重写的虚函数地址(与基类中的一样)和自己重写出来的虚函数地址。
普通函数是在编译或者链接(声明定义分离)时,根据函数名修饰规则,通过符号表找到函数的地址。
因此,我们也称多态为动态绑定,
在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数;而 在程序编译期间确定了程序的行为 , 也称为静态多态 ,比如:函数重载
然后我们也可以尝试去理解为什么需要一个父类的引用或者指针就能调用对应的内容:
派生类本来就是包含基类的,但是两种类里面的虚表不同,Base对象直接通过指针和引用访问,派生类也是直接使用父类的指针引用才能访问虚函数表。
这样底层逻辑就通了,不管是基类还是派生类,都是通过同一类型的指针去访问虚函数表调用函数的。
同一个类型的变量共享同一个虚表。
常见易错:
类域之后直接用类域中的,不会再去调用多态。
比如下图就不是运行绑定:
复习:
虚函数和普通函数一样,都是存在代码段的。
不同的是,虚函数的函数指针会被放在虚表中的。
类里面存的是虚表的地址。而虚表根据平台的不同可能存在静态区等,C++没有严格规定。
我们通过以下方式(指针和指针之间可以强转)得到虚表的地址:
或者再创建几个不同存储位置的变量,打印不同类型的变量地址,比较大小:
对象是在栈中,虚表被处理在常量区,虚表指针在对象中所以也在栈中,虚函数在代码段。
3. 静态多态
函数重载和模版也算是静态多态,在编译时就能确定的多态
练习:
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
考察的是切割部分的知识,在多继承时,谁先继承谁在前面,p1会被切割为只管d的Base1部分,p2会被切割为只管d的Base2部分,p3则是管理整个d的部分。
故选C