MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++纯虚函数的定义与含纯虚函数类的特点

2022-09-261.5k 阅读

C++纯虚函数的定义

纯虚函数概念引入

在C++面向对象编程中,我们常常会遇到这样的情况:在基类中定义一个函数,这个函数的具体实现对于基类来说是没有意义的,但是它的所有派生类都需要有自己特定的实现。例如,我们定义一个Shape类作为所有形状类的基类,Shape类中有一个计算面积的函数calculateArea。对于Shape类本身而言,由于它是一个抽象的概念,并没有具体的形状,所以计算面积这个操作在Shape类中无法给出具体的实现。然而,当我们从Shape类派生出Circle(圆形)类、Rectangle(矩形)类等具体形状类时,每个派生类都需要有自己独特的计算面积的方式。

为了满足这种需求,C++引入了纯虚函数的概念。纯虚函数就是在基类中声明的虚函数,它在基类中没有具体的实现,要求所有派生类必须对其进行重写。

纯虚函数的声明语法

纯虚函数的声明语法为在虚函数声明的末尾加上= 0。其一般形式如下:

class ClassName {
public:
    virtual return_type function_name(parameter_list) = 0;
};

其中,ClassName是类名,return_type是函数的返回类型,function_name是函数名,parameter_list是函数的参数列表。例如,对于上述的Shape类,其纯虚函数calculateArea的声明可以如下:

class Shape {
public:
    virtual double calculateArea() = 0;
};

这里Shape类中的calculateArea函数就是一个纯虚函数,它返回一个double类型的值,没有参数,并且由于= 0的存在表明它在Shape类中没有具体实现。

纯虚函数与普通虚函数的区别

普通虚函数在基类中有具体的实现,派生类可以选择重写也可以不重写。如果派生类不重写,那么调用派生类对象的该虚函数时,会调用基类中的实现。例如:

class Animal {
public:
    virtual void makeSound() {
        std::cout << "Some generic animal sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Woof!" << std::endl;
    }
};

class Cat : public Animal {
    // 未重写makeSound,会调用Animal类中的实现
};

在上述代码中,Animal类的makeSound是普通虚函数。Dog类重写了该函数,而Cat类没有重写,当调用Cat类对象的makeSound函数时,会调用Animal类中的实现。

而纯虚函数在基类中没有具体实现,派生类必须重写该函数,否则派生类也会成为抽象类(关于抽象类后面会详细介绍)。这是两者最主要的区别。纯虚函数更强调一种接口规范,它迫使派生类根据自身特点去实现特定的功能。

含纯虚函数类的特点

抽象类的定义

当一个类中至少包含一个纯虚函数时,这个类就被称为抽象类。例如前面提到的Shape类,因为含有纯虚函数calculateArea,所以Shape类就是一个抽象类。抽象类不能被实例化,即不能创建抽象类的对象。以下代码尝试创建Shape类的对象,会导致编译错误:

class Shape {
public:
    virtual double calculateArea() = 0;
};

int main() {
    Shape s; // 编译错误:不能实例化抽象类Shape
    return 0;
}

编译器会提示错误信息,指出不能实例化抽象类。这是因为抽象类只是作为一种抽象概念的载体,为派生类提供一个通用的接口框架,它本身不应该有具体的实例。

抽象类的作用

  1. 作为接口规范:抽象类定义了一组接口,要求派生类去实现这些接口。这样可以确保所有派生类都具有某些特定的行为。例如Shape类定义了calculateArea接口,所有从Shape派生的具体形状类,如CircleRectangle等都必须实现这个接口,从而保证了对于任何形状对象都可以调用calculateArea函数来计算其面积。
  2. 实现多态性:通过抽象类和虚函数,C++可以实现运行时多态。我们可以定义一个指向抽象类的指针或引用,然后让它指向不同的派生类对象,在运行时根据实际指向的对象类型来调用相应派生类的函数实现。例如:
class Shape {
public:
    virtual double calculateArea() = 0;
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double calculateArea() override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    double length;
    double width;
public:
    Rectangle(double l, double w) : length(l), width(w) {}
    double calculateArea() override {
        return length * width;
    }
};

void printArea(Shape& shape) {
    std::cout << "The area is: " << shape.calculateArea() << std::endl;
}

int main() {
    Circle c(5.0);
    Rectangle r(4.0, 6.0);

    printArea(c);
    printArea(r);

    return 0;
}

在上述代码中,printArea函数接受一个Shape类的引用作为参数,通过这个引用可以调用不同派生类的calculateArea函数,实现了运行时多态。

抽象类的继承

当一个类继承自抽象类时,如果它没有重写基类中的所有纯虚函数,那么这个派生类也会成为抽象类。例如:

class Shape {
public:
    virtual double calculateArea() = 0;
};

class AbstractRectangle : public Shape {
protected:
    double length;
    double width;
public:
    AbstractRectangle(double l, double w) : length(l), width(w) {}
    // 未重写calculateArea,AbstractRectangle也是抽象类
};

class Rectangle : public AbstractRectangle {
public:
    Rectangle(double l, double w) : AbstractRectangle(l, w) {}
    double calculateArea() override {
        return length * width;
    }
};

在上述代码中,AbstractRectangle类继承自Shape类,但没有重写calculateArea函数,所以AbstractRectangle也是抽象类。而Rectangle类继承自AbstractRectangle类并重写了calculateArea函数,因此Rectangle类不是抽象类,可以被实例化。

抽象类的成员函数

  1. 纯虚函数作为成员函数:抽象类中的纯虚函数是一种特殊的成员函数,它只提供了函数的声明,没有实现。其主要目的是为派生类提供一个必须实现的接口。
  2. 普通成员函数:抽象类也可以包含普通成员函数,这些函数有具体的实现,并且可以被派生类继承和调用。例如:
class Shape {
public:
    virtual double calculateArea() = 0;
    void printInfo() {
        std::cout << "This is a shape." << std::endl;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double calculateArea() override {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Circle c(5.0);
    c.printInfo();
    std::cout << "The area of the circle is: " << c.calculateArea() << std::endl;
    return 0;
}

在上述代码中,Shape类的printInfo是普通成员函数,Circle类继承了这个函数并可以调用它。

抽象类的构造函数和析构函数

  1. 构造函数:抽象类可以有构造函数,其构造函数主要用于初始化类的数据成员。当派生类对象被创建时,会先调用基类(抽象类)的构造函数,然后再调用派生类自己的构造函数。例如:
class Shape {
protected:
    std::string name;
public:
    Shape(const std::string& n) : name(n) {}
    virtual double calculateArea() = 0;
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(const std::string& n, double r) : Shape(n), radius(r) {}
    double calculateArea() override {
        return 3.14159 * radius * radius;
    }
};

在上述代码中,Shape类有一个构造函数用于初始化name成员变量。Circle类在构造时先调用Shape类的构造函数来初始化name,然后再初始化自己的radius变量。 2. 析构函数:抽象类也应该有虚析构函数。如果抽象类的析构函数不是虚的,当通过基类指针删除派生类对象时,可能不会调用派生类的析构函数,从而导致内存泄漏。例如:

class Shape {
public:
    virtual double calculateArea() = 0;
    ~Shape() {
        std::cout << "Shape destructor" << std::endl;
    }
};

class Circle : public Shape {
private:
    double* radiusPtr;
public:
    Circle(double r) {
        radiusPtr = new double(r);
    }
    double calculateArea() override {
        return 3.14159 * *radiusPtr * *radiusPtr;
    }
    ~Circle() {
        std::cout << "Circle destructor" << std::endl;
        delete radiusPtr;
    }
};

int main() {
    Shape* s = new Circle(5.0);
    delete s; // 如果Shape的析构函数不是虚的,Circle的析构函数不会被调用,导致内存泄漏
    return 0;
}

为了避免这种情况,应将Shape类的析构函数声明为虚的:

class Shape {
public:
    virtual double calculateArea() = 0;
    virtual ~Shape() {
        std::cout << "Shape destructor" << std::endl;
    }
};

这样,当通过基类指针删除派生类对象时,会先调用派生类的析构函数,再调用基类的析构函数,确保资源正确释放。

抽象类与模板类的关系

抽象类和模板类是C++中两个不同但又可以相互配合的概念。

  1. 抽象类提供接口规范:抽象类主要用于定义一组接口,要求派生类去实现这些接口,以实现多态性和代码的可扩展性。
  2. 模板类提供代码复用:模板类则是一种参数化类型的机制,通过将类型参数化,可以生成针对不同类型的类或函数,提高代码的复用性。
  3. 结合使用:在实际编程中,可以将抽象类作为模板类的参数。例如,我们可以定义一个模板类Container,它可以存储不同类型的对象,但这些对象都必须是从某个抽象类派生的,以确保它们具有某些共同的接口。
class Shape {
public:
    virtual double calculateArea() = 0;
};

template <typename T>
class Container {
private:
    T* data;
public:
    Container(T* t) : data(t) {}
    double calculateTotalArea() {
        return data->calculateArea();
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double calculateArea() override {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Circle c(5.0);
    Container<Circle> container(&c);
    std::cout << "Total area in container: " << container.calculateTotalArea() << std::endl;
    return 0;
}

在上述代码中,Container模板类可以存储Circle对象(Circle继承自抽象类Shape),并调用其calculateArea函数来计算面积。通过这种方式,结合了抽象类的接口规范和模板类的代码复用特性。

含纯虚函数类在设计模式中的应用

  1. 策略模式:策略模式中常常会用到抽象类和纯虚函数。策略模式定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。抽象类定义了一个纯虚函数作为算法的接口,具体的算法在派生类中实现。例如:
class SortingAlgorithm {
public:
    virtual void sort(int* arr, int size) = 0;
};

class BubbleSort : public SortingAlgorithm {
public:
    void sort(int* arr, int size) override {
        for (int i = 0; i < size - 1; ++i) {
            for (int j = 0; j < size - i - 1; ++j) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
};

class QuickSort : public SortingAlgorithm {
private:
    int partition(int* arr, int low, int high) {
        int pivot = arr[high];
        int i = low - 1;
        for (int j = low; j < high; ++j) {
            if (arr[j] <= pivot) {
                i++;
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
        int temp = arr[i + 1];
        arr[i + 1] = arr[high];
        arr[high] = temp;
        return i + 1;
    }
    void quickSort(int* arr, int low, int high) {
        if (low < high) {
            int pi = partition(arr, low, high);
            quickSort(arr, low, pi - 1);
            quickSort(arr, pi + 1, high);
        }
    }
public:
    void sort(int* arr, int size) override {
        quickSort(arr, 0, size - 1);
    }
};

class Sorter {
private:
    SortingAlgorithm* algorithm;
public:
    Sorter(SortingAlgorithm* alg) : algorithm(alg) {}
    void performSort(int* arr, int size) {
        algorithm->sort(arr, size);
    }
};

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int size = sizeof(arr) / sizeof(arr[0]);

    BubbleSort bubbleSort;
    Sorter sorter1(&bubbleSort);
    sorter1.performSort(arr, size);

    // 可以轻松切换到QuickSort
    QuickSort quickSort;
    Sorter sorter2(&quickSort);
    sorter2.performSort(arr, size);

    return 0;
}

在上述代码中,SortingAlgorithm是一个抽象类,sort是纯虚函数。BubbleSortQuickSort是派生类,实现了不同的排序算法。Sorter类可以根据传入的不同排序算法对象来执行不同的排序策略。 2. 工厂模式:工厂模式也经常借助抽象类和纯虚函数来创建对象。抽象工厂类定义了创建对象的纯虚函数接口,具体的工厂类继承自抽象工厂类并实现这些函数来创建具体的产品对象。例如:

class Shape {
public:
    virtual void draw() = 0;
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a rectangle." << std::endl;
    }
};

class ShapeFactory {
public:
    virtual Shape* createShape() = 0;
};

class CircleFactory : public ShapeFactory {
public:
    Shape* createShape() override {
        return new Circle();
    }
};

class RectangleFactory : public ShapeFactory {
public:
    Shape* createShape() override {
        return new Rectangle();
    }
};

int main() {
    ShapeFactory* circleFactory = new CircleFactory();
    Shape* circle = circleFactory->createShape();
    circle->draw();

    ShapeFactory* rectangleFactory = new RectangleFactory();
    Shape* rectangle = rectangleFactory->createShape();
    rectangle->draw();

    delete circle;
    delete rectangle;
    delete circleFactory;
    delete rectangleFactory;

    return 0;
}

在上述代码中,Shape是抽象类,draw是纯虚函数。ShapeFactory是抽象工厂类,createShape是纯虚函数。CircleFactoryRectangleFactory是具体的工厂类,分别创建CircleRectangle对象。

含纯虚函数类的局限性

  1. 不能直接实例化:这是抽象类的一个重要特点,但同时也限制了我们直接使用抽象类对象。如果在某些情况下我们希望有一个通用的对象来代表抽象概念,抽象类无法满足这个需求。
  2. 增加代码复杂度:引入抽象类和纯虚函数会增加代码的层次结构和复杂度。在大型项目中,过多的抽象类和纯虚函数可能会导致代码难以理解和维护,特别是对于新加入项目的开发人员。
  3. 运行时开销:由于纯虚函数涉及到虚函数表和动态绑定机制,会带来一定的运行时开销。在对性能要求极高的场景下,这种开销可能会成为一个问题。

尽管含纯虚函数类存在这些局限性,但在正确使用的情况下,它们可以极大地提高代码的可维护性、可扩展性和可复用性,是C++面向对象编程中非常重要的概念。在实际编程中,需要根据项目的具体需求和特点,合理地运用抽象类和纯虚函数,以达到最佳的编程效果。