目录
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
是一个类模板,它接受一个类型参数T
。Box
类有一个名为value
的成员变量和一个构造函数,它们都可以操作类型T
的数据。在main
函数中,我们根据提供的类型(int
和double
)实例化了Box
类的两个对象,并调用了它们的display
方法。
总之,编译时多态通过函数重载和模板在编译阶段就确定了函数的调用版本或类的类型,从而提高了代码的重用性和灵活性。
2. 运行时多态(动态多态)
在C++的上下文中,当我们提到多态时,通常指的是运行时多态,它通过虚函数(Virtual Functions)和抽象基类(Abstract Base Classes,含有至少一个纯虚函数的类)来实现。当通过基类的指针或引用来调用虚函数时,程序会在运行时根据对象的实际类型来决定调用哪个版本的函数。这种机制允许使用基类指针或引用来操作派生类的对象,并且能够调用到派生类中定义的函数版本,从而实现多态性。
2.1 关键点
-
虚函数:在基类中,使用
virtual
关键字声明的函数称为虚函数。虚函数允许在派生类中被重写。 -
函数重写:在派生类中,可以提供一个与基类虚函数具有相同签名(函数名、参数列表和返回类型)的函数,以重写基类中的虚函数。C++11引入了
override
关键字作为可选标记,以明确表示某个成员函数是重写了基类中的虚函数。 -
动态绑定:通过基类指针或引用调用虚函数时,会在运行时确定实际调用的函数版本,这称为动态绑定(也称为晚期绑定或运行时绑定)。这与静态绑定(在编译时确定函数版本)形成对比。
-
虚析构函数:如果基类指针被用来删除派生类对象,则基类的析构函数应该被声明为虚函数,以确保在删除对象时能够调用到派生类的析构函数,从而正确释放资源。
-
抽象基类:包含至少一个纯虚函数(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
),而狗、猫、鸟等则是派生类(比如Dog
、Cat
、Bird
)。多态性允许我们以一种统一的方式与这些不同的动物交互,而不需要关心它们具体的种类。
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
类是一个基类,它有一个虚函数makeSound
。Dog
、Cat
和Bird
类都是从Animal
类派生出来的,并且它们都重写了makeSound
函数。在main
函数中,我们创建了一个Animal
类型的指针数组,但实际上它存储的是指向Dog
、Cat
和Bird
对象的指针。当我们通过myAnimals
数组中的指针调用makeSound
函数时,C++运行时系统会根据指针实际指向的对象的类型来调用相应的函数版本,这就是多态性的体现。
4. 实现步骤
-
定义一个基类:在基类中声明虚函数。如果基类不打算被实例化(即它仅仅是一个接口),则可以将虚函数声明为纯虚函数(在函数声明的末尾加上
= 0
)。 -
派生类继承基类:派生类继承基类并实现(重写)基类中的虚函数。
-
通过基类指针或引用来操作派生类对象:当通过基类指针或引用来调用虚函数时,会根据指针或引用所指向的对象的实际类型来调用相应的函数版本。
5. 注意事项
- 虚函数表(Virtual Function Table, VTable):C++编译器通过虚函数表来实现动态多态。每个包含虚函数的类都有一个虚函数表,表中存储了该类所有虚函数的地址。
- 虚析构函数:如果基类指针指向派生类对象,并且基类中定义了虚函数,那么基类的析构函数也应该声明为虚函数,以确保通过基类指针删除派生类对象时能够调用到派生类的析构函数,从而正确释放资源。
- 构造函数不能是虚函数,因为对象在构造时其类型已经完全确定,且构造函数调用的时机早于虚函数表的形成。
- 静态成员函数不能是虚函数,因为静态成员函数属于类本身,而非类的某个特定对象。