默认构造函数是一个不接受任何参数的构造函数。通常,这是一个没有定义任何参数的构造函数。 以下是一个带有默认构造函数的类的示例:
#include <iostream>
class Foo
{
public:
Foo() // 默认构造函数
{
std::cout << "Foo默认构造\n";
}
};
int main()
{
Foo foo{}; // 没有初始化值,调用Foo的默认构造函数
return 0;
}
当上述程序运行时,会创建一个Foo
类型的对象。由于没有提供初始化值,将调用默认构造函数Foo()
,它打印出:
Foo默认构造
类类型的值初始化与默认初始化
如果一个类类型有一个默认构造函数,那么值初始化和默认初始化都将调用默认构造函数。因此,对于像上面示例中的Foo
这样的类,以下两种方式本质上是等价的:
Foo foo{}; // 值初始化,调用Foo()默认构造函数
Foo foo2; // 默认初始化,调用Foo()默认构造函数
然而,正如我们在第13.9课——默认成员初始化中已经讨论过的,对于聚合体,值初始化更安全。由于很难判断一个类类型是聚合体还是非聚合体,使用值初始化更安全,无需担心。
最佳实践:对于所有类类型,优先使用值初始化而不是默认初始化。
带有默认参数的构造函数
与所有函数一样,构造函数的最右参数可以有默认参数。
相关内容:我们在第11.5课——默认参数中讨论了默认参数。
例如:
#include <iostream>
class Foo
{
private:
int m_x {};
int m_y {};
public:
Foo(int x = 0, int y = 0) // 带有默认参数
: m_x { x }, m_y { y }
{
std::cout << "Foo(" << m_x << ", " << m_y << ")构造\n";
}
};
int main()
{
Foo foo1{}; // 使用默认参数调用Foo(int, int)构造函数
Foo foo2{6, 7}; // 调用Foo(int, int)构造函数
return 0;
}
该程序输出:
Foo(0, 0)构造
Foo(6, 7)构造
如果构造函数的所有参数都有默认参数,则该构造函数是一个默认构造函数(因为它可以不带任何参数被调用)。
我们将在下一课(14.12——委托构造函数)中看到这在哪些情况下是有用的。
重载构造函数
由于构造函数是函数,因此它们可以被重载。也就是说,我们可以有多个构造函数,以便以不同的方式构造对象:
#include <iostream>
class Foo
{
private:
int m_x {};
int m_y {};
public:
Foo() // 默认构造函数
{
std::cout << "Foo构造\n";
}
Foo(int x, int y) // 非默认构造函数
: m_x { x }, m_y { y }
{
std::cout << "Foo(" << m_x << ", " << m_y << ")构造\n";
}
};
int main()
{
Foo foo1{}; // 调用Foo()构造函数
Foo foo2{6, 7}; // 调用Foo(int, int)构造函数
return 0;
}
上述内容的一个推论是,一个类应该只有一个默认构造函数。如果提供了一个以上的默认构造函数,编译器将无法区分应该使用哪一个:
#include <iostream>
class Foo
{
private:
int m_x {};
int m_y {};
public:
Foo() // 默认构造函数
{
std::cout << "Foo构造\n";
}
Foo(int x = 1, int y = 2) // 默认构造函数
: m_x { x }, m_y { y }
{
std::cout << "Foo(" << m_x << ", " << m_y << ")构造\n";
}
};
int main()
{
Foo foo{}; // 编译错误:构造函数调用不明确
return 0;
}
在上述示例中,我们实例化foo
时没有提供任何参数,因此编译器将寻找默认构造函数。它会找到两个,并且无法区分应该使用哪一个。这将导致编译错误。
隐式默认构造函数
如果一个非聚合类类型对象没有用户声明的构造函数,编译器将生成一个公共默认构造函数(以便该类可以进行值初始化或默认初始化)。这个构造函数被称为隐式默认构造函数。
考虑以下示例:
#include <iostream>
class Foo
{
private:
int m_x{};
int m_y{};
// 注意:没有声明构造函数
};
int main()
{
Foo foo{};
return 0;
}
这个类没有用户声明的构造函数,因此编译器将为我们生成一个隐式默认构造函数。该构造函数将被用来实例化foo{}
。
隐式默认构造函数相当于一个没有参数、没有成员初始化列表且构造函数体中没有任何语句的构造函数。换句话说,对于上面的Foo
类,编译器生成了以下内容:
public:
Foo() // 隐式生成的默认构造函数
{
}
当类没有数据成员时,隐式默认构造函数最有用。如果一个类有数据成员,我们可能希望用用户提供的值来初始化它们,而隐式默认构造函数对于这一点是不够的。
使用= default
生成显式默认的默认构造函数
在某些情况下,我们可能会编写一个与隐式生成的默认构造函数等价的默认构造函数。在这种情况下,我们可以告诉编译器为我们生成一个默认构造函数。这个构造函数被称为显式默认的默认构造函数,可以通过= default
语法生成:
#include <iostream>
class Foo
{
private:
int m_x {};
int m_y {};
public:
Foo() = default; // 生成显式默认的默认构造函数
Foo(int x, int y)
: m_x { x }, m_y { y }
{
std::cout << "Foo(" << m_x << ", " << m_y << ")构造\n";
}
};
int main()
{
Foo foo{}; // 调用Foo()默认构造函数
return 0;
}
在上述示例中,由于我们有一个用户声明的构造函数(Foo(int, int)
),通常不会生成隐式默认构造函数。然而,因为我们告诉编译器生成这样的构造函数,它就会生成。随后,这个构造函数将被我们的foo{}
实例化所使用。
最佳实践:优先使用显式默认的默认构造函数(= default
)而不是带有空体的默认构造函数。
显式默认的默认构造函数与空的用户定义构造函数
至少有两种情况下,显式默认的默认构造函数的行为与空的用户定义构造函数不同。
当值初始化一个类时,如果该类有一个用户定义的默认构造函数,对象将被默认初始化。然而,如果该类有一个非用户提供的默认构造函数(即,隐式定义的默认构造函数,或者使用= default
定义的默认构造函数),在被默认初始化之前,对象将被零初始化。
#include <iostream>
class User
{
private:
int m_a; // 注意:没有默认初始化值
int m_b {};
public:
User() {} // 用户定义的空构造函数
int a() const { return m_a; }
int b() const { return m_b; }
};
class Default
{
private:
int m_a; // 注意:没有默认初始化值
int m_b {};
public:
Default() = default; // 显式默认的默认构造函数
int a() const { return m_a; }
int b() const { return m_b; }
};
class Implicit
{
private:
int m_a; // 注意:没有默认初始化值
int m_b {};
public:
// 隐式默认构造函数
int a() const { return m_a; }
int b() const { return m_b; }
};
int main()
{
User user{}; // 默认初始化
std::cout << user.a() << ' ' << user.b() << '\n';
Default def{}; // 零初始化,然后默认初始化
std::cout << def.a() << ' ' << def.b() << '\n';
Implicit imp{}; // 零初始化,然后默认初始化
std::cout << imp.a() << ' ' << imp.b() << '\n';
return 0;
}
在作者的机器上,这打印出:
782510864 0
0 0
0 0
注意