【C++11】第一篇:琐碎知识+右值引用

简介: 在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。

一. C++简介


在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98成为C++11之前的最新C++标准名称。不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。


背景故事:1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。


C++11的介绍文档:https://en.cppreference.com/w/cpp/11


二. 统一的{}列表初始化


1. C++98中的初始化问题


在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:


int arr1[] = {1,2,3,4,5};
int arr2[5] = {0};// 数组元素全部初始化为0


但是对于一些自定义的类型,却无法使用这样的初始化。比如:


vector<int> v = {1,2,3,4,5};

在C++98中上面的例子就无法通过编译,导致每次使用vector时,都需要先把vector对象定义出来,然后使用循环对其赋初始值,非常不方便。


2. C++11中的初始化问题


C++11扩大了{}初始化和赋值的使用场景,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。


2.1 内置类型的列表初始化


int main()
{
  // 内置类型变量
  int x1{10};
  int x2 = {10};
  // 数组
  int arr1[5]{1,2,3,4,5};
  int arr2[5] = {1,2,3,4,5};
  // 动态数组(列表前不能再加等号)
  int* arr1 = new int[5]{0};
  int* arr2 = new int[5]{1,2,3,4,5};
  // error:int* arr2 = new int[5] = {1,2,3,4,5};
  // 标准容器
  vector<int> v1{1,2,3,4,5};
  vector<int> v2 = {1,2,3,4,5};
  map<int, int> m1{{1,1}, {2,2,},{3,3},{4,4}};
  map<int, int> m2 = {{1,1}, {2,2,},{3,3},{4,4}};
  return 0;
}


PS:除了定义动态数组的列表前不能加等号以外,其他类型的容器或数组列表初始化都可以在{}之前使用等号,其效果与不使用=没有什么区别。


2.2 自定义类型的列表初始化


标准库支持确定数量对象的列表初始化


对于我们自定义类型的对象,在构造函数传值时除了用圆括号(初始化列表)外,标准库支持使用花括号{}来传值:


class Point    
{    
public:    
  Point(int x = 0, int y = 0)    
    : _x(x)    
    , _y(y)    
  {}    
private:                                                                                                              
  int _x;    
  int _y;    
};  
int main()
{
  Point p1(1, 2);
  Point p2{1, 2};
  Point p3 = {1, 2};
  return 0;
}


不确定对象个数的列表初始化


如果需要完成多个对象的列表初始化,则需要实现一个该类的带有initializer_list< T >类型形参的构造函数即可,比如vector容器就可以在初始化列表中放入任意个数的对象而不用关心其容量和具体存储实现方法:


std::vector<int> vec = { 100, 200, 300, 400 };// 任意个数的对象都可以


注意:initializer_list是系统定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size():

871f5e6211fa47c19f559b71385e444a.png


关于构造函数,有如下两种构造方法:


initializer_list<T> li:构造一个空的列表。
initializer_list<T> li{a, b, c, ...}:构造一个指定的列表。

initializer_list类型对象的构造示例:


#include <initializer_list>
using namespace std;
int main()
{
  initializer_list<int> la{10, 20, 30, 40, 50};
  initializer_list<int> lb = {100, 200, 300, 400, 500};
  return 0;
}


从上分析,initializer_list没有类似initializer_list(int, int, int, ...)无限个对象的构造函数,那么对于initializer_list la{10, 20, 30, 40, 50};是怎么初始化的呢?


initializer_list初始化原理


下面是initializer_list类的实现:


template<class _Elem>
class initializer_list
{ // list of pointers to elements
public:
  typedef _Elem value_type;
  typedef const _Elem& reference;
  typedef const _Elem& const_reference;
  typedef size_t size_type;
  typedef const _Elem* iterator;
  typedef const _Elem* const_iterator;
  constexpr initializer_list() noexcept
  : _First(nullptr), _Last(nullptr)
  { 
    // empty list
  }
  constexpr initializer_list(const _Elem *_First_arg,
  const _Elem *_Last_arg) noexcept
  : _First(_First_arg), _Last(_Last_arg)
  {  
    // construct with pointers
  }
  _NODISCARD constexpr const _Elem * begin() const noexcept
  { 
    // get beginning of list
    return (_First);
  }
  _NODISCARD constexpr const _Elem * end() const noexcept
  { 
    // get end of list
    return (_Last);
  }
  _NODISCARD constexpr size_t size() const noexcept
  { 
    // get length of list
    return (static_cast<size_t>(_Last - _First));
  }
private:
  const _Elem *_First;
  const _Elem *_Last;
};


观察构造函数的实现代码,不能直接得到我们想要的答案。推测可能是编译器对initializer_list模板类的构造函数做了特别的处理,接下来我们去看看la对象初始的反汇编代码:


initializer_list<int> la{ 10, 20, 30, 40, 50 };
00007FF75D28188D  mov         edx,10h  
00007FF75D281892  lea         rcx,[la]  
00007FF75D281896  call        std::initializer_list<int>::__autoclassinit2 (07FF75D2811F9h)  
00007FF75D28189B  mov         dword ptr [rbp+38h],0Ah  
00007FF75D2818A2  mov         dword ptr [rbp+3Ch],14h  
00007FF75D2818A9  mov         dword ptr [rbp+40h],1Eh  
00007FF75D2818B0  mov         dword ptr [rbp+44h],28h  
00007FF75D2818B7  mov         dword ptr [rbp+48h],32h  
00007FF75D2818BE  lea         rax,[rbp+4Ch]  
00007FF75D2818C2  mov         r8,rax  
00007FF75D2818C5  lea         rdx,[rbp+38h]  
00007FF75D2818C9  lea         rcx,[la]  
00007FF75D2818CD  call        std::initializer_list<int>::initializer_list<int> (07FF75D281384h)


推测initializer_list底层构造原理为:


先在栈上面分配一个数组。

取到数组第一个元素和最后一个元素的下一个位置的地址(rbp+38h, rbp+4Ch)。

传入两个地址调用构造函数initializer_list(const _Elem *_First_arg, const _Elem *_Last_arg) noexcept完成对象的初始化。

initializer_list的应用


std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加

std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值:

ce83c4046d174c14a1021d95d7f04e2a.png


下面我们模拟std::vector配合initializer_list< T >去构造无限个元素的构造函数和赋值运算负重载:


namespace Myself
{
  template<class T>
  class vector 
  {
  public:
  typedef T* iterator;
  vector(initializer_list<T> l)
  {
    _start = new T[l.size()];
    _finish = _start + l.size();
    _endofstorage = _start + l.size();
    iterator vit = _start;
    typename initializer_list<T>::iterator lit = l.begin();
    for (auto e : l)
         *vit++ = e;
  }
  vector<T>& operator=(initializer_list<T> l) 
  {
    // 移动拷贝
    vector<T> tmp(l);
    std::swap(_start, tmp._start);
    std::swap(_finish, tmp._finish);
    std::swap(_endofstorage, tmp._endofstorage);
    return *this;
  }
  private:
  iterator _start;
  iterator _finish;
  iterator _endofstorage;
  };
}



三. 变量类型推导


1. auto


为什么需要类型推导?


在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂,比如:


#include <map>
#include <string>
int main()
{
  short a = 32670;
  short b = 32670;
  // 场景一:如果给成short,会造成数据丢失
  // 如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
  short c = a + b;
  // 场景二:使用迭代器遍历容器, 迭代器类型太繁琐
  std::map<std::string, std::string> m{{"apple", "苹果"}, {"banana","香蕉"}};
  std::map<std::string, std::string>::iterator it = m.begin();
  while(it != m.end())
  {
  cout<<it->first<<" "<<it->second<<endl;
  ++it;
  }
  return 0;
}



在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行变量显示初始化,使用auto定义变量会根据初始值自动识别变量的类型,编译器在编译期间会将auto替换为变量实际的类型:


#include <iostream>    
#include <typeinfo>    
using namespace std;    
int main()    
{    
  auto i = 10;                                                                                                        
  auto p = &i;    
  cout << typeid(i).name() << endl;    
  cout << typeid(p).name() << endl;    
  return 0;    
}


编译运行:


f3a3f8cb7f2c4e559ea3cfa9e8b3e097.png

auto使用注意事项


用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。

auto不能作为形参、函数返回值和数组元素的类型。

auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法根据初始值推导出auto的实际类型。

范围for


auto在实际中最常见的优势用法就是搭配C++11提供的基于范围的for循环一起使用。


对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。


int main()
{
  int arr[] = { 1,2,3,4,5 };
  // 打印出:1 2 3 4 5
  for (const auto e : arr)
  {
  cout << e << " ";
  }
  return 0;
}


范围for的几点意事项


支持迭代器和元素可以进行++、==操作的对象都可以使用范围for去遍历自己的元素。

与普通循环一样,可以用continue来结束本次循环直接去进行下一次,也可以用break来跳出整个循环。

如果不修改元素的值就加上const。

如果元素类型是基本类型(int、char等等)就没必要传引用符号&;元素类型是自定义类型或STL容器最好加上引用符号&来减少拷贝,另外如果想要修改元素的值也需要传引用。


2. decltype


关键字decltype可以识别变量或表达式的类型,其返回值可以用来定义同类型变量且可以通过typeid打印出来。


#include <iostream>    
#include <typeinfo>    
using namespace std;    
// decltype的一些使用使用场景    
template<class T1, class T2>    
void F(T1 t1, T2 t2)    
{    
  decltype(t1 * t2) ret;    
  cout << typeid(ret).name() << endl;    
}    
int main()    
{    
  int x = 1;    
  double y = 2.2;    
  decltype(x * y) ret; // ret的类型是double    
  decltype(&x) p; // p的类型是int*    
  cout << typeid(ret).name() << endl;    
  cout << typeid(p).name() << endl;    
  F(1, 'a');    
  return 0;    
}


编译运行:

05afc3a86f9b492a96eb0c71f9bbe44d.png


四. 继承控制关键字


1. override关键字


一个派生类可以在重载基类的虚函数,但在重载时可能因为函数名写错或参数、返回值类型不匹配导致重载基类虚函数失败:


class A                                                                             
{                                                                                   
  public:                                                                           
    virtual void Func(int);                                                     
};                                                                              
class B                                                                         
{                                                                               
  public:                                                                                                             
    // 1、返回值类型不匹配导致基类虚函数失败    
    virtual int Func(int);    
    // 2、函数名不匹配导致重载基类虚函数失败    
    virtual void func(int);    
    // 3、参数类型不匹配导致重载基类虚函数失败    
    virtual void Func(double);    
};


通常,我们在编写时不会注意到这个错误,运行时发现结果不对回头检查时才可能找到。在C++11中,通过使用新关键字override可以明确地表示一个函数是对基类中一个虚函数的重写,它会检查基类虚函数和派生类中重载函数的签名不匹配问题。如果签名不匹配,编译器会发出错误信息。


解决办法:在重写的成员函数的参数列表后加override关键字,它可以检查子类的虚函数是否完成重写,若未完成则报错:

1cbe424e34f64218b8a227f0287039c8.png


此时编译器会去检查被修饰的函数是否有重写基类的虚函数,如果没有重写就会报错,所以养成一个好的习惯,重写基类的虚函数时加上override关键字,相当于一个断言的作用。


PS:override只是检查子类的虚函数是否完成重写,不是让子类去强制重写虚函数;一般纯虚函数,才要求子类强制重写,如果子类不重写,则子类依然是抽象类,不能实例化对象。


2. final关键字


作用一:final用来修饰类时,这个类不能被继承


在C++98中,如果不想让一个类被继承,需要把这个类的构造函数声明成私有。C++11引入了final关键字,可以加在类名后面表示这个类不能被继承:


class A final            
{                        
  public:                
    A()                  
    {}                   
 };                       
// error:A类不能被继承    
class B : public A    
{};


编译报错:


5d5ae5121087459099815616e366cdc7.png

作用二:final用来修饰虚函数时,这个虚函数不能被派生类重写


用法:在虚函数的参数列表后加final关键字:


class A    
{    
  public:    
    virtual void Func() final;    
};    
// error:基类的Func()函数被final修饰,不派生类允许重写    
class B : public A    
{    
  public:                                                                                                           
    virtual void Func() override;    
};


编译不通过:


948951e2933f4aeb916198257ddf8166.png


五. STL中的一些变化


用橘色圈起来是C++11中增加的几个新容器,但是实际最有用的是unordered_map和

unordered_set:

95dc5d0135f04ce2b31275707843886f.png


如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作,目的让我我们使用迭代器时可以更加规范的使用。


实际上C++11更新后,容器中增加的新方法最有用的是插入接口函数(insert、push等)的右值引用版本:

012b2f0fed0e45fc9253b3323dfd0b84.png


但是这些接口通过右值引用和移动语义提高了效率,至于它们是如何提高效率的,后面部分讲右值引用会提到。


1. array容器


头文件:#include < array >


原型定义:template < class T, size_t N > class array;


容器介绍:

array的元素是存储在栈上的,这也就决定了array存不下太多的元素。


相比较于直接用方括号定义的定长数组,array容器的话相当于对其进行了封装,增加了迭代器和模板以及规定了一些特定的调用接口。


另外,array在封装之下可以自定义的更严格地去检查数组越界问题:


#include <array>                     
#include <iostream>                  
using namespace std;                 
int main()                           
{                                         
  array<int, 10> a1;                 
  a1[11];   // 断言检查           
  a1.at(11);// 抛异常检查         
  int a2[10];                                                                                                       
  a2[11];   // 不检查    
  return 0;    
}


array更多接口参考下面连接:http://cplusplus.com/reference/array/array/?kw=array


2. forward_list容器


forward_list 容器具有和 list 容器相同的特性,不过前者结构是单链表,后者是双链表结构。


forward_list相比较于list的特点:


单链表只能从前向后遍历,而不支持反向遍历,因此 forward_list 容器只提供前向迭代器,而不是双向迭代器。

插入节点时,forward_list是在pos的后面位置插入,list是在pos的前面位置插入。

存储相同个数的同类型元素时,forward_list耗用的内存空间更少,因为单链表每个节点的结构更简单。

forward_list头插和头删的效率要比list更高,因为前者结构更简单。

forward_list相关接口:http://cplusplus.com/reference/forward_list/forward_list/?kw=forward_list


3. unordered系列容器


主要增加了unordered_set和unordered_map这两个容器,它们的底层是哈希桶,增删查改的效率为O(1)。


unordered系列容器的相关接口:unordered_map、unordered_set。


六. 默认成员函数控制


在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、赋值运算符重载、析构函数和&和const&的重载。这些成员函数我们如果在类中显式定义了,编译器将不会重新生成默认版本。


有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本的默认构造函数。这样有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。


1. 强制生成默认函数的关键字 — default:


C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,编译器就不会生成默认的构造函数了,那么我们可以使用default关键字显示指定构造函数的生成。


class A 
{
  public:
    // 自定义的构造函数
    A(const A& a)
    {}
    // 强制编译器生成默认构造函数
    A() = default;
};


2. 禁止生成默认函数的关键字 — delete:


如果能想要限制某些默认函数的生成,在C++98中,只需该函数设置成private,并且只声明不定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。


class A     
{    
  public:                                                                                                           
    // 禁止编译器生成的默认构造函数
    A() = delete;   
    // 禁止编译器生成的默认拷贝构造函数
    A(const A& a) = delete;  
    // 禁止编译器生成的默认赋值重载函数            
    A& operator=(const A&) = delete;     
};

             


七. 右值引用与移动语义


传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,无论左值引用还是右值引用,都是给对象取别名。


1. 概念介绍


什么是左值?什么是左值引用?


左值就是我们通常定义的变量或解引用的指针,左值引用就是给左值的引用,给左值取别名。


左值具有如下三个特征:


可以取地址。

一般情况下可以修改(被const修饰的左值不能修改)。

既可以出现在=的左边,也可以出现在=的右边。

#include <iostream>    
using namespace std;    
int main()    
{                                                                                                                   
  // 以下的p、b、c、*p都是左值    
  int* p = new int(0);    
  int b = 1;    
  const int c = 2;    
  // 以下几个是对上面左值的左值引用    
  int*& rp = p;    
  int& rb = b;    
  const int& rc = c;    
  int& pvalue = *p;    
  return 0;    
}



什么是右值?什么是右值引用?


右值是一个表示数据的表达式,如:字面常量、字面常量表达式、临时变量(匿名对象、函数传值返回的返回值)等等,右值引用就是对右值的引用,给右值取别名。


右值具有以下三个特性:


不能取地址。

不能修改。

不能出现在=的左边,只能出现在右边。

#include <iostream>    
using namespace std;                
int main()             
{    
  double x = 1.1, y = 2.2;    
  // 以下几个都是常见的右值    
  10;    
  x + y;    
  // 以下几个都是对右值的右值引用    
  int&& rr1 = 10;    
  double&& rr2 = x + y;    
  // 下面是对右值的错误使用    
  // 这里编译会报错 “=”: 左操作数必须为左值    
  10 = 1;                                                                                                           
  x + y = 1;    
  return 0;    
}


PS:需要注意的是右值是不能取地址的,因为右值压根就没有被存储到内存中;但是给右值取别名后,会导致右值被存储到内存里的特定位置,且可以取到该位置的地址。也就是说例如:不能取字面量10的地址,但是经过rr1右值引用后,可以对rr1取地址,也可以修改rr1的值。如果不想rr1被修改,可以用const int&& rr1 去右值引用,平时很少用到这个特性,且实际中右值引用的使用场景并不在于此,了解一下就行。


int main()                         
{                                  
  int&& rr1 = 10;                  
  const double&& rr2 = 10;    
  rr1 = 20; // 不报错     
  rr2 = 20; // 报错    
  return 0;    
}


2. 左值引用与右值引用比较


左值引用总结


左值引用只能引用左值,不能引用右值。

但是const左值引用既可引用左值,也可引用右值。

int main()                
{                         
   // 左值引用只能引用左值,不能引用右值。    
   int a = 10;             
   int& ra1 = a;  // 编译通过,ra为a的别名    
   int& ra2 = 10; // 编译失败,因为10是右值    
   // const左值引用既可引用左值,也可引用右值。    
   const int& ra3 = 10; // 编译通过     
   const int& ra4 = a;  // 编译通过    
   return 0;    
 }


右值引用总结


右值引用只能右值,不能引用左值。

但是右值引用可以move以后的左值。

#include <utility>
int main()                             
{    
  // 编译失败: “初始化”: 无法从“int”转换为“int &&”    
  // message : 无法将左值绑定到右值引用    
  int a = 10;    
  int&& r2 = a;    
  // 右值引用可以引用move以后的左值    
  int&& r3 = std::move(a);    
  return 0;    
}


3. 右值引用使用场景和意义


前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?


b9a4f9007ef74fda9242f0fdc519bbeb.png

右值引用场景一:对于有些函数,它的返回值必须是传值返回,调用时外界通过构造一个对象来接收函数的返回值,整个返回和接收的过程难免要进行深拷贝,这时左值引用无法解决的:

6db668fcb01649fda2170378445585fb.png


有没有什么办法能够避免拷贝构造里的深拷贝呢?如果我们要拷贝的对象是个右值,即拷贝完成后这个右值将被销毁,那么能否不去深拷贝这个将亡右值,而是直接交换过来它的成员变量的,达到一个移动拷贝的效果,来提高拷贝的效率:

af89f6378c314daea1f69de8ad4b7f2e.png


PS:拷贝构造定义一个新对象时,如果被拷贝对象是左值则调用拷贝构造函数;如果该被拷贝对象是右值,则调用移动构造函数。移动构造不用进行深拷贝,效率比起拷贝构造函数高得多。


9bf3131001e44f7d9151ecfaeca52f88.png

STL中的容器都是有增加了移动构造的:


9a64cef9a45c40908588888539154591.png

右值引用场景二:


前面说过,在一次调用中,如果出现连续构造,编译器会优化为只进行一次构造,其他情况不会优化。

如果我们不是用右值去构造一个新对象,而是用右值去赋值一个已经存在的对象,那么在赋值过程中也有深拷贝,这个深拷贝能否被优化呢?

a1a9cfb604c44f6485b84e2afd1f08bd.png


借鉴前面移动拷贝的思路,实现一个针对右值的移动赋值:

8811af63a53c438c927c2a5452b07914.png

PS:在对象进行赋值操作时,如果=右边是左值(已经定义出来的对象),则需进行深拷贝来完成赋值操作;如果=右边是右值的话就调用资源转移的移动赋值函数,提高效率。


4308264cca8f4466b77d4732b4d23c62.png

同样,C++11中的STL容器也都是增加了移动赋值:


b00ff6ed59fd4045b67a6b4154513a85.png

右值引用场景三:

bae046f9fee5442081d1b1493ca27a05.png

右值引用还可以使用在容器的插入接口函数中,如果插入的参数是右值,则可以转移它的资源,减少拷贝。



// 左值引用的插入版本    
  void push_back (const value_type& val);    
  // 右值引用的插入版本    
  void push_back (value_type&& val);    
  int test()    
  {    
    list<string> lt;    
    string s1("1111");    
    // 这里调用的是拷贝构造    
    lt.push_back(s1);    
    // 下面调用都是移动构造    
    lt.push_back("2222");    
    lt.push_back(std::move(s1));    
    return 0;    
  }


不要拿右值引用去做返回值,这是错误的使用方法:

5932f8937cf3432387441466876fd10a.png



4. 右值引用总结


999abdb5e51243e5a7730115d7b1488d.png



5. move实现左值的资源转移


按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move(左值)函数将左值转化为右值。C++11中,std::move()函数位于头文件< utility >中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。如果move后的左值参与了移动拷贝或移动赋值的话,这个左值的资源会被转移。


#include <iostream>
#include <string>
#include <utility>
int main()
{
  std::string s1("hello world");
  // 这里s1是左值,调用的是拷贝构造
  std::string s2(s1);
  // 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
  // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
  // 资源被转移给了s3,s1被置空了。
  std::string s3(std::move(s1));
  return 0;
}


6. 完美转发


模板中的&& 万能引用


模板中的&&形参不代表右值引用,而是万能引用,其既能接收左值又能接收右值。

万能引用只是提供了能够同时接收左值引用和右值引用实参的能力,但是这个万能引用在后续使用中都退化成了左值。

#include <iostream>                                                                                                 
using namespace std;
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<typename T>    
void PerfectForward(T&& t)    
{    
  // t是万能引用,它即能够接收左值引用和右值引用
  // t实际接收到了实参之后统一退化成了左值引用
  Fun(t);    
}    
int main()    
{    
  PerfectForward(10); // 右值    
  int a;    
  PerfectForward(a); // 左值    
  PerfectForward(std::move(a)); // 右值    
  const int b = 8;    
  PerfectForward(b); // const 左值    
  PerfectForward(std::move(b)); // const 右值    
  return 0;    
}


编译运行,发现各种不同属性的实参传入模板函数void PerfectForward(T&& t)后,能够被万能引用的形参t接受到,但是接收到之后它们都退化成了左值,原本的属性被丢失:


7b984f2d5ce641c9a0126c81d9abc62e.png

PS:外面实参的右值属性传给万能引用对象后被丢失,因为万能引用接收到右值后,系统会为万能引用对象在内存上开辟一块内存空间存储它的值,这时万能引用对象也就可以取到地址,自然不能再归为右值了。


std::forward 完美转发在传参的过程中保留对象原生类型属性


为了解决上面的问题,C++11提供了一个叫做完美转发的函数模板forward,,其定义如下:abc4ca6decc24579945c52041c90e73b.png

我们只需把万能引用的对象传入forward函数模板即可实现完美转发:

f3e378919ac44d1bbb533dd962f2e2c6.png

接下来我们让PerfectForward函数中的万能引用对象t以完美转发的形式作为Fun()函数的实参,看t的属性是否正确:

#include <iostream>                                                                                                 
using namespace std;
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; }
// std::forward<T>(t),在传参的过程中保持了t的原生类型属性。    
template<typename T>    
void PerfectForward(T&& t)    
{    
  Fun(std::forward<T>(t));    
}  
int main()    
{    
  PerfectForward(10); // 右值    
  int a;    
  PerfectForward(a); // 左值    
  PerfectForward(std::move(a)); // 右值    
  const int b = 8;    
  PerfectForward(b); // const 左值    
  PerfectForward(std::move(b)); // const 右值    
  return 0;    
}


编译运行,结果正确:


fad99eb3475746b8a3709d83ae294d96.png


八. 新的类功能


1. 新增默认成员函数


原来C++类中,有6个默认成员函数:


构造函数

析构函数

拷贝构造函数

拷贝赋值重载

取地址重载

const 取地址重载

PS:最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。


在C++11中新增了两个:移动构造函数和移动赋值运算符重载,它们对应编译器默认生成版本的规则如下:


如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)。


2. 类成员变量声明时给缺省值


C++11允许在类声明时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值去初始化这个成员变量。


#include <iostream>                                                                                                   
using namespace std;    
class Date    
{    
  public:    
    // 打印成员变量的值    
    void Print()    
    {    
      cout<<_year<<'.'<<_month<<'.'<<_day<<endl;    
    }    
    // 成员变量声明时给缺省值    
    int _year = 2022;    
    int _month = 5;    
    int _day = 4;    
};    
int main()    
{    
  Date d;    
  d.Print();    
  return 0;    
}



编译运行:

b6be8314613c4af29de98053ca8bf1c8.png

相关文章
|
9月前
|
算法 编译器 程序员
【C/C++ 解惑 】 std::move 将左值转换为右值的背后发生了什么?
【C/C++ 解惑 】 std::move 将左值转换为右值的背后发生了什么?
118 0
|
3月前
|
存储 安全 C++
【C++11】右值引用
C++11引入的右值引用(rvalue references)是现代C++的重要特性,允许更高效地处理临时对象,避免不必要的拷贝,提升性能。右值引用与移动语义(move semantics)和完美转发(perfect forwarding)紧密相关,通过移动构造函数和移动赋值运算符,实现了资源的直接转移,提高了大对象和动态资源管理的效率。同时,完美转发技术通过模板参数完美地转发函数参数,保持参数的原始类型,进一步优化了代码性能。
55 2
|
5月前
|
编译器 C++
C++ 11新特性之右值引用
C++ 11新特性之右值引用
69 1
|
9月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
56 1
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
|
9月前
|
存储 安全 C语言
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(上)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
49 2
|
9月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(下)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
61 1
|
8月前
|
编译器 C++ 开发者
C++一分钟之-右值引用与完美转发
【6月更文挑战第25天】C++11引入的右值引用和完美转发增强了资源管理和模板灵活性。右值引用(`&&`)用于绑定临时对象,支持移动语义,减少拷贝。移动构造和赋值允许有效“窃取”资源。完美转发通过`std::forward`保持参数原样传递,适用于通用模板。常见问题包括误解右值引用只能绑定临时对象,误用`std::forward`,忽视`noexcept`和过度使用`std::move`。高效技巧涉及利用右值引用优化容器操作,使用完美转发构造函数和创建通用工厂函数。掌握这些特性能提升代码效率和泛型编程能力。
72 0
|
9月前
|
编译器 C++ 容器
【C++11(一)】右值引用以及列表初始化
【C++11(一)】右值引用以及列表初始化
|
9月前
|
存储 安全 程序员
C++11:右值引用
C++11:右值引用
57 0
|
9月前
|
存储 算法 程序员
【C++入门到精通】右值引用 | 完美转发 C++11 [ C++入门 ]
【C++入门到精通】右值引用 | 完美转发 C++11 [ C++入门 ]
68 0