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

C++类抽象类的应用价值

2021-09-287.5k 阅读

C++ 类抽象类的应用价值

抽象类基础概念

在 C++ 编程中,抽象类是一种特殊的类,它不能被实例化,即无法创建该类的对象。抽象类主要为其他类提供一个通用的框架,作为派生类的基类。抽象类通常包含至少一个纯虚函数,纯虚函数是一种在基类中声明但没有定义实现的函数,其语法形式为在函数声明后加上 = 0。例如:

class Shape {
public:
    // 纯虚函数
    virtual double area() const = 0; 
};

在上述代码中,Shape 类是一个抽象类,因为它包含了纯虚函数 area。任何试图创建 Shape 类对象的操作,如 Shape s;,都会导致编译错误。

代码复用与可维护性提升

  1. 复用通用代码
    • 假设有一个图形绘制系统,其中包含不同类型的图形,如圆形、矩形、三角形等。每个图形都有计算面积和周长的操作。通过创建一个抽象的 Shape 类,将这些图形共有的操作抽象出来。例如:
class Shape {
public:
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
    double perimeter() const override {
        return 2 * 3.14159 * radius;
    }
};

class Rectangle : public Shape {
private:
    double length;
    double width;
public:
    Rectangle(double l, double w) : length(l), width(w) {}
    double area() const override {
        return length * width;
    }
    double perimeter() const override {
        return 2 * (length + width);
    }
};
- 在上述代码中,`Circle` 和 `Rectangle` 类继承自抽象类 `Shape`,并实现了其纯虚函数。这样,在编写图形绘制系统的其他部分,如计算多个图形总面积的函数时,可以统一操作 `Shape` 类型的指针或引用,实现代码复用。
double totalArea(const std::vector<Shape*>& shapes) {
    double total = 0;
    for (const auto* shape : shapes) {
        total += shape->area();
    }
    return total;
}
  1. 便于维护
    • 当需求发生变化时,比如需要为所有图形添加一个新的操作,如获取图形的颜色。只需要在抽象类 Shape 中添加一个纯虚函数 getColor,然后在所有派生类中实现该函数即可。这种方式使得代码的修改集中在少数几个地方,提高了代码的可维护性。
class Shape {
public:
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
    virtual std::string getColor() const = 0;
};

class Circle : public Shape {
private:
    double radius;
    std::string color;
public:
    Circle(double r, const std::string& c) : radius(r), color(c) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
    double perimeter() const override {
        return 2 * 3.14159 * radius;
    }
    std::string getColor() const override {
        return color;
    }
};

class Rectangle : public Shape {
private:
    double length;
    double width;
    std::string color;
public:
    Rectangle(double l, double w, const std::string& c) : length(l), width(w), color(c) {}
    double area() const override {
        return length * width;
    }
    double perimeter() const override {
        return 2 * (length + width);
    }
    std::string getColor() const override {
        return color;
    }
};

实现多态性

  1. 运行时多态
    • 抽象类是实现运行时多态的关键。通过使用抽象类作为基类,派生类重写纯虚函数,然后通过基类指针或引用调用这些函数,就可以实现运行时多态。例如:
void printArea(const Shape& shape) {
    std::cout << "Area: " << shape.area() << std::endl;
}

int main() {
    Circle circle(5);
    Rectangle rectangle(4, 6);

    printArea(circle);
    printArea(rectangle);

    return 0;
}
- 在上述代码中,`printArea` 函数接受一个 `Shape` 类的引用。当传入 `Circle` 或 `Rectangle` 对象时,会根据对象的实际类型调用相应的 `area` 函数,实现了运行时多态。这使得程序能够根据对象的实际类型来执行不同的操作,增加了程序的灵活性。

2. 多态容器 - 可以使用抽象类创建多态容器,例如 std::vector<Shape*>。这样的容器可以存储不同类型的派生类对象指针,在遍历容器时可以根据对象的实际类型调用相应的函数。

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle(3));
    shapes.push_back(new Rectangle(5, 7));

    for (const auto* shape : shapes) {
        std::cout << "Area: " << shape->area() << ", Perimeter: " << shape->perimeter() << std::endl;
    }

    for (auto* shape : shapes) {
        delete shape;
    }

    return 0;
}
- 在这个例子中,`shapes` 容器存储了 `Circle` 和 `Rectangle` 对象的指针。通过遍历容器,可以对不同类型的图形进行统一的操作,如计算面积和周长。但要注意内存管理,在使用完指针后需要手动释放内存,以避免内存泄漏。

架构设计与模块解耦

  1. 分层架构
    • 在大型软件项目中,抽象类常用于分层架构设计。例如,在一个游戏开发项目中,可以有一个抽象的 GameObject 类作为所有游戏对象(如角色、道具、场景元素等)的基类。GameObject 类可以定义一些通用的接口,如 update(用于更新对象状态)、render(用于渲染对象)等。
class GameObject {
public:
    virtual void update() = 0;
    virtual void render() = 0;
};

class Player : public GameObject {
public:
    void update() override {
        // 实现玩家对象的更新逻辑
    }
    void render() override {
        // 实现玩家对象的渲染逻辑
    }
};

class Item : public GameObject {
public:
    void update() override {
        // 实现道具对象的更新逻辑
    }
    void render() override {
        // 实现道具对象的渲染逻辑
    }
};
- 通过这种方式,游戏的不同层(如逻辑层、渲染层)可以通过 `GameObject` 类的接口进行交互,而不需要关心具体对象的类型。这使得各层之间的耦合度降低,便于独立开发和维护。

2. 模块解耦 - 假设一个图形处理库,其中包含图形生成模块和图形显示模块。抽象类可以用于解耦这两个模块。图形生成模块可以生成各种具体的图形对象(派生自抽象的 Shape 类),然后将这些对象传递给图形显示模块。图形显示模块只需要通过 Shape 类的接口来显示图形,而不需要知道具体图形的生成细节。

// 图形生成模块
class ShapeGenerator {
public:
    static Shape* createCircle(double radius) {
        return new Circle(radius);
    }
    static Shape* createRectangle(double length, double width) {
        return new Rectangle(length, width);
    }
};

// 图形显示模块
class ShapeDisplayer {
public:
    void display(const Shape& shape) {
        std::cout << "Displaying shape with area: " << shape.area() << std::endl;
    }
};
- 在上述代码中,`ShapeGenerator` 负责创建具体的图形对象,`ShapeDisplayer` 负责显示图形。它们通过抽象类 `Shape` 进行交互,实现了模块之间的解耦。

接口定义与契约约束

  1. 定义接口
    • 抽象类可以作为一种接口定义的方式。例如,在一个数据库访问层的设计中,可以定义一个抽象类 DatabaseAccess,其中包含一些纯虚函数,如 connect(用于连接数据库)、query(用于执行查询语句)、update(用于执行更新操作)等。
class DatabaseAccess {
public:
    virtual bool connect(const std::string& server, const std::string& user, const std::string& password) = 0;
    virtual std::vector<std::vector<std::string>> query(const std::string& sql) = 0;
    virtual bool update(const std::string& sql) = 0;
};
- 不同类型的数据库(如 MySQL、Oracle 等)可以通过继承 `DatabaseAccess` 类并实现这些纯虚函数来提供具体的数据库访问实现。这样,其他使用数据库访问功能的模块只需要依赖 `DatabaseAccess` 接口,而不需要关心具体的数据库类型。

2. 契约约束 - 抽象类的纯虚函数为派生类定义了一种契约。派生类必须实现这些纯虚函数,否则派生类也会成为抽象类。这种契约约束确保了所有派生类都具有一致的接口,使得代码的调用者可以依赖这些接口进行编程。例如,在一个图形编辑软件中,所有可编辑的图形(派生自抽象的 EditableShape 类)都必须实现 edit 函数,以提供图形编辑的功能。

class EditableShape {
public:
    virtual void edit() = 0;
};

class Square : public EditableShape {
public:
    void edit() override {
        // 实现正方形的编辑逻辑
    }
};
- 这种契约约束使得代码在设计上更加规范,提高了代码的可靠性和可预测性。

代码的扩展性与灵活性

  1. 易于添加新类型
    • 当需要在系统中添加新的类型时,使用抽象类可以使代码的扩展性更好。例如,在前面的图形绘制系统中,如果需要添加一种新的图形,如三角形,只需要继承抽象类 Shape 并实现其纯虚函数即可。
class Triangle : public Shape {
private:
    double side1;
    double side2;
    double side3;
public:
    Triangle(double s1, double s2, double s3) : side1(s1), side2(s2), side3(s3) {}
    double area() const override {
        double s = (side1 + side2 + side3) / 2;
        return std::sqrt(s * (s - side1) * (s - side2) * (s - side3));
    }
    double perimeter() const override {
        return side1 + side2 + side3;
    }
};
- 然后可以在使用 `Shape` 的地方,如 `totalArea` 函数和多态容器中,直接使用 `Triangle` 对象,而不需要对原有代码进行大规模修改。

2. 灵活的行为定制 - 抽象类的派生类可以根据自身需求定制行为。例如,在一个动画系统中,有一个抽象类 Animatable 定义了基本的动画操作接口,如 startAnimationstopAnimation 等。不同类型的动画对象(如角色动画、场景动画等)继承自 Animatable 类,并根据自身特点实现这些接口。

class Animatable {
public:
    virtual void startAnimation() = 0;
    virtual void stopAnimation() = 0;
};

class CharacterAnimation : public Animatable {
public:
    void startAnimation() override {
        // 实现角色动画的启动逻辑
    }
    void stopAnimation() override {
        // 实现角色动画的停止逻辑
    }
};

class SceneAnimation : public Animatable {
public:
    void startAnimation() override {
        // 实现场景动画的启动逻辑
    }
    void stopAnimation() override {
        // 实现场景动画的停止逻辑
    }
};
- 这种灵活性使得系统能够适应不同的需求和场景,提高了代码的复用性和适应性。

抽象类在设计模式中的应用

  1. 工厂模式
    • 在工厂模式中,抽象类常用于定义产品的接口。例如,有一个创建不同类型文档的工厂。可以定义一个抽象的 Document 类作为所有文档类型的基类,然后通过工厂类创建具体的文档对象,如 WordDocumentPDFDocument 等。
class Document {
public:
    virtual void save() = 0;
    virtual void open() = 0;
};

class WordDocument : public Document {
public:
    void save() override {
        std::cout << "Saving Word document..." << std::endl;
    }
    void open() override {
        std::cout << "Opening Word document..." << std::endl;
    }
};

class PDFDocument : public Document {
public:
    void save() override {
        std::cout << "Saving PDF document..." << std::endl;
    }
    void open() override {
        std::cout << "Opening PDF document..." << std::endl;
    }
};

class DocumentFactory {
public:
    static Document* createDocument(const std::string& type) {
        if (type == "word") {
            return new WordDocument();
        } else if (type == "pdf") {
            return new PDFDocument();
        }
        return nullptr;
    }
};
- 在上述代码中,`Document` 抽象类定义了文档的基本操作接口,`DocumentFactory` 根据传入的类型创建具体的文档对象。这种方式将对象的创建和使用分离,提高了代码的可维护性和扩展性。

2. 策略模式 - 策略模式中,抽象类可以用于定义不同策略的接口。例如,在一个排序算法的应用中,可以定义一个抽象类 SortStrategy,其中包含一个纯虚函数 sort。然后不同的排序算法(如冒泡排序、快速排序等)继承自 SortStrategy 类并实现 sort 函数。

class SortStrategy {
public:
    virtual void sort(std::vector<int>& data) = 0;
};

class BubbleSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override {
        int n = data.size();
        for (int i = 0; i < n - 1; ++i) {
            for (int j = 0; j < n - i - 1; ++j) {
                if (data[j] > data[j + 1]) {
                    std::swap(data[j], data[j + 1]);
                }
            }
        }
    }
};

class QuickSort : public SortStrategy {
private:
    int partition(std::vector<int>& data, int low, int high) {
        int pivot = data[high];
        int i = low - 1;
        for (int j = low; j < high; ++j) {
            if (data[j] <= pivot) {
                ++i;
                std::swap(data[i], data[j]);
            }
        }
        std::swap(data[i + 1], data[high]);
        return i + 1;
    }
    void quickSort(std::vector<int>& data, int low, int high) {
        if (low < high) {
            int pi = partition(data, low, high);
            quickSort(data, low, pi - 1);
            quickSort(data, pi + 1, high);
        }
    }
public:
    void sort(std::vector<int>& data) override {
        quickSort(data, 0, data.size() - 1);
    }
};
- 这样,在需要进行排序的地方,可以根据不同的需求选择不同的排序策略,通过 `SortStrategy` 接口来调用相应的排序算法,实现了算法的灵活切换和代码的解耦。

综上所述,C++ 中的抽象类在代码复用、多态实现、架构设计、接口定义、扩展性以及设计模式应用等方面都具有极高的应用价值,是构建大型、复杂、可维护和可扩展软件系统的重要工具。