
🎬 个人主页:Vect个人主页 🎬 GitHub:Vect的代码仓库 🔥 个人专栏: 《数据结构与算法》《C++学习之旅》《计算机基础》
⛺️Per aspera ad astra.
多态是面向对象的一大特性,通俗理解即是 不同类型对象去做同一个行为,产生的结果不同,比如“叫”这个动作,狗、猫、牛同样是“叫”,发声结果不一样,这就是多态
在上一章中,Carp和Tuna从Fish那里继承了swim(),但是,Carp和Tuna都提供了自己的swim()实现,而它们都是鱼类,那么如果将Carp实例化出的对象作为实参传给Fish参数,并通过这个参数调用swim(),最终执行的是Fish::swim(),而不是Fish::swim(),代码如下:
class Fish {
public:
void swim() { cout << "Fish swims" << endl; }
};
class Carp : public Fish {
public:
void swim() { cout << "Carp swims" << endl; } // 重写swim
};
void makeFishSwim(Fish& fish) { fish.swim(); }
void test1() {
Carp myLunch;
myLunch.swim();
makeFishSwim(myLunch); // 想要输出Carp swims 但是并未输出
}分析:
Carp继承了Fish,并且重写了Fish::swim()16行传入的Carp对象,但是 makeFishSwim(Fish&)也将其视为Fish,进而调用Fish::swim。而我们想要的结果是Carp对象表现出鲤鱼的行为,即便是通过Fish调用swim也能表现鲤鱼行为(不同对象做同一种动作,产生的结果不同),让Fish参数表现出实际类型(派生类)的行为结果,可将Fish::swim()声明为虚函数
可以通过基类指针或基类引用,这个指针或引用可以指向Fish、Carp、Tuna对象,但是我们无需关心指向的是哪种对象,只要这个指针或引用调用了swim,就可以实现不同类型对象的不同行为结果
class Fish {
public:
// 声明为虚函数 可实现多态行为
virtual void swim() { cout << "Fish swims" << endl; }
};
class Carp : public Fish {
public:
virtual void swim() { cout << "Carp swims" << endl; } // 重写swim
};
class Tuna : public Fish {
public:
virtual void swim() { cout << "Tuna swims" << endl; } // 重写swim
};
void makeFishSwim(Fish& fish) { fish.swim(); }
void test1() {
Carp myLunch;
Tuna mydinner;
makeFishSwim(myLunch);
makeFishSwim(mydinner);
}分析:这次没有调用Fish::swim(),覆盖重写了Fish::swim()的动作,重写的函数优先于被声明为虚函数的Fish::swim(),这意味着可以通过Fish&参数调用派生类定义的swim(),而无需关心这个参数指向的是那个类型的对象
总结一下多态构成条件:

如果基类指针指向派生类对象,并通过该指针调用了delete时,结果如何?
class Fish {
public:
Fish() { cout << "Fish 构造" << endl; }
~Fish() { cout << "Fish 析构" << endl; }
};
class Carp : public Fish {
public:
Carp() { cout << "Carp 构造" << endl; }
~Carp() { cout << "Carp 析构" << endl; }
};
// 使用new在堆区中实例化的派生类对象 如果将其赋给基类指针
// 并通过该指针调用 delete 将不会调用派生类的析构函数
// 这可能导致资源未释放 内存泄露等问题
void deleteFishMemory(Fish* pFish) { delete pFish; }
void test2() {
cout << "== 演示开始" << endl;
// 使用new在堆区中实例化的派生类对象 如果将其赋给基类指针
// 并通过该指针调用 delete 将不会调用派生类的析构函数
// 这可能导致资源未释放 内存泄露等问题
Carp* pCarp = new Carp;
cout << "==开始删除" << endl;
// 在析构过程中 需要调用所有相关的析构 但是!!!!
deleteFishMemory(pCarp); // 没有对Carp进行清理!!!! ---->基类析构函数构造成虚函数!!!
cout << endl << "Carp 在栈区" << endl;
Carp myDinner;
cout << "== 析构行为" << endl;
}对于使用new在堆区中实例化的派生类对象,如果将其赋给基类指针,并通过该指针调用delete,将不会调用派生类的析构函数。这可能导致资源未释放、内存泄露等问题
所以需要给基类析构函数构造为虚函数,编译器会对析构函数做特殊处理,变成destructor,仍然满足函数重写的规则
class Fish {
public:
Fish() { cout << "Fish 构造" << endl; }
virtual ~Fish() { cout << "Fish 析构" << endl; }
};
class Carp : public Fish {
public:
Carp() { cout << "Carp 构造" << endl; }
~Carp() { cout << "Carp 析构" << endl; }
};
// 使用new在堆区中实例化的派生类对象 如果将其赋给基类指针
// 并通过该指针调用 delete 将不会调用派生类的析构函数
// 这可能导致资源未释放 内存泄露等问题
void deleteFishMemory(Fish* pFish) { delete pFish; }
void test2() {
cout << "== 演示开始" << endl;
// 使用new在堆区中实例化的派生类对象 如果将其赋给基类指针
// 并通过该指针调用 delete 将不会调用派生类的析构函数
// 这可能导致资源未释放 内存泄露等问题
Carp* pCarp = new Carp;
cout << "==开始删除" << endl;
// 在析构过程中 需要调用所有相关的析构 但是!!!!
deleteFishMemory(pCarp); // 没有对Carp进行清理!!!! ---->基类析构函数构造成虚函数!!!
cout << endl << "Carp 在栈区" << endl;
Carp myDinner;
cout << "== 析构行为" << endl;
}这样Carp也做了资源清理工作
C++对于函数重写要求较高,我们很容易疏忽,可能会导致函数名字字母次序写反而构成不了重载,这种错误在编译期间是不报错的,只会在程序运行时得不到预期结果。
final:修饰虚函数,表示该虚函数不能再被重写(上一篇修饰类的话,表明类不能被继承)
class Car {
public:
virtual void drive() { cout << "Car" << endl; }
};
class Benz : public Car{
public:
virtual void drive() final { cout << " Benz 豪华" << endl; } // 虚函数不能再被重写
};override:检查派生类虚函数是否重写了基类的某个虚函数,若没有重写编译报错
class Car {
public:
virtual void drive() { cout << "Car" << endl; }
};
class BMW : public Car {
public:
virtual void drive() override { cout << "BWM 运动" << endl; }
};class Base {
public:
virtual void func1() { cout << "Base" << endl; }
private:
int _b = 1;
};
void testSize() { cout << sizeof(Base) << endl; }除了_b成员,还多了一个_vfptr(virtual function ptr)存在对象的前面,对象中的这个指针称为虚函数表指针

一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被存放到虚函数表中,那么派生类中这个表中放了什么呢?我们继续扩充代码:
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func3() { cout << "Base::fun3" << endl; }
private:
int _b = 1;
};
class Derived : public Base {
public:
void func1() { cout << "Derived::func1" << endl; } // 只重写func1
private:
int _d = 0;
};
void testShow() {
Base b;
Derived d;
}通过调试可以知道:
d也有一个虚表指针b对象和派生类d对象的虚表不同,** func1完成重写,所以d的虚表中存的是重写的Derived::func1,** 而func2未被重写,所以两份虚表中的func2都是Base::func2,地址相同。

func3也继承了,但不是虚函数,不会放到虚表中
只有通过基类指针或引用调用虚函数时,才会发生动态绑定(根据对象的动态类型走虚函数表)动态绑定是在运行时发生的
Base b;
Derived d;
Base* pb = &b;
Base* pd = &d; // 指向“派生类对象的基类子对象”
pb->func1(); // Base::func1
pd->func1(); // Derived::func1 ← 多态
pd->func2(); // Base::func2 ← 未重写,落回基类直接用对象名(按值)调用或者非虚函数,都是静态绑定(在编译时发生):
b.func1();调的就是Base::func1();d.func3();只是普通函数,不走虚表。
func3 不是虚函数,不会进虚函数表;通过 Base*/Base& 调它也只会静态绑定到 Base::func3()。
只要类里有虚函数,就应给基类一个虚析构函数,否则通过基类指针 delete 派生对象会只调用基类析构,导致资源泄露:
class Base {
public:
virtual ~Base() = default; // 建议
virtual void func1();
virtual void func2();
void func3();
};在虚函数的后面写上 “=0” ,则这个函数为纯虚函数。原理
包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Fish {
public:
virtual void swim() = 0; // 声明纯虚函数
};
class Carp : public Fish {
public:
void swim() override { cout << "Carp swims" << endl; } // 必须重写纯虚函数
};
class Tuna : public Fish {
public:
void swim() override { cout << "Tuna swims" << endl; } // 必须重写纯虚函数
};
void test() {
Fish* carp = new Carp;
carp->swim();
Fish* tuna = new Tuna;
tuna->swim();
}普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
目的:替换基类虚函数行为,支持多态。
位置:派生类对基类的虚函数进行实现替换。
特征:
virtualoverride,不匹配会编译报错。
示例:struct Base { virtual void f(int) const; };
struct D : Base { void f(int) const override; }; // 重写易错:签名微差(如少了 const、引用限定、默认参数不算签名的一部分)会变成重载而非重写。
目的:同一作用域下,让同名函数根据参数列表不同有多个版本。 位置:同一个类/命名空间内(包括派生类“继承来”的同名也会形成可见性关系)。 特征:
void g(int);
void g(double); // 重载
// void g(int) -> int; // 仅改返回值 不构成重载易错:把想“重写”的函数写成了“参数略不同”的版本,结果成了重载,导致多态失效。
目的:写一份泛型代码,编译期对具体实参实例化出具体函数/类。
位置:函数模板、类模板等
特征:
template<class T>
void h(T); // 模板
void h(int); // 非模板重载
// 调用 h(42) 通常选非模板版本(更佳匹配)为什么需要多态 希望“通过基类指针/引用调用同名接口”,实际执行对象的动态类型对应的实现。若基类函数不为虚,调用会静态绑定到基类版本,违背“同接口不同行为”的初衷。
如何实现多态
virtual;虚表与对象布局
.rodata/.rdata),表项是虚函数的地址;函数代码在 .text。多态何时发生
只有基类指针/引用 + 虚函数才有动态绑定。
直接用对象按值调用/非虚函数 ⇒ 静态绑定。
析构函数要点
只要类有虚函数/要“多态删除”,务必让基类析构为虚:virtual ~Base() = default;,否则 delete Base* 指向 Derived 会只析构基类,可能泄漏。
纯虚函数与抽象类
virtual void f() = 0; 声明接口约束;包含纯虚的类不可实例化;派生类必须重写后方可实例化。