代码仓库shanchuann/CPP-Learninng

统一初始化

传统圆括号初始化可能产生歧义,被编译器误解析为函数声明,而列表初始化可避免这一问题。

1
2
3
4
5
6
7
class Foo {};

// 传统方式的歧义:被解析为“返回Foo的无参函数声明”,而非对象初始化
Foo f1();

// 列表初始化:明确为对象初始化(调用默认构造函数)
Foo f2{};

类似地,对于带参数的场景:

1
2
3
4
5
6
7
8
9
10
class Bar {
public:
Bar(int x) {}
};

// 歧义:被解析为“返回Bar的函数,参数为int类型的x”
Bar b1(int x);

// 明确为对象初始化(调用Bar(int))
Bar b2{10};

初始化是指为变量赋予初始值

在C++中,初始化有多种方式,包括复制初始化和直接初始化。

  • 复制初始化(copy initialization):使用等号=进行初始化

  • 直接初始化(direct initialization):使用圆括号()或花括号{}进行初始化

1
2
3
4
5
6
7
8
int a = 1;		//(1) 复制初始化(copy initialization)
int b = { 2 }; //(2) 复制初始化(copy initialization)
int c(3); //(3) 直接初始化(direct initialization)
int d{ 4 }; //(4) 直接初始化(direct initialization)

int arr_copy[5] = { 1,2,3,4,5 }; //复制初始化(copy initialization)
int arr_direct1[5]{ 1,2,3 }; //直接初始化(direct initialization)
int arr_direct2[5]{ 1,2,3,4,5 }; //直接初始化(direct initialization)

C++ 初始化形式中的 (2) (4) 都属于列表初始化,在 C++11 中得到全面应用

初始化和赋值:初始化的等号和赋值的等号含义不同

  • 初始化:为变量申请存储空间,创建新的变量。如果是类类型,将调用类的构造函数
  • 赋值:把一个现有变量的值用另一个值替代,不创建新的变量。如果是类类型,将调用类的赋值运算符 operator=()
1
2
3
4
5
int a = 1; // 初始化 
a = 2; // 赋值
ClassType obj1; // 初始化,调用构造函数
ClassType obj2("Hello"); // 初始化,调用构造函数
obj1 = obj2; // 赋值
  • 列表初始化(list initialization):使用花括号{}进行初始化

列表初始化的底层支持

C++11 引入的std::initializer_list<T>是列表初始化的核心机制,它本质是一个轻量级容器,用于封装同类型元素的列表。当使用{}初始化时,编译器会自动将列表元素转换为std::initializer_list<T>对象(若元素类型一致)。

  • 类中使用std::initializer_list:若类定义了接受std::initializer_list<T>的构造函数,列表初始化会优先调用该构造函数,即使存在其他更匹配的构造函数。这是列表初始化的特殊优先级规则。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <initializer_list>
    #include <iostream>

    class MyClass {
    public:
    MyClass(int a, int b) {
    std::cout << "构造函数(int, int)\n";
    }
    MyClass(std::initializer_list<int> list) {
    std::cout << "构造函数(initializer_list)\n";
    }
    };

    int main() {
    MyClass obj1(1, 2); // 调用 (int, int)
    MyClass obj2{1, 2}; // 优先调用 initializer_list(即使参数数量匹配)
    MyClass obj3{1, 2, 3}; // 只能调用 initializer_list
    return 0;
    }

    std::initializer_list的特性

    • 元素为const,无法修改(如list[0] = 5;是错误的)。
    • 生命周期与初始化列表绑定,超出范围后失效。
    • 常用于容器初始化(如std::vectorstd::map),标准库容器均提供接受std::initializer_list的构造函数。

统一初始化特点

  • 统一语法:使用花括号{}进行初始化,适用于几乎所有类型的初始化。

  • 自动避免窄化转换:列表初始化禁止 “窄化转换”(可能导致数据丢失的转换):

    • 从浮点类型到整数类型(如doubleint)。
    • long doubledoublefloat(除非源是常量且值在目标范围內)。
    • 从整数类型到浮点类型(除非源是常量且值在目标范围內)。
    • 从大整数类型到小整数类型(如long longchar,且值超出小类型范围)。
    1
    2
    3
    4
    5
    long double num = 3.1415;
    //int a1{ num }; 错误 C2397 从“long double”转换到“int”需要收缩转换
    //int a2 = { num }; 错误 C2397 从“long double”转换到“int”需要收缩转换
    int b1 = num; // 警告 C4244 “初始化” : 从“long double”转换到“int”,可能丢失数据
    int b2(num); // 警告 C4244 “初始化” : 从“long double”转换到“int”,可能丢失数据
  • 适于所有类型:包括 POD(Plain Old Data,可以被直接使用memcpy进行复制的对象)类型、聚合类型、类类型等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //聚合类型(指没有用户自定义的构造函数、析构函数、拷贝构造函数和赋值运算符的类,包括数组、结构体等)初始化:
    struct A
    {
    int a;
    int b;
    };
    A a1 = { 1,2 }; //复制初始化, C++11引入的列表初始化
    A a2{ 3,4 }; //直接初始化,,比上一行初始化的方式更加通用

    //类类型初始化
    class MyClass {
    public:
    MyClass(int x, double y) : a(x), b(y) {}
    int a;
    double b;
    };

    MyClass obj1{ 5, 3.14 }; // 列表初始化
    std::string str{ "Hello, C plusplus!" }; // 列表初始化

    //容器和数组初始化
    std::vector<int> vec{ 1,2,3,4,5 }; // 列表初始化
    int arr[]{ 1,2,3,4,5 }; // 数组列表初始化

    聚合类型:

    • C++11:聚合类型需满足:无用户定义构造函数、无私有 / 保护非静态成员、无基类、无虚函数。
    • C++17:允许包含公共基类(基类也必须是聚合类型)。
    • C++20:进一步放松,允许存在用户声明但未定义的构造函数(即= default= delete的构造函数)。

    示例(C++17 及以上)

    1
    2
    3
    4
    struct Base { int x; };
    struct Derived : Base { int y; }; // 聚合类型(C++17允许公共基类)

    Derived d{ {1}, 2 }; // 正确:Base的x=1,Derived的y=2

C++17 指定初始化器(Designated Initializers)

C++17 引入了类似 C 语言的 “指定初始化器”,允许通过成员名称初始化聚合类型,顺序可与声明顺序不同。

1
2
3
4
struct Point { int x; int y; };

Point p1{.x = 1, .y = 2}; // 正确:x=1,y=2
Point p2{.y = 2, .x = 1}; // 正确:顺序无关(C++17支持)

注意:指定初始化器不能跳过前面的成员(如Point{.y=2}在 C++ 中错误,C 语言允许)。

构造函数与列表初始化的交互

explicit关键字用于禁止构造函数的隐式转换,但对列表初始化的影响需注意:

  • explicit构造函数不能用于复制初始化(如Bar b = 10;错误)。
  • 可以用于直接列表初始化(如Bar b{10};正确),因为列表初始化属于显式行为。
1
2
3
4
5
6
7
8
class Bar {
public:
explicit Bar(int x) {} // 声明为explicit
};

Bar b1 = 10; // 错误:explicit构造函数禁止复制初始化
Bar b2{10}; // 正确:直接列表初始化允许
Bar b3(10); // 正确:直接初始化允许