C++三大特性----多态

1.概念

    在 C++ 中,多态(Polymorphism)是面向对象编程的三大核心特性之一(另外两个是封装和继承),它指的是同一操作作用于不同对象时,会产生不同的行为,不同对象做同一件事,产生的效果不同。

2.使用场景及构成条件

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

实现多态的条件:

  1. 存在继承关系:必须有基类派生类的继承结构,派生类通过public继承方式从基类派生(保证基类接口可被派生类使用)。
  2. 基类中声明虚函数,派生类重写该虚函数:基类在成员函数声明前加virtual关键字,将其定义为虚函数
  3. 通过基类指针或引用调用虚函数。

总结两句话:

必须是基类的指针或者引用调用虚函数。

被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。

class person
{
public:
	virtual void ticket()
	{
		cout << "买票-全价" << endl;
	}
};

class student :public person
{
public:
	void ticket()
	{
		cout << "买票-半价" << endl;
	}
};

void test1()
{
	person* p1 = new person;
	person* p2 = new student;

	p1->ticket();
	p2->ticket();

}

3.虚函数的重写

    在 C++ 中,虚函数的重写(Override) 是指派生类重新实现基类中声明的虚函数,是实现多态的核心机制。重写后的函数会在运行时根据对象实际类型被调用,而非编译时的指针 / 引用类型。

1.函数重写的要求

    虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。


注意:

  1. 在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
  2. 虚函数的重写只是重写了函数的实现,有时会出现基类和派生类的函数参数的缺省值不同,这时要以基类的缺省值为主。

2.析构函数的重写

在C++继承部分提到了,派生类的默认成员函数例如,构造函数,拷贝构造等,要显示调用基类的成员函数,但是析构函数是个特例。

在析构函数中如果显示调用了基类的析构函数,会产生对同一部分的内存空间析构两次产生问题。

注意:

  • 派生类对象继承了基类对象,派生类对象在析构时,要先析构派生类的部分,再析构基类的部分,遵循先创建后析构,如果先析构基类对象,无法找到派生类在内存中的位置,导致内存泄露。
  • 虚函数重写要求函数名,参数,返回类型相同,但是析构函数的名字要求和类名相同,所以C++通过函数名修饰把所有析构函数改为了destructor
  • 析构函数编译器会自动调用无需人为显示调用。

4.override和final关键字

    虚函数重写要求比较严格,难免会发生函数名写错等错误,导致无法成功重写虚函数,override关键字可以帮助程序员判断虚函数是否成功被重写。 

    如果不想一个虚函数被派生类重写可以使用final修饰

class Base {
public:
    virtual void func(int x) { /* 基类实现 */ }
    virtual void show() { /* 基类实现 */ }
};

class Derived : public Base {
public:
    // 正确:重写基类的func(int),编译器验证通过
    void func(int x) override { /* 派生类实现 */ }

    // 错误:基类中是func(int),此处参数为double,签名不匹配
    // 编译器会报错:'Derived::func' does not override any base class member
    void func(double x) override { /* 错误实现 */ }

    // 错误:基类中无show(int)函数,无法重写
    // 编译器会报错:'Derived::show' does not override any base class member
    void show(int x) override { /* 错误实现 */ }
};
class Base {
public:
    // 声明为final,禁止派生类重写
    virtual void func() final { /* 基类实现 */ }
};

class Derived : public Base {
public:
    // 错误:试图重写被final标记的函数
    // 编译器会报错:'Derived::func' cannot override 'Base::func' because it is declared final
    void func() override { /* 错误实现 */ }
};

5.重载/重写/隐藏的区别

重载
  1. 两个函数在同一作用域
  2. 函数名相同,参数不同,参数的类型或个数不同,返回值可同,可不同
重写/覆盖
  1. 两个函数分别的继承体系的基类,派生类不同作用域
  2. 函数名,参数,返回值,必须相同
  3. 两个函数必须是虚函数
隐藏
  1. 两个函数分别的继承体系的基类,派生类不同作用域
  2. 函数名相同
  3. 两个函数不构成重写就是隐藏
  4. 父子类成员变量相同也叫隐藏

6.纯虚函数和抽象类

1.纯虚函数

定义:纯虚函数是一种特殊的虚函数,它在基类中声明但不提供具体实现,而是强制派生类必须重写该函数。

声明:在虚函数声明的末尾加上 = 0

class Base {
public:
    // 纯虚函数:基类不实现,仅定义接口
    virtual void func() = 0; 
};
  • 纯虚函数没有函数体,基类中仅作为接口存在;
  • 派生类必须重写所有纯虚函数,否则派生类仍为抽象类;
  • 纯虚函数可以有函数体(C++11 后允许),但仍需在声明时加 = 0,且派生类仍需重写(可通过 Base::func() 调用基类实现)。
class Base {
public:
    virtual void func() = 0; // 纯虚函数声明
};

// 纯虚函数的函数体(可选,需在类外定义)
void Base::func() {
    cout << "Base的默认实现" << endl;
}

2.抽象类

定义:包含纯虚函数的类称为抽象类,它不能实例化对象,只能作为基类被继承。

// 抽象类(含纯虚函数)
class Shape { 
public:
    virtual double area() = 0; // 纯虚函数:计算面积
    virtual void draw() = 0;   // 纯虚函数:绘制图形
};

// 错误:抽象类不能实例化对象
Shape s; // 编译报错:cannot declare variable 's' to be of abstract type 'Shape'

7.多态的原理

1.虚函数表

虚函数表存在的原因:虚函数的地址无法在编译链接时确定

    虚函数的地址在编译链接阶段无法确定,核心原因是多态机制要求函数调用与具体实现的绑定必须延迟到运行时,而编译链接阶段的信息不足以支撑这种动态关联。

  1. 编译阶段的信息局限性
    编译是按单个源文件(编译单元)独立进行的。当编译器处理基类的虚函数时,无法预知所有可能的派生类会如何重写该虚函数 —— 派生类可能定义在其他文件中,甚至在编译时还未被编写(例如后续扩展的代码)。                                                                                         例如:基类 Shape 有虚函数 draw(),编译器编译 Shape 时,根本不知道将来会有 CircleRectangle 等派生类重写 draw(),自然无法确定最终调用的 draw() 地址。

  2. 链接阶段无法处理动态类型
    链接阶段虽然会合并多个编译单元的符号,但它只能确定静态可见的函数地址(如非虚函数的地址,因为它们的调用在编译时就已绑定)。
    然而,虚函数的调用依赖于对象的实际类型(而非指针 / 引用的声明类型),而对象的实际类型在运行时才能确定(例如基类指针可能指向基类对象,也可能指向任意派生类对象)。
    链接阶段无法预知程序运行时指针会指向哪种具体类型的对象,因此无法提前确定要调用的虚函数版本。

  3. 动态绑定的本质需求
    多态的核心是 “动态绑定”:即调用哪个函数版本,由运行时对象的实际类型决定,而非编译时的声明类型。
    若虚函数地址在编译链接时就被确定,就会退化为 “静态绑定”(类似非虚函数),无法实现 “同一接口,不同实现” 的多态特性。
    例如:Shape* p = new Circle(); p->draw(); 中,p 的声明类型是 Shape,但实际指向 Circle,必须在运行时确定调用 Circle::draw() 而非 Shape::draw()

综上,虚函数地址无法在编译链接时确定,是多态机制对 “动态绑定” 的本质要求,也是应对代码扩展性(如未知派生类、动态库)的必然选择。编译器通过虚函数表将地址解析延迟到运行时,最终实现了 “根据对象实际类型调用对应函数” 的多态能力。

派生类的虚函数是如何进行重写的:

1. 虚函数表的独立性

每个包含虚函数的类(包括基类和派生类)都会编译生成一个独立的虚函数表(vtable):

  • 基类的 vtable 中存储着基类所有虚函数的地址。
  • 派生类的 vtable 是在基类 vtable 的基础上构建的:
    • 对于派生类未重写的基类虚函数,派生类 vtable 中会直接复用基类虚函数的地址。
    • 对于派生类重写的基类虚函数,派生类 vtable 中会用派生类自己的虚函数地址替换基类对应位置的地址。
    • 派生类新增的虚函数,会被添加到自身 vtable 的末尾(顺序由声明顺序决定)。

简言之:基类和派生类的 vtable 是相互独立的,但派生类 vtable 会 “继承” 基类 vtable 的结构并根据重写 / 新增函数进行修改。

2. 派生类虚函数重写的过程

“重写”(override)指派生类定义了与基类虚函数签名完全相同(返回值、参数列表、const 属性均一致)的函数,且该函数也声明为虚函数(或隐式继承虚属性)。

重写的本质是替换派生类 vtable 中对应位置的函数地址,具体步骤:

  1. 编译器为基类生成 vtable,其中包含基类所有虚函数的地址(例如:Base::func1()Base::func2())。
  2. 派生类继承基类时,先复制基类 vtable 的结构作为自身 vtable 的基础。
  3. 若派生类重写了基类的虚函数(例如Derived::func1()重写Base::func1()),则派生类 vtable 中原来Base::func1()的地址会被替换为Derived::func1()的地址。
  4. 若派生类有新增的虚函数(例如Derived::func3()),则在自身 vtable 的末尾添加该函数的地址。

此时,当通过基类指针 / 引用指向派生类对象并调用虚函数时,会通过对象的虚指针(vptr)找到派生类的 vtable,进而调用派生类重写后的函数(多态的实现)。

虚函数表为什么存储在对象中:

  1. 确保每个对象关联正确的虚函数表
    虚函数表是类级别的全局资源(公共空间),但不同类型的对象(基类 / 派生类)需要关联不同的虚函数表。如果不将 vptr(虚函数表指针) 存在对象中,就无法在运行时区分一个基类指针指向的是基类对象还是派生类对象,也就无法确定该调用哪个版本的虚函数。

  2. 内存效率平衡
    虚函数表本身存放在全局公共空间(每个类一份),避免了重复存储;而对象中仅需一个 vptr(通常 4/8 字节),开销极小。如果不使用 vptr,而是让每个对象直接存储所有虚函数的地址,会导致大量冗余(尤其是当类有多个虚函数或对象数量庞大时)。

2.多态是如何实现的

在 C++ 中,多态(尤其是动态多态)的实现依赖于虚函数(virtual function) 和虚函数表(vtable) 机制,核心是通过 “运行时绑定” 让不同对象对同一行为做出不同响应。

运行时绑定(动态 dispatch)

当通过基类指针 / 引用调用虚函数时,程序不会在编译期确定调用哪个函数,而是在运行时

  1. 通过基类指针找到对象的 vptr。
  2. 由 vptr 定位到该对象实际所属类的 vtable。
  3. 在 vtable 中找到对应虚函数的地址并调用。
#include <iostream>
using namespace std;

// 基类
class Animal {
public:
    // 虚函数:允许派生类重写
    virtual void speak() { 
        cout << "动物叫" << endl; 
    }
};

// 派生类Dog
class Dog : public Animal {
public:
    // 重写基类的虚函数
    void speak() override { 
        cout << "汪汪叫" << endl; 
    }
};

// 派生类Cat
class Cat : public Animal {
public:
    // 重写基类的虚函数
    void speak() override { 
        cout << "喵喵叫" << endl; 
    }
};

int main() {
    Animal* animal1 = new Dog();  // 基类指针指向Dog对象
    Animal* animal2 = new Cat();  // 基类指针指向Cat对象
    
    animal1->speak();  // 运行时调用Dog::speak(),输出"汪汪叫"
    animal2->speak();  // 运行时调用Cat::speak(),输出"喵喵叫"
    
    delete animal1;
    delete animal2;
    return 0;
}

多态的核心是通过虚函数表和虚表指针,在运行时根据对象的实际类型动态选择要调用的函数。这种机制让代码更灵活(如上述示例中,新增派生类无需修改调用逻辑),是面向对象编程中 “封装、继承、多态” 三大特性的重要体现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值