目录
[1.1 多态的通俗理解](#1.1 多态的通俗理解)
[1.2 多态的分类](#1.2 多态的分类)
[1.2.1 编译时多态(静态多态)](#1.2.1 编译时多态(静态多态))
[1.2.2 运行时多态(动态多态)](#1.2.2 运行时多态(动态多态))
[2.1 核心条件一:继承关系](#2.1 核心条件一:继承关系)
[2.2 核心条件二:虚函数与重写](#2.2 核心条件二:虚函数与重写)
[2.2.1 虚函数的定义](#2.2.1 虚函数的定义)
[2.2.2 虚函数的重写(覆盖)](#2.2.2 虚函数的重写(覆盖))
[2.3 核心条件三:基类的指针或引用调用虚函数](#2.3 核心条件三:基类的指针或引用调用虚函数)
[2.4 多态实现的完整示例](#2.4 多态实现的完整示例)
[3.1 协变:返回值类型不同的特殊重写](#3.1 协变:返回值类型不同的特殊重写)
[3.1.1 协变的定义](#3.1.1 协变的定义)
[3.2 析构函数的重写:避免内存泄漏](#3.2 析构函数的重写:避免内存泄漏)
[3.3 override 和 final 关键字:增强重写的安全性](#3.3 override 和 final 关键字:增强重写的安全性)
[3.3.1 override:验证重写是否正确](#3.3.1 override:验证重写是否正确)
[3.3.2 final:禁止虚函数被重写](#3.3.2 final:禁止虚函数被重写)
[3.4 重载、重写、隐藏的区别(面试高频)](#3.4 重载、重写、隐藏的区别(面试高频))
[4.1 纯虚函数的定义](#4.1 纯虚函数的定义)
[4.2 抽象类的特性](#4.2 抽象类的特性)
[4.3 抽象类的应用场景](#4.3 抽象类的应用场景)
[5.1 虚函数表指针(__vfptr)](#5.1 虚函数表指针(__vfptr))
[5.1.1 虚函数表指针的本质](#5.1.1 虚函数表指针的本质)
[5.1.2 包含虚函数的类的对象大小](#5.1.2 包含虚函数的类的对象大小)
[5.2 虚函数表(vtable)的结构](#5.2 虚函数表(vtable)的结构)
[5.2.1 基类的虚表](#5.2.1 基类的虚表)
[5.2.2 派生类的虚表](#5.2.2 派生类的虚表)
[5.2.3 虚表的存储位置](#5.2.3 虚表的存储位置)
[5.3 动态绑定与静态绑定](#5.3 动态绑定与静态绑定)
[5.3.1 静态绑定(Static Binding)](#5.3.1 静态绑定(Static Binding))
[5.3.2 动态绑定(Dynamic Binding)](#5.3.2 动态绑定(Dynamic Binding))
[5.4 多态的性能开销](#5.4 多态的性能开销)
[6.1 选择题:虚函数重写与多态判断](#6.1 选择题:虚函数重写与多态判断)
[6.2 简答题:多态的实现条件](#6.2 简答题:多态的实现条件)
[6.3 简答题:虚函数表是什么?它存储在哪里?](#6.3 简答题:虚函数表是什么?它存储在哪里?)
[6.4 简答题:为什么析构函数要声明为虚函数?](#6.4 简答题:为什么析构函数要声明为虚函数?)
[6.5 编程题:利用多态实现计算器](#6.5 编程题:利用多态实现计算器)
前言
在 C++ 面向对象编程的三大核心特性(封装、继承、多态)中,多态无疑是最具灵活性和扩展性的特性。它允许不同类的对象对同一消息做出不同响应,让代码更具通用性和可维护性,是实现设计模式、框架开发的基础。很多开发者在初学多态时,往往只能掌握表面用法,对其底层原理和进阶细节理解不深。本文将从概念定义出发,逐步深入多态的实现条件、核心机制、底层原理,结合大量实战代码和面试高频考点,全面解析 C++ 多态的方方面面,帮助大家真正吃透这一核心特性。下面就让我们正式开始吧!
一、多态的概念:什么是多态?
1.1 多态的通俗理解
多态(polymorphism),字面意思是 "多种形态"。在编程语境中,指的是同一个行为(函数调用),作用于不同的对象,会产生不同的执行结果。生活中处处可见多态的影子:
- 买票行为:普通人买票全价、学生买票打折、军人买票优先,同样是 "买票" 操作,不同身份的人(不同对象)执行结果不同;
- 动物叫声:猫叫是 "喵",狗叫是 "汪汪",同样是 "发声" 行为,不同动物(不同对象)表现形式不同;
- 交通工具行驶:汽车在路上跑,飞机在天上飞,轮船在水里游,同样是 "移动" 行为,不同交通工具(不同对象)实现方式不同。  
这种 "一个接口,多种实现" 的思想,正是多态的核心价值 ------ 它屏蔽了不同对象之间的差异,让开发者可以通过统一的方式调用不同对象的方法,极大简化了代码逻辑。
1.2 多态的分类
C++ 中的多态分为两大类:编译时多态(静态多态) 和运行时多态(动态多态),二者的核心区别在于 "行为确定的时机" 不同。
1.2.1 编译时多态(静态多态)
编译时多态是指在编译阶段就确定了函数的调用关系,行为结果在编译时已经明确。它的实现方式主要有两种:
- 函数重载:同一作用域内,函数名相同但参数列表(参数类型、个数、顺序)不同的函数,编译器会根据实参类型匹配对应的函数;
- **函数模板:**通过模板参数自动适配不同类型,编译时会为每种使用的类型生成对应的函数实例。
示例:函数重载实现静态多态
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
// 函数重载:参数类型不同
int Add(int a, int b) {
    cout << "int Add: ";
    return a + b;
}
double Add(double a, double b) {
    cout << "double Add: ";
    return a + b;
}
// 函数重载:参数个数不同
int Add(int a, int b, int c) {
    cout << "int Add(3 params): ";
    return a + b + c;
}
int main() {
    cout << Add(1, 2) << endl;         // 调用int Add(int, int)
    cout << Add(1.5, 2.5) << endl;     // 调用double Add(double, double)
    cout << Add(1, 2, 3) << endl;      // 调用int Add(int, int, int)
    return 0;
}运行结果:
            
            
              cpp
              
              
            
          
          int Add: 3
double Add: 4
int Add(3 params): 6静态多态的特点是效率高 (编译时确定调用地址,无运行时开销),但灵活性差(必须在编译时明确所有可能的行为,无法适应运行时动态变化的场景)。
1.2.2 运行时多态(动态多态)
运行时多态是指在程序运行阶段才确定函数的调用关系,行为结果取决于运行时的对象类型。它是 C++ 多态的核心,也是本文重点讲解的内容。
示例:运行时多态的直观体现
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
// 基类:人
class Person {
public:
    // 虚函数:买票
    virtual void BuyTicket() {
        cout << "普通人买票:全价" << endl;
    }
};
// 派生类:学生(继承自Person)
class Student : public Person {
public:
    // 重写基类虚函数
    virtual void BuyTicket() {
        cout << "学生买票:半价(硬座)/75折(高铁二等座)" << endl;
    }
};
// 派生类:军人(继承自Person)
class Soldier : public Person {
public:
    // 重写基类虚函数
    virtual void BuyTicket() {
        cout << "军人买票:优先购票" << endl;
    }
};
// 统一接口:调用买票行为
void DoBuyTicket(Person& people) {
    people.BuyTicket(); // 同一调用语句,不同对象表现不同
}
int main() {
    Person p;
    Student s;
    Soldier sol;
    
    DoBuyTicket(p);   // 输出:普通人买票:全价
    DoBuyTicket(s);   // 输出:学生买票:半价(硬座)/75折(高铁二等座)
    DoBuyTicket(sol); // 输出:军人买票:优先购票
    
    return 0;
}运行结果:
            
            
              cpp
              
              
            
          
          普通人买票:全价
学生买票:半价(硬座)/75折(高铁二等座)
军人买票:优先购票在这个示例中,DoBuyTicket函数接收Person类型的引用,但传入不同的派生类对象时,会执行对应的BuyTicket方法。这种 "同一接口,多种实现" 的效果,正是运行时多态的核心体现。它的特点是灵活性高 (支持动态扩展,新增派生类无需修改原有接口代码),但有轻微运行时开销(需要在运行时查找函数地址)。
二、多态的定义及实现:三大核心条件
想要实现 C++ 运行时多态,必须满足三个核心条件,缺一不可。很多开发者在使用多态时出现问题,本质上都是没有完全满足这三个条件。
2.1 核心条件一:继承关系
多态必须建立在类的继承体系之上,即存在基类(父类)和派生类(子类)的继承关系。派生类通过继承基类,获得基类的接口(虚函数),并可以根据自身需求重写该接口。
需要注意:
- 支持单一继承(一个派生类继承一个基类)和多重继承(一个派生类继承多个基类),但多重继承可能导致虚函数表复杂,需要谨慎使用;
- 派生类必须是公有继承(public inheritance),才能保证基类的指针 / 引用可以访问派生类的虚函数(私有继承或保护继承会限制访问权限)。

2.2 核心条件二:虚函数与重写
2.2.1 虚函数的定义
虚函数是多态的**"开关"** ,在基类的成员函数前加上virtual关键字,该函数就成为虚函数。
语法格式:
            
            
              cpp
              
              
            
          
          class 基类名 {
public:
    virtual 返回值类型 函数名(参数列表) {
        // 函数实现
    }
};注意事项:
- virtual关键字仅需在基类声明时添加,派生类重写时可加可不加,但建议加上,能够提高代码可读性;
- 非成员函数(全局函数)、静态成员函数(static修饰)、构造函数不能声明为虚函数;
- 析构函数可以(且建议)声明为虚函数,这是面试高频考点,后续会详细讲解。
2.2.2 虚函数的重写(覆盖)
虚函数的重写(也叫覆盖)是指:派生类中有一个与基类虚函数完全相同的函数,即满足 "三同" 原则:
- 函数名相同;
- 参数列表(参数类型、个数、顺序)相同;
- 返回值类型相同(协变情况除外,后续讲解)。
示例:虚函数重写的正确实现
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Animal {
public:
    // 基类虚函数
    virtual void Talk() const {
        cout << "动物发出声音" << endl;
    }
};
class Cat : public Animal {
public:
    // 重写基类虚函数:三同原则
    virtual void Talk() const {
        cout << "(>^ω^<)喵~" << endl;
    }
};
class Dog : public Animal {
public:
    // 重写基类虚函数:三同原则(派生类可省略virtual,但不推荐)
    void Talk() const { // 仍构成重写,因为继承了基类虚函数属性
        cout << "汪汪汪!" << endl;
    }
};
// 统一接口
void LetHear(const Animal& animal) {
    animal.Talk();
}
int main() {
    Cat cat;
    Dog dog;
    
    LetHear(cat); // 输出:(>^ω^<)喵~
    LetHear(dog); // 输出:汪汪汪!
    
    return 0;
}常见错误:不满足三同原则,导致重写失败
            
            
              cpp
              
              
            
          
          // 错误示例1:参数列表不同
class Animal {
public:
    virtual void Eat(string food) { // 参数为string类型
        cout << "动物吃" << food << endl;
    }
};
class Rabbit : public Animal {
public:
    virtual void Eat(const char* food) { // 参数为const char*类型,不满足三同
        cout << "兔子吃" << food << endl;
    }
};
// 错误示例2:返回值类型不同(非协变)
class Animal {
public:
    virtual int GetAge() { // 返回int类型
        return 0;
    }
};
class Bird : public Animal {
public:
    virtual double GetAge() { // 返回double类型,不满足三同(非协变)
        return 1.5;
    }
};在上述错误的示例中,派生类的函数与基类虚函数不满足 "三同" 原则,无法构成重写,因此也就无法实现多态。
2.3 核心条件三:基类的指针或引用调用虚函数
多态的触发必须通过基类的指针 或基类的引用来调用虚函数,直接通过对象本身调用虚函数无法触发多态。
示例:不同调用方式的对比
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Person {
public:
    virtual void BuyTicket() {
        cout << "普通人买票:全价" << endl;
    }
};
class Student : public Person {
public:
    virtual void BuyTicket() {
        cout << "学生买票:半价" << endl;
    }
};
int main() {
    Person p;
    Student s;
    
    // 1. 直接通过对象调用:无多态
    p.BuyTicket(); // 输出:普通人买票:全价
    s.BuyTicket(); // 输出:学生买票:半价(这是直接调用派生类函数,非多态)
    
    // 2. 基类指针调用:触发多态
    Person* p1 = &p;
    Person* p2 = &s;
    p1->BuyTicket(); // 输出:普通人买票:全价
    p2->BuyTicket(); // 输出:学生买票:半价(多态生效)
    
    // 3. 基类引用调用:触发多态
    Person& r1 = p;
    Person& r2 = s;
    r1.BuyTicket(); // 输出:普通人买票:全价
    r2.BuyTicket(); // 输出:学生买票:半价(多态生效)
    
    return 0;
}为什么必须用基类指针 / 引用?
- 基类指针 / 引用具有 "兼容性":可以指向基类对象,也可以指向派生类对象(派生类对象包含基类部分);
- 直接通过对象调用时,编译器会根据对象的静态类型(编译时确定的类型)调用函数,是无法体现动态多态的;
- 通过基类指针 / 引用调用时,编译器会延迟到运行时,根据指针 / 引用实际指向的对象类型来调用对应的虚函数。
2.4 多态实现的完整示例
结合以上三个条件,下面给出一个完整的多态实现示例,涵盖继承、虚函数重写、基类指针 / 引用调用三个核心要素:
            
            
              cpp
              
              
            
          
          #include <iostream>
#include <string>
using namespace std;
// 基类:交通工具
class Vehicle {
public:
    Vehicle(string name) : _name(name) {}
    
    // 虚函数:行驶
    virtual void Run() {
        cout << _name << ":正在行驶" << endl;
    }
    
    // 虚函数:鸣笛
    virtual void Honk() {
        cout << _name << ":鸣笛警告" << endl;
    }
protected:
    string _name; // 交通工具名称
};
// 派生类:汽车
class Car : public Vehicle {
public:
    Car(string name) : Vehicle(name) {}
    
    // 重写Run函数
    virtual void Run() {
        cout << _name << ":在公路上匀速行驶,速度60km/h" << endl;
    }
    
    // 重写Honk函数
    virtual void Honk() {
        cout << _name << ":嘀嘀嘀~" << endl;
    }
};
// 派生类:飞机
class Plane : public Vehicle {
public:
    Plane(string name) : Vehicle(name) {}
    
    // 重写Run函数
    virtual void Run() {
        cout << _name << ":在蓝天上飞行,高度10000米" << endl;
    }
    
    // 重写Honk函数
    virtual void Honk() {
        cout << _name << ":呜呜呜~(航空警报)" << endl;
    }
};
// 派生类:轮船
class Ship : public Vehicle {
public:
    Ship(string name) : Vehicle(name) {}
    
    // 重写Run函数
    virtual void Run() {
        cout << _name << ":在海面上航行,航向正东" << endl;
    }
    
    // 重写Honk函数
    virtual void Honk() {
        cout << _name << ":嘟嘟嘟~(雾笛)" << endl;
    }
};
// 统一接口:控制交通工具运行和鸣笛
void ControlVehicle(Vehicle* vehicle) {
    vehicle->Run();   // 多态调用Run函数
    vehicle->Honk();  // 多态调用Honk函数
    cout << "------------------------" << endl;
}
int main() {
    // 创建不同交通工具对象
    Car car("家用轿车");
    Plane plane("民航客机");
    Ship ship("远洋货轮");
    
    // 通过基类指针调用,触发多态
    ControlVehicle(&car);
    ControlVehicle(&plane);
    ControlVehicle(&ship);
    
    return 0;
}运行结果如下:
            
            
              cpp
              
              
            
          
          家用轿车:在公路上匀速行驶,速度60km/h
家用轿车:嘀嘀嘀~
------------------------
民航客机:在蓝天上飞行,高度10000米
民航客机:呜呜呜~(航空警报)
------------------------
远洋货轮:在海面上航行,航向正东
远洋货轮:嘟嘟嘟~(雾笛)
------------------------从运行结果可以看出,ControlVehicle函数通过基类指针Vehicle接收不同的派生类对象,却能正确调用各自的Run和Honk方法,实现了 "一个接口,多种实现" 的多态效果。如果后续需要新增 "高铁""自行车" 等交通工具,只需新增派生类并重写虚函数,无需修改ControlVehicle*函数,这大大提高了代码的可扩展性。
三、虚函数重写的进阶细节
在实际开发和面试中,虚函数重写还有很多容易混淆的进阶细节,比如协变、析构函数重写、override 和 final 关键字等。这些细节既是重点,也是高频考点,大家也需要深入理解。
3.1 协变:返回值类型不同的特殊重写
我在前面提到,虚函数重写需要满足**"三同"** 原则,其中返回值类型必须相同。但存在一种特殊情况 ------协变,允许派生类虚函数的返回值类型与基类虚函数不同。
3.1.1 协变的定义
协变是指:基类虚函数返回基类对象的指针或引用 ,派生类虚函数返回派生类对象的指针或引用(派生类是基类的子类)。这种情况下,即使返回值类型不同,也构成虚函数重写。
示例:协变的实现
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
// 基类:A
class A {};
// 派生类:B(继承自A)
class B : public A {};
// 基类:Person
class Person {
public:
    // 基类虚函数:返回A*
    virtual A* BuyTicket() {
        cout << "普通人买票:全价" << endl;
        return new A();
    }
};
// 派生类:Student
class Student : public Person {
public:
    // 派生类虚函数:返回B*(B是A的子类),构成协变
    virtual B* BuyTicket() {
        cout << "学生买票:半价" << endl;
        return new B();
    }
};
int main() {
    Person p;
    Student s;
    
    Person* p1 = &p;
    Person* p2 = &s;
    
    delete p1->BuyTicket(); // 输出:普通人买票:全价
    delete p2->BuyTicket(); // 输出:学生买票:半价(多态生效)
    
    return 0;
}协变的注意事项
- 协变仅支持返回值为**"指针" 或 "引用"**的情况,返回值为普通对象时不支持;
- 派生类虚函数的返回值类型必须是基类虚函数返回值类型的子类(即存在继承关系);
- 协变的实际应用场景较少,主要用于一些特殊的设计模式(如工厂模式),了解即可。
3.2 析构函数的重写:避免内存泄漏
析构函数的重写是面试中最高频的考点之一。很多开发者在使用多态时,容易忽略析构函数的虚函数声明,导致派生类对象的资源无法释放,引发内存泄漏。
下面通过一个问题场景来引入对析构函数重写的介绍------未声明虚析构函数:
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Base {
public:
    Base() {
        cout << "Base::构造函数" << endl;
        _p = new int[10]; // 动态分配内存
    }
    
    // 非虚析构函数
    ~Base() {
        cout << "Base::析构函数" << endl;
        delete[] _p; // 释放内存
    }
private:
    int* _p;
};
class Derive : public Base {
public:
    Derive() {
        cout << "Derive::构造函数" << endl;
        _q = new char[100]; // 动态分配内存
    }
    
    ~Derive() {
        cout << "Derive::析构函数" << endl;
        delete[] _q; // 释放内存
    }
private:
    char* _q;
};
int main() {
    Base* p = new Derive(); // 基类指针指向派生类对象
    delete p; // 释放对象
    
    return 0;
}运行结果:
            
            
              cpp
              
              
            
          
          Base::构造函数
Derive::构造函数
Base::析构函数问题分析:
- delete p时,由于基类Base的析构函数不是虚函数,编译器根据基类指针的静态类型(Base*)调用基类的析构函数;
- 派生类Derive的析构函数没有被调用,导致**_q**指向的内存无法释放,引发内存泄漏。
解决方案:声明虚析构函数
将基类的析构函数声明为虚函数,派生类的析构函数会自动构成重写,从而触发多态析构。
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Base {
public:
    Base() {
        cout << "Base::构造函数" << endl;
        _p = new int[10];
    }
    
    // 虚析构函数
    virtual ~Base() {
        cout << "Base::析构函数" << endl;
        delete[] _p;
    }
private:
    int* _p;
};
class Derive : public Base {
public:
    Derive() {
        cout << "Derive::构造函数" << endl;
        _q = new char[100];
    }
    
    // 派生类析构函数:自动重写基类虚析构
    ~Derive() {
        cout << "Derive::析构函数" << endl;
        delete[] _q;
    }
private:
    char* _q;
};
int main() {
    Base* p = new Derive();
    delete p;
    
    return 0;
}运行结果 :
            
            
              cpp
              
              
            
          
          Base::构造函数
Derive::构造函数
Derive::析构函数
Base::析构函数原理说明:
- 编译器会对析构函数的名称做特殊处理,编译后所有析构函数的名称统一为destructor;
- 基类析构函数声明为virtual后,派生类析构函数会自动重写该虚函数;
- delete p时,通过基类指针调用虚析构函数,触发多态,先调用派生类析构函数(释放派生类资源),再调用基类析构函数(释放基类资源),避免内存泄漏。
因此,只要类可能被继承,并且可能通过基类指针删除派生类对象,就必须将基类的析构函数声明为虚函数;若类不会被继承,或不会通过基类指针删除派生类对象,可不用声明虚析构函数(因为虚函数会增加对象内存开销)。
3.3 override 和 final 关键字:增强重写的安全性
C++11 引入了override和final两个关键字,用于解决虚函数重写中的 "隐式错误",增强代码的可读性和安全性。
3.3.1 override:验证重写是否正确
 override关键字用于派生类的虚函数,表示该函数意图重写基类的虚函数。编译器会检查是否满足重写条件,若不满足则编译报错。
示例:override 的使用
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Car {
public:
    // 基类虚函数:注意函数名是Drive(拼写正确)
    virtual void Drive() {
        cout << "汽车:基本行驶功能" << endl;
    }
};
class Benz : public Car {
public:
    // 意图重写Drive函数,但拼写错误(Dirve)
    virtual void Dirve() override { // 编译报错:没有重写任何基类方法
        cout << "奔驰:舒适行驶" << endl;
    }
};
int main() {
    Benz benz;
    return 0;
}VS 编译器下的编译错误信息:
            
            
              cpp
              
              
            
          
          error C3668: "Benz::Dirve": 包含重写说明符"override"的方法没有重写任何基类方法作用:
- 强制编译器验证重写条件(三同原则),避免因拼写错误、参数不匹配等导致的重写失败;
- 明确告知其他开发者该函数是重写基类的虚函数,提高代码可读性。
3.3.2 final:禁止虚函数被重写
 final关键字用于基类的虚函数,表示该函数禁止被任何派生类重写;也可用于类,表示该类禁止被继承。
示例 1:禁止虚函数被重写
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Car {
public:
    // final修饰虚函数:禁止派生类重写
    virtual void Drive() final {
        cout << "汽车:基本行驶功能" << endl;
    }
};
class BMW : public Car {
public:
    // 试图重写被final修饰的函数,编译报错
    virtual void Drive() {
        cout << "宝马:操控性行驶" << endl;
    }
};
int main() {
    BMW bmw;
    return 0;
}VS 编译器下的编译错误信息:
            
            
              cpp
              
              
            
          
          error C3248: "Car::Drive": 声明为"final"的函数无法被"BMW::Drive"重写示例 2:禁止类被继承
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
// final修饰类:禁止被继承
class Car final {
public:
    virtual void Drive() {
        cout << "汽车:基本行驶功能" << endl;
    }
};
// 试图继承被final修饰的类,编译报错
class Audi : public Car {
public:
    virtual void Drive() {
        cout << "奥迪:科技感行驶" << endl;
    }
};
int main() {
    Audi audi;
    return 0;
}VS 编译器下的编译错误信息:
            
            
              cpp
              
              
            
          
          error C3246: "Audi": 无法从"Car"继承,因为它已被声明为"final"作用:
- 限制虚函数的重写或类的继承,避免不必要的扩展,保证代码的稳定性;
- 明确告知其他开发者该函数 / 类不允许被修改,提高代码可维护性。
3.4 重载、重写、隐藏的区别(面试高频)
C++ 中函数的重载、重写、隐藏是三个容易混淆的概念,面试中经常会以选择题或简答题的形式考察。通过下面这张图来详细对比三者的区别:
示例:三者的直观对比
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Base {
public:
    // 1. 重载:同一作用域,参数列表不同
    void Func(int a) {
        cout << "Base::Func(int): " << a << endl;
    }
    
    void Func(double a) {
        cout << "Base::Func(double): " << a << endl;
    }
    
    // 虚函数:用于重写
    virtual void Show() {
        cout << "Base::Show()" << endl;
    }
    
    // 普通函数:用于隐藏
    void Display() {
        cout << "Base::Display()" << endl;
    }
};
class Derive : public Base {
public:
    // 2. 重写:基类虚函数,三同原则
    virtual void Show() {
        cout << "Derive::Show()" << endl;
    }
    
    // 3. 隐藏:与基类同名,不满足重写条件
    void Display() {
        cout << "Derive::Display()" << endl;
    }
    
    // 隐藏:与基类Func同名,但参数列表不同(非重载,因为作用域不同)
    void Func(const char* str) {
        cout << "Derive::Func(const char*): " << str << endl;
    }
};
int main() {
    Derive d;
    
    // 测试重载:调用Base类的重载函数
    d.Func(10);        // 输出:Base::Func(int): 10
    d.Func(3.14);      // 输出:Base::Func(double): 3.14
    
    // 测试隐藏:调用Derive类的Func(隐藏基类Func)
    d.Func("hello");   // 输出:Derive::Func(const char*): hello
    
    // 测试重写:基类指针指向派生类,多态调用
    Base* p = &d;
    p->Show();         // 输出:Derive::Show()(重写,多态)
    
    // 测试隐藏:基类指针调用基类Display,派生类对象调用派生类Display
    p->Display();      // 输出:Base::Display()(隐藏,静态绑定)
    d.Display();       // 输出:Derive::Display()(隐藏,静态绑定)
    
    return 0;
}运行结果:
            
            
              cpp
              
              
            
          
          Base::Func(int): 10
Base::Func(double): 3.14
Derive::Func(const char*): hello
Derive::Show()
Base::Display()
Derive::Display()结论:
- 重载看**"同一作用域 + 参数不同"**;
- 重写看 "不同作用域 + 三同 + 虚函数";
- 隐藏看**"不同作用域 + 同名 + 非重写"**;
- 只有重写能触发多态,重载和隐藏都是静态绑定。
四、纯虚函数和抽象类
在实际开发中,有些基类只需要定义接口,不需要实现具体功能,具体功能由派生类实现。这时就需要用到纯虚函数和抽象类。
4.1 纯虚函数的定义
纯虚函数是指在基类中声明的、没有具体实现 的虚函数,语法格式为在虚函数声明后加上**=0**。
语法格式:
            
            
              cpp
              
              
            
          
          class 基类名 {
public:
    virtual 返回值类型 函数名(参数列表) = 0; // 纯虚函数
};需要注意以下两点:
- 纯虚函数不需要实现(语法上允许提供实现,但无实际意义);
- 包含纯虚函数的类称为抽象类。
4.2 抽象类的特性
抽象类是一种特殊的类,具有以下核心特性:
- 抽象类不能实例化对象(编译报错);
- 派生类 必须重写 抽象类中的所有纯虚函数,否则派生类也会成为抽象类,无法实例化;
- 抽象类可以定义普通成员函数和成员变量;
- 抽象类的指针 / 引用可以指向其派生类对象(用于实现多态)。
示例:纯虚函数和抽象类的使用
            
            
              cpp
              
              
            
          
          #include <iostream>
#include <string>
using namespace std;
// 抽象类:形状(包含纯虚函数)
class Shape {
public:
    Shape(string name) : _name(name) {}
    
    // 纯虚函数:计算面积(仅声明,无实现)
    virtual double CalculateArea() = 0;
    
    // 纯虚函数:计算周长(仅声明,无实现)
    virtual double CalculatePerimeter() = 0;
    
    // 普通成员函数:显示形状名称
    void ShowName() {
        cout << "形状:" << _name << endl;
    }
protected:
    string _name;
};
// 派生类:圆形(重写所有纯虚函数)
class Circle : public Shape {
public:
    Circle(string name, double radius) : Shape(name), _radius(radius) {}
    
    // 重写纯虚函数:计算面积(圆面积=πr²)
    virtual double CalculateArea() {
        return 3.14159 * _radius * _radius;
    }
    
    // 重写纯虚函数:计算周长(圆周长=2πr)
    virtual double CalculatePerimeter() {
        return 2 * 3.14159 * _radius;
    }
private:
    double _radius; // 半径
};
// 派生类:矩形(重写所有纯虚函数)
class Rectangle : public Shape {
public:
    Rectangle(string name, double length, double width) 
        : Shape(name), _length(length), _width(width) {}
    
    // 重写纯虚函数:计算面积(矩形面积=长×宽)
    virtual double CalculateArea() {
        return _length * _width;
    }
    
    // 重写纯虚函数:计算周长(矩形周长=2×(长+宽))
    virtual double CalculatePerimeter() {
        return 2 * (_length + _width);
    }
private:
    double _length; // 长
    double _width;  // 宽
};
// 派生类:三角形(未重写所有纯虚函数,仍为抽象类)
class Triangle : public Shape {
public:
    Triangle(string name, double a, double b, double c) 
        : Shape(name), _a(a), _b(b), _c(c) {}
    
    // 仅重写一个纯虚函数,另一个未重写
    virtual double CalculatePerimeter() {
        return _a + _b + _c;
    }
private:
    double _a, _b, _c; // 三边长
};
// 统一接口:计算并显示形状的面积和周长
void ShowShapeInfo(Shape* shape) {
    shape->ShowName();
    cout << "面积:" << shape->CalculateArea() << endl;
    cout << "周长:" << shape->CalculatePerimeter() << endl;
    cout << "------------------------" << endl;
}
int main() {
    // 1. 抽象类不能实例化对象(编译报错)
    // Shape shape("未知形状"); 
    
    // 2. 派生类(重写所有纯虚函数)可以实例化
    Circle circle("圆形", 5.0);
    Rectangle rect("矩形", 4.0, 6.0);
    
    // 3. 未重写所有纯虚函数的派生类不能实例化(编译报错)
    // Triangle tri("三角形", 3.0, 4.0, 5.0);
    
    // 4. 抽象类指针指向派生类对象,实现多态
    ShowShapeInfo(&circle);
    ShowShapeInfo(&rect);
    
    return 0;
}运行结果:
            
            
              cpp
              
              
            
          
          形状:圆形
面积:78.5397
周长:31.4159
------------------------
形状:矩形
面积:24
周长:20
------------------------4.3 抽象类的应用场景
抽象类的核心价值是定义统一接口,强制派生类实现特定功能,常见应用场景包括:
- **框架开发:**定义框架的核心接口,具体实现由用户自定义的派生类完成;
- **设计模式:**如工厂模式、策略模式等,通过抽象类定义产品或策略的接口;
- **团队协作:**统一代码规范,确保不同开发者实现的派生类都包含必要的功能接口。
例如,在图形处理软件中,抽象类Shape定义了所有图形必须具备的 "计算面积" 和 "计算周长" 接口,后续新增 "正方形""椭圆形" 等图形时,只需继承Shape并重写纯虚函数,即可无缝集成到现有代码中,无需修改原有接口逻辑。
五、多态的底层原理:虚函数表与动态绑定
很多人在使用多态的时候,只知道 "怎么用",却不知道 "为什么能这样用"。理解多态的底层原理,不仅能帮助我们更灵活地使用多态,也是面试中的核心考点(比如 "虚函数表是什么?""动态绑定的过程是怎样的?"等)。
5.1 虚函数表指针(__vfptr)
在 C++ 中,当一个类包含虚函数时,编译器会为该类的每个对象添加一个隐藏的成员变量 ------虚函数表指针(virtual function table pointer) ,简写为**__vfptr**。
5.1.1 虚函数表指针的本质
- __vfptr是一个指针,指向一个存储虚函数地址的数组,这个数组称为虚函数表(virtual function table) ,简称虚表(vtable);
- 一个类的所有对象共用同一张虚表(虚表存储在代码段 / 常量区,而非对象中);
- 虚函数表指针的大小为 4 字节(32 位系统)或 8 字节(64 位系统),独立于类的其他成员变量。
5.1.2 包含虚函数的类的对象大小
下面通过示例验证包含虚函数的类的对象大小(以 32 位系统为例):
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
// 普通类(无虚函数)
class Base1 {
public:
    void Func() {}
private:
    int _a = 1;
    char _ch = 'x';
};
// 包含虚函数的类
class Base2 {
public:
    virtual void Func() {}
private:
    int _a = 1;
    char _ch = 'x';
};
int main() {
    cout << "Base1对象大小:" << sizeof(Base1) << endl; // 输出:8(int占4字节,char占1字节,内存对齐为8字节)
    cout << "Base2对象大小:" << sizeof(Base2) << endl; // 输出:12(4字节__vfptr + 4字节int + 1字节char,内存对齐为12字节)
    
    return 0;
}运行结果(32 位系统):
            
            
              cpp
              
              
            
          
          Base1对象大小:8
Base2对象大小:12分析:
- Base1无虚函数,对象大小由成员变量决定(int 4 字节 + char 1 字节,内存对齐为 8 字节);
- Base2包含虚函数,对象大小 = 虚函数表指针(4 字节) + 成员变量大小(8 字节),内存对齐后为 12 字节。
这证明了包含虚函数的类的对象中,确实存在一个隐藏的虚函数表指针。
5.2 虚函数表(vtable)的结构
虚函数表是一个存储虚函数地址的数组,其结构取决于类的继承关系和虚函数重写情况。
5.2.1 基类的虚表
基类包含虚函数时,编译器会为其生成一张虚表,虚表中存储基类所有虚函数的地址。
示例:基类虚表结构
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Base {
public:
    virtual void Func1() { cout << "Base::Func1" << endl; }
    virtual void Func2() { cout << "Base::Func2" << endl; }
private:
    int _a = 1;
};
int main() {
    Base b;
    // 通过内存查看虚表结构(32位系统)
    // b的内存布局:__vfptr(4字节) + _a(4字节)
    // __vfptr指向虚表,虚表中存储Func1和Func2的地址,末尾可能有nullptr标记(编译器相关)
    return 0;
}基类虚表的结构示意如下:
            
            
              cpp
              
              
            
          
          Base的虚表(vtable for Base):
[0] → &Base::Func1
[1] → &Base::Func2
[2] → nullptr(VS编译器标记,g++无此标记)5.2.2 派生类的虚表
派生类继承基类后,会继承基类的虚表指针和虚表。当派生类重写基类的虚函数时,会将虚表中对应基类虚函数的地址替换为派生类重写函数的地址;同时,派生类新增的虚函数会被添加到虚表的末尾。
示例:派生类虚表结构
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Base {
public:
    virtual void Func1() { cout << "Base::Func1" << endl; }
    virtual void Func2() { cout << "Base::Func2" << endl; }
private:
    int _a = 1;
};
class Derive : public Base {
public:
    // 重写基类Func1
    virtual void Func1() { cout << "Derive::Func1" << endl; }
    // 新增虚函数Func3
    virtual void Func3() { cout << "Derive::Func3" << endl; }
private:
    int _b = 2;
};
int main() {
    Derive d;
    // d的内存布局:__vfptr(4字节) + 继承的_a(4字节) + 自身的_b(4字节)
    // __vfptr指向派生类的虚表,虚表中Func1被替换为Derive::Func1,新增Func3
    return 0;
}派生类虚表的结构示意如下:
            
            
              cpp
              
              
            
          
          Derive的虚表(vtable for Derive):
[0] → &Derive::Func1(重写,替换基类Func1地址)
[1] → &Base::Func2(未重写,继承基类Func2地址)
[2] → &Derive::Func3(新增,添加到虚表末尾)
[3] → nullptr(VS编译器标记)5.2.3 虚表的存储位置
虚函数表存储在代码段(常量区),而非堆或栈中。可以通过以下代码验证:
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Base {
public:
    virtual void Func1() {}
};
class Derive : public Base {
public:
    virtual void Func1() {}
};
int main() {
    // 栈:局部变量
    int i = 0;
    // 静态区:静态变量
    static int j = 1;
    // 堆:动态分配内存
    int* pHeap = new int;
    // 常量区:字符串常量
    const char* pConst = "hello";
    
    Base b;
    Derive d;
    Base* pBase = &b;
    Derive* pDerive = &d;
    
    // 输出各区域地址
    cout << "栈地址:" << &i << endl;
    cout << "静态区地址:" << &j << endl;
    cout << "堆地址:" << pHeap << endl;
    cout << "常量区地址:" << (void*)pConst << endl;
    cout << "Base虚表地址:" << *(int*)pBase << endl; // 虚表指针指向的地址即虚表地址
    cout << "Derive虚表地址:" << *(int*)pDerive << endl;
    
    delete pHeap;
    return 0;
}运行结果(32 位系统):
            
            
              cpp
              
              
            
          
          栈地址:0x0019FF3C
静态区地址:0x0041D000
堆地址:0x0042D740
常量区地址:0x0041ABA4
Base虚表地址:0x0041AB44
Derive虚表地址:0x0041AB84我们也可以通过VS中的监视窗口和内存窗口进行观察:


分析:
- 虚表地址(0x0041AB44、0x0041AB84)与常量区地址(0x0041ABA4)非常接近,说明虚表存储在代码段 / 常量区;
- 栈、堆、静态区的地址与虚表地址差异较大,进一步验证了虚表的存储位置。
5.3 动态绑定与静态绑定
多态的实现本质是动态绑定 ,而普通函数调用是静态绑定。二者的核心区别在于函数地址的确定时机。
5.3.1 静态绑定(Static Binding)
静态绑定是指在编译阶段就确定函数的调用地址,直接生成函数调用指令。
适用场景:
- 普通函数调用;
- 静态成员函数调用;
- 通过对象直接调用虚函数(非基类指针 / 引用);
- 不满足多态条件的函数调用。
示例:静态绑定的汇编代码分析
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Base {
public:
    void Func() { cout << "Base::Func" << endl; } // 普通函数
};
int main() {
    Base b;
    b.Func(); // 静态绑定
    return 0;
}对应的汇编代码(VS 编译器,32 位)如下:
            
            
              cpp
              
              
            
          
          0041141E  lea         ecx,[b]
00411421  call        Base::Func (04110ACh) // 直接调用Func的地址04110AC分析 :编译时直接确定Func的地址(04110AC),生成call指令调用该地址,无运行时开销。
5.3.2 动态绑定(Dynamic Binding)
动态绑定是指在运行阶段通过虚函数表指针查找虚函数地址,再调用函数。
适用场景:通过基类指针 / 引用调用虚函数(满足多态条件)。
示例:动态绑定的汇编代码分析
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class Base {
public:
    virtual void Func() { cout << "Base::Func" << endl; } // 虚函数
};
class Derive : public Base {
public:
    virtual void Func() { cout << "Derive::Func" << endl; } // 重写虚函数
};
void CallFunc(Base* p) {
    p->Func(); // 动态绑定
}
int main() {
    Derive d;
    CallFunc(&d);
    return 0;
}对应的汇编代码(VS 编译器,32 位)如下:
            
            
              cpp
              
              
            
          
          void CallFunc(Base* p) {
00411450  push        ebp
00411451  mov         ebp,esp
00411453  push        esi
00411454  mov         esi,dword ptr [p] // esi = p(基类指针)
    p->Func();
00411457  mov         eax,dword ptr [esi] // eax = p->__vfptr(虚表指针)
00411459  mov         edx,dword ptr [eax] // edx = 虚表[0](Func的地址)
0041145B  mov         ecx,dword ptr [p] // ecx = p(this指针)
0041145E  call        edx // 调用edx中的地址(Derive::Func)
00411460  pop         esi
00411461  pop         ebp
00411462  ret
}动态绑定的过程如下:
- 取出基类指针
p指向的对象中的虚表指针**__vfptr**(*eax = p);- 从虚表中取出对应的虚函数地址(*edx = eax,即虚表第 0 个元素);
- 调用该地址对应的函数(call edx);
- 由于p指向Derive对象,虚表中存储的是Derive::Func的地址,因此最终调用Derive::Func。
这就是多态的底层实现原理:通过虚函数表指针和虚表,在运行时动态查找函数地址,实现 "同一接口,多种实现"。
5.4 多态的性能开销
多态的灵活性是以轻微的性能开销为代价的,主要体现在两个方面:
- 内存开销:包含虚函数的类的每个对象都会增加一个虚函数表指针(4/8 字节);
- 时间开销 :动态绑定需要在运行时查找虚表,比静态绑定多两次指针间接访问(
__vfptr→ 虚表 → 虚函数地址)。
但在大多数场景下,这种开销是可以忽略不计的。只有在对性能要求极高的核心模块(如高频调用的函数),才需要考虑是否使用多态。
六、多态的面试高频题解析
多态是 C++ 面试的重中之重,以下整理了常见的面试题及详细解析,帮助读者应对面试。
6.1 选择题:虚函数重写与多态判断
题目:以下程序的输出结果是什么?( )
            
            
              cpp
              
              
            
          
          #include <iostream>
using namespace std;
class A {
public:
    virtual void func(int val = 1) { cout << "A->" << val << endl; }
    virtual void test() { func(); }
};
class B : public A {
public:
    void func(int val = 0) { cout << "B->" << val << endl; }
};
int main() {
    B* p = new B;
    p->test();
    delete p;
    return 0;
}选项:A. A->0 B. B->1 C. A->1 D. B->0 E. 编译出错 F. 以上都不正确
解析:
- A::func是虚函数,B::func与A::func满足三同原则(函数名、参数列表、返回值相同),构成重写;
- p是B类型,调用test()函数(继承自A*),test()中调用func();
- func()是虚函数,通过this指针(B类型)调用,触发多态,调用B::func*;
- 虚函数的默认参数是由基类函数声明 决定的(静态绑定),而非派生类。A::func的默认参数是 1,因此val的值为 1;
- 最终输出B->1。
因此最终答案为:B
6.2 简答题:多态的实现条件
题目:C++ 中运行时多态的实现需要满足哪些条件?
解析:
- 存在继承关系(基类和派生类,公有继承);
- 基类声明虚函数 (
virtual修饰);- 派生类重写基类的虚函数(满足三同原则,协变除外);
- 通过基类的指针或引用调用虚函数。
6.3 简答题:虚函数表是什么?它存储在哪里?
题目:什么是虚函数表?虚函数表存储在内存的哪个区域?
解析:
- 虚函数表(vtable)是一个存储虚函数地址的指针数组,由编译器自动生成;
- 包含虚函数的类的每个对象都会有一个虚函数表指针(__vfptr),指向该类的虚表;
- 虚函数表存储在代码段(常量区),而非堆、栈或静态区;
- 同一类的所有对象共用同一张虚表,不同类(基类和派生类)有各自独立的虚表。
6.4 简答题:为什么析构函数要声明为虚函数?
题目:为什么基类的析构函数建议声明为虚函数?如果不声明会有什么问题?
解析:
- 目的:避免内存泄漏。当通过基类指针删除派生类对象时,确保派生类的析构函数被调用;
- 问题:若基类析构函数不是虚函数,删除基类指针指向的派生类对象时,编译器会根据基类指针的静态类型调用基类析构函数,派生类析构函数不会被调用,导致派生类中动态分配的资源无法释放,引发内存泄漏;
- 原理:基类析构函数声明为虚函数后,派生类析构函数会自动重写该虚函数(编译器统一处理析构函数名称),删除对象时触发多态,先调用派生类析构函数,再调用基类析构函数。
6.5 编程题:利用多态实现计算器
题目:利用 C++ 多态实现一个计算器,支持加法、减法、乘法、除法运算,要求可以灵活扩展新的运算(如取模、平方)。
解析:
- 定义抽象基类Calculator,声明纯虚函数Calculate(统一接口);
- 定义派生类Add、Subtract、Multiply、Divide,分别重写Calculate函数,实现对应运算;
- 新增运算时,只需新增派生类并重写Calculate,无需修改原有代码。
实现代码:
            
            
              cpp
              
              
            
          
          #include <iostream>
#include <stdexcept>
using namespace std;
// 抽象基类:计算器
class Calculator {
public:
    Calculator(double a, double b) : _a(a), _b(b) {}
    virtual ~Calculator() {}
    
    // 纯虚函数:计算接口
    virtual double Calculate() const = 0;
    
    // 获取操作数
    double GetA() const { return _a; }
    double GetB() const { return _b; }
protected:
    double _a; // 操作数1
    double _b; // 操作数2
};
// 派生类:加法
class Add : public Calculator {
public:
    Add(double a, double b) : Calculator(a, b) {}
    virtual double Calculate() const {
        return GetA() + GetB();
    }
};
// 派生类:减法
class Subtract : public Calculator {
public:
    Subtract(double a, double b) : Calculator(a, b) {}
    virtual double Calculate() const {
        return GetA() - GetB();
    }
};
// 派生类:乘法
class Multiply : public Calculator {
public:
    Multiply(double a, double b) : Calculator(a, b) {}
    virtual double Calculate() const {
        return GetA() * GetB();
    }
};
// 派生类:除法
class Divide : public Calculator {
public:
    Divide(double a, double b) : Calculator(a, b) {
        if (b == 0) {
            throw invalid_argument("除数不能为0");
        }
    }
    virtual double Calculate() const {
        return GetA() / GetB();
    }
};
// 派生类:取模(新增运算,无需修改原有代码)
class Mod : public Calculator {
public:
    Mod(int a, int b) : Calculator(a, b) {
        if (b == 0) {
            throw invalid_argument("模数不能为0");
        }
    }
    virtual double Calculate() const {
        return static_cast<int>(GetA()) % static_cast<int>(GetB());
    }
};
// 统一接口:执行计算并输出结果
void DoCalculate(const Calculator& calc, const string& opName) {
    cout << calc.GetA() << " " << opName << " " << calc.GetB() << " = " << calc.Calculate() << endl;
}
int main() {
    try {
        Add add(10, 5);
        Subtract sub(10, 5);
        Multiply mul(10, 5);
        Divide div(10, 5);
        Mod mod(10, 3);
        
        DoCalculate(add, "+");    // 输出:10 + 5 = 15
        DoCalculate(sub, "-");    // 输出:10 - 5 = 5
        DoCalculate(mul, "*");    // 输出:10 * 5 = 50
        DoCalculate(div, "/");    // 输出:10 / 5 = 2
        DoCalculate(mod, "%");    // 输出:10 % 3 = 1
        
        // 测试除数为0的异常
        // Divide div2(10, 0);
    } catch (const exception& e) {
        cout << "错误:" << e.what() << endl;
    }
    
    return 0;
}运行结果:
            
            
              cpp
              
              
            
          
          10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2
10 % 3 = 1总结
多态是 C++ 面向对象编程的灵魂,掌握多态的概念、实现和原理,不仅能写出更灵活、可维护的代码,也是成为高级 C++ 开发者的必备技能。建议大家结合本文的示例代码反复练习,深入理解每个细节,尤其是虚函数表和动态绑定的底层逻辑,应对面试时才能游刃有余。我们下期再见!