C++ 小白入门:多态到底是什么?一篇看懂!

作为编程小白,刚接触 C++ 的面向对象时,是不是被 “多态” 这个词搞得晕头转向?其实多态一点都不神秘,它就像生活中 “同一个行为,不同人做有不同结果” 的场景 —— 比如买票,普通人全价、学生半价、军人优先;再比如动物叫,猫 “喵喵”、狗 “汪汪”。今天就用最通俗的语言,结合实例带你吃透 C++ 多态的核心知识点!

一、先搞懂:多态到底是什么?

多态的本质是:继承关系下的不同类对象,调用同一个函数,却产生不同的行为

举个生活例子:

  • 基类(父类):Person(普通人),有一个BuyTicket()(买票)函数,行为是 “全价”;
  • 派生类(子类):Student(学生),继承自Person,重写BuyTicket()函数,行为是 “半价”;
  • 当用基类的指针或引用分别指向普通人对象和学生对象时,调用BuyTicket()会自动执行对应的行为 —— 这就是多态的魅力!

二、实现多态的 2 个核心条件(缺一不可)

想让代码实现多态效果,必须满足以下两个条件,记牢啦!

条件 1:基类的指针或引用调用函数

为什么必须用指针 / 引用?因为只有基类的指针或引用,才能 “身兼数职”—— 既能指向基类对象,又能指向派生类对象。就像一个通用接口,能适配不同的 “子类对象”。

条件 2:被调用的函数是虚函数,且完成重写

这是多态的核心,我们拆成两步理解:

第一步:什么是虚函数?

在类的成员函数前加virtual关键字,这个函数就是虚函数。比如:

class Person {
public:
    // 虚函数:加了virtual关键字
    virtual void BuyTicket() {
        cout << "普通人买票-全价" << endl;
    }
};

⚠️ 注意:非成员函数(比如全局函数)不能加virtual,加了会报错!

第二步:什么是虚函数重写?

派生类中有一个和基类完全相同的虚函数(函数名、参数列表、返回值都一样),这就叫 “重写”(也叫覆盖)。比如学生类重写买票函数:

class Student : public Person {
public:
    // 重写基类的虚函数:函数名、参数、返回值完全一致
    virtual void BuyTicket() {
        cout << "学生买票-半价" << endl;
    }
};

⚠️ 小坑提醒:派生类重写时,就算不加virtual关键字,也能构成重写(因为继承了基类的虚函数属性),但这种写法不规范,千万别学!

三、手把手写第一个多态程序

结合上面的条件,我们写一个完整的例子,跑起来看看效果:

#include <iostream>
using namespace std;

// 基类:普通人
class Person {
public:
    // 虚函数
    virtual void BuyTicket() {
        cout << "普通人买票-全价" << endl;
    }
};

// 派生类:学生(继承自Person)
class Student : public Person {
public:
    // 重写基类虚函数
    virtual void BuyTicket() {
        cout << "学生买票-半价" << endl;
    }
};

// 派生类:军人(继承自Person)
class Soldier : public Person {
public:
    // 重写基类虚函数
    virtual void BuyTicket() {
        cout << "军人买票-优先" << endl;
    }
};

// 关键:用基类引用调用函数(满足条件1)
void DoBuyTicket(Person& people) {
    people.BuyTicket(); // 调用哪个版本,由people指向的对象决定
}

int main() {
    Person p;       // 普通人对象
    Student s;      // 学生对象
    Soldier sol;    // 军人对象

    DoBuyTicket(p);  // 输出:普通人买票-全价
    DoBuyTicket(s);  // 输出:学生买票-半价
    DoBuyTicket(sol); // 输出:军人买票-优先

    return 0;
}

运行结果完全符合预期!这就是多态的魔力 —— 同一个DoBuyTicket函数,传入不同对象,自动执行不同行为。

四、多态的 “特殊情况”:协变与析构函数重写

除了上面的常规情况,还有两个实用的特殊场景,小白也得了解:

1. 协变(了解即可)

正常重写要求返回值完全一致,但有一种例外:基类虚函数返回基类指针 / 引用,派生类虚函数返回派生类指针 / 引用,这叫 “协变”,也能构成多态。比如:

class A {};
class B : public A {}; // B继承A

class Person {
public:
    // 返回基类A的指针
    virtual A* BuyTicket() {
        cout << "普通人买票-全价" << endl;
        return nullptr;
    }
};

class Student : public Person {
public:
    // 返回派生类B的指针(协变)
    virtual B* BuyTicket() {
        cout << "学生买票-半价" << endl;
        return nullptr;
    }
};

协变实际用得不多,知道有这个情况就行~

2. 析构函数的重写(重点!)

析构函数比较特殊:基类析构函数加virtual后,派生类析构函数不管加不加virtual,都算重写。

为什么要这么做?防止内存泄漏!比如:

class A {
public:
    // 基类析构函数加virtual
    virtual ~A() {
        cout << "~A()" << endl;
    }
};

class B : public A {
public:
    B() {
        _p = new int[10]; // 申请堆内存
    }
    // 派生类析构函数(自动重写基类虚析构)
    ~B() {
        cout << "~B():释放内存" << endl;
        delete[] _p; // 释放堆内存
    }
private:
    int* _p;
};

int main() {
    A* p1 = new A;
    A* p2 = new B; // 基类指针指向派生类对象

    delete p1; // 输出:~A()(正常释放)
    delete p2; // 输出:~B():释放内存 → ~A()(正确释放子类+基类)
    return 0;
}

如果基类析构函数不加virtualdelete p2时只会调用基类~A(),子类的_p内存不会释放,就会造成内存泄漏!所以记住:基类析构函数尽量加 virtual

五、C++11 新特性:override 和 final(避坑神器)

新手写重写时,很容易犯 “函数名写错”“参数漏写” 的错误,这些错误编译时不报错,运行时才出问题,排查起来很麻烦。C++11 提供了两个关键字,帮我们快速避坑:

1. override:检查是否重写成功

在派生类的虚函数后加override,编译器会自动检查是否真的重写了基类虚函数。如果没重写(比如函数名错了),直接编译报错!

class Student : public Person {
public:
    // 函数名写错了(BuyTicket→BuyTicker),加override会编译报错
    virtual void BuyTicker() override {
        cout << "学生买票-半价" << endl;
    }
};

2. final:禁止派生类重写

在基类的虚函数后加final,表示这个函数 “最终版”,派生类不能再重写它,否则编译报错:

class Person {
public:
    // 加final,禁止子类重写
    virtual void BuyTicket() final {
        cout << "普通人买票-全价" << endl;
    }
};

class Student : public Person {
public:
    // 报错:不能重写final函数
    virtual void BuyTicket() {
        cout << "学生买票-半价" << endl;
    }
};

六、纯虚函数与抽象类(强制子类实现接口)

有时候我们希望基类只定义 “接口”,不实现具体功能,强制子类必须实现这个接口 —— 这时候就需要 “纯虚函数” 和 “抽象类”。

1. 纯虚函数

在虚函数后加=0,就是纯虚函数,不需要写实现(写了也没用):

class Animal {
public:
    // 纯虚函数:只声明,不实现
    virtual void Sound() = 0; // 动物叫的接口
    virtual void Walk() = 0;  // 动物走的接口
};

2. 抽象类

包含纯虚函数的类,叫 “抽象类”。抽象类有两个特点:

  • 不能直接实例化对象(比如Animal a;会编译报错);
  • 派生类必须重写所有纯虚函数,否则派生类也是抽象类,不能实例化。

举个例子:

// 抽象类:Animal
class Animal {
public:
    virtual void Sound() = 0;
    virtual void Walk() = 0;
};

// 派生类:Dog(必须重写所有纯虚函数)
class Dog : public Animal {
public:
    virtual void Sound() {
        cout << "汪汪!" << endl;
    }
    virtual void Walk() {
        cout << "四条腿跑" << endl;
    }
};

// 派生类:Duck(必须重写所有纯虚函数)
class Duck : public Animal {
public:
    virtual void Sound() {
        cout << "嘎嘎!" << endl;
    }
    virtual void Walk() {
        cout << "两条腿走" << endl;
    }
};

int main() {
    // Animal a; // 报错:抽象类不能实例化
    Dog dog;
    Duck duck;
    dog.Sound(); // 汪汪!
    duck.Walk(); // 两条腿走
    return 0;
}

抽象类的作用就像 “模板”,强制所有子类遵循统一的接口,比如所有动物都必须有 “叫” 和 “走” 的功能,保证了代码的规范性。

七、多态的底层原理(小白浅尝即可)

最后聊聊多态的底层逻辑,不用深究,知道大概就行:

  1. 含有虚函数的类,每个对象都会多一个 “虚函数表指针”(简称vfptr),这个指针指向一张 “虚函数表”(简称vtable);
  2. 虚函数表是一个存储虚函数地址的数组,基类和派生类有各自的虚表;
  3. 派生类重写虚函数后,会用自己的函数地址覆盖虚表中对应的基类函数地址;
  4. 当用基类指针 / 引用调用虚函数时,会在运行时通过vfptr找到对应的虚表,再找到函数地址 —— 这就是 “动态绑定”(运行时确定调用哪个函数);
  5. 普通函数调用是 “静态绑定”(编译时就确定函数地址)。

简单说:多态是靠 “虚表指针 + 虚表” 实现的,运行时动态查找函数地址,才能做到 “一个调用,多种行为”。

八、总结:多态核心知识点速记

  1. 多态本质:继承下的不同对象,调用同一函数产生不同行为;
  2. 实现条件:基类指针 / 引用 + 虚函数重写;
  3. 虚函数:加virtual的成员函数,非成员函数不能加;
  4. 重写要求:函数名、参数、返回值完全一致(协变除外);
  5. 避坑神器:override检查重写,final禁止重写;
  6. 抽象类:含纯虚函数(=0),不能实例化,强制子类重写接口;
  7. 析构函数:基类析构加virtual,避免子类内存泄漏。

多态是 C++ 面向对象的核心特性之一,学会它能让代码更灵活、更易扩展。新手可以先从简单例子入手,理解清楚重写和多态的条件,再慢慢接触抽象类、析构函数重写等场景。多写几遍代码,跑一跑不同的情况,很快就能掌握啦!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值