从静态多态、动态多态到虚函数表、虚函数指针
多态(Polymorphism)是面向对象编程中的一个重要概念,它允许不同类的对象对同一消息做出不同的响应。多态性使得可以使用统一的接口来操作不同类的对象,从而提高了代码的灵活性和可扩展性。
一、多态的表现形式
1. 静态多态(编译时多态)
静态多态主要通过函数重载、运算符重载以及模板来实现。通过不同的参数列表、泛型类来选择合适的函数。
重载
#include <iostream>
void print(int a) {
std::cout << "Integer: " << a << std::endl;
}
void print(double a) {
std::cout << "Double: " << a << std::endl;
}
int main() {
print(10); // 调用 void print(int)
print(10.5); // 调用 void print(double)
return 0;
}
模板
#include <iostream>
template <typename T>
void print(T a) {
std::cout << "Value: " << a << std::endl;
}
int main() {
print(10); // 生成 void print<int>(int)
print(10.5); // 生成 void print<double>(double)
return 0;
}
2. 动态多态(运行时多态)
动态多态主要由虚函数和继承来实现,根据对象的实际类型来调用相应的函数。
#include <iostream>
class Base {
public:
virtual void print() const { // 如果这里将virtual注释掉,下面两个都会输出"Base"
std::cout << "Base" << std::endl;
}
virtual ~Base() {} // 虚析构函数
};
class Derived : public Base {
public:
void print() const override {
std::cout << "Derived" << std::endl;
}
};
int main() {
Base* basePtr = new Base();
Base* derivedPtr = new Derived();
basePtr->print(); // 输出 "Base"
derivedPtr->print(); // 输出 "Derived"
delete basePtr;
delete derivedPtr;
return 0;
}
上面的代码中又提到了把virtual注释掉的情况,这涉及到了 “静态类型” 和 “动态类型” 。在这一部分结束之后会讲到。
二、虚函数表、虚函数指针
虚函数通过运行时的动态绑定,来实现在子类中重写基类的函数。虚函数的原理可以通过虚函数表、虚函数指针来解释。
1. VTable和vptr
·每个包含虚函数的类,都有一个虚函数表VTable,是一个指向函数指针的数组。
·每个对象创建之后,会有一个虚函数指针vptr,指向类的虚函数表VTable。
·当调用虚函数时,会通过vptr查找VTable,然后调用对应的函数。
2. 构造函数不能是虚函数
在对象创建时,编译器会给对象的vptr赋值,然后再调用构造函数,如果构造函数是虚函数,此时就陷入了死循环。
3. 析构函数可以是虚函数
通过将基类的析构函数声明为虚函数,可以确保在通过基类指针删除子类对象时,调用到子类的析构函数,合适地释放资源。
#include <iostream>
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 先调用 Derived 的析构函数,然后再调用 Base 的析构函数
return 0;
}
三、静态类型、动态类型
静态类型:是指对象在声明时的类型,在编译期已既定。
动态类型:一个指针、引用目前指向的对象的类型,在运行时确定的。
再来看我们刚才的代码。
在函数调用时,虚函数会根据动态类型来调用,而普通函数就通过静态类型。
#include <iostream>
class Base {
public:
/*virtual*/ void print() const { // 将virtual注释掉,下面两个都会输出"Base"
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
void print() const /*override*/ { // override和上面的virtual对应
std::cout << "Derived" << std::endl;
}
};
int main() {
Base* basePtr = new Base(); // 静态类型Base,动态类型Base
Base* derivedPtr = new Derived(); // 静态类型Base,动态类型Derived
Derived* thirdPtr = new Derived(); // 静态类型Derived,动态类型Derived
basePtr->print(); // 输出 "Base"
derivedPtr->print(); // 输出 "Base"
thirdPtr->print(); // 输出 "Derived"
delete basePtr;
delete derivedPtr;
delete thirdPtr;
return 0;
}
四、static_cast和dynamic_cast的安全与否
1. static_cast
static_cast是一种显式类型转换,主要用于已知的类型转换。
- 向上转型(从派生类指针或引用转换为基类指针或引用)是安全的,因为派生类对象可以被视为基类对象的一个特例。
- 基本类型转换(如 int 到 double)也是安全的。
- 不进行运行时检查,因此在某些情况下可能会导致未定义行为,特别是当进行向下转型时。
2. dynamic_cast
dynamic_cast是一种运行时类型检查的类型转换,主要用于多态类型之间的转换。
- 向上转型(从派生类指针或引用转换为基类指针或引用)是安全的。
- 向下转型(从基类指针或引用转换为派生类指针或引用)是安全的,因为它会在运行时检查类型的有效性。
- 运行时检查类型安全,如果转换不成功,会返回 nullptr(对于指针)或抛出 std::bad_cast 异常(对于引用)。