c++左值、右值引用和移动语义

简介: c++左值、右值引用和移动语义

C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。

一、左值和右值

左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象。

区分表达式的左右值属性:如果可对表达式用&符取址,则为左值,否则为右值

比如:

int a=10;
int b=a;

对于a=10,由于10是临时的,表达式结束这个值就不存在了,因此是右值。而a=10中,a作为接受的变量,是表达式结束后仍然存在的对象,因此是左值。

对于b=a,由于a、b都是持久存在的,因此都是左值。

二、左值引用&

&只能用作左值引用,因此对于下面的test(int& x),中的参数x一定是左值

对于一个常数10,它是右值,因此没法调用test(10);

void test(int& x){//这个x一定是左值
    cout<<x<<endl;
}
int main(){
    int a=10;
    //test(10); //传入的是右值,因此会报错
    test(a); //传入的是左值
}

三、右值引用&&

右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。

无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内

存,只是该对象的一个别名。

通过右值引用的声明,该右值又“重获新生”,其生命周期其生命周期与右值引用类型变量的生命周期一

样,只要该变量还活着,该右值临时量将会一直存活下去。

如下面代码,10作为右值,正常情况下,在使用完后该临时变量就应该消失了。但是int&& x相当于给这个10取了别名为x,也叫右值引用。它的好处是避免了拷贝。

void test(int&& x){
    cout<<x<<endl;
}
int main(){
    int a=10;
    test(10); 
}

&&总结:

  • 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值(也就是说对于&,只能用作左值引用,而&&既可以作为左值引用也可以作为右值引用,取决于传入的参数是左值还是右值)
  • auto&& 或函数参数类型自动推导的 T&& 是一个未定的引用类型,被称为 universal references,
    它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。
  • 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引 用。当 T&& 为
    模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右 值引用
  • 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值

应用:右值引用优化性能,避免深拷贝

A(const A& a)是拷贝构造函数,如果没有移动构造函数A(A&& a)的情况下,执行 A a = Get(false),调用的是拷贝构造函数,它会将Get(flase)返回的结果拷贝给a,然后执行析构函数,但是这样会造成性能的损耗。

因此可以使用移动构造函数A(A&& a),由于Get(false)得到的是一个右值,因此会调用移动构造函数,只需要将需要构造的对象的指针拷贝就行了,将被移动的对象的指针置为空,就完成了移动拷贝构造。(避免了深拷贝)

移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少

不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护

(创建和销毁)对性能的影响。

#include <iostream>
using namespace std;
class A
{
public:
    A() :m_ptr(new int(0)) {
        cout << "constructor A"  << endl;
    }
    A(const A& a) :m_ptr(new int(*a.m_ptr)) {
        cout << "copy constructor A"  << endl;
    }
    A(A&& a) :m_ptr(a.m_ptr) {
        a.m_ptr = nullptr;
        cout << "move  constructor A"  << endl;
    }
    ~A(){
        cout << "destructor A, m_ptr:" << m_ptr  << endl;
        if(m_ptr)
            delete m_ptr;
    }
private:
    int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
    A a;
    A b;
    cout << "ready return" << endl;
    if (flag)
        return a;
    else
        return b;
}
int main()
{
    {
        A a = Get(false); // 正确运行
    }
    cout << "main finish" << endl;
    return 0;
}

四、移动语义 move

我们知道移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借组移动语义来优化性

能呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语

义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝

int main()
{
  MyString a;
  a = MyString("Hello"); // move
  MyString b = a; // copy
  MyString c = std::move(a); // move, 将左值转为右值
  return 0;
}

五、完美转发forward

forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左

值,若是右值,则传递之后仍然是右值。

对于一个函数

Template<class T>
void func(T &&val);

根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。

但要注意,引用以后,这个val值它本质上是一个左值!

看下面例子

int &&a = 10;
int &&b = a; //错误

注意这里,a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这

是不对的

因此我们有了std::forward()完美转发,这种T &&val中的val是左值,但如果我们用std::forward (val),

就会按照参数原来的类型转发;

int &&a = 10;
int &&b = std::forward<int>(a);

例子:

#include <iostream>
using namespace std;
template <class T>
void Print(T &t)
{
    cout << "L" << t << endl;
}
template <class T>
void Print(T &&t)
{
    cout << "R" << t << endl;
}
template <class T>
void func(T &&t) // 左值右值都只能走这个
{
    cout << "func(T &&t)" << endl;
    Print(t);  // L   (什么都不做,默认当作左值)
    Print(std::move(t));       // 肯定调用"R"
    Print(std::forward<T>(t)); // 不确定L  R  ,如果要完美往下层函数转发加forward
}
int main()
{
    cout << "-- func(1)" << endl;
    func(1);  //  1是R
    int x = 10;
    int y = 20;
    cout << "-- func(x)" << endl;
    func(x);  // x本身是左值     (因此后续调用Print(std::forward<T>(t))的时候,传入的依然是左值)
    cout << "-- func(std::forward<int>(y))" << endl;
    func(std::forward<int>(y)); // std::forward<int>(y)变成右值  (如果传入的参数,本身不带有&&符号,那么forward就把它当作右值)
    return 0;
}

可以看到结果,分别是3个fun调用的输出。

对于func(1)1是右值。func(T &&t)因此这边t的语言应该是右值,但是由于编译器会将已命名的右值看作左值,因此Print(t)中传入的是左值,Print(std::move(t))这里肯定是调用右值,因为显示地转为右值了。Print(std::forward(t)),传入的值是根据&&类型的值,原来的类型决定,因为传入&&前,是右值,因此Print(std::forward(t))在forward调用后是右值。

其他3种func情况也是类似的理解。

另外对于 func(std::forward(y));由于y本身并不带有&&符号,因此forward后,默认是变成右值的。(什么是带&&符号?就是func(T &&t)这个函数的参数带了&&)

相关文章
|
3月前
|
存储 安全 C++
【C++11】右值引用
C++11引入的右值引用(rvalue references)是现代C++的重要特性,允许更高效地处理临时对象,避免不必要的拷贝,提升性能。右值引用与移动语义(move semantics)和完美转发(perfect forwarding)紧密相关,通过移动构造函数和移动赋值运算符,实现了资源的直接转移,提高了大对象和动态资源管理的效率。同时,完美转发技术通过模板参数完美地转发函数参数,保持参数的原始类型,进一步优化了代码性能。
53 2
|
5月前
|
编译器 C++
C++ 11新特性之右值引用
C++ 11新特性之右值引用
68 1
|
9月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
56 1
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
|
9月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(下)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
61 1
|
8月前
|
编译器 C++ 开发者
C++一分钟之-右值引用与完美转发
【6月更文挑战第25天】C++11引入的右值引用和完美转发增强了资源管理和模板灵活性。右值引用(`&&`)用于绑定临时对象,支持移动语义,减少拷贝。移动构造和赋值允许有效“窃取”资源。完美转发通过`std::forward`保持参数原样传递,适用于通用模板。常见问题包括误解右值引用只能绑定临时对象,误用`std::forward`,忽视`noexcept`和过度使用`std::move`。高效技巧涉及利用右值引用优化容器操作,使用完美转发构造函数和创建通用工厂函数。掌握这些特性能提升代码效率和泛型编程能力。
72 0
|
2天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
50 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
50 5
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
40 5