当类类型是聚合体时,我们可以使用聚合初始化直接初始化该类类型:
struct Foo // Foo是一个聚合体
{
int x {};
int y {};
};
int main()
{
Foo foo { 6, 7 }; // 使用聚合初始化
return 0;
}
聚合初始化会逐成员进行初始化(成员按照它们被定义的顺序进行初始化)。因此,在上述示例中,当foo
被实例化时,foo.x
被初始化为6,foo.y
被初始化为7。
相关内容:我们在第13.8课——结构体聚合初始化中讨论了聚合体的定义和聚合初始化。
然而,一旦我们将任何成员变量声明为私有(以隐藏数据),我们的类类型就不再是一个聚合体(因为聚合体不能有私有成员)。这意味着我们不能再使用聚合初始化:
class Foo // Foo不是一个聚合体(有私有成员)
{
int m_x {};
int m_y {};
};
int main()
{
Foo foo { 6, 7 }; // 编译错误:不能使用聚合初始化
return 0;
}
不允许具有私有成员的类类型通过聚合初始化进行初始化是有道理的,原因如下:
- 聚合初始化需要了解类的实现(因为你需要知道成员是什么,以及它们的定义顺序),而我们隐藏数据成员时正是想避免这种情况。
- 如果我们的类有某种不变量,我们将依赖用户以一种保持不变量的方式初始化类。
那么,我们如何初始化具有私有成员变量的类呢?编译器为前面的示例给出的错误消息提供了一个线索:“错误:没有匹配的构造函数用于初始化‘Foo’”。
我们需要一个匹配的构造函数。但构造函数到底是什么?
构造函数
构造函数是一种特殊的成员函数,它在非聚合类类型对象被创建后自动被调用。
当定义一个非聚合类类型对象时,编译器会查找是否能找到一个与调用者提供的初始化值匹配的可访问构造函数(如果有)。
- 如果找到一个可访问的匹配构造函数,对象的内存将被分配,然后调用构造函数。
- 如果找不到可访问的匹配构造函数,将生成一个编译错误。
关键洞察:许多新手程序员对构造函数是否创建对象感到困惑。它们并不创建对象——编译器在调用构造函数之前为对象设置内存分配。然后在未初始化的对象上调用构造函数。
然而,如果找不到一组初始化器的匹配构造函数,编译器将报错。因此,尽管构造函数不创建对象,但缺少匹配的构造函数将阻止对象的创建。
除了确定对象的创建方式外,构造函数通常执行以下两个功能:
- 它们通常初始化任何成员变量(通过成员初始化列表)。
- 它们可以执行其他设置功能(通过构造函数体内的语句)。这可能包括对初始化值进行错误检查、打开文件或数据库等。
构造函数执行完毕后,我们说对象已经被“构造”了,对象现在应该处于一个一致的、可用的状态。
注意,聚合体不允许有构造函数——因此,如果你给聚合体添加了一个构造函数,它就不再是一个聚合体了。
构造函数的命名
与普通成员函数不同,构造函数在命名上有特定的规则:
- 构造函数必须与类同名(大小写相同)。对于模板类,这个名称不包括模板参数。
- 构造函数没有返回类型(甚至不是
void
)。 - 由于构造函数通常是类接口的一部分,它们通常是公共的。
基本构造函数示例
让我们在上面的示例中添加一个基本的构造函数:
#include <iostream>
class Foo
{
private:
int m_x {};
int m_y {};
public:
Foo(int x, int y) // 这是我们的构造函数,它接受两个初始化器
{
std::cout << "Foo(" << x << ", " << y << ") constructed\n";
}
void print() const
{
std::cout << "Foo(" << m_x << ", " << m_y << ")\n";
}
};
int main()
{
Foo foo{ 6, 7 }; // 调用Foo(int, int)构造函数
foo.print();
return 0;
}
该程序现在可以编译并产生以下结果:
Foo(6, 7) constructed
Foo(0, 0)
当编译器看到定义Foo foo{ 6, 7 }
时,它会查找一个匹配的Foo
构造函数,该构造函数可以接受两个int
参数。Foo(int, int)
是一个匹配项,因此编译器将允许该定义。
在运行时,当foo
被实例化时,会为foo
分配内存,然后调用Foo(int, int)
构造函数,参数x
被初始化为6,参数y
被初始化为7。然后构造函数体被执行并打印Foo(6, 7) constructed
。
当我们调用print()
成员函数时,你会注意到成员m_x
和m_y
的值为0。这是因为尽管我们的Foo(int, int)
构造函数被调用了,但它实际上并没有初始化成员。我们将在下一课中展示如何做到这一点。
相关内容:我们在第14.15课——类初始化和拷贝消除中讨论了使用拷贝、直接和列表初始化来初始化带有构造函数的对象之间的差异。
构造函数的参数隐式转换
在第10.1课——隐式类型转换中,我们指出编译器会在函数调用中对参数进行隐式转换(如果需要),以匹配参数类型不同的函数定义:
void foo(int, int)
{
}
int main()
{
foo('a', true); // 将匹配foo(int, int)
return 0;
}
对于构造函数来说,情况并无不同:Foo(int, int)
构造函数将匹配任何其参数可以隐式转换为int
的调用:
class Foo
{
public:
Foo(int x, int y)
{
}
};
int main()
{
Foo foo{ 'a', true }; // 将匹配Foo(int, int)构造函数
return 0;
}
构造函数不应为const
构造函数需要能够初始化正在构造的对象——因此,构造函数不能是const
。
#include <iostream>
class Something
{
private:
int m_x{};
public:
Something() // 构造函数必须是非const的
{
m_x = 5; // 在非const构造函数中修改成员是允许的
}
int getX() const { return m_x; } // const
};
int main()
{
const Something s{}; // const对象,隐式调用(非const)构造函数
std::cout << s.getX(); // 输出5
return 0;
}
通常,非const成员函数不能在const对象上调用。然而,C++标准明确指出(根据class.ctor.general#5),const不适用于正在构造的对象,只有在构造函数结束后才生效。
构造函数与设置器
构造函数旨在在实例化时初始化整个对象。设置器旨在为现有对象的单个成员赋值。