C++多态
C++中多态是指同一个接口可以表现出不同的行为,是 C++ 面向对象的主要特征之一,实现方式主要有静态多态和动态多态。
静态多态
静态多态是一种在编译期实现的多态机制,通过函数重载、模板重载、CRTP等方式实现,静态多态的函数调用在编译时确定,无需运行时开销。
基本概念区分
这里有几个很重要的概念,分别是重写(覆盖)、重载与重定义,首先介绍这三个概念。
1、重写(override)
重写也称为覆盖,常用于类中虚函数的重写,表示子类从父类继承而来的虚函数,重写只是对函数体部分有效函数名、返回值、参数列表(包括参数缺省值)都是继承而来的,不能修改,这里体现接口继承的特性。
如果子类重写从基类继承而来的虚函数,就被用来实现多态,当子类重写基类的虚函数时,基类指针或引用可以根据赋给这个指针的不同类型的子类对象来调用子类中的虚函数,实现动态多态。具体内容后面详细讲解。
例如下面的例子:
#include <iostream>
class Base {
public:
virtual void print(int x = 10) {
std::cout << "Base::print(x=" << x << ")" << std::endl;
}
};
class Derived : public Base {
public:
/// 重写虚函数,函数体被替换,但缺省参数仍使用Base::print的定义
void print(int x = 20) override {
std::cout << "Derived::print(x=" << x << ")" << std::endl;
}
};
int main() {
Base* basePtr = new Derived();
Derived* derivedPtr = new Derived();
/// 动态绑定调用Derived::print,但缺省参数由Base::print决定
basePtr->print(); /// 输出: Derived::print(x=10)
/// 静态绑定调用Derived::print,使用Derived::print的缺省参数
derivedPtr->print(); /// 输出: Derived::print(x=20)
/// 显式传参时,缺省参数不起作用
basePtr->print(30); /// 输出: Derived::print(x=30)
delete basePtr;
delete derivedPtr;
return 0;
}
2、函数重载(Function Overloading)
重载是指的在相同的作用域中存在多个同名的函数,这些函数的参数列表不同,编译器根据函数的不同形参列表对同名函数的名称做修饰,就成了不同的函数。
重载要求函数的参数列表必须不同,比如参数类型,参数个数,参数顺序不同,如果仅仅是函数的返回值不同的话无法实现重载。
函数在被C和C++中被编译后的名字也是不一样的,一个函数如:func(),在C语言中被编译后的名字是:_func, 但是在c++中是_func_int_int这一类的名字。
#include <iostream>
/// 1. 参数类型不同
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
/// 2. 参数个数不同
int sum(int a) {
return a;
}
int sum(int a, int b) {
return a + b;
}
int sum(int a, int b, int c) {
return a + b + c;
}
/// 3. 参数顺序不同
void print(int a, char b) {
std::cout << "Int: " << a << ", Char: " << b << std::endl;
}
void print(char a, int b) {
std::cout << "Char: " << a << ", Int: " << b << std::endl;
}
/// 错误示例:仅返回值不同,无法重载
/// double add(int a, int b); // 编译错误:无法与上面的int add(int, int)区分
/// 4. extern "C"演示C和C++编译后函数名差异
extern "C" {
int func(int a, int b) {
return a + b;
}
}
int main() {
/// 调用重载函数
std::cout << add(1, 2) << std::endl; /// 调用int add(int, int)
std::cout << add(1.5, 2.5) << std::endl; /// 调用double add(double, double)
std::cout << sum(1, 2, 3) << std::endl; /// 调用int sum(int, int, int)
print(100, 'A'); /// 调用print(int, char)
print('B', 200); /// 调用print(char, int)
/// C风格函数调用
std::cout << func(3, 4) << std::endl; /// 调用extern "C"的func
return 0;
}
3、重定义
重定义也被称隐藏,是指子类重新定义了父类中同名的非虚函数,导致父类函数在子类作用域内被隐藏。这与重写(Override) 形成对比,重写仅针对虚函数,而重定义适用于非虚函数。
当子类定义了与父类同名但参数列表不同的非虚函数,或完全相同的非虚函数时:
- 父类的同名函数会被隐藏,无法通过子类对象直接访问(可以使用using声明恢复其作用域)。
- 隐藏规则基于函数名,与参数列表、返回值无关。
以下针对不同的情况给出示例:
- 参数列表不同
class Base {
public:
void func(int x) { cout << "Base::func(int)" << endl; }
};
class Derived : public Base {
public:
/// 对父类的非虚函数重定义
void func(double x) { cout << "Derived::func(double)" << endl; } // 隐藏Base::func
};
int main() {
Derived d;
d.func(10); /// 错误:Derived::func(double)隐藏了Base::func(int)
d.func(3.14); /// 调用Derived::func(double)
d.Base::func(10); /// 显式调用Base::func(int)
}
- 参数列表相同但非虚
class Base {
public:
void func() { cout << "Base::func()" << endl; } /// 非虚函数
};
class Derived : public Base {
public:
void func() { cout << "Derived::func()" << endl; } /// 隐藏Base::func
};
int main() {
Derived d;
d.func(); /// 调用Derived::func()
d.Base::func(); /// 显式调用Base::func()
}
重定义的应用场景:
- 接口隔离,子类需要自定义与父类接口同名但功能逻辑不同的函数
- 适配基类接口,通过隐藏基类函数并提供新实现,适配特定需求
- 逐渐重构,在不修改基类的情况下,逐步替换子类的实现逻辑
静态多态实现原理
静态多态(Static Polymorphism)是 C++ 中通过编译期机制实现的多态性,主要基于函数重载、模板和CRTP等技术。其核心原理是在编译时根据类型信息确定具体执行的代码,无需运行时开销。
- 函数重载
函数重载主要功能就如上一节中介绍的一样,这里主要是将函数名和参数类型组合成唯一标识符。并且编译器会按照以下优先级匹配实参与函数参数
- 精确匹配(Exact Match)
- 类型提升(Type Promotion,如
char
→int
) - 标准转换(Standard Conversion,如
int
→double
) - 用户自定义转换
示例:
void print(int x) { cout << "Int: " << x << endl; }
void print(double x) { cout << "Double: " << x << endl; }
print(10); /// 调用 print(int)
print(3.14); /// 调用 print(double)
print('A'); /// 调用 print(int)(char 提升为 int)
- 模板
模板分为函数模板与类模板,主要是通过泛型编程,编译器在实例化时生成具体类型的代码,每个实例化的模板函数是独立的实体。
这里我仅仅给出简单的介绍,这部分大家感兴趣的可以自行了解C++中模板相关完整语法。
模板实例化
使用具体类型调用模板函数时,编译器生成对应版本的代码
template <typename T>
T max(T a, T b) { return (a > b) ? a : b; }
int x = max(5, 10); /// 实例化为 max<int>(int, int)
double y = max(3.14, 2.71); /// 实例化为 max<double>(double, double)
类模板
在我们使用一些通用接口时,使用模板非常有用,例如
template <typename T>
class Vector {
T* data;
public:
Vector(size_t size) : data(new T[size]) {}
T& operator[](size_t i) { return data[i]; }
};
Vector<int> intVec(5); /// 生成 Vector<int> 类型
Vector<double> dblVec(3); /// 生成 Vector<double> 类型
模板特化
为特定类型提供定制的模板实现,覆盖通用模板,编译器优先选择最匹配的特化版本。
- 全特化
template <typename T>
struct IsPointer { static constexpr bool value = false; };
template <>
struct IsPointer<T*> { static constexpr bool value = true; }; /// 全特化
- 偏特化(仅适用类模板)
template <typename T>
struct Container { /* 通用实现 */ };
template <typename T>
struct Container<T*> { /* 指针类型的特化 */ }
- CRTP
CRTP(Curiously Recurring Template Pattern,奇异递归模板模式),其核心思想是让一个类(派生类)继承自以自身为模板参数的基类模板,即利用模板的编译期间实例化特性,将派生类的类型信息注入基类。
通过这种方式,能够在编译期实现代码复用、静态多态(编译期多态)等功能,避免了动态多态(虚函数)的运行时开销。
基本形式
/// 基类模板,模板参数为派生类
template <typename Derived>
class Base {
public:
/// 基类中可以通过static_cast<Derived*>(this)调用派生类的成员
void doSomething() {
/// 调用派生类的实现
static_cast<Derived*>(this)->implementation();
}
};
/// 派生类继承自以自身为参数的基类模板
class Derived : public Base<Derived> {
public:
/// 派生类实现具体逻辑
void implementation() {
/// 具体功能实现
}
};
派生类Derived
继承自Base<Derived>
,基类模板通过static_cast
将自身指针转换为派生类指针,从而调用派生类的成员函数,实现 “基类调用派生类方法” 的效果。
动态多态
动态多态(Dynamic Polymorphism)是 C++ 中通过运行时机制实现的多态性,主要基于虚函数(Virtual Functions)和继承。其核心原理是通过虚函数表(VTable)和虚表指针(VPTR)在运行时确定调用的函数版本,从而实现 “一个接口,多种实现” 的效果
虚函数
虚函数是在基类中使用virtual
关键字声明的函数,允许派生类提供自己的实现版本。当通过基类指针或引用调用虚函数时,实际执行的是对象的动态类型(即实际指向的对象类型)对应的函数版本。
例如:
class Shape {
public:
virtual void draw() { cout << "Shape::draw()" << endl; } // 虚函数
};
class Circle : public Shape {
public:
void draw() override { cout << "Circle::draw()" << endl; } // 重写虚函数
};
class Square : public Shape {
public:
void draw() override { cout << "Square::draw()" << endl; } // 重写虚函数
};
int main() {
Shape* shape1 = new Circle();
Shape* shape2 = new Square();
shape1->draw(); /// 输出:Circle::draw()(动态绑定)
shape2->draw(); /// 输出:Square::draw()(动态绑定)
delete shape1;
delete shape2;
return 0;
}
虚函数表
每个包含虚函数的类都有自己的虚函数表(在编译期就会创建这个虚函数表),虚函数表是一个存储函数指针的静态数组,里边包含这个类所有的虚函数地址,如果派生类没有重写基类虚函数,那么虚函数表中对应条目就是继承而来的基类虚函数地址,如果进行了重写那么就用自己的虚函数地址进行替换。
虚表指针
每个包含虚函数的对象都有一个虚表指针(VPTR),通常位于对象的起始位置,虚表指针在对象构造时被初始化,指向该对象所属类的虚函数表。
内存布局
以下面这个代码为例
class Base {
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2() { cout << "Base::func2()" << endl; }
};
class Derived : public Base {
public:
void func1() override { cout << "Derived::func1()" << endl; } /// 重写func1
};
普通继承下,子类和基类共用一个虚表地址。
派生类在继承时内存中各个变量所储存的位置如下分析:首先初始化对象指针(派生类对象),会在分配的内存空间的栈区为对象指针分配空间,这个派生类对象继承自基类对象,会在堆区创建一个虚函数表指针vptr,这个虚函数表指针指向类对象的虚函数表,这个虚函数表存放在数据区,虚函数表中存放着各个虚函数的指针,指向虚函数的地址空间,这些虚函数代码存放在代码段,代码段中都是编译好的二进制机器码。
实现动态多态的核心原理是虚函数表,即:派生类继承自基类,会生成相应的虚函数表指针,虚函数表等,在派生类对象的内存布局空间会存放以下几个指针
多重继承不同的继承顺序也会导致类对象的内存布局顺序不同。
虚继承
虚继承(Virtual Inheritance)是 C++ 中用于解决多重继承带来的菱形继承(钻石继承)问题的一种机制,其主要目的是避免在派生类中出现基类成员的重复副本,同时确保无论经过多少层继承,基类在派生类对象中只有一份实例。
菱形继承
假设存在以下继承关系
class A {
public:
int data;
};
class B : public A {};
class C : public A {};
class D : public B, public C {};
在上述代码中,D
同时继承自 B
和 C
,而 B
和 C
又都继承自 A
,形成了菱形继承结构。这会导致 D
类对象中包含两份 A
类的成员(分别通过 B
和 C
继承而来),如果要访问或修改A
类的成员,会产生二义性,并且造成内存浪费。
虚继承的实现方式
为了解决菱形继承的问题,C++ 引入了虚继承,语法上在派生类继承列表中使用 virtual
关键字声明虚继承,示例代码如下:
class A {
public:
int a; /// 虚基类A的数据成员
};
class B : virtual public A {
public:
int b; /// 派生类B的数据成员
};
class C : virtual public A {
public:
int c; /// 派生类C的数据成员
};
class D : public B, public C {
public:
int d; /// 最终派生类D的数据成员
};
在虚继承情况下,D
类中只会有一份A
类的成员
- 虚基类表
虚基类是指虚继承的基类,编译器为每个使用虚继承的类生成了一个虚基类表,存储虚基类相对于派生类对象起始位置的偏移量,这个偏移量用于在运行时找到虚基类子对象在派生类对象中的位置。
注意:菱形继承没有虚基类表,只有菱形虚继承才有!!!
- 虚基类指针
在派生类对象中,会额外增加一个虚基类指针(通常位于对象的起始位置或紧随虚表指针之后),该指针指向虚基类表。
- 内存布局
按照上方的代码示例
- 定位虚基类的流程
当通过派生类对象访问虚基类成员时,编译器生成的代码会:
- 获取虚基类表指针(VBPtr):从派生类对象的固定位置(通常紧随 VPTR 之后)读取 VBPtr。
- 查找虚基类表:跟随 VBPtr 找到虚基类表。
- 获取偏移量:从虚基类表中读取目标虚基类的偏移量。
- 计算虚基类地址:
虚基类地址 = 对象起始地址 + 偏移量
。
- 虚基类表指针与虚表指针关系
在使用虚继承的对象中,内存布局通常包含:
- 虚表指针(VPTR):指向类的虚函数表,用于动态绑定虚函数。
- 虚基类表指针(VBPtr):指向类的虚基类表,用于定位虚基类子对象。
构造函数和析构函数能否是虚函数?
为什么构造函数不能是虚函数?
构造函数是在编译期间确定对象的类型以及为对象分配空间,如果类中有虚函数,那么就需要初始化虚函数表,虚函数的执行依靠虚函数表;如果构造函数是虚函数,那么就需要依靠虚函数表执行构造,但是虚函数表又只有在构造时才会初始化,陷入死循环了,所以构造函数不能是虚函数!!!
为什么基类析构函数要是虚函数?
因为如果不设置为虚析构函数,那么对象在调用析构函数时,会根据定义的类型来寻找析构函数,一般都是基类,此时派生类的析构函数无法被调用,派生类无法析构,会造成内存泄漏,所以需要设置为虚函数。
对象切片
当使用值访问时,所定义的变量是基类类型,那么只会复制基类的成员,而派生类中的数据和虚表指针指向的重写函数都被切掉了。
实例分析
class Base {
public:
int base_val = 1;
virtual void show() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
int derived_val = 2;
void show() override { std::cout << "Derived\n"; }
};
int main() {
Derived d;
Base b = d; /// 对象切片
b.show(); /// 调用 Base::show,而不是 Derived::show
}
当Base b = d;
时,只复制了 Base
类型的成员;
Derived
中的 derived_val
、虚表指针指向的重写函数都被切掉了;
b.show()
调用的是 Base::show()
,因为 b
是个完全的 Base
对象。
所以这里也说明了虚函数实现动态多态的其中一个条件必须是使用基类的指针或者引用才能实现,而不是普通对象。
最后,以上内容仅是个人的总结和看法,也许存在一些错误的地方,如果错误,敬请指出,接受一切批评,知错就改,如果侵权,请联系我删除~