跳转至

8 Polymorphism

说明

本文档正在更新中……

说明

本文档仅涉及部分内容,仅可用于复习重点知识

多态是面向对象编程的核心特性之一,它允许同一个接口以不同的方式执行操作。C++ 中的多态分为两种主要类型:编译时多态(静态多态)和运行时多态(动态多态)

  1. 静态多态:编译时多态通过函数重载和运算符重载实现,行为在编译时确定
    1. 函数重载:允许在同一个作用域中定义多个同名函数,但参数列表必须不同
    2. 运算符重载:允许为用户定义的类型赋予运算符新的行为
  2. 动态多态:运行时多态通过继承和虚函数实现,行为在运行时确定
    1. 虚函数:允许子类重写父类的方法,并通过基类指针或引用调用子类的实现
    2. 纯虚函数与抽象类:纯虚函数是没有实现的虚函数,包含纯虚函数的类称为抽象类,不能直接实例化

纯虚函数与抽象类

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() = 0; // 纯虚函数
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing Circle" << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout << "Drawing Rectangle" << endl;
    }
};

int main() {
    Shape* shape1 = new Circle();
    Shape* shape2 = new Rectangle();

    shape1->draw(); // 输出:Drawing Circle
    shape2->draw(); // 输出:Drawing Rectangle

    delete shape1;
    delete shape2;
    return 0;
}

多态的实现原理:运行时多态的实现依赖于虚函数表(vtable) 和虚函数指针(vptr):

  • 每个包含虚函数的类都有一个虚函数表,存储虚函数的地址
  • 每个对象都有一个虚函数指针,指向所属类的虚函数表

当通过基类指针调用虚函数时,程序会根据虚函数指针动态查找实际调用的函数

8.1 Up-casting

Upcasting(向上转型)是指将派生类指针或引用转换为基类指针或引用的过程。这是 C++ 中多态性的基础

1
2
3
4
5
6
class Base { /*...*/ };
class Derived : public Base { /*...*/ };

Derived d;
Base* bp = &d;  // Upcasting - 将Derived*转换为Base*
Base& br = d;   // Upcasting - 将Derived&转换为Base&

特点:

  1. 隐式转换:Upcasting 是自动进行的,不需要显式类型转换
  2. 类型安全:编译器保证这种转换是安全的
  3. 多态基础:通过 Upcasting 可以实现运行时多态

Object Slicing(对象切片):当通过值传递进行 Upcasting 时,会发生对象切片

1
2
3
4
void processAnimal(Animal animal) { /*...*/ }

Dog dog;
processAnimal(dog);  // 对象切片 - 只复制Animal部分

要避免对象切片,应使用指针或引用

1
2
3
void processAnimal(Animal& animal) { /*...*/ }
// 或
void processAnimal(Animal* animal) { /*...*/ }

使用 dynamic_cast 可以实现向下转型

1
2
3
4
5
Base* bp = new Derived();
Derived* dp = dynamic_cast<Derived*>(bp);  // 向下转型
if (dp) {
    // 转换成功
}

注意事项

只有公有继承时才能进行 Upcasting

8.2 Static Type and Dynamic Type

  1. Static type:变量或表达式在编译时已知的类型
    1. 由变量声明或表达式决定
    2. 在编译时完全确定
    3. 不会随程序运行而改变
  2. Dynamic type:指针或引用实际指向的对象的类型(运行时类型)
    1. 仅适用于指针和引用
    2. 在运行时才能确定
    3. 可能随程序运行而变化
    4. 需要基类有虚函数才能体现差异
class Base {
public:
    virtual void print() { cout << "Base" << endl; }
};

class Derived : public Base {
public:
    void print() override { cout << "Derived" << endl; }
};

int main() {
    Derived d;
    Base* bp = &d;  // Static Type: Base*, Dynamic Type: Derived*

    bp->print();    // 输出"Derived" (动态类型决定)

    // typeid可以反映动态类型(当有虚函数时)
    cout << typeid(*bp).name();  // 输出Derived类型信息
}

typeid()

1
2
3
Base* bp = new Derived();
cout << typeid(bp).name();    // 输出Base* (static type)
cout << typeid(*bp).name();   // 输出Derived (dynamic type,需有虚函数)

无虚函数时的行为:

1
2
3
4
5
class Base { void foo() {} };
class Derived : public Base { void foo() {} };

Base* b = new Derived();
b->foo();  // 调用Base::foo (static type决定,无多态)

8.3 Virtual

  1. non-virtual function
    1. static binding(静态绑定):函数调用在编译时确定
    2. 效率高:无运行时查找开销
    3. 不可覆盖:派生类中的同名函数会隐藏而非覆盖基类函数
  2. virtual function
    1. dynamic binding(动态绑定):函数调用在运行时确定
    2. 多态基础:支持运行时多态
    3. 可覆盖:派生类可以使用 override 关键字明确覆盖

细节:

  1. 静态成员函数不能被声明为虚函数
  2. 构造函数不能被声明为虚函数
  3. 构造函数中调用的虚函数是 static binding:在构造函数执行期间,对象的虚表(vtable)可能尚未完全初始化(尤其是基类构造函数运行时,派生类部分尚未构造),因此,构造函数中调用的虚函数会静态绑定(即直接调用当前类的版本,而非派生类的覆盖版本),这是 C++ 的明确规定,避免未定义行为

8.3.1 工作原理

C++ 通过虚函数表(vtable)和虚函数指针(vptr)实现运行时多态,这是虚函数工作的底层基础。当类包含虚函数时,编译器会为该类创建一个虚函数表,并在每个对象中嵌入一个指向该表的指针

虚函数表:

  1. 每个多态类一个 vtable:编译器为包含虚函数的类生成一个虚函数表
  2. 表内容:
    1. 按声明顺序存储类的虚函数地址
    2. 包含类型信息
  3. 继承关系:
    1. 派生类的 vtable 包含基类 vtable 的内容
    2. 覆盖的函数替换为派生类的实现地址
    3. 新增虚函数追加到表末尾

虚函数指针:

  1. 每个对象一个 vptr:包含虚函数的类的每个实例都包含一个隐藏的 vptr 成员
  2. 初始化时机:
    1. 在构造函数中初始化,指向对应类的 vtable
    2. 在构造过程中,vptr 会随构造阶段变化(基类 → 派生类)
  3. 内存布局:
    1. 通常位于对象起始位置(具体由编译器决定)
    2. 大小通常为一个指针大小(32 位系统 4 字节,64 位系统 8 字节)

虚函数调用过程:当通过基类指针/引用调用虚函数时

  1. 通过 vptr 找到 vtable:访问对象的 vptr 成员,获取类的 vtable 地址
  2. 查找函数地址:在 vtable 中找到对应偏移位置的函数地址
  3. 间接调用:通过找到的地址调用实际函数

细节

  1. 虚析构函数:虚析构函数也会进入 vtable,确保通过基类指针删除派生类对象时能正确调用整个析构链
  2. 纯虚函数:纯虚函数在 vtable 中通常用空指针或特殊函数地址表示,使抽象类无法实例化

Img 1

Img 2

Img 3

8.4 Override

  1. 只有虚函数(virtual)才能被重写
  2. 函数签名(名称、参数列表、返回类型)必须完全相同
  3. 在 C++ 11 后建议使用 override 关键字明确表示重写
class Base {
public:
    virtual void func() { 
        cout << "Base implementation" << endl; 
    }
};

class Derived : public Base {
public:
    // 重写基类的虚函数
    void func() override {  // C++11起推荐添加override关键字
        cout << "Derived implementation" << endl;
    }
};

int main() {
    Base* b = new Derived();
    b->func(); // 输出"Derived implementation"
    delete b;
}

final

使用 final 禁止重写

class Base {
public:
    virtual void foo() final {  // 这个虚函数不能被子类重写
        // ...
    }
};

class Derived : public Base {
public:
    void foo() override;  // 编译错误:foo 是 final 的,不能重写
};

final 还可以禁止继承

1
2
3
4
5
6
7
class Base final {  // 这个类不能被继承
    // ...
};

class Derived : public Base {  // 编译错误:Base 是 final 类
    // ...
};

结合 override 使用

class Base {
public:
    virtual void foo() {}
    virtual void bar() {}
};

class Derived : public Base {
public:
    void foo() final override {}  // 明确表示重写,并禁止进一步重写
    void bar() override {}        // 允许子类继续重写
};

class FurtherDerived : public Derived {
public:
    // void foo() override {}  // 错误:foo 是 final 的
    void bar() override {}    // 可以重写 bar
};

调用被覆盖的基类函数:在派生类中重写(override)基类函数时,可以使用 Base::func() 语法显式调用基类版本的函数

返回类型协变(Covariant return type):

  1. 当派生类重写基类虚函数时,C++ 允许派生类版本的函数返回基类函数返回类型的子类
  2. 这只适用于返回类型为指针或引用的情况
  3. 例如,如果基类函数返回 B*,派生类函数可以返回 D*(假设 D 继承自 B)
class Expr {
public:
    virtual Expr* newExpr();
    virtual EXpr& clone();
    virtual Expr self();
};
class BinaryExpr : public Expr {
public:
    virtual BinaryExpr* newExpr();  // ok
    virtual BinaryExpr& clone();  // ok
    virtual BinaryExpr self();  // error!
}

重载(Overloading)可以为函数添加多个签名(signature),如果重写(override)一个重载的函数,必须重写所有的变体,不能只重写其中一个,如果没有全部重写,某些版本会被隐藏(hidden)

class Base {  
public:  
    virtual void func();  
    virtual void func(int);  
};

class Derived : public Base {  
public:  
    virtual void func() {  
        Base::func();  // 调用基类版本  
    }  
    virtual void func(int) { ... };  // 必须重写所有重载版本  
};  

如果派生类只重写 func() 而不重写 func(int),那么 func(int) 会被隐藏,导致派生类对象无法直接调用 func(int)

建议

  1. 永远不要重新定义继承的非虚函数
  2. 永远不要重新定义继承的默认参数值
    • 默认参数也是静态绑定的

8.5 Abstract Class

抽象类是面向对象编程中的一个重要概念,用于定义一个通用的接口或基类,供派生类实现具体功能。抽象类的主要特征是包含至少一个纯虚函数

定义:

  1. 抽象类是包含一个或多个纯虚函数的类
  2. 纯虚函数是没有实现的虚函数,定义时使用 = 0 表示
  3. 抽象类不能直接实例化,但可以通过指针或引用指向派生类对象
  4. 抽象类可以作为函数返回值或参数类型(但实际返回或传入的必须是派生类对象)
#include <iostream>
using namespace std;

class AbstractShape {
public:
    virtual void draw() = 0; // 纯虚函数
    virtual ~AbstractShape() {} // 虚析构函数,确保派生类正确析构
};

class Circle : public AbstractShape {
public:
    void draw() override {
        cout << "Drawing Circle" << endl;
    }
};

class Rectangle : public AbstractShape {
public:
    void draw() override {
        cout << "Drawing Rectangle" << endl;
    }
};

int main() {
    AbstractShape* shape1 = new Circle();
    AbstractShape* shape2 = new Rectangle();

    shape1->draw(); // 输出:Drawing Circle
    shape2->draw(); // 输出:Drawing Rectangle

    delete shape1;
    delete shape2;
    return 0;
}

特点:

  1. 不能实例化:抽象类不能直接创建对象
  2. 可以包含非纯虚函数:抽象类可以包含普通成员函数,供派生类复用
  3. 可以包含成员变量:抽象类可以包含成员变量,供派生类使用
  4. 派生类必须实现所有纯虚函数:如果派生类没有实现基类的所有纯虚函数,那么派生类也会成为抽象类
  5. 虚析构函数:抽象类通常需要定义虚析构函数,以确保派生类对象被正确销毁

用途:

  1. 定义接口:抽象类用于定义一组通用的接口,派生类实现具体功能。
  2. 实现多态:抽象类通过虚函数支持运行时多态,允许通过基类指针或引用调用派生类的实现
  3. 代码复用:抽象类可以包含通用的实现,供派生类复用

8.6 Multiple Inheritance

评论区

欢迎在评论区指出文档错误,为文档提供宝贵意见,或写下你的疑问