14-11 默认构造函数和默认参数
默认构造函数default constructor是不接受任何参数的构造函数。通常,这是一个不带参数定义的构造函数。
下面是一个具有默认构造函数的类的示例:
#include <iostream>
class Foo
{
public:
Foo() // default constructor
{
std::cout << "Foo default constructed\n";
}
};
int main()
{
Foo foo{}; // No initialization values, calls Foo's default constructor
return 0;
}
当上面的程序运行时,会创建一个 Foo 类型的对象。由于没有提供初始化值,因此调用默认构造函数 Foo(),它会打印:

值初始化与默认初始化在类类型中的区别
若类类型具有默认构造函数,则值初始化与默认初始化均会调用该默认构造函数。因此,对于如上例中的 Foo 类,以下两种写法本质上等效:
Foo foo{}; // value initialization, calls Foo() default constructor
Foo foo2; // default initialization, calls Foo() default constructor
然而,正如我们在第13.9节——默认成员初始化中所述,对于聚合类型而言,值初始化更为安全。由于难以判断类类型是否属于聚合类型,采用值初始化处理所有情况更为稳妥,无需为此担忧。
最佳实践:
对于所有类类型,优先采用值初始化而非默认初始化。
带默认参数的构造函数
与所有函数类似,构造函数最右侧的参数可以包含默认参数。
相关内容:
我们在第11.5节——默认参数中介绍了默认参数。
例如:
#include <iostream>
class Foo
{
private:
int m_x { };
int m_y { };
public:
Foo(int x=0, int y=0) // has default arguments
: m_x { x }
, m_y { y }
{
std::cout << "Foo(" << m_x << ", " << m_y << ") constructed\n";
}
};
int main()
{
Foo foo1{}; // calls Foo(int, int) constructor using default arguments
Foo foo2{6, 7}; // calls Foo(int, int) constructor
return 0;
}
这会输出:

如果构造函数中的所有参数都具有默认值,则该构造函数为默认构造函数(因为它可以不带参数地被调用)。
我们将在下一课(14.12——委托构造函数)中看到它在哪些方面有用的示例。
重载构造函数
由于构造函数是函数,因此可以重载。也就是说,我们可以定义多个构造函数,从而以不同方式构造对象:
#include <iostream>
class Foo
{
private:
int m_x {};
int m_y {};
public:
Foo() // default constructor
{
std::cout << "Foo constructed\n";
}
Foo(int x, int y) // non-default constructor
: m_x { x }, m_y { y }
{
std::cout << "Foo(" << m_x << ", " << m_y << ") constructed\n";
}
};
int main()
{
Foo foo1{}; // Calls Foo() constructor
Foo foo2{6, 7}; // Calls Foo(int, int) constructor
return 0;
}

由此可知,类中应仅包含一个默认构造函数。若提供多个默认构造函数,编译器将无法确定应使用哪个构造函数:
#include <iostream>
class Foo
{
private:
int m_x {};
int m_y {};
public:
Foo() // default constructor
{
std::cout << "Foo constructed\n";
}
Foo(int x=1, int y=2) // default constructor
: m_x { x }, m_y { y }
{
std::cout << "Foo(" << m_x << ", " << m_y << ") constructed\n";
}
};
int main()
{
Foo foo{}; // compile error: ambiguous constructor function call
return 0;
}

在上例中,我们未传入参数就实例化了 foo,因此编译器将寻找默认构造函数。它会找到两个构造函数,却无法确定应使用哪个构造函数。这将导致编译错误。
隐式默认构造函数
若非聚合类型的对象未声明构造函数,编译器将生成一个公共默认构造函数(以便该类可进行值初始化或默认初始化)。此构造函数称为隐式默认构造函数 implicit default constructor。
#include <iostream>
class Foo
{
private:
int m_x{};
int m_y{};
// Note: no constructors declared
};
int main()
{
Foo foo{};
return 0;
}

该类没有用户声明的构造函数,因此编译器将为我们生成一个隐式默认构造函数。该构造函数将用于实例化 foo{}。
隐式默认构造函数等同于一个没有参数、没有成员初始化列表、且构造函数主体中没有语句的构造函数。换言之,对于上述 Foo 类,编译器生成了以下内容:
public:
Foo() // implicitly generated default constructor
{
}
隐式默认构造函数主要在类没有数据成员时才有用。如果类包含数据成员,我们通常希望用户能为这些成员提供初始化值,而隐式默认构造函数无法满足这一需求。
使用 = default 生成显式默认构造函数
当需要编写与隐式生成默认构造函数等效的默认构造函数时,可直接指示编译器为我们生成默认构造函数。此构造函数称为显式默认构造函数,可通过 = default 语法生成:
#include <iostream>
class Foo
{
private:
int m_x {};
int m_y {};
public:
Foo() = default; // generates an explicitly defaulted default constructor
Foo(int x, int y)
: m_x { x }, m_y { y }
{
std::cout << "Foo(" << m_x << ", " << m_y << ") constructed\n";
}
};
int main()
{
Foo foo{}; // calls Foo() default constructor
return 0;
}

在上例中,由于存在用户声明的构造函数(Foo(int, int)),通常不会自动生成隐式默认构造函数。但由于我们已指示编译器生成此构造函数,它便会生成。该构造函数随后将被用于 foo{} 的实例化过程。
最佳实践:
应优先使用显式默认构造函数(即 default),而非空体默认构造函数。
显式默认构造函数VS用户定义空构造函数
至少存在两种情形下,显式默认构造函数的行为与用户定义空u s构造函数不同。
1.当对类进行值初始化时,若该类具有用户定义的默认构造函数,则对象将进行默认初始化。然而,若该类具有非用户提供的默认构造函数(即隐式定义的默认构造函数,或使用= default定义的默认构造函数),则对象在默认初始化前会先进行零初始化。
#include <iostream>
class User
{
private:
int m_a; // note: no default initialization value
int m_b {};
public:
User() {} // user-defined empty constructor
int a() const { return m_a; }
int b() const { return m_b; }
};
class Default
{
private:
int m_a; // note: no default initialization value
int m_b {};
public:
Default() = default; // explicitly defaulted default constructor
int a() const { return m_a; }
int b() const { return m_b; }
};
class Implicit
{
private:
int m_a; // note: no default initialization value
int m_b {};
public:
// implicit default constructor
int a() const { return m_a; }
int b() const { return m_b; }
};
int main()
{
User user{}; // default initialized
std::cout << user.a() << ' ' << user.b() << '\n';
Default def{}; // zero initialized, then default initialized
std::cout << def.a() << ' ' << def.b() << '\n';
Implicit imp{}; // zero initialized, then default initialized
std::cout << imp.a() << ' ' << imp.b() << '\n';
return 0;
}
在作者的机器上,这段代码输出:

请注意,在默认初始化之前,user.a并未被零初始化,因此处于未初始化状态。
实际应用中这通常无关紧要,因为您应当为所有数据成员提供默认成员初始化器!
提示:
对于未提供用户自定义默认构造函数的类,值初始化会先将类初始化为零,而默认初始化则不会。因此默认初始化可能比值初始化更高效(但安全性稍低)。若需在大量初始化无用户定义默认构造函数的对象时极致优化性能,可考虑将这些对象改为默认初始化。另一方案是为类添加空体默认构造函数,此举虽能避免值初始化时的零初始化操作,但可能抑制其他优化机制。
在C++20之前,具有用户定义默认构造函数的类(即使其主体为空)会被视为非聚合类,而显式默认构造函数则不会。假设该类原本属于聚合类,前者会导致其使用列表初始化而非聚合初始化。自C++20起,此不一致性已得到修正,两者均使类成为非聚合类。
仅在合理时创建默认构造函数
默认构造函数允许我们创建非聚合类型的对象,且无需用户提供初始化值。因此,当某类型的对象需要使用所有默认值创建时,该类才应提供默认构造函数。
例如:
#include <iostream>
class Fraction
{
private:
int m_numerator{ 0 };
int m_denominator{ 1 };
public:
Fraction() = default;
Fraction(int numerator, int denominator)
: m_numerator{ numerator }
, m_denominator{ denominator }
{
}
void print() const
{
std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
}
};
int main()
{
Fraction f1 {3, 5};
f1.print();
Fraction f2 {}; // will get Fraction 0/1
f2.print();
return 0;
}

对于表示分数的类,允许用户通过无初始化构造函数创建Fraction对象是合理的(此时用户将得到分数0/1)。
现在考虑这个类:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
private:
std::string m_name{ };
int m_id{ };
public:
Employee(std::string_view name, int id)
: m_name{ name }
, m_id{ id }
{
}
void print() const
{
std::cout << "Employee(" << m_name << ", " << m_id << ")\n";
}
};
int main()
{
Employee e1 { "Joe", 1 };
e1.print();
Employee e2 {}; // compile error: no matching constructor
e2.print();
return 0;
}

对于表示员工的类而言,允许创建无名的员工是没有意义的。因此,该类不应提供默认构造函数,这样当类的使用者尝试创建无名员工时,编译器就会报错。

浙公网安备 33010602011771号