C++ 多态

多态(Polymorphism)是面向对象编程的三大核心特性(封装、继承、多态)之一。它允许使用统一的接口来处理不同的派生类对象,从而在运行时根据对象的实际类型来调用相应的方法。

1、原理

虚函数表 (vTable) 和虚函数指针 (vPtr)

  • 虚函数 (Virtual Function) : 使用 virtual 关键字声明的成员函数。派生类可以重写(override)它。
  • 虚函数表 (vTable) : 编译器为每个包含虚函数的类 自动生成一个隐藏的、静态的函数指针数组。表中按顺序存放该类所有虚函数的地址
    • 如果派生类重写了基类的虚函数,则派生类的 vTable 中对应项更新为派生类函数的地址。
    • 如果派生类定义了新的虚函数,这些新虚函数的地址会被追加到 vTable 的末尾。
  • 虚函数指针 (vPtr) : 编译器在每个包含虚函数的类的对象 中自动添加一个隐藏的指针成员(通常是对象的开头位置)。这个 vPtr 指向该类对应的 vTable。

2、内存模型与工作原理

cpp 复制代码
class Base {
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
    void func3() { cout << "Base::func3" << endl; } // 非虚函数
    int base_data;
};

class Derived : public Base {
public:
    void func1() override { cout << "Derived::func1" << endl; } // 重写 func1
    virtual void func4() { cout << "Derived::func4" << endl; } // 新的虚函数
    int derived_data;
};

调用过程

当你通过一个基类指针或引用调用虚函数时,例如 basePtr->func1(),编译器会生成以下代码:

  1. 通过 basePtr 找到对象的 vPtr
  2. 通过 vPtr 找到对应的 vTable
  3. vTable 中找到 func1 对应的条目(通常是固定的偏移量,比如第 0 项)。
  4. 通过该条目中的函数地址,调用正确的函数 (Derived::func1)。

正是因为每次调用都要经过这个查表过程,所以虚函数调用比普通函数调用多一次间接寻址,有轻微的性能开销。

3、实现多态的代码实例

3.1 基础用法

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

class Animal {
public:
    // 虚函数
    virtual void speak() {
        cout << "Animal speaks!" << endl;
    }
    // 虚析构函数(极其重要!)
    virtual ~Animal() {
        cout << "Animal destructor" << endl;
    }
};

class Dog : public Animal {
public:
    // 重写基类虚函数
    void speak() override { // C++11 引入 override 关键字,更安全
        cout << "Woof! Woof!" << endl;
    }
    ~Dog() override {
        cout << "Dog destructor" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        cout << "Meow! Meow!" << endl;
    }
    ~Cat() override {
        cout << "Cat destructor" << endl;
    }
};

int main() {
    // 关键:使用基类指针指向派生类对象
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->speak(); // 输出: Woof! Woof! (调用的是 Dog 的 speak)
    animal2->speak(); // 输出: Meow! Meow! (调用的是 Cat 的 speak)

    // 如果没有虚函数,这里将都输出 "Animal speaks!"

    delete animal1; // 由于有虚析构函数,会先调用 ~Dog(),再调用 ~Animal()
    delete animal2; // 先调用 ~Cat(),再调用 ~Animal()

    return 0;
}

3.2 工厂模式

cpp 复制代码
#include <iostream>
#include <memory>
#include <vector>

// 抽象基类(接口类)
class Logger {
public:
    virtual void log(const std::string& message) = 0; // 纯虚函数
    virtual ~Logger() = default;
};

// 具体实现类
class FileLogger : public Logger {
public:
    void log(const std::string& message) override {
        std::cout << "Logging to FILE: " << message << std::endl;
    }
};

class ConsoleLogger : public Logger {
public:
    void log(const std::string& message) override {
        std::cout << "Logging to CONSOLE: " << message << std::endl;
    }
};

// 工厂函数,返回基类指针,但实际创建的是派生类对象(多态的典型应用)
std::unique_ptr<Logger> createLogger(const std::string& type) {
    if (type == "file") {
        return std::make_unique<FileLogger>();
    } else if (type == "console") {
        return std::make_unique<ConsoleLogger>();
    }
    return nullptr;
}

int main() {
    std::vector<std::unique_ptr<Logger>> loggers;

    loggers.push_back(createLogger("file"));
    loggers.push_back(createLogger("console"));

    // 统一接口,不同行为
    for (const auto& logger : loggers) {
        logger->log("Hello, Polymorphism!"); // 根据具体logger类型调用不同的log方法
    }

    return 0;
}

4、多态的关键特性与注意事项

  1. 虚析构函数的必要性

    若基类析构函数不是虚函数,用基类指针删除派生类对象时,只会调用基类析构函数,导致派生类资源泄漏。因此基类析构函数必须声明为虚函数

    cpp 复制代码
    class Base {
    public:
        ~Base() { cout << "Base 析构" << endl; }  // 非虚析构函数(错误)
    };
    
    class Derived : public Base {
    private:
        int* data;
    public:
        Derived() { data = new int; }
        ~Derived() { 
            delete data; 
            cout << "Derived 析构" << endl; 
        }
    };
    
    int main() {
        Base* ptr = new Derived()
        delete ptr;  // 仅调用 Base::~Base(),Derived 的 data 内存泄漏
        return 0;
    }

    修复 :将基类析构函数声明为 virtual ~Base() { ... },确保 delete ptr 时调用 Derived::~Derived()

  2. 重写的条件(三同原则)

    派生类重写基类虚函数必须满足:

    • 函数名相同
    • 参数列表相同(包括 const 修饰)
    • 返回值相同(或协变返回类型,如基类返回 Base*,派生类返回 Derived*

    违反则构成函数隐藏(而非重写),不会触发多态。

  3. 静态函数与虚函数

    静态函数不能是虚函数(无 this 指针,无法访问 vptr),调用时采用静态绑定。

  4. 构造函数与虚函数

    构造函数不能是虚函数(对象未完全创建,vptr 未初始化)。派生类构造时,先调用基类构造函数,此时调用虚函数会执行基类版本(而非派生类)。

5、常见问题

1. 什么是 C++ 多态?它是如何实现的?

多态允许使用基类的指针或引用来调用派生类的方法。它通过虚函数 实现,底层机制是每个对象内部的虚函数指针 (vPtr) 指向一个特定的虚函数表 (vTable),运行时通过查表来决定调用哪个具体的函数。

2. 虚函数表 (vTable) 是什么时候创建的?虚函数指针 (vPtr) 又是什么时候初始化的?

  • vTable : 在编译期由编译器为每个包含虚函数的类创建,整个类只有一份,存储在静态内存区。
  • vPtr : 在对象的构造过程中 被初始化。
    1. 在构造派生类对象时,先调用基类构造函数。此时,对象的 vPtr 被初始化为指向基类的 vTable。
    2. 然后调用派生类构造函数,此时 vPtr重新赋值 为指向派生类的 vTable。
    3. 这就是为什么在构造函数中调用虚函数不会发生多态 的原因(因为当时 vPtr 指向的是当前正在构造的类对应的 vTable)。

3. 为什么析构函数要声明为虚函数?

如果可能通过基类指针来删除派生类对象,基类的析构函数必须是虚函数 。否则,只会调用基类的析构函数,而派生类的析构函数不会被调用,导致派生类的资源泄露

cpp 复制代码
Base* obj = new Derived();
delete obj; // 如果 ~Base() 不是虚函数,则 ~Derived() 不会被调用,造成内存泄漏!

4. 什么是纯虚函数和抽象类?

  • 纯虚函数 : 在基类中声明但没有定义的虚函数,语法是 virtual void func() = 0;
  • 抽象类: 包含至少一个纯虚函数的类。它不能实例化对象,只能作为接口被继承。派生类必须重写所有纯虚函数,否则派生类也会成为抽象类。

5. overridefinal 关键字有什么用?

  • override (C++11): 明确表示要重写基类的虚函数。如果标记了 override 的函数没有成功重写任何虚函数(比如函数名拼错或参数列表不一致),编译器会报错。强烈建议使用,增加代码安全性。
  • final (C++11):
    • 用于类:class Derived final : public Base,表示 Derived 不能再被继承。
    • 用于虚函数:virtual void func() final;,表示该虚函数在后续的派生类中不能再被重写。

6. 虚函数有什么缺点?

  • 性能开销: 每次调用需要一次额外的指针解引用(查 vTable),并且阻碍了编译器内联优化。
  • 空间开销 : 每个对象需要额外空间存储 vPtr,每个类需要空间存储 vTable
  • 二进制兼容性: 在库中,给类增加虚函数可能会破坏二进制兼容性。

7. 构造函数和析构函数中能调用虚函数吗?会发生多态吗?

可以调用,但不会发生多态。

  • 在构造函数中调用虚函数,会调用当前构造函数所在类的版本。
  • 在析构函数中调用虚函数,会调用当前析构函数所在类的版本。
  • 原因 : 在构造/析构期间,对象的类型被视为当前正在构造/析构的类型,vPtr 指向的是当前类的 vTable。派生类部分尚未构造或已经销毁。

8. 静态函数可以是虚函数吗?

不可以 。虚函数调用依赖于特定的对象(需要通过 vPtr 找到 vTable),而静态函数属于类而不属于任何对象,可以直接通过类名调用。两者概念冲突。

相关推荐
沐怡旸9 小时前
【底层机制】std::string 解决的痛点?是什么?怎么实现的?怎么正确用?
c++·面试
River41612 小时前
Javer 学 c++(十三):引用篇
c++·后端
感哥14 小时前
C++ std::set
c++
侃侃_天下15 小时前
最终的信号类
开发语言·c++·算法
博笙困了15 小时前
AcWing学习——差分
c++·算法
青草地溪水旁16 小时前
设计模式(C++)详解—抽象工厂模式 (Abstract Factory)(2)
c++·设计模式·抽象工厂模式
青草地溪水旁16 小时前
设计模式(C++)详解—抽象工厂模式 (Abstract Factory)(1)
c++·设计模式·抽象工厂模式
感哥16 小时前
C++ std::vector
c++
zl_dfq16 小时前
C++ 之【C++11的简介】(可变参数模板、lambda表达式、function\bind包装器)
c++