C++11左值引用和右值引用
目录
1、什么是左值?什么是左值引用?
1.1左值
1.2左值引用
编辑
2.什么是右值?什么是右值引用?
2.1右值
2.2右值引用
3.左值引用和右值引用比较
3.1左值引用
3.2右值引用
4.右值引用的使用场景
5.完美转发
1、什么是左值?什么是左值引用?
1.1左值
左值(lvalue)是“左值表达式”(lvalue expression)的简称,它指的是可以出现在赋值操作符左边的表达式,我们可以获取它的地址,一般可以对它赋值(const修饰符后的左值,不能赋值)。
特点:
-
持久性:左值通常表示一个对象(如变量、数组元素等),这个对象在内存中有一个固定的存储位置,其生命周期是明确的。
-
可寻址性:左值可以被取地址,因为它们有固定的内存地址。
-
可赋值性:左值可以出现在赋值操作符的左边,表示它可以被赋值。
int a = 10; // a 是一个左值
int* p = &a; // 可以取 a 的地址,因为 a 是左值,p和*p也都是左值
a = 20; // a 可以出现在赋值操作符左边,因为它是一个左值
const int b = 10; // b也是一个左值
常见的左值:
-
变量名(如int a中的
a
) -
数组元素(如
arr[0]
) -
对象的成员变量(如
obj.member
)
1.2左值引用
左值引用(lvalue reference)就是给左值取别名。左值引用的声明方式是在类型后加&
,例如int&
表示一个对int
类型左值的引用。
2.什么是右值?什么是右值引用?
2.1右值
右值(rvalue)是“右值表达式”(rvalue expression)的简称,它表示一个临时的、不可持久的值,是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
特点:
-
临时性:右值通常是临时对象或字面量,它们的生命周期较短,通常只在表达式求值期间存在。
-
不可持久性:右值没有固定的内存地址,因此不能被取地址。
2.2右值引用
右值引用(rvalue reference)就是给右值取别名。右值引用的声明方式是在类型后加&&
,例如int&&
表示一个对int
类型右值的引用。
3.左值引用和右值引用比较
3.1左值引用
1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值。
3.2右值引用
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。move函数本身不会移动任何资源,它只是将对象的类型从左值转换为右值引用。
4.右值引用的使用场景
左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
namespace my
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造
// s2(s1)
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
// 扩容
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
const char* c_str() const
{
return _str;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
string getString()
{
string str("12345");
return str;
}
}
int main()
{
my::string s1 = my::getString();
return 0;
}
上面图片展示的内容是编译器没有进行优化的效果
左值引用的短板:当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。string getString()函数中可以看到,这里只能使用传值返回,如果是一些旧一点的编译器,传值返回会导致是两次拷贝构造。如果数据量大,拷贝构造非常的耗费时间和资源(先要开辟大的空间,然后将数据逐个复制),为了提高效率,在my::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
5.完美转发
右值引用本身的属性是左值,只有右值引用本身的属性是左值才能进行swap交换它的资源
如果s是右值,就不能修改s,后面一系列的行为运行不了
模板中的&& 万能引用
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
如何让左值对于左值引用,右值对应右值引用,要用到完美转发
完美转发(Perfect Forwarding)是C++中一个高级特性,主要用于模板编程。它的目的是将函数模板的参数完美地转发给另一个函数,同时保留参数的值类别(左值或右值)。完美转发的核心是通过std::forward
实现的,它确保了参数的类型和值类别在转发过程中保持不变。