只要有可能,我们总是希望减少重复代码(遵循DRY原则——不要重复自己)。
考虑以下函数:
void A()
{
// 执行任务A的语句
}
void B()
{
// 执行任务A的语句
// 执行任务B的语句
}
这两个函数都有一组执行完全相同操作(任务A)的语句。在这种情况下,我们可以重构如下:
void A()
{
// 执行任务A的语句
}
void B()
{
A();
// 执行任务B的语句
}
通过这种方式,我们消除了函数A()
和B()
中存在的重复代码。这使得我们的代码更易于维护,因为只需要在一个地方进行更改。
当一个类包含多个构造函数时,每个构造函数中的代码通常非常相似,甚至完全相同,存在大量重复。我们同样希望尽可能地消除构造函数的冗余。
考虑以下示例:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
private:
std::string m_name { "???" };
int m_id { 0 };
bool m_isManager { false };
public:
Employee(std::string_view name, int id) // 员工必须有名字和ID
: m_name{ name }, m_id { id }
{
std::cout << "Employee " << m_name << " created\n";
}
Employee(std::string_view name, int id, bool isManager) // 他们可以是经理
: m_name{ name }, m_id{ id }, m_isManager { isManager }
{
std::cout << "Employee " << m_name << " created\n";
}
};
int main()
{
Employee e1{ "James", 7 };
Employee e2{ "Dave", 42, true };
}
每个构造函数的主体都有完全相同的打印语句。
作者注:通常不建议让构造函数打印内容(除非是调试目的),因为这意味着你无法在不希望打印内容的情况下使用该构造函数创建对象。我们在这个例子中这么做是为了帮助说明发生了什么。
构造函数可以调用其他函数,包括类的其他成员函数。因此,我们可以这样重构:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
private:
std::string m_name { "???" };
int m_id{ 0 };
bool m_isManager { false };
void printCreated() const // 我们的新辅助函数
{
std::cout << "Employee " << m_name << " created\n";
}
public:
Employee(std::string_view name, int id)
: m_name{ name }, m_id { id }
{
printCreated(); // 在这里调用
}
Employee(std::string_view name, int id, bool isManager)
: m_name{ name }, m_id{ id }, m_isManager { isManager }
{
printCreated(); // 在这里调用
}
};
int main()
{
Employee e1{ "James", 7 };
Employee e2{ "Dave", 42, true };
}
这比之前的版本要好(因为重复的语句被一个重复的函数调用取代了),但它需要引入一个新函数。而且我们的两个构造函数都在初始化m_name
和m_id
。理想情况下,我们也应该消除这种冗余。
我们能做得更好吗?可以。但这是许多新手程序员遇到麻烦的地方。
在函数体中调用构造函数会创建临时对象
类似于我们在上面的例子中让函数B()
调用函数A()
,显而易见的解决方案似乎是让Employee(std::string_view, int, bool)
的构造函数体调用Employee(std::string_view, int)
构造函数来初始化m_name
、m_id
并打印语句。看起来是这样的:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
private:
std::string m_name { "???" };
int m_id { 0 };
bool m_isManager { false };
public:
Employee(std::string_view name, int id)
: m_name{ name }, m_id { id } // 这个构造函数初始化名字和ID
{
std::cout << "Employee " << m_name << " created\n"; // 我们的打印语句又回来了
}
Employee(std::string_view name, int id, bool isManager)
: m_isManager { isManager } // 这个构造函数初始化m_isManager
{
// 调用Employee(std::string_view, int)来初始化m_name和m_id
Employee(name, id); // 这不会按预期工作!
}
const std::string& getName() const { return m_name; }
};
int main()
{
Employee e2{ "Dave", 42, true };
std::cout << "e2 has name: " << e2.getName() << "\n"; // 打印e2.m_name
}
但这并不能正确工作,因为程序输出如下:
Employee Dave created
e2 has name: ???
尽管打印出了Employee Dave created
,但在e2
完成构造后,e2.m_name
似乎仍然被设置为其初始值"???"
。这是怎么做到的?
我们期望Employee(name, id)
调用构造函数来继续初始化当前隐式对象(e2
)。但类对象的初始化在成员初始化列表执行完毕后就完成了。等到我们开始执行构造函数的主体时,已经太晚了,无法再进行更多的初始化。
从函数体中调用时,看似对构造函数的函数调用通常会创建并直接初始化一个临时对象(在另一种情况下,你会得到一个编译错误)。在我们上面的例子中,Employee(name, id);
创建了一个临时(无名的)Employee
对象。这个临时对象的m_name
被设置为"Dave"
,并且是它打印了Employee Dave created
。然后临时对象被销毁。e2
的m_name
或m_id
从未从默认值中改变。
最佳实践:不应从另一个函数的主体中直接调用构造函数。这样做要么导致编译错误,要么直接初始化一个临时对象。
如果你确实想要一个临时对象,优先使用列表初始化(这清楚地表明你打算创建一个对象)。
委托构造函数
构造函数允许将初始化委托(转移初始化的责任)给同一类类型的另一个构造函数。这个过程有时被称为构造函数链,这样的构造函数被称为委托构造函数。
要让一个构造函数将初始化委托给另一个构造函数,只需在成员初始化列表中调用该构造函数:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
private:
std::string m_name { "???" };
int m_id { 0 };
public:
Employee(std::string_view name)
: Employee{ name, 0 } // 将初始化委托给Employee(std::string_view, int)构造函数
{
}
Employee(std::string_view name, int id)
: m_name{ name }, m_id { id } // 实际初始化成员
{
std::cout << "Employee " << m_name << " created\n";
}
};
int main()
{
Employee e1{ "James" };
Employee e2{ "Dave", 42 };
}
当e1 { "James" }
被初始化时,匹配的构造函数Employee(std::string_view)
被调用,参数name
被设置为"James"
。这个构造函数的成员初始化列表将初始化委托给另一个构造函数,因此Employee(std::string_view, int)
随后被调用。name
的值("James"
)作为第一个参数传递,字面量0
作为第二个参数传递。委托构造函数的成员初始化列表随后初始化成员。委托构造函数的主体随后运行。然后控制返回到初始构造函数,其(空的)主体运行。最后,控制返回到调用者。
这种方法的缺点是有时需要重复初始化值。在委托给Employee(std::string_view, int)
构造函数时,我们需要一个int
参数的初始化值。我们不得不硬编码字面量0
,因为没有方法可以引用默认成员初始化器。
关于委托构造函数,还有几点需要注意。首先,委托给另一个构造函数的构造函数不允许自己进行任何成员初始化。因此,你的构造函数可以委托或初始化,但不能同时做这两件事。
顺便说一下:注意我们让Employee(std::string_view)
(参数较少的构造函数)委托给Employee(std::string_view name, int id)
(参数较多的构造函数)。通常,参数较少的构造函数会委托给参数较多的构造函数。
如果我们选择让Employee(std::string_view name, int id)
委托给Employee(std::string_view)
,那么我们将无法使用id
初始化m_id
,因为构造函数