多态类原理+四种类型转换+异常处理

一、运行时多态的核心原理:虚函数与虚函数表

运行时多态是面向对象的核心特性,允许基类指针 / 引用在运行时根据实际指向的对象类型,调用对应派生类的成员函数 。其底层实现依赖虚函数表 (vtable)虚函数表指针 (__vfptr)

1.1 虚函数的存储机制

(1)虚函数表(vtable)
  • 本质:一个存储虚函数地址的静态数组 ,每个有虚函数的类会在编译期生成唯一的虚函数表。
  • 结构:数组元素是对应虚函数的入口地址,末尾通常包含一个 nullptr 作为结束标记,因此数组长度 = 类中虚函数个数 + 1。
  • 继承特性:派生类会完整继承基类的虚函数表;若派生类重写了某个虚函数,会覆盖表中对应位置的地址;新增的虚函数会追加到表的末尾。
(2)虚函数表指针(__vfptr)
  • 每个包含虚函数的类的对象 ,会在内存布局的最开头 隐藏一个__vfptr指针,指向所属类的虚函数表。
  • 关键特性:同一类的所有对象共享同一个虚函数表,因此每个对象仅需额外存储一个指针的空间(32 位系统 4 字节,64 位系统 8 字节)。

1.2 运行时多态的执行过程

多态调用的核心是编译期只做语法检查,运行期才绑定具体函数实现

  1. 编译阶段 :编译器仅检查基类是否存在该虚函数,若存在则生成 "通过__vfptr调用虚函数表中对应位置" 的代码,不绑定具体函数地址。
  2. 运行阶段
    • 通过基类指针 / 引用获取实际对象的__vfptr
    • 通过__vfptr找到对象实际所属类的虚函数表;
    • 调用表中对应位置的函数,实现动态绑定。

1.3 代码实例:形状类的多态实现

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

// 基类:形状
class Shape {
public:
    // 虚函数:绘制形状
    virtual void draw() const { cout << "绘制形状" << endl; }
    // 虚析构函数:必须声明为虚,防止内存泄漏
    virtual ~Shape() = default;
};

// 派生类:圆形
class Circle : public Shape {
public:
    // 重写虚函数(C++11推荐加override关键字)
    void draw() const override { cout << "绘制圆形" << endl; }
};

// 派生类:矩形
class Rectangle : public Shape {
public:
    void draw() const override { cout << "绘制矩形" << endl; }
};

int main() {
    // 基类指针指向不同派生类对象
    Shape* shape1 = new Circle();
    Shape* shape2 = new Rectangle();

    // 运行时多态:调用对应派生类的draw()
    shape1->draw(); // 输出:绘制圆形
    shape2->draw(); // 输出:绘制矩形

    delete shape1;
    delete shape2;
    return 0;
}

1.4 面试高频考点

  1. 虚函数表的存储位置 :编译期生成,存储在程序的只读数据段 (.rodata),运行时不可修改。
  2. 空类与有虚函数类的大小
    • 空类大小为 1 字节(用于区分不同对象的地址);
    • 有一个虚函数的类大小为sizeof(void*)(32 位 4 字节,64 位 8 字节),即__vfptr的大小。
  3. 构造函数 / 析构函数能否为虚函数?
    • 构造函数不能:构造时对象尚未完全创建,__vfptr还未初始化,无法进行动态绑定;
    • 析构函数建议声明为虚:当用基类指针删除派生类对象时,会先调用派生类析构函数,再调用基类析构函数,防止内存泄漏。
  4. 静态成员函数能否为虚函数? 不能:静态成员函数属于类而非对象,没有this指针,无法访问__vfptr

二、纯虚函数与抽象类

纯虚函数用于定义接口规范,强制派生类必须实现指定功能,是实现 "接口与实现分离" 的核心手段。

2.1 纯虚函数的语法

复制代码
virtual 返回类型 函数名(参数列表) = 0;
  • = 0是语法标记,不是赋值为 0,表示该函数在当前类中没有默认实现
  • 注意:纯虚函数可以在类外提供实现(用于给派生类提供默认逻辑),但派生类仍需重写该函数才能实例化。

2.2 抽象类

(1)定义

包含至少一个纯虚函数的类称为抽象类。

(2)核心特性
  • 不能创建抽象类的实例(但可以声明抽象类的指针 / 引用,用于指向派生类对象);
  • 只能作为基类被继承;
  • 若派生类未实现基类的所有纯虚函数,则派生类仍是抽象类,无法实例化。

2.3 接口类

(1)定义

所有成员函数都是纯虚函数,且没有非静态成员变量的类称为接口类。

(2)作用

定义统一的接口规范,支持 C++ 的多继承(接口类的多继承不会产生二义性,因为没有成员变量和普通成员函数)。

2.4 代码实例:计算器抽象类

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

// 抽象类:计算器接口
class AbsCompute {
public:
    // 纯虚函数:计算功能,强制派生类实现
    virtual int calculate(int a, int b) const = 0;
    virtual ~AbsCompute() = default;
};

// 派生类:加法计算器
class Add : public AbsCompute {
public:
    int calculate(int a, int b) const override { return a + b; }
};

// 派生类:减法计算器
class Sub : public AbsCompute {
public:
    int calculate(int a, int b) const override { return a - b; }
};

// 通用计算函数:依赖抽象接口,不依赖具体实现
void compute(const AbsCompute& calc, int a, int b) {
    cout << "计算结果:" << calc.calculate(a, b) << endl;
}

int main() {
    Add add;
    Sub sub;
    compute(add, 10, 5); // 输出:计算结果:15
    compute(sub, 10, 5); // 输出:计算结果:5
    return 0;
}

2.5 面试高频考点

  1. 纯虚函数与普通虚函数的区别

    表格

    特性 普通虚函数 纯虚函数
    有无默认实现 无(可在类外提供,但不强制)
    所在类能否实例化 不能(所在类为抽象类)
    派生类是否必须重写 是(否则派生类仍是抽象类)
  2. 抽象类与接口类的区别

    • 抽象类可以包含普通成员函数、成员变量和纯虚函数;
    • 接口类只能包含纯虚函数,不能有成员变量。
  3. 纯虚函数的实现场景:当多个派生类需要共享部分默认逻辑时,可以在基类中给纯虚函数提供实现,派生类重写时可以调用基类的实现。


三、C++ 四种类型转换

C++ 提供了四种显式类型转换运算符,替代 C 语言的隐式 / 强制转换,目的是提高类型安全性和代码可读性。四种转换的适用场景和安全性各不相同,是面试必考点。

3.1 静态转换 static_cast

(1)特点
  • 编译期进行类型检查,无运行时开销;
  • 不能转换掉const/volatile属性;
  • 是 C++ 中最常用的转换,用于 "合理且安全" 的类型转换。
(2)适用场景
  1. 基本数据类型之间的转换 (如intdoublecharint):

    复制代码
    int a = 10;
    double b = static_cast<double>(a); // int转double
    char c = static_cast<char>(a);     // int转char
  2. void*与任意类型指针的互转

    复制代码
    int num = 20;
    void* p = &num;
    int* q = static_cast<int*>(p); // void*转int*
  3. 基类与派生类之间的指针 / 引用转换

    • 上行转换(派生类→基类):安全,编译器自动支持;

    • 下行转换(基类→派生类):不安全,因为没有运行时类型检查,若基类指针实际指向基类对象,转换后访问派生类成员会导致未定义行为。

      Derived d;
      Base* b = static_cast<Base*>(&d); // 上行转换,安全
      Derived* d2 = static_cast<Derived*>(b); // 下行转换,不安全(需程序员保证b实际指向Derived)

  4. 左值转右值引用std::move的底层实现):

    复制代码
    int x = 10;
    int&& r = static_cast<int&&>(x); // 将左值x转为右值引用

3.2 动态转换 dynamic_cast

(1)特点
  • 运行期进行类型检查(基于RTTI 运行时类型识别机制);
  • 仅适用于有虚函数的类的指针 / 引用转换;
  • 是唯一安全的下行转换方式。
(2)转换结果
  • 指针转换:成功返回目标类型指针,失败返回nullptr
  • 引用转换:成功返回目标类型引用,失败抛出std::bad_cast异常。
(3)RTTI 机制与typeid运算符

RTTI 允许在运行时获取对象的实际类型,核心是typeid运算符:

  • typeid(表达式)返回std::type_info对象的引用;
  • type_info::name()返回类型名称(不同编译器输出格式不同);
  • typeid(*p)获取指针p指向对象的实际类型,typeid(p)获取指针本身的类型。
(4)代码实例
cpp 复制代码
#include <iostream>
#include <typeinfo>
using namespace std;

class Base {
public:
    virtual void func() {} // 必须有虚函数才能使用dynamic_cast
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void func() override {}
    void derivedFunc() { cout << "派生类特有函数" << endl; }
};

int main() {
    Base* b1 = new Derived();
    Base* b2 = new Base();

    // 下行转换:成功
    Derived* d1 = dynamic_cast<Derived*>(b1);
    if (d1) {
        d1->derivedFunc(); // 输出:派生类特有函数
    }

    // 下行转换:失败,返回nullptr
    Derived* d2 = dynamic_cast<Derived*>(b2);
    if (!d2) {
        cout << "转换失败,b2实际指向Base对象" << endl;
    }

    // typeid获取实际类型
    cout << "b1实际类型:" << typeid(*b1).name() << endl; // 输出Derived的类型名
    cout << "b2实际类型:" << typeid(*b2).name() << endl; // 输出Base的类型名

    delete b1;
    delete b2;
    return 0;
}

3.3 常转换 const_cast

(1)特点
  • 唯一能转换掉const/volatile属性的转换运算符;
  • 仅作用于指针或引用,不能转换基本数据类型。
(2)合法使用场景

原对象本身不是const类型 ,但被临时赋予了const属性(如函数参数为const指针,需要修改其指向的内容)。

(3)注意事项

若原对象是const类型,通过const_cast修改其内容是未定义行为 (编译器可能将const对象存储在只读内存段,修改会导致程序崩溃)。

(4)代码实例
cpp 复制代码
#include <iostream>
using namespace std;

// 函数参数为const指针,但原对象非const时可修改
void modify(int* p) { *p = 100; }

void func(const int* p) {
    // modify(p); // 错误:不能将const int*传给int*
    int* q = const_cast<int*>(p); // 转换掉const属性
    modify(q);
}

int main() {
    int a = 10; // 原对象非const
    const int* p = &a;
    func(p);
    cout << a << endl; // 输出:100,合法

    const int b = 20; // 原对象是const
    int* q = const_cast<int*>(&b);
    *q = 200; // 未定义行为!可能导致程序崩溃
    return 0;
}

3.4 重解释转换 reinterpret_cast

(1)特点
  • 最不安全的转换,不做任何类型检查,仅重新解释内存的二进制内容;
  • 可转换任意指针 / 引用类型,也可将指针与整数互转。
(2)适用场景

仅用于底层编程,需要直接操作内存二进制内容的场景:

  • 指针与整数的互转(如将指针保存为整数);
  • 不同类型指针的互转(如char*int*,按字节访问内存);
  • 底层硬件编程(如操作寄存器地址)。
(3)代码实例:查看整数的字节序
cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    int num = 0x12345678;
    // 将int*转为char*,按字节访问内存
    char* p = reinterpret_cast<char*>(&num);
    
    // 小端模式输出:78 56 34 12;大端模式输出:12 34 56 78
    for (int i = 0; i < 4; i++) {
        cout << hex << (int)p[i] << " ";
    }
    cout << endl;
    return 0;
}

3.5 四种转换对比与面试考点

转换运算符 检查时机 能否转换 const 适用场景 安全性
static_cast 编译期 不能 基本类型转换、void * 互转、安全的上下行转换 较高
dynamic_cast 运行期 不能 有虚函数类的安全下行转换 最高
const_cast 编译期 临时去除 const/volatile 属性
reinterpret_cast 编译期 不能 底层内存操作、指针与整数互转 最低
相关推荐
脆皮炸鸡7551 小时前
库制作与原理~动态链接
linux·开发语言·经验分享·笔记·学习方法
XMYX-01 小时前
26 - Go recover 捕获错误:优雅恢复的真正意义
开发语言·golang
王老师青少年编程1 小时前
csp信奥赛C++高频考点专项训练之字符串 --【回文字符串】:回文拼接
c++·字符串·csp·高频考点·信奥赛·字符串回文·回文拼接
小白学大数据1 小时前
基于大模型的Python智能爬虫:语义识别与数据清洗实践
开发语言·爬虫·python·数据分析
迷渡1 小时前
聊一聊 Bun 用 Rust 重写这件事
开发语言·后端·rust
古怪今人1 小时前
Gradle构建工具 Groovy/Kotlin DSL的现代化自动化构建工具
开发语言·kotlin·自动化
赏金术士1 小时前
Kotlin 协程与挂起函数(Coroutines & suspend)入门到实战
android·开发语言·kotlin
y = xⁿ2 小时前
Java并发八股学习日记
java·开发语言·学习
xifangge20252 小时前
【深度排障】从 OS 底层寻址剖析 javac 不是内部或外部命令 核心报错:变量空间隔离与自动化部署终极范式
java·开发语言·jdk·自动化