[转]c++ 左值引用与右值引用

左值引用

先看一下传统的左值引用。

int a = 10;
int &b = a; // 定义一个左值引用变量
b = 20; // 通过左值引用修改引用内存的值
左值引用在汇编层面其实和普通的指针是一样的;定义引用变量必须初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。

int &var = 10;
上述代码是无法编译通过的,因为10无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中,可以通过下述方法解决:

const int &var = 10;
使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量,相当于下面的操作:

const int temp = 10;
const int &var = temp;
根据上述分析,得出如下结论:

左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;
但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了。
那么C++11 引入了右值引用的概念,使用右值引用能够很好的解决这个问题。

右值引用

C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:

可以取地址的,有名字的,非临时的就是左值;
不能取地址的,没有名字的,临时的就是右值;
可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值。

从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。

定义右值引用的格式如下:

类型 && 引用名 = 右值表达式;
右值引用是C++ 11新增的特性,所以C++ 98的引用为左值引用。右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。

int &&var = 10;
在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。

右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。

用C++实现一个简单的顺序栈:

class Stack
{
public:
    // 构造
    Stack(int size = 1000) 
    :msize(size), mtop(0)
    {
    cout << "Stack(int)" << endl;
    mpstack = new int[size];
    }
    
    // 析构
    ~Stack()
    {
    cout << "~Stack()" << endl;
    delete[]mpstack;
    mpstack = nullptr;
    }
    
    // 拷贝构造
    Stack(const Stack &src)
    :msize(src.msize), mtop(src.mtop)
    {
    cout << "Stack(const Stack&)" << endl;
    mpstack = new int[src.msize];
    for (int i = 0; i < mtop; ++i) {
        mpstack[i] = src.mpstack[i];
    }
    }
    
    // 赋值重载
    Stack& operator=(const Stack &src)
    {
    cout << "operator=" << endl;
    if (this == &src)
            return *this;

    delete[]mpstack;

    msize = src.msize;
    mtop = src.mtop;
    mpstack = new int[src.msize];
    for (int i = 0; i < mtop; ++i) {
        mpstack[i] = src.mpstack[i];
    }
    return *this;
    }

    int getSize() 
    {
    return msize;
    }
private:
    int *mpstack;
    int mtop;
    int msize;
};

Stack GetStack(Stack &stack)
{
    Stack tmp(stack.getSize());
    return tmp;
}

int main()
{
    Stack s;
    s = GetStack(s);
    return 0;
}

运行结果如下:

Stack(int)             // 构造s
Stack(int)             // 构造tmp
Stack(const Stack&)    // tmp拷贝构造main函数栈帧上的临时对象
~Stack()               // tmp析构
operator=              // 临时对象赋值给s
~Stack()               // 临时对象析构
~Stack()               // s析构

为了解决浅拷贝问题,为类提供了自定义的拷贝构造函数和赋值运算符重载函数,并且这两个函数内部实现都是非常的耗费时间和资源(首先开辟较大的空间,然后将数据逐个复制),我们通过上述运行结果发现了两处使用了拷贝构造和赋值重载,分别是tmp拷贝构造main函数栈帧上的临时对象、临时对象赋值给s,其中tmp和临时对象都在各自的操作结束后便销毁了,使得程序效率非常低下。

那么我们为了提高效率,是否可以把tmp持有的内存资源直接给临时对象?是否可以把临时对象的资源直接给s?

在C++11中,我们可以解决上述问题,方式是提供带右值引用参数的拷贝构造函数和赋值运算符重载函数.

// 带右值引用参数的拷贝构造函数
Stack(Stack &&src)
    :msize(src.msize), mtop(src.mtop)
{
    cout << "Stack(Stack&&)" << endl;

    /*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
    mpstack = src.mpstack;  
    src.mpstack = nullptr;
}

// 带右值引用参数的赋值运算符重载函数
Stack& operator=(Stack &&src)
{
    cout << "operator=(Stack&&)" << endl;

    if(this == &src)
        return *this;
        
    delete[]mpstack;

    msize = src.msize;
    mtop = src.mtop;

    /*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
    mpstack = src.mpstack;
    src.mpstack = nullptr;

    return *this;
}

运行结果如下:

Stack(int)             // 构造s
Stack(int)             // 构造tmp
Stack(Stack&&)         // 调用带右值引用的拷贝构造函数,直接将tmp的资源给临时对象
~Stack()               // tmp析构
operator=(Stack&&)     // 调用带右值引用的赋值运算符重载函数,直接将临时对象资源给s
~Stack()               // 临时对象析构
~Stack()               // s析构

程序自动调用了带右值引用的拷贝构造函数和赋值运算符重载函数,使得程序的效率得到了很大的提升,因为并没有重新开辟内存拷贝数据。

mpstack = src.mpstack;
可以直接赋值的原因是临时对象即将销毁,不会出现浅拷贝的问题,我们直接把临时对象持有的资源赋给新对象就可以了。

所以,临时量都会自动匹配右值引用版本的成员方法,旨在提高内存资源使用效率。

带右值引用参数的拷贝构造和赋值重载函数,又叫移动构造函数和移动赋值函数,这里的移动指的是把临时量的资源移动给了当前对象,临时对象就不持有资源,为nullptr了,实际上没有进行任何的数据移动,没发生任何的内存开辟和数据拷贝。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容

  • 本文根据众多互联网博客内容整理后形成,引用内容的版权归原始作者所有,仅限于学习研究使用,不得用于任何商业用途。 左...
    深红的眼眸阅读 11,271评论 1 12
  • c++中引入了右值引用和移动语义,可以避免无谓的复制,提高程序性能。有点难理解,于是花时间整理一下自己的理解。 左...
    StormZhu阅读 113,807评论 42 191
  • 日更第四天,说实话,我已经不知道改写些什么了,而且有些后悔,但是,我不能去妥协,因为,我们一直妥协,一直将就,就会...
    W_十一阅读 138评论 0 0
  • 一生中,有人在乎,有人心疼,就是最好的幸福,一辈子,有人陪伴,有人可依,就是最美的情话。 被别人在乎、有人惦记是幸...
    不愿长大的孩子阅读 596评论 0 1
  • 何为缘份?一千万人有一千万种回答,就感情而言,多数解答都逃不过聚散二字。 如同释迦牟尼说的: “偶然的相遇,蓦然的...
    24K戀芯純娘子阅读 667评论 4 20