菜鸟笔记
提升您的技术认知

C++ 什么是虚函数?什么是纯虚函数,以及区别?(通俗易懂)


前言

虚函数使得面向对象编程中的多态性得以实现,能够更灵活地处理不同派生类的对象,提高代码的可扩展性和可维护性。

虚函数

虚函数(Virtual Function)是在面向对象编程中用于实现动态多态性的一种机制。通过将基类中的成员函数声明为虚函数,可以在派生类中重写(Override)这些函数,从而根据对象的实际类型确定调用的函数版本。

声明方式:在基类中用 virtual 关键字声明的函数称为虚函数。

class Base {
public:
    virtual void display() {
        // Base class implementation
    }
};

多态调用:通过基类指针或引用调用虚函数时,实际调用的是指向对象的派生类版本(如果派生类重写了这个函数)。

动态绑定:在运行时根据对象的实际类型来确定调用的函数版本,而不是在编译时静态确定。

虚函数表(vtable):编译器通常通过添加一个指向虚函数表的指针来实现虚函数的机制。虚函数表存储了每个类的虚函数的地址。

代码示例:

#include <iostream>

// 基类 Base
class Base {
public:
    // 虚函数 display,提供默认实现
    virtual void display() {
        std::cout << "Display function of Base class" << std::endl;
    }
};

// 派生类 Derived
class Derived : public Base {
public:
    // 重写基类的虚函数 display
    void display() override {
        std::cout << "Display function of Derived class" << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived(); // 基类指针指向派生类对象
    basePtr->display(); // 调用派生类中的 display 函数
    delete basePtr;
    return 0;
}

Base 类中的 display() 被声明为虚函数,并提供了默认实现。

Derived 类重写了 display() 函数,改变了默认行为。

在主函数中,通过基类指针 basePtr 调用 display() 函数时,实际调用的是 Derived 类中的版本。

  纯虚函数

纯虚函数(Pure Virtual Function)是一个在基类中声明的虚函数,但没有在基类中提供实现。它通过在函数声明的结尾处使用 = 0 来标记:

在很多情况下,基类生成对象很不合理。为了解决这个问题,引入了纯虚函数的概念,将函
数定义为纯虚函数,派生类中必须重写实现纯虚函数。对于实现了纯虚函数的子类,该纯虚
函数在子类中就变成了虚函数。

声明方式

class Base {
public:
    virtual void display() = 0; // Pure virtual function
};

无法实例化类:包含纯虚函数的类被称为抽象类(Abstract Class),不能直接创建实例对象。

强制派生类实现:派生类必须实现基类中的纯虚函数,否则它们也会成为抽象类,无法实例化。

代码示例:

#include <iostream>

// 抽象基类 AbstractBase
class AbstractBase {
public:
    // 纯虚函数 display,没有默认实现
    virtual void display() = 0;
};

// 派生类 Derived 实现抽象基类
class Derived : public AbstractBase {
public:
    // 实现抽象基类中的纯虚函数 display
    void display() override {
        std::cout << "Display function of Derived class" << std::endl;
    }
};

int main() {
    // AbstractBase baseObj; // 不能实例化抽象类
    Derived derivedObj; // 可以实例化派生类
    AbstractBase* basePtr = &derivedObj; // 抽象基类指针指向派生类对象
    basePtr->display(); // 调用派生类中实现的 display 函数
    return 0;
}

AbstractBase 类中的 display() 被声明为纯虚函数,没有提供默认实现,使得 AbstractBase 成为抽象类,不能实例化。

Derived 类继承自 AbstractBase,必须实现 AbstractBase 中的纯虚函数 display

函数中,派生类 Derived 被实例化,而抽象基类 AbstractBase 的指针 basePtr 可以指向 Derived 类对象,并调用其实现的 display() 函数。

无论虚函数还是纯虚函数,定义中都不能有 static 关键字。因为 static 关键字修饰的内容在编译前就要确定,而虚函数、纯虚函数是在运行时动态绑定的。

两者区别

虚函数 允许在派生类中重写函数,但可以有默认实现。它是可选的,可以在基类中提供实现。

纯虚函数 没有默认实现,派生类必须提供实现。它使得基类成为抽象类,不能实例化。

实践案例

假设我们有一个基类 Shape,它定义了所有形状的基本属性和行为。我们希望能够计算各种形状的面积,但具体的面积计算方法因形状而异,因此我们可以使用虚函数和纯虚函数来达到这个目的。

首先,定义 Shape 类作为抽象基类,其中包含一个纯虚函数 area() 用于计算形状的面积:

#include <iostream>

// Abstract base class Shape
class Shape {
public:
    // 纯虚函数用于计算面积
    virtual double area() const = 0;

    // 虚析构函数(对多态性很重要)
    virtual ~Shape() {}
};

接着,我们可以创建不同类型的形状类(如矩形、圆形)作为 Shape 的派生类,并实现它们的具体面积计算方法。

创建一个矩形类 Rectangle 和一个圆形类 Circle

class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    // 重写矩形的面积函数
    double area() const override {
        return width * height;
    }
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    // 重写圆的面积函数
    double area() const override {
        return 3.14 * radius * radius;
    }
};

我们可以使用这些类来计算具体形状的面积,而无需关心具体是哪种形状

int main() {
    Rectangle rect(5, 3);
    Circle circle(2.5);

    // 使用 Shape 指针访问派生类 基类的指针指向了子类的对象
    Shape *shape1 = &rect;
    Shape *shape2 = &circle;

    // 使用虚函数计算并打印面积
    std::cout << "Area of Rectangle: " << shape1->area() << std::endl;
    std::cout << "Area of Circle: " << shape2->area() << std::endl;

    return 0;
}


// 运行结果
Area of Rectangle: 15
Area of Circle: 19.625

通过使用虚函数 area() 和纯虚函数 virtual double area() const = 0;,我们实现了多态性,使得能够根据实际的对象类型来调用适当的面积计算方法。同时,基类 Shape 的设计强制所有派生类实现 area() 方法,确保了面积计算的统一性和规范性。

总结

在实际应用中,虚函数和纯虚函数结合使用,通常用来定义接口和基类的通用行为,同时强制派生类实现特定的行为,从而实现一种规范化的设计模式。