Bootstrap

C++11之列表初始化

《提前总结:一切皆可以使用{}进行初始化》

列表初始化

请好好看代码,理解代码,其实代码中的注释有很多精华的呢😍

C++98中传统的{}

C++98中,一般数组和结构体可以使用{}进行初始化:

struct Point
{
	int _x;
	int _y;
};
int main()
{
	int array1[] = { 1, 2, 3, 4, 5 };
	int array2[5] = { 0 };
	Point p = { 1, 2 };
	return 0;
}

C++11中的{}

在C++11之前,初始化对象的方式多种多样,这不仅使得代码难以统一,也增加了出错的可能性。C++11引入了列表初始化(也称为统一初始化),旨在简化和统一初始化方式,使得一切对象都可以通过花括号{}进行初始化。

  • 列表初始化的基本概念

列表初始化是C++11中的一项新特性,它允许我们使用花括号{}来初始化对象。这种方式不仅适用于内置类型,也适用于自定义类型。对于自定义类型,列表初始化本质上是一种类型转换,中间可能会产生临时对象,但编译器优化后,可以变成直接构造。

  • 列表初始化的优势

  1. 统一的初始化方式:C++11的列表初始化提供了一种统一的初始化方式,使得代码更加整洁和一致。
  2. 省略等号:在列表初始化中,我们可以省略掉等号=,直接使用花括号进行初始化。(但这只是省略,代码逻辑不变
  3. 方便容器操作:在容器中插入或push多参数构造的对象时,列表初始化提供了极大的便利。
  • 列表初始化的示例

下面是一个简单的示例,展示了如何在C++11中使用列表初始化:

#include<iostream>
#include<vector>

using namespace std;

// 定义一个简单的Point结构体,包含两个整数成员变量
struct Point {
    int _x; // x坐标
    int _y; // y坐标
};

// 定义一个Date类,包含年、月、日三个私有成员变量
class Date {
public:
    // 这是一个多参数的构造函数,提供了默认参数值
    Date(int year = 1, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day) {
        cout << "Date(int year, int month, int day)" << endl; // 构造时输出信息
    }
    // 拷贝构造函数,用于复制一个Date对象
    Date(const Date& d)
        : _year(d._year), _month(d._month), _day(d._day) {
        cout << "Date(const Date& d)" << endl; // 拷贝构造时输出信息
    }
private:
    int _year; // 年份
    int _month; // 月份
    int _day; // 日期
};

int main() {
    // C++98风格的数组初始化
    int a1[] = { 1, 2, 3, 4, 5 }; // 初始化一个整型数组
    int a2[5] = { 0 }; // 初始化一个整型数组,所有元素为0
    Point p = { 1, 2 }; // 初始化一个Point对象,_x为1,_y为2

    // C++11支持的列表初始化
    // 内置类型支持列表初始化
    int x1 = { 2 }; // 列表初始化一个整型变量x1,值为2

    // 自定义类型支持列表初始化
    // 使用列表初始化构造一个Date对象d1,年为2025,月为1,日为1
    Date d1 = { 2025, 1, 1 };
    // 使用列表初始化构造一个临时Date对象,并将其引用赋值给d2
    // 这里实际上调用了赋值拷贝构造函数,因为列表初始化创建了一个临时对象
    const Date& d2 = { 2024, 7, 25 };

    // 可以省略掉=的列表初始化
    Point p1{ 1, 2 }; 
    int x2{ 2 }; 
    Date d6{ 2024, 7, 25 }; 
    const Date& d7{ 2024, 7, 25 }; 

    vector<Date> v; // 定义一个Date类型的向量
    v.push_back(d1); 
    v.push_back(Date(2025, 1, 1)); // 将一个Date匿名对象添加到v中

    //比起有名对象和匿名对象传参,这里{}更有性价比
    // 使用列表初始化将一个Date对象添加到v中
    // 这里列表初始化创建了一个临时对象,然后调用拷贝构造函数将其添加到向量中
    v.push_back({ 2025, 1, 1 });

    return 0;
}
  • 代码中的优化和构造过程
  1. 多参数构造函数Date类的多参数构造函数允许使用默认参数值,这使得在不提供所有参数的情况下也能进行对象的构造。

  2. 拷贝构造函数Date类的拷贝构造函数允许通过复制现有对象来创建新对象。在const Date& d2 = {2024, 7, 25};这行代码中,由于列表初始化创建了一个临时对象,所以实际上调用了拷贝构造函数。

  3. 列表初始化与临时对象:在const Date& d2 = {2024, 7, 25};v.push_back({2025, 1, 1});这两处,列表初始化创建了临时对象,然后通过拷贝构造函数将这些临时对象复制到相应的变量或容器中。(因为临时对象具有常性,所以说Date&需要使用const修饰

  4. 省略等号的列表初始化:在Point p1 {1, 2};int x2 {2};Date d6 {2024, 7, 25};这几行代码中,我们可以看到可以省略等号的列表初始化,这是C++11提供的一个便利特性。

  5. 隐式类型转换:在Date d4 = 2025;这行代码中,我们可以看到单参数的隐式类型转换。由于Date类没有定义接受单个int参数的构造函数,这里会调用多参数构造函数,并使用默认值填充其他参数

C++中std::initializer_list

std::initializer_list的认识

std::initializer_list是C++11中引入的一个类模板,它提供了一种便捷的方式来初始化对象,特别是容器类对象std::initializer_list内部维护了两个指针,分别指向初始化列表的开始和结束。

std::initializer_list的特点

  • 内部结构std::initializer_list内部包含两个指针,分别指向数组的开始和结束
  • 迭代器支持std::initializer_list支持迭代器遍历,使得可以方便地访问列表中的元素。
  • 容器构造函数STL容器类增加了接受std::initializer_list作为参数的构造函数,支持使用花括号{}进行初始化。

示例代码:

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

int main() {
    // 创建一个int类型的std::initializer_list对象
    std::initializer_list<int> mylist = { 10, 20, 30 };
    cout << sizeof(mylist) << endl; // 输出sizeof(std::initializer_list<int>),通常为sizeof(void*)的两倍

    // 输出begin和end指针的地址,这两个指针指向列表的开始和结束
    // 由于列表存储在栈上,所以这两个指针的地址与局部变量i的地址相近
    int i = 0;
    cout << mylist.begin() << endl;
    cout << mylist.end() << endl;
    cout << &i << endl;

    // 使用std::initializer_list初始化vector对象
    vector<int> v1({ 1, 2, 3, 4, 5 }); // 直接构造
    vector<int> v2 = { 1, 2, 3, 4, 5 }; // 构造临时对象+临时对象拷贝v2+优化为直接构造
    const vector<int>& v3 = { 1, 2, 3, 4, 5 }; // 常量引用绑定到一个临时vector对象

    //去掉=:要注意:耶?还可以有一个有(),一个没有
    // vector<int>& v4 { 1, 2, 3, 4, 5 };//要注意,这只是省略掉了=,临时对象具有常性,引用对象要const修饰
    const vector<int>& v3 { 1, 2, 3, 4, 5 };
    vector<int> v5({ 1, 2, 3, 4, 5 }); // 直接构造

    // 使用pair对象的{}初始化和map的initializer_list构造结合
    map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} }; // 使用花括号初始化map

    // 使用initializer_list版本的赋值
    v1 = { 10, 20, 30, 40, 50 }; // 将v1赋值为新的initializer_list

    return 0;
}

std::initializer_list的应用

  1. 容器初始化std::initializer_list使得容器可以直接使用花括号{}进行初始化,如vector<int> v = {1, 2, 3};
  2. 简化代码:使用std::initializer_list可以简化代码,避免编写多个构造函数来支持不同数量的参数。
  3. 性能优化:编译器可以优化std::initializer_list的使用,避免不必要的临时对象拷贝。

🤔 在C++中,`std::initializer_list`是否只能用于容器类的初始化?

在C++中,std::initializer_list不仅限于容器类的初始化,尽管它在容器类中非常常见。std::initializer_list可以用于任何需要从初始化列表中创建对象的场景。以下是一些std::initializer_list的用途:

  1. 容器类初始化:如std::vectorstd::liststd::map等STL容器,都提供了接受std::initializer_list作为参数的构造函数和赋值操作符,使得可以直接使用花括号初始化容器。

  2. 自定义类的初始化自定义类也可以定义接受std::initializer_list的构造函数,从而允许使用花括号列表来初始化对象。

  3. 函数参数:函数可以接受`std::init`


  • 示例 1:自定义类使用 std::initializer_list

这个示例定义了一个 Matrix 类,它使用 std::initializer_list 来初始化一个 2x2 矩阵。

#include <iostream>
#include <initializer_list>
#include <array>

using namespace std;
class Matrix {
public:
    // 使用 std::initializer_list 列表初始化矩阵
    Matrix(initializer_list<initializer_list<double>> list) {
        // 检查是否为 2x2 矩阵
        if (list.size() != 2 || list.begin()->size() != 2) {
            throw std::invalid_argument("Matrix must be 2x2");
        }

        // 从初始化列表中复制值到矩阵
        auto it = list.begin();
        for (int i = 0; i < 2; ++i) {
            for (int j = 0; j < 2; ++j) {
                matrix[i][j] = *(it->begin() + j);
            }
            ++it;
        }
    }

    // 打印矩阵
    void print() const {
        for (const auto& row : matrix) {
            for (const auto& elem : row) {
                cout << elem << " ";
            }
            cout << endl;
        }
    }

private:
    array<array<double, 2>, 2> matrix; // 2x2 矩阵
};

int main() {
    // 使用列表初始化 Matrix 对象
    Matrix matrix = {
        {1.0, 2.0},
        {3.0, 4.0}
    };

    cout << "Matrix:" << endl;
    matrix.print(); // 输出矩阵

    return 0;
}
  • 示例 2:函数使用 std::initializer_list 作为参数

这个示例定义了一个函数,它接受 std::initializer_list<int> 作为参数,并计算所有元素的和。

#include <iostream>
#include <initializer_list>

// 计算初始化列表中所有元素的和
int sum(std::initializer_list<int> list) {
    int total = 0;
    for (int value : list) {
        total += value;
    }
    return total;
}

int main() {
    // 使用列表初始化并计算和
    int result = sum({1, 2, 3, 4, 5});
    std::cout << "Sum: " << result << std::endl; // 输出和

    return 0;
}

std::initializer_list是C++11中一个非常有用的工具,它提供了一种简洁且高效的方式来初始化对象,特别是容器类对象。通过使用std::initializer_list,我们可以简化代码并提高性能。


 使用 std::initializer_list 可以简化代码并提高性能,原因如下:

  • 简化代码
  1. 统一的初始化语法std::initializer_list 提供了一种统一的方式来初始化对象,无论是基本数据类型、自定义类型还是容器,都可以使用花括号 {} 来初始化,这使得代码更加简洁和一致。

  2. 减少构造函数数量:对于自定义类型,如果需要支持多种初始化方式,通常需要定义多个构造函数。使用 std::initializer_list 可以减少所需的构造函数数量,因为一个接受 std::initializer_list 的构造函数可以处理多种不同的初始化情况

  3. 避免临时对象在没有 std::initializer_list 的情况下,初始化容器可能需要创建临时对象,然后再将这些对象复制或移动到容器中。使用 std::initializer_list 可以直接在构造函数中处理初始化列表,避免了不必要的临时对象创建和复制。

  • 提高性能
  1. 避免拷贝std::initializer_list 本质上是一个包装了两个指针(指向初始化列表的开始和结束)的轻量级对象。它不会复制存储在初始化列表中的元素,因此可以避免不必要的拷贝操作,提高性能。

  2. 直接访问元素std::initializer_list 提供了迭代器,可以直接访问初始化列表中的元素,这允许编译器和标准库算法直接操作这些元素,而不需要额外的间接访问。

  3. 编译器优化:现代编译器可以对使用 std::initializer_list 的代码进行优化。例如,当使用列表初始化来创建对象时,编译器可以将其优化为直接构造对象,而不是先创建临时对象再进行拷贝或移动。

  4. 内存分配对于容器类,使用 std::initializer_list 可以在构造函数中一次性分配足够的内存来存储所有元素,避免了后续可能的多次内存分配和数据复制,这在处理大量数据时尤其重要。

  5. STL算法支持:STL算法(如 std::sort)可以直接接受 std::initializer_list 作为参数,这意味着可以直接对初始化列表进行算法操作,无需将数据复制到另一个容器中。

综上所述,std::initializer_list 提供了一种高效且简洁的方式来处理初始化列表,它简化了代码,减少了不必要的对象创建和拷贝,从而提高了程序的性能。


与数组初始化的区别

在C++中,std::initializer_list 和数组初始化是两种不同的概念,它们在用途和行为上有一些关键的区别:

  • std::initializer_list
  1. 类型和定义std::initializer_list 是一个模板类,定义在头文件 <initializer_list> 中。它提供了对初始化列表的访问,包括迭代器、大小等。

  2. 用途std::initializer_list 通常用于函数参数和构造函数参数,以允许函数和构造函数接受可变数量的参数。

  3. 存储std::initializer_list 不存储元素的副本,它只包含指向传递给它的初始化列表的指针。这意味着它不会增加额外的存储开销。

  4. 生命周期std::initializer_list 的对象通常是临时对象,它们的生命周期仅在创建它们的表达式中有效。

  5. 访问方式std::initializer_list 提供了 begin()end() 方法来获取指向列表开始和结束的迭代器,允许通过迭代器访问元素。

  6. 类型安全std::initializer_list 需要知道元素的类型,因此它是类型安全的。

  • 数组初始化
  1. 类型和定义:数组是C++中的内置数据结构,用于存储固定大小的同类型元素的集合。

  2. 用途:数组用于存储同类型的多个元素,它们在声明时必须指定大小,或者使用初始化列表来确定大小。

  3. 存储:数组自身存储了所有的元素,因此它们有固定的内存开销。

  4. 生命周期:数组的生命周期取决于它们的声明周期,可以是局部的、全局的或者静态的。

  5. 访问方式数组通过下标访问其元素,不支持迭代器。

  6. 大小限制

initializer_list的框架实现

std::initializer_list 是 C++ 标准库中的一个类模板,它提供了一种便捷的方式来访问初始化列表中的元素。下面是一个简化版的 std::initializer_list 的实现,展示了它的基本结构和功能:

#ifndef INITIALIZER_LIST_H
#define INITIALIZER_LIST_H

#include <iterator>
#include <cstddef> // For std::size_t

// 定义一个类模板
template<typename E> 
class initializer_list 
{
public:
    // 使用 T* 作为内部的迭代器类型
    typedef E* iterator;
    typedef E* const_iterator;
    
    // 构造函数
    constexpr initializer_list(const E* array, std::size_t n) noexcept
        : m_array(array), m_n(n) {}

    // 返回初始化列表中的元素数量
    constexpr std::size_t size() const noexcept { return m_n; }

    // 返回指向初始化列表第一个元素的指针
    constexpr const_iterator begin() const noexcept { return m_array; }

    // 返回一个指向初始化列表尾后位置的迭代器
    constexpr const_iterator end() const noexcept { return m_array + m_n; }

private:
    const E* m_array; // 指向数组的指针
    std::size_t m_n;  // 数组中元素的数量
};

#endif // INITIALIZER_LIST_H

这个简化版的 std::initializer_list 实现包括以下部分:

  1. 类型定义:定义了 iteratorconst_iterator 类型,它们都是指向元素类型的指针。

  2. 构造函数:接受一个指向元素数组的指针和数组的大小,并将它们存储为私有成员。

  3. 大小方法:返回初始化列表中的元素数量。

  4. 迭代器方法:提供 begin()end() 方法,返回指向初始化列表开始和结束的迭代器。

实现细节

  • 内存管理std::initializer_list 不拥有其内部数组,它只是持有一个指向数组的指针。因此,它不负责分配或释放内存。
  • 生命周期std::initializer_list 的生命周期应该与创建它的初始化列表的生命周期一致。如果初始化列表存储在栈上,那么 std::initializer_list 的对象也应该存储在栈上
  • 异常安全性std::initializer_list 的实现应该是异常安全的,因为它不涉及任何动态内存分配或其他可能抛出异常的操作。
  • 性能由于 std::initializer_list 不复制元素,它提供了一种高效的方式来传递初始化列表。
;