C++ 左值引用和右值引用

目录

1. 左值和右值引用的定义

2. 左值和右值的区别:

3. 指针和引用的优缺点

4. 右值引用的应用-移动语义

5. 零规则


1. 左值和右值引用的定义

左值引用实际上是一种隐式的指针,它为对象建立一个别名,通过操作符&来实现。

// 左值定义格式
数据类型 & 表达式


// 实例
int a=10;
int & ia=a;
ia=2;

左值引用的特点:

(1)一个C++引用被初始化后,无法使用它再去引用另一个对象,它不能被重新约束。

(2)引用变量只是其他对象的别名,对它的操作和对原来变量的操作具有同样作用。

(3)指针变量与引用有两点主要区别:

        (A)指针是一种数据类型,而引用不是一个数据类型。指针可以转换为它所指向的变量的数据类型,以便赋值运算两边的类型相匹配;使用引用时,系统要求引用和变量的数据烈性必须相同,不能进行类型转换。

        (B)指针变量和引用变量都用来指向其他变量,但是指针变量使用的语法要更复杂一些。

注意:引用应该初始化,不进行初始化的话,编译器会报错。类的 成员可以是引用,如果不使用其他变量,引用就无法存在。因此,必须在构造函数初始化器(constructor initializer)中初始化引用数据程序,而不是在构造函数体内。实例如下:

class MyClass
{
    public:
        MyClass(int& ref):mref(ref){}
    private;
        int& mRef;
}

右值引用定义:

右值引用可以理解为右值得引用,当右值引用初始化后,临时变量消失。

// 定义格式
类型 && i=被引用的对象


// 实例如下
#include <iostream>
int get()
{
    int i=4;
    return i;
}


int main(void)
{
    int && k = get()+4;
    // int & i=get()+4; //出错
    
    k++;
    std::cout << "k 的值" << k << std::endl;

    return 0;
}

右值引用的特点:

        (1)一个右值引用被初始化后,无法使用它再去引用另一个对象,它不能被重新约束。

        (2)右值引用初始化后,具有该类型数据的所有操作。

        (3)右值引用只可以初始化右值,但右值引用实质上是一个左值,它具有临时变量的数据类型。

2. 左值和右值的区别:

  c++11中增加了右值引用和move语义来避免一些不必要的构造和copy操作,以此来提升程序的运行效率。首先说左值和右值,他们绝不是简单的等号左边和右边的区别,总结来说:

  1 .左值可以寻址,而右值不可以。

  2 .左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。

  3 左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。

  例:int a = 1;左值a可以被寻值,右值1不可以 ,左值a可以被赋值,1不可以,a可变1不可变,int a = b + c,同理a左值,b+c右值。最后c++11中还有一个将移值的概念,是c++11中新增的跟右值引用相关的表达式,这样的表达式通常是将要被移动的对象。其实标准库中有三个函数帮我们判断引用,左值引用和右值引用:

std::is_rvalue_reference<class _Tp>::value,

std::is_lvalue_reference<<class _Tp>>::value,

std::is_reference<class _Tp>::value

  左值引用就是对左值的引用类型,用T & a来表示,右值引用是对右值的引用类型,用T &&a来表示,左值引用和右值引用同为引用,他们在声明的同时必须被初始化。他们的初始化绑定有一定的规则和限制,例如一个非常量左值引用不能绑定一个常量左值。具体规则如下:

  

    这里我们可以看到常量左值引用是可以绑定右值的,其实这在c98中已经存在,只是我们平时没有留意这个细节而已,例如const int&a = 1等,那么他和const int a = 1,有什么不同呢,从语法上讲后者的右值在表达时结束后就销毁了,而前者不会。这就是我们尽量用const 引用代替值传递做函数参数的原因,它在某种程度上可以提高效率。

   那么右值引用有什么用呢?看一个例子。T&& a = ReturnValue(); 当我们调用一个返回一个右值的函数时,当函数返回后,函数返回的右值生命周期也就结束了,但是当我们通过一个右值引用来接收时,该右值有重新获生命,只要我们的右值引用a存在,该右值也同时存在,这又有什么用呢当我们使用T a = ReturnValue();这样的方式来接收时,会多一次对象的析构和构造,首先用函数返回值构造a,然后函数返回值生命期结束析构。而右值引用则不会,因为右值引用直接绑定了函数返回的右值。

  总结上面两段话:  左值引用和右值引用作为函数参数都能避免对象的拷贝和构造,但是我们通过右值引用改变一个右值时是没有意义的,而我们通过左值引用改变一个左值是有意义的。


原文链接:https://blog.csdn.net/D_Guco/article/details/63253045

3. 指针和引用的优缺点

指针的优点:        

        (1)可以减少参数传递带来的开销。

        (2)可以随意修改指针参数指向的对象。

指针的缺点:

        (1)需要验证指针参数是否为空指针。因为调用函数传递0,语句是合法的,被认为是空指针,但却也带来了隐患。

引用的优点:

        (1)可以减少参数传递带来的开销。

        (2)引用必须被初始化一个对象,并且不能使它再指向其他对象,因此对应赋值实际上是对目标对象的赋值。在函数中不需要验证引用参数的合法性。

引用的缺点:

        (1)引用一旦初始化后,就不能修改指定的对象。

4. 右值引用的应用-移动语义

移动语义是通过右值引用实现的。为了对类增加移动语义,需要实现移动构造函数和移动赋值运算符。移动构造和移动赋值运算符,应该使用nonexcept限定符标记。这告诉编译器,该接口不会抛出异常,这对标准库兼容非常重要。

移动语义的实现:

移动构造函数移动赋值运算符都将mCells的内存所有权从源对象移动到新对象,这两个方法将源对象的mCells指针设置为空指针,以防源对象的析构函数释放这一块内存,因为新的对象现在拥有了这块内存。

如果类中分配了内存,则通常应当实现析构函数,复制构造函数,移动构造函数,复制赋值运算符和移动运算符。这称为“5规则”


Spreadsheet::Spreadsheet(Spreadsheet&& src) noexcept
{
	moveFrom(src);
}

Spreadsheet& Spreadsheet::operator=(Spreadsheet&& rhs) noexcept
{
	if (this == &rhs)
	{
		return *this;
	}
	// release the old memory
	cleanup();

	moveFrom(rhs);

	return *this;
}

void Spreadsheet::cleanup() noexcept
{
	for (size_t i = 0; i < mWidth; i++)
	{
		delete[] mCells[i];
	}

	delete[]  mCells;
	mCells = nullptr;
	mWidth = mHeight = 0;
}


void Spreadsheet::moveFrom(Spreadsheet& src) noexcept
{
	//mName = std::move(src.mName);

	// shallow copy of data
	mWidth = src.mWidth;
	mHeight = src.mHeight;
	mCells = src.mCells;

	//reset the source object,because ownership has been moved
	src.mWidth = 0;
	src.mHeight = 0;
	src.mCells = nullptr;
}

   使用交换方式实现移动构造函数和移动赋值运算符:

void swap(Spreadsheet& first, Spreadsheet& second) noexcept
{
	using std::swap;

	//swap(first.mWidth, second.mWidth);
	//swap(first.mWidth, second.mWidth);
	//swap(first.mCells, second.mCells);

}


// 使用默认构造函数和swap()函数实现移动构造函数和移动赋值运算符的效率稍微差一些,这种做法的优点是代码比较少,
// 需要的代码较少,类增加数据成员时,需要的代码较少,也不太可能引入bug,因为只需要更新swap()实现,加入新的数据成员就可以。


class Spreadsheet
{
private:
	Spreadsheet() = default;
};



// 使用交换方式实现移动构造函数和移动赋值运算符
Spreadsheet::Spreadsheet(Spreadsheet&& src) noexcept
{
	swap(*this, src);
}

Spreadsheet& Spreadsheet::operator=(Spreadsheet&& rhs) noexcept
{
	Spreadsheet temp(std::move(rhs));
	swap(*this, temp);

	return *this;
}

   使用移动语义实现交换函数:


// 交换两个对象的swap()函数,这是另一个使用移动语义提高性能的示例,下面的函数为没有使用移动语义
// 函数实现需要将a 赋值到temp,其次将b复制到a,最后将temp复制到b,如果类型T的赋值开销很大的话,这一实现将严重影响性能
void swapCopy(T& a, T& b)
{
	T temp(a);
	a = b;
	b = temp;
}

// 使用移动语义实现交换函数,这正是标准库的实现方式
void swapCopy(T& a, T& b)
{
	T temp(std::move(a));
	a = std::move(b);
	b = std::move(temp);
}

5. 零规则

零规则是和5规则相对应的,零规则指出,在设计类时,应当使其不需要5规则中的5个特殊成员函数。如何做到这一点,基本上应该避免拥有任何旧式的、动态分配的内存。而应改用现代结构,如标准容器库。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值