C++ 多态核心知识点笔记(整理与补充)

一、多态的概念

多态是面向对象编程(OOP)的核心特性之一,核心定义为:同一行为(调用同一方法)作用于不同对象时,表现出不同的执行结果

1.1 生活类比

以 “动物发声” 为例:

  • 行为统一:所有动物都有 “发出叫声” 的行为;
  • 结果不同:小狗执行 “叫” 时输出 “汪汪”,小猫执行 “叫” 时输出 “喵喵”;
  • 本质:通过统一接口(“叫”),触发不同对象的专属实现。

1.2 编程场景类比

以 “买票” 为例:

  • 行为统一:所有用户都有 “买票” 的行为;
  • 结果不同:普通人买票全价,学生买票半价;
  • 实现依赖:通过继承关系 + 虚函数,让 “买票” 接口表现出多态性。

二、多态的定义及实现

多态的实现需满足两个强制条件,核心依赖 “虚函数” 机制,以下结合代码详细说明。

2.1 多态实现的两个强制条件

  1. 通过基类的指针或引用调用目标函数;
  2. 被调用的函数是虚函数,且派生类重写了该虚函数。

2.2 核心组件:虚函数(Virtual Function)

2.2.1 虚函数的定义

virtual关键字修饰的类成员函数,称为虚函数。其作用是允许派生类重写该函数,为多态提供基础

cpp

运行

// 基类:Person(普通人)
class Person {
public:
    // 定义虚函数:买票(全价)
    virtual void BuyTicket() { 
        cout << "买票-全价" << endl; 
    }
};
2.2.2 虚函数的重写(覆盖)

派生类中定义与基类完全一致的虚函数(返回值类型、函数名、参数列表均相同),称为 “重写”(语法层面),底层本质是 “覆盖”(虚表中函数地址的替换)。

cpp

运行

// 派生类:Student(学生,继承自Person)
class Student : public Person {
public:
    // 重写基类的虚函数BuyTicket(半价)
    // 注:即使不加virtual,因继承基类虚函数的“虚属性”,仍构成重写;
    // 但规范要求显式加virtual,提升代码可读性
    virtual void BuyTicket() { 
        cout << "买票-半价" << endl; 
    }
};
2.2.3 多态调用的完整代码示例

cpp

运行

// 统一接口:通过基类引用调用虚函数
void Func(Person& people) {
    // 关键:调用时不依赖“引用的声明类型(Person)”,而依赖“实际指向的对象类型”
    people.BuyTicket(); 
}

int main() {
    Person Mike;    // 基类对象(普通人)
    Student Johnson;// 派生类对象(学生)
    
    Func(Mike);     // 实际对象:Person → 执行Person::BuyTicket → 输出“买票-全价”
    Func(Johnson);  // 实际对象:Student → 执行Student::BuyTicket → 输出“买票-半价”
    return 0;
}

2.3 多态与非多态的调用区别

调用场景绑定方式执行逻辑示例结果
满足多态条件(基类指针 / 引用 + 虚函数重写)动态绑定(运行时)看 “实际指向的对象类型”,执行对应虚函数Base* p = new Derive; p->Func () → 执行 Derive::Func
不满足多态条件(基类对象直接调用)静态绑定(编译时)看 “变量的声明类型”,执行对应函数Base b = Derive (); b.Func () → 执行 Base::Func

三、虚函数重写的特殊例外

虚函数重写的默认规则是 “返回值、函数名、参数列表完全一致”,但以下两种场景为合法例外,需单独掌握。

3.1 例外 1:协变(返回值类型不同)

3.1.1 定义

基类虚函数返回基类对象的指针 / 引用,派生类虚函数返回派生类对象的指针 / 引用(派生类与基类需有继承关系),仍构成重写,称为 “协变”。

3.1.2 代码示例

cpp

运行

// 辅助类:A(基类)、B(派生类)
class A {};
class B : public A {}; // B是A的子类

// 基类:Person
class Person {
public:
    // 虚函数返回A*(基类指针)
    virtual A* f() { 
        return new A; 
    }
};

// 派生类:Student
class Student : public Person {
public:
    // 重写虚函数,返回B*(派生类指针)→ 符合协变规则,构成重写
    virtual B* f() { 
        return new B; 
    }
};

3.2 例外 2:析构函数的重写(函数名不同)

3.2.1 核心原因

编译器会将所有类的析构函数名称统一处理为destructor,因此即使基类与派生类析构函数的表面名称不同(如~Person()~Student()),仍可构成重写。

3.2.2 关键问题:基类析构函数未设为虚函数的风险

若基类析构函数非虚函数,通过 “基类指针删除派生类对象” 时,仅会调用基类析构函数,导致派生类的资源(如堆内存)无法释放,引发资源泄漏

cpp

运行

// 错误示例:基类析构非虚函数
class Person {
public:
    // 非虚析构:风险点
    ~Person() { 
        cout << "~Person()" << endl; 
    }
};

class Student : public Person {
public:
    ~Student() { 
        cout << "~Student()" << endl; 
    }
};

int main() {
    Person* p2 = new Student; // 基类指针指向派生类对象
    delete p2; // 仅调用~Person(),未调用~Student() → 派生类资源泄漏
    return 0;
}
3.2.3 修正方案:基类析构函数设为虚函数

cpp

运行

// 正确示例:基类析构设为虚函数
class Person {
public:
    // 虚析构:确保多态调用
    virtual ~Person() { 
        cout << "~Person()" << endl; 
    }
};

class Student : public Person {
public:
    // 派生类析构:即使不加virtual,仍构成重写
    ~Student() { 
        cout << "~Student()" << endl; 
    }
};

int main() {
    Person* p2 = new Student;
    delete p2; // 先调用~Student(),再调用~Person() → 资源完全释放
    return 0;
}

3.3 补充:C++11 重写校验关键字(override/final)

虚函数重写规则严格,若手动写错(如函数名、参数),编译器可能不报错,仅在运行时出问题。C++11 新增overridefinal,实现编译期校验,提前规避错误。

3.3.1 override:强制校验重写合法性

加在派生类虚函数后,明确声明 “该函数需重写基类虚函数”。若不满足重写规则(如基类无对应虚函数、参数不匹配),编译器直接报错。

cpp

运行

class Person {
public:
    virtual void BuyTicket(int n = 1) { cout << "全价票x" << n << endl; }
};

class Student : public Person {
public:
    // 校验:若基类BuyTicket参数不是int,或非虚函数,编译器报错
    virtual void BuyTicket(int n = 1) override { 
        cout << "半价票x" << n << endl; 
    }
};
3.3.2 final:限制重写或继承
  • 作用于基类虚函数:禁止派生类重写该函数;
  • 作用于类名:禁止该类被继承。

cpp

运行

// 场景1:限制虚函数重写
class Person {
public:
    // final:Person的派生类无法重写BuyTicket
    virtual void BuyTicket() final { 
        cout << "全价" << endl; 
    }
};

class Student : public Person {
public:
    // 错误:编译器报错,无法重写final修饰的虚函数
    virtual void BuyTicket() { 
        cout << "半价" << endl; 
    }
};

// 场景2:限制类继承
class Person final { // final:禁止任何类继承Person
public:
    virtual void BuyTicket() { cout << "全价" << endl; }
};

// 错误:编译器报错,无法继承final类
class Student : public Person {
};

四、易混淆概念:重载、重写、重定义

三者均涉及 “同名函数”,但作用域、规则、效果完全不同,需精准区分。

特性重载(Overload)重写(Override)重定义(Redefine)
作用域同一作用域(如同一类内)不同作用域(基类 + 派生类)不同作用域(基类 + 派生类)
函数名必须相同必须相同必须相同
参数列表必须不同(个数 / 类型 / 顺序)必须相同可相同、可不同
返回值类型可相同、可不同必须相同(协变例外)可相同、可不同
虚函数要求无(与虚函数无关)必须均为虚函数(基类需加 virtual)无(若基类是虚函数但参数不同,仍算重定义)
核心效果编译期根据参数区分调用运行期根据对象类型区分调用(多态)派生类函数 “隐藏” 基类同名函数,直接调用时优先用派生类

4.1 示例对比

4.1.1 重载(同一类内)

cpp

运行

class Math {
public:
    // 重载:同一类内,函数名相同,参数不同
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
};
4.1.2 重写(基类 + 派生类,虚函数 + 参数相同)

cpp

运行

class Person {
public:
    virtual void BuyTicket() { cout << "全价" << endl; } // 基类虚函数
};

class Student : public Person {
public:
    virtual void BuyTicket() { cout << "半价" << endl; } // 重写:参数/返回值相同,均为虚函数
};
4.1.3 重定义(基类 + 派生类,不满足重写)

cpp

运行

class Person {
public:
    virtual void func(int a) { cout << "Person::func(" << a << ")" << endl; }
};

class Student : public Person {
public:
    // 重定义:参数不同(int→double),即使基类是虚函数,也不构成重写
    virtual void func(double a) { cout << "Student::func(" << a << ")" << endl; }
};

int main() {
    Student s;
    s.func(10); // 调用Student::func(double),基类func(int)被隐藏 → 输出“Student::func(10.0)”
    return 0;
}

五、抽象类(接口类)

抽象类是包含 “纯虚函数” 的类,核心作用是定义接口规范,强制派生类实现特定行为,无法直接实例化。

5.1 核心组件:纯虚函数

纯虚函数是在虚函数后加=0的函数,仅声明接口,不提供实现(也可提供实现,但派生类仍需重写才能实例化)。

cpp

运行

class Animal {
public:
    // 纯虚函数:声明“发声”接口,无具体实现
    virtual void MakeSound() = 0; 
};

5.2 抽象类的特性

  1. 无法直接实例化:抽象类不能创建对象,如Animal a;会编译报错;
  2. 派生类需重写所有纯虚函数:若派生类未重写抽象类的全部纯虚函数,则该派生类仍为抽象类,无法实例化;
  3. 可定义抽象类的指针 / 引用:用于实现多态调用(指向派生类对象)。

5.3 代码示例

cpp

运行

// 抽象类:Animal(含纯虚函数MakeSound)
class Animal {
public:
    virtual void MakeSound() = 0; // 纯虚函数
};

// 派生类1:Dog(重写纯虚函数)
class Dog : public Animal {
public:
    void MakeSound() override { // 重写纯虚函数
        cout << "汪汪汪" << endl;
    }
};

// 派生类2:Cat(重写纯虚函数)
class Cat : public Animal {
public:
    void MakeSound() override { // 重写纯虚函数
        cout << "喵喵喵" << endl;
    }
};

// 派生类3:Bird(未重写纯虚函数 → 仍为抽象类)
class Bird : public Animal {
    // 未实现MakeSound() → Bird是抽象类,无法实例化
};

int main() {
    // Animal a; // 错误:抽象类无法实例化
    // Bird b;  // 错误:Bird未重写纯虚函数,仍是抽象类
    
    Animal* p1 = new Dog;  // 抽象类指针指向派生类对象
    Animal* p2 = new Cat;  // 多态调用前提
    p1->MakeSound(); // 输出“汪汪汪”
    p2->MakeSound(); // 输出“喵喵喵”
    delete p1;
    delete p2;
    return 0;
}

5.4 补充:接口继承 vs 实现继承

多态依赖 “接口继承”,需与普通函数的 “实现继承” 区分:

  • 接口继承:派生类继承基类虚函数的 “接口规范”(函数名、参数、返回值),需自行实现逻辑(如 Animal 的 MakeSound);核心目的是实现多态。
  • 实现继承:派生类直接继承基类普通函数的 “完整实现”,无需重写即可使用(如 Person 的 Eat 函数);核心目的是代码复用。

六、多态的底层实现原理(虚表与虚表指针)

多态的底层依赖 “虚表(Virtual Function Table)” 和 “虚表指针(Virtual Table Pointer, _vftptr)” 机制,以下从内存结构、生成逻辑、调用流程三方面解析。

6.1 核心概念定义

组件本质存在位置核心作用
虚表指针(_vftptr)指向虚表的指针(4 字节 / 32 位,8 字节 / 64 位)类对象的内存头部(隐藏成员)关联对象与对应的虚表
虚表(vftable)存储虚函数地址的指针数组(结尾含 nullptr)代码段的常量区(编译时确定)记录类的所有虚函数地址,供调用查询
虚函数可重写的成员函数(二进制指令)代码段(与普通函数存储位置一致)实际执行的业务逻辑

6.2 虚表与虚表指针的生成规则

6.2.1 虚表指针的生成
  • 仅当类中包含虚函数(或继承自含虚函数的类)时,该类的对象才会自动添加_vftptr(隐藏成员,不占用类的显式成员内存);
  • 同一类的所有对象共享同一张虚表,因此每个对象仅需一个_vftptr指向该虚表(节省内存)。
6.2.2 派生类虚表的生成步骤(以单继承为例)
  1. 拷贝基类虚表:派生类先将基类的虚表完整拷贝到自己的虚表中;
  2. 替换重写的虚函数:若派生类重写了基类的某虚函数,用派生类虚函数的地址替换虚表中对应基类虚函数的地址;
  3. 追加新增虚函数:若派生类有自己新增的虚函数,按函数声明顺序,将其地址追加到虚表末尾(nullptr 前)。
6.2.3 示例:单继承下的虚表结构

cpp

运行

// 基类:Base(含2个虚函数)
class Base {
public:
    virtual void Func1() { cout << "Base::Func1" << endl; }
    virtual void Func2() { cout << "Base::Func2" << endl; }
private:
    int b; // 显式成员(4字节/32位)
};

// 派生类:Derive(重写Func1,新增Func3)
class Derive : public Base {
public:
    virtual void Func1() override { cout << "Derive::Func1" << endl; } // 重写Func1
    virtual void Func3() { cout << "Derive::Func3" << endl; } // 新增虚函数
private:
    int d; // 新增显式成员(4字节/32位)
};

虚表结构对比

  • Base 的虚表:[&Base::Func1, &Base::Func2, nullptr]
  • Derive 的虚表:[&Derive::Func1(替换), &Base::Func2(保留), &Derive::Func3(追加), nullptr]

6.3 对象的内存布局(32 位系统)

Derive d;为例,其内存结构(共 12 字节)如下:

内存偏移内容大小(字节)说明
0~3_vftptr(虚表指针)4指向 Derive 的虚表
4~7继承的 Base::b4从基类继承的显式成员
8~11自身的 Derive::d4派生类新增的显式成员

6.4 多态调用的底层流程(以Base* p = new Derive; p->Func1();为例)

  1. 获取虚表指针:通过指针p访问派生类对象Derive的内存头部,拿到_vftptr
  2. 查找虚表:通过_vftptr指向代码段常量区中的Derive虚表;
  3. 定位虚函数地址:在虚表中找到Func1对应的地址(已被替换为&Derive::Func1);
  4. 执行函数:跳转到该地址,执行Derive::Func1的二进制指令。

6.5 补充:关键细节澄清

  1. 虚表不存储在对象中:虚表在代码段常量区,对象仅通过_vftptr关联虚表,避免内存浪费;
  2. 静态函数无法成为虚函数:静态函数无this指针,无法通过this->_vftptr访问虚表,因此不能定义为虚函数;
  3. 虚表指针的位置:随对象存储(对象在栈上,_vftptr在栈上;对象在堆上,_vftptr在堆上)。

七、单继承与多继承的虚函数表差异

单继承和多继承下,虚表的数量、结构及子类虚函数的存储位置均不同,需分别分析。

7.1 单继承的虚函数表

7.1.1 核心特性
  • 虚表数量:1 张(派生类仅需在基类虚表基础上修改 / 追加);
  • 虚表指针:派生类对象仅含1 个_vftptr(继承自基类,指向派生类的虚表);
  • 共享特性:同一类的所有对象共享同一张虚表(如Derive d1, d2;_vftptr指向同一张虚表)。
7.1.2 内存布局示例(32 位系统)

cpp

运行

class Base {
public:
    virtual void Func1() { cout << "Base::Func1" << endl; }
private:
    int b = 1; // 4字节
};

class Derive : public Base {
public:
    virtual void Func1() override { cout << "Derive::Func1" << endl; }
    virtual void Func2() { cout << "Derive::Func2" << endl; }
private:
    int d = 2; // 4字节
};

Derive d;的内存布局(共 12 字节):

偏移内容大小说明
0~3_vftptr(指向 Derive 虚表)4唯一虚表指针
4~7Base::b(值 = 1)4继承自基类的成员
8~11Derive::d(值 = 2)4派生类新增成员

7.2 多继承的虚函数表

多继承指派生类同时继承多个基类(均含虚函数),此时虚表数量与基类数量一致,虚函数存储位置需区分 “重写” 和 “新增”。

7.2.1 核心特性
  • 虚表数量:与含虚函数的基类数量一致(每个基类对应 1 张虚表);
  • 虚表指针:派生类对象含多个_vftptr(每个基类对应 1 个,按继承顺序排列);
  • 虚函数存储规则:
    1. 子类重写的虚函数:存储在对应基类的虚表中(如重写Base1::Func1,则替换Base1虚表中的Func1地址);
    2. 子类新增的虚函数:默认存储在第一个基类的虚表末尾(按继承顺序,如先继承Base1,则新增函数追加到Base1虚表)。
7.2.2 代码示例与内存分析(32 位系统)

cpp

运行

// 基类1:Base1(含虚函数)
class Base1 {
public:
    virtual void Func1() { cout << "Base1::Func1" << endl; }
    virtual void Func2() { cout << "Base1::Func2" << endl; }
private:
    int b1 = 1; // 4字节
};

// 基类2:Base2(含虚函数)
class Base2 {
public:
    virtual void Func1() { cout << "Base2::Func1" << endl; }
    virtual void Func2() { cout << "Base2::Func2" << endl; }
private:
    int b2 = 2; // 4字节
};

// 派生类:Derive(多继承Base1、Base2,重写Func1,新增Func3)
class Derive : public Base1, public Base2 {
public:
    // 重写Base1::Func1和Base2::Func1
    virtual void Func1() override { cout << "Derive::Func1" << endl; }
    // 新增虚函数Func3
    virtual void Func3() { cout << "Derive::Func3" << endl; }
private:
    int d1 = 3; // 4字节
};
7.2.3 虚表结构
  • Base1 对应的虚表[&Derive::Func1(重写), &Base1::Func2(保留), &Derive::Func3(新增), nullptr]
  • Base2 对应的虚表[&Derive::Func1(重写), &Base2::Func2(保留), nullptr]
7.2.4 内存布局(共 20 字节)

Derive d;的内存结构(32 位系统,按继承顺序排列):

偏移内容大小说明
0~3_vftptr1(指向 Base1 的虚表)4对应 Base1 的虚表指针
4~7Base1::b1(值 = 1)4Base1 的成员
8~11_vftptr2(指向 Base2 的虚表)4对应 Base2 的虚表指针
12~15Base2::b2(值 = 2)4Base2 的成员
16~19Derive::d1(值 = 3)4Derive 的新增成员

7.3 补充:多继承的内存对齐

多继承下,对象内存需满足 “内存对齐” 规则(通常与指针大小一致,32 位 4 字节,64 位 8 字节)。上述示例中,各部分均为 4 字节,无需额外补位,总大小为 20 字节;若存在非对齐成员(如 char),会自动补位至对齐大小。

八、静态绑定与动态绑定(多态的绑定机制)

多态的本质是 “动态绑定”,需与 “静态绑定” 对比,明确两者的适用场景。

8.1 静态绑定(前期绑定 / 早绑定)

  • 定义:编译期间确定函数的调用关系,不依赖运行时对象类型;
  • 适用场景:函数重载、普通函数调用、非虚函数的继承调用;
  • 核心特点:效率高,编译时已确定调用地址;
  • 示例

    cpp

    运行

    void f(int a) { cout << "int: " << a << endl; }
    void f(double a) { cout << "double: " << a << endl; }
    
    int main() {
        int i = 10;
        double d = 5.5;
        f(i); // 编译时确定调用f(int) → 静态绑定
        f(d); // 编译时确定调用f(double) → 静态绑定
        return 0;
    }
    

8.2 动态绑定(后期绑定 / 晚绑定)

  • 定义:运行期间根据实际对象类型确定函数的调用关系,是多态的核心;
  • 适用场景:通过基类指针 / 引用调用重写的虚函数;
  • 核心特点:灵活性高,支持多态,但需通过虚表查询地址,效率略低;
  • 示例

    cpp

    运行

    class Base {
    public:
        virtual void Func() { cout << "Base::Func" << endl; }
    };
    
    class Derive : public Base {
    public:
        virtual void Func() override { cout << "Derive::Func" << endl; }
    };
    
    int main() {
        Base* p = nullptr;
        // 运行时根据对象类型确定调用
        p = new Base;  p->Func(); // 指向Base对象 → 调用Base::Func
        p = new Derive;p->Func(); // 指向Derive对象 → 调用Derive::Func
        delete p;
        return 0;
    }
    

九、补充:常见疏漏点与易错点

  1. 纯虚函数可提供实现:纯虚函数的=0仅表示 “强制派生类重写”,不禁止基类提供实现,但派生类仍需重写后才能实例化;

    cpp

    运行

    class Animal {
    public:
        virtual void MakeSound() = 0; // 纯虚函数
    };
    
    // 基类提供纯虚函数的实现(合法)
    void Animal::MakeSound() {
        cout << "动物发声" << endl;
    }
    
    class Dog : public Animal {
    public:
        void MakeSound() override {
            Animal::MakeSound(); // 可调用基类的实现
            cout << "汪汪汪" << endl;
        }
    };
    
  2. 虚函数的默认参数是静态绑定:虚函数的默认参数由 “指针 / 引用的声明类型” 决定(编译时绑定),而非实际对象类型;

    cpp

    运行

    class Base {
    public:
        virtual void Func(int a = 1) { cout << "Base: " << a << endl; }
    };
    
    class Derive : public Base {
    public:
        virtual void Func(int a = 2) override { cout << "Derive: " << a << endl; }
    };
    
    int main() {
        Base* p = new Derive;
        p->Func(); // 默认参数用Base的a=1,函数体用Derive的实现 → 输出“Derive: 1”
        return 0;
    }
    
  3. 析构函数必须设为虚函数的场景:仅当 “需通过基类指针删除派生类对象” 时,基类析构才需设为虚函数;若仅用派生类对象直接释放,无需虚析构。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值