C++ 多态入门:虚函数、重写、虚析构及 override/final 实战指南(附腾讯面试题)

目录

  • 前言
  • 一、多态的概念
  • 二、多态的定义及实现
    • [2.1 多态的构成条件](#2.1 多态的构成条件)
      • [2.1.1 虚函数](#2.1.1 虚函数)
      • [2.1.2 实现多态还有两个必须重要条件](#2.1.2 实现多态还有两个必须重要条件)
      • [2.1.3 虚函数的重写/覆盖](#2.1.3 虚函数的重写/覆盖)
      • [2.1.4 多态场景的一个选择题(腾讯面试题)](#2.1.4 多态场景的一个选择题(腾讯面试题))
      • [2.1.5 虚函数重写的一些其他问题](#2.1.5 虚函数重写的一些其他问题)
        • [2.1.5.1 协变(了解)](#2.1.5.1 协变(了解))
        • [2.1.5.2 析构函数的重写(笔试常见)](#2.1.5.2 析构函数的重写(笔试常见))
      • [2.1.6 override 和 final 关键字](#2.1.6 override 和 final 关键字)
      • [2.1.7 重载/重写/隐藏的对比(面试考点)](#2.1.7 重载/重写/隐藏的对比(面试考点))
  • 结语

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、多态的概念

多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点讲运行时多态,编译时多态(静态多态)和运行时多态(动态多态)。编译时多态(静态多态)主要就是我前面文章讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。

运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票(5折或75折);军人买票时是优先买票。再比如,同样是动物叫的一个行为(函数),传猫对象过去,就是"(> ^ w ^ <)喵",传狗对象过去,就是"汪汪"。

cpp 复制代码
#include<iostream>
using namespace std;

class Person {
public:
	virtual void BuyTicket()
	{
		cout << "买票 - 全价" << endl;
	}
};

class Student : public Person {
public:
	virtual void BuyTicket()
	{
		cout << "买票 - 打折" << endl;
	}
};

void Func(Person* ptr)
{
	ptr->BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	return 0;
}

这串代码如果交给没有学过多态的新手来看,新手会看到Func函数的形参定义是Person* ptr,因此会认为:无论传入的是Person对象的地址(&ps)还是Student对象的地址(&st),ptr的类型永远是Person*(编译器在编译阶段就固定的,不会因为传入的实际对象不同而改变)

既然ptr的类型是Person*,那么ptr->BuyTicket()就只能调用Person类中定义的BuyTicket()函数。

基于以上理解,新手会预判这段代码的运行结果是:

cpp 复制代码
买票 - 全价
买票 - 全价

实则不然,不同的对象区调用会产生不同的结果

上面第一个是基类的指针调用虚函数,第二个是基类的引用调用虚函数

二、多态的定义及实现

2.1 多态的构成条件

多态是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票

2.1.1 虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰

注意:虚函数和虚继承虽然用的是同一个关键字virtual,但是并没有什么关联关系

2.1.2 实现多态还有两个必须重要条件

  • 必须是基类的指针或者引用调用虚函数(如下图用基类对象直接调用就无法达成多态了)

  • 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖

    如图若被调用的函数不是虚函数,也不构成多态

2.1.3 虚函数的重写/覆盖

虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为C++ 规定:基类中被virtual修饰的成员函数,其 "虚函数" 的特性会被派生类继承。只要派生类定义的函数与基类虚函数的签名完全一致(函数名、参数列表、const/volatile修饰符、返回类型都匹配),那么这个派生类函数会自动成为虚函数,无需显式加virtual关键字,自然也能构成对基类虚函数的重写),但是该种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意留这个坑,让你判断是否构成多态,主包之前在腾讯的笔试题中见过,非常恶心,只能说相关出题人用心险恶。

2.1.4 多态场景的一个选择题(腾讯面试题)

上面提及的非常变态的面试题就是下面这一道,兄弟们可以看一下大厂的笔试题恶心到什么程度,我个人感觉这就属于专为考试而设计的题目了,公司内如果真的有人写出这种代码的话,第二天应该直接就要走人了

以上程序输出结果是什么()

cpp 复制代码
A:A->0 
B:B->1 
C:A->1 
D:B->0 
E:编译出错 
F:以上都不正确

题目解析:

首先要清楚4个核心概念

上面静态类型动态类型的绑定规则是 C++ 语言标准明确规定 的,不是编译器随意实现的,所有符合标准的 C++ 编译器都必须遵守

第一步:拆解题目完整代码(标注继承关系)

cpp 复制代码
#include <iostream>
using namespace std;

// 父类(基类)A
class A {
public:
    // 虚函数func:允许子类重写,默认参数val=1(静态绑定的关键)
    virtual void func(int val = 1) { 
        cout << "A->" << val << endl; 
    }
    // 虚函数test:内部调用func(),B未重写此方法(继承复用的关键)
    virtual void test() { 
        func(); // 等价于 this->func(),this是隐藏指针
    }
};

// 子类(派生类)B:public继承A(继承的核心)
class B : public A {
public:
    // 重写(override)A的虚函数func:函数签名和父类一致(多态的前提)
    // 默认参数改为val=0,但默认参数不参与多态
    void func(int val = 0) { 
        cout << "B->" << val << endl; 
    }
    // 关键:B没有重写test() → 完全继承A的test()逻辑
};

int main() {
    B* p = new B; // 静态类型:B*;动态类型:B*
    p->test();    // 核心调用1:重点分析
    p->func();    // 核心调用2:对比分析
    delete p;     // 补充:避免内存泄漏
    return 0;
}

第二步:核心调用 1 → p->test ()(4 个概念全联动)
步骤 1:执行 p->test () → 继承 + 静态 / 动态类型

  • 继承:B 没有重写 test(),因此继承并复用 A 的 test() 方法;
  • 静态类型:p 的静态类型是 B*(声明时的类型);
  • 动态类型:p 的动态类型是 B*(实际指向 B 对象);
  • 多态:此步骤暂未触发多态(test () 未被重写,调用的是确定的 A::test ())。

步骤 2:进入 A::test () → this 指针的静态 / 动态类型

A::test () 中的 func() 等价于 this->func(),this 是隐藏的成员函数参数,此时:

  • 继承:this 是 A 类成员函数的隐藏指针,因此静态类型被编译器固定为 A*(继承链中 "父类方法的 this 天然认为自己指向父类");
  • 静态类型:this 的静态类型 = A*(编译期确定,和 A 类绑定);
  • 动态类型:this 的动态类型 = B*(运行期传递,实际指向 p 所指的 B 对象);
  • 多态:即将触发多态(因为 func () 是虚函数)。

步骤 3:执行 this->func () → 多态 + 静态 / 动态类型

这是整个题目的核心,多态和静态绑定在此分道扬镳:

  • 多态(函数体):func () 是虚函数,按动态类型(B* 绑定 → 执行 B::func () 的函数体(输出 "B->");
  • 静态类型(默认参数):默认参数是编译期决议,按静态类型(A* 绑定 → 取 A::func () 的默认参数 1;
  • 继承:B 重写了 A 的 func (),才让多态有了 "不同实现" 的基础;
  • 动态类型:保证了 "执行的是子类的函数体"。

步骤 4:最终结果

执行 B::func (1) → 输出 B->1

第三步:核心调用 2 → p->func ()

步骤 1:执行 p->func () → 继承 + 静态 / 动态类型

  • 继承:B 重写了 A 的 func (),因此优先调用自身的 func ();
  • 静态类型:p 的静态类型 = B*(声明时的类型);
  • 动态类型:p 的动态类型 = B*(实际指向 B 对象);
  • 多态:即将触发多态,但静态 / 动态类型一致,结果更直观。

步骤 2:执行 p->func () → 多态 + 静态 / 动态类型

  • 多态(函数体):按动态类型(B*)绑定 → 执行 B::func () 的函数体(输出 "B->");
  • 静态类型(默认参数):按静态类型(B*)绑定 → 取 B::func () 的默认参数 0;
  • 继承:B 重写 func () 是多态的前提;
  • 动态类型:和静态类型一致,无 "矛盾"。

步骤 3:最终结果

执行 B::func (0) → 输出 B->0

2.1.5 虚函数重写的一些其他问题

2.1.5.1 协变(了解)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或引用时,称为协变。协变的实际意义并不大,所以了解一下即可

但是若A,B类没有继承关系,就会有报错,就是因为没有满足协变的要求,如下图

返回值并不局限于其他类的类型,用自己的类型也可以,只要这两个类型是继承关系就可以

2.1.5.2 析构函数的重写(笔试常见)

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成 destructor ,所以基类的析构函数加了 virtual 修饰,派生类的析构函数就构成重写。

下面结合代码说明一下某些情况下若析构函数不加 virtual 会出现的问题(为什么基类中的析构函数建议设计为虚函数),这个问题在面试中经常考察,一定要结合样例才可以讲清楚,主包写的示例代码也可以参考



情况 1:栈上对象(A a; B b;

两种析构版本分析(无虚表 / 虚指针)

情况 2:基类指针指向基类堆对象(A* ptr = new A; delete ptr;

两种析构版本分析(无虚表 / 虚指针)

情况 3:基类指针指向派生类堆对象(A* ptr = new B; delete ptr;

情况 4:基类指针管理派生类 + 基类堆对象(A* ptr1 = new B; delete ptr1; A* ptr2 = new A; delete ptr2;

上面派生类重写的虚函数可以不加 virtual 某种程度上来说原因就在这里,避免派生类的析构函数没写 virtual 不构成多态进而造成内存泄漏

补充一点:内存泄漏是非常严重的问题,也是因为C++的特性最容易出现的问题,泄露一点点没有问题,但是如果用一次泄露一点就是慢性死亡了,等到项目上线运行十天半个月后,内存就会逐渐捉襟见肘,越用越卡

2.1.6 override 和 final 关键字

从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错,参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug灰得不偿失,因此C++11提供了 override ,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final修饰

如图加了 override 可以检查出基类的函数名写错,未加就检查不出

加了 final 后,基类中有函数重写了就会报错

不重写就没事

注意两个关键字的位置,override 是放在派生类的虚函数处,final 是放在基类的虚函数处

2.1.7 重载/重写/隐藏的对比(面试考点)


结语

相关推荐
仰泳的熊猫1 小时前
题目1535:蓝桥杯算法提高VIP-最小乘积(提高型)
数据结构·c++·算法·蓝桥杯
yanghuashuiyue2 小时前
lambda+sealed+record
java·开发语言
闻缺陷则喜何志丹2 小时前
【前后缀分解】P9255 [PA 2022] Podwyżki|普及+
数据结构·c++·算法·前后缀分解
yzx9910133 小时前
Python数据结构入门指南:从基础到实践
开发语言·数据结构·python
消失的旧时光-19433 小时前
智能指针(二):机制篇 —— 移动语义与所有权转移
c++·智能指针
衍生星球3 小时前
【JSP程序设计】Servlet对象 — page对象
java·开发语言·servlet·jsp·jsp程序设计
扶苏瑾3 小时前
线程安全问题的产生原因与解决方案
java·开发语言·jvm
小小小米粒4 小时前
函数式接口 + Lambda = 方法逻辑的 “插拔式解耦”
开发语言·python·算法
风吹乱了我的头发~4 小时前
Day31:2026年2月21日打卡
开发语言·c++·算法