17.C++11新特性之列表初始化

        在 C++11 之前,初始化对象的方式多种多样,不同的类型和场景可能需要不同的初始化语法,这使得代码的一致性和可读性受到影响。例如,数组可以使用花括号初始化,而对于类对象,可能需要使用构造函数调用的方式进行初始化。C++11 引入了列表初始化(也称为统一初始化),提供了一种统一的初始化语法,能够应用于各种类型和场景。

1.列表初始化语法

        列表初始化使用花括号 {} 来进行初始化,其基本语法如下:

// 初始化基本类型
int num{10};
double d{3.14};

// 初始化数组
int arr[]{1, 2, 3, 4, 5};

// 初始化类对象
class MyClass {
public:
    MyClass(int value) : data(value) {}
private:
    int data;
};
MyClass obj{20};

2. 列表初始化的优点

2.1 统一的初始化语法

        列表初始化提供了一种统一的方式来初始化各种类型的对象,无论是基本类型、数组、类对象还是容器等,都可以使用花括号进行初始化,提高了代码的一致性和可读性。例如:

#include <vector>
#include <iostream>

int main() {
    // 初始化基本类型
    int x{5};
    // 初始化数组
    int arr[]{1, 2, 3};
    // 初始化 std::vector
    std::vector<int> vec{4, 5, 6};

    std::cout << "x: " << x << std::endl;
    for (int num : arr) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    for (int num : vec) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}
2.2 防止缩窄转换

        列表初始化会禁止缩窄转换,即不允许将一个可能导致数据丢失的大类型值转换为小类型。例如:

// 错误,缩窄转换
// int num{3.14}; 
// char c{128}; // 假设 char 是 8 位有符号类型,128 超出范围

// 正确
int num{3};
char c{65};

        在上面的代码中,试图将 3.14 赋值给 int 类型的 num 以及将 128 赋值给 char 类型的 c 都会导致编译错误,因为这是缩窄转换。而将 3 赋值给 num 和将 65 赋值给 c 是允许的,因为不会发生数据丢失。

2.3 初始化聚合类型

        在C++11中,列表初始化的使用范围被大大增强了,但是一些模糊的概念也随之而来,在前面的例子可以得知,列表初始化可以用于自定义类型的初始化,但是对于一个自定义类型,列表初始化可能有两种执行结果:

#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    int y;
}a = { 123, 321 };

struct T2
{
    int x;
    int y;
    T2(int, int) : x(10), y(20) {}
}b = { 123, 321 };

int main(void)
{
    cout << "a.x: " << a.x << ", a.y: " << a.y << endl;
    cout << "b.x: " << b.x << ", b.y: " << b.y << endl;
    return 0;
}

        程序执行的结果是这样的:

a.x: 123, a.y: 321
b.x: 10, b.y: 20

         在上边的程序中都是用列表初始化的方式对对象进行了初始化,但是得到结果却不同,对象b并没有被初始化列表中的数据初始化,这是为什么呢?

  • 对象a是对一个自定义的聚合类型进行初始化,它将以拷贝的形式使用初始化列表中的数据来初始化T1结构体中的成员。
  • 在结构体T2中自定义了一个构造函数,因此实际的初始化是通过这个构造函数完成的。

        同样是自定义结构体并且在创建对象的时候都使用了列表初始化来初始化对象,为什么在类内部对对象的初始化方式却不一样呢?因为如果使用列表初始化对对象初始化时,还需要判断这个对象对应的类型是不是一个聚合体,如果是初始化列表中的数据就会拷贝到对象中

        那么,使用列表初始化时,对于什么样的类型C++会认为它是一个聚合体呢?

  • 普通数组本身可以看做是一个聚合类型
  • 在 C++11 到 C++17 标准中,满足以下条件的类(class、struct、union)可以被看做是一个聚合类型:
  1. 没有用户提供的构造函数:即类中不能有用户显式定义的构造函数。如果定义了构造函数,编译器就不会认为它是聚合类型。
  2. 没有私有或受保护的非静态数据成员:类的所有非静态数据成员都必须是公有的。如果存在私有或受保护的非静态数据成员,该类就不是聚合类型。
  3. 没有基类:类不能有任何基类,即不能从其他类派生而来。若有基类,它就不符合聚合类型的条件。
  4. 没有虚函数:类中不能包含虚函数。虚函数的存在会破坏聚合类型的简单性,因此有虚函数的类不是聚合类型。
2.4 初始化非聚合类型

        对于聚合类型的类可以直接使用列表初始化进行对象的初始化,如果不满足聚合条件还想使用列表初始化其实也是可以的,需要在类的内部自定义一个构造函数, 在构造函数中使用初始化列表对类成员变量进行初始化

#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    double y;
    // 在构造函数中使用初始化列表初始化类成员
    T1(int a, double b, int c) : x(a), y(b), z(c){}
    virtual void print()
    {
        cout << "x: " << x << ", y: " << y << ", z: " << z << endl;
    }
private:
    int z;
};

int main(void)
{
    T1 t{ 520, 13.14, 1314 };	// ok, 基于构造函数使用初始化列表初始化类成员
    t.print();
    return 0;
}

3. 列表初始化与构造函数

3.1 匹配构造函数

        当使用列表初始化类对象时,编译器会尝试匹配合适的构造函数。例如:

#include <iostream>

class MyClass {
public:
    MyClass(int value) : data(value) {
        std::cout << "Single argument constructor" << std::endl;
    }
    MyClass(int a, int b) : data(a + b) {
        std::cout << "Two argument constructor" << std::endl;
    }
private:
    int data;
};

int main() {
    MyClass obj1{5};  // 调用单参数构造函数
    MyClass obj2{3, 4}; // 调用双参数构造函数
    return 0;
}
3.2 初始化列表构造函数

        C++11 还引入了初始化列表构造函数,它使用 std::initializer_list 作为参数。std::initializer_list 是一个轻量级的容器,用于表示一个常量值序列。这里简要介绍一下它的特点:

  • 内部定义了迭代器iterator等容器必须的概念,遍历时得到的迭代器是只读的。
  • 对于std::initializer_list<T>而言,它可以接收任意长度的初始化列表,但是要求元素必须是同种类型T。
  • 在std::initializer_list内部有三个成员接口:size(), begin(), end()。
  • std::initializer_list对象只能被整体初始化或者赋值。

        当使用列表初始化时,如果存在初始化列表构造函数,它会具有更高的优先级。例如:

#include <iostream>
#include <initializer_list>

class MyList {
public:
    MyList(std::initializer_list<int> list) {
        for (int num : list) {
            std::cout << num << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    MyList list{1, 2, 3, 4, 5};
    return 0;
}

        在这个例子中,MyList 类有一个初始化列表构造函数,它接受一个 std::initializer_list<int> 类型的参数。当使用 MyList list{1, 2, 3, 4, 5}; 进行列表初始化时,会调用这个初始化列表构造函数。

4. 列表初始化的局限性

  • 与旧代码的兼容性:虽然列表初始化提供了统一的初始化语法,但在一些旧的代码库中,可能仍然使用传统的初始化方式,需要注意代码的兼容性。
  • 复杂类型的初始化:对于一些复杂的类型,特别是具有多个构造函数和初始化方式的类型,列表初始化可能会导致一些混淆,需要仔细考虑构造函数的匹配规则。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值