现代C++之std::initializer_list的特性分析

一、从变量初始化开始谈起

        一般而言,对变量或对象使用括号初始化的方式被称为直接初始化,其本质是调用了相应的构造函数;而使用等号初始化的方式则被称为拷贝初始化,说到拷贝大家可能马上就会想到拷贝构造函数、operator =()函数,但此时并不一定是调用了这两个函数,这点极容易混淆!!!

        为了阐明上述的观点,我们不妨通过一个小实验的方式来验证上述的说法:

#include<iostream>

class Test
{
public:
    Test(int i) {}
};

Test func(Test t)
{
    return t;
}

int main()
{
    int a = 0;     //拷贝初始化
    int b(100);   //直接初始化

    Test t = 1;  //拷贝初始化, 实际上是隐式调用了转换构造函数Test(int i){}
    Test t1(5); //直接初始化, 调用构造函数Test(int i){}

    //传参以及返回值都是拷贝初始化, 但本质是调用了拷贝构造函数(可能存在编译器的优化,拷贝次数不定)
    Test t2 = func(t1);  
    
    return 0;
}

        如何证明Test t = 1是编译器隐式地调用了转换构造函数来初始化对象t的呢?只需在该构造函数前面加上explicit前缀修饰,此时就能发现编译器的错误提示(如下)。

二、有意或无意中使用的列表初始化

        除了圆括号和等号的初始化方式,我们还能在一些代码中看到用大括号{}来初始化变量或对象的身影,比如数组的初始化int a[] = {10,20,30,40}。除了数组以外,可能读者还看到在容器、自定义类类型中也用到了这种大括号{}的初始化方式,其背后的原因究竟是什么呢?

        自C++11标准开始就引入了列表初始化的概念,即支持使用{}对变量或对象进行初始化,且与传统的变量初始化的规则一样,也分为拷贝初始化和直接初始化两种方式。

        下面是这种列表初始化的使用方式,相信读者已经或多或少地使用过它了:

#include<iostream>
#include<vector>
#include<map>

class Test
{
public:
    Test(std::string s, int val) {}
};


int main()
{
    int arr[] = {10, 20, 30, 40};  //拷贝初始化
    int brr[]{10, 20, 30, 40};    //直接初始化

    std::vector<int> vc1 = {10, 20, 30 ,40};
    std::vector<int> vc2{10, 20, 30, 40};
    std::map<std::string, int> m1 = { {"a", 1}, {"b", 2}, {"c", 3} };

    Test *pt = new Test{"test", 100};
    delete pt;

    return 0;
}

        一些读者可能对于m1对象初始化的写法上会感到困惑。这里的大括号嵌套大括号是什么玩意儿?实际上这里发生了两步构造:

  • {"a", 1}, {"b", 2}, {"c", 3}先调用了std::pair的构造函数pair(const T1& x, const T2& y)获得了3个pair对象。

  • 外层的{}又调用了map的构造函数,那这里传递给map构造函数的参数是什么呢?先留个悬念,顺着往下看你就懂了!

三、std::initializer_list的本质

        上面所举的例子中用到了{}对标准库容器进行初始化,而标准库容器之所以能够支持初始化列表,除了有编译器的支持外,更直接的是这些容器存在以std::initializer_list为形参的构造函数

        说到这,std::initializer_list到底是什么呢?不妨通过源码的方式来认识一下它:

template<class _E>
    class initializer_list
    {
    public:
      typedef _E 		value_type;
      typedef const _E& 	reference;
      typedef const _E& 	const_reference;
      typedef size_t 		size_type;
      typedef const _E* 	iterator;
      typedef const _E* 	const_iterator;

    private:
      iterator			_M_array;
      size_type			_M_len;

      // The compiler can call a private constructor.
      constexpr initializer_list(const_iterator __a, size_type __l)
      : _M_array(__a), _M_len(__l) { }

    public:
      constexpr initializer_list() noexcept
      : _M_array(0), _M_len(0) { }

      // Number of elements.
      constexpr size_type
      size() const noexcept { return _M_len; }

      // First element.
      constexpr const_iterator
      begin() const noexcept { return _M_array; }

      // One past the last element.
      constexpr const_iterator
      end() const noexcept { return begin() + size(); }
    };

    

        上面贴出了STL中std::initializer_list的源码,从中也可以看出来std::initializer_list是一个类模板,其内部机制是const T* _M_array,且携带有长度信息size_t _M_len,同时还提供了指向该类对象首端、尾端的迭代器(即常量对象指针const T*)。

        另外,从下图也可以看出,编译器会负责将列表里的元素(大括号中的内容)构造为一个std::initializer_list对象,实际也就是编译器调用了这个私有的构造函数:

         当得到了一个std::initializer_list对象后,再来寻找标准容器中以std::initializer_list为形参的构造函数,并调用该构造函数对容器进行初始化。

四、std::initializer_list能被进行遍历吗?

        因为std::initializer_list是一个类模板,当然也能使用它来创建相应的类对象,但该对象一般只能用来初始化标准容器或特定的自定义类对象,但感觉它确实又是存放了一堆元素在一个列表中,有点容器的感觉,而且initializer_list的底层机制不就是指针空间嘛,说到此处,读者是否与我一样会心一笑呢?来看下该怎么遍历initializer_list吧:

#include<iostream>
#include<initializer_list>

int main()
{
    using namespace std;

    initializer_list<int> list;
    list = {10, 20, 30 ,40};

    for(auto it = list.begin(); it != list.end(); ++it) {
        cout << *it << endl;
    }
    
    //cout << list[0] << endl; //error, 源码中并没有重载[]
    
    return 0;
}

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页
评论

打赏作者

留恋单行路

看得开心就好,打赏随意!

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值