文章目录
前言
多态性是面向对象编程的重要特性之一,而C++通过虚函数、继承等机制实现了这一强大的功能。多态性使得代码更加灵活和可扩展,允许不同类型的对象以统一的方式进行操作。在本篇文章中,我们将深入探讨C++中多态的实现原理、使用场景及其优劣势,并通过具体代码示例展示如何利用多态来提升代码的可维护性和复用性。
🌸一、多态的定义与概念
**多态(Polymorphism)**是面向对象编程中的一个重要概念,字面意思是“多种形态”。在编程中,多态指的是使用相同的接口或方法名来操作不同类型的对象,从而实现不同的行为。它允许一个接口在不同的上下文中表现出不同的行为,增加了程序的灵活性和可扩展性。
🌻1.1 多态的核心思想:
多态性使得一个基类可以定义统一的接口,而不同的子类则提供具体的实现。在程序运行时,可以根据对象的实际类型选择调用适当的函数实现。这样做可以通过相同的代码处理不同类型的对象,而不必显式地指定它们的类型。
🌻1.2 多态的两种主要形式:
- 编译时多态(静态多态):通过函数重载和运算符重载来实现,编译器在编译时决定调用哪个函数。这种多态是在编译阶段确定的,因此被称为静态多态。
- 例如:函数重载、运算符重载。
- 运行时多态(动态多态):通过虚函数和继承来实现,程序在运行时根据对象的实际类型决定调用哪个函数。这种多态是在运行时确定的,因此被称为动态多态。
- 例如:虚函数、接口实现。
🌸二、多态的使用条件
🌻2.1 基类指针或引用
在C++的多态性中,基类指针或引用是实现多态调用的关键。通过基类指针或引用指向派生类对象,可以在运行时调用派生类的重写方法,而不依赖于对象的静态类型。这种方式称为运行时多态或动态多态。
2.1.1 为什么需要基类指针或引用
在C++中,如果直接使用派生类对象,即使它重写了基类的虚函数,编译器仍然会使用静态绑定,即在编译时确定调用的函数版本。而使用基类指针或引用时,C++会使用动态绑定(通过虚函数表)来决定在运行时调用派生类的版本。这是多态的核心机制。
【示例代码】
以下是一个使用基类指针或引用实现多态的简单示例:
#include <iostream>
class Animal {
public:
virtual void sound() const { // 基类中的虚函数
std::cout << "Some generic animal sound" << std::endl;
}
virtual ~Animal() = default; // 虚析构函数
};
class Dog : public Animal {
public:
void sound() const override { // 派生类中重写 sound 方法
std::cout << "Woof" << std::endl;
}
};
class Cat : public Animal {
public:
void sound() const override { // 另一个派生类中重写 sound 方法
std::cout << "Meow" << std::endl;
}
};
void makeSound(const Animal& animal) { // 基类引用,支持多态
animal.sound(); // 动态绑定,根据实际对象类型调用派生类的方法
}
int main() {
Dog dog;
Cat cat;
// 使用基类引用,触发多态
makeSound(dog); // 输出:Woof
makeSound(cat); // 输出:Meow
// 使用基类指针,也可以实现多态
Animal *animalPtr = new Dog();
animalPtr->sound(); // 输出:Woof
delete animalPtr;
return 0;
}
【代码分析】
- 基类中的虚函数:
Animal
类中的sound
方法是虚函数,允许在派生类中重写。 - 基类指针或引用:
makeSound
函数接受一个Animal
的引用,而不是具体的Dog
或Cat
对象,使其能够调用不同的sound
实现。 - 动态绑定:在
main
函数中,通过基类引用和指针来调用派生类的sound
方法,输出的是实际派生类的结果。
🌻2.2 虚函数(virtual function)
在C++中,虚函数(virtual function) 是一种特殊的成员函数,通过它可以实现运行时多态。虚函数允许基类的指针或引用在运行时根据对象的实际类型调用派生类的重写方法,而不仅仅局限于基类的实现。这种机制在面向对象设计中非常重要,尤其在抽象接口、工厂模式等设计模式中广泛应用。
2.2.1 虚函数的定义和基本特性
- 虚函数是在基类中用关键字
virtual
声明的成员函数。 - 虚函数可以在派生类中被重写(override),并在运行时决定调用派生类的重写方法。
- 虚函数必须通过基类指针或引用来调用,才能触发多态行为。
2.2.2 如何定义虚函数
虚函数在基类中声明时加上 virtual
关键字即可。推荐使用override
关键字在派生类中重写虚函数,便于编译器检查是否正确地进行了重写。
[示例代码]
以下是一个虚函数的简单示例:
#include <iostream>
class Animal {
public:
virtual void sound() const { // 基类中的虚函数
std::cout << "Some generic animal sound" << std::endl;
}
};
class Dog : public Animal {
public:
void sound() const override { // 派生类中重写 sound 方法
std::cout << "Woof" << std::endl;
}
};
class Cat : public Animal {
public:
void sound() const override { // 另一个派生类中重写 sound 方法
std::cout << "Meow" << std::endl;
}
};
void makeSound(const Animal &animal) { // 基类引用,支持多态
animal.sound(); // 动态绑定,根据实际对象类型调用派生类的方法
}
int main() {
Dog dog;
Cat cat;
makeSound(dog); // 输出:Woof
makeSound(cat); // 输出:Meow
return 0;
}
【代码解析】
- 基类声明虚函数:
Animal
类中的sound
方法声明为虚函数,因此派生类可以重写该方法。 - 派生类重写虚函数:
Dog
和Cat
类分别重写了sound
方法,提供了各自的实现。 - 多态调用:
makeSound
函数接受Animal
类型的引用作为参数,在运行时会根据传入对象的实际类型调用相应的sound
实现,输出Woof
或Meow
。
【注意事项】
- 构造函数不能是虚函数:构造函数不支持
virtual
关键字,因为对象在构造时还未完成初始化。 - 静态成员函数不能是虚函数:静态成员函数不依赖于对象,无法实现多态。
- 基类指针或引用:虚函数的多态性只能通过基类的指针或引用来调用,如果直接使用派生类对象,则编译时会使用静态绑定。
总结
- 虚函数实现了C++的多态机制,允许基类指针或引用在运行时动态选择合适的派生类实现。
- 虚函数表支持动态绑定,通过表中指针定位到实际调用的函数。
虚函数使得代码在结构上更加灵活,提升了程序设计的可扩展性。
🌻2.3 重写虚函数(Override virtual function)
2.3.1 虚函数重写的三大条件
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person
{
public:
virtual void BuyTicket() const//虚函数
{
cout << "买全价票" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket() const//虚函数
{
cout << "买半价票" << endl;
}
};
void Func(const Person& people)
{
people.BuyTicket();
}
int main()
{
Func(Person()); //普通人
Func(Student()); //学生
return 0;
}
在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
2.3.2 虚函数重写的两个例外
在C++中,虚函数重写存在两个例外情况,即使满足了通常的虚函数重写条件,也不会被认为是对基类虚函数的重写。这两个例外是:
- 参数默认值不参与重写
在C++中,虚函数的重写不会受到参数默认值的影响,即使在基类的虚函数中定义了默认参数值,派生类重写时也可以选择不同的默认值。但是,当调用虚函数时,默认参数值总是根据指针或引用的静态类型确定,而不是动态类型。这意味着默认参数值在多态调用中不会变化。
示例:
#include <iostream>
class Base {
public:
virtual void printMessage(int times = 1) const { // 基类虚函数,默认值为1
for (int i = 0; i < times; ++i)
std::cout << "Base message" << std::endl;
}
};
class Derived : public Base {
public:
void printMessage(int times = 3) const override { // 重写时设置默认值为3
for (int i = 0; i < times; ++i)
std::cout << "Derived message" << std::endl;
}
};
int main() {
Base *ptr = new Derived();
ptr->printMessage(); // 输出1次,因为默认值取自Base类
delete ptr;
return 0;
}
解释:虽然Derived
类为printMessage
方法设置了默认值3
,但在多态调用时,默认值取决于基类Base
的定义(即1
),因为编译器在静态类型为Base
时就已确定默认值。
- 返回类型的协变限制
虽然C++支持协变返回类型(即派生类的重写函数可以返回一个更具体的类型),但协变限制仅限于指针或引用类型。如果基类的虚函数返回非指针或非引用类型,派生类不能重写该虚函数并更改返回类型。
示例:
#include <iostream>
class Base {
public:
virtual int getValue() const { // 基类虚函数返回int类型
return 42;
}
};
class Derived : public Base {
public:
// 错误:无法重写并更改返回类型
// double getValue() const override {
// return 3.14;
// }
};
解释:Base
类的getValue
函数返回int
类型。即使Derived
类想返回double
,这种重写是不允许的,因为返回类型不是指针或引用,违反了协变的限制。
🌻2.4 虚析构函数(Virtual Destructor)的重写
在C++中,虚析构函数(Virtual Destructor)是一种特殊的析构函数,通过在基类中将析构函数声明为虚函数,可以确保在通过基类指针删除派生类对象时,派生类的析构函数被正确调用。这在涉及多态和动态内存管理时尤为重要,可以有效避免内存泄漏和资源未正确释放的问题。
2.4.1 为什么需要虚析构函数?
当基类指针指向派生类对象时,如果删除对象时基类的析构函数不是虚函数,那么调用的仅仅是基类的析构函数,而不会调用派生类的析构函数。这样,派生类中分配的资源就无法释放,导致内存泄漏或其他资源管理问题。
示例
以下是一个不使用虚析构函数的例子,演示潜在的内存泄漏问题:
#include <iostream>
class Base {
public:
~Base() { // 非虚析构函数
std::cout << "Base destructor called" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() { // 派生类析构函数
std::cout << "Derived destructor called" << std::endl;
}
};
int main() {
Base* obj = new Derived();
delete obj; // 仅调用 Base 的析构函数,不调用 Derived 的析构函数
return 0;
}
输出
Base destructor called
解释:在删除obj
时,由于基类的析构函数不是虚函数,因此只调用了Base
的析构函数,没有调用Derived
的析构函数。派生类中可能分配的资源未被释放,导致潜在的内存泄漏。
2.4.2 使用虚析构函数
通过将基类的析构函数声明为虚函数,可以确保正确调用派生类的析构函数,避免内存泄漏问题:
#include <iostream>
class Base {
public:
virtual ~Base() { // 虚析构函数
std::cout << "Base destructor called" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() override { // 重写析构函数
std::cout << "Derived destructor called" << std::endl;
}
};
int main() {
Base* obj = new Derived();
delete obj; // 正确调用 Derived 和 Base 的析构函数
return 0;
}
输出:
Derived destructor called
Base destructor called
解释:在delete obj
时,虚析构函数确保先调用Derived
的析构函数,然后调用Base
的析构函数,资源得到正确释放。
2.4.3 虚析构函数的注意事项
- 虚析构函数的必要性:任何含有虚函数的基类都应定义虚析构函数,以确保派生类对象通过基类指针删除时能够正确析构。
- 性能影响:虚析构函数会引入一定的性能开销(如虚函数表查找)。但对于具有多态需求的类,这是一个合理的取舍。
- 纯虚析构函数:基类析构函数也可以定义为纯虚函数,用于将类设计为抽象基类,但必须提供函数体,因为析构函数始终需要可执行代码。
class AbstractBase {
public:
virtual ~AbstractBase() = 0; // 纯虚析构函数
};
AbstractBase::~AbstractBase() {} // 提供析构函数体
总结
- 虚析构函数确保通过基类指针删除派生类对象时正确调用派生类的析构函数,避免内存泄漏。
- 基类析构函数声明为虚函数是实现多态的良好实践,尤其当基类有其他虚函数时。
- 纯虚析构函数可用来定义抽象基类,但仍需提供函数体。
🌻2.5 C++11的override和final
在C++中,override
和final
是C++11引入的两个关键字,主要用于类的继承和虚函数的管理。它们在面向对象编程中用于提高代码的安全性和可读性,确保虚函数的正确性和防止意外的重写。
2.5.1 override
关键字
override
关键字用于显式声明一个函数是从基类中**重写(override)**的虚函数。它能够帮助编译器检查函数是否确实重写了基类中的虚函数。如果函数签名不匹配(比如返回类型不同或参数不同),编译器会报错。
使用override
的主要好处是:
- 增加代码的可读性,表明该函数是重写基类中的函数。
- 提供编译期检查,避免因为函数签名不匹配导致的隐藏错误。
示例:
class Base {
public:
virtual void display() const {
std::cout << "Base display" << std::endl;
}
};
class Derived : public Base {
public:
void display() const override { // 正确重写了基类的 display 函数
std::cout << "Derived display" << std::endl;
}
};
如果你误写了函数签名,比如忘记了 const
修饰符:
class Derived : public Base {
public:
void display() override { // 错误,没有 const 修饰符
std::cout << "Derived display" << std::endl;
}
};
编译器会报错,因为你没有正确重写基类的函数。
2.5.2 final
关键字
final
关键字用于两种情况:
- 防止类被继承:当你不希望某个类再被继承时,可以将这个类标记为
final
。 - 防止虚函数被重写:当你不希望派生类重写某个虚函数时,可以将该虚函数标记为
final
。
示例1:防止类被继承
class FinalClass final {
// 该类不能再被继承
};
// 下面的代码会导致编译错误
class DerivedClass : public FinalClass {
// 错误:FinalClass 被标记为 final,不能被继承
};
示例2:防止虚函数被重写
class Base {
public:
virtual void display() const {
std::cout << "Base display" << std::endl;
}
};
class Derived : public Base {
public:
void display() const final { // 这个函数不能再被派生类重写
std::cout << "Derived display" << std::endl;
}
};
// 下面的代码会导致编译错误
class MoreDerived : public Derived {
public:
void display() const override { // 错误:Derived::display 被标记为 final,不能被重写
std::cout << "MoreDerived display" << std::endl;
}
};
总结:
override
:用于确保你正在重写基类中的虚函数,提供编译期检查。final
:用于防止类被继承或者虚函数被重写。
这两个关键字提高了代码的安全性,避免继承或虚函数重写中的常见错误。
🌻2.6 重载、覆盖(重写)、隐藏(重定义)的对比
🌸三、抽象类
在C++中,抽象类是一种不能直接实例化的类,通常作为其他类的基类,目的是为子类提供接口定义。抽象类至少包含一个纯虚函数(pure virtual function),这是抽象类的核心特征。
🌻3.1 抽象类的定义
抽象类的定义中包含纯虚函数,纯虚函数的声明形式为:
virtual 返回类型 函数名(参数列表) = 0;
这个 = 0
表示该函数是纯虚函数,必须在派生类(子类)中实现。
以下是一个抽象类的简单例子:
#include <iostream>
using namespace std;
// 定义抽象类 Shape
class Shape {
public:
// 纯虚函数
virtual void draw() = 0;
virtual double area() = 0;
};
// 定义派生类 Circle
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// 实现抽象类中的纯虚函数
void draw() override {
cout << "Drawing a Circle" << endl;
}
double area() override {
return 3.14159 * radius * radius;
}
};
// 定义派生类 Rectangle
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
// 实现抽象类中的纯虚函数
void draw() override {
cout << "Drawing a Rectangle" << endl;
}
double area() override {
return width * height;
}
};
int main() {
Shape* shape1 = new Circle(5.0); // 创建Circle对象
Shape* shape2 = new Rectangle(4.0, 6.0); // 创建Rectangle对象
shape1->draw(); // 调用Circle的draw方法
cout << "Area: " << shape1->area() << endl; // 调用Circle的area方法
shape2->draw(); // 调用Rectangle的draw方法
cout << "Area: " << shape2->area() << endl; // 调用Rectangle的area方法
delete shape1;
delete shape2;
return 0;
}
- Shape 是一个抽象类,它包含两个纯虚函数
draw()
和area()
。 - Circle 和 Rectangle 是从 Shape 派生的类,它们实现了抽象类中的纯虚函数。
- 在
main()
函数中,定义了两个指向抽象类的指针shape1
和shape2
,分别指向Circle
和Rectangle
对象,并调用了它们的具体实现。
🌻3.2 抽象类的特点:
- 不能直接实例化抽象类对象。例如
Shape
不能直接创建对象。 - 抽象类中的纯虚函数必须在派生类中实现,否则派生类也会变成抽象类。
- 抽象类可以有数据成员和普通成员函数,但如果有纯虚函数,则它仍然是抽象类。
结语
通过对C++多态性的深入了解,我们可以更好地编写具有高扩展性和灵活性的代码。多态不仅让代码变得更具适应性,还能够减少代码重复,提高维护效率。在未来的开发中,合理运用多态将为我们的项目带来显著的提升。希望本文的讲解能够帮助读者在实践中更好地掌握这一重要概念。
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!