多态性是面向对象编程中的一个核心概念,尤其在C++中,它提供了一种强大的机制,使得相同的接口可以表现出不同的行为。多态性允许我们在不知道对象具体类型的情况下,通过基类指针或引用来调用派生类的重写方法,从而使代码更加灵活和通用。本文将详细介绍C++中的多态性概念,以及两种主要的多态类型:编译时多态(静态多态)和运行时多态(动态多态)。
一、多态性的概念
多态,字面意思就是多种形态。在面向对象编程中,多态指的是当不同的对象去完成同一个行为时,会产生不同的状态或结果。例如,在车站买票时,普通人需要支付全价,学生则可以享受半价,军人则可能享有优先权。这些不同的身份在执行“买票”这一行为时,表现出不同的形态,这就是多态的一种体现。
在C++中,多态性是通过虚函数和继承关系来实现的。当一个基类指针或引用指向一个派生类对象时,通过该指针或引用调用的虚函数会根据对象的实际类型来决定调用哪个版本的函数。这种机制使得我们可以在不修改现有代码的情况下,通过增加新的派生类来扩展程序的功能。
二、编译时多态(静态多态)
编译时多态,也称为静态多态或前期绑定(早绑定),是在编译期间确定函数的实现。它主要通过函数重载和模板函数来实现。
1.函数重载
函数重载是指在同一个作用域中定义多个同名函数,但它们的参数列表不同。编译器会根据函数的参数列表唯一地确定要调用的函数。函数重载的实现可以通过编译时的函数匹配来实现,实现起来比较简单。
示例代码:
#include <iostream>
void print(int i) {
std::cout << "This is an integer: " << i << std::endl;
}
void print(float f) {
std::cout << "This is a float: " << f << std::endl;
}
int main() {
print(42);
print(3.14f);
return 0;
}
在上述代码中,我们定义了两个同名的函数print,但它们的参数列表不同,一个接受整数,一个接受浮点数。在调用函数print时,编译器会自动根据参数的类型选择调用哪个函数。
2.模板函数
模板函数是指在定义函数时使用了类型参数,可以让函数适用于多种不同的类型。编译器会在编译时根据参数类型来生成具体的函数实现。
示例代码:
#include <iostream>
#include <cstdlib>
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
int main() {
int x = 42, y = 23;
float f = 3.14f, g = 2.71f;
std::cout << "Max of " << x << " and " << y << " is " << max(x, y) << std::endl;
std::cout << "Max of " << f << " and " << g << " is " << max(f, g) << std::endl;
return 0;
}
在上述代码中,我们定义了一个模板函数max,它可以针对整数、浮点数等多种类型进行运算。在调用函数max时,编译器会根据参数类型自动推断出要使用哪个具体的函数实现。
三、运行时多态(动态多态)
运行时多态,也称为动态多态或后期绑定(晚绑定),是在程序运行期间确定函数的实现。它主要通过虚函数和抽象类来实现。
1.虚函数
虚函数是指在基类中定义的函数,它允许派生类对其进行重写。通过将函数声明为虚函数,我们可以在运行时根据对象的实际类型来确定要调用的函数实现。
示例代码:
#include <iostream>
class Shape {
public:
virtual float calculateArea() {
return 0;
}
};
class Square : public Shape {
public:
Square(float l) : _length(l) {}
virtual float calculateArea() {
return _length * _length;
}
private:
float _length;
};
class Circle : public Shape {
public:
Circle(float r) : _radius(r) {}
virtual float calculateArea() {
return 3.14f * _radius * _radius;
}
private:
float _radius;
};
int main() {
Shape *s1 = new Square(5);
Shape *s2 = new Circle(3);
std::cout << "Area of square is " << s1->calculateArea() << std::endl;
std::cout << "Area of circle is " << s2->calculateArea() << std::endl;
delete s1;
delete s2;
return 0;
}
在上述代码中,我们定义了一个基类Shape和两个派生类Square和Circle,它们都实现了函数calculateArea。在调用函数calculateArea时,我们将基类指针指向派生类对象,可以看到运行时实际调用的是派生类的实现函数。
2.虚函数表(V-Table)
C++运行时使用虚函数表来实现多态。每个包含虚函数的类都有一个虚函数表,表中存储了指向类中所有虚函数的指针。对象中包含一个指向该类虚函数表的指针(V-Ptr)。当通过基类指针或引用调用虚函数时,编译器会根据对象的实际类型,通过虚函数表找到并调用相应的函数实现。
3.抽象类
抽象类是指包含至少一个纯虚函数的类,这个类不能被实例化,只能用作基类来派生出其他类。纯虚函数没有函数体,声明时使用= 0。它强制派生类提供具体的实现。
示例代码:
#include <iostream>
class Shape {
public:
virtual float calculateArea() = 0;
};
class Square : public Shape {
public:
Square(float l) : _length(l) {}
virtual float calculateArea() {
return _length * _length;
}
private:
float _length;
};
class Circle : public Shape {
public:
Circle(float r) : _radius(r) {}
virtual float calculateArea() {
return 3.14f * _radius * _radius;
}
private:
float _radius;
};
int main() {
// Shape *s = new Shape(); // error: cannot instantiate abstract class
Shape *s1 = new Square(5);
Shape *s2 = new Circle(3);
std::cout << "Area of square is " << s1->calculateArea() << std::endl;
std::cout << "Area of circle is " << s2->calculateArea() << std::endl;
delete s1;
delete s2;
return 0;
}
在上述代码中,我们将基类Shape中的函数calculateArea声明为纯虚函数,从而实现了抽象类。抽象类不能被实例化,只能用作基类来派生出其他类。在调用函数calculateArea时,我们将基类指针指向派生类对象,可以看到运行时实际调用的是派生类的实现函数。
四、虚函数的重写与final、override关键字
1.虚函数的重写(覆盖)
重写虚函数是派生类中重写出一个和基类的虚函数完全相同的虚函数(返回类型、函数名和参数列表都相同)。虽然派生类中的虚函数可以省略virtual关键字(因为继承后基类的虚函数在派生类中依旧保持虚函数属性),但这种写法不规范,不建议使用。
2.协变
协变是指基类虚函数和派生类虚函数的返回值类型不同(基类返回的是基类对象的指针/引用;派生类返回的是派生类对象的指针/引用)。
3.析构函数的重写
如果基类虚函数是析构函数,那么派生类的析构函数无论有没有virtual关键字都会对基类析构函数构成重写。这是因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
4.override和final关键字
- override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则编译报错。这有助于防止因函数名拼写错误而导致的未重写问题。
- final:修饰虚函数,表示该虚函数不能再被重写了。这有助于防止派生类对某个虚函数进行不必要的重写。
五、多态性的优势与应用
- 代码复用:通过基类指针或引用,可以操作不同类型的派生类对象,实现代码的复用。
- 扩展性:新增派生类时,不需要修改依赖于基类的代码,只需要确保新类正确重写了虚函数。
- 解耦:多态允许程序设计更加模块化,降低类之间的耦合度。