[C++]多态

发布于:2024-12-18 ⋅ 阅读:(14) ⋅ 点赞:(0)

1. 什么是多态性?

1.定义

多态性是指同一个函数或操作在不同对象上表现出不同的行为。

2.分类

C++ 中的多态性主要分为两种:

1.编译时多态性(静态多态性)

  • 编译时决定调用哪个函数。
  • 通过 函数重载运算符重载 实现。

2.运行时多态性(动态多态性)

  • 程序运行时根据对象的实际类型决定调用哪个函数。
  • 通过 虚函数 实现。

3.核心思想

多态的核心是 “一个接口,多种实现”

  • 基类定义一个公共接口。
  • 子类提供不同的实现。
  • 调用时,通过基类指针或引用操作不同的对象,表现出多种行为。

2. 多态的经典例子

例子 1:动物的叫声

一个基类 Animal,它有一个 makeSound() 方法:

  • 对于 Dog 类,makeSound() 表现为 “汪汪”。
  • 对于 Cat 类,makeSound() 表现为 “喵喵”。
实现多态的目标:

通过基类指针或引用,调用 makeSound(),实现动态决定调用哪种动物的声音。


3. 利用虚函数实现多态性

虚函数是实现运行时多态性的核心机制。核心点为

1.静态绑定与动态绑定:

1.普通函数是编译时绑定(静态绑定,又叫静态关联),即调用的函数在编译时确定。

在程序编译阶段,编译器就能确定要调用的函数具体是哪一个。例如,有下面这样简单的类和函数调用的代码:

#include<iostream>
using namespace std;
class Base {
public:
    void show() {
       cout << "Base::show()" << cout;
    }
};

int main() {
    Base b;
    b.show();  // 这里编译器一看对象是Base类型,在编译的时候就确定调用Base类里定义的show函数
    return 0;
}

在上述代码中,当在 main 函数里调用 b.show() 时,编译器在编译阶段就能明确知道这里调用的就是 Base 类中定义的 show 函数,这种在编译时就确定具体调用哪个函数的方式就叫做静态绑定,也就是编译时绑定。 

2.虚函数是运行时绑定(动态绑定,又叫动态关联),即调用的函数在程序运行时根据实际对象的类型决定。

动态绑定是指在程序运行的时候,才根据实际对象的类型去决定调用哪个类的函数。这就需要用到虚函数了,看下面的代码示例:

#include<iostream>
using namespace std;

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

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

int main() {
    Base* ptr = new Derived();  // 基类指针指向子类对象
    ptr->show();  // 这里在运行时,会根据指针所指向的实际对象(是Derived类的对象)来决定调用Derived类里重写的show函数,而不是Base类的show函数,这就是运行时绑定
    delete ptr;
    return 0;
}

 在这个例子里,虽然 ptr 是 Base 类型的指针,但是在运行时调用 show 函数时,会根据它实际指向的对象(这里是 Derived 类的对象)去决定调用 Derived 类中重写后的 show 函数,这种在运行时才能确定具体调用哪个函数的机制就是动态绑定,也就是运行时绑定。

2.通过基类指针或引用调用子类的实现:

1.使用基类指针或引用操作子类对象。

  • 基类指针:指针是一个变量,它存储的是另一个变量的内存地址。基类指针就是声明为指向基类类型的指针变量。例如:
    #include<iostream>
    using namespace std;
    class Animal {
        // 类的定义内容
    };
    
    class Dog : public Animal {
        // 类的定义内容
    };
    
    int main() {
        Animal* animalPtr;  // 这就是一个基类指针,它可以用来指向Animal类的对象,当然也可以指向Animal类的派生类(子类)的对象,像下面这样
        Dog dogObj;
        animalPtr = &dogObj;  // 让基类指针指向Dog类(子类)的对象,这里是合法的,因为Dog类是Animal类的派生类
        return 0;
    }
  •  引用操作子类对象:
    #include<iostream>
    using namespace std;
    class Shape {
    public:
        virtual void draw() {
            cout << "Drawing a generic shape" << endl;
        }
    };
    
    class Circle : public Shape {
    public:
        void draw() override {
            cout << "Drawing a circle" << endl;
        }
    };
    
    class Rectangle : public Shape {
    public:
        void draw() override {
            cout << "Drawing a rectangle" << endl;
        }
    };
    
    int main() {
        Shape* shapePtr1 = new Circle();  // 基类指针指向Circle子类对象
        Shape* shapePtr2 = new Rectangle();  // 基类指针指向Rectangle子类对象
    
        shapePtr1->draw();  // 运行时调用Circle类重写的draw函数,体现多态
        shapePtr2->draw();  // 运行时调用Rectangle类重写的draw函数,体现多态
    
        delete shapePtr1;
        delete shapePtr2;
        return 0;
    }

    在这个例子中,Shape 是基类,Circle 和 Rectangle 是它的子类。在 main 函数里定义了基类指针 shapePtr1 和 shapePtr2,并分别让它们指向不同的子类对象。当通过这些基类指针调用 draw 这个虚函数时,在运行时会根据指针实际指向的子类对象来决定调用子类中重写后的 draw 函数,这就是通过基类指针操作子类对象实现多态性。

2.如果函数是虚函数,则调用子类的版本;如果不是虚函数,则调用基类的版本。

3.代码示例:利用虚函数实现多态

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void makeSound() {  // 虚函数
        cout << "Animal makes a sound." << endl;
    }
};

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

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

int main() {
    Animal* animal;  // 基类指针

    Dog dog;
    Cat cat;

    animal = &dog;
    animal->makeSound();  // 调用 Dog 的 makeSound()

    animal = &cat;
    animal->makeSound();  // 调用 Cat 的 makeSound()

    return 0;
}

//输出:
//Dog barks: Woof!
//Cat meows: Meow!

解释:
  • 虽然 animalAnimal* 类型,但因为 makeSound() 是虚函数,运行时根据对象的实际类型(DogCat)调用对应的版本。

4. 纯虚函数与抽象类

关于虚函数与纯虚函数和抽象函数的详细知识点讲解大家可以看这里

纯虚函数和抽象类是实现动态多态性的高级工具,用于设计 接口类

1.纯虚函数

1.定义:

  • 是没有实现的虚函数,用 = 0 声明。
  • 基类只规定接口(函数名、参数、返回值类型),具体实现由子类负责。
  • 纯虚函数的基类是抽象类。

2.语法:

class Base {
public:
    virtual void functionName() = 0;  // 纯虚函数
};

2.抽象类

1.定义

  • 包含至少一个纯虚函数的类称为抽象类。
  • 不能实例化抽象类
  • 用于定义接口,让子类实现具体行为。

2.代码示例:纯虚函数与抽象类

#include <iostream>
using namespace std;

// 抽象类:定义接口
class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数,子类必须实现
};

class Circle : public Shape {
public:
    void draw() override {  // 实现接口
        cout << "Drawing a Circle" << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {  // 实现接口
        cout << "Drawing a Rectangle" << endl;
    }
};

int main() {
    Shape* shape;  // 基类指针

    Circle circle;
    Rectangle rectangle;

    shape = &circle;
    shape->draw();  // 调用 Circle 的 draw()

    shape = &rectangle;
    shape->draw();  // 调用 Rectangle 的 draw()

    return 0;
}
/*
输出:
Drawing a Circle
Drawing a Rectangle
*/
解释:
  1. Shape 是抽象类,定义了 draw() 接口。
  2. CircleRectangle 实现了 draw()
  3. 使用基类指针 shape 调用子类的实现。

5. 静态多态性 vs 动态多态性

特性 静态多态性 动态多态性
绑定时间 编译时绑定 运行时绑定
实现机制 通过函数重载、运算符重载实现 通过虚函数实现
调用方式 调用函数时直接确定 根据对象的实际类型决定调用的函数
性能开销 无运行时开销 有运行时开销(需要通过虚函数表查找函数指针)

6. 多态的核心机制:虚函数表

1.对虚函数表的浅层理解:

虚函数表是一个存储虚函数地址的表格。当一个类包含虚函数时,编译器会为这个类创建一个虚函数表。这个表就像是一个函数指针数组,数组中的每个元素都是一个虚函数的地址。例如,假设有一个简单的类层次结构:

class Base {
public:
    virtual void func1() {}
    virtual void func2() {}
};

对于'Base'类,编译器会创建一个虚函数表。这个表中存储了'func1'和'func2'这两个虚函数的地址。如果有一个派生类'Derived'继承自'Base'并且重写了这些虚函数,那么'Derived'类的虚函数表中的相应函数指针会指向'Derived'类中重写后的函数。 假设'Base'类的虚函数表在内存中的地址是'0x1000','func1'的地址在虚函数表中的偏移量是0,'func2'的地址偏移量是1(这只是一个简单的假设,实际的偏移量和内存布局由编译器决定)。在内存中,可能会像这样存储(以简单的十六进制表示): 0x1000: [地址 of Base::func1, 地址 of Base::func2]。

2.虚函数通过虚函数表实现:

1.每个包含虚函数的类有一个虚函数表,记录了类中虚函数的地址。

2.每个对象有一个指针(虚指针,vptr),指向所属类的虚函数表。

3.调用虚函数时,程序根据对象的虚指针查找虚函数表,从而找到正确的函数地址。


7. 多态的优势与应用场景

1.优势

1.代码复用: 基类定义接口,子类实现,减少重复代码。

2.扩展性强: 新增子类时,不需要修改基类代码,遵循开放/封闭原则。

3.运行时灵活性: 通过基类指针操作不同类型的对象,行为不同。

2.应用场景

  • 图形库设计: 抽象类 Shape 定义 draw() 接口,具体形状如 CircleRectangle 实现它。
  • 文件处理: 基类 File 定义 open()read(),子类实现具体操作(如文本文件、二进制文件)。
  • 游戏开发: 基类 Character 定义 attack(),具体角色(战士、法师)实现不同攻击方式。

8. 总结

1.多态性

  • 是面向对象编程的核心概念,实现“一个接口,多种实现”。
  • 分为 静态多态性(编译时)和 动态多态性(运行时)。

2.动态多态性

1.通过虚函数实现:

  • 基类定义虚函数,子类重写。
  • 使用基类指针或引用调用子类的实现。

2.通过纯虚函数与抽象类:

  • 纯虚函数强制子类实现,抽象类用于设计接口。