多态是由虚函数实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。
如果一个类中包含虚函数(virtual修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。如下图:
这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表。
注:对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表
原始基类虚函数表
下图是原始基类的对象,可以看到虚指针在地址的最前面,指向基类的虚函数表(假设基类定义了3个虚函数)
单继承
1、无重写基类虚函数
假设现在派生类继承基类,并且重新定义了3个虚函数,派生类会自己产生一个兼容基类虚函数表的属于自己的虚函数表。
Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive类的虚函数表,派生类新增的虚函数置于虚函数表的后面,并按声明顺序存放。
2、重写基类虚函数
现在派生类重写基类的x函数,可以看到这个派生类构建自己的虚函数表的时候,修改了base::x()这一项,指向了自己的虚函数。
总结如下:
- 一个类中的某个方法被声明为虚函数,则它将放在虚函数表中。
- 当一个类继承了另一个类,就会继承它的虚函数表,虚函数表中所包含的函数,如果在子类中有重写,则指向当前重写的实现,否则指向基类实现。若在子类中定义了新的虚函数,则该虚函数指针在虚函数表的后面。
- 在继承或多级继承中,要用一个祖先类的指针调用一个后代类实例的方法,若想体现出多态,则必须在该祖先类中就将需要的方法声明为虚函数,否则虽然后代类的虚函数表中有这个方法在后代类中的实现,但对祖先类指针的方法调用依然是早绑定的。
多继承(Derived ::public Base1,public Base2)
这个派生类多重继承了两个基类base1,base2,因此它有两个虚函数表。
它的对象会有多个虚指针(据说和编译器相关),指向不同的虚函数表。通过某个类型的指针访问某个成员时,编译器只是根据类型的定义查找这个成员所在偏移量,用这个偏移量获取成员。
总结如下:对于多继承的情况,子类会包含多个基类的内存结构,包括多个虚函数表,若子类中重写了基类种被定义为虚函数的方法,则虚函数表中的函数指针指向子类的实现,否则指向基类的实现。
棱形继承
#include <iostream>
using namespace std;
class Animal {
public:
int name;
virtual void breathe() {
cout << "Animal breathe" << endl;
}
};
class LandAnimal: public Animal {
public:
int numLegs;
virtual void run() {
cout << "Land animal run" << endl;
}
};
class Mammal: public Animal {
public:
int numBreasts;
virtual void milk() {
cout << "Mammal milk" << endl;
}
};
class Human: public Mammal, public LandAnimal {
public:
int race;
void milk() {
cout << "Human milk" << endl;
}
void run() {
cout << "Human run" << endl;
}
void eat() {
cout << "Human eat" << endl;
}
};
int main(void) {
Human human;
cout << "用LandAnimal指针调用Human实例的方法" << endl;
LandAnimal *laPtr = NULL;
laPtr = &human;
laPtr->run();
cout << "用Mammal指针调用Human实例的方法" << endl;
Mammal *mPtr = NULL;
mPtr = &human;
mPtr->milk();
cout << "用Animal指针调用Human实例的方法" << endl;
Animal *aPtr = NULL;
aPtr = &human; // error: base class "Animal" is ambiguous
return 0;
}
当我们让Animal指针指向human实例时,编译会报错,因为Mammal类和LandAnimal类都继承自Animal类,它们的一些成员变量和方法是相同的。如果用Animal指针指向Human类的实例,则对于共同的成员变量和方法,编译器无法判断是要使用Mammal类中的还是使用LandAnimal类中的,因此编译出错。
这时,我们需要用到虚继承。我们在继承的时候,加上virutal关键字,使LandAnimal类和Mammal类虚继承Animal类。
虚继承
#include <iostream>
using namespace std;
class Animal {
public:
int name;
virtual void breathe() {
cout << "Animal breathe" << endl;
}
};
class LandAnimal: virtual public Animal {
public:
int numLegs;
virtual void run() {
cout << "Land animal run" << endl;
}
};
class Mammal: virtual public Animal {
public:
int numBreasts;
virtual void milk() {
cout << "Mammal milk" << endl;
}
};
class Human: public Mammal, public LandAnimal {
public:
int race;
void breathe() {
cout << "Human breathe" << endl;
}
void milk() {
cout << "Human milk" << endl;
}
void run() {
cout << "Human run" << endl;
}
void eat() {
cout << "Human eat" << endl;
}
};
int main(void) {
Human human;
cout << "用LandAnimal指针调用Human实例的方法" << endl;
LandAnimal *laPtr = NULL;
laPtr = &human;
laPtr->run();
cout << "用Mammal指针调用Human实例的方法" << endl;
Mammal *mPtr = NULL;
mPtr = &human;
mPtr->milk();
cout << "用Animal指针调用Human实例的方法" << endl;
Animal *aPtr = NULL;
aPtr = &human;
aPtr->breathe();
return 0;
}
运行结果如下:
用LandAnimal指针调用Human实例的方法
Human run
用Mammal指针调用Human实例的方法
Human milk
用Animal指针调用Human实例的方法
Human breathe
一个子类虚继承自另一个基类,它不再像普通继承那样直接拥有一份基类的内存结构,而是加了一个虚基类表指针vbptr指向虚基类表,虚基类表每项记录了被继承的虚基类子对象相对于虚基类表指针的偏移量。这样,当出现类似这里的菱形继承时,基类Animal在子类Human中出现一次,子类Human所包含的Mammal类和LandAnimal类各有一个虚表指针指向虚基类。从而避免了菱形继承时的冲突。
参考链接:
1、https://www.cnblogs.com/LUO77/p/5771237.html
2、https://www.jianshu.com/p/02183498a2c2