构造函数与成员初始化器列表

来自cppreference.com
< cpp‎ | language

构造函数是类的一种特殊的非静态成员函数,用于初始化该类类型的对象。

在类的构造函数定义中,成员初始化器列表指定各个直接基类、虚基类和非静态数据成员的初始化器。 (请勿与 std::initializer_list 混淆)

构造函数不能是协程

(C++20 起)

构造函数不能有显式对象形参

(C++23 起)

语法

构造函数用以下形式的成员函数声明符声明:

类名 ( 形参列表(可选) ) 异常说明(可选) 属性(可选) (1)

其中 类名 必须指名当前类(或类模板的当前实例化),或当在命名空间作用域或在友元声明中声明时,它必须是有限定的类名。

构造函数声明的 声明说明符序列 中只允许说明符 friendinlineconstexpr (C++11 起)consteval (C++20 起)explicit(尤其是不允许返回类型)。注意 cv 及引用限定符也不受允许;const 与 volatile 语义对于构造过程中的对象没有效果,它们要到最终派生类的构造函数完成才会生效。

任何构造函数的函数定义的函数体可以在复合语句的开花括号之前包含成员初始化器列表,其语法是冒号字符 : 后随一个或多个 成员初始化器 的逗号分隔列表,每项均具有以下语法:

类或标识符 ( 表达式列表(可选) ) (1)
类或标识符 花括号初始化列表 (2) (C++11 起)
形参包 ... (3) (C++11 起)
1)直接初始化,或当 表达式列表 为空时用值初始化,初始化 类或标识符 所指名的基类或成员
2)列表初始化(列表为空时进行值初始化,而在初始化聚合体时进行聚合初始化),初始化 类或标识符 所指名的基类或成员
3)包展开初始化多个基类
类或标识符 - 任何指名非静态数据成员的标识符,或任何指名该类自身(对于委托构造函数)、直接基类或虚基类的类型名。
表达式列表 - 传递给基类或成员的参数的逗号分隔列表,可以为空
花括号初始化列表 - 花括号包围的以逗号分隔的初始化器和嵌套的花括号初始化列表的列表
形参包 - 变参模板形参包的名字
struct S
{
    int n;
 
    S(int);       // 构造函数声明
 
    S() : n(7) {} // 构造函数定义:
                  // ": n(7)" 是初始化器列表
                  // ": n(7) {}" 是函数体
};
 
S::S(int x) : n{x} {} // 构造函数定义:": n{x}" 是初始化器列表
 
int main()
{
    S s;      // 调用 S::S()
    S s2(10); // 调用 S::S(int)
}

解释

构造函数没有名字且无法被直接调用。它们在发生初始化时被调用,且它们按照初始化的规则进行选择。没有 explicit 说明符的构造函数是转换构造函数。有 constexpr 说明符的构造函数会让其类型成为字面类型 (LiteralType) 。可以不带任何实参调用的构造函数是默认构造函数。可以接收同类型的另一对象为实参的构造函数是复制构造函数移动构造函数

在开始执行组成构造函数体的复合语句之前,所有直接基类、虚基类和非静态数据成员的初始化均已结束。这些对象的非默认初始化只能在成员初始化器列表指定。对于不能默认初始化的基类或非静态数据成员,例如引用和 const 限定的类型的成员,必须指定成员初始化器。对没有成员初始化器的匿名联合体变体成员不进行初始化。

类或标识符 指名虚基类的初始化器,在并非所构造对象的最终派生类的类的构造期间被忽略。

表达式列表花括号初始化列表 中出现的名字在构造函数的作用域中求值:

class X
{
    int a, b, i, j;
public:
    const int& r;
    X(int i)
      : r(a) // 初始化 X::r 以指代 X::a
      , b{i} // 初始化 X::b 为形参 i 的值
      , i(i) // 初始化 X::i 为形参 i 的值
      , j(this->i) // 初始化 X::j 为 X::i 的值
    {}
};

成员初始化器所抛出的异常可以被函数 try 块处理。

成员函数(包括虚成员函数)可以从成员初始化器调用,但如果在该点还有直接基类尚未被初始化,那么行为未定义。

对于虚调用(如果在该点已初始化直接基类),适用与从构造函数与析构函数中进行虚函数调用相同的规则:虚成员函数表现如同 *this 的动态类型是正在构造的类的静态类型(动态派发不在继承层级下传),而对纯虚成员函数的虚调用(但非静态调用)是未定义行为。

如果非静态数据成员具有 默认成员初始化器且也在成员初始化器列表中出现,那么使用该成员初始化器而忽略默认成员初始化器:

struct S
{
    int n = 42;   // 默认成员初始化器
    S() : n(7) {} // 将设置 n 为 7,而非 42
};
(C++11 起)

引用成员不能绑定到成员初始化器列表中的临时量:

struct A
{
    A() : v(42) {} // 错误
    const int& v;
};

注:这同样适用于默认成员初始化器

委托构造函数

如果类自身的名字在初始化器列表中作为 类或标识符 出现,那么该列表只能由这一个成员初始化器组成;这种构造函数被称为委托构造函数(delegating constructor),而构造函数列表的仅有成员所选择的构造函数是目标构造函数

此时首先由重载决议选择目标构造函数并予以执行,然后控制返回到委托构造函数并执行其函数体。

委托构造函数不能递归。

class Foo
{
public: 
    Foo(char x, int y) {}
    Foo(int y) : Foo('a', y) {} // Foo(int) 委托到 Foo(char, int)
};

继承的构造函数

using 声明

(C++11 起)

初始化顺序

列表中的成员初始化器的顺序是不相关的:初始化的实际顺序如下:

1) 如果构造函数是最终派生类的,那么按基类声明的深度优先、从左到右的遍历中的出现顺序(从左到右指的是基说明符列表中所呈现的),初始化各个虚基类
2) 然后,以在此类的基类说明符列表中出现的从左到右顺序,初始化各个直接基类
3) 然后,以类定义中的声明顺序,初始化各个非静态成员。
4) 最后,执行构造函数体

(注意:如果初始化的顺序是由不同构造函数中的成员初始化器列表中的出现所控制,那么析构函数就无法确保销毁顺序是构造顺序的逆序了)

示例

#include <fstream>
#include <string>
#include <mutex>
 
struct Base
{
    int n;
};   
 
struct Class : public Base
{
    unsigned char x;
    unsigned char y;
    std::mutex m;
    std::lock_guard<std::mutex> lg;
    std::fstream f;
    std::string s;
 
    Class(int x) : Base{123}, // 初始化基类
        x (x),  // x(成员)以 x(形参)初始化
        y {0},  // y 初始化为 0
        f{"test.cc", std::ios::app}, // 在 m 和 lg 初始化之后发生
        s(__func__), //__func__ 可用,因为初始化器列表是构造函数的一部分
        lg (m), // lg 使用已经初始化的 m
        m{}     // m 在 lg 前初始化,即使它最后出现在此处
    {}          // 空复合语句
 
    Class(double a) : y(a + 1),
        x(y), // x 将在 y 前初始化,它的值不确定
        lg(m)
    {} // 基类初始化器未在列表中出现,它被默认初始化(这与使用 Base() 不同,那是值初始化)
 
    Class()
    try // 函数 try 块在包含初始化器列表的函数体之前开始
      : Class(0.0) // 委托构造函数
    {
        // ...
    }
    catch (...)
    {
        // 初始化中发生的异常
    }
};
 
int main()
{
    Class c;
    Class c1(1);
    Class c2(0.1);
}

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 194 C++98 构造函数的声明器语法最多只能有一个函数说明符
(例如无法声明构造函数为 inline explicit
可以有多个函数说明符
CWG 257 C++98 未指明抽象类是否需要为它的虚基类提供成员初始化器 指明为不需要,并且在执行过程中忽略它们
CWG 263 C++98 构造函数的声明器语法导致构造函数不能是友元 构造函数可以是友元
CWG 1345 C++98 没有默认成员初始化器的匿名联合体成员会被默认初始化 不会进行初始化
CWG 1435 C++98 构造函数的声明器语法中“类名”的含义不明确 改为使用特殊的函数声明符器语法
CWG 1696 C++98 引用成员可被初始化为(生存期会在构造函数结尾结束的)临时量 这种初始化非良构

引用

  • C++20 标准(ISO/IEC 14882:2020):
  • 11.4.4 Constructors [class.ctor]
  • 11.10.2 Initializing bases and members [class.base.init]
  • C++17 标准(ISO/IEC 14882:2017):
  • 15.1 Constructors [class.ctor]
  • 15.6.2 Initializing bases and members [class.base.init]
  • C++14 标准(ISO/IEC 14882:2014):
  • 12.1 Constructors [class.ctor]
  • 12.6.2 Initializing bases and members [class.base.init]
  • C++11 标准(ISO/IEC 14882:2011):
  • 12.1 Constructors [class.ctor]
  • 12.6.2 Initializing bases and members [class.base.init]
  • C++98 标准(ISO/IEC 14882:1998):
  • 12.1 Constructors [class.ctor]
  • 12.6.2 Initializing bases and members [class.base.init]

参阅