admin管理员组

文章数量:1794759

十一、多态

十一、多态

多态是面向对象编程中的一个核心概念,它指的是允许不同类的对象对同一消息作出响应,即同一操作作用于不同的对象,可以有不同的行为。多态问题的引入,可以从以下几个方面进行阐述:

多态的定义与特性

  • 定义:多态(Polymorphism)指为不同数据类型的实体提供统一的接口。多态类型(Polymorphic Type)可以将自身所支持的操作套用到其它类型的值上。简单来说,多态意味着相同的消息给予不同的对象会引发不同的动作。
  • 特性
    • 接口性:多态是超类通过方法签名,向子类提供一个共同的接口。
    • 延迟绑定(动态绑定):调用方法时,在运行时再决定调用哪个类的方法。
    • 替换性:多态对已存在具有继承关系的类进行扩展。

多态问题的引入背景

在面向对象编程中,多态的引入主要是为了解决以下问题:

  1. 提高代码的复用性:通过继承,子类可以继承父类的属性和方法,而多态则允许子类重写父类的方法,从而实现特定于子类的行为。这样,当使用父类类型的引用指向子类对象时,就可以根据对象的实际类型调用相应的方法,从而提高代码的复用性。
  2. 增强程序的扩展性:多态使得程序能够更容易地添加新的类,并且能够在不修改现有代码的情况下,使新类能够正确地工作。这是因为多态允许程序在运行时动态地确定对象的实际类型,并根据该类型调用相应的方法。
  3. 解耦:多态有助于实现低耦合的设计,即各个模块之间的依赖关系尽可能简单。通过多态,可以在不同的模块之间定义统一的接口,使得模块之间不需要知道对方的具体实现细节,只需要知道接口即可。

多态三要素

C++多态是面向对象编程中的一个重要特性,它允许不同类的对象对同一消息作出响应。在C++中,多态主要通过虚函数来实现,并涉及以下三个关键要素:

继承关系

  • 说明:多态必须发生在具有继承关系的类之间。即,一个类(子类或派生类)必须从另一个类(父类或基类)继承而来。
  • 作用:继承为子类提供了重写基类方法的能力,是实现多态的基础。

虚函数

  • 说明:在基类中,需要有一个或多个被声明为virtual的函数,这些函数被称为虚函数。子类可以重写这些虚函数,以提供特定的实现。
  • 重要性:虚函数是实现多态的核心机制。通过虚函数,程序可以在运行时根据对象的实际类型来决定调用哪个版本的函数。

指针或引用调用

  • 说明:多态的调用必须通过基类类型的指针或引用来实现。即,需要使用基类类型的指针或引用来指向子类对象,并通过该指针或引用来调用虚函数。
  • 原理:在运行时,程序会根据指针或引用所指向对象的实际类型,在虚函数表中查找并调用相应的函数版本。

总结

C++多态的三要素可以概括为:

  • 继承关系:子类继承自基类,为多态提供了基础。
  • 虚函数:基类中声明为virtual的函数,允许子类进行重写。
  • 指针或引用调用:通过基类类型的指针或引用来调用虚函数,实现多态。

这三个要素共同作用,使得C++中的多态成为可能,从而提高了代码的复用性、扩展性和灵活性。

多态的分类

多态主要分为以下几种类型:

  • 变量多态:基类型的变量(对于C++是引用或指针)可以被赋值基类型对象,也可以被赋值派生类型的对象。
  • 函数多态:相同的函数调用界面(函数名与实参表),传送给一个对象变量,可以有不同的行为,这视该对象变量所指向的对象类型而定。
  • 动态多态:通过类继承机制和虚函数机制生效于运行期,可以优雅地处理异质对象集合。
  • 静态多态:模板也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为“静态”。

多态的意义

  1. 提高代码的复用性和可扩展性: 多态允许使用基类类型的指针或引用来引用派生类的对象,这样就可以通过基类指针或引用来调用派生类中的方法,而无需知道具体的派生类类型。这大大增加了代码的复用性和可扩展性。
  2. 实现接口的重用: 在多态中,可以为多个类提供一个共同的接口,这些类在继承这个接口后可以各自实现自己的功能。这样,当需要调用这个接口时,就可以根据实际的对象类型来调用相应的方法,而无需为每个类都编写相同的接口代码。
  3. 增强程序的灵活性和可维护性: 多态使得程序能够更灵活地应对变化。当需要添加新的派生类时,只需要确保这个新类实现了基类中的接口,就可以将其无缝地集成到现有的程序中。此外,由于多态的存在,当需要修改某个类的行为时,通常只需要修改这个类本身,而无需修改其他使用这个类的代码。
  4. 支持泛型编程: 在一些支持泛型编程的语言中(如C++的模板),多态也是实现泛型编程的重要机制之一。通过多态,可以编写出与具体类型无关的代码,这些代码可以在编译时根据具体的类型参数来生成相应的代码。 提高软件的可测试性:

多态使得可以使用模拟(Mock)对象来测试那些依赖于外部系统或复杂对象的代码。通过创建基类类型的模拟对象,并在测试中使用这些模拟对象来替代真实的对象,可以更容易地控制测试环境并验证代码的正确性。

函数的重载、重写、重定义

函数重载

  • 必须在同一个类中进行(作用域相同)
  • 子类无法重载父类的函数,父类同名函数将被名称覆盖
  • 重载是在编译期间根据参数类型和个数决定函数调用

函数重定义

  • 发生于父类和子类之间,如果子类写了个和父类函数原型一样的函数,并且父类中的函数没有声明为虚函数,则子类会直接覆盖掉父类的函数
  • 通过父类指针或引用执行子类对象时,会调用父类的函数

虚函数重写

  • 必须发生于父类和子类之间
  • 并且父类与子类中的函数必须有完全相同的原型
  • 使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义)
  • 多态是在运行期间根据具体对象的类型决定函数调用

虚函数

虚函数是基类中声明并在派生类中可以重写的函数。通过在基类中将函数声明为virtual,我们可以实现多态,即使用基类类型的指针或引用来调用派生类中的成员函数。这是通过虚函数表(Virtual Function Table, VTable)实现的,每个包含虚函数的类的对象都会有一个指向其对应虚函数表的指针。

使用场景

  • 当你想通过基类指针或引用来调用派生类中的成员函数时。
  • 实现多态性,即同一接口对应不同的实现。

示例

代码语言:javascript代码运行次数:0运行复制
class Base {
public:
    virtual void show() {
        cout << "Base show" << endl;
    }
    virtual ~Base() {} // 虚析构函数示例
};

class Derived : public Base {
public:
    void show() override { // C++11 引入的 override 关键字,用于明确表示函数是重写自基类
        cout << "Derived show" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    ptr->show(); // 输出 "Derived show",体现了多态
    delete ptr; // 调用 Derived 的析构函数,因为 ~Base() 是虚的
    return 0;
}

虚析构函数

虚析构函数是一个特殊的虚函数,它在对象被销毁时自动调用。当通过基类指针删除派生类对象时,如果基类的析构函数不是虚的,那么只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致资源泄漏或未定义行为,因为派生类可能分配了需要手动释放的资源(如动态分配的内存、文件句柄等)。

使用场景

  • 当通过基类指针删除派生类对象时,确保派生类的析构函数被调用。

示例(续上):

代码语言:javascript代码运行次数:0运行复制
class Base {
public:
    // ... 其他成员函数 ...
    virtual ~Base() { // 虚析构函数
        // 基类清理代码(如果有的话)
    }
};

class Derived : public Base {
public:
    // ... 其他成员函数 ...
    ~Derived() {
        // 派生类特有的清理代码
    }
};

// ... 在 main 函数中 ...
Base* ptr = new Derived();
// ...
delete ptr; // 安全地删除 Derived 对象,先调用 Derived 的析构函数,然后调用 Base 的析构函数

总结

  • 虚函数允许在派生类中重写基类的成员函数,并通过基类指针或引用来调用派生类的函数实现,从而实现多态。
  • 虚析构函数确保通过基类指针删除派生类对象时,派生类的析构函数会被调用,从而安全地释放资源。

纯虚函数和抽象类

在面向对象编程(OOP)中,纯虚函数和抽象类是两个重要的概念,它们主要用于实现多态性和定义接口。这两个概念在C++等语言中特别常见,但也存在于其他支持面向对象编程的语言中。

纯虚函数(Pure Virtual Function)

纯虚函数是一个在基类中被声明但没有在基类中定义的函数。其目的是要求派生类(子类)必须提供该函数的实现。纯虚函数通过在函数声明的末尾添加= 0来定义。

示例(C++):

代码语言:javascript代码运行次数:0运行复制
class Base {
public:
    // 纯虚函数
    virtual void myFunction() = 0;
};

// 派生类必须实现纯虚函数
class Derived : public Base {
public:
    void myFunction() override {
        // 实现细节
    }
};

抽象类(Abstract Class)

抽象类是一个包含至少一个纯虚函数的类。由于至少含有一个纯虚函数,抽象类不能被实例化(即不能直接创建该类的对象)。抽象类的主要用途是作为一个基类,为派生类提供一个公共的接口。

注意:一个类即使不包含纯虚函数,只要它的构造函数被声明为protectedprivate,它也被视为抽象类,因为这样的类不能被直接实例化。

示例(继续上面的C++示例):

  • Base类是一个抽象类,因为它包含了一个纯虚函数myFunction()
  • Derived类不是抽象类,因为它实现了从Base类继承来的纯虚函数myFunction()

使用场景

  • 接口定义:抽象类用于定义一组派生类必须实现的接口。这有助于确保派生类遵循特定的行为模式。
  • 多态性:通过基类指针或引用来操作派生类对象时,可以实现多态性。这允许在运行时根据对象的实际类型来调用相应的函数实现。
  • 模板方法模式:在模板方法模式中,抽象类定义了一个算法的骨架,将一些步骤延迟到子类中实现。纯虚函数用于定义这些必须由子类实现的步骤。

总结

纯虚函数和抽象类是面向对象编程中用于实现接口和多态性的重要工具。纯虚函数要求派生类必须提供实现,而抽象类则是因为包含至少一个纯虚函数而不能被直接实例化。这两个概念共同工作,为程序的设计和实现提供了强大的灵活性和可扩展性。

final关键字

final关键字在C++11中被引入,用于防止类被继承或防止虚函数在派生类中被覆盖。

  • final被用于类声明之后时,它表示该类不能被用作基类,即不能从该类继承出新的类。
代码语言:javascript代码运行次数:0运行复制
class NoInherit final {
public:
    void func() {
        // 函数体
    }
};

// 尝试从NoInherit继承将会导致编译错误
// class Error : public NoInherit {}; // 错误
  • final被用于虚函数声明之后时,它表示该函数在派生类中不能被覆盖(即不能被重写)。
代码语言:javascript代码运行次数:0运行复制
class Base {
public:
    virtual void func() final {
        // 函数体
    }
};

class Derived : public Base {
public:
    // 尝试覆盖func将会导致编译错误
    // void func() override { /* 错误 */ }
};

总结

  • 抽象类是通过包含至少一个纯虚函数来实现的,它不能被实例化,但可以作为基类供其他类继承。
  • final关键字用于防止类被继承或防止虚函数在派生类中被覆盖,以提供更严格的类继承控制。

多态的实现方式

在C++等面向对象编程语言中,多态主要通过以下几种方式实现:

  • 虚函数:在基类中声明虚函数,并在派生类中重写这些函数。通过基类类型的指针或引用来调用虚函数时,会根据实际的对象类型来调用相应的函数。
  • 抽象类:定义一个只包含纯虚函数的类作为抽象基类,派生类必须实现这些纯虚函数才能被实例化。这样可以通过抽象基类来定义一组接口规范,并确保所有派生类都遵循这些规范。
  • 模板:虽然模板本身并不直接支持多态(静态多态除外),但可以通过模板来编写与类型无关的代码,并在编译时根据具体的类型参数来生成相应的代码。这在一定程度上也体现了多态的思想。

多态的应用实例

以比萨制作为例,可以定义一个比萨类(Pizza)作为基类,然后定义培根比萨(BaconPizza)和海鲜比萨(SeafoodPizza)等子类来继承比萨类。每个子类都可以重写基类中的show方法,以展示各自独特的属性(如培根克数、配料等)。然后,可以定义一个比萨工厂类(PizzaFactory),根据用户的输入产生具体的比萨对象,并通过基类类型的引用或指针来调用show方法,展示不同比萨的信息。这就是多态在实际应用中的一个典型例子。

虚函数表指针

在C++中,虚函数表(Virtual Function Table,简称VTable)是实现多态性的关键机制之一。当一个类包含至少一个虚函数时,编译器会为这个类创建一个虚函数表。这个表包含了类中所有虚函数的地址。每个包含虚函数的类的对象都会包含一个指向其类虚函数表的指针,这个指针通常被称为虚函数表指针(VTable Pointer),在MSVC(Microsoft Visual C++)中,这个指针通常被命名为__vfptr(但在标准C++中并没有直接暴露这个名称,它是编译器内部实现的细节)。

虚函数表指针的工作原理

  1. 创建:当一个类被编译器处理并且该类包含虚函数时,编译器会为该类创建一个虚函数表。这个表包含了类中所有虚函数的地址。
  2. 存储:每个包含虚函数的类的对象都会包含一个指向其类虚函数表的指针。这个指针在对象内部是隐藏的,但它在运行时被用来解析虚函数的调用。
  3. 调用:当通过对象的指针或引用来调用虚函数时,编译器会首先查找这个指针或引用所指向对象的虚函数表指针,然后通过这个指针找到虚函数表中对应函数的地址,最后调用这个函数。

虚函数表指针的重要性

虚函数表指针使得C++能够支持运行时多态性。这意味着,即使两个对象有不同的类型,但只要它们共享一个基类,并且这个基类中有虚函数,那么通过基类指针或引用来调用虚函数时,就可以根据对象的实际类型来调用相应的函数实现。

注意事项

  • 虚函数表指针是编译器实现的一部分,标准C++并没有直接暴露这个指针。
  • 虚函数表指针的存在增加了对象的大小(通常是4或8字节,取决于指针的大小)。
  • 虚函数表指针和虚函数表是C++实现多态性的底层机制,但开发者通常不需要直接与之交互。
  • 构造函数和析构函数虽然可以是虚的,但它们并不直接通过虚函数表来调用。构造和析构过程中虚函数表指针的行为是特殊的,需要特别注意。

总之,虚函数表指针是C++多态性实现的底层机制之一,它使得通过基类指针或引用来调用虚函数时,能够根据对象的实际类型来调用相应的函数实现。

总结

多态是面向对象编程中的一个重要特性,它允许不同类的对象对同一消息作出响应。通过多态,可以提高代码的复用性、增强程序的扩展性,并实现模块之间的低耦合设计。在C++等面向对象语言中,多态主要通过虚函数、纯虚函数和抽象类等方式实现。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2024-10-28,如有侵权请联系 cloudcommunity@tencent 删除继承接口指针对象函数

本文标签: 十一多态