13.8 - 重叠和委托构造函数
Key Takeaway
- 分清楚初始化和赋值
重叠构造函数
当实例化一个新对象时,将隐式调用该对象的构造函数。具有多个具有重叠功能的构造函数的类并不罕见。考虑以下类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
这个类有两个构造函数:一个默认构造函数以及一个接受一个int
类型参数的构造函数。因为 “code to do A” 这一部分在两个构造函数中都存在,则可以说它们在每个构造函数中是重复的。
课程学到这里,相比你已经了解到,要尽可能避免重复代码,所以让我们看看解决这个问题的一些方法。
显而易见的解决方案并不奏效
显而易见的解决方案是让Foo(int)
构造函数调用Foo()
构造函数来完成A部分工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
不过,如果你像上面这样在一个构造函数里面调用另外一个构造函数的话,程序虽然可以编译(也许会产生告警),但是绝对不会像你想象的那样工作,然后你可能需要花费很多时间去定位问题。实际上,构造函数 Foo();
会实例化一个新的 Foo
对象。
构造函数委派
构造函数可以调用来自同一类的其他构造函数,称为委托构造函数(或构造函数链)。
要让一个构造函数调用另一个构造函数,只需在成员初始化列表中调用它。这是可以直接调用另一个构造函数的一种情况。应用到我们上面的例子中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
这完全符合您的预期。确保从成员初始化列表中调用构造函数,而不是在构造函数的函数体中调用。
下面是使用委托构造函数减少冗余代码的另一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
这个类有两个构造函数,其中一个委托给Employee(int, std::string_view)
。通过这种方式,冗余代码的数量被最小化了(我们只需要编写一个构造函数体而不是两个)。
关于委托构造函数的一些额外注意事项。首先,委托给另一个构造函数的构造函数本身不允许进行任何成员初始化。构造函数可以委托或初始化,但不能同时委托和初始化。
其次,如果一个构造函数委托给另一个构造函数,而另一个构造函数又委托回第一个构造函数。这将形成一个死循环,并将导致的程序耗尽堆栈空间后崩溃。可以通过确保所有构造函数解析为非委托构造函数来避免这种情况。
最佳实践
如果您有多个具有相同功能的构造函数,请使用委托构造函数以避免重复代码。
使用普通成员函数进行启动配置
因为构造函数只能用作初始化或委托,所以如果默认构造函数需要执行一些常见的初始化,就会很麻烦。考虑以下类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
构造函数 Foo(int)
要么初始化 m_value
,要么委托给Foo()
执行其中的代码,但鱼和熊掌不可兼得。但是如果一定要兼得呢?当然,最笨的的办法就是把需要执行的代码复制一份过来。但这无疑会带来很多重复代码,导致代码难以维护。
其实构造函数是可以调用非构造成员函数(和非成员函数)的,因此更好的办法是使用一个普通函数来执行这些代码,就像下面这样:
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 |
|
在这个例子中。我们定义 setup()
成员函数来执行启动配置相关的代码,类中的两个构造函数都可以调用它。我们将该函数定义为私有成员,所以只有这个类的成员可以访问它。
当然,setup()
并不是构造函数,所以它不能初始化成员。 当构造函数调用setup()
时,类成员们都已经被创建了(如果提供了初始化值则甚至已经完成了初始化)。setup()
函数只能为成员赋值或进行一些其他操作,但都只能通过一般的语句进行(例如打开文件或数据库)。setup()
函数不能创建成员的左值引用,也不能为 const 类型的成员赋值(两者都需要通过初始化完成),当然也不能向任何不支持赋值操作的成员赋值。
重置对象
有时候你可能会想要编写一个成员函数(例如 reset()
)将类对象重置为其初始状态。
因为默认构造函数可以为类成员初始化所需的值,所以你可能会尝试在reset()
函数中调用构造函数以达到目的。不过,直接调用构造函数通常会导致非预期的行为,就像之前展示的那样,它并不能正确的工作。
reset()
函数的一般实现可能是这样的:
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 38 39 |
|
虽然这种实现可以工作,但是违反了DRY验证,因为“默认”值出现在了两个位置:非静态成员初始化和reset()
函数体中。不过,也的确没有办法能够让reset()
直接获取非静态成员初始化值。
但是,如果一个类是可赋值的(即可以访问其赋值操作符),我们可以先创建一个新的对象,然后将新对象赋值给老对象,实现重置。
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 38 39 40 |
|
在上面的例子中,我们首先创建了一个默认的 Foo
对象(包含默认初值)。然后我们将这个新 Foo
对象赋值给调用reset()
成员函数的对象 (*this
)。此时编译器会进行成员依次拷贝。
相关内容
我们会在 13.10 - 隐藏的this指针 中介绍this指针,类对象的赋值则会在 14.15 -- Overloading the assignment operator 中进行介绍。