C++ 多态

目录

​编辑

0.前言

1.多态的定义及构成

1.1什么是多态?

1.2构成多态的条件

2.虚函数

2.1什么是虚函数?

2.2虚函数的重写

2.3虚函数重写的特例

2.3.1协变

2.3.2析构函数的重写

2.4 override和final关键字

2.5重载、重写(覆盖)和重定义(隐藏)的对比

2.5.1重载(Overloading)

2.5.2重写(覆盖)(Overriding)

2.5.3重定义(隐藏)(Hiding)

3.抽象类

3.1概念

3.2接口继承和实现继承

3.2.1接口继承

3.2.2实现继承

4.多态原理详析

4.1虚函数表

4.2多态的实现原理

4.3静态绑定和动态绑定

5.结语


(图像由AI生成) 

0.前言

多态是C++语言的三大特性之一,另两个特性是封装和继承。多态性使得对象可以根据运行时的实际类型来表现出不同的行为,从而实现灵活和可扩展的设计。在软件开发过程中,多态能够提高代码的复用性和可维护性,减少重复代码,并提供更加抽象和通用的接口。本文将详细探讨C++中的多态,包括其定义、构成、虚函数、抽象类以及实现原理,帮助读者全面理解这一重要概念。

1.多态的定义及构成

1.1什么是多态?

多态(Polymorphism)是指同一接口在不同场景下可以表现出不同的行为。在面向对象编程中,多态允许程序在不同的上下文中调用相同的接口,从而处理不同的数据类型或对象。具体来说,多态可以分为两类:编译时多态(静态多态)和运行时多态(动态多态)。在C++中,编译时多态通过函数重载和模板实现,而运行时多态则通过虚函数和继承实现。本文主要讨论运行时多态。

1.2构成多态的条件

在C++中,要实现多态,需要满足以下条件:

  1. 继承:必须有一个基类和至少一个派生类。
  2. 虚函数:基类中必须有至少一个函数被声明为虚函数(使用virtual关键字)。
  3. 基类指针或引用:通过基类指针或引用来调用虚函数。

示例代码

#include <iostream>
using namespace std;

// 基类
class Animal {
public:
    // 声明虚函数
    virtual void makeSound() const {
        cout << "Animal makes a sound" << endl;
    }
};

// 派生类:Dog
class Dog : public Animal {
public:
    // 重写虚函数
    void makeSound() const override {
        cout << "Dog barks" << endl;
    }
};

// 派生类:Cat
class Cat : public Animal {
public:
    // 重写虚函数
    void makeSound() const override {
        cout << "Cat meows" << endl;
    }
};

int main() {
    // 创建对象
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    // 通过基类指针调用虚函数
    animal1->makeSound();  // 输出:Dog barks
    animal2->makeSound();  // 输出:Cat meows

    // 释放内存
    delete animal1;
    delete animal2;

    return 0;
}

在上面的代码中,Animal类是基类,DogCat类是派生类。基类中的makeSound函数被声明为虚函数,派生类对该函数进行了重写。在main函数中,通过基类指针调用虚函数,根据实际对象的类型,调用了不同的函数版本,实现了多态。这就是运行时多态的典型实现方式。

2.虚函数

2.1什么是虚函数?

虚函数是使用 virtual 关键字声明的函数,用于实现运行时多态。在基类中声明虚函数时,派生类可以重写该函数。当通过基类指针或引用调用虚函数时,会根据实际对象的类型调用对应的函数版本,而不是基类的版本。虚函数允许派生类提供自己特有的实现,使得代码更加灵活和可扩展。

2.2虚函数的重写

虚函数的重写是指在派生类中重新定义基类中的虚函数。重写虚函数时,派生类的函数签名必须与基类中的虚函数相同。通过这种方式,派生类可以提供其特有的行为。

示例代码

#include <iostream>
using namespace std;

class Base {
public:
    virtual void display() const {
        cout << "Display from Base class" << endl;
    }
};

class Derived : public Base {
public:
    void display() const override {
        cout << "Display from Derived class" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->display();  // 输出:Display from Derived class

    delete basePtr;
    return 0;
}

2.3虚函数重写的特例

2.3.1协变

协变是指在派生类中重写虚函数时,允许返回类型是基类返回类型的派生类。这种特性使得派生类可以返回更具体的对象,而不违反函数重写的规则。

示例代码

#include <iostream>
using namespace std;

class Base {
public:
    virtual Base* clone() const {
        return new Base(*this);
    }
    virtual void display() const {
        cout << "Base" << endl;
    }
};

class Derived : public Base {
public:
    Derived* clone() const override {  // 协变返回类型
        return new Derived(*this);
    }
    void display() const override {
        cout << "Derived" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    Base* newBasePtr = basePtr->clone();  // 返回Derived类的对象
    newBasePtr->display();  // 输出:Derived

    delete basePtr;
    delete newBasePtr;
    return 0;
}

在这个示例中,Derived类重写了clone函数,并返回类型是Derived*,而不是基类的Base*,这就是协变的应用。

2.3.2析构函数的重写

在使用继承和多态时,基类的析构函数应该声明为虚函数,以确保派生类对象被正确销毁。这是因为如果析构函数不是虚函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,导致资源泄漏。

示例代码

#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() {  // 基类析构函数声明为虚函数
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() override {
        cout << "Derived destructor" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    delete basePtr;  // 正确调用Derived和Base的析构函数

    return 0;
}

在这个示例中,基类Base的析构函数被声明为虚函数,因此通过基类指针删除派生类对象时,会正确调用派生类的析构函数,避免资源泄漏。

2.4 override和final关键字

override关键字用于显式声明派生类中的虚函数是重写基类中的虚函数。它有助于编译器进行检查,以确保派生类中的函数确实是在重写基类中的虚函数,而不是定义一个新的函数。这不仅提高了代码的可读性,还减少了因拼写错误或参数不匹配导致的隐藏错误。

#include <iostream>
using namespace std;

class Base {
public:
    virtual void display() const {
        cout << "Display from Base class" << endl;
    }
};

class Derived : public Base {
public:
    void display() const override {  // 使用override关键字
        cout << "Display from Derived class" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->display();  // 输出:Display from Derived class

    delete basePtr;
    return 0;
}

在这个示例中,Derived类中的display函数使用了override关键字,明确指出这是对基类中display函数的重写。

final关键字用于防止虚函数在派生类中再次被重写。它可以用于虚函数的声明中,表示该函数不能在进一步派生的类中被重写。另外,final关键字还可以用于类声明,表示该类不能被继承。

#include <iostream>
using namespace std;

class Base {
public:
    virtual void display() const {
        cout << "Display from Base class" << endl;
    }
};

class Derived final : public Base {  // 使用final关键字禁止进一步继承
public:
    void display() const override {
        cout << "Display from Derived class" << endl;
    }
};

/*
class FurtherDerived : public Derived {  // 错误!Derived类被final修饰,不能被继承
};
*/

class AnotherBase {
public:
    virtual void show() const final {  // 使用final关键字禁止重写
        cout << "Show from AnotherBase class" << endl;
    }
};

class AnotherDerived : public AnotherBase {
/*
    void show() const override {  // 错误!show函数被final修饰,不能被重写
        cout << "Show from AnotherDerived class" << endl;
    }
*/
};

int main() {
    Base* basePtr = new Derived();
    basePtr->display();  // 输出:Display from Derived class

    delete basePtr;
    return 0;
}

在这个示例中,Derived类被声明为final,表示不能再派生新的类。同时,AnotherBase类中的show函数被声明为final,表示不能在派生类中被重写。

2.5重载、重写(覆盖)和重定义(隐藏)的对比

2.5.1重载(Overloading)

重载是指在同一个作用域中,函数名相同但参数列表不同的多个函数。重载函数可以有不同的参数类型、数量或顺序,但不能仅靠返回类型区分。

#include <iostream>
using namespace std;

class Example {
public:
    void func(int x) {
        cout << "Function with int parameter: " << x << endl;
    }

    void func(double x) {
        cout << "Function with double parameter: " << x << endl;
    }

    void func(int x, double y) {
        cout << "Function with int and double parameters: " << x << ", " << y << endl;
    }
};

int main() {
    Example ex;
    ex.func(5);         // 输出:Function with int parameter: 5
    ex.func(5.5);       // 输出:Function with double parameter: 5.5
    ex.func(5, 5.5);    // 输出:Function with int and double parameters: 5, 5.5

    return 0;
}

在这个示例中,func函数被重载了三次,分别接受不同的参数列表。

2.5.2重写(覆盖)(Overriding)

重写是指在派生类中重新定义基类中的虚函数。重写函数的签名必须与基类中的虚函数一致。通过重写,派生类可以提供特定的实现。

#include <iostream>
using namespace std;

class Base {
public:
    virtual void display() const {
        cout << "Display from Base class" << endl;
    }
};

class Derived : public Base {
public:
    void display() const override {  // 重写基类的虚函数
        cout << "Display from Derived class" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->display();  // 输出:Display from Derived class

    delete basePtr;
    return 0;
}

在这个示例中,Derived类重写了Base类中的虚函数display

2.5.3重定义(隐藏)(Hiding)

重定义(隐藏)是指在派生类中定义了与基类同名但参数列表不同的函数。这种情况下,基类的同名函数在派生类中会被隐藏,但它们并不是重写,因此不会发生多态。

 

#include <iostream>
using namespace std;

class Base {
public:
    void show() const {
        cout << "Show from Base class" << endl;
    }
};

class Derived : public Base {
public:
    void show(int x) const {  // 重定义基类的show函数
        cout << "Show from Derived class with parameter: " << x << endl;
    }
};

int main() {
    Derived derived;
    derived.show(5);  // 输出:Show from Derived class with parameter: 5
    // derived.show();  // 错误!没有与之匹配的show函数

    Base* basePtr = &derived;
    basePtr->show();  // 输出:Show from Base class

    return 0;
}

在这个示例中,Derived类中的show函数隐藏了Base类中的同名函数。通过派生类对象只能调用重定义后的函数,通过基类指针调用时则调用基类的函数。

3.抽象类

3.1概念

抽象类是不能实例化的类,通常作为其他类的基类使用。它包含至少一个纯虚函数。纯虚函数是没有具体实现的函数,只提供接口规范。纯虚函数使用=0语法声明,表示派生类必须重写该函数。抽象类用于定义一个统一的接口,让派生类去实现具体的行为,从而实现多态。

#include <iostream>
using namespace std;

class AbstractClass {
public:
    virtual void pureVirtualFunction() const = 0;  // 纯虚函数
    void concreteFunction() const {
        cout << "Concrete function in AbstractClass" << endl;
    }
};

class ConcreteClass : public AbstractClass {
public:
    void pureVirtualFunction() const override {
        cout << "Implementation of pureVirtualFunction in ConcreteClass" << endl;
    }
};

int main() {
    // AbstractClass obj;  // 错误!不能实例化抽象类
    ConcreteClass obj;
    obj.pureVirtualFunction();  // 输出:Implementation of pureVirtualFunction in ConcreteClass
    obj.concreteFunction();  // 输出:Concrete function in AbstractClass

    return 0;
}

在这个示例中,AbstractClass是一个抽象类,包含一个纯虚函数pureVirtualFunctionConcreteClassAbstractClass的派生类,提供了pureVirtualFunction的实现。

3.2接口继承和实现继承

3.2.1接口继承

接口继承是指派生类继承抽象类的函数声明,而不继承其具体实现。派生类必须提供所有纯虚函数的具体实现。接口继承使得派生类可以有不同的实现,但遵循相同的接口规范。

#include <iostream>
using namespace std;

class Interface {
public:
    virtual void doSomething() const = 0;  // 纯虚函数,接口声明
};

class ImplementationA : public Interface {
public:
    void doSomething() const override {
        cout << "Implementation A doing something" << endl;
    }
};

class ImplementationB : public Interface {
public:
    void doSomething() const override {
        cout << "Implementation B doing something" << endl;
    }
};

int main() {
    Interface* a = new ImplementationA();
    Interface* b = new ImplementationB();

    a->doSomething();  // 输出:Implementation A doing something
    b->doSomething();  // 输出:Implementation B doing something

    delete a;
    delete b;

    return 0;
}

在这个示例中,Interface类是一个抽象类,定义了一个纯虚函数doSomethingImplementationAImplementationB类分别提供了该函数的具体实现,实现了接口继承。

3.2.2实现继承

实现继承是指派生类不仅继承基类的接口,还继承其具体实现。基类中的非纯虚函数可以在派生类中直接使用,也可以在派生类中被重写。实现继承使得派生类可以复用基类的代码,减少重复实现。

#include <iostream>
using namespace std;

class BaseClass {
public:
    virtual void virtualFunction() const {
        cout << "BaseClass implementation of virtualFunction" << endl;
    }
    void anotherFunction() const {
        cout << "BaseClass implementation of anotherFunction" << endl;
    }
};

class DerivedClass : public BaseClass {
public:
    void virtualFunction() const override {
        cout << "DerivedClass override of virtualFunction" << endl;
    }
};

int main() {
    DerivedClass obj;
    obj.virtualFunction();  // 输出:DerivedClass override of virtualFunction
    obj.anotherFunction();  // 输出:BaseClass implementation of anotherFunction

    return 0;
}

在这个示例中,BaseClass包含一个虚函数virtualFunction和一个普通成员函数anotherFunctionDerivedClass重写了virtualFunction,但直接继承并使用了anotherFunction的实现。这就是实现继承的应用。

4.多态原理详析

4.1虚函数表

虚函数表(Virtual Table,简称vtable)是实现C++多态的核心机制。当一个类包含虚函数时,编译器会为该类生成一个虚函数表。虚函数表是一个指针数组,每个元素指向该类的一个虚函数。每个对象在创建时都会包含一个指向虚函数表的指针(vptr),通过这个指针,程序在运行时能够找到并调用对象实际类型的虚函数。

#include <iostream>
using namespace std;

class Base {
public:
    virtual void func1() { cout << "Base func1" << endl; }
    virtual void func2() { cout << "Base func2" << endl; }
};

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

int main() {
    Base* basePtr = new Derived();
    basePtr->func1();  // 输出:Derived func1
    basePtr->func2();  // 输出:Derived func2

    delete basePtr;
    return 0;
}

在这个示例中,Base类和Derived类都有两个虚函数func1func2Base类的虚函数表会指向其虚函数实现,而Derived类的虚函数表会指向其重写的虚函数实现。通过基类指针调用虚函数时,会通过vptr找到实际对象的虚函数表,从而调用正确的函数版本。

4.2多态的实现原理

多态的实现原理依赖于虚函数表和虚函数表指针。以下是多态实现的几个关键步骤:

  1. 创建对象:当创建一个包含虚函数的类的对象时,编译器会为该对象分配内存,并初始化其vptr,使其指向该类的虚函数表。
  2. 调用虚函数:通过基类指针或引用调用虚函数时,程序会通过vptr找到实际对象的虚函数表,然后根据函数的偏移量找到正确的函数地址并进行调用。
  3. 运行时决策:由于vptr在运行时指向实际对象的虚函数表,程序可以在运行时根据对象的实际类型调用相应的函数,实现运行时多态。
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() { cout << "Animal speaks" << endl; }
};

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

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

void makeAnimalSpeak(Animal& animal) {
    animal.speak();
}

int main() {
    Dog dog;
    Cat cat;
    makeAnimalSpeak(dog);  // 输出:Dog barks
    makeAnimalSpeak(cat);  // 输出:Cat meows

    return 0;
}

 在这个示例中,makeAnimalSpeak函数接受一个Animal类的引用参数,通过该引用调用虚函数speak。实际调用的是DogCat类重写的speak函数,从而实现多态。

4.3静态绑定和动态绑定

  • 静态绑定(Static Binding):静态绑定在编译时进行决策,函数调用在编译时被解析。普通成员函数和非虚函数使用静态绑定。静态绑定的优点是速度快,因为在编译时已经确定了调用地址。

  • 动态绑定(Dynamic Binding):动态绑定在运行时进行决策,函数调用在运行时通过虚函数表解析。虚函数使用动态绑定。动态绑定的优点是灵活,可以在运行时根据对象的实际类型调用相应的函数版本。

静态绑定示例代码

#include <iostream>
using namespace std;

class Base {
public:
    void staticFunc() { cout << "Base staticFunc" << endl; }
};

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

int main() {
    Base base;
    Derived derived;
    Base* basePtr = &derived;
    
    base.staticFunc();   // 输出:Base staticFunc
    derived.staticFunc();  // 输出:Derived staticFunc
    basePtr->staticFunc(); // 输出:Base staticFunc

    return 0;
}

在这个示例中,staticFunc是普通成员函数,不是虚函数,因此使用静态绑定。即使通过基类指针调用staticFunc,也调用的是基类版本。

动态绑定示例代码

#include <iostream>
using namespace std;

class Base {
public:
    virtual void dynamicFunc() { cout << "Base dynamicFunc" << endl; }
};

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

int main() {
    Base base;
    Derived derived;
    Base* basePtr = &derived;

    base.dynamicFunc();    // 输出:Base dynamicFunc
    derived.dynamicFunc(); // 输出:Derived dynamicFunc
    basePtr->dynamicFunc(); // 输出:Derived dynamicFunc

    return 0;
}

在这个示例中,dynamicFunc是虚函数,因此使用动态绑定。通过基类指针调用dynamicFunc时,会根据实际对象的类型调用Derived类的版本。

5.结语

通过深入探讨C++中的多态特性及其实现原理,我们可以理解虚函数、虚函数表以及静态绑定和动态绑定的机制。多态作为C++的三大特性之一,不仅提升了代码的灵活性和可扩展性,还提高了程序设计的抽象能力。掌握多态的概念和应用,对于编写高质量的面向对象程序至关重要。希望本文能够帮助读者更好地理解和运用C++中的多态特性。

相关推荐

  1. <span style='color:red;'>C</span>++<span style='color:red;'>多</span><span style='color:red;'>态</span>

    C++

    2024-07-11 02:18:03      43 阅读
  2. 八股文 c++

    2024-07-11 02:18:03       39 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-11 02:18:03       66 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-11 02:18:03       70 阅读
  3. 在Django里面运行非项目文件

    2024-07-11 02:18:03       57 阅读
  4. Python语言-面向对象

    2024-07-11 02:18:03       68 阅读

热门阅读

  1. 代码随想录Day76(图论Part11)

    2024-07-11 02:18:03       24 阅读
  2. 优雅下线的艺术:Eureka服务管理深度解析

    2024-07-11 02:18:03       23 阅读
  3. 【LeetCode】12. 小张刷题计划

    2024-07-11 02:18:03       23 阅读
  4. samout 最新版本state 逐层控制加速收敛

    2024-07-11 02:18:03       25 阅读
  5. 【个人笔记】跨域问题

    2024-07-11 02:18:03       21 阅读
  6. webpack 打包配置

    2024-07-11 02:18:03       20 阅读
  7. 人类历史时间轴

    2024-07-11 02:18:03       19 阅读
  8. 使用Python自动化收集和处理视频资源的教程

    2024-07-11 02:18:03       21 阅读
  9. 参数式确定的函数的导数公式及其推导过程

    2024-07-11 02:18:03       25 阅读