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

C++抽象类构造函数在设计模式中的作用

2022-08-191.5k 阅读

C++抽象类构造函数基础概念

抽象类的定义

在C++中,抽象类是一种不能被实例化的类,它至少包含一个纯虚函数。纯虚函数是在声明时被初始化为0的虚函数。例如:

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

这里的Shape类就是一个抽象类,因为它包含了纯虚函数area。任何试图实例化Shape类的操作,比如Shape s;,都会导致编译错误。

构造函数的基本作用

构造函数是一种特殊的成员函数,它在对象创建时被自动调用,用于初始化对象的成员变量。对于普通类,构造函数的形式如下:

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;
    }
};

Rectangle类的构造函数中,我们使用成员初始化列表来初始化widthheight成员变量。

抽象类构造函数的存在意义

虽然抽象类不能被实例化,但它仍然可以拥有构造函数。抽象类构造函数的主要作用是为派生类对象的初始化提供一个起点。当派生类对象被创建时,首先会调用其基类(抽象类)的构造函数,然后再调用派生类自身的构造函数。例如:

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

当创建一个Circle对象时,首先会调用Shape类的构造函数(即使Shape是抽象类),然后再调用Circle类的构造函数。

在设计模式中抽象类构造函数的作用

工厂模式

工厂模式概述

工厂模式是一种创建型设计模式,它提供了一种创建对象的方式,将对象的创建和使用分离。工厂模式通常包含一个工厂类,该类负责创建具体的对象。

抽象类构造函数在工厂模式中的应用

以一个简单的图形绘制工厂为例,假设我们有一个抽象的Shape类,以及具体的RectangleCircle类。我们创建一个ShapeFactory类来创建这些图形对象。

class Shape {
public:
    virtual double area() const = 0;
    // 抽象类构造函数可以用于初始化一些通用的属性
    Shape() {
        std::cout << "Shape constructor called." << std::endl;
    }
    virtual ~Shape() {}
};

class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(double w, double h) : width(w), height(h) {
        std::cout << "Rectangle constructor called." << std::endl;
    }
    double area() const override {
        return width * height;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {
        std::cout << "Circle constructor called." << std::endl;
    }
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

class ShapeFactory {
public:
    static Shape* createShape(const std::string& type) {
        if (type == "rectangle") {
            return new Rectangle(5, 3);
        } else if (type == "circle") {
            return new Circle(4);
        }
        return nullptr;
    }
};

在这个例子中,Shape类的构造函数虽然简单,但它在创建RectangleCircle对象时会首先被调用。这对于初始化一些Shape类共有的属性或执行一些通用的初始化操作是非常有用的。比如,Shape类可能有一个用于记录创建时间的成员变量,就可以在其构造函数中进行初始化。

模板方法模式

模板方法模式概述

模板方法模式是一种行为型设计模式,它定义了一个操作中的算法骨架,将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下重新定义算法的某些步骤。

抽象类构造函数在模板方法模式中的应用

考虑一个文件处理的场景,我们有一个抽象的FileProcessor类,它定义了一个模板方法processFile,该方法包含一些通用的文件处理步骤,如打开文件、读取文件、处理文件内容和关闭文件。其中读取文件和处理文件内容的具体实现由子类提供。

class FileProcessor {
protected:
    std::ifstream file;
    // 抽象类构造函数可以初始化文件路径等通用属性
    FileProcessor(const std::string& filePath) {
        file.open(filePath);
        if (!file.is_open()) {
            std::cerr << "Failed to open file: " << filePath << std::endl;
        }
    }
public:
    virtual ~FileProcessor() {
        if (file.is_open()) {
            file.close();
        }
    }
    void processFile() {
        if (file.is_open()) {
            std::string content = readFile();
            processContent(content);
        }
    }
    virtual std::string readFile() const = 0;
    virtual void processContent(const std::string& content) const = 0;
};

class TextFileProcessor : public FileProcessor {
public:
    TextFileProcessor(const std::string& filePath) : FileProcessor(filePath) {}
    std::string readFile() const override {
        std::string content;
        std::string line;
        while (std::getline(file, line)) {
            content += line + "\n";
        }
        return content;
    }
    void processContent(const std::string& content) const override {
        std::cout << "Processing text file content: " << content << std::endl;
    }
};

在这个例子中,FileProcessor类的构造函数用于打开文件。当创建TextFileProcessor对象时,首先会调用FileProcessor类的构造函数,确保文件被正确打开。这种方式保证了模板方法processFile所需的文件资源在对象创建时就已经准备好。

策略模式

策略模式概述

策略模式是一种行为型设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。策略模式使得算法的变化独立于使用算法的客户。

抽象类构造函数在策略模式中的应用

假设我们有一个支付系统,有不同的支付策略,如信用卡支付、支付宝支付等。我们可以定义一个抽象的PaymentStrategy类,具体的支付策略类继承自它。

class PaymentStrategy {
public:
    // 抽象类构造函数可以初始化一些支付相关的通用信息
    PaymentStrategy() {
        std::cout << "PaymentStrategy constructor called." << std::endl;
    }
    virtual void pay(double amount) const = 0;
    virtual ~PaymentStrategy() {}
};

class CreditCardPayment : public PaymentStrategy {
private:
    std::string cardNumber;
    std::string expirationDate;
    std::string cvv;
public:
    CreditCardPayment(const std::string& num, const std::string& exp, const std::string& cvv)
        : cardNumber(num), expirationDate(exp), cvv(cvv) {
        std::cout << "CreditCardPayment constructor called." << std::endl;
    }
    void pay(double amount) const override {
        std::cout << "Paying $" << amount << " with credit card " << cardNumber << std::endl;
    }
};

class AlipayPayment : public PaymentStrategy {
private:
    std::string account;
public:
    AlipayPayment(const std::string& acc) : account(acc) {
        std::cout << "AlipayPayment constructor called." << std::endl;
    }
    void pay(double amount) const override {
        std::cout << "Paying $" << amount << " with Alipay account " << account << std::endl;
    }
};

class ShoppingCart {
private:
    double totalAmount;
    PaymentStrategy* paymentStrategy;
public:
    ShoppingCart(double amount) : totalAmount(amount), paymentStrategy(nullptr) {}
    void setPaymentStrategy(PaymentStrategy* strategy) {
        paymentStrategy = strategy;
    }
    void checkout() const {
        if (paymentStrategy) {
            paymentStrategy->pay(totalAmount);
        } else {
            std::cerr << "No payment strategy set." << std::endl;
        }
    }
};

在这个例子中,PaymentStrategy类的构造函数可以用于初始化一些支付相关的通用信息,比如日志记录等。当创建具体的支付策略对象,如CreditCardPaymentAlipayPayment时,首先会调用PaymentStrategy类的构造函数,然后再进行具体支付策略的初始化。

抽象类构造函数的注意事项

调用顺序

当派生类对象被创建时,基类(抽象类)的构造函数总是在派生类构造函数之前被调用。这种调用顺序保证了对象的正确初始化。例如:

class BaseAbstract {
public:
    BaseAbstract() {
        std::cout << "BaseAbstract constructor called." << std::endl;
    }
    virtual ~BaseAbstract() {
        std::cout << "BaseAbstract destructor called." << std::endl;
    }
    virtual void doSomething() = 0;
};

class Derived : public BaseAbstract {
public:
    Derived() {
        std::cout << "Derived constructor called." << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
    void doSomething() override {
        std::cout << "Derived doSomething called." << std::endl;
    }
};

当创建一个Derived对象时,输出将是:

BaseAbstract constructor called.
Derived constructor called.

而当对象被销毁时,顺序相反,先调用Derived的析构函数,再调用BaseAbstract的析构函数。

初始化列表

在抽象类构造函数中,使用成员初始化列表来初始化成员变量是一种高效的方式。这不仅可以避免不必要的默认构造和赋值操作,还可以保证在构造函数体执行之前成员变量就已经被正确初始化。例如:

class AbstractClass {
private:
    int value;
public:
    AbstractClass(int v) : value(v) {
        // 构造函数体可以执行其他操作
    }
    virtual ~AbstractClass() {}
    virtual void performAction() = 0;
};

在这个例子中,AbstractClass的构造函数使用成员初始化列表来初始化value成员变量。

抽象类构造函数与多态

抽象类构造函数本身不支持多态行为。这是因为在构造函数执行期间,对象的虚函数表还没有完全初始化。例如:

class AbstractBase {
public:
    AbstractBase() {
        doSomething();
    }
    virtual void doSomething() {
        std::cout << "AbstractBase doSomething." << std::endl;
    }
    virtual ~AbstractBase() {}
};

class DerivedClass : public AbstractBase {
public:
    DerivedClass() {
        // 派生类构造函数体
    }
    void doSomething() override {
        std::cout << "DerivedClass doSomething." << std::endl;
    }
};

当创建一个DerivedClass对象时,在AbstractBase构造函数中调用doSomething,输出将是AbstractBase doSomething.,而不是DerivedClass doSomething.。这是因为在AbstractBase构造函数执行时,对象还没有完全成为DerivedClass类型,虚函数表尚未正确设置为DerivedClass的版本。

高级应用场景

构建复杂对象层次结构

在大型项目中,可能会有复杂的对象层次结构,其中抽象类处于层次结构的较高位置。抽象类构造函数可以用于初始化一些跨层次的通用属性或资源。例如,在一个游戏开发项目中,可能有一个抽象的GameObject类,它是所有游戏对象(如角色、道具等)的基类。

class GameObject {
protected:
    std::string name;
    int id;
    // 用于初始化通用的游戏对象属性
    GameObject(const std::string& objName, int objId) : name(objName), id(objId) {
        std::cout << "GameObject constructor for " << name << " with id " << id << std::endl;
    }
public:
    virtual ~GameObject() {
        std::cout << "GameObject destructor for " << name << " with id " << id << std::endl;
    }
    virtual void update() = 0;
};

class Player : public GameObject {
private:
    int health;
public:
    Player(const std::string& playerName, int playerId, int playerHealth)
        : GameObject(playerName, playerId), health(playerHealth) {
        std::cout << "Player constructor for " << name << " with health " << health << std::endl;
    }
    void update() override {
        std::cout << "Player " << name << " is updating. Health: " << health << std::endl;
    }
};

class Item : public GameObject {
private:
    std::string description;
public:
    Item(const std::string& itemName, int itemId, const std::string& itemDesc)
        : GameObject(itemName, itemId), description(itemDesc) {
        std::cout << "Item constructor for " << name << " with description " << description << std::endl;
    }
    void update() override {
        std::cout << "Item " << name << " is updating. Description: " << description << std::endl;
    }
};

在这个例子中,GameObject的构造函数初始化了nameid属性,这些属性对于PlayerItem等派生类都是通用的。

资源管理与依赖注入

抽象类构造函数可以用于资源管理和依赖注入。例如,在一个数据库访问层的设计中,可能有一个抽象的DatabaseAccess类,具体的数据库访问实现类(如MySQLAccessOracleAccess等)继承自它。

class DatabaseConnection {
public:
    DatabaseConnection(const std::string& host, int port, const std::string& user, const std::string& password) {
        // 实际的连接逻辑
        std::cout << "Connecting to database at " << host << ":" << port << " as " << user << std::endl;
    }
    // 数据库操作方法
    void executeQuery(const std::string& query) {
        std::cout << "Executing query: " << query << std::endl;
    }
};

class DatabaseAccess {
protected:
    DatabaseConnection* connection;
    // 通过构造函数进行依赖注入
    DatabaseAccess(DatabaseConnection* dbConn) : connection(dbConn) {
        std::cout << "DatabaseAccess constructor with connection." << std::endl;
    }
public:
    virtual ~DatabaseAccess() {
        if (connection) {
            delete connection;
        }
        std::cout << "DatabaseAccess destructor." << std::endl;
    }
    virtual void queryDatabase(const std::string& query) = 0;
};

class MySQLAccess : public DatabaseAccess {
public:
    MySQLAccess(DatabaseConnection* dbConn) : DatabaseAccess(dbConn) {}
    void queryDatabase(const std::string& query) override {
        std::cout << "MySQLAccess querying database: ";
        connection->executeQuery(query);
    }
};

class OracleAccess : public DatabaseAccess {
public:
    OracleAccess(DatabaseConnection* dbConn) : DatabaseAccess(dbConn) {}
    void queryDatabase(const std::string& query) override {
        std::cout << "OracleAccess querying database: ";
        connection->executeQuery(query);
    }
};

在这个例子中,DatabaseAccess的构造函数接受一个DatabaseConnection对象指针,实现了依赖注入。这样,具体的数据库访问类(如MySQLAccessOracleAccess)可以在创建时获得所需的数据库连接资源,并且DatabaseAccess的构造函数还可以负责一些通用的资源初始化和管理操作。

与智能指针结合使用

在现代C++中,智能指针(如std::unique_ptrstd::shared_ptr)被广泛用于管理动态分配的对象,以避免内存泄漏。抽象类构造函数可以与智能指针结合使用,使得对象的生命周期管理更加安全和高效。例如:

#include <memory>
class AbstractProduct {
public:
    virtual void use() = 0;
    virtual ~AbstractProduct() {}
};

class ConcreteProduct : public AbstractProduct {
public:
    void use() override {
        std::cout << "Using ConcreteProduct." << std::endl;
    }
};

class ProductFactory {
public:
    static std::unique_ptr<AbstractProduct> createProduct() {
        return std::make_unique<ConcreteProduct>();
    }
};

class Client {
private:
    std::unique_ptr<AbstractProduct> product;
public:
    Client() {
        product = ProductFactory::createProduct();
    }
    void doWork() {
        if (product) {
            product->use();
        }
    }
};

在这个例子中,ProductFactory使用std::make_unique创建一个AbstractProduct的派生类对象,并返回一个std::unique_ptrClient类在其构造函数中获取这个std::unique_ptr,这样AbstractProduct及其派生类对象的生命周期由std::unique_ptr自动管理。当Client对象被销毁时,std::unique_ptr会自动释放其所管理的AbstractProduct对象,避免了手动释放内存可能导致的错误。抽象类构造函数虽然没有直接参与智能指针的创建,但通过对象的层次结构,确保了智能指针所管理对象的正确初始化。

通过以上详细的介绍,我们深入了解了C++抽象类构造函数在设计模式中的重要作用,以及在不同场景下的应用和注意事项。在实际的软件开发中,合理利用抽象类构造函数可以提高代码的可维护性、可扩展性和健壮性。