【c++面向对象编程】第14篇:多态(一):虚函数——实现“一个接口,多种方法”

目录

一、一个没有多态的问题

二、虚函数的基本语法

声明虚函数

重写虚函数

通过基类指针/引用调用

三、override关键字(C++11)

它的作用

常见错误被拦截

正确写法

[四、静态绑定 vs 动态绑定](#四、静态绑定 vs 动态绑定)

静态绑定(编译时确定)

动态绑定(运行时确定)

对比表

五、完整例子:动物园的动物叫声

六、虚函数的限制

七、常见错误

[1. 忘记写virtual,导致静态绑定](#1. 忘记写virtual,导致静态绑定)

[2. 重写时签名不匹配(但忘了override)](#2. 重写时签名不匹配(但忘了override))

[3. 通过对象(而非指针/引用)调用虚函数](#3. 通过对象(而非指针/引用)调用虚函数)

[4. 在构造函数或析构函数中调用虚函数](#4. 在构造函数或析构函数中调用虚函数)

八、这一篇的收获


一、一个没有多态的问题

假设你有一个图形绘制程序,需要处理多种形状:圆形、矩形、三角形。

没有多态的写法可能是这样:

cpp

复制代码
class Circle {
public:
    void draw() { cout << "画一个圆" << endl; }
};

class Rectangle {
public:
    void draw() { cout << "画一个矩形" << endl; }
};

class Triangle {
public:
    void draw() { cout << "画一个三角形" << endl; }
};

// 需要一个一个处理,每种形状单独写代码
Circle c;
Rectangle r;
Triangle t;
c.draw();
r.draw();
t.draw();

如果有一个"形状数组",想统一调用draw(),就麻烦了------因为编译器不知道每个位置到底是什么类型。

多态的解决方案 :让所有形状继承自一个共同的基类Shape,然后通过基类指针来操作。

cpp

复制代码
class Shape {
public:
    virtual void draw() { cout << "画一个形状" << endl; }
};

class Circle : public Shape {
public:
    void draw() override { cout << "画一个圆" << endl; }
};

class Rectangle : public Shape {
public:
    void draw() override { cout << "画一个矩形" << endl; }
};

// 现在可以统一处理了
Shape* shapes[] = {new Circle(), new Rectangle()};
for (int i = 0; i < 2; i++) {
    shapes[i]->draw();  // 输出:画一个圆 / 画一个矩形
}

这就是多态的魅力:写代码时不知道具体类型,运行时决定调用哪个函数


二、虚函数的基本语法

声明虚函数

在基类中,用virtual关键字声明一个成员函数为虚函数:

cpp

复制代码
class Base {
public:
    virtual void show() { cout << "Base::show" << endl; }
};

重写虚函数

在派生类中,定义一个签名完全相同的函数来重写:

cpp

复制代码
class Derived : public Base {
public:
    void show() override { cout << "Derived::show" << endl; }
};

通过基类指针/引用调用

cpp

复制代码
Base* ptr = new Derived();
ptr->show();   // 输出 Derived::show(动态绑定)

Base& ref = *new Derived();
ref.show();    // 也是 Derived::show

关键:只有通过指针或引用调用虚函数时,才会发生动态绑定。直接通过对象调用,是静态绑定。

cpp

复制代码
Derived d;
d.show();           // 静态绑定,编译时就确定调用Derived::show
Base b = d;         // 对象切片!
b.show();           // 静态绑定,调用Base::show(因为b是Base对象)

三、override关键字(C++11)

override是C++11引入的关键字,强烈建议使用

它的作用

  1. 明确表达意图:告诉读者这个函数是要重写基类的虚函数

  2. 让编译器帮你检查:如果基类没有对应的虚函数(签名不匹配),编译报错

常见错误被拦截

cpp

复制代码
class Base {
public:
    virtual void func(int x) {}
};

class Derived : public Base {
public:
    void func(double x) override {   // ❌ 编译错误!
        // 基类没有 func(double),签名不匹配
    }
};

没有override的话,这只是一个隐藏 (不是重写),不会报错,程序逻辑就错了。有了override,编译器会帮你发现。

正确写法

cpp

复制代码
class Derived : public Base {
public:
    void func(int x) override {   // ✅ 正确重写
        // ...
    }
};

四、静态绑定 vs 动态绑定

这是理解虚函数的关键。

静态绑定(编译时确定)

cpp

复制代码
Base obj;
obj.show();   // 编译时就确定了:调用Base::show

编译器看到obj的类型是Base,直接生成调用Base::show的代码。

动态绑定(运行时确定)

cpp

复制代码
Base* ptr = getShape();  // 运行时才知道ptr指向什么
ptr->show();             // 运行时才知道调用哪个show

编译器不知道ptr到底指向BaseCircle还是Rectangle,所以它生成一段代码:通过虚函数表在运行时查找真正的函数地址(下一讲详细讲)。

对比表

特性 静态绑定 动态绑定
决定时机 编译时 运行时
函数类型 非虚函数 虚函数
调用方式 对象名调用 指针/引用调用
性能 快(直接调用) 稍慢(查表开销)

五、完整例子:动物园的动物叫声

cpp

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

// 基类:动物
class Animal {
protected:
    string name;
public:
    Animal(string n) : name(n) {}
    
    // 虚函数,每个动物叫法不同
    virtual void speak() const {
        cout << name << "发出某种声音" << endl;
    }
    
    // 虚析构函数(重要!下篇详细讲)
    virtual ~Animal() {}
};

// 派生类:狗
class Dog : public Animal {
public:
    Dog(string n) : Animal(n) {}
    
    void speak() const override {
        cout << name << "汪汪叫:汪汪!" << endl;
    }
    
    void wagTail() const {
        cout << name << "摇尾巴" << endl;
    }
};

// 派生类:猫
class Cat : public Animal {
public:
    Cat(string n) : Animal(n) {}
    
    void speak() const override {
        cout << name << "喵喵叫:喵~" << endl;
    }
    
    void climb() const {
        cout << name << "爬树" << endl;
    }
};

// 派生类:鸟
class Bird : public Animal {
public:
    Bird(string n) : Animal(n) {}
    
    void speak() const override {
        cout << name << "叽叽喳喳:啾啾!" << endl;
    }
    
    void fly() const {
        cout << name << "飞翔" << endl;
    }
};

// 让动物们依次叫(多态的核心用法)
void makeAllSpeak(const vector<Animal*>& animals) {
    cout << "\n=== 动物大合唱 ===" << endl;
    for (const auto* animal : animals) {
        animal->speak();  // 动态绑定,调用实际类型的版本
    }
}

int main() {
    // 创建动物数组,全部用基类指针指向
    vector<Animal*> zoo;
    zoo.push_back(new Dog("旺财"));
    zoo.push_back(new Cat("咪咪"));
    zoo.push_back(new Bird("小小"));
    zoo.push_back(new Animal("未知生物"));
    
    // 统一调用speak,每个动物发出自己的叫声
    makeAllSpeak(zoo);
    
    // 注意:通过基类指针无法访问派生类特有函数
    // zoo[0]->wagTail();  // ❌ 编译错误,Animal没有wagTail
    
    // 如果需要访问派生类特有函数,需要用dynamic_cast(后续章节)
    
    // 释放内存
    for (auto* animal : zoo) {
        delete animal;
    }
    
    return 0;
}

输出:

text

复制代码
=== 动物大合唱 ===
旺财汪汪叫:汪汪!
咪咪喵喵叫:喵~
小小叽叽喳喳:啾啾!
未知生物发出某种声音

注意到没有:makeAllSpeak函数只知道参数是Animal*,但实际执行时,每个动物都发出了自己特有的叫声。这就是多态的力量。


六、虚函数的限制

不是所有函数都可以是虚函数:

函数类型 可以是虚函数吗 原因
普通成员函数 ✅ 可以 最常见的虚函数
析构函数 ✅ 可以 非常推荐(下篇讲)
静态函数 ❌ 不可以 静态函数属于类,不属于对象,没有this
构造函数 ❌ 不可以 对象还没构造完,虚表未建立
内联函数 ⚠️ 不保证 虚函数动态绑定,内联是编译时展开,通常会被忽略
友元函数 ❌ 不可以 友元不是成员函数

七、常见错误

1. 忘记写virtual,导致静态绑定

cpp

复制代码
class Base {
public:
    void show() { cout << "Base" << endl; }   // 忘了virtual
};
class Derived : public Base {
public:
    void show() { cout << "Derived" << endl; }
};

Base* p = new Derived();
p->show();   // 输出"Base"!因为静态绑定,没有多态

2. 重写时签名不匹配(但忘了override)

cpp

复制代码
class Base {
public:
    virtual void func(int x) {}
};
class Derived : public Base {
public:
    void func(double x) {}   // 参数不同,这是隐藏,不是重写
    // 没有override,编译器不报错,但逻辑错了
};

3. 通过对象(而非指针/引用)调用虚函数

cpp

复制代码
Derived d;
Base b = d;   // 对象切片!派生类部分被切掉了
b.show();     // 调用Base::show,不是多态

4. 在构造函数或析构函数中调用虚函数

cpp

复制代码
class Base {
public:
    Base() { show(); }   // 调用Base::show,不会调用Derived::show
    virtual void show() { cout << "Base" << endl; }
};
class Derived : public Base {
public:
    void show() override { cout << "Derived" << endl; }
};

在构造期间,派生类部分还没构造完成,虚表指向基类,所以不会发生多态。


八、这一篇的收获

你现在应该理解:

  • virtual声明虚函数,用override标记重写

  • 通过基类指针或引用调用虚函数,实现动态绑定(多态)

  • 动态绑定的决策在运行时 ,静态绑定的决策在编译时

  • 多态让代码可以对扩展开放(加新形状不用改旧代码),符合开闭原则

💡 小作业:写一个MediaPlayer基类,有虚函数play()。派生类MP3PlayerVideoPlayerStreamingPlayer各自重写play()。写一个函数playMedia(MediaPlayer&),传入不同播放器时播放各自的内容。


下一篇预告:第15篇《多态(二):虚函数表(vtable)内存布局揭秘》------虚函数到底是怎么实现的?为什么会有性能开销?虚函数表(vtable)和虚指针(vptr)的原理是什么?了解这些,你才算真正精通C++多态。

相关推荐
_waylau1 小时前
“Java+AI全栈工程师”问答02:Spring Boot 自动配置原理
java·开发语言·spring boot·后端·spring
JAVA面经实录9171 小时前
Java架构师最终完整版学习路线图
java·开发语言·学习
雪度娃娃1 小时前
结构型设计模式——享元模式
c++·设计模式·享元模式
勤自省1 小时前
ROS2从入门到“重启解决”:21讲8~12章踩坑血泪史与核心总结
linux·开发语言·ubuntu·ssh·ros
TIEM_691 小时前
C++string|遍历、模拟实现、赋值拷贝现代写法
开发语言·c++
tellmewhoisi2 小时前
单独抽取用户服务(请求不通):feign添加拦截器(添加token)
java·开发语言
Hua-Jay2 小时前
OpenCV联合C++/Qt 学习笔记(十七)----凸包检测、直线检测及点集拟合
c++·笔记·qt·opencv·学习·计算机视觉
basketball6162 小时前
C++ Lambda 表达式完全指南
开发语言·c++·算法
不知名的老吴2 小时前
C++中emplace函数的不适场景总结(三)
开发语言·c++·算法