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(),它会打印:

image


值初始化与默认初始化在类类型中的区别

若类类型具有默认构造函数,则值初始化与默认初始化均会调用该默认构造函数。因此,对于如上例中的 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;
}

这会输出:

image

如果构造函数中的所有参数都具有默认值,则该构造函数为默认构造函数(因为它可以不带参数地被调用)。

我们将在下一课(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;
}

image

由此可知,类中应仅包含一个默认构造函数。若提供多个默认构造函数,编译器将无法确定应使用哪个构造函数:

#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;
}

image

在上例中,我们未传入参数就实例化了 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;
}

image

该类没有用户声明的构造函数,因此编译器将为我们生成一个隐式默认构造函数。该构造函数将用于实例化 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;
}

image

在上例中,由于存在用户声明的构造函数(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;
}

在作者的机器上,这段代码输出:

image

请注意,在默认初始化之前,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;
}

image

对于表示分数的类,允许用户通过无初始化构造函数创建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;
}

image

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

posted @ 2025-12-29 16:16  游翔  阅读(18)  评论(0)    收藏  举报