彻底搞懂C++多态
基础
多态的条件
- 有继承关系;
- 有虚函数;
- 父类指针或引用指向子类对象;
哪些函数不能是虚函数?
-
构造函数:
- 原因:虚函数表指针在构造函数中初始化。
-
静态成员函数:
- 原因:静态成员函数属于类,不属于类对象。而多态的机制和类对象息息相关。
-
内联函数:
- 原因:在编译期间,内联函数就会展开。
关于虚函数表指针和虚函数表
- 虚函数表在编译期间就确定好了。
- 虚函数表指针在构造函数中初始化。(运行时动态绑定)
面试问题
1. 纯虚函数在虚函数表中的形式
- https://blog.csdn.net/qq_31788759/article/details/106225916
- 有虚函数表项,但是可能为nullptr,只是起到占位作用,等待子类去重写它。
2. 从汇编角度解释虚函数机制
成员函数的地址是编译期间就确定了。所以我们在调用普通成员函数的时候,可以直接call 函数地址,实现函数调用。
而虚函数的话,有一套虚函数的调用机制。虚函数表的地址在编译期间就确认了,但是虚函数表指针是在类的构造函数中初始化的,和类对象息息相关,存在于类对象的开始8字节处。
在汇编的表示形式就是先取出对象的开始8个字节(也就是虚函数表指针),然后根据用户调用的是哪个虚函数,就加上偏移取到对应的虚函数然后调用。
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Animal
{
public:
Animal(int age)
: m_age(age)
{
}
virtual void speak()
{
}
virtual void eatFood()
{
}
virtual void sleep()
{
}
void printAge()
{
cout << m_age << endl;
}
public:
int m_age;
};
class Dog : public Animal
{
public:
Dog(int age)
: Animal(age)
{
}
void speak() override
{
cout << "汪汪汪\n";
}
void eatFood() override
{
cout << "吃骨头\n";
}
void sleep() override
{
cout << "睡20分钟\n";
}
};
int main()
{
Animal *dogPtr = new Dog(10);
dogPtr->printAge();
dogPtr->speak(); // 0x0
dogPtr->eatFood(); // 0x8
dogPtr->sleep(); // 0x10
}
- 普通函数:
► 0x555555555214 <main+43> mov rax, qword ptr [rbp - 0x18]
0x555555555218 <main+47> mov rdi, rax
0x55555555521b <main+50> call Animal::printAge() <Animal::printAge()>
In file: /home/ubuntu/GithubFile/Code/CPP/objectmodel/virtual_function_test5.cpp
65
66 int main()
67 {
68 Animal *dogPtr = new Dog(10);
► 69 dogPtr->printAge();
70 dogPtr->speak(); // 0x0
71 dogPtr->eatFood(); // 0x8
72 dogPtr->sleep(); // 0x10
73 }
- 虚函数表的样子:
pwndbg> info vtbl dogPtr
vtable for 'Animal' @ 0x555555557d18 (subobject @ 0x55555556aeb0):
[0]: 0x5555555553a2 <Dog::speak()>
[1]: 0x5555555553c8 <Dog::eatFood()>
[2]: 0x5555555553ee <Dog::sleep()>
- 虚函数speak:
► 0x555555555220 <main+55> mov rax, qword ptr [rbp - 0x18] <std::cout@@GLIBCXX_3.4>
0x555555555224 <main+59> mov rax, qword ptr [rax] # 取到虚函数表指针的地址
0x555555555227 <main+62> mov rdx, qword ptr [rax] # 取到需要调用的虚函数speak的地址放到rdx中
0x55555555522a <main+65> mov rax, qword ptr [rbp - 0x18]
0x55555555522e <main+69> mov rdi, rax
0x555555555231 <main+72> call rdx
In file: /home/ubuntu/GithubFile/Code/CPP/objectmodel/virtual_function_test5.cpp
65
66 int main()
67 {
68 Animal *dogPtr = new Dog(10);
69 dogPtr->printAge();
► 70 dogPtr->speak(); // 0x0
71 dogPtr->eatFood(); // 0x8
72 dogPtr->sleep(); // 0x10
73 }
pwndbg> p &dogPtr
$1 = (Animal **) 0x7fffffffe4d8
pwndbg> p dogPtr
$2 = (Dog *) 0x55555556aeb0
pwndbg> p $rbp -0x18
$3 = (void *) 0x7fffffffe4d8
pwndbg> info line *0x5555555553a2
Line 50 of "virtual_function_test5.cpp" starts at address 0x5555555553a2 <Dog::speak()> and ends at 0x5555555553b2 <Dog::speak()+16>.
dogPtr的地址是0x7fffffffe4d8(栈上的局部变量)、保存的内容是0x55555556aeb0(堆上申请的Dog地址最开始位置)。
rbp - 0x18就是dogPtr的地址,[rbp - 0x18]就是0x55555556aeb0(堆上申请的Dog地址最开始位置)。
- 虚函数eatFood:
0x555555555233 <main+74> mov rax, qword ptr [rbp - 0x18]
► 0x555555555237 <main+78> mov rax, qword ptr [rax] # 取到虚函数表指针的地址
0x55555555523a <main+81> add rax, 8 # eatFood在虚函数表中的位置偏移是8
0x55555555523e <main+85> mov rdx, qword ptr [rax] # 取eatFood的地址到rdx中
0x555555555241 <main+88> mov rax, qword ptr [rbp - 0x18]
0x555555555245 <main+92> mov rdi, rax
0x555555555248 <main+95> call rdx
In file: /home/ubuntu/GithubFile/Code/CPP/objectmodel/virtual_function_test5.cpp
66 int main()
67 {
68 Animal *dogPtr = new Dog(10);
69 dogPtr->printAge();
70 dogPtr->speak(); // 0x0
► 71 dogPtr->eatFood(); // 0x8
72 dogPtr->sleep(); // 0x10
73 }
- 虚函数sleep:
0x55555555524a <main+97> mov rax, qword ptr [rbp - 0x18]
► 0x55555555524e <main+101> mov rax, qword ptr [rax] # 取到虚函数表指针的地址
0x555555555251 <main+104> add rax, 0x10 # sleep在虚函数表中的位置偏移是8
0x555555555255 <main+108> mov rdx, qword ptr [rax] # 取sleep的地址到rdx中
0x555555555258 <main+111> mov rax, qword ptr [rbp - 0x18]
0x55555555525c <main+115> mov rdi, rax
0x55555555525f <main+118> call rdx
In file: /home/ubuntu/GithubFile/Code/CPP/objectmodel/virtual_function_test5.cpp
67 {
68 Animal *dogPtr = new Dog(10);
69 dogPtr->printAge();
70 dogPtr->speak(); // 0x0
71 dogPtr->eatFood(); // 0x8
► 72 dogPtr->sleep(); // 0x10
73 }
3. 函数重载和多态的区别
- 函数重载主要是函数签名不同(编译期间确定);
- 多态主要是虚函数机制;
C++的函数签名
-
函数签名组成部分:
- 函数名;
- 参数类型;
- 参数个数;
- 参数顺序;
- 它所在的类和命名空间;
-
举例:
// g++ -c function_signature.cpp -o function_signature.o // nm function_signature.o #include <iostream> using namespace std; // _Z8sayHellov // _Z 前缀:表示这是一个修饰后的名称,通常是为了标识这个名称是 C++ 编译所生成的。 // 8sayHello:数字 8 表示后面跟随的名称的长度(8 个字符),即 sayHello // v 后缀:表示这个函数没有参数(Void),在 C++ 中,v 是表示没有参数的标识。 void sayHello() { cout << "hello\n"; } // _Z8sayHelloi void sayHello(int a) { cout << "hello " << a << endl; } class Books { public: // _ZN5BooksC1Ev 构造函数 // _ZN5BooksC2Ev 默认构造函数 Books() { } // _ZN5BooksD1Ev 析构函数 // _ZN5BooksD2Ev 默认析构函数 ~Books() { } // _ZN5Books11printNumberEv void printNumber() { cout << m_cnt << endl; } // _ZN5Books12printAddressEv virtual void printAddress() { cout << "Chengdu\n"; } public: int m_cnt; }; int main() { sayHello(); sayHello(2); Books books; books.printNumber(); }