前两天去面试,面试官问到多态是如何实现的,以下依次讲解多态基本概念、虚函数表的工作原理,以及利用IDA反编译exe讲解汇编指令来清晰底层实现原理。
多态性(Polymorphism)是面向对象编程中的一个重要概念,它允许对象以多种形式存在。C++中的多态性主要通过继承和虚函数来实现。多态性使得一个基类指针或引用能够指向派生类的对象,并调用派生类的成员函数。这样,程序可以在运行时决定调用哪个函数,从而实现动态绑定。
以下是多态性的主要类型:
- 编译时多态性(静态多态性):通过函数重载和运算符重载实现。
- 运行时多态性(动态多态性):通过虚函数和继承实现。
1. 编译时多态
1.1函数重载
函数重载是指在同一个作用域内,可以有多个同名函数,但它们的参数列表(参数的类型、个数或顺序)不同。编译器会根据调用时传递的参数类型和个数来选择合适的函数版本。
// 重载函数
void Connect(int ip) {
std::cout << "Connect by ip: " << ip << std::endl;
}
void Connect(int ip, int portId) {
std::cout << "Connect by ip and portId:" << ip << portId << std::endl;
}
void Connect(const std::string& deviceName) {
std::cout << "String: " << deviceName << std::endl;
}
int main() {
Connect(10651); // 调用 Connect(int)
Connect(10652, 1); // 调用 Connect(int, int)
Connect("Hello"); // 调用 Connect(const std::string&)
return 0;
}
1.2 模板
模板是C++中实现泛型编程的工具,允许编写与类型无关的代码。模板可以用于函数和类,通过在编译时生成特定类型的实例来实现多态性。
函数模板示例
template <typename T>
void printVector(const std::vector<T>& v) {
for (const T& item : v) {
std::cout << item << std::endl;
}
}
int main() {
printVector(std::vector<int>{1, 2, 3, 4, 5});
printVector(std::vector<double>{1.1, 2.1, 3.2, 4.4, 5.5});
return 0;
}
类模板示例
template <typename T>
class MyClass {
private:
T value;
public:
MyClass(T value) : value(value) {};
T GetValue() const {
return value;
}
};
int main() {
MyClass<int> myClass(10);
std::cout << "int class:" << myClass.GetValue() << std::endl;
MyClass<std::string> myClass2("Hello");
std::cout << "string class:" << myClass2.GetValue() << std::endl;
return 0;
}
编译时多态性(静态多态性)通过函数重载和模板实现,允许在编译阶段确定函数调用的具体实现。与运行时多态性不同,编译时多态性不依赖于继承和虚函数,而是通过编译器在编译时解析和选择合适的函数或模板实例。这种多态性提高了代码的灵活性和可重用性,同时也避免了运行时的开销。
2. 运行时多态
运行时多态性是通过虚函数实现的。虚函数是在基类中使用 virtual 关键字声明的函数。派生类可以重写这些虚函数。当通过基类指针或引用调用虚函数时,实际调用的是派生类的实现。
简单示例:
#include <iostream>
class Base {
public:
virtual void Run() {
std::cout << "Base::Run() called." << std::endl;
}
virtual ~Base() = default; // 虚析构函数确保派生类对象被正确销毁
};
class Sub : public Base {
public:
void Run() override {
std::cout << "Sub::Run() called." << std::endl;
}
};
int main() {
Base* instance = new Sub(); // 基类指针指向派生类对象
instance->Run(); // 调用派生类的 Run() 函数
delete instance; // 删除对象,调用虚析构函数
return 0;
}
2.1 运行时多态性实现
基类指针调用派生类的虚函数是通过虚函数表(Virtual Table,简称vtable)和虚函数指针(Virtual Pointer,简称vptr)机制实现的。下面是这个机制的工作原理:
虚函数表(vtable)
每个包含虚函数的类都有一个虚函数表(vtable),它是一个指针数组,数组中的每个元素指向该类的虚函数实现。对于每个类,编译器会生成一个唯一的vtable。
虚函数指针(vptr)
每个包含虚函数的对象都有一个隐藏的指针(vptr),它指向该对象所属类的vtable。这个vptr在对象创建时由编译器自动设置。
实现步骤:
对象创建:当创建一个包含虚函数的类的对象时,编译器会将该对象的vptr指向该类的vtable。
虚函数调用:当通过基类指针调用虚函数时,在运行时,程序根据 basePtr 实际指向的对象类型查找虚函数表,并调用 其对象的函数。这就是运行时多态的实现。
#include <iostream>
class Base {
public:
virtual void Run() {
std::cout << "Base::Run() called." << std::endl;
}
virtual ~Base() = default; // 虚析构函数确保派生类对象被正确销毁
};
class Sub : public Base {
public:
void Run() override {
std::cout << "Sub::Run() called." << std::endl;
}
};
int main() {
Base* instance = new Sub(); // 基类指针指向派生类对象
instance->Run(); // 调用派生类的 Run() 函数
delete instance; // 删除对象,调用虚析构函数
return 0;
}
输出:
详细解释上述代码:
(1)类定义阶段:
编译器为Base类生成一个vtable,其中包含一个指向Base::Run函数的指针。
编译器为Sub类生成一个vtable,其中包含一个指向Sub::Run函数的指针。
(2)对象创建阶段:
当创建Sub类对象时,编译器将该对象的vptr指向Sub类的vtable。
(3)函数调用阶段:
当通过Base* instance调用Run函数时,instance实际上指向Sub类对象。
运行时程序通过instance的vptr找到Sub类的vtable,并调用其中的Run函数指针,实际调用的是Sub::Run函数。
2.2 查看汇编了解底层实现
可以使用IDA反编译软件打开生成的exe文件(编译优化开的-o2级),可以看到汇编代码如下(从main函数入口查看):
(1)函数开始
.text:0000000140001030 sub rsp, 28h
sub rsp, 28h:调整栈指针,在rsp栈指针寄存器分配 40 字节的栈空间用于局部变量和临时数据。(28h的h表示十六进制的28,即为十进制的40)
(2)动态内存分配
.text:0000000140001034 mov ecx, 8 ; size
.text:0000000140001039 call ??2@YAPEAX_K@Z ; operator new(unsigned __int64)
mov ecx, 8:将 8 存入 ecx寄存器,表示要分配的字节数。
call ??2@YAPEAX_K@Z:调用 operator new,分配 8 字节的内存。返回的Sub对象指针存储在 rax 中。(对应代码中的new Sub()
)
(3) 设置虚函数表指针
.text:000000014000103E lea rcx, ??_7Sub@@6B@ ; const Sub::`vftable'
.text:0000000140001045 mov [rax], rcx
lea rcx, ??_7Sub@@6B@:将 Sub 类的虚函数表地址加载到 rcx 中。
mov [rax], rcx:将虚函数表地址存储到新分配的Sub对象内存的起始位置。
(4)调用虚函数
.text:0000000140001048 mov rcx, rax ; this
.text:000000014000104B mov rax, cs:??_7Sub@@6B@+8 ; const Sub::`vftable'
.text:0000000140001052 call rax ; Sub::Run(void) ; Sub::Run(void)
mov rcx, rax:将Sub对象指针(this 指针)存储到 rcx 中。
mov rax, cs:??_7Sub@@6B@+8:将虚函数表中 Sub::Run 函数的地址加载到 rax寄存器 中。
call rax:调用 Sub::Run 函数。
(5)函数结束
.text:0000000140001054 xor eax, eax
.text:0000000140001056 add rsp, 28h
.text:000000014000105A retn
.text:000000014000105A main endp
xor eax, eax:将 eax寄存器 清零,设置返回值为 0。
add rsp, 28h:恢复栈40字节的空间。
retn:返回调用者。
通过虚函数表和虚函数指针,C++实现了运行时多态性,使得基类指针能够调用派生类的虚函数。