多态指同一个“接口”或“调用语句”,在不同类型上表现出不同的行为。C++中主要有两种多态,静态多态和动态多态
静态多态
函数在被调用时,具体执行哪段代码,在编译阶段就已经确定了。编译器根据函数参数的类型或数量,直接把函数调用链接到具体的函数地址,无需继承和虚函数。
实现方式
- 函数重载
- 运算符重载
- 模板
代码示例
函数重载:
#include <iostream> void print(int i) { std::cout << "打印整数: " << i << '\n'; } void print(double d) { std::cout << "打印浮点数: " << d << '\n'; } int main() { // 编译器根据参数是int,绑定到 print(int) 的地址 print(10); // 编译器根据参数是double,绑定到 print(double) 的地址 print(3.14); return0; }运算符重载:
#include <iostream> class Point { private: int x, y; public: Point(int x = 0, int y = 0) : x(x), y(y) {} // 重载 + 运算符 Point operator+(const Point& other) const { return Point(this->x + other.x, this->y + other.y); } // 友元函数重载 << friendstd::ostream& operator<<(std::ostream& os, const Point& p); }; // 全局重载 << std::ostream& operator<<(std::ostream& os, const Point& p) { os << "(" << p.x << ", " << p.y << ")"; return os; } int main() { Point p1(10, 20); Point p2(5, 5); // Point + Point, 调用 Point::operator+(const Point&) Point p3 = p1 + p2; std::cout << "p3: " << p3 << std::endl; // 输出: (15, 25) return0; }模版:
#include <iostream> template <typename T> void add(T a, T b) { std::cout << "模板加法结果: " << a + b << std::endl; } int main() { // 编译器根据参数类型是int,自动生成 add<int> 版本 add(1, 2); return 0; }优缺点
优点:编译器完成方法绑定,编译器可进行内联优化;不依赖继承体系,组合更灵活。
缺点:必须在编译时就知道所有类型;编译耗时、代码膨胀。
动态多态
函数在被调用时,具体执行哪段代码,在编译阶段无法确定,只有在程序运行时,根据对象的实际类型(是基类对象还是派生类对象)来决定。
实现方式
三条必须满足,缺一不可:
有继承关系
基类中有虚函数,派生类重写了该虚函数
通过基类的指针或引用去调用虚函数
#include <iostream> // 基类 class Shape { public: // 虚函数 virtual void draw() { std::cout << "绘制一个通用的形状" << std::endl; } }; // 派生类 class Circle :public Shape { public: // 重写基类虚函数 void draw() override { std::cout << "绘制一个圆形 O" << std::endl; } }; // 派生类 class Rectangle :public Shape { public: // 重写基类虚函数 void draw() override { std::cout << "绘制一个矩形 []" << std::endl; } }; // 注意这里函数参数是Shape*,编译时不知道具体指向哪个类型 // 只有运行时才知道指针指向的具体类型 void startDrawing(Shape* shape) { shape->draw(); } int main() { Circle c; Rectangle r; startDrawing(&c); // 输出:绘制一个圆形 O startDrawing(&r); // 输出:绘制一个矩形 [] return0; }实现原理
在编译含有 virtual 函数的类时,编译器会自动添加虚函数表和虚指针,通过查找虚函数表得到实际要调用函数的地址
虚函数表(vtable):
- 归属于类,同一个类共享一个虚函数表。
- 是一个函数指针数组,存放该类所有虚函数地址。
- 存储在只读数据段 .rodata,程序运行时加载
虚指针(vptr):
- 归属于对象,每个对象都有一个虚指针。
- 是一个指针,指向该类的vtable。
- 存储在对象内存布局的最头部,便于快速读取。
假设有一个基类 Base,含有两个虚函数 func1 和 func2,Derived 类继承自 Base,并重写了 func1 函数,func2 函数则继承了 Base 的 func2:
class Base { public: virtual void func1() { cout << "Base::func1" << endl; } // 虚函数 virtual void func2() { cout << "Base::func2" << endl; } // 虚函数 virtual ~Base() = default; // 虚析构函数 void normal() {} // 普通函数,不占对象内存 private: int a; int b; }; class Derived :public Base { public: // 重写了 func1 void func1() override { cout << "Derived::func1" << endl; } // func2继承了Base的func2 private: int c; int d; };优缺点
1.优点:灵活性高,可以通过基类指针或引用操作派生类对象。
2.缺点:编译器无法优化虚函数;运行时进行类型检查和方法绑定,有一定开销。
虚函数是怎么实现动态绑定的?vptr 和 vtable是什么?
有虚函数的类中会有一个隐藏指针 vptr,指向该类的虚函数表 vtable,vtable 是一个函数指针数组,存放该类所有虚函数地址。调用虚函数时,编译器生成代码从对象取 vptr,再在 vtable 的槽位中找到函数地址并间接调用,从而在运行期决定调用哪个版本。
一个对象里有几个 vptr?
单继承通常只有一个。多继承时,每个含虚函数的基类子对象各自含有一个 vptr。
普通成员函数占对象内存吗?
不占。成员函数代码存储在可执行文件的代码段,每个对象只存数据成员和编译器需要的字段
基类有一个 func(int),派生类写 func(double),会怎样?
会触发名字隐藏,派生类的 func(double) 会把基类的所有同名函数隐藏掉,导致无法通过派生类对象调用基类的 func(int)
为什么基类析构函数要设为 virtual?
通过基类指针删除派生类对象时,如果基类析构函数非 virtual,会导致只调用基类的析构函数,而不调用派生类的析构函数,导致资源泄露