【C++】面向对象编程(OOP)的四大基本特性之三:多态

目录

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

1.1. 函数重载

1.2. 模板

函数模板

类模板

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

2.1 关键点

2.2. 示例

3. 多态的理解

3.1. 生动比喻

3.2. 代码示例

4. 实现步骤

5. 注意事项


C++中的多态(Polymorphism)是面向对象编程(OOP)的一个核心概念,它允许通过基类指针或引用来调用派生类(子类)的方法,并且这些调用会根据对象的实际类型(即运行时类型)来解析。多态性主要分为两种类型:编译时多态(静态多态)和运行时多态(动态多态)。

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

编译时多态主要通过函数重载(Function Overloading)和模板(Templates)来实现。这些多态性在编译时就已经确定,因此被称为静态多态。函数重载允许同一个作用域内存在多个同名函数,只要它们的参数列表不同即可。模板则允许程序员编写与类型无关的代码,编译器会在编译时根据具体类型生成相应的代码。

1.1. 函数重载

函数重载允许在同一个作用域内定义多个同名函数,但这些函数的参数列表(参数个数、类型或顺序)必须不同。编译器根据函数调用时提供的参数类型、数量和顺序来确定应该调用哪个函数版本。

#include <iostream>  
  
void print(int i) {  
    std::cout << "Printing int: " << i << std::endl;  
}  
  
void print(double f) {  
    std::cout << "Printing float: " << f << std::endl;  
}  
  
int main() {  
    print(5);    // 调用 print(int)  
    print(5.5);  // 调用 print(double)  
    return 0;  
}

在这个例子中,print函数被重载了两次,一次接受int类型参数,另一次接受double类型参数。编译器在编译时就能根据提供的参数类型确定应该调用哪个版本的print函数。

1.2. 模板

模板是C++中实现泛型编程的一种机制。它允许程序员编写与类型无关的代码,这些代码可以在编译时根据提供的类型参数生成特定类型的代码。模板可以应用于函数和类。

函数模板

函数模板允许你定义一个函数,该函数可以操作任意类型的数据。在函数模板的定义中,类型参数被用作占位符,实际的类型在模板实例化时确定。

#include <iostream>  
  
template<typename T>  
void print(T value) {  
    std::cout << value << std::endl;  
}  
  
int main() {  
    print(5);    // 实例化 print<int>(int)  
    print(5.5);  // 实例化 print<double>(double)  
    return 0;  
}
类模板

类模板与函数模板类似,但它用于定义可以操作任意类型数据的类。类模板中的类型参数在类实例化时确定。

#include <iostream>  
  
template<typename T>  
class Box {  
public:  
    T value;  
    Box(T val) : value(val) {}  
    void display() {  
        std::cout << value << std::endl;  
    }  
};  
  
int main() {  
    Box<int> intBox(10);  
    intBox.display(); // 输出 10  
  
    Box<double> doubleBox(5.5);  
    doubleBox.display(); // 输出 5.5  
  
    return 0;  
}

在这个例子中,Box是一个类模板,它接受一个类型参数TBox类有一个名为value的成员变量和一个构造函数,它们都可以操作类型T的数据。在main函数中,我们根据提供的类型(intdouble)实例化了Box类的两个对象,并调用了它们的display方法。

总之,编译时多态通过函数重载和模板在编译阶段就确定了函数的调用版本或类的类型,从而提高了代码的重用性和灵活性。

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

在C++的上下文中,当我们提到多态时,通常指的是运行时多态,它通过虚函数(Virtual Functions)和抽象基类(Abstract Base Classes,含有至少一个纯虚函数的类)来实现。当通过基类的指针或引用来调用虚函数时,程序会在运行时根据对象的实际类型来决定调用哪个版本的函数。这种机制允许使用基类指针或引用来操作派生类的对象,并且能够调用到派生类中定义的函数版本,从而实现多态性。

2.1 关键点

  1. 虚函数:在基类中,使用virtual关键字声明的函数称为虚函数。虚函数允许在派生类中被重写。

  2. 函数重写:在派生类中,可以提供一个与基类虚函数具有相同签名(函数名、参数列表和返回类型)的函数,以重写基类中的虚函数。C++11引入了override关键字作为可选标记,以明确表示某个成员函数是重写了基类中的虚函数。

  3. 动态绑定:通过基类指针或引用调用虚函数时,会在运行时确定实际调用的函数版本,这称为动态绑定(也称为晚期绑定或运行时绑定)。这与静态绑定(在编译时确定函数版本)形成对比。

  4. 虚析构函数:如果基类指针被用来删除派生类对象,则基类的析构函数应该被声明为虚函数,以确保在删除对象时能够调用到派生类的析构函数,从而正确释放资源。

  5. 抽象基类:包含至少一个纯虚函数(Pure Virtual Function)的类称为抽象基类。纯虚函数是在声明时以= 0结尾的虚函数,它不能被直接实例化。抽象基类主要用于定义接口,强制派生类实现特定的成员函数。

2.2. 示例

#include <iostream>  
  
class Base {  
public:  
    virtual void display() {  
        std::cout << "Base display" << std::endl;  
    }  
    virtual ~Base() {} // 虚析构函数  
};  
  
class Derived : public Base {  
public:  
    void display() override { // 使用override关键字明确表示这是重写  
        std::cout << "Derived display" << std::endl;  
    }  
};  
  
int main() {  
    Base* ptr = new Derived(); // 基类指针指向派生类对象  
    ptr->display(); // 运行时多态:调用Derived类的display函数  
    delete ptr; // 调用Derived类的析构函数,然后调用Base类的析构函数(如果有的话)  
    return 0;  
}

在这个示例中,ptr是一个指向Base类型的指针,但它实际上指向了一个Derived类型的对象。当调用ptr->display()时,由于display是一个虚函数,因此会调用ptr实际指向的对象(即Derived类型对象)的display函数版本,这体现了多态性。

3. 多态的理解

想象一下有一个动物园,里面有许多不同种类的动物,比如狗、猫、鸟等。这些动物都有一些共同的行为,比如“叫”(make a sound),但每种动物叫的方式都是不同的。在C++中,这个动物园可以看作是一个基类(比如Animal),而狗、猫、鸟等则是派生类(比如DogCatBird)。多态性允许我们以一种统一的方式与这些不同的动物交互,而不需要关心它们具体的种类。

3.1. 生动比喻

想象一下你手里拿着一个遥控器,上面有一个按钮标记为“听声音”。不管你是对着狗、猫还是鸟按下这个按钮,你都会听到不同的声音,而不需要更换遥控器或按下不同的按钮。遥控器(基类指针或引用)并不知道(也不需要知道)它当前指向的是哪种动物,但它知道如何调用“叫”这个行为(虚函数)。这就是多态性的魅力所在:它允许我们在不知道对象具体类型的情况下,通过基类接口调用对象的特定行为。

3.2. 代码示例

#include <iostream>  
  
// 基类  
class Animal {  
public:  
    // 虚函数,允许在派生类中被重写  
    virtual void makeSound() {  
        std::cout << "Some generic sound" << std::endl;  
    }  
  
    // 虚析构函数,确保通过基类指针删除派生类对象时调用正确的析构函数  
    virtual ~Animal() {}  
};  
  
// 派生类 Dog  
class Dog : public Animal {  
public:  
    void makeSound() override { // 使用override确保函数确实重写了基类中的虚函数  
        std::cout << "Woof!" << std::endl;  
    }  
};  
  
// 派生类 Cat  
class Cat : public Animal {  
public:  
    void makeSound() override {  
        std::cout << "Meow!" << std::endl;  
    }  
};  
  
// 派生类 Bird  
class Bird : public Animal {  
public:  
    void makeSound() override {  
        std::cout << "Tweet!" << std::endl;  
    }  
};  
  
// 使用多态  
int main() {  
    Animal* myAnimals[3]; // 基类指针数组,可以指向任何派生类对象  
    myAnimals[0] = new Dog();  
    myAnimals[1] = new Cat();  
    myAnimals[2] = new Bird();  
  
    // 通过基类指针调用虚函数,展示多态性  
    for (int i = 0; i < 3; ++i) {  
        myAnimals[i]->makeSound(); // 根据对象的实际类型调用不同的函数版本  
    }  
  
    // 清理资源  
    for (int i = 0; i < 3; ++i) {  
        delete myAnimals[i];  
    }  
  
    return 0;  
}

在这个例子中,Animal类是一个基类,它有一个虚函数makeSoundDogCatBird类都是从Animal类派生出来的,并且它们都重写了makeSound函数。在main函数中,我们创建了一个Animal类型的指针数组,但实际上它存储的是指向DogCatBird对象的指针。当我们通过myAnimals数组中的指针调用makeSound函数时,C++运行时系统会根据指针实际指向的对象的类型来调用相应的函数版本,这就是多态性的体现。

4. 实现步骤

  1. 定义一个基类:在基类中声明虚函数。如果基类不打算被实例化(即它仅仅是一个接口),则可以将虚函数声明为纯虚函数(在函数声明的末尾加上= 0)。

  2. 派生类继承基类:派生类继承基类并实现(重写)基类中的虚函数。

  3. 通过基类指针或引用来操作派生类对象:当通过基类指针或引用来调用虚函数时,会根据指针或引用所指向的对象的实际类型来调用相应的函数版本。

5. 注意事项

  • 虚函数表(Virtual Function Table, VTable):C++编译器通过虚函数表来实现动态多态。每个包含虚函数的类都有一个虚函数表,表中存储了该类所有虚函数的地址。
  • 虚析构函数:如果基类指针指向派生类对象,并且基类中定义了虚函数,那么基类的析构函数也应该声明为虚函数,以确保通过基类指针删除派生类对象时能够调用到派生类的析构函数,从而正确释放资源。
  • 构造函数不能是虚函数,因为对象在构造时其类型已经完全确定,且构造函数调用的时机早于虚函数表的形成。
  • 静态成员函数不能是虚函数,因为静态成员函数属于类本身,而非类的某个特定对象。
  • 15
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值