委托构造函数 Delegating Constructors

面试官: 在 C++ 中,我们经常会为一个类提供多个构造函数,以方便使用者用不同的方式来初始化对象。

请看下面这个 C++03 风格的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <string>
#include <iostream>

class MyData {
private:
    int a;
    double b;
    std::string c;

public:
    // 构造函数 1
    MyData() {
        a = 0;
        b = 0.0;
        c = "default";
        // 假设还有一些其他的通用初始化逻辑
        std::cout << "Common logic executed.\n";
    }

    // 构造函数 2
    MyData(int val_a) {
        a = val_a;
        b = 0.0;
        c = "default";
        // 同样的通用初始化逻辑
        std::cout << "Common logic executed.\n";
    }

    // 构造函数 3
    MyData(int val_a, double val_b) {
        a = val_a;
        b = val_b;
        c = "default";
        // 又是同样的通用初始化逻辑
        std::cout << "Common logic executed.\n";
    }
};

你可以看到,这三个构造函数中,有很多重复的代码(比如对 b, c 的初始化,以及那个通用的逻辑)。在 C++11 之前,处理这种代码重复是一个比较头疼的问题。

请问,C++11 引入的委托构造函数机制,是如何解决这个问题的?它的语法是怎样的?你能用它来重构上面这个 MyData 类吗?

回答:

  • 委托构造函数机制,允许一个构造函数调用另一个构造函数来完成初始化,从而减少代码冗余。
  • 注意:委托构造函数不能在函数体中调用,而是在函数体外的成员初始化列表中调用。

比如,在上面的代码中,我们可以先定义一个最复杂、最完整的构造函数,然后将其他构造函数的工作委托给这个函数完成,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class MyData {
private:
    int a;
    double b;
    std::string c;

public:
	MyData(int val_a, double val_b, std::string val_c) 
		: a(val_a), b(val_b), c(val_c) {}
    // 构造函数 1
    MyData(): MyData(0, 0.0, "default") {
    }

    // 构造函数 2
    MyData(int val_a): MyData(val_a, 0.0, "default") {
    }

    // 构造函数 3
    MyData(int val_a, double val_b): MyData(val_a, val_b, "default") {
    }
};

面试官 (总结与补充): 你说得非常好。我来帮你把这个知识点再梳理和补充一下。

委托构造函数 (Delegating Constructors)

  • 目的:解决多个构造函数之间代码重复的问题,将通用的初始化逻辑集中在一个地方,提高代码的可维护性

  • 语法

    1
    2
    3
    
    ClassName(params) : ClassName(args) { // 委托调用
        // 委托构造函数(Delegating Constructor)的函数体
    }
    • 一个构造函数 A 通过在自己的成员初始化列表中调用同一个类的另一个构造函数 BA 就被称为委托构造函数B 被称为目标构造函数 (Target Constructor)
  • 执行顺序

    1. 首先,目标构造函数 B 的逻辑被完全执行(包括它的成员初始化列表和函数体)。
    2. 然后,控制权返回,委托构造函数 A函数体被执行。

你重构的 MyData 类是一个绝佳的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 目标构造函数,包含了所有成员的初始化
MyData(int val_a, double val_b, std::string val_c) 
    : a(val_a), b(val_b), c(val_c) 
{
    // 所有通用的初始化逻辑都放在这里
    std::cout << "Common logic executed.\n";
}

// 委托构造函数们,它们的函数体通常是空的
MyData() : MyData(0, 0.0, "default") {}
MyData(int val_a) : MyData(val_a, 0.0, "default") {}
MyData(int val_a, double val_b) : MyData(val_a, val_b, "default") {}

这种写法,将所有初始化逻辑都集中在了那个三参数的目标构造函数中。如果你未来需要修改 c 的默认值,或者在通用逻辑里增加新的操作,你只需要修改一个地方,所有其他构造函数都会自动享受到这个更新。

在 C++11 之前,为了避免代码重复,程序员通常会写一个 privateinit() 各种函数,然后在每个构造函数里都调用它。委托构造函数的出现,提供了一种更原生、更安全的官方解决方案。

你对这个 C++11 的便捷特性已经理解得非常透彻了。


派生类调用基类特定构造函数

面试官 (追问与对比): 好的,我们知道了委托构造函数是用来在同一个类的构造函数之间互相调用的。

现在,我们把目光放到继承体系中。当一个派生类 (Derived Class) 的对象被创建时,它需要调用基类 (Base Class) 的构造函数来初始化从基类继承来的那部分成员。

请问,在派生类的构造函数中,我们是如何显式地调用基类的特定构造函数的?它的语法和委托构造函数有什么相似和不同之处?

回答:

  • 派生类也可以在初始化列表中显式调用基类的构造函数。
  • 它和委托构造函数的相似之处:
    • 都在初始化列表中调用,而不是在函数体中调用。
    • 都是为了在对象构造阶段复用已有的初始化逻辑,减少冗余。
  • 它和委托构造函数的不同之处:
    • 委托构造函数是类内部的自我调用,强调构造复用逻辑。
    • 基类构造函数调用是类之间的继承初始化,强调继承成员的正确构造。

面试官 (总结与肯定): 你说的完全正确。

调用基类构造函数

  • 目的:初始化对象中从基类继承来的那一部分。这是必须的步骤,因为派生类无权直接访问基类的 private 成员,必须通过基类的构造函数来完成初始化。

  • 语法

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    class Base {
    public:
        Base(int b) : b_(b) {}
    private:
        int b_;
    };
    
    class Derived : public Base {
    public:
        // 在成员初始化列表中,通过 Base(args) 的形式调用
        Derived(int b_val, int d_val) : Base(b_val), d_(d_val) {}
    private:
        int d_;
    };

与委托构造函数的对比

特性调用基类构造函数委托构造函数
调用对象基类的构造函数同一个类的另一个构造函数
语法位置成员初始化列表成员初始化列表
目的初始化基类子对象避免当前类的构造代码重复
能否共存可以。可以同时调用基类构造和委托构造吗?(这是一个陷阱问题,我们马上讨论)不可以。一个构造函数要么委托,要么初始化成员和基类。

委托构造函数与基类构造函数不能同时调用

面试官 (最终问题): 好的,你对两者都理解得很清楚。最后一个问题,来考验一下你对细节的把握。

请看下面这段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Derived : public Base {
public:
    // 构造函数 D1
    Derived(int b_val, int d_val) : Base(b_val), d_(d_val) {} // OK
    
    // 构造函数 D2 (委托给 D3)
    Derived() : Derived(0, 0) {}
    
    // 构造函数 D3 (目标构造函数)
    Derived(int val) : d_(val) { /* ??? */ }

    // 我们能写一个同时调用基类构造和委托构造的函数吗?
    // 比如下面这样:
    Derived(double not_used) 
        : Base(10),       // 调用基类构造
          Derived(10, 10) // 又进行委托
    {
    }
private:
    int d_;
};
  1. 在构造函数 D3 中,我们没有显式调用 Base 的构造函数。那么,Base 类的哪一个构造函数会被调用?
  2. 我尝试写的最后一个构造函数,它试图同时调用基类构造函数和进行委托调用,你觉得这段代码能编译通过吗?为什么?

回答:

  • 如果派生类 Derived 没有显式调用基类 Base 的构造函数,那么基类的默认构造函数(无参数构造函数)会被隐式调用。
    • 如果基类没有可访问的默认构造函数,那么派生类的这个构造函数就会编译失败。
  • 如果一个构造函数同时调用基类构造函数和委托调用函数,那么编译器会报错。
    • 原因是,委托构造函数会完全接管当前函数的初始化过程,这包括了基类的初始化。
    • 就像上面代码中调用 Derived(10, 10),这实际上会回到 D1,由 D1 完成基类的初始化,而不能再在当前函数调用基类构造函数了。

面试官 (总结与肯定):

1. 默认调用基类的默认构造函数

“如果派生类 Derived 没有显式调用基类 Base 的构造函数,那么基类的默认构造函数会被调用。”

完全正确。如果派生类构造函数的成员初始化列表中,没有出现 Base(...) 的身影,编译器就会隐式地尝试调用 Base默认构造函数 (无参构造函数)

  • 如果 Base 有一个可访问的默认构造函数,编译通过。
  • 如果 Base 没有默认构造函数(比如它只有一个带参数的构造函数),那么派生类的这个构造函数就会编译失败

2. 委托与基类构造不能共存

“委托构造函数会完全接管当前函数的初始化过程,这包括了基类的初始化。”

这个解释非常深刻,直达本质! C++ 标准规定:

一个构造函数的成员初始化列表,要么进行委托调用,要么进行成员和基类的初始化,两者有我没你,有你没我。

你对原因的分析也完全正确: 委托的本质,就是把整个对象(包括基类部分和派生类部分)的初始化工作,全权交给了那个“目标构造函数”。D1 已经负责调用 Base 的构造函数了,如果你在委托给 D1 的同时,又自己去调用 Base 的构造函数,这就产生了逻辑上的冲突——基类到底应该被谁初始化?

所以,Derived(double not_used) 这样的写法,是不能编译通过的。


总结

今天我们系统地学习了 C++ 构造函数的一个重要特性——委托构造函数

  • 我们理解了它的目的(减少代码重复)。
  • 掌握了它的语法(在成员初始化列表)。
  • 并深入地将它与调用基类构造函数这一相似但不同的概念进行了对比。
  • 最后,你还准确地指出了两者之间“有我没你”的排他关系。

你今天的表现非常出色,对这些复杂的类设计规则理解得既快速又透彻。继续保持!