传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名
1. 左值(lvalue)
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名
特点:
示例:
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
是一个表达式,可以取地址
2. 右值(rvalue)
定义: 右值是不能被持久访问的临时值,通常是表达式的结果或常量值。右值没有具体的内存地址,或者它的地址无法被直接访问。右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址
特点:
示例:
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
3. 引用和表达式类型
(1)左值引用
左值引用只能绑定到左值。
int x = 10;
int& ref = x; // OK,x 是左值
(2)右值引用
右值引用是 C++11 引入的一种引用类型,只能绑定到右值。
int&& rref = 10; // OK,10 是右值
int&& rref2 = std::move(x); // std::move 将 x 转换为右值
C++ 将表达式进一步分为以下几类:
示例:
int x = 10;
int&& rref = std::move(x); // std::move(x) 是亡值
int y = x + 10; // x + 10 是纯右值
特性 | 左值(lvalue) | 右值(rvalue) |
---|---|---|
是否可取地址 | 可以取地址(&a 有意义) | 通常不能取地址 |
生命周期 | 通常比表达式更长 | 生命周期短,通常是临时的 |
赋值能力 | 可以出现在赋值号左边或右边 | 只能出现在赋值号的右边 |
引用绑定 | 可以绑定到左值引用(T&) | 不能绑定到左值引用,但能绑定到右值引用(T&&) |
(1)赋值规则
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
(2)函数返回值
如果函数返回左值,则返回的值可以被修改:
int& getValue(int& a) {
return a; // 返回左值引用
}
int main() {
int x = 10;
getValue(x) = 20; // OK,修改 x
}
如果函数返回右值,则返回的值是临时的,无法直接修改:
int getValue() {
return 42; // 返回右值
}
int main() {
int x = getValue(); // OK
// getValue() = 20; // 错误:右值不能被修改
}
(3)右值引用的应用 右值引用常用于 移动语义 和 避免拷贝,比如:
#include <iostream>
#include <string>
int main() {
std::string a = "Hello";
std::string b = std::move(a); // 将 a 的资源转移给 b
std::cout << b << std::endl; // 输出 Hello
std::cout << a << std::endl; // a 的内容不再定义
return 0;
}
总结
引用本身就是为了减少拷贝,提高效率(传参与返回值引用),右值设计的目的也是为了解决左值引用的短板(没有彻底解决返回值的问题)
string& func2();
如果返回值是func2中局部对象,不能用引用返回
下面是我们自定义的string
namespace myown
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
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)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
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];
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';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
namespace myown
{
myown::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
myown::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
这个函数返回的是局部变量str,当函数结束时,局部变量 str 的生命周期结束,因此返回的是一个临时对象。如果试图用左值引用绑定这个返回值,就会产生编译错误
例如:
myown::string& result = to_string(42); // 错误:不能将临时对象绑定到左值引用
原因:
str
是一个临时对象,它的生命周期仅限于当前表达式。右值引用(T&&
)专门设计用于绑定临时对象(右值)。例如:
myown::string&& result = to_string(42); // OK,可以绑定右值
右值引用可以安全地延长资源的生命周期,避免因局部对象销毁导致的问题。
2. 解决问题的方案
(1)返回值不使用引用 修改调用方式,避免绑定到左值引用。直接接收返回值,使用右值或拷贝构造:
myown::string result = to_string(42); // OK,将返回值拷贝到 result 中
(2)使用右值引用 如果需要明确处理返回值,可以使用右值引用绑定返回值:
myown::string&& result = to_string(42); // OK,右值引用延长临时对象生命周期
(3)返回 const
左值引用
虽然不建议直接返回局部对象的引用,但如果需要返回一个类的成员或某些长期存在的对象,可以使用 const
左值引用:
const myown::string& to_string_ref(int value) {
static myown::string str; // 使用静态变量,避免生命周期问题
// 填充 str...
return str;
}
注意:
我们这里屏蔽移动构造
to_string的返回值是一个右值,用这个右值构造ret2,如果没有移动构造,调用就会匹配调用拷贝构造,因为const左值引用是可以引用右值的,这里就是一个深拷贝。
1. 移动构造函数
定义
string(string&& s)
: _str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
目的
string&& s
,表示将从另一个临时对象中转移资源到当前对象。swap
方法将右值 s
的资源交给当前对象,而避免了重新分配和拷贝内存,从而提升性能。过程分析
string&& s
是一个右值引用,只能绑定到右值(例如临时对象或使用 std::move
转换后的对象)。_str
设置为 nullptr
,并将 _size
和 _capacity
设置为 0,确保当前对象处于安全的初始状态。swap(s)
: s
的内部数据(如字符串内容和容量等)与当前对象交换。s
的资源被转移到当前对象,而 s
被置于空的状态。s
的资源,而不需要重新分配内存。优点
s
被置于有效但为空的状态,不会对系统造成影响。该函数在以下场景中被调用:
将一个右值(如临时对象)用于构造另一个对象:
string s1("hello");
string s2(std::move(s1)); // 调用移动构造函数
返回一个局部对象:
string createString() {
return string("temp");
}
string s = createString(); // 调用移动构造函数
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
定义
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
目的
string&& s
,将右值 s
的资源转移到当前对象。string&& s
是右值引用,只能绑定到右值。swap(s)
: s
的资源交换。s
的资源,而 s
被置于空的状态。*this
以支持链式赋值,如 s1 = s2 = std::move(s3)
。当一个右值用于赋值时,调用移动赋值运算符:
string s1("hello");
string s2;
s2 = std::move(s1); // 调用移动赋值运算符
性能优势
示例代码 结合上述移动构造函数和移动赋值运算符的实际用法:
int main() {
string s1("hello");
string s2(std::move(s1)); // 调用移动构造函数
string s3;
s3 = std::move(s2); // 调用移动赋值运算符
return 0;
}
输出:
string(string&& s) -- 移动语义
string& operator=(string&& s) -- 移动语义
std::move
是 C++11 引入的一个标准库函数,主要用于转换左值为右值。它并不真正“移动”任何内容,而是提供了一种显式的方式,告诉编译器可以“偷走”资源,启用 移动语义,而不进行昂贵的深拷贝操作。它是实现 移动构造函数 和 移动赋值运算符 的关键工具。
1. std::move
的作用
将左值转换为右值:
std::move
实际上并不会执行任何内存移动或拷贝操作,它的作用仅仅是将一个左值转换为右值引用(T&&
),允许后续的移动操作。
示例:
int a = 42;
int&& b = std::move(a); // 将 a 转换为右值引用 b
启用移动语义:
通过 std::move
,你可以显式地告诉编译器某个对象可以安全地从一个地方转移到另一个地方,而不是拷贝数据。这对于性能优化非常重要,尤其是在涉及动态内存管理的类(如 std::vector
、std::string
)时,可以避免不必要的深拷贝。
移动语义通常是通过移动构造函数和移动赋值运算符实现的,这些函数会使用 std::move
来将资源从一个对象转移到另一个对象,而不进行复制。
2. std::move
的工作原理
std::move
将传入的对象转换成右值引用,使得该对象能够被“移动”。右值引用是 C++11 引入的一个新特性,它允许资源从一个对象转移到另一个对象,避免了拷贝的开销。
template <typename T>
T&& move(T&& arg) {
return static_cast<T&&>(arg); // 将传入的参数转换为右值引用
}
解释:
std::move
实际上使用 static_cast
将传入的对象转换为右值引用,并不会真的做任何移动操作。3. std::move
的典型用法
(1)移动构造函数
通过 std::move
,可以将一个对象的资源转移到新对象,而不是进行拷贝。
#include <iostream>
#include <vector>
class MyClass {
public:
MyClass(std::vector<int> data) : data_(std::move(data)) {
std::cout << "Move constructor called" << std::endl;
}
private:
std::vector<int> data_;
};
int main() {
std::vector<int> v = {1, 2, 3};
MyClass obj(std::move(v)); // 使用 std::move 来移动资源
// v 现在是空的
return 0;
}
在这个例子中,std::move(v)
将 v
转换为右值引用,并把 v
的内容移动到 obj
中,而不是拷贝。
(2)移动赋值运算符
移动赋值运算符允许通过 std::move
将一个对象的资源转移到另一个对象,而不是进行深拷贝。
#include <iostream>
#include <vector>
class MyClass {
public:
std::vector<int> data_;
// 移动赋值运算符
MyClass& operator=(MyClass&& other) {
std::cout << "Move assignment called" << std::endl;
if (this != &other) {
data_ = std::move(other.data_); // 使用 std::move 进行资源移动
}
return *this;
}
};
int main() {
MyClass obj1, obj2;
obj1.data_ = {1, 2, 3};
obj2 = std::move(obj1); // 使用移动赋值运算符
// obj1 现在的 data_ 是空的
return 0;
}
在上面的例子中,obj2 = std::move(obj1);
使用 std::move
将 obj1
的数据资源转移到 obj2
中,而不是复制。
std::move
的作用:将左值转换为右值引用,启用移动语义,以避免深拷贝的开销。std::move
主要用于在移动构造函数、移动赋值运算符以及容器类等地方提高效率。std::move
后,原对象的状态变得不可预测,因此应避免在移动后访问该对象。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; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(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;
}
PerfectForward
函数模板展示了 完美转发 的用法,依靠 std::forward<T>(t)
保持传入参数的值类别(左值或右值)及其 const
属性。
运行结果分析 运行结果如下:
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
PerfectForward(10);
10
是一个右值。PerfectForward
的模板参数 T
被推导为 int
(值类别为右值)。std::forward<T>(t)
等价于 std::move(t)
,将 t
作为右值传递。Fun(int&& x)
,输出 “右值引用”。int a; PerfectForward(a);
a
是一个左值。T
被推导为 int&
(因为 a
是左值)。std::forward<T>(t)
等价于 t
(保持左值属性)。Fun(int& x)
,输出 “左值引用”。PerfectForward(std::move(a));
std::move(a)
将 a
转换为右值。T
被推导为 int
(因为 std::move(a)
是右值)。std::forward<T>(t)
等价于 std::move(t)
,保持右值属性。Fun(int&& x)
,输出 “右值引用”。const int b = 8; PerfectForward(b);
b
是一个 const
左值。T
被推导为 const int&
(因为 b
是 const
左值)。std::forward<T>(t)
等价于 t
(保持左值属性)。Fun(const int& x)
,输出 “const 左值引用”。PerfectForward(std::move(b));
std::move(b)
将 b
转换为右值,但 b
是 const
类型,因此 std::move(b)
的值类别是 const int&&
。T
被推导为 const int
。std::forward<T>(t)
等价于 std::move(t)
,保持右值属性。Fun(const int&& x)
,输出 “const 右值引用”。完美转发与万能引用
T&&
在模板中是一种特殊形式的 万能引用(也称为 转发引用),其行为取决于传入参数的值类别:
T
被推导为 类型&
。T&&
展开为 类型& &
,折叠规则将其简化为 类型&
(左值引用)。std::forward<T>(t)
等价于 t
,保持左值属性。T
被推导为 类型
。T&&
展开为 类型&&
(右值引用)。std::forward<T>(t)
等价于 std::move(t)
,保持右值属性。T&&
是万能引用,可以接受左值、右值、const
类型等不同类别的参数。std::forward<T>(t)
实现完美转发,保持参数的原始值类别及 const
性质。std::forward<T>(t)
在左值情况下返回左值,在右值情况下返回右值。总结:
在这段代码中:
std::string&& s1 = std::string("123");
std::string&& s1
是一个 右值引用,用于绑定到一个 右值(std::string("123")
)。
std::string&&
定义 s1
,可以直接引用这个右值。
此时,s1
成为右值引用,绑定到该临时对象。虽然是右值引用,但s1
本身是一个左值,因为它有名字(可以通过名字访问它)。
std::string&&
只限制它可以绑定右值,但它本质上是一个普通的变量,存储了右值的引用。
std::string&& s1
定义后,s1
变成了一个左值变量,可以像普通变量一样操作它的地址。
右值引用本身是左值,这样的意义是为了移动构造和移动赋值,转移资源的语法是自洽的
右值引用的属性如果是右值,那么移动构造和移动赋值,要转移资源的语法逻辑是矛盾的,右值是不能被改变的
string&& s1 = string("11111");
string& s2 =s1;
s1 是一个右值引用变量,绑定了临时对象 std::string("11111")
。
s2 是一个左值引用,引用了 s1 所绑定的对象。
此时,s1 和 s2 都指向同一个对象(std::string("11111"))
移动语义与赋值构造针对的是自定义类型的深拷贝的类的效率提升,因为深拷贝的类才有转移资源的说法,对于内置类型和浅拷贝自定义类型,没有移动系列函数
list<int> lt;
lt.push_back(10);
int x = 20;
lt.push_back(x);
void push_back(const T& value); // 插入一个左值
void push_back(T&& value); // 插入一个右值(支持移动语义)
对于 int,移动构造和拷贝构造的效果相同,因为 int 是一个标量类型,没有动态资源