14-9 构造函数介绍

当类类型是聚合体时,我们可以使用聚合初始化来直接初始化该类类型:

struct Foo // Foo is an aggregate
{
    int x {};
    int y {};
};

int main()
{
    Foo foo { 6, 7 }; // uses aggregate initialization

    return 0;
}

集合初始化采用成员逐个初始化(成员按定义顺序初始化)。因此在上例中,当 foo 被实例化时,foo.x 被初始化为 6,foo.y 被初始化为 7。

相关内容:
我们在第13.8节——结构体聚合初始化中讨论了聚合的定义及其初始化方式。

然而,一旦我们将任何成员变量设为私有(以隐藏数据),该类类型便不再属于聚合(因为聚合不能包含私有成员)。这意味着我们无法再使用聚合初始化:

class Foo // Foo is not an aggregate (has private members)
{
    int m_x {};
    int m_y {};
};

int main()
{
    Foo foo { 6, 7 }; // compile error: can not use aggregate initialization

    return 0;
}

禁止通过聚合初始化来初始化具有私有成员的类类型,其合理性体现在以下几个方面:

聚合初始化需要了解类的实现细节(因为必须知道成员的具体内容及其定义顺序),而隐藏数据成员正是为了刻意规避这种依赖。

若类包含某种不变量,则需依赖用户以保持不变量的方式初始化类。

那么如何初始化具有私有成员变量的类?编译器对前例给出的错误信息提供了线索:“error: no matching constructor for initialization of ‘Foo’”

显然需要匹配构造函数。但这究竟是什么?


构造函数

构造函数constructor是一种特殊成员函数,在非聚合类型的对象创建后会自动调用。

当定义非聚合类型的对象时,编译器会检查是否存在可访问的构造函数,且该构造函数能匹配调用方提供的初始化值(若有)。

若找到匹配的可访问构造函数,则为对象分配内存,随后调用构造函数。

若未找到可访问的匹配构造函数,则会产生编译错误。

关键要点:
许多新手程序员常困惑于构造函数是否创建对象。其实不然——编译器在调用构造函数前已完成对象内存分配,构造函数随后对未初始化的对象进行调用。
但若无法为初始化表达式找到匹配构造函数,编译器将报错。因此构造函数虽不直接创建对象,但缺乏匹配构造函数将阻碍对象生成。

除决定对象生成方式外,构造函数通常承担两项功能:

• 通过成员初始化列表完成成员变量初始化

• 通过构造函数主体中的语句执行其他设置操作,例如初始化值错误检查、文件或数据库打开等

构造函数执行完毕后,该对象即被视为“构造完成”,此时对象应处于一致且可用的状态。

需注意聚合体不允许拥有构造函数——若为聚合体添加构造函数,则该对象将失去聚合体属性。


命名构造函数

与普通成员函数不同,构造函数的命名有特定规则:

  • 构造函数必须与类同名(大小写一致)。对于模板类,此名称不包含模板参数。
  • 构造函数没有返回类型(甚至不能是 void)。

由于构造函数通常是类接口的一部分,它们通常为 public。


一个基本构造函数示例

让我们在上面的示例中添加一个基本构造函数:

#include <iostream>

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo(int x, int y) // here's our constructor function that takes two initializers
    {
        std::cout << "Foo(" << x << ", " << y << ") constructed\n";
    }

    void print() const
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ")\n";
    }
};

int main()
{
    Foo foo{ 6, 7 }; // calls Foo(int, int) constructor
    foo.print();

    return 0;
}

该程序现在将进行编译并输出结果:

image

当编译器看到定义 Foo foo{ 6, 7 } 时,它会寻找匹配的 Foo 构造函数,该构造函数需接受两个 int 参数。Foo(int, int) 符合条件,因此编译器将允许该定义。

运行时实例化 foo 时,会为其分配内存,随后调用 Foo(int, int) 构造函数,其中参数 x 初始化为 6,参数 y 初始化为 7。构造函数主体执行后会输出 Foo(6, 7) 已构造完成。

当调用 print() 成员函数时,你会发现成员 m_x 和 m_y 的值均为 0。这是因为虽然调用了 Foo(int, int) 构造函数,但并未实际初始化成员变量。具体实现方法将在下一课中说明。

相关内容:
第 14.15 课--类初始化与复制省略 将探讨使用复制初始化、直接初始化和列表初始化通过构造函数初始化对象的区别。


构造函数参数的隐式转换

在第10.1节——隐式类型转换中,我们提到编译器会在函数调用中对参数进行隐式转换(如有必要),以匹配参数类型不同的函数定义:

void foo(int, int)
{
}

int main()
{
    foo('a', true); // will match foo(int, int)

    return 0;
}

构造函数也是如此:Foo(int, int)构造函数将匹配任何参数可隐式转换为int的调用:

class Foo
{
public:
    Foo(int x, int y)
    {
    }
};

int main()
{
    Foo foo{ 'a', true }; // will match Foo(int, int) constructor

    return 0;
}

构造函数不应为 const

构造函数需要能够初始化正在构造的对象——因此,构造函数必须不是 const。

#include <iostream>

class Something
{
private:
    int m_x{};

public:
    Something() // constructors must be non-const
    {
        m_x = 5; // okay to modify members in non-const constructor
    }

    int getX() const { return m_x; } // const
};

int main()
{
    const Something s{}; // const object, implicitly invokes (non-const) constructor

    std::cout << s.getX(); // prints 5

    return 0;
}

image

通常情况下,非 const 成员函数不能在 const 对象上调用。然而,C++ 标准明确规定(参见 class.ctor.general#5),const 不适用于正在构造的对象,仅在构造函数结束之后才生效。


构造函数与设置器

构造函数旨在实例化时初始化整个对象。设置器则用于为现有对象的单个成员赋值。

posted @ 2025-12-28 21:15  游翔  阅读(12)  评论(0)    收藏  举报