首先要明确,当通过指针访问类的成员函数时:
1、如果该成员函数是非虚函数,那么编译器会根据指针的类型来找到该函数。即,该指针的类型是哪个类就去调用哪个类的成员函数。
2、如果该成员函数是虚函数,并且派生类中有同类型的成员函数形成覆盖关系,那么编译器会根据指针的指向来找到该函数。即,该指向指向哪个类就去调用哪个类的成员函数。这就是多态的本质。
编译器能够根据指针的指向来找到成员函数,是因为在创建对象时额外添加了一个虚函数表。
如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这里的数组就是虚函数表(Virtual function table),简写为vtable。
我们来看下面的代码:
#ifndef VTABLE_H
#define VTABLE_H
#include <iostream>
#include <string>
using namespace std;
class People{
public:
People(string name, int age);
string m_name;
int m_age;
virtual void display();
virtual void eating();
};
People::People(string name, int age): m_name(name), m_age(age) {}
void People::display() {
cout << "People:: name = " << m_name << " age = " << m_age << endl;
}
void People::eating() {
cout << "People:: eating..." << endl;
}
class Student: public People{
public:
Student(string name, int age, double score);
double m_score;
virtual void display();
virtual void examing();
};
Student::Student(string name, int age, double score):People(name, age), m_score(score) {}
void Student::display() {
cout << "Student:: name = " << m_name << " age = " << m_age << " score = " << m_score << endl;
}
void Student::examing() {
cout << "Student:: examing..." << endl;
}
class Senior:public Student{
public:
Senior(string name, int age, double score, bool hasJob);
bool m_hasJob;
virtual void display();
virtual void partying();
};
Senior::Senior(string name, int age, double score, bool hasJob):Student(name, age, score), m_hasJob(hasJob) {}
void Senior::display() {
cout << "Senior:: name = " << m_name << " age = " << m_age << " score = " << m_score;
if(m_hasJob) cout << " is hasJob." << endl;
else cout << "no hasJob." <<endl;
}
void Senior::partying(){
cout << "Senior:: partying..." << endl;
}
#endif
各个类的内存模型如下所示:
可以看到,基类的虚函数在虚函数表中的位置是固定的,派生类新增的虚函数添加到虚函数表的最后,如果派生类中有跟基类中同类型的虚函数且形成覆盖关系,那么新增的这个虚函数会在虚函数表中替换掉基类中原有的虚函数,这样具有覆盖关系的虚函数只会在虚函数表中出现一次。编译器就是根据这个表来找到指针指向的对象的成员函数的。