C++ 多态与虚函数入门:从概念到规则

引言

在面向对象编程中,多态是三大特性(封装、继承、多态)中最精髓的一个。它字面意思是"多种形态",在C++中,多态允许我们通过基类指针或引用调用派生类的重写函数,从而实现"一个接口,多种实现"。

简单来说:同一个函数名,在不同的对象上执行不同的行为

在我学习C++的过程中,多态曾让我既兴奋又困惑------兴奋的是它让代码如此灵活,困惑的是虚函数、虚表、重写这些概念交织在一起。今天,我们先从基础开始,理解多态的概念和虚函数的规则。

第一部分:什么是多态?

一、多态的分类

类型 别名 发生时机 实现方式
静态多态 编译时多态 编译阶段 函数重载、模板
动态多态 运行时多态 运行阶段 虚函数、继承
cpp 复制代码
// 静态多态:函数重载
class Calculator {
public:
    int add(int a, int b) { return a + b; }      // 编译时确定
    double add(double a, double b) { return a + b; } // 编译时确定
};

// 动态多态:虚函数(本节重点)
class Animal {
public:
    virtual void speak() { cout << "动物叫" << endl; }  // 运行时确定
};

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

二、多态的核心思想***

第二部分:虚函数的基本概念

一、什么是虚函数?

虚函数是在基类中使用关键字 virtual 声明的成员函数,它可以在派生类中被重写(override)

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

class Shape {
public:
    // 虚函数
    virtual void draw() {
        cout << "绘制图形" << endl;
    }
    
    // 普通函数(非虚)
    void info() {
        cout << "这是一个图形" << endl;
    }
};

class Circle : public Shape {
public:
    // 重写虚函数
    void draw() override {
        cout << "绘制圆形" << endl;
    }
    
    // 隐藏普通函数(不推荐)
    void info() {
        cout << "这是一个圆形" << endl;
    }
};

int main() {
    Circle c;
    Shape* p = &c;  // 基类指针指向派生类对象
    
    p->draw();      // 输出:绘制圆形(虚函数:调用派生类版本)
    p->info();      // 输出:这是一个图形(普通函数:调用基类版本)
    
    return 0;
}

二、虚函数的作用

cpp 复制代码
// 没有虚函数:无法实现多态
class Bird {
public:
    void fly() { cout << "鸟在飞" << endl; }
};

class Penguin : public Bird {
public:
    void fly() { cout << "企鹅不会飞" << endl; }
};

void makeFly(Bird* b) {
    b->fly();  // 永远调用 Bird::fly()
}

// 有虚函数:实现多态
class BirdV {
public:
    virtual void fly() { cout << "鸟在飞" << endl; }
};

class PenguinV : public BirdV {
public:
    void fly() override { cout << "企鹅不会飞" endl; }
};

void makeFlyV(BirdV* b) {
    b->fly();  // 根据实际对象类型调用
}

int main() {
    Penguin p;
    makeFly(&p);   // 输出:鸟在飞(不是期望的结果)
    
    PenguinV pv;
    makeFlyV(&pv); // 输出:企鹅不会飞(正确的多态行为)
    
    return 0;
}

第三部分:虚函数的规则

一、虚函数的基本规则

cpp 复制代码
class Base {
public:
    // 规则1:虚函数用 virtual 关键字声明
    virtual void func1() { cout << "Base::func1" << endl; }
    
    // 规则2:虚函数可以有默认实现
    virtual void func2() { cout << "Base::func2" << endl; }
    
    // 规则3:虚函数可以是纯虚函数(下节讲解)
    // virtual void func3() = 0;  // 纯虚函数
    
    // 规则4:析构函数通常应该是虚函数
    virtual ~Base() { cout << "Base析构" << endl; }
};

class Derived : public Base {
public:
    // 规则5:重写虚函数时,函数签名必须完全相同
    // 返回类型、函数名、参数列表都要一致
    void func1() override { cout << "Derived::func1" << endl; }
    
    // 规则6:可以使用 override 关键字(C++11)明确表示重写
    void func2() override { cout << "Derived::func2" endl; }
    
    // 错误示例:参数不同,这是重载/隐藏,不是重写
    // void func1(int x) { }  // 这会隐藏基类的 func1
    
    ~Derived() { cout << "Derived析构" << endl; }
};

二、虚函数规则详细说明

规则1:虚函数必须通过指针或引用调用才能实现多态
cpp 复制代码
class Base {
public:
    virtual void show() { cout << "Base" << endl; }
};

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

int main() {
    Derived d;
    Base b;
    
    Base* p1 = &d;
    p1->show();  // ✅ 多态:输出 Derived
    
    Base& r1 = d;
    r1.show();   // ✅ 多态:输出 Derived
    
    Base b1 = d; // 对象切片
    b1.show();   // ❌ 不是多态:输出 Base(切片丢失了派生类信息)
    
    return 0;
}
规则2:虚函数不能是静态函数
cpp 复制代码
class Test {
public:
    // static virtual void func();  // 错误!虚函数不能是静态的
    // 静态函数属于类,不属于对象,无法实现多态
};
规则3:虚函数不能是内联函数(但编译器可能忽略)
cpp 复制代码
class Test {
public:
    // virtual inline void func();  // 不推荐,虚函数通常不内联
    // 因为虚函数的调用需要在运行时确定,内联在编译时展开
};
规则4:构造函数不能是虚函数
cpp 复制代码
class Test {
public:
    // virtual Test() { }  // 错误!构造函数不能是虚函数
    // 构造对象时需要知道确切类型,无法动态决定
};
规则5:虚函数的重写要求函数签名完全一致
cpp 复制代码
class Base {
public:
    virtual void func() { }
    virtual void func(int x) { }
};

class Derived : public Base {
public:
    // ✅ 正确:签名完全一致
    void func() override { }
    
    // ✅ 正确:重写另一个虚函数
    void func(int x) override { }
    
    // ❌ 错误:返回值类型不同(特殊情况除外:协变返回类型)
    // int func() override { return 0; }
    
    // ❌ 错误:参数不同,这是隐藏,不是重写
    // void func(double x) { }
};
规则6:协变返回类型(特殊情况)
cpp 复制代码
class Base {
public:
    virtual Base* clone() const {
        return new Base(*this);
    }
};

class Derived : public Base {
public:
    // 允许:返回类型是基类返回类型的派生类指针
    virtual Derived* clone() const override {
        return new Derived(*this);
    }
};
规则7:虚函数的默认参数不会动态绑定
cpp 复制代码
class Base {
public:
    virtual void func(int x = 10) {
        cout << "Base::func: " << x << endl;
    }
};

class Derived : public Base {
public:
    void func(int x = 20) override {
        cout << "Derived::func: " << x << endl;
    }
};

int main() {
    Base* p = new Derived();
    p->func();  // 输出:Derived::func: 10
    // 函数体是 Derived 的,但默认参数使用的是 Base 的!
    // 警告:不要重新定义虚函数的默认参数
    
    delete p;
    return 0;
}

第四部分:虚函数与析构函数

一、为什么基类析构函数应该是虚函数?

cpp 复制代码
// 错误示例:基类析构函数不是虚函数
class BaseWrong {
public:
    ~BaseWrong() { cout << "BaseWrong析构" << endl; }
};

class DerivedWrong : public BaseWrong {
private:
    int* data;
public:
    DerivedWrong() : data(new int[100]) { }
    ~DerivedWrong() { 
        delete[] data;
        cout << "DerivedWrong析构" << endl; 
    }
};

int main() {
    BaseWrong* p = new DerivedWrong();
    delete p;  // 只调用 BaseWrong 的析构函数!
    // 问题:DerivedWrong 的析构函数没有被调用,data 内存泄漏!
    return 0;
}

// 正确示例:基类析构函数是虚函数
class BaseCorrect {
public:
    virtual ~BaseCorrect() { cout << "BaseCorrect析构" << endl; }
};

class DerivedCorrect : public BaseCorrect {
private:
    int* data;
public:
    DerivedCorrect() : data(new int[100]) { }
    ~DerivedCorrect() override { 
        delete[] data;
        cout << "DerivedCorrect析构" << endl; 
    }
};

int main() {
    BaseCorrect* p = new DerivedCorrect();
    delete p;  // 先调用 DerivedCorrect 析构,再调用 BaseCorrect 析构
    // ✅ 正确释放资源
    return 0;
}

二、规则总结:只要类会被继承,析构函数就应该是虚函数

cpp 复制代码
class Interface {
public:
    virtual ~Interface() = default;  // 虚析构函数
    virtual void doSomething() = 0;
};

第五部分:override 和 final 关键字(C++11)

一、override:显式声明重写

cpp 复制代码
class Base {
public:
    virtual void func1() { }
    virtual void func2(int x) { }
    virtual void func3() const { }
};

class Derived : public Base {
public:
    // ✅ 明确表示要重写基类的虚函数
    void func1() override { }
    
    // ❌ 编译错误:参数不匹配,override 会检查
    // void func2(double x) override { }
    
    // ❌ 编译错误:const 不匹配
    // void func3() override { }
    
    // 建议:只要重写虚函数,就加上 override
};

二、final:禁止重写或禁止继承

cpp 复制代码
class Base {
public:
    virtual void func() final {  // final:派生类不能重写这个函数
        cout << "Base::func" << endl;
    }
};

class Derived : public Base {
public:
    // void func() override { }  // 错误!func 被 final 禁止重写
};

class FinalClass final {  // final:不能被继承
    // ...
};

// class Bad : public FinalClass { };  // 错误!FinalClass 被 final 禁止继承

第六部分:完整示例------动物叫声系统

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

// 基类:动物
class Animal {
protected:
    string name;
    
public:
    Animal(const string& n) : name(n) { }
    
    // 虚函数:发出声音
    virtual void speak() const {
        cout << name << "发出声音" << endl;
    }
    
    // 虚函数:获取类型
    virtual string getType() const {
        return "动物";
    }
    
    // 虚析构函数:确保派生类正确析构
    virtual ~Animal() {
        cout << name << "被销毁" << endl;
    }
};

// 派生类:狗
class Dog : public Animal {
public:
    Dog(const string& n) : Animal(n) { }
    
    void speak() const override {
        cout << name << "汪汪叫" << endl;
    }
    
    string getType() const override {
        return "狗";
    }
};

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

// 派生类:鸟
class Bird : public Animal {
public:
    Bird(const string& n) : Animal(n) { }
    
    void speak() const override {
        cout << name << "叽叽喳喳" << endl;
    }
    
    string getType() const override {
        return "鸟";
    }
};

// 多态演示函数
void makeSound(const Animal* animal) {
    cout << "这是一只" << animal->getType();
    cout << ",它在";
    animal->speak();
}

int main() {
    cout << "=== 多态演示 ===" << endl;
    
    // 基类指针数组
    vector<Animal*> animals;
    animals.push_back(new Dog("旺财"));
    animals.push_back(new Cat("咪咪"));
    animals.push_back(new Bird("啾啾"));
    
    // 统一调用,行为不同
    for (Animal* a : animals) {
        makeSound(a);
    }
    
    // 清理内存
    for (Animal* a : animals) {
        delete a;  // 虚析构函数确保正确释放
    }
    
    return 0;
}

/* 输出:
=== 多态演示 ===
这是一只狗,它在旺财汪汪叫
这是一只猫,它在咪咪喵喵叫
这是一只鸟,它在啾啾叽叽喳喳
旺财被销毁
咪咪被销毁
啾啾被销毁
*/

总结

一、虚函数核心规则速查表

规则 说明
声明方式 使用 virtual 关键字
调用方式 通过指针或引用调用才能实现多态
重写要求 函数签名必须完全相同(返回值、函数名、参数)
构造函数 不能是虚函数
析构函数 基类析构函数应该是虚函数
静态函数 不能是虚函数
默认参数 不会动态绑定,不要重新定义
override 推荐使用,编译器会检查重写是否正确
final 禁止重写或禁止继承

二、虚函数 vs 普通函数

特性 普通函数 虚函数
绑定时机 编译时 运行时
调用方式 通过对象或指针 通常通过指针/引用
派生类重写 隐藏(不推荐) 重写(override)
性能 快(直接调用) 稍慢(通过虚表)
多态支持 不支持 支持

三、使用建议

  1. 基类析构函数必须设为虚函数(如果类会被继承)

  2. 重写虚函数时使用 override 关键字(C++11)

  3. 不需要被继承的类可以使用 final

  4. 不要重新定义虚函数的默认参数

  5. 通过基类指针/引用调用虚函数才能实现多态

虚函数是实现运行时多态的基础。理解虚函数的规则,是掌握C++面向对象编程的关键一步。

核心记忆:

  • virtual 告诉编译器:这个函数需要动态绑定

  • override 告诉编译器:我要重写基类的虚函数

  • final 告诉编译器:到此为止,不能再重写/继承

下一节,我们将深入讲解虚函数的底层实现原理------虚函数表(vtable)和虚函数指针(vptr),敬请期待!

相关推荐
kyle~2 小时前
工业以太网协议---EtherCAT
开发语言·c++·网络协议·机器人·ros2
say_fall2 小时前
深入理解AVL树:平衡调整机制与性能优化实战
开发语言·数据结构·c++·学习
赖在沙发上的熊2 小时前
Python数据序列
开发语言·python
Hello--_--World2 小时前
Js面试题目录表
开发语言·javascript·ecmascript
tankeven2 小时前
HJ180 游游的最长稳定子数组
c++·算法
聆风吟º2 小时前
【C标准库】深入理解C语言strcmp函数:字符串比较的核心用法
c语言·开发语言·库函数·strcmp
kyle~2 小时前
机器人广域网通信---MQTT技术
大数据·c++·机器人·ros2
Fanfanaas2 小时前
Linux 进程篇 (四)
linux·运维·服务器·开发语言·c++·学习
Sylvia-girl2 小时前
C++中类与对象
开发语言·c++