多态
多态是面向对象编程中的一个重要概念,它允许我们使用共同的接口来处理不同类型的对象,从而提高了代码的灵活性和可扩展性。
C++ 多态分类及实现:
- 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
- 子类型多态(Subtype Polymorphism,运行期):虚函数
- 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
- 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换
实现原理
多态的实现原理主要依赖于虚函数表(vtable)和动态绑定。
种类
在C++中,多态分为静态多态和动态多态。
静态多态(编译器/早绑定)
静态多态也称编译时多态,主要通过模板和函数重载实现,在编译时确定函数调用;
动态多态(实现关键在于虚函数表,运行期/晚绑定)
动态多态通过继承和虚函数实现,在运行时确定函数调用。当基类和子类拥有同名同参同返回的方法,且该方法声明为虚方法,当基类对象、指针、引用指向的是派生类的对象的时候,基类对象、指针、引用在调用基类的虚函数,实际上调用的是派生类函数。其几个关键点包括:
- 虚函数
- 虚函数表
- #动态绑定
- 动态联编
虚函数
在基类中使用virtual关键字声明的函数称为虚函数。这些函数可以在派生类中被重写。
虚函数表(vtable)
当一个类中声明了虚函数,编译器会为该类生成一个虚函数表(vtable),表中存储了该类所有虚函数的地址。这个表由编译器自动生成和维护。
性质:
- 每个对象都有一个指向虚函数表的指针vptr,这个指针通常位于对象的开头。
- 当通过基类指针或引用调用一个虚函数时,运行时会在对象的vptr指向的虚函数表中查找相应的函数地址并调用。这样,就可以实现根据对象的实际类型来调用相应的函数,即多态。
动态绑定:
在运行时,根据对象的实际类型从虚函数表中查找并调用相应的函数。
动态联编
通过基类指针或引用调用虚函数时,编译时不确定具体调用哪个实现,而是在运行时根据对象的实际类型确定。
优缺点:
- 灵活性和可维护性,通过动态联编,多态允许我们编写与特定实现无关的代码,提高了代码的灵活性和可维护性。
- 额外的空间和时间开销,因为每个有虚函数的对象都需要额外的空间来存储vptr,并且在运行时需要进行虚函数表的查找。
实现方式
C++实现多态的主要方式有:
- 重载(Overloading):通过函数名相同但参数不同的多个函数实现不同行为。在编译时通过参数类型决定调用哪个函数。
void add(int a, int b) { ... }
void add(double a, double b) { ... }
- 重写(Overriding):通过继承让派生类重新实现基类的虚函数。在运行时通过指针/引用的实际类型调用对应的函数。
class Base {
public:
virtual void func() { ... }
};
class Derived :public Base {
public:
virtual void func() { ... }
};
Base* b = new Derived();
b->func(); // Calls Derived::func()
- 编译时多态:通过模板和泛型实现针对不同类型具有不同实现的函数。在编译时通过传入类型决定具体实现。
template <typename T>
void func(T t) { ... }
func(1); // Calls func<int>
func(3.2); // Calls func<double>
- 条件编译:通过#ifdef/#elif等预处理命令针对不同条件编译不同的代码实现产生不同行为的程序。编译时通过定义的宏决定具体实现
#ifdef _WIN32
void func() { ... } // Windows version
#elif __linux__
void func() { ... } // Linux version
#endif
备注:
- 普通函数(非类成员函数)不能是虚函数;
- 静态函数(static)不能是虚函数;
- 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针);
- 内联函数不能是表现多态性时的虚函数(虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生)。
#include <iostream>
using namespace std;
class Base
{
public:
inline virtual void who()
{
cout << "I am Base\n";
}
virtual ~Base() {}
};
class Derived : public Base
{
public:
inline void who() // 不写inline时隐式内联
{
cout << "I am Derived\n";
}
};
int main()
{
// 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。
Base b;
b.who();
// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。
Base *ptr = new Derived();
ptr->who();
// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
delete ptr;
ptr = nullptr;
system("pause");
return 0;
}
###参考链接:
请简述C++多态实现的原理,较全面