目录
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个特殊成员函数。如何做到这一点,基本上应该避免拥有任何旧式的、动态分配的内存。而应改用现代结构,如标准容器库。