文章目录
- 左值lvalue
- 右值rvalue
- 左值右值的转换
- 左值引用
- 右值引用
- 最佳实践
- 移动构造和移动赋值函数加nonexcept
- 显式删除拷贝和构造
- 隐式删除拷贝和构造
- RAII (Resource Acquisition Is Initialization)
原文地址
左值lvalue
有具体地址,可以通过地址访问的对象。(以后可能都不应该说变量了。应该c++中的所有变量都是对象,连int都有构造函数的
)
原本所有的左值都可以被赋值(=左边),自从有了const,出现了不可修改的左值。
右值rvalue
不是左值的对象都是右值。
可以认为右值不在内存中具有地址,暂时储存在寄存器中。
左值右值的转换
左值可以转右值:
int a = 1; // a is an lvalue
int b = 2; // b is an lvalue
int c = a + b; // + needs rvalues, so a and b are converted to rvalues
// and an rvalue is returned
显然右值不可以转左值:因为违背了左值有内存地址的定义,
但是,可以从右值生成左值:使用解引用符*
int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10; // OK: p + 1 is an rvalue, but *(p + 1) is an lvalue
取地址符&左值转右值:
int var = 10;
int* bad_addr = &(var + 1); // ERROR: lvalue required as unary '&' operand
int* addr = &var; // OK: var is an lvalue
&var = 40; // ERROR: lvalue required as left operand
// of assignment
左值引用
正常的左值引用比较常见。子函数的形式参数定义为引用。避免拷贝。
如下实现了函数返回一个左引用,因此函数的返回值可以被赋值。
int globalvar = 20;
int& foo()
{
return globalvar;
}
int main()
{
foo() = 10;
return 0;
}
右值引用
c++11出现的特性,用处很多。暂时先只了解用于类中移动赋值函数的用途。
拷贝构造函数:一个构造函数,Dog dog = Dog()
调用
拷贝赋值函数:一个运算符重载函数,
移动构造函数:
移动赋值函数:
例如,考虑一个动态“整数向量”的简单实现:
class Intvec
{ public :
显式Intvec(size_t num = 0 )
: m_size(num), m_data( new int [m_size])
{
log( "constructor" );
}
~Intvec()
{
日志( “析构函数” );
if (m_data) {
删除[] m_data;
m_data = 0 ;
}
}
INTVEC(const的INTVEC&其他)
:m_size(other.m_size),M_DATA(新INT
[m_size])
{
log( "复制构造函数" );
for (size_t i = 0 ; i < m_size; ++i)
m_data[i] = other.m_data[i];
}
Intvec& operator =( const Intvec& other)
{
log( "复制赋值运算符" );
Intvec tmp(其他);
std::swap(m_size, tmp.m_size);
std::swap(m_data, tmp.m_data); 返回*这个;
}私人:
无效日志(常量字符* MSG)
{
cout << "[" << this << "] " << msg << "\n" ;
}
size_t m_size; int * m_data;
};
因此,我们定义了通常的构造函数、析构函数、复制构造函数和复制赋值运算符[4],所有这些都使用日志函数来让我们知道它们何时被实际调用。
将v1的内容复制到v2 中:
Intvec v1( 20 );
Intvec v2;
cout << "分配左值...\n" ;
v2 = v1;
cout << "结束赋值左值...\n" ;
这打印的是:
Intvec v1( 20 );
Intvec v2;
cout << "分配左值...\n" ;
v2 = v1;
cout << "结束赋值左值...\n" ;
分配右值:
cout << "分配右值...\n" ;
v2 = Intvec( 33 );
cout << "结束分配右值...\n" ;
虽然在这里我只是分配了一个新构造的向量,但这只是一个更一般情况的演示,其中正在构建一些临时右值然后分配给v2(例如,这可能发生在某些返回向量的函数中)。现在打印的是这样的:
分配右值...
[0x28ff08] 构造函数
[0x28fef8] 复制赋值运算符
[0x28fec8] 复制构造函数
[0x28fec8] 析构函数
[0x28ff08] 析构函数
结束分配右值...
它有一对额外的构造函数/析构函数调用来创建然后销毁临时对象。这是一种耻辱,因为在复制赋值运算符内部,正在创建和销毁另一个临时副本。那是额外的工作,毫无意义。
C++11 为我们提供了右值引用,我们可以用它来实现“移动语义”,特别是“移动赋值运算符” [5]。让我们向Intvec添加另一个operator=,重载另一个=函数:
& operator =(Intvec&& other)
{
log( "移动赋值运算符" );
std::swap(m_size, other.m_size);
std::swap(m_data, other.m_data); 返回*这个;
}
该&&语法是新的右值引用。为我们提供了对右值的引用,该引用将在调用后销毁。我们可以使用这个事实来“窃取”右值的内部结构!这打印:
...
[0x28ff08] 构造函数
[0x28fef8] 移动赋值运算符
[0x28ff08] 析构函数
结束分配右值...
这里发生的是我们新的移动赋值运算符被调用,因为一个右值被分配给v2。Intvec(33)创建的临时对象仍然需要构造函数和析构函数
调用,但不再需要赋值运算符内部的另一个临时对象。操作符简单地将右值的内部缓冲区与它自己的缓冲区进行切换,对其进行排列,以便右值的析构函数将释放我们对象自己的不再使用的缓冲区。整洁的。
我将再次提到这个例子只是移动语义和右值引用的冰山一角。正如您可能猜到的那样,这是一个复杂的主题,需要考虑许多特殊情况和问题。我的观点是展示 C++ 中左值和右值之间差异的一个非常有趣的应用。编译器显然知道某个实体何时是右值,并且可以安排在编译时调用正确的构造函数。
最佳实践
移动构造和移动赋值函数加nonexcept
Always declare your moves as noexcept
Failing to do so can make your code slower
Consider: push_back in a vector
显式删除拷贝和构造
explicitly deleted copies and moves
We may not want a type to be copyable / moveable
If so, we can declare fn() = delete
class T {
T(const T&) = delete;
T(T&&) = delete;
T& operator=(const T&) = delete;
T& operator=(T&&) = delete;
};
隐式删除拷贝和构造
Destructor
Copy constructor
Copy assignment
Move assignment
Move constructor
这五个任一定义了,那么所有的都应该定义,
RAII (Resource Acquisition Is Initialization)