多态的概念
静态多态
之前的课程学过的函数重载,或者运算符重载等。
属于早绑定,函数地址在编译阶段会确定下来。
动态多态
派生类和虚函数来实现的动态多态。
属于晚绑定,函数地址在运行时确定。
下面讲解的多态都属于动态多态
class Animal {
public:
void action() {
cout << "Animal action" << endl;
}
};
class Cat : public Animal {
public:
void action(){
cout << "Cat action" << endl;
}
};
//那么会执行Cat中的action还是Animal中的action呢?
void action(Animal& animal) {
animal.action();
}
int main() {
Cat cat;
//这里传入的cat的子类, 函数action形参animal指向cat;
action(cat);
return 0;
}
即使传入的对象是Cat,但是打印的依然是 Animal action。在编译期间已经确定了函数地址,所以打印的只能是Animal的action。
晚绑定
如何让运行期间再确定函数地址呢?
class Animal {
public:
virtual void action() {
cout << "Animal action" << endl;
}
};
仅需要在函数前面加上virtual,变成虚函数。此时再调用action(cat)时,就会打印Cat action。
总结
静态多态是指函数重载(或操作符重载),属于编译期间函数地址的早绑定(编译期间确定了函数地址)。
动态多态需要满足以下条件:
- 有继承关系
- 子类重写(方法名和参数完全一致)父类的虚函数
- 父类指针或者引用指向子类的对象
早绑定和晚绑定
对于初学者对这两个概念不是很清楚,这里做一下简单的描述。
早绑定
就是编译器把代码编译成可执行文件(或静态库或动态库)时,在编译后的代码中生成的机器码(二进制)就会确定好执行的地址。
通过逆向编译为汇编语言伪代码(代码不是很准确,仅是方便理解)如下:
> 这里红框中很明确的指出来了jmp(跳转到地址 1111),也就是action函数调用被编译器直接翻译成函数地址。
晚绑定
编译器无法确定函数地址,需要运行时才可以确定。
> 可以看到红框中的jmp v2,v2是一个变量,只有运行时才给它赋值,所以地址是未知的,所以这里属于晚绑定。
多态的底层原理
class Animal {
public:
void action() {
cout << "Animal action" << endl;
}
};
sizeof(Animal)的大小是多少呢?一个无成员变量的类对象的大小只有一个字节。
class Animal {
public:
virtual void action() {
cout << "Animal action" << endl;
}
};
sizeof(Animal)的大小是多少呢?一个无成员变量的类对象的大小有4个字节。(32位编译器)
类结构
我们使用命令行提示工具,之前的文章已经多次介绍了如何使用。
我们可以看到Animal占用了4个字节,并且有一个成员变量是vfptr(虚函数指针virtual function pointer),这个虚函数指针指向了vftable(虚函数表 virtual function table),并且虚函数表中有一个函数地址&Animal::action
class Cat : public Animal {
public:
void action(){
cout << "Cat action" << endl;
}
};
可以看到Cat继承了Animal的vfptr的指针,但是指向的vftable不一样,里面的函数地址是&Cat::action。
结论
这里再回顾一下为什么多态调用的是子类的方法呢?
Animal * animal = new Cat;
animal->action();
这里将会调用cat的action方法,因为animal中的vfptr指针指向的地址是Cat中的vftable,所以调用的时候就会使用Cat中的action。