1.引言
多态是面向对象编程的一种特性,简单来说就是调用同名函数,根据具体的对象类型、或者参数类型,执行不同的方法实现。在C++中多态有两种实现方式,即静态多态(编译时多态)和动态多态(运行时多态)。
2.静态多态
2.1.概念
也称编译期多态。静态多态是指在编译时就能够确定函数调用的具体实现,主要包括函数重载、模板和隐藏3种实现方式。函数重载是指在同一个作用域内,名称相同的函数参数类型或参数个数不同,从而可以根据调用时提供的实参来确定实际调用的函数。模板多态是指编译时进行代码生成,根据传递的模板参数不同,生成不同的代码。隐藏是指在子类中定义父类同名同参数函数。
2.2.实现方法
C++中的静态多态可以通过函数重载、模板以及隐藏实现,以下分别展示各自的使用例子。
一、通过模板实现静态多态:
#include <iostream>
template<typename T>
T max(T a, T b){
return a > b ? a : b;
}
int main()
{
// 输出20,调用max<int>函数
std::cout << max(10, 20) << std::endl;
//输出3.14,调用max<double>函数
std::cout << max(1.2, 3.14) << std::endl;
return 0;
}
二、通过函数重载实现静态多态:
#include <iostream>
void print(int num){
std::cout << "The integer is " << num << std::endl;
}
void print(double num){
std::cout << "The double is " << num << std::endl;
}
int main(){
print(10); // 调用第一个print函数
print(3.14); // 调用第二个print函数
return 0;
}
三、通过隐藏实现静态多态——在子类中定义父类同名同参数函数。
class Rectangle{
public:
void name() {
std::cout<<"矩形"<<std::endl;
}
};
class Circle {
public:
void name() {
std::cout<<"圆形"<<std::endl;
}
};
template<typename T>
void print(T shape){
shape.name();
}
int main() {
print<Rectangle>();//矩形
print<Circle>();//圆形
return 0;
}
2.3.原理
静态多态都是通过name mangling实现的。name mangling是一种编译器技术,用于为程序中的变量和函数生成唯一的内部名称,这个过程涉及改变原始名称的形式,从而避免了变量和函数之间名称的冲突。name mangling规则的具体实现方式因编译器而不同,下面是一些常见编译器的name mangling规则:
- MSVC编译器:在函数名前加上下划线,然后加上函数名,以及加上所有参数的数据类型与长度信息,最终生成修饰过的函数名。例如,函数名
foo(int, float)
会被修饰为_foo@8
。其中8是函数参数的总长度,指示了函数的参数类型。 - G++编译器:以函数名和参数类型组成的字符串作为函数的新名字。例如,函数名
foo(int, float)
会被编译器重命名为_Z3fooif
,其中Z
表示这是一个C++函数,3
表示该函数名长度为3,f
和i
分别表示两个参数的数据类型为float和int。 - Clang编译器:类似于GCC,使用函数名和参数类型生成一个字符串作为新的函数名,不过字符串的格式可能略有不同。
编译前,不同名称空间内的同名函数、同名但参数不同的函数,经过name mangling处理,都有唯一名称,从而能够被区分。
2.4.CRTP
CRTP是Curiously Recurring Template Pattern的缩写,是一种利用继承和模板技术实现的编程模式,用于在编译时实现静态多态,也称为根据类型递归静态多态,它是静态多态使用案例中的一种最佳实践。CRTP通过继承一个模板类,并在模板参数中传递自身类型,来实现静态多态,举个例子:
template<typename T>
class Base {
public:
void foo() { static_cast<T *>(this)->impl_foo(); }
};
class Derived1 : public Base<Derived1> {
public:
void impl_foo() { cout << "Derived1 foo" << endl; }
};
class Derived2 : public Base<Derived2> {
public:
void impl_foo() { cout << "Derived2 foo" << endl; }
};
标准库中的enable_shared_from_this
就是通过CRTP实现的。
3.动态多态
3.1.概念
也称运行时多态。C++中的动态多态是通过虚函数(virtual function)实现的。其基本使用是:在父类中定义一个虚函数,在子类中重写这个虚函数。在程序运行时,通过基类指针或引用指向子类对象,并使用基类对象调用虚函数时,会自动调用子类的重写版本。
3.2.使用方法
以下是一个使用动态多态的例子。假设我们需要实现一个图形类Shape
,包含一个虚函数 say()
用于表面该图形的形状。我们要实现两个派生类 Circle
和 Rectangle
,分别用于表示圆形和矩形,它们重写了基类 Shape
中的虚函数 say()
。
class Shape {
public:
virtual void say() = 0;
};
class Rectangle: public Shape {
public:
void say() {
std::cout<<"矩形"<<std::endl;
}
};
class Circle: public Shape {
public:
void say() {
std::cout<<"圆形"<<std::endl;
}
};
接下来就可以通过基类指针调用同名函数的不同实现了。
int main()
{
Shape *shape;
Rectangle rec;
Circle cir;
shape = &rec;
shape->say();//矩形
shape = ○
shape->say();//圆形
return 0;
}
3.3.原理
C++动态多态是通过虚函数和虚函数表实现的。C++编译器为每一个有虚函数的类生成一个虚函数表,虚函数表是一个指向各个虚函数的函数指针数组,类实例化成对象后,对象首地址处存放有一个指针vptr
指向虚函数表。
虚函数表
类继承的情况下,每个子类以及当前类都会有一个虚函数表。每个子类的虚函数表,会存储子类和父类所有的虚函数地址,如果子类与父类有同名虚函数,则只填子类中的同名虚函数地址。
继承下的虚函数表
当程序运行时,基类指针或引用调用虚函数,会根据对象的实际类型(即运行时类型),选择一个虚函数表,并在虚函数表中查找对应的函数指针,然后调用指向的具体实现代码。
4.总结
总之,多态是面向对象编程的一个重要特性,通过多态可以增强代码的灵活性和可扩展性、提高代码的可复用性、简化代码的设计和维护以及改善代码的可读性和可理解性,包括静态多态和动态多态,两者互有优劣,适用于不同场景。
静态多态 | 动态多态 | |
---|---|---|
编译时确定 | 是 | 否 |
运行时开销 | 较小,不需要虚函数的开销 | 较大,需要查找虚函数表并调用虚函数的开销 |
扩展性 | 不易扩展,需要编译时确定函数实现代码 | 易于扩展,可以在运行时动态添加新的类及其成员函数实现 |
安全性 | 更容易确保类型安全,编译时会检查参数类型是否匹配 | 有可能因为运行时传入的参数类型不匹配而出现未定义的行为或内存安全问题 |
使用简便性 | 需要手动重载函数,代码量较大 | 方便,使用基类指针或引用即可调用派生类的函数,代码量较小 |
动态绑定 | 无法在运行时动态绑定 | 能够在运行时动态绑定 |
在线修改 | 无法在运行时修改函数实现,需要重新编译和部署代码 | 能够在运行时修改函数实现,不需要重新编译和部署代码 |
应用场景 | 适用于已知的固定类型,且需要高效性和类型安全的场景 | 适用于未知的类型、需要灵活性和扩展性的场景 |