&与&&

转自:https://blog.csdn.net/viggirl/article/details/52858108

c++11中的新概念,主要解决了移动语义和完美转发

我们平常使用的引用都是指左值引用。 
以下是我经常用到的手法: 
void func( int& v); 
void func( std::string& s);

下面是一个测试例子

// =======================================================================================
// rvalue

void func ( int& i ) {
  std::cout << "func(int& i) = " << i << std::endl;
}

void func ( std::string& s ) {
  std::cout << "func(std::string& s) = " << s << std::endl;
}


void rvalue () {
  std::cout << std::endl << std::endl << "-------------------------- rvalue test------------------------" << std::endl;

  int i = 123;
  std::string s = "hello";

  func ( i );  // ok
  func ( s );  // ok

  func ( 1 );  // error : error C2665: “func”: 2 个重载中没有一个可以转换所有参数类型
}

// =======================================================================================

反正上面是出了问题,我么先了解一下基础知识在来回答上面的问题。 
左值(lvalue)和右值(rvalue)是从c继承过来的概念,在c++11之后,新标准基于这两个概念新增了部分特征(右值引用,用来解决移动和转发语义)。 
什么是左值、右值? 
通俗的将:=左边的是左值,右边的是右值。 
lvalue和rvalue的前缀怎么理解,left和right?好吧,l表示location,r表示read。 
location表示可以在内存中寻址,可以被赋值,read表示可以直接知道值 
什么是临时变量? 
对于全局变量、局部变量,我们都清楚,那么什么是临时变量,什么时候出现。 
怎么交换两个数?建一个临时变量和第一个交换,两个变量交换,临时变量和第二个交换。sorry,这只是我们业务逻辑上的临时变量,确切的说,只是我们创建的一个局部变量而已,那对于程序(或者说运行中的程序来说)什么才是临时变量?网上有位大神是这么解释的:编译期由编译器根据程序需要自动生成的,在运行期跑的时候是真实存在的,而主要发生在两个地方:函数传参发生类型转换时、函数返回值时。 
比如说函数调用时,我们传一个字面量过去;或者函数返回一个字面量时;或是表达式中发生了类型转换时。

在c++中,临时对象不能作为左值,但可以作为常量引用const & 
看下面的

  ++i = 3;  // ok
  i++ = 3;  // error C2106: “=”: 左操作数必须为左值

++i:i先自增1,++i之后还是指向i的对象 
i++:先把i自增之后的值丢给一个临时变量,自己去玩泥巴去了,i++之后指向的是那个临时变量。如果放在单条语句中,这两个没啥区别。

好了,现在看看上面出问题的地方func(1); 这个函数需要的是一个左值,但是字面量丢过去,她也只能放在一个临时变量中,而这个临时变量不能作为一个左值存在,就出了问题。

简单解决一下:

void func ( int&& i ) {
  std::cout << "func(int&& i) = " << i << std::endl;
}

或是用func ( const int& i )

好吧,这都不是今天要说的重点,重点是const int&& i; 
&& 和 &一样都是引用,&&是新标准弄出来的,称为 右值引用

无名右值引用 
一般由static_cast < T&& >(t)转换操作转换而来 
也可以用标准库提供的std::move()来将左值转换成右值引用

带名右值引用 
T&& 这是一个左值,只不过她的类型是右值引用,只能绑定右值 
额外说一句:如果类型是T&& 且这个T类型无需推导即可确定,那么这个变量称为带名右值引用;如果这个T类型需要推导,那么这个变量称为转发型引用。

总结一下: 
1、新标准为c++带来了一个新的左值类型:带名右值引用;带来了一个新的右值类型:无名右值引用 
2、左值引用(就是以前的引用)可以绑定左值,也可以绑定右值;右值引用只能绑定右值(新型右值类型(右值引用)、传统右值类型(临时对象))。

A a; 
A&& b = static_cast< A&&>(a); 
A&& c = std::move(a); 
创建两个无名引用,用来初始化两个右值引用对象

A& d = a; 
A& e = b; 
左值引用可以绑定左值

const A& f = c; 
const A& g = A(); 
const引用(若无特殊说明,引用二字都是指传统的左值引用)还可以绑定临时变量

A&& h = A(); 
右值引用也可以绑定临时变量

如果重载存在const引用和右值引用,新标准规定右值引用参数绑定的函数优先级高于const引用

// =======================================================================================
// rvalue

void func (const int& i ) {
  std::cout << "func(int& i) = " << i << std::endl;
}

void func ( int&& i ) {
  std::cout << "func(int&& i) = " << i << std::endl;
}

void func ( std::string& s ) {
  std::cout << "func(std::string& s) = " << s << std::endl;
}


void rvalue () {
  std::cout << std::endl << std::endl << "-------------------------- rvalue test------------------------" << std::endl;

  int i = 123;
  std::string s = "hello";

  func ( i );  // ok
  func ( s );  // ok

//  ++i = 3;  // ok
//  i++ = 3;  // error C2106: “=”: 左操作数必须为左值

  func ( 1 );  // 传过去是一个右值,根据新标准规定调用void func ( int&& i )

  int&& a = std::move ( i );
  func ( a );  // 左值 别忘了a是左值,类型是右值引用
  func ( static_cast<int&&>( i ) );  // 右值
}

// =======================================================================================

结果如下: 
————————– rvalue test———————— 
func(int& i) = 123 
func(std::string& s) = hello 
func(int&& i) = 1 // 虽然两个重载都满足,但是新标准规定右值引用绑定的优先级高于const左值引用 
func(int& i) = 123 
func(int&& i) = 123

接下来看看右值引用在移动构造和移动赋值中的应用 
什么是移动语义? 
可以对比拷贝语义对着看

目的: 
移动:接管另一个对象所有外部资源的所有权 
拷贝:复制另一个对象所有外部资源,并接管新生资源的所有权

过程: 
移动: 
释放this资源 
将this对象的指针或句柄指向that对象拥有的资源 
将that对象中指向资源的指针或句柄置空 
拷贝: 
释放this资源 
拷贝that拥有的资源 
将this对象的指针或句柄指向新生资源 
如果that对象时临时对象(右值),在拷贝完之后,that对象会被释放

move constructor和move assignment 
copy constructor和copy assignment的区别: 
构造函数相比赋值函数来说,因为this对象为空,所以不需要进行this资源释放(第一步)

如果that是左值,移动和拷贝不能替换;如果是右值,此时移动可以代替拷贝,好处是完成相同的目标的同时,少用了一次拷贝和释放。 
下面是网上找的一个例子,

#include <iostream>

class MemoryBlock
{
public:

    // 构造器(初始化资源)
    explicit MemoryBlock(size_t length)
        : _length(length)
        , _data(new int[length])
    {
    }

    // 析构器(释放资源)
    ~MemoryBlock()
    {
        if (_data != nullptr)
        {
            delete[] _data;
        }
    }

    // 拷贝构造器(实现拷贝语义:拷贝that)
    MemoryBlock(const MemoryBlock& that)
        // 拷贝that对象所拥有的资源
        : _length(that._length)
        , _data(new int[that._length])
    {
        std::copy(that._data, that._data + _length, _data);
    }

    // 拷贝赋值运算符(实现拷贝语义:释放this + 拷贝that)
    MemoryBlock& operator=(const MemoryBlock& that)
    {
        if (this != &that)
        {
            // 释放自身的资源
            delete[] _data;

            // 拷贝that对象所拥有的资源
            _length = that._length;
            _data = new int[_length];
            std::copy(that._data, that._data + _length, _data);
        }
        return *this;
    }

    // 移动构造器(实现移动语义:移动that)
    MemoryBlock(MemoryBlock&& that)
        // 将自身的资源指针指向that对象所拥有的资源
        : _length(that._length)
        , _data(that._data)
    {
        // 将that对象原本指向该资源的指针设为空值
        that._data = nullptr;
        that._length = 0;
    }

    // 移动赋值运算符(实现移动语义:释放this + 移动that)
    MemoryBlock& operator=(MemoryBlock&& that)
    {
        if (this != &that)
        {
            // 释放自身的资源
            delete[] _data;

            // 将自身的资源指针指向that对象所拥有的资源
            _data = that._data;
            _length = that._length;

            // 将that对象原本指向该资源的指针设为空值
            that._data = nullptr;
            that._length = 0;
        }
        return *this;
    }
private:
    size_t _length; // 资源的长度
    int* _data; // 指向资源的指针,代表资源本身
};

MemoryBlock f() { return MemoryBlock(50); }

int main()
{
    MemoryBlock a = f();            // 调用移动构造器,移动语义
    MemoryBlock b = a;              // 调用拷贝构造器,拷贝语义
    MemoryBlock c = std::move(a);   // 调用移动构造器,移动语义
    a = f();                        // 调用移动赋值运算符,移动语义
    b = a;                          // 调用拷贝赋值运算符,拷贝语义
    c = std::move(a);               // 调用移动赋值运算符,移动语义
}

对于转发型引用多说两句: 
转发型引用由以下两种语法产生: 
auto&& 
在模版函数中参数被声明为T&& 
这两种都需要推导,都属于转发型引用

好了 再回到我们的移动语义 
有一点需要注意的是:移动构造的参数不是const,从上面的例子也可以看到这一点;移动赋值也是一样的。

当=右边是一个右值或是用一个右值来初始化对象时,移动语义最适合了。 
当使用std::move之后并不会改变任何事,只是返回一个右值引用(无名)

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页