C++11 列表初始化与右值引用核心解析

列表初始化

C++98中传统的{}

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

C++11中的{}

  • C++11 试图实现==一切对象皆可用 {} 初始化, {} 初始化也叫列表初始化

  • 内置类型支持,自定义类型也支持

  • =={} 初始化的过程中,可以省略掉 ===

    struct Point
    {
    int _x;
    int _y;
    };

    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(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()
    {
    int array1[] = { 1,2,3,4,5 };
    int array2[5] = { 0 };
    Point p = { 1,2 };

    复制代码
      //C++11 支持的
      //内置类型支持
      int x1 = { 2 };
      //自定义类型支持
      //这里本质是用{2025,1,1}构造一个Date临时对象
      //临时对象再去拷贝构造d1,编译器优化后合二为一变成{2025,1,1}直接构造初始化
    
      //运行之后发现是没有调用拷贝构造的
      Date d1 = { 2025,1,1 };
    
      //d2引用的是{2024,7,25}构造的临时对象
      const Date& d2 = { 2024, 7, 25 };
    
      // 需要注意的是 C++98 支持单参数时类型转换,也可以不用{}
      Date d3 = { 2025 };
      Date d4 = 2025;
    
      //可以省略掉 =
      Point p1{ 1, 2 };
      int x2{ 2 };
      Date d6{ 2024, 7, 25 };
      const Date& d7{ 2024, 7, 25 };
    
      // 不支持,只有{}初始化,才能省略 =
      // Date d8 2025;
    
      vector<Date> v;
      v.push_back(d1);
      v.push_back(Date(2025, 1, 1));
    
      // 比起有名对象和匿名对象传参,这里 {} 更有性价比
      v.push_back({ 2025, 1, 1 });
    
      return 0;

    }

C++11中的std::initializer_list

  • 虽然上面的初始化已经比较方便了,但是对象容器初始化还是不太方便,比如一个 vector 对象用 N 个值初始化,那么要实现多个构造函数vector(int a)、vector(int a, int b)、vector(int a, int b, int c)......

  • C++11库中提出了⼀个std::initializer_list的类,这个类的本质是底层开⼀个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束

  • ==容器支持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的初始化。==STL中的容器支持任意多个值构成的 {x1,x2,x3...} 进行 {x1,x2,x3...} 进行初始化,就是通过 std::initializer_list的构造函数支持的

    int main()
    {
    std::initializer_list<int> mylist;
    mylist = { 10,20,30 };
    cout << sizeof(mylist) << endl; //16(64位下),两个指针大小

    复制代码
      //这里begin和end返回的值是initializer_list对象中存的两个指针
      //这两个指针的值跟i的地址接近,说明数组在栈上
      int i = 0;
      cout << mylist.begin() << endl;  //000000721E50F7F8
      cout << mylist.end() << endl;    //000000721E50F804
      cout << &i << endl;              //000000721E50F074
    
      //{}列表中可以是任意多个值
      //这两个写法语义上还是有差别的,第一个v1是直接构造
      //第二个v2是 构造临时对象+临时对象拷贝v2 ,优化后是直接构造
      vector<int> v1({ 1,2,3,4,5 });
      vector<int> v2 = { 1,2,3,4,5 };
      const vector<int>& v3 = { 1,2,3,4,5 };
    
      //这里是pair对象的{}初始化和map的initializer_list构造结合到一起用了
      map<string, string> dict = { {"sort","排序"},{"string","字符串"} };
    
      //initializer_list版本的赋值支持
      v1 = { 10,20,30,40,50 };
    
      return 0;

    }

右值引用和移动语义

左值和右值

  • 左值是一个表示数据的表达式,一般==有持久状态,存储在内存中,可以获取它的地址。==左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。

  • 右值也是一个表示数据的表达式,可以是字面值常量(没有名字的常量)、表达式、匿名对象或临时对象,不能被取地址,可以出现在赋值符号的右边,但是不能出现在赋值符号的左边。

  • 左右值的核心区别就是能否取地址

    int main()
    {
    //左值:可以取地址
    //以下的p、b、c、p、s、s[0]都是常见的左值
    int
    p = new int(0);
    int b = 1;
    const int c = b;
    *p = 10;
    string s("11111");
    s[0] = 'x';

    复制代码
      //右值:不能取地址
      double x = 1.1, y = 2.2;
      //以下是常见的右值
      10;
      x + y;
      fmin(x, y); //返回浮点数中较小的那个
      string("11111");
      
      return 0;

    }

左值引用和右值引用

  • 左值引用 Type& r1 = x; 左值引用就是给左值取别名

  • 右值引用Type&& rr1 = y; 右值引用就是给右值取别名

  • 左值引用不能直接引用右值,但是const左值引用可以引用右值

  • 右值引用不能直接引用左值,但是右值引用可以引用move(左值)

  • const 左值引用(const T&),既能绑定左值,也能绑定右值

  • move 是库里面的一个函数模板,本质内部是进行强制类型转换,将左值变为右值引用,传入右值直接返回

  • 注意:变量表达式都是左值属性,就是说一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值

  • 左值引用和右值引用都是取别名,不开空间,底层都是用指针实现的。

    int main()
    {
    double x = 1.1, y = 2.2;
    int* p = new int(0);
    int b = 1;
    const int c = b;
    *p = 10;
    string s("11111");

    复制代码
      //右值引用给右值取别名
      int&& rr1 = 10;
      double&& rr2 = x + y;
      double&& rr3 = fmin(x, y);
      string&& rr4 = string("11111");
    
      //左值引用不能直接引用右值,但是const左值引用可以引用右值
      const int& rx1 = 10;
      const double& rx2 = x + y;
      const double& rx3 = fmin(x, y);
      const string& rx4 = string("11111");
    
      //右值引用不能直接引用左值,但是右值引用可以引用move(左值)
      int&& rrx1 = move(b);   //b还是左值,不会改变本身的属性
      int*&& rrx2 = move(p);
      int&& rrx3 = move(*p);
      string&& rrx4 = move(s);
      string&& rrx5 = (string&&)s;
      
      //rr1的属性是左值,所以不能再被右值引用绑定,除非move一下
      int&& rrx6 = move(rr1);
      
      return 0;

    }

延长生命周期

  • 右值引用可用于为临时对象延长生命周期;const的左值引用也能延长临时对象的生命周期,但是被const引用的对象无法被修改

  • 临时对象原本的生命周期就是那一句指令结束。如果临时对象在函数里面,生命周期最长也会在函数结束随着栈帧销毁而销毁

    int main()
    {
    std::string s1 = "Test";
    //std::string&& r1 = s1; //错误,不能绑定到左值

    复制代码
      const std::string& r2 = s1 + s1;    //const左值引用延长生命周期
      //r2 += "Test";     //不能修改const的左值引用
    
      std::string&& r3 = s1 + s1; //右值引用延长生命周期
      r3 += "Test";   //非const引用可以修改
    
      std::cout << r3 << endl;    //TestTestTest
    
      return 0;

    }

左值和右值的参数匹配

  • C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配

  • C++11之后,分别重载左值引用、const左值引用、右值引用作为形参的函数,那么实参是左值会匹配 左值引用,实参是 const 左值 会匹配 const 左值引用,实参是右值会匹配 右值引用

  • 右值引用变量在用于表达式时的属性是左值

    void f(int& x)
    {
    std::cout << "左值引用重载f(" << x << ")\n";
    }
    void f(const int& x)
    {
    std::cout << "到const的左值引用重载f(" << x << ")\n";
    }
    void f(int&& x)
    {
    std::cout << "右值引用重载f(" << x << ")\n";
    }

    int main()
    {
    int i = 1;
    const int ci = 2;
    f(i); //调用f(int& x)
    f(ci); //调用f(const int& x)
    f(3); //调用f(int&& x),如果没有f(int&& x),则会调用f(const int& x)
    f(std::move(i)); //调用f(int&& x)

    复制代码
      //右值引用变量在用于表达式时是左值(右值引用本身的属性是左值)
      int&& x = 1;    //右值本身不能修改,但是x是左值,可以修改
      f(x);   //调用f(int& x);
      f(std::move(x)); //调用f(int&& x)
    
      return 0;

    }

右值引用和移动语义的使用场景

左值引用主要使用场景回顾

  • 左值引用主要场景是在函数 中左值引用传参 和 左值引用传返回值 时减少拷贝,同时还可以修改实参和修改返回对象的值

  • 如下方代码,str 是局部变量,不能传引用返回,函数结束这个对象就销毁了,右值引用也无法改变对象已经析构的事实

    class Solution
    {
    public:
    //传值返回需要拷贝
    string addStrings(string num1, string num2) {
    string str;
    int end1 = num1.size() - 1, end2 = num2.size() - 1;
    //进位
    int next = 0;
    while (end1 >= 0 || end2 >= 0)
    {
    int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
    int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
    int ret = val1 + val2 + next;
    next = ret / 10;
    ret = ret % 10;
    str += ('0' + ret);
    }
    if (next == 1)
    str += '1';
    reverse(str.begin(), str.end());
    return str;
    }
    };

移动构造和移动赋值

  • 移动构造函数是一种构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值

  • 移动赋值时一个赋值运算符的重载,跟拷贝赋值构成函数重载,移动赋值函数要求第一个参数时该类类型的引用,但是不同的是要求这个参数是右值引用

  • 移动构造和移动赋值的第一个参数都是右值引用的类型,它的本质是要"窃取"引用的右值对象的资源,而不是像拷贝构造和赋值构造那样去拷贝资源,从而提高效率

    namespace ssp
    {
    class string
    {
    public:
    typedef char* iterator;
    typedef const char* const_iterator;

    复制代码
          iterator begin()
          {
              return _str;
          }
          iterator end()
          {
              return _str + _size;
          }
          const_iterator begin() const
          {
              return _str;
          }
          const_iterator end() const
          {
              return _str + _size;
          }
    
          string(const char* str = "")
              :_size(strlen(str))
              ,_capacity(_size)
          {
              cout << "string(char* str) -- 构造" << endl;
              _str = new char[_capacity + 1];
              strcpy(_str, str);
          }
    
          void swap(string& s)
          {
              ::swap(_str, s._str);
              ::swap(_size, s._size);
              ::swap(_capacity, s._capacity);
          }
    
          string(const string& s)
              :_str(nullptr)
          {
              cout << "string(const string& s) -- 拷贝构造" << endl;
              reserve(s._capacity);
              for (auto ch : s)
              {
                  push_back(ch);
              }
          }
    
          //移动构造
          string(string&& s)
          {
              cout << "string(string&& s) -- 移动构造" << endl;
              swap(s);    //s类型是右值引用,本身的属性是左值,是用来绑定传入的右值的
          }
    
          string& operator=(const string& s)
          {
              cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
              if (this != &s)
              {
                  //将当前对象的字符串内容 "清空"
                  _str[0] = '\0';
                  _size = 0;
    
                  reserve(s._capacity);
                  for (auto ch : s)
                  {
                      push_back(ch);
                  }
              }
          }
    
          //移动赋值
          string& operator=(string&& s)
          {
              cout << "string& operator=(string&& s) -- 移动赋值" << endl;
              swap(s);
              return *this;
          }
    
          ~string()
          {
              cout << "~string() -- 析构" << endl;
              delete[] _str;
              _str = nullptr;
          }
    
          char& operator[](size_t pos)
          {
              assert(pos < _size);
              return _str[pos];
          }
    
          void reserve(size_t n)
          {
              if (n > _capacity)
              {
                  char* tmp = new char[n + 1];
                  if (_str)
                  {
                      strcmp(tmp, _str);
                      delete[] _str;
                  }
                  _str = tmp;
                  _capacity = n;
              }
          }
    
          void push_back(char ch)
          {
              if (_size >= _capacity)
              {
                  size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
                  reserve(newcapacity);
              }
    
              _str[_size] = ch;
              ++_size;
              _str[_size] = '\0';
          }
    
          string& operator+=(char ch)
          {
              push_back(ch);
              return *this;
          }
    
          const char* c_str() const
          {
              return _str;
          }
    
          size_t size() const
          {
              return _size;
          }
    
      private:
          char* _str = nullptr;
          size_t _size = 0;
          size_t _capacity = 0;
      };

    }

    int main()
    {
    //构造
    ssp::string s1("xxxx");
    //拷贝构造
    ssp::string s2 = s1;
    //构造+移动构造,优化后 直接构造
    ssp::string s3 = ssp::string("yyy");
    //移动构造
    ssp::string s4 = move(s1);

    复制代码
      cout << "***********************************" << endl;
    
      return 0;

    }

右值引用和移动语义解决传值返回问题

  • 临时存储在调用者的栈帧里,在下面例子中是在main中调用的,所以在main栈帧里面

    namespace ssp
    {
    string addStrings(string num1, string num2)
    {
    string str;
    int end1 = num1.size() - 1, end2 = num2.size() - 1;
    int next = 0;
    while (end1 >= 0 || end2 >= 0)
    {
    int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
    int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
    int ret = val1 + val2 + next;
    next = ret / 10;
    ret = ret % 10;
    str += ('0' + ret);
    }

    复制代码
          if (next == 1)
              str += '1';
          reverse(str.begin(), str.end());
          cout << "******************************" << endl;
          return str;
      }

    }

    //场景1
    int main()
    {
    ssp::string ret = ssp::addStrings("11111", "2222");
    cout << ret.c_str() << endl;
    return 0;
    }

    //场景2
    int main()
    {
    ssp::string ret;
    ret = ssp::addStrings("11111", "2222");
    cout << ret.c_str() << endl;
    return 0;
    }

右值对象构造,只有拷贝构造,没有移动构造的场景

  • vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的拷合二为一变为⼀次拷贝构造。
  • 在vs2019的release和vs2022的debug和release,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为⼀,变为直接构造。(在下面的图中有展示)

右值对象构造,有拷贝构造,也有移动构造的场景

  • vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷贝合⼆为⼀变为⼀次移动构造

  • 在vs2019的release和vs2022的debug和release,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为⼀,变为直接构造。

右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景

  • 左边展示了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次拷贝构造,⼀次拷贝赋值
  • 在vs2019的release和vs2022的debug和release下,右边直接构造要返回的临时对象,str本质是临时对象的引用 ,底层角度⽤指针实现。

右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景

  • 左边返回的对象为临时对象,是右值,会匹配移动构造
  • 左边展示了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值
  • 在vs2019的release和vs2022的debug和release下,直接构 要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现

右值引用和移动语义在传参中的提效

  • 在C++11以后容器的push和insert系列的接口都增加了右值引用版本
  • 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
  • 当实参是一个右值,容器内部则调用移动构造,将右值对象的资源移到容器空间的对象上

类型分类(了解)

  • C++11中,右值又被划分为纯右值和将亡值
    • 纯右值是指那些字面值常量或求值结果,相当于字面值或是一个不具名的临时对象
    • 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达
  • 范左值包括将亡值和左值

引用折叠

  • C++中不能直接定义引用的引用,可以通过模板或 typedef 中的类型操作构成引用的引用

  • 右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用,所以可以用T&&来接收所有实参(完美模板)

    int main()
    {
    typedef int& lref;
    typedef int&& rref;
    int n = 0;

    复制代码
      lref& r1 = n;   //r1的类型为 int&
      lref&& r2 = n;  //r2的类型为 int&
      rref& r3 = n;   //r3的类型为 int&
      rref&& r4 = 1;  //r4的类型为 int&& ,只有 右值引用+右值引用 才是右值引用
    
      return 0;

    }

    template<class T>
    void Function(T&& t)
    {
    int a = 0;
    T x = a;
    //x++;
    cout << &a << endl;
    cout << &x << endl << endl;
    }

    int main()
    {
    //10是右值,推导出T为int,模板实例化为void Function(int&& t)
    Function(10); //右值

    复制代码
      int a;
      //a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
      Function(a);    //左值
    
      //std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
      Function(std::move(a)); //右值
          
      const int b = 8;
      //b为左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
      Function(b);  //const 左值
    
      //std::move(b)为右值,推导出T为const int,引用折叠,模板实例化为void Function(const int&& t)
      //t虽然属性是左值,但是加了const之后不能修改了
      Function(std::move(b)); //const 右值
    
      return 0;

    }

完美转发

  • 变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式 Function 的属性是左值,那么我们把它传给下一次层函数Fun,那么匹配的是左值引用版本的Func函数。想要保持 t 对象的属性,就需要使用完美转发实现
  • 完美转发forward本质是一个函数模板,它主要还是通过引用折叠方式实现
    • Function的实参是右值,则没有发生折叠,forward内部 t 恢复为右值引用返回

    • Functon的实参是左值,T推到为int&,引用折叠为左值引用。forward内部 t 恢复为左值引用返回

      void Fun(int& x) { cout << "左值引用" << endl; }
      void Fun(const int& x) { cout << "const 左值引用" << endl; }

      void Fun(int&& x) { cout << "右值引用" << endl; }
      void Fun(const int&& x) { cout << "const 右值引用" << endl; }

      template<class T>
      void Function(T&& t)
      {
      //Fun(t);
      Fun(forward<T>(t)); //完美转发
      }

      int main()
      {
      //10是右值,推导出T为int,模板实例化为void Function(int&& t)
      Function(10); //右值引用

      复制代码
      int a;
      //a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
      Function(a);    //左值引用
      
      //std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
      Function(std::move(a)); //右值引用
          
      const int b = 8;
      //b为左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
      Function(b);    //const 左值引用
          
      //std::move(b)为右值,推导出T为const int,引用折叠,模板实例化为void Function(const int&& t)
      //t虽然属性是左值,但是加了const之后不能修改了
      Function(std::move(b)); ////const 右值引用
      
      return 0;

      }

相关推荐
小北方城市网2 小时前
Spring Boot 多数据源与事务管理实战:主从分离、动态切换与事务一致性
java·开发语言·jvm·数据库·mysql·oracle·mybatis
痴儿哈哈2 小时前
C++与硬件交互编程
开发语言·c++·算法
roman_日积跬步-终至千里2 小时前
【Java 并发-面试】从线程基础到企业级开发的知识点概况
java·开发语言
云中飞鸿2 小时前
VS2015安装后,安装QT59,之后安装qt-vsaddin-msvc2015-2.4.3.vsix 文件失败问题!
开发语言·qt
m0_748233172 小时前
C与C++:底层编程的六大核心共性
java·开发语言
沐知全栈开发2 小时前
HTTP Content-Type
开发语言
闻缺陷则喜何志丹2 小时前
【栈 递归】P8650 [蓝桥杯 2017 省 A] 正则问题|普及+
c++·数学·蓝桥杯·递归·
苏宸啊2 小时前
vecto底层模拟实现
c++
一切尽在,你来2 小时前
C++多线程教程-1.2.2 C++标准库并发组件的设计理念
开发语言·c++