多态及其原理

多态的构成条件

  1. 必须通过父类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写
  3. inline、静态成员、构造函数不可为虚函数

虚函数的重写

子类中有一个跟父类完全相同的虚函数(返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数。(虚函数同样存在于代码段)

重写父类虚函数时,子类虚函数不加virtual关键字时,也可以构成重写(继承后父类虚函数被继承下来了在子类依旧保持虚函数属性)但不建议这样使用

两个例外

  1. 协变(父类与子类虚函数返回值类型不同)
    父类虚函数返回有继承关系的父类对象的指针或者引用,子类虚函数返回有继承关系子类对象的指针或者引用时,称为协变
  2. 析构函数的重写(子类与父类析构函数的名字不同)
    若父类的析构函数为虚函数,则子类析构函数只要定义,无论是否加virtual,都与父类析构函数构成重写,编译后析构函数的名称统一处理成destructor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student: public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main() {
Person* p1 = new Person;
Person* p2 = new Student;
delete p1; // Person()
delete p2; // Student() ~Person()
return 0;
}

C++11 override和final

函数名字写错无法构成重载,类似这种错误在编译期间是不会报出的,所以C++11提供了override和final两个关键字,可以帮助用户检测是否重写

父类中final:修饰虚函数,表示该虚函数不能再被继承
virtual void Fuc() final {}
子类中override:检查子类虚函数是否重写了父类某个虚函数,如果没有编译会报错
virtual void Fuc() override {}

抽象类

虚函数后面加上 =0,则此函数为纯虚函数。包含纯虚函数的类为抽象类,不能实例化出对象。只有子类继承后重写纯虚函数,才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

多态的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AAA {
public:
virtual void Fuc1() {}
void Fuc2() {}
virtual void Fuc3() { cout << "AAA::Fuc3()" << endl; }
private:
int _a;
};
class BBB: public AAA {
public:
virtual void Fuc3() { cout << "BBB::Fuc3()" << endl; }
private:
int _b;
};
void Test(AAA* a) {
a->Fuc3();
a->Fuc2();
}
int main() {
AAA a;
BBB b;
Test(&b);
return 0;
}

在VS下调试我们发现:
父类a对象中除了_a成员,还多一个_vfptr的void**类型指针在对象前面(前后跟平台有关)这个指针我们叫做虚函数表指针,一个含有虚函数的类中至少有一个虚函数表指针,虚函数的地址要被放到虚函数表中

子类b对象中除了_b成员,也多一个_vfptr指针(两个指针不同)
img1

虚函数表

本质是一个存虚函数指针的指针数组(存在于代码段、编译阶段生成),此数组最后面放了一个nullptr

只有虚函数指针才会放在这个表中(为了实现多态)

Func3完成了重写,所以b的虚表中存的是覆盖的BBB::Func3(重写是语法层的叫法,覆盖是原理层的叫法)

子类将父类中的虚表内容拷贝一份到子类虚表中,如果子类重写了基类中某个虚函数,就用子类自己的虚函数指针覆盖虚表中父类的虚函数指针,子类自己新增加的虚函数按其在子类中的声明次序增加到子类虚表的最后

多继承中的虚函数表

虚表个数: 直接父类的个数
子类新增的虚函数:存放在第一个直接父类的虚表末尾(按声明顺序存放)
如:

Base1: 函数func1(virtual),func2(virtual) 数据b1

Base2: 函数func1(virtual),func2(virtual) 数据b2

Derive: 函数func1(virtual),func3(virtual) 数据d1---->内存模型:
img2

为什么达到多态必须父类对象的指针或引用调用虚函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    a->Fuc3();
// a中存的是b对象的指针,将a移动到eax中
00CF2138 mov eax,dword ptr [a]
// [eax]是取eax值指向的内容,b对象头4个字节(虚表指针)移动到了edx
00CF213B mov edx,dword ptr [eax]
00CF213D mov esi,esp
00CF213F mov ecx,dword ptr [a]
// 由于虚标指针数组第二个是Fuc3函数,通过edx+4找到函数指针,放到eax
00CF2142 mov eax,dword ptr [edx+4]
// call eax中的函数指针,运行时在对象中找函数地址
00CF2145 call eax
00CF2147 cmp esi,esp
00CF2149 call __RTC_CheckEsp (0CF12EEh)
a->Fuc2();
00CF214E mov ecx,dword ptr [a]
// 由于Fuc2不是虚函数,编译时已经从符号表确定了函数地址
00CF2151 call AAA::Fuc2 (0CF1546h)
1
2
3
4
5
6
7
int main() {
BBB b;
b.Fuc3(); // 虽然Fuc3是虚函数但是由对象直接调用,不满足多态直接call地址
// 001D2A4A lea ecx,[b]
// 001D2A4D call BBB::Fuc3 (01D1541h)
return 0;
}

打印虚函数表

VS监视窗口中看不见Func3和Func4,只能自定义打印虚表函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class AAA {
public:
virtual void Fuc1() { cout << "AAA::Fuc1()" << endl; }
virtual void Fuc2() { cout << "AAA::Fuc2()" << endl; }
private:
int _a;
};
class BBB: public AAA {
public:
virtual void Fuc3() { cout << "BBB::Fuc3()" << endl; }
virtual void Fuc4() { cout << "BBB::Fuc4()" << endl; }
private:
int _b;
};
void PrintVTable(void(**vTable)()) {
cout << "虚表地址:" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i) {
// 可能死循环,因为编译器有时在虚表最后面没有放nullptr,生成->清理解决方案,编译即可
cout << " 第" << i << "个虚函数地址为:0x" << vTable[i] << endl;
vTable[i]();
}
}
int main() {
BBB b;
// VS下对象模型前四个字节是虚标指针,所以强转成int*在解引用去除前四个字节
// 再强转成void(**)()类型的函数指针
void(**vTableb)() = (void(**)())(*(int*)&b);
PrintVTable(vTableb);
return 0;
}

多继承子类虚函数表访问方法

如:
Base1: 函数func1,func2(virtual) 数据b1

Base2: 函数func1,func2(virtual) 数据b2

Derive: 函数func1,func3(virtual) 数据d1

访问Derive中Base2的虚表

1
2
3
4
5
6
7
8
9
Derive d;
// 方法一
// 取出d的地址,强转成 char* 加上Base1大小个字节
// 强转成 int* 再解引用取出前四个字节,其内容就是虚表指针
PrintVTable((void(**)())(*(int*)((char*)&d + sizeof(Base1))));
// 方法二
// 利用切片操作
Base2* pd = &d;
PrintVTable((void(**)())(*(int*)pd));