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)这个函数的参数带了&&)

相关文章
|
2月前
|
编译器 C++
C++ 11新特性之右值引用
C++ 11新特性之右值引用
43 1
|
6月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
35 1
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
|
6月前
|
存储 安全 C语言
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(上)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
33 2
|
6月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(下)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
36 1
|
5月前
|
编译器 C++ 开发者
C++一分钟之-右值引用与完美转发
【6月更文挑战第25天】C++11引入的右值引用和完美转发增强了资源管理和模板灵活性。右值引用(`&&`)用于绑定临时对象,支持移动语义,减少拷贝。移动构造和赋值允许有效“窃取”资源。完美转发通过`std::forward`保持参数原样传递,适用于通用模板。常见问题包括误解右值引用只能绑定临时对象,误用`std::forward`,忽视`noexcept`和过度使用`std::move`。高效技巧涉及利用右值引用优化容器操作,使用完美转发构造函数和创建通用工厂函数。掌握这些特性能提升代码效率和泛型编程能力。
47 0
|
6天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
29 4
|
7天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
25 4
|
30天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
30天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
30天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1