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

C++构造函数的委托构造

2021-05-293.6k 阅读

C++ 构造函数委托构造的基本概念

在 C++ 编程中,构造函数是用于初始化对象的特殊成员函数。当一个类有多个构造函数时,可能会出现代码重复的情况,例如某些初始化操作在多个构造函数中都需要执行。C++11 引入了委托构造(Constructor Delegation)的特性,允许一个构造函数调用同一个类的其他构造函数,从而避免重复代码,提高代码的可维护性和可读性。

从本质上来说,委托构造就是让一个构造函数将部分或全部的初始化工作交给另一个构造函数来完成。被调用的构造函数称为目标构造函数,调用者称为委托构造函数。这种机制使得在类的设计中,可以更有效地组织构造函数之间的关系,避免重复编写初始化代码。

委托构造的语法

委托构造的语法相对简洁明了。在构造函数的初始化列表中,使用类名后跟圆括号,括号内是目标构造函数的参数列表,以此来表示委托构造。其基本语法格式如下:

class ClassName {
public:
    // 目标构造函数
    ClassName(Type1 param1, Type2 param2) {
        // 执行初始化操作
    }
    // 委托构造函数
    ClassName(Type3 param3) : ClassName(param3, defaultValue) {
        // 可以在此处进行额外的初始化
    }
};

在上述代码中,ClassName(Type3 param3) 是委托构造函数,它通过 : ClassName(param3, defaultValue) 委托给了 ClassName(Type1 param1, Type2 param2) 这个目标构造函数。注意,委托构造必须在构造函数的初始化列表中进行,不能在构造函数体内部。

委托构造的实际应用场景

  1. 简化构造函数逻辑 在实际开发中,一个类可能需要多种不同的构造方式,但有些初始化工作是通用的。例如,我们定义一个表示日期的 Date 类,它有多种构造方式,既可以通过年、月、日完整信息构造,也可以只传入年份,默认月份为 1,日期为 1。
class Date {
private:
    int year;
    int month;
    int day;
public:
    // 目标构造函数,完整初始化
    Date(int y, int m, int d) : year(y), month(m), day(d) {
        // 可以添加日期合法性检查等逻辑
    }
    // 委托构造函数,仅传入年份
    Date(int y) : Date(y, 1, 1) {
        // 此处可以进行针对仅传入年份的额外操作
    }
};

在这个例子中,Date(int y) 委托构造函数将初始化工作委托给了 Date(int y, int m, int d) 目标构造函数,这样避免了在 Date(int y) 中重复编写设置年、月、日的基本初始化逻辑,使得代码更加简洁。

  1. 根据不同参数选择不同初始化逻辑 假设我们有一个 Shape 类,它有不同的派生类 CircleRectangleShape 类可以根据传入参数的类型不同,选择不同的构造方式。
class Shape {
public:
    Shape(double radius) : type("circle") {
        // 针对圆形的初始化逻辑
    }
    Shape(double width, double height) : type("rectangle") {
        // 针对矩形的初始化逻辑
    }
    // 委托构造函数,根据参数类型选择初始化逻辑
    Shape(const std::string& str) {
        if (str.find("circle") != std::string::npos) {
            double radius = 1.0; // 假设默认半径
            *this = Shape(radius);
        } else if (str.find("rectangle") != std::string::npos) {
            double width = 1.0, height = 1.0; // 假设默认宽高
            *this = Shape(width, height);
        }
    }
private:
    std::string type;
};

在上述代码中,Shape(const std::string& str) 委托构造函数根据传入字符串中是否包含 “circle” 或 “rectangle” 来选择不同的目标构造函数进行初始化。这里虽然不是严格意义上在初始化列表中委托构造,但展示了根据不同参数动态选择初始化逻辑的一种思路。

  1. 处理复杂对象初始化 对于一些复杂对象,可能需要多个步骤来完成初始化,并且不同的构造方式可能只是在某些步骤上有差异。以一个 DatabaseConnection 类为例,它需要连接数据库、选择数据库、设置字符集等操作。
class DatabaseConnection {
public:
    // 完整初始化构造函数
    DatabaseConnection(const std::string& host, const std::string& user, const std::string& password, const std::string& dbName, const std::string& charset) {
        connect(host, user, password);
        selectDatabase(dbName);
        setCharset(charset);
    }
    // 委托构造函数,使用默认字符集
    DatabaseConnection(const std::string& host, const std::string& user, const std::string& password, const std::string& dbName) : DatabaseConnection(host, user, password, dbName, "utf8") {
        // 可以在此处添加使用默认字符集时的额外操作
    }
private:
    void connect(const std::string& host, const std::string& user, const std::string& password) {
        // 实际连接数据库的逻辑
    }
    void selectDatabase(const std::string& dbName) {
        // 选择数据库的逻辑
    }
    void setCharset(const std::string& charset) {
        // 设置字符集的逻辑
    }
};

通过委托构造,DatabaseConnection(const std::string& host, const std::string& user, const std::string& password, const std::string& dbName) 构造函数可以复用 DatabaseConnection(const std::string& host, const std::string& user, const std::string& password, const std::string& dbName, const std::string& charset) 中的大部分初始化逻辑,仅在字符集设置上有所不同。

委托构造的注意事项

  1. 初始化顺序 虽然委托构造可以让代码更简洁,但需要注意初始化顺序。在委托构造中,目标构造函数会先执行其初始化列表和构造函数体,然后委托构造函数再执行其自身构造函数体中的代码。例如:
class Example {
public:
    Example(int value) : data(value) {
        std::cout << "Target constructor: data = " << data << std::endl;
    }
    Example() : Example(0) {
        std::cout << "Delegating constructor" << std::endl;
    }
private:
    int data;
};

在这个例子中,先执行 Example(int value) 目标构造函数,输出 “Target constructor: data = 0”,然后执行 Example() 委托构造函数,输出 “Delegating constructor”。

  1. 避免循环委托 委托构造时必须避免出现循环委托的情况,即构造函数 A 委托构造函数 B,而构造函数 B 又委托回构造函数 A。这会导致无限递归调用,最终使程序崩溃。例如:
class BadExample {
public:
    BadExample(int value) : BadExample() {
        // 错误:循环委托
    }
    BadExample() : BadExample(0) {
        // 错误:循环委托
    }
};

编译器通常会检测到这种明显的循环委托并报错,但在一些复杂的继承和委托关系中,可能需要开发者更加仔细地检查代码,以避免这种错误。

  1. 委托构造与继承 在继承体系中,委托构造同样需要谨慎处理。派生类的委托构造函数不能直接委托基类的构造函数,而是要通过调用基类构造函数的方式来初始化基类部分。例如:
class Base {
public:
    Base(int value) : baseData(value) {
    }
private:
    int baseData;
};
class Derived : public Base {
public:
    Derived(int value) : Base(value), derivedData(value * 2) {
    }
    Derived() : Derived(0) {
    }
private:
    int derivedData;
};

Derived 类中,Derived(int value) 构造函数通过 Base(value) 调用基类的构造函数来初始化基类部分,Derived() 委托构造函数则委托给 Derived(int value) 构造函数。

委托构造与其他构造函数特性的结合

  1. 委托构造与默认构造函数 默认构造函数是指没有参数的构造函数。当一个类既有默认构造函数又有其他带参数的构造函数时,委托构造可以很好地协调它们之间的关系。例如:
class MyClass {
public:
    MyClass() : MyClass(0) {
    }
    MyClass(int value) : data(value) {
    }
private:
    int data;
};

这里 MyClass() 默认构造函数委托给了 MyClass(int value) 构造函数,使用默认值 0 进行初始化,使得代码更加简洁统一。

  1. 委托构造与拷贝构造函数 拷贝构造函数用于通过已有的对象创建一个新的对象。在某些情况下,委托构造可以与拷贝构造函数结合使用。例如:
class AnotherClass {
public:
    AnotherClass(int value) : data(value) {
    }
    AnotherClass(const AnotherClass& other) : AnotherClass(other.data) {
        // 可以在此处添加额外的拷贝逻辑
    }
private:
    int data;
};

在这个例子中,AnotherClass(const AnotherClass& other) 拷贝构造函数委托给了 AnotherClass(int value) 构造函数,先进行基本的数据初始化,然后可以在拷贝构造函数体中添加其他与拷贝相关的操作。

  1. 委托构造与移动构造函数 移动构造函数用于从一个临时对象高效地获取资源。委托构造也可以与移动构造函数配合使用。例如:
class Resource {
public:
    Resource(int size) : data(new int[size]), length(size) {
    }
    Resource(Resource&& other) noexcept : Resource(other.length) {
        data = other.data;
        other.data = nullptr;
        other.length = 0;
    }
    ~Resource() {
        delete[] data;
    }
private:
    int* data;
    int length;
};

在上述代码中,Resource(Resource&& other) noexcept 移动构造函数委托给了 Resource(int size) 构造函数,先初始化基本的资源长度,然后再进行资源的移动操作。

委托构造在现代 C++ 设计模式中的应用

  1. 单例模式 在单例模式中,确保一个类只有一个实例存在。委托构造可以用于简化单例类的构造过程。例如:
class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
private:
    Singleton() : Singleton(0) {
    }
    Singleton(int value) : data(value) {
    }
    int data;
    // 禁止拷贝构造和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

在这个单例类中,Singleton() 构造函数委托给 Singleton(int value) 构造函数,使用默认值 0 进行初始化。这样既保证了单例类的构造逻辑统一,又避免了重复代码。

  1. 工厂模式 工厂模式用于创建对象,将对象的创建和使用分离。委托构造可以在工厂类中帮助处理不同类型对象的构造。例如:
class Product {
public:
    virtual ~Product() = default;
};
class ConcreteProduct1 : public Product {
public:
    ConcreteProduct1(int value) : data(value) {
    }
private:
    int data;
};
class ConcreteProduct2 : public Product {
public:
    ConcreteProduct2(const std::string& str) : text(str) {
    }
private:
    std::string text;
};
class ProductFactory {
public:
    static std::unique_ptr<Product> createProduct(int type) {
        if (type == 1) {
            return std::make_unique<ConcreteProduct1>(0);
        } else if (type == 2) {
            return std::make_unique<ConcreteProduct2>("default");
        }
        return nullptr;
    }
};

虽然这里没有直接在产品类中体现委托构造,但在工厂类 ProductFactory 创建不同类型产品对象时,可以通过委托构造来简化产品类的构造过程,例如如果 ConcreteProduct1ConcreteProduct2 有更复杂的初始化逻辑,可以通过委托构造来优化。

委托构造的性能考虑

从性能角度来看,委托构造本身并不会引入显著的性能开销。因为委托构造只是在构造函数初始化列表中调用另一个构造函数,其本质上还是顺序执行构造函数的初始化和构造函数体的代码。

然而,在一些极端情况下,如果委托构造导致过多的函数调用嵌套,可能会对性能产生一定影响。例如:

class DeepDelegation {
public:
    DeepDelegation(int value) : data(value) {
    }
    DeepDelegation() : DeepDelegation(1) {
    }
    DeepDelegation(const DeepDelegation& other) : DeepDelegation(other.data) {
    }
private:
    int data;
};

在这个例子中,如果有多层委托构造,并且构造函数中包含复杂的操作,可能会导致函数调用栈较深,从而影响程序的性能。但在大多数实际应用场景中,这种情况并不常见,并且现代编译器通常会对这种简单的委托构造进行优化,以减少性能损失。

委托构造在不同编译器中的支持情况

C++11 引入委托构造后,主流的编译器如 GCC、Clang 和 Visual Studio Community Edition 等都对其提供了良好的支持。

  1. GCC GCC 从 4.6 版本开始支持 C++11 特性,包括委托构造。在使用 GCC 编译包含委托构造的代码时,只需使用合适的 C++ 标准编译选项,如 -std=c++11 或更新的标准选项 -std=c++14-std=c++17 等。例如:
g++ -std=c++11 -o my_program my_program.cpp
  1. Clang Clang 也从较早版本开始支持 C++11 委托构造。同样,在编译时使用相应的 C++ 标准选项,如 -std=c++11。例如:
clang++ -std=c++11 -o my_program my_program.cpp
  1. Visual Studio Community Edition Visual Studio Community Edition 从 Visual Studio 2013 开始支持 C++11 特性,包括委托构造。在 Visual Studio 中创建 C++ 项目时,可以在项目属性中设置 C++ 语言标准为 “ISO C++11 标准 (/std:c++11)” 或更新的标准。

委托构造与代码维护和扩展性

委托构造对于代码的维护和扩展性有着积极的影响。

  1. 代码维护 通过委托构造,避免了在多个构造函数中重复编写相同的初始化代码。这使得代码的维护更加容易,因为如果需要修改某个初始化逻辑,只需要在目标构造函数中进行修改,而不需要在所有相关的构造函数中逐一修改。例如,在前面提到的 Date 类中,如果日期合法性检查逻辑需要修改,只需要在 Date(int y, int m, int d) 目标构造函数中修改,Date(int y) 委托构造函数会自动受益。

  2. 扩展性 当类的功能需要扩展时,委托构造也能很好地适应变化。例如,在 DatabaseConnection 类中,如果需要添加新的初始化步骤,只需要在目标构造函数中添加,委托构造函数也会自动包含这些新的初始化逻辑。同时,添加新的构造函数时,也可以方便地利用已有的目标构造函数进行委托构造,保持代码的一致性和简洁性。

委托构造的局限性

虽然委托构造是一个非常有用的特性,但它也存在一些局限性。

  1. 初始化列表的限制 委托构造必须在构造函数的初始化列表中进行,这就限制了在某些情况下无法灵活地根据运行时条件选择委托的目标构造函数。例如,如果需要根据一个动态计算的值来选择不同的目标构造函数,在初始化列表中无法实现这种逻辑。
  2. 复杂继承体系中的复杂性 在复杂的继承体系中,委托构造与基类构造函数以及其他派生类构造函数之间的关系可能变得复杂。需要开发者仔细处理基类初始化、派生类特有的初始化以及委托构造之间的顺序和逻辑,否则容易出现错误。

总结委托构造的优势与应用建议

委托构造作为 C++11 引入的重要特性,具有以下优势:

  • 减少代码重复:避免在多个构造函数中重复编写相同的初始化代码,提高代码的可维护性。
  • 提高代码可读性:使构造函数之间的关系更加清晰,更易于理解和调试。
  • 增强代码扩展性:方便在类的功能扩展时,统一管理构造函数的初始化逻辑。

在应用委托构造时,建议:

  • 合理设计构造函数关系:根据类的功能和不同的初始化需求,设计清晰合理的构造函数委托关系,避免循环委托和不必要的复杂性。
  • 注意初始化顺序:明确目标构造函数和委托构造函数的执行顺序,确保初始化逻辑的正确性。
  • 结合其他构造函数特性:与默认构造函数、拷贝构造函数、移动构造函数等结合使用,以实现更完整和高效的对象初始化和管理。

通过充分理解和合理应用委托构造,开发者可以编写出更加简洁、高效和易于维护的 C++ 代码。