Skip to content

C++ 多态

术语 多态性(polymorphism)源自希腊语,意为“多种形式”。在 C++ 中,多态性通常指通过继承相关的不同类的对象,能够对相同的函数调用做出不同响应的能力。这通常涉及一个指向派生类对象的基类指针或引用。

具体来说,C++ 运行时多态性(runtime polymorphism)意味着当你通过基类指针或引用调用成员函数时,执行的函数是在运行时根据实际指向的对象的类型确定的,而不是根据指针/引用的静态类型确定。

静态绑定(Static Binding) vs. 动态绑定(Dynamic Binding)(早期绑定 vs. 晚期绑定)

Section titled “静态绑定(Static Binding) vs. 动态绑定(Dynamic Binding)(早期绑定 vs. 晚期绑定)”

考虑以下没有多态性的示例:

#include <iostream>
#include <memory> // For std::unique_ptr
// 用于 std::unique_ptr
// Base Class
// 基类
class Shape {
protected:
int width, height;
public:
// Constructor using initializer list
// 使用初始化列表的构造函数
Shape(int w = 0, int h = 0) : width{w}, height{h} {}
// Non-virtual function
// 非虚函数
int area() const { // Added const
// 添加 const
std::cout << "Base Shape::area() called." << std::endl;
return 0; // Base class might not have a meaningful area
// 基类可能没有有意义的面积
}
};
// Derived Class 1
// 派生类 1
class Rectangle : public Shape {
public:
Rectangle(int w = 0, int h = 0) : Shape{w, h} {}
int area() const { // Hides the base class function, doesn't override
// 隐藏基类函数,但不覆盖
std::cout << "Rectangle::area() called." << std::endl;
return (width * height);
}
};
// Derived Class 2
// 派生类 2
class Triangle : public Shape {
public:
Triangle(int w = 0, int h = 0) : Shape{w, h} {}
int area() const { // Hides the base class function, doesn't override
// 隐藏基类函数,但不覆盖
std::cout << "Triangle::area() called." << std::endl;
return (width * height / 2);
}
};
int main() {
Rectangle rect(10, 7);
Triangle tri(10, 5);
Shape* shapePtr; // Base class pointer
// 基类指针
// Point to Rectangle
// 指向 Rectangle
shapePtr = &rect;
std::cout << "Calling area() via base pointer (pointing to Rectangle): ";
// 通过基类指针调用 area() (指向 Rectangle):
shapePtr->area(); // Calls Shape::area()
// 调用 Shape::area()
// Point to Triangle
// 指向 Triangle
shapePtr = &tri;
std::cout << "Calling area() via base pointer (pointing to Triangle): ";
// 通过基类指针调用 area() (指向 Triangle):
shapePtr->area(); // Calls Shape::area()
// 调用 Shape::area()
return 0;
}

编译并执行后,会产生以下输出:

Calling area() via base pointer (pointing to Rectangle): Base Shape::area() called.
Calling area() via base pointer (pointing to Triangle): Base Shape::area() called.

为什么输出不正确?因为 area() 函数是非虚函数(non-virtual)。编译器根据指针的静态类型(Shape*)来解析函数调用,而不是根据它在运行时实际指向的对象的类型。这称为静态绑定(static binding)或早期绑定(early binding)。

为了启用多态性,我们使用 virtual 关键字声明基类函数。这告诉编译器使用动态绑定(dynamic binding)或晚期绑定(late binding),在运行时根据对象的实际类型来解析函数调用。

让我们修改 Shape 类:

// Base Class with virtual function
// 带有虚函数的基类
class Shape {
protected:
int width, height;
public:
Shape(int w = 0, int h = 0) : width{w}, height{h} {}
// Declare area() as virtual
// 将 area() 声明为虚函数
virtual int area() const {
std::cout << "Base Shape::area() called." << std::endl;
return 0;
}
// Virtual destructor (Important!)
// 虚析构函数(重要!)
virtual ~Shape() {
std::cout << "Base Shape destructor called." << std::endl;
// 调用基类析构函数
}
};

在派生类中使用 override 关键字也是一种好的做法,可以确保你确实覆盖了基类的虚函数,这提高了代码清晰度和安全性:

// Derived Class 1 (Rectangle)
// 派生类 1 (Rectangle)
class Rectangle : public Shape {
public:
Rectangle(int w = 0, int h = 0) : Shape{w, h} {}
// Use 'override' for clarity and safety
// 使用 'override' 提高清晰度和安全性
int area() const override {
std::cout << "Rectangle::area() called." << std::endl;
// 调用 Rectangle::area()
return (width * height);
}
~Rectangle() override {
std::cout << "Rectangle destructor called." << std::endl;
// 调用 Rectangle 析构函数
}
};
// Derived Class 2 (Triangle)
// 派生类 2 (Triangle)
class Triangle : public Shape {
public:
Triangle(int w = 0, int h = 0) : Shape{w, h} {}
// Use 'override'
// 使用 'override'
int area() const override {
std::cout << "Triangle::area() called." << std::endl;
// 调用 Triangle::area()
return (width * height / 2);
}
~Triangle() override {
std::cout << "Triangle destructor called." << std::endl;
// 调用 Triangle 析构函数
}
};

现在,运行相同的 main 函数(最好使用智能指针如 std::unique_ptr 进行更安全的内存管理)会产生多态行为:

// Modified main using smart pointers and demonstrating polymorphism
// 使用智能指针修改 main 函数并演示多态性
int main() {
std::unique_ptr<Shape> shapePtr;
// Point to Rectangle
// 指向 Rectangle
shapePtr = std::make_unique<Rectangle>(10, 7);
std::cout << "Calling area() via base unique_ptr (holding Rectangle): ";
// 通过基类 unique_ptr 调用 area() (持有 Rectangle):
int rectArea = shapePtr->area(); // Calls Rectangle::area()
// 调用 Rectangle::area()
std::cout << "Rectangle Area: " << rectArea << std::endl;
// Rectangle 面积:
// Point to Triangle
// 指向 Triangle
shapePtr = std::make_unique<Triangle>(10, 5);
std::cout << "Calling area() via base unique_ptr (holding Triangle): ";
// 通过基类 unique_ptr 调用 area() (持有 Triangle):
int triArea = shapePtr->area(); // Calls Triangle::area()
// 调用 Triangle::area()
std::cout << "Triangle Area: " << triArea << std::endl;
// Triangle 面积:
return 0; // unique_ptr automatically manages memory release and calls appropriate destructors
// unique_ptr 自动管理内存释放并调用适当的析构函数
}

预期输出(包括析构函数的调用):

Calling area() via base unique_ptr (holding Rectangle): Rectangle::area() called.
Rectangle Area: 70
Rectangle destructor called.
Base Shape destructor called.
Calling area() via base unique_ptr (holding Triangle): Triangle::area() called.
Triangle Area: 25
Triangle destructor called.
Base Shape destructor called.

关键点: 多态性通过指向基类的指针和引用来实现。使用 virtual 函数使得程序能够在运行时根据对象的实际类型决定调用哪个版本的函数。

如果一个类打算作为类层次结构中的基类,并且你可能会通过基类指针删除派生类对象,那么基类必须有一个 virtual 析构函数。否则,只会调用基类析构函数,如果派生类管理了资源,这可能导致资源泄露。注意更新后的示例中的 virtual ~Shape()。

抽象类(Abstract Classes)和纯虚函数(Pure Virtual Functions)

Section titled “抽象类(Abstract Classes)和纯虚函数(Pure Virtual Functions)”

有时,一个基类提供一个接口或概念,但无法为某个虚函数提供有意义的实现。例如,“泛型 Shape” 的“面积”是什么?在这种情况下,我们可以将该函数声明为纯虚函数(pure virtual function)。

纯虚函数通过在其声明末尾添加 = 0 来声明:

// Base Class as an Abstract Class
// 作为抽象类的基类
class Shape {
protected:
int width, height;
public:
Shape(int w = 0, int h = 0) : width{w}, height{h} {}
// Pure virtual function - makes Shape an abstract class
// 纯虚函数 - 使 Shape 成为抽象类
virtual int area() const = 0;
// Virtual destructor is still crucial for abstract base classes
// 对于抽象基类来说,虚析构函数仍然至关重要
virtual ~Shape() = default; // Use default if no custom cleanup needed
// 如果不需要自定义清理,则使用 default
};

包含一个或多个纯虚函数的类称为抽象类(abstract class)。你不能直接从抽象类创建对象(例如,Shape myShape; 会是一个错误)。抽象类充当派生类必须实现的接口。任何没有 override 并提供所有继承的纯虚函数实现的派生类也将是抽象的。

你可以在派生类中将一个虚函数标记为 final,以防止其在继承链中被进一步覆盖:

class Circle : public Shape {
// ...
int area() const override final {
// Implementation...
// 实现...
// No class derived from Circle can override area()
// 任何从 Circle 派生的类都不能再覆盖 area()
}
// ...
};

你也可以将整个类标记为 final,以防止它被继承:

class Uninheritable final { /* ... */ };
// class Derived : public Uninheritable {}; // Error!
// 错误!