C++虚函数实现父类指针调用子类方法详解

0 次阅读

C++中的虚函数是实现运行时多态的核心机制之一。当父类指针指向子类对象时,如果通过该指针调用的是虚函数,程序可以根据对象的实际类型选择对应的子类方法,而不是简单按照指针声明类型调用父类方法。这也是C++面向对象编程中“父类指针调用子类方法”的典型实现方式。

一、什么是C++虚函数

普通成员函数的调用通常在编译阶段就能够确定。例如:

C++
class Animal {
public:
    void speak() {
        std::cout << "Animal speak" << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() {
        std::cout << "Dog speak" << std::endl;
    }
};

如果使用父类指针:

C++
Animal* animal = new Dog();
animal->speak();

此时输出的是:

Animal speak

原因在于 speak() 是普通成员函数,调用关系主要根据指针的静态类型 Animal* 在编译阶段确定。

如果希望父类指针能够根据实际对象类型调用子类版本,就需要将父类中的函数声明为 virtual

C++
class Animal {
public:
    virtual void speak() {
        std::cout << "Animal speak" << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Dog speak" << std::endl;
    }
};

再次执行:

C++
Animal* animal = new Dog();
animal->speak();

输出:

Dog speak

这就是C++虚函数实现父类指针调用子类方法的基本原理。

二、父类指针调用子类方法的完整示例

下面通过一个更加完整的例子理解运行时多态。

C++
#include 

class Animal {
public:
    virtual void speak() {
        std::cout << "Animal is speaking" << std::endl;
    }

    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Dog is barking" << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        std::cout << "Cat is meowing" << std::endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->speak();
    animal2->speak();

    delete animal1;
    delete animal2;

    return 0;
}

运行结果:

Dog is barking
Cat is meowing

这里有两个变量都是 Animal* 类型,但它们实际分别指向 DogCat 对象。

C++
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();

调用:

C++
animal1->speak();
animal2->speak();

程序会根据对象实际类型分别执行:

C++
Dog::speak()
Cat::speak()

这种根据运行时对象类型决定调用哪个函数的机制,就是运行时多态

三、为什么父类指针能够调用子类方法

理解这个问题,需要区分两个概念:静态类型动态类型

例如:

C++
Animal* animal = new Dog();

这里:

  • animal 的静态类型是 Animal*

  • animal 实际指向的对象类型是 Dog

对于普通函数:

C++
animal->speak();

编译器通常根据 Animal* 确定调用哪个函数。

对于虚函数,C++会保留运行时动态绑定的能力。程序运行过程中,会根据当前对象的实际类型找到对应的虚函数实现。

因此:

C++
Animal* animal = new Dog();
animal->speak();

最终可以调用:

C++
Dog::speak()

而不是:

C++
Animal::speak()

这也是“父类指针调用子类方法”成立的关键。

四、virtual关键字的作用

父类函数必须声明为虚函数。

例如:

C++
class Animal {
public:
    virtual void speak() {
        std::cout << "Animal" << std::endl;
    }
};

其中:

C++
virtual void speak()

表示 speak() 是虚函数。

子类可以重新实现:

C++
class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Dog" << std::endl;
    }
};

C++11以后推荐使用 override

override 并不是实现多态所必需的,但可以帮助编译器检查函数是否正确重写了父类虚函数。

例如不小心写成:

C++
class Dog : public Animal {
public:
    void speak(int type) override {
        std::cout << "Dog" << std::endl;
    }
};

由于参数列表不同,这并没有正确重写父类函数,编译器会直接报错。

因此,使用:

C++
void speak() override

能够减少函数重写过程中的隐蔽错误。

五、虚函数与普通函数的区别

可以通过一个简单例子直观比较。

普通成员函数

C++
class Parent {
public:
    void show() {
        std::cout << "Parent" << std::endl;
    }
};

class Child : public Parent {
public:
    void show() {
        std::cout << "Child" << std::endl;
    }
};

Parent* p = new Child();
p->show();

输出:

Parent

虚函数

C++
class Parent {
public:
    virtual void show() {
        std::cout << "Parent" << std::endl;
    }
};

class Child : public Parent {
public:
    void show() override {
        std::cout << "Child" << std::endl;
    }
};

Parent* p = new Child();
p->show();

输出:

Child

两段代码最关键的区别就是父类的 show() 是否使用 virtual

六、虚函数表是如何工作的

C++标准并没有规定必须使用“虚函数表”实现虚函数,但主流C++编译器通常采用虚函数表,也就是 vtable,配合虚函数指针实现动态绑定。

简单理解,可以把它看成一个函数地址表。

例如:

C++
class Animal {
public:
    virtual void speak() {
        std::cout << "Animal" << std::endl;
    }
};

编译器通常会为包含虚函数的类建立相应的虚函数表。

子类:

C++
class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Dog" << std::endl;
    }
};

其虚函数表中对应的 speak 项通常会指向:

Dog::speak()

当执行:

C++
Animal* animal = new Dog();
animal->speak();

程序会通过对象关联的虚函数机制找到 Dog::speak(),从而实现运行时多态。

需要注意的是,vtable、vptr属于常见实现方式,并不是C++语言标准强制规定的对象布局。因此编写跨平台代码时,不应该依赖某个编译器具体的虚表内存布局。

七、父类指针为什么是多态编程的重要形式

假设有多个动物类:

C++
class Animal {
public:
    virtual void speak() = 0;
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Dog" << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        std::cout << "Cat" << std::endl;
    }
};

此时可以使用统一的父类指针:

C++
Animal* animals[] = {
    new Dog(),
    new Cat()
};

for (Animal* animal : animals) {
    animal->speak();
}

不需要分别判断:

C++
if (animal is Dog) {
    // ...
}

if (animal is Cat) {
    // ...
}

而是直接调用统一接口:

C++
animal->speak();

最终由对象自身决定执行哪个版本。

这种设计能够降低代码之间的耦合,特别适合插件系统、图形界面、游戏开发、支付系统、数据库驱动等需要统一接口、多种实现的场景。

八、纯虚函数实现抽象父类

如果父类本身不需要提供具体实现,可以定义纯虚函数:

C++
class Animal {
public:
    virtual void speak() = 0;

    virtual ~Animal() = default;
};

其中:

C++
virtual void speak() = 0;

就是纯虚函数。

包含纯虚函数的类属于抽象类,不能直接创建对象:

C++
Animal animal;  // 错误

但可以使用父类指针指向子类对象:

C++
Animal* animal = new Dog();
animal->speak();
delete animal;

这种设计非常适合定义统一接口。

例如:

C++
class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

不同图形分别实现:

C++
class Circle : public Shape {
public:
    double area() const override {
        return 3.14159 * 10 * 10;
    }
};

class Rectangle : public Shape {
public:
    double area() const override {
        return 20 * 10;
    }
};

调用代码只需要:

C++
Shape* shape = new Circle();
std::cout << shape->area() << std::endl;

delete shape;

并不需要关心 shape 当前具体是哪一种图形。

九、虚析构函数非常重要

使用父类指针删除子类对象时,父类析构函数通常应该声明为虚函数。

例如:

C++
class Parent {
public:
    virtual ~Parent() {
        std::cout << "Parent destructor" << std::endl;
    }
};

子类:

C++
class Child : public Parent {
public:
    ~Child() {
        std::cout << "Child destructor" << std::endl;
    }
};

然后:

C++
Parent* p = new Child();
delete p;

能够按照正确的析构顺序执行子类和父类析构函数。

通常会看到:

Child destructor
Parent destructor

如果父类析构函数不是虚函数:

C++
class Parent {
public:
    ~Parent() {}
};

再执行:

C++
Parent* p = new Child();
delete p;

通过父类指针删除实际指向的派生类对象会产生未定义行为。

因此,只要一个类准备作为多态基类使用,通常就应该考虑提供虚析构函数:

C++
virtual ~Parent() = default;

十、使用智能指针管理多态对象

现代C++中,不建议频繁使用裸指针配合 newdelete 管理对象。

可以使用 std::unique_ptr

C++
#include 
#include 

class Animal {
public:
    virtual void speak() {
        std::cout << "Animal" << std::endl;
    }

    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Dog" << std::endl;
    }
};

int main() {
    std::unique_ptr<Animal> animal = std::make_unique<Dog>();

    animal->speak();

    return 0;
}

输出:

Dog

程序结束时,unique_ptr 会自动释放对象资源。

如果需要保存多种不同子类对象,可以使用:

C++
std::vector<std::unique_ptr<Animal>> animals;

animals.push_back(std::make_unique<Dog>());
animals.push_back(std::make_unique<Cat>());

for (const auto& animal : animals) {
    animal->speak();
}

这是一种更加符合现代C++实践的多态对象管理方式。

十一、哪些情况下父类指针不会调用子类方法

并不是所有子类方法都可以通过父类指针调用。

例如:

C++
class Parent {
public:
    virtual void show() {
        std::cout << "Parent show" << std::endl;
    }
};

class Child : public Parent {
public:
    void show() override {
        std::cout << "Child show" << std::endl;
    }

    void test() {
        std::cout << "Child test" << std::endl;
    }
};

下面的代码:

C++
Parent* p = new Child();
p->show();

可以调用:

C++
Child::show()

但是:

C++
p->test();

无法编译。

原因是 test() 并没有定义在 Parent 接口中。虽然实际对象是 Child,但表达式 p 的静态类型仍然是 Parent*

如果确实需要访问子类特有功能,可以使用类型转换,但应谨慎设计:

C++
Child* child = dynamic_cast<Child*>(p);

if (child != nullptr) {
    child->test();
}

如果代码中大量依赖这种向下转型,通常说明类层次结构或接口设计可能需要重新考虑。

十二、构造函数和析构函数中的虚函数需要特别注意

不要把普通运行时多态的理解简单套用到构造函数和析构函数中。

例如:

C++
class Parent {
public:
    Parent() {
        show();
    }

    virtual void show() {
        std::cout << "Parent" << std::endl;
    }
};

class Child : public Parent {
public:
    void show() override {
        std::cout << "Child" << std::endl;
    }
};

创建:

C++
Child child;

构造 Parent 部分时,虚函数调用不会像普通运行阶段那样分派到 Child::show()

这是因为此时派生类对象部分尚未完成构造。

析构阶段也存在类似规则。执行父类析构函数时,派生类部分已经完成析构,因此不能把它当作完整的派生类对象进行普通虚函数分派。

实际开发中,应尽量避免在构造函数和析构函数中依赖虚函数实现多态行为。

十三、父类指针调用子类方法的核心条件

要实现稳定的运行时多态,一般需要满足以下条件:

  1. 父类函数使用 virtual 声明。

  2. 子类提供正确的函数重写。

  3. 通过父类指针或父类引用访问该虚函数。

  4. 父类与子类之间存在继承关系。

  5. 多态对象的生命周期必须正确管理。

  6. 多态基类通常应提供虚析构函数。

典型结构可以概括为:

C++
class Base {
public:
    virtual void run() {
        std::cout << "Base" << std::endl;
    }

    virtual ~Base() = default;
};

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

然后:

C++
Base* ptr = new Derived();
ptr->run();
delete ptr;

最终调用:

Derived

这就是C++虚函数实现父类指针调用子类方法的完整基本模型。

十四、常见错误总结

1. 忘记使用virtual

C++
class Base {
public:
    void run() {}
};

这种情况下通过 Base* 调用时不会产生真正的运行时多态。

2. 子类函数签名写错

例如父类:

C++
virtual void run(int value);

子类却写成:

C++
void run(double value);

这并不是重写,而是定义了另外一个函数。

推荐使用:

C++
void run(int value) override;

让编译器帮助检查。

3. 父类析构函数不是virtual

如果通过父类指针管理子类对象,非虚析构函数可能导致未定义行为。

推荐:

C++
virtual ~Base() = default;

4. 把“父类指针”理解成“可以访问所有子类成员”

C++
Base* ptr = new Derived();

并不意味着 ptr 可以直接访问 Derived 独有的方法。

父类指针只能直接访问父类接口中可见的成员,而其中的虚函数可以根据实际对象类型发生动态绑定。

5. 滥用dynamic_cast

如果业务代码频繁出现:

C++
dynamic_cast<Derived*>(ptr)

往往意味着没有充分利用多态接口。更好的方式通常是把共同操作抽象到基类的虚函数中。

十五、C++虚函数与运行时多态的实际意义

虚函数真正解决的问题,并不是单纯“让父类指针调用子类方法”,而是让调用者依赖抽象接口,而不必依赖具体实现。

例如设计一个支付接口:

C++
class Payment {
public:
    virtual void pay(double amount) = 0;
    virtual ~Payment() = default;
};

不同支付方式实现统一接口:

C++
class Alipay : public Payment {
public:
    void pay(double amount) override {
        std::cout << "Alipay: " << amount << std::endl;
    }
};

class WeChatPay : public Payment {
public:
    void pay(double amount) override {
        std::cout << "WeChatPay: " << amount << std::endl;
    }
};

业务代码只需要:

C++
void processPayment(Payment& payment) {
    payment.pay(100);
}

调用:

C++
Alipay alipay;
WeChatPay wechat;

processPayment(alipay);
processPayment(wechat);

同一个:

C++
processPayment()

就可以处理不同的具体支付实现。

这就是面向对象设计中多态的价值:统一接口、隐藏实现、降低耦合、提高扩展能力

十六、总结

C++虚函数是实现运行时多态的重要机制。通过在父类中使用 virtual 声明函数,并由子类使用 override 正确重写,就可以让父类指针或父类引用指向不同子类对象时,根据对象实际类型调用对应的子类方法。

最典型的代码结构是:

C++
class Base {
public:
    virtual void show() {
        std::cout << "Base" << std::endl;
    }

    virtual ~Base() = default;
};

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

int main() {
    Base* ptr = new Derived();

    ptr->show();

    delete ptr;
}

其中最关键的三个知识点是:

virtual  → 开启虚函数动态绑定
override → 明确检查子类重写
virtual ~Base() → 保证多态对象正确析构

理解这套机制之后,就能进一步掌握C++中的抽象类、纯虚函数、接口设计、运行时多态以及面向对象设计原则。