c++ 多态

1. 简介

多态按字面的意思就是多种形态。当类与类之间存在继承关系的时候,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型 来执行不同的函数。多态分为为静态多态动态多态 两种。

平常说的多态是 动态多态

2. 静态多态

静态多态是编译器在编译期间 完成的,编译器会根据实参类型 来选择调用合适的函数,如果有合适的函数可以调用就调,没有的话就会发出警告或者报错 。 该种方式的出现有两处地方: 函数重载泛型编程 | 函数模板

  • 特点
  1. 在编译时确定函数调用或代码生成,效率高。
  2. 可以根据不同的参数类型或数量进行函数重载或模板特化。
  3. 静态多态决策发生在编译阶段,对于每次运行都是固定的。
  • 使用场景
  1. 函数重载(Function Overloading): 在同一个作用域内定义了多个同名但参数列表不同的函数。根据函数调用时传递的参数类型或数量,编译器会决定调用哪个具体的函数。
cpp 复制代码
#include <iostream>

void print(int num) {
    std::cout << "Integer: " << num << std::endl;
}

void print(double num) {
    std::cout << "Double: " << num << std::endl;
}

int main() {
    int a = 20;
    double b = 3.14;

    print(a);  // 调用print(int)
    print(b);  // 调用print(double)

    return 0;
}
  1. 模板(Template): 使用模板可以编写泛型代码,在编译时生成对应特定类型的代码,从而实现静态多态。
cpp 复制代码
#include <iostream>

template<typename T>
void print(T value) {
    std::cout << "Value: " << value << std::endl;
}

int main() {
    int a = 20;
    double b = 3.14;

    print(a);  // 根据实参类型生成print<int>(int)
    print(b);  // 根据实参类型生成print<double>(double)

    return 0;
}

3. 动态多态

动态多态: 指只有在运行的时候才能决定到底调用哪个类的函数

动态多态的必须满足两个条件:

  1. 父类中必须包含虚函数,并且子类中一定要对父类中的虚函数进行重写
  2. 父类的 指针 | 引用 接收子类对象 ,使用这个指针 | 引用 来调用同名的虚函数。

代码:

cpp 复制代码
#include <iostream>

using namespace std;

class father{
public:
    void doSomething(){
        cout << "父亲在做事..." <<endl;
    }
};
class son : public father{
public:
    void doSomething(){
        cout << "儿子在干活..." <<endl;
    }
};
int main() {

    //father &f0 = father f0;   报错,'father' does not refer to a value
    //这是父类的对象
    father f;
    f.doSomething();
    //父类的引用能接受父类的对象
    father &f1 = f;
    f1.doSomething();
    //父类的指针接收父类的对象。
    father * f2  =new father();
    f2->doSomething();
    //子类的指针接收子类的对象
    son * s = new son();
    s->doSomething();
    //父类的指针接收子类对象
    father *f4 = new son();
    f4->doSomething();

    // son * s1 =new father(); 报错 Cannot initialize a variable of type 'son *' with an rvalue of type 'father *'

    return 0;
}

运行结果:

cpp 复制代码
父亲在做事...
父亲在做事...
父亲在做事...
儿子在干活...
父亲在做事...
  • 特点
  1. 虚函数:动态多态性依赖于虚函数。在基类中使用 virtual 关键字声明虚函数,派生类可以对其进行重写。当通过基类的指针或引用调用虚函数时,实际调用的是对象的派生类函数,而不是基类函数。
  2. 运行时确定:动态多态性在运行时确定,而不是在编译时。这意味着在程序运行时根据实际对象的类型来动态选择调用的函数,而不是根据指针或引用的类型来决定。
  3. 基类的通用接口:动态多态性使得可以通过基类的指针或引用来访问派生类的对象,并调用它们的成员函数。这为实现基类的通用接口提供了便利,可以处理一组派生类对象,统一地访问它们的接口,提高代码的灵活性和可维护性。
  • 使用场景
  1. 多态行为:当有多个派生类对象,但是希望以一种统一的方式处理它们时,动态多态性非常有用。通过使用基类的指针或引用,可以在运行时根据实际对象的类型来选择调用的函数,实现多态行为。
cpp 复制代码
#include <iostream>

// 基类 Shape
class Shape {
public:
    virtual double area() const = 0;
};

// 派生类 Rectangle
class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override {
        return width * height;
    }
};

// 派生类 Circle
class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    double area() const override {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Shape* shape1 = new Rectangle(4, 3);
    Shape* shape2 = new Circle(5.0);

    std::cout << "Rectangle area: " << shape1->area() << std::endl;
    std::cout << "Circle area: " << shape2->area() << std::endl;

    delete shape1;
    delete shape2;

    return 0;
}
  1. 统一接口:通过使用基类的指针或引用,可以定义一个通用的接口,处理一组派生类对象。这样可以在不了解具体派生类的情况下,统一地访问它们的接口,提供代码的灵活性和可维护性。
cpp 复制代码
#include <iostream>

// 基类 Drawable
class Drawable {
public:
    virtual void draw() const = 0;
};

// 派生类 Rectangle
class Rectangle : public Drawable {
public:
    void draw() const override {
        std::cout << "Drawing a rectangle." << std::endl;
    }
};

// 派生类 Circle
class Circle : public Drawable {
public:
    void draw() const override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

void drawShapes(const Drawable& shape) {
    shape.draw();
}

int main() {
    Rectangle rectangle;
    Circle circle;

    drawShapes(rectangle);
    drawShapes(circle);

    return 0;
}
  1. 扩展性:通过继承和虚函数,动态多态性提供了一种灵活的方式来扩展代码。当需要添加新的派生类时,只需继承基类并重写虚函数即可,而不需要修改已有的代码。
cpp 复制代码
#include <iostream>

// 基类 Animal
class Animal {
public:
    virtual void sound() const {
        std::cout << "Animal makes sound." << std::endl;
    }
};

// 派生类 Dog
class Dog : public Animal {
public:
    void sound() const override {
        std::cout << "Dog barks." << std::endl;
    }
};

// 派生类 Cat
class Cat : public Animal {
public:
    void sound() const override {
        std::cout << "Cat meows." << std::endl;
    }
};

// 扩增派生类 Bird
class Bird : public Animal {
public:
    void sound() const override {
        std::cout << "Bird sings." << std::endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();
    Animal* animal3 = new Bird();

    animal1->sound();
    animal2->sound();
    animal3->sound();

    delete animal1;
    delete animal2;
    delete animal3;

    return 0;
}
  • 注意点
  1. 必须使用虚函数:为了实现动态多态性,必须在基类中将需要在派生类中重写的函数声明为虚函数。
  2. 析构函数必须为虚函数:如果在基类中使用了虚函数,那么基类的析构函数也必须声明为虚函数。这是为了确保在使用基类指针删除派生类对象时,能够正确调用派生类的析构函数,避免内存泄漏。
  3. 注意对象切片问题:当将派生类对象赋值给基类对象时,会发生对象切片(Object Slicing)问题。这意味着只会复制基类部分的成员变量和函数,派生类特有的成员将被截断。为了避免对象切片问题,通常需要使用基类的指针或引用来操作派生类对象。
  4. 避免在析构函数中调用虚函数:在基类的析构函数中调用虚函数可能导致意外行为,因为在析构函数期间,派生类的部分可能还未初始化或已被销毁。因此,应该避免在构造函数和析构函数中调用虚函数,或者使用非虚函数或静态函数来完成相应的操作。
  5. 注意函数重载与函数重写:函数重载是在同一个类中定义了多个同名函数,它们的参数列表不同。而函数重写是在派生类中重写了基类的虚函数,函数名、参数列表和返回类型都必须相同。在使用基类指针或引用调用函数时,会根据对象的实际类型选择适当的函数,因此要确保在派生类中正确重写基类的虚函数。

4. 虚函数

C++中的虚函数的作用主要是实现了多态的机制 , 有了虚函数就可以在父类的指针或者引用指向子类的实例的前提下,然后通过父类的指针或者引用调用实际子类的成员函数。这时父类的指针或引用具备了多种形态。定义虚函数:在函数声明前,加上 virtual 关键字即可。 在父类的函数上添加 virtual 关键字,可使子类的同名函数也变成虚函数。

如果基类指针指向的是一个基类对象,则基类的虚函数被调用 ,如果基类指针指向的是一个派生类对象,则派生类的虚函数被调用。

  • 特点
  1. 多态性(Polymorphism):通过使用虚函数,可以实现运行时多态性。当基类指针或引用指向派生类对象时,调用虚函数会根据实际对象类型来确定要执行的具体函数。
  2. 动态绑定(Dynamic Binding):由于虚函数的动态绑定机制,在运行时才确定要调用哪个版本的虚函数。这使得程序能够根据对象类型动态地选择正确的成员函数。
  3. 虚函数表(Virtual function table):为了实现虚函数的多态性,编译器会在每个含有虚函数的类中创建一个虚函数表(vtable),该表存储了虚函数的地址。基类和派生类都有各自的虚函数表,虚函数的绑定是通过查找虚函数表来实现的。

4.1 工作原理

通常情况下,编译器处理虚函数的方法是: 给每一个对象添加一个隐藏指针成员,它指向一个数组,数组里面存放着对象中所有函数的地址。这个数组称之为虚函数表(virtual function table v-table)。表中存储着类对象的虚函数地址。

父类对象包含的指针,指向父类的虚函数表地址,子类对象包含的指针,指向子类的虚函数表地址。

如果子类重新定义了父类的函数,那么函数表中存放的是新的地址,如果子类没有重新定义,那么表中存放的是父类的函数地址。

若子类有自己虚函数,则只需要添加到表中即可。

4.2 构造函数可以是虚函数吗?

构造函数不能为虚函数 , 因为虚函数的调用,需要虚函数表(指针),而该指针存在于对象开辟的空间中,而对象的空间开辟依赖构造函数的执行,这就矛盾了。

cpp 复制代码
#include <iostream>

using namespace std;

class father{
public:
    // virtual father(){}   Constructor cannot be declared 'virtual'
    father(){
        cout << "父类的构造..." <<endl;
    }
};

class son : public father{
public:
    son(){
        cout << "子类的构造..." <<endl;
    }
};

int main() {

    son s;
    
    return 0;
}

4.3 析构函数可以是虚函数吗?

在继承体系下, 如果父类的指针可以指向子类对象,这就导致在使用 delete 释放内存时,却是通过父类指针来释放,这会导致父类的析构函数会被执行,而子类的析构函数并不会执行,此举有可能导致程序结果并不是我们想要的。究其原因,是因为静态联编的缘故,在编译时,就知道要执行谁的析构函数。
为了解决这个问题,需要把父类的析构函数变成虚拟析构函数,也就是加上 virtual的定义。一旦父类的析构函数是虚函数,那么子类的析构函数也将自动变成虚函数。

一句话概括: 继承关系下,所有人的构造都不能是虚函数,并且所有人的析构函数都必须是虚函数。

只要在父亲的析构函数加上 virtual ,那么所有的析构函数都变成 虚函数

cpp 复制代码
using namespace std;

class father{
public:
    father(){
        cout << "父类的构造..." <<endl;
    }

    virtual ~father(){
        cout << "父类的析构..." <<endl;
    }
};

class son : public father{
public:
    son(){
        cout << "子类的构造..." <<endl;
    }
    ~son(){
        cout << "子类的析构..." <<endl;
    }
};

int main() {

    father * f = new son ();
    delete f;

    return 0;
}

5. 纯虚函数

纯虚函数是一种特殊的虚函数,C++中包含纯虚函数的类,被称为是"抽象类"。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。C++中的纯虚函数更像是"只提供声明,没有实现",是对子类的约束。
纯虚函数就是没有函数体,同时在定义的时候,其函数名后面要加上"= 0"

代码:

cpp 复制代码
#include <iostream>

using namespace std;


class Animal{
public:
    // 动物类的吃的行为,看起来更像是对子类的一种抽象,或者是看起来像是一个功能的声明。
    virtual void eat() = 0 ;

    /*virtual void eat(){
        cout << "动物在吃..." <<endl;
    }*/
};

class Bear : public Animal{
public:
    void eat(){
        cout << "熊吃鱼..." <<endl;
    }
};

class Tiger : public Animal{
public:
    void eat(){
        cout << "老虎吃肉..." <<endl;
    }
};

class Pangolin : public Animal{
public:
    void eat(){
        cout << "穿山甲吃蚂蚁..." <<endl;
    }
};

class Suckler : public Animal{};


int main() {

    //Animal S = new Suckler;  报错, Allocating an object of abstract class type 'Suckler'

    Animal * B = new Bear;
    B->eat();

    Animal * T = new Tiger;
    T->eat();

    Animal * P = new Pangolin;
    P->eat();


    return 0;
}
  • 注意点
  1. 纯虚函数,一般会出现在父类里面,它看起来就像是一种功能的声明而已,没有具体的实现,因为每一个子类,具体功能都不太一样。
  2. 如果一个类含有纯虚函数,那么该类即可称之为: 抽象类。
  3. 抽象类禁止创建对象。因为如果能创建对象,万一调用了纯虚函数,这就有混乱了。
  4. 子类继承了抽象类之后,必须实现纯虚函数,如果不实现,那么子类也变成了抽象类。比如上面的 Suckler 类
  5. 抽象类里面,能写普通函数,虽然抽象类不能创建对象,不能调用方法,但是子类可以创建对象,可以让子类调用方法。
相关推荐
Thomas_YXQ2 分钟前
Unity3D Lua如何支持面向对象详解
开发语言·游戏·junit·性能优化·lua·unity3d
MYBOYER6 分钟前
Kotlin DSL Gradle 指南
android·开发语言·kotlin
武昌库里写JAVA11 分钟前
SpringCloud+SpringCloudAlibaba学习笔记
java·开发语言·算法·spring·log4j
夏天吃哈密瓜12 分钟前
用Scala来解决成绩排名的相关问题
开发语言·后端·scala
subject625Ruben13 分钟前
代码美学:MATLAB制作渐变色
开发语言·matlab
IRevers23 分钟前
使用Python和Pybind11调用C++程序(CMake编译)
开发语言·c++·人工智能·python·深度学习
Mr.1343 分钟前
什么是 C++ 中的多继承?它有哪些优缺点?什么是虚继承?为什么要使用虚继承?
c++
cdut_suye44 分钟前
C++11新特性探索:Lambda表达式与函数包装器的实用指南
开发语言·数据库·c++·人工智能·python·机器学习·华为
桃园码工1 小时前
第一章:Go 语言概述 1.什么是 Go 语言? --Go 语言轻松入门
开发语言·后端·golang
K.L.Zous1 小时前
Arduino键盘
c++