(基本常识)左值引用、右值引用、万能引用、移动语义和完美转发——原理和代码示例
作者:求一个demo
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处
内容通俗易懂,没有废话,文章最后是面试常问内容(建议通过标题目录学习)
废话不多说,我们直接开始------>>>>>>
一、左值
1、含义:左值是指出现在赋值表达式左侧的表达式,它有持久的状态(名字、存储空间等)。
2、特点:
①有明确的存储空间(地址)。
②可以通过取地址符(&)获取其地址。
③生命周期超过当前表达式(例如类中定义一个变量,运行完这个定义的变量后,后面仍然可以使用这个左值变量)。
④左值一般是:函数名、变量名、++i、--i、*p等。
3、示例:
int a = 10; // a是左值
int* p = &a; // 可以获取a的地址
二、右值
1、含义:右值是指出现在赋值表达式右侧的表达式,通常是指临时对象或字面量。
2、特点:
①没有持久的状态(通常是临时对象)。
②不能获取其地址。
③通常是匿名的临时对象或字面量。
④生命周期仅限于当前表达式。
3、示例:
int b = 20; // 20是右值
int c = a + b; // (a + b)的结果是右值
三、左值引用
1、含义:使用&声明的引用,只能绑定到左值。左值引用可以指向左值,但是不能指向右值,但是前面加const可以指向右值(因为const左值引用延长了右值的生命周期,使得右值在引用的生命周期内保持有效)。
2、特点:
①语法:Type&。
②只能绑定到左值。
③延长被引用对象的生命周期(只要引用存在,对象就不会被销毁)。
④常用于函数参数,避免拷贝。
3、示例:
int x = 10;
int& ref = x; // 正确,x是左值
// int& ref = 10; // 错误,不能绑定到右值
四、右值引用
1、含义:使用&&声明的引用,只能绑定到右值(C++11引入)。std::move可以将左值转化为右值引用(move作用是转移对象的所有权),从而允许对左值进行类似右值的操作。
2、特点:
①语法:Type&&。
②只能绑定到右值(临时对象)。
③允许修改右值。
④主要用于移动语义和完美转发。
⑤可以延长临时对象的生命周期。
3、示例:
int&& rref = 10; // 正确,10是右值
// int&& rref = x; // 错误,不能绑定到左值x
std::string&& s = getStr(); // getStr()返回的临时对象
五、优缺点和对比(左值引用和右值引用)
1、左值引用优缺点:
(1)优点:
①避免对象拷贝,提高效率。
②允许修改原对象(左值引用作为函数参数且不是const引用时,可以在函数内部修改引用绑定的对象)。
void increment(int& num) {
num++;
}
③语法简单直观。
(2)缺点:
①不能绑定到临时对象/右值(左值引用只能绑定有持久内存地址和名称的对象,即左值。而右值是临时创建的,没有持久内存地址)。
②有时会导致不必要的对象保留(左值引用延长对象生命周期,当函数返回一个局部对象的引用时,如果该引用被外部使用<返回对象的引用作为函数参数传入、返回静态局部对象的引用等>,导致局部对象生命周期延长,可能引发内存泄漏或未定义)。
int& getLocalReference() {
int num = 10;
return num; // 错误,返回局部对象的引用,会导致未定义行为
}
int main() {
// int& ref = getLocalReference();
return 0;
}
2、右值引用优缺点:
(1)优点:
①支持移动语义,避免深拷贝(移动语义允许将一个资源从一个对象快速转移到另一个对象,而不是进行深拷贝。右值引用是实现移动语义的关键,通过移动构造函数和移动赋值运算符,可以在对象所有权转移时避免不必要的复制)。
class MyVector {
private:
int* data;
size_t size;
public:
// 构造函数
MyVector(size_t s) : size(s) {
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = i;
}
}
// 移动构造函数
MyVector(MyVector&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
// 析构函数
~MyVector() {
delete[] data;
}
};
int main() {
MyVector v1(1000000);
MyVector v2(std::move(v1)); // 使用移动构造函数,避免深拷贝
return 0;
}
②可以绑定并修改临时对象,并且可以在绑定后对临时对象进行修改(例如在函数返回临时对象时,可以通过右值引用接收并处理)。
int&& getRValue() {
return std::move(10);
}
int main() {
int&& rref = getRValue();
rref = 20;
std::cout << rref << std::endl; // 输出 20
return 0;
}
③右值引用结合forward函数可以实现完美转发(即在函数模板中能将参数以原有的值类别<左值或右值>传递给其他函数)。
void printValue(int& value) {
std::cout << "左值: " << value << std::endl;
}
void printValue(int&& value) {
std::cout << "右值: " << value << std::endl;
}
template<typename T>
void forwardValue(T&& value) {
printValue(std::forward<T>(value));
}
int main() {
int x = 10;
forwardValue(x); // 传递左值
forwardValue(20); // 传递右值
return 0;
}
④提供性能,特别是处理大型对象时(使用右值引用和移动语义可以避免深拷贝,直接转移资源的所有权,提高性能)。
3、左值引用和右值引用的对比
①绑定对象上:左值引用只能绑定左值;右值引用只能绑定右值。
②语法上:左值引用是单个&;右值引用是&&。
③主要用途上:左值引用是避免拷贝;右值引用是移动语义,资源转移。
④生命周期影响上:左值引用是延长被引用的对象;右值引用是延长临时对象。
(2)缺点:
①语法较复杂(C++11新特性)。
②不当使用可能导致对象状态意外改变(右值引用会对临时对象进行修改或者资源转移,如果移动构造或移动运算符没正确处理对象,可能导致对象在移动后处于无效状态,从而导致未定义等)。
六、万能引用
1、含义:使用T&&的形参,既能绑定右值,也能绑定左值(C++11新特性)。
2、特点:
①既可以绑定右值,也可以绑定左值。
②依赖类型推导(模板或auto),如果不是类型推导的时候使用,那么T&&表示右值引用。
③配合forward实现完美转发。
3、示例:
template<typename T>
void foo(T&& param); // 万能引用
auto&& var = x; // 万能引用
七、移动语义
1、含义:转移资源所有权而非拷贝,提高性能。
2、关键操作:
(1)移动构造函数:T(T&& other)。
(2)移动赋值运算符:T& operator=(T&& other)。
3、优点:
①避免不必要的深拷贝(移动语义允许将一个资源从一个对象快速转移到另一个对象,而不是进行深拷贝。右值引用是实现移动语义的关键,通过移动构造函数和移动赋值运算符,可以在对象所有权转移时避免不必要的复制)。
②提升大型对象处理效率。
③使不可拷贝的对象可以转移。
4、示例:
class Buffer {
char* data;
public:
Buffer(Buffer&& other) : data(other.data) {
other.data = nullptr; // 转移所有权
}
};
八、完美转发
1、保持参数原始值类别(左值或右值)转发到其他函数。
2、实现方法:
①结合万能引用和forward。
②保持参数的const属性和值类别。
3、示例:
template<typename T>
void wrapper(T&& arg) {
target(std::forward<T>(arg));
}
九、校招面试常问内容
1、什么是左值右值?如何区分?
(1)左值和右值:左值有持久的状态,可以取地址,通常出现在赋值左侧;右值一般是临时对象或字面量,没有持久的状态,不能取地址,只能出现在赋值右侧。
(2)如何区分?
①能否用&取地址:左值可以,右值不可以。
②生命周期:左值超出当前表达式,右值通常仅限于当前表达式。
③能够放在赋值左侧:左值可以,右值不可以。
2、左值引用和右值引用的区别?
①绑定对象上:左值引用只能绑定左值;右值引用只能绑定右值。
②语法上:左值引用是单个&;右值引用是&&。
③主要用途上:左值引用是避免拷贝;右值引用是移动语义,资源转移。
④生命周期影响上:左值引用是延长被引用的对象;右值引用是延长临时对象。
3、什么是万能引用?它和右值引用有什么不同?
(1)万能引用:万能引用是形式上类似右值引用(T&&),是类型推导中的特殊引用,可以绑定到左值或右值。
(2)区别:
①万能引用只出现在模板参数或auto&&推导中。
②万能引用可以绑定任意值(在类型推导中),右值引用只能绑定右值。
③万能引用主要用于完美转发(万能引用可以接受左值或右值),右值引用主要用于移动语义(资源高效转移,而不是深拷贝,适用于处理大型对象)。
4、移动语义解决了什么问题?如何实现?
(1)解决的问题:
①避免不必要的深拷贝,提高性能(通过移动构造函数和移动赋值运算符,可以在对象所有权转移时避免不必要的复制)。
②使不可拷贝的对象可以转移资源所有权。
③优化临时对象处理
(2)如何实现:
①移动构造函数:T(T&& other)。
②移动赋值运算符:T& operator=(T&& other)。
③使用std::move将左值转为右值引用。
5、std::move和std::forward的区别?
①作用上:move是无条件转为右值引用;forward是有条件(根据模板参数)保持值类别。
②使用场景上:move主要用在移动语义;forward主要用在完美转发。
③参数类型上:move任意类型;forward必须配合万能引用。
④返回值上:move是T&&;forward是T&&。
⑤实质:move是static_cast<T&&>;forward是根据T决定转换方式。
6、完美转发是如何实现的?为什么要用forward?
(1)如何实现?
①结合万能引用接收任意值类别的参数。
②使用forward根据模板参数类型决定转发为左值还是右值。
③保持参数的原始值类别和const属性。
(2)为什么用forward?
①避免转发时丢失值类别信息。
②保证右值参数能正确触发移动语义。
③实现精准的参数传递。
最后,如有不足和错误的地方,期待私信指正!