右值引用和移动构造函数

本文详细介绍了C++11中的右值引用、移动构造函数以及std::move和std::forward的概念和用法。右值引用用于延长右值的生命周期,移动构造函数则用于提高对象构造和赋值的效率,减少不必要的拷贝。std::move将左值转换为右值引用,而std::forward则根据参数类型保持左值或右值性质。文中通过实例展示了这些概念在实际编程中的应用。
摘要由CSDN通过智能技术生成


以下程序都均在 VS2019测试

C++左值和右值

  • C语言中区分左值和右值最为经典的判别方法是:在赋值表达式中,出现在等号左边的是“左值”,在等号右边的称为“右值”;
  • C++ 还有一个更为广泛的说法:可以取地址的、有名字的就是“左值”,不能取地址的、没有名字的就是“右值”;
  • 左值: 可以标识函数、对象的值。
  • C++11中,右值由两个概念构成:将亡值纯右值
  • 纯右值: 就是C++98标准中右值的概念,用于辨识临时变量和一些跟对象关联的值。如:临时变量、字面量、lambda表达式等。
  • 将亡值: C++11 新增的词,和右值引用相关的表达式,这样表达式通常是要被移动的对象。比如:返回右值引用T&& 的函数返回值、std::move的返回值、转换为T&&的类型转换函数的返回值。
  • C++11 程序中,所有值必属于左值、将亡值、纯右值三者之一。
    深入理解C++11 --C++11新特性解析与应用

C++左值引用和右值引用

  • 右值引用是C++11 引入,对一个右值进行引用的类型,用&&表示。
  • C++98中的引用类型称为“左值引用”,声明时通过 &表示。一块内存的多个名称(别名),可以被程序的其他部分访问。
  • 无论左值引用还是右值引用,都必须立即初始化。原因是:引用类型本身不具有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
  • 右值引用不能绑定任何左值,即左值不能用于初始化右值;
  • 只要能够绑定右值的引用类型,都能够延长右值的生命期。
#include <iostream>
int square(int a){ return a * a;}
int main()
{
    int x = 12;    // x是左值
    std::cout << "左值 x : "<< x << ", 左值 x  地址: " << &x << std::endl;
    int& x1 = x;    //正确,x1是x的别名,x1和x指向同一块内存
    std::cout << "左值 x1: " << x1 <<", 左值 x1 地址: "<<  &x1 << std::endl;

    x = square(5);  // x是左值,square(5)是右值,更新内存中的值
    std::cout << "左值 x : " << x << ", 左值 x 地址: " << &x << std::endl;
    //int& x2 = square(5);    // 不能编译,x2是左值,square(5)是右值 

    // int&& y = x;    //右值初始化,错误,编译出错,左值不能用来初始化右值
    int&& y = 20;    //右值初始化,正确
    std::cout << "右值 y : " << y << ", 右值 y  地址: " << &y << std::endl;
   // int&& y1 = y;     //不能编译,在初始化完成之后,y变成了一个左值

    int&& y2 = square(5);    //右值初始化,正确
    std::cout << "右值 y2: " << y2 << ", 右值 y2 地址: " << &y2 << std::endl;
    y2 = 10;
    std::cout << "右值 y2: " << y2 << ", 右值 y2 地址: " << &y2 << std::endl;

    const int& y3 = x * 42;  //正确,我们可以将一个const的引用绑定到一个右值上
    return 0;
}

结果:

左值 x : 12, 左值 x  地址: 000000D9B90FFBD0
左值 x1: 12, 左值 x1 地址: 000000D9B90FFBD0
左值 x : 25, 左值 x  地址: 000000D9B90FFBD0
右值 y : 20, 右值 y  地址: 000000D9B90FFBD4
右值 y2: 25, 右值 y2 地址: 000000D9B90FFBD8
右值 y2: 10, 右值 y2 地址: 000000D9B90FFBD8
  • 常量左值引用是万能型的引用类型(从C++98开始),可以接受非常量左值、常量左值、右值对其初始化。使用右值对其初始化时,还可以像右值引用一样将右值的声明周期延长。不足点就是只能只读,不能修改其内容。
  • 右值引用和移动语句紧密相关,右值存在的最大价值就是实现移动语句,另外一个价值就是用于转发。

左值、右值、类型判断

  • 标准库在<type_traits>头文件提供了3个模板类来判断:
    is_reference 、is_rvalue_reference 、is_lvalue_reference
  • 可配合操作符decltype对变量进行类型推导来得到引用类型
int main() {
	std::cout << "std::string是否是引用: "<<std::is_reference<std::string>::value<<std::endl;
	std::cout << "std::string&是否是引用: "<<std::is_reference<std::string&>::value<<std::endl;
	std::cout << "std::string&&是否是引用: "<<std::is_reference<std::string&&>::value<<std::endl;

	std::cout << "std::string&是否是右值引用: "<<std::is_rvalue_reference<std::string&>::value<<std::endl;
	std::cout << "std::string&&是否是右值引用: "<<std::is_rvalue_reference<std::string&&>::value<<std::endl;

	std::cout << "std::string&是否是左值引用: " << std::is_lvalue_reference<std::string&>::value << std::endl;
	std::cout << "std::string&&是否是左值引用: "<<std::is_lvalue_reference<std::string&&>::value<<std::endl;
	
	std::string szUT = "123";
	std::cout << "szUT是否是引用: " << std::is_reference<decltype(szUT)>::value << std::endl;
	return 0;
}
std::string是否是引用: 0
std::string&是否是引用: 1
std::string&&是否是引用: 1
std::string&是否是右值引用: 0
std::string&&是否是右值引用: 1
std::string&是否是左值引用: 1
std::string&&是否是左值引用: 0
szUT是否是引用: 0

std::move()函数

  • C++11中, std::move()在头文件< utility >中
  • std::move() 的唯一作用是将左值强制转换为右值引用,并不能移动任何东西。在实现上讲,std::move基本等同于一个类型转换:static_cast< T&& >(value)
  • 真正的移动发生在移动构造函数和移动赋值运算符函数中
  • std::move()的作用只是为了让调用构造函数时告诉编译器选择移动构造函数。
  • 注意:使用std::move()输入的参数,在调用之后,不能在使用了,因为已经把所以权转移了,若是指针最好转移后赋值为nullptr
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
    int x = 10;
    std::cout << " x : " << x << ",  x  地址: " << &x << std::endl;
    int& z = x;    //左值引用,z是x的别名,z和x的内存地址相同
    std::cout << " z : " << z << ",  z  地址: " << &z << std::endl;
    // int&& y = x; //y是右值,x是左值,编译失败
    int&& y = std::move(x);  //y是右值,x是左值,y和x的内存地址相同
    std::cout << " x : " << x << ",  x  地址: " << &x << std::endl;
    std::cout << " y : " << x << ",  y  地址: " << &y << std::endl;
    std::cout << " z : " << z << ",  z  地址: " << &z << std::endl;

    std::string szT = "Test";
    std::vector<std::string> vecT;
    vecT.push_back(szT);
    std::cout << "szT: " << szT << std::endl;
    vecT.push_back(std::move(szT));//调用move()函数后,最好不要使用szT,以为所有权已经转移出去了,内容为空
    std::cout << "After move,szT: " << szT << std::endl;
    
    for_each(vecT.begin(), vecT.end(), [](std::string szT) {std::cout << "Vector : "<< szT << std::endl; });
    return 0;
}

结果:

 x : 10,  x  地址: 00000040E8CFFD08
 z : 10,  z  地址: 00000040E8CFFD08
 x : 10,  x  地址: 00000040E8CFFD08
 y : 10,  y  地址: 00000040E8CFFD08
 z : 10,  z  地址: 00000040E8CFFD08
szT: Test
After move,szT:
Vector : Test
Vector : Test

std::move()函数只是将传入的值转换为右值,此外没有其他动作。std::move()用到移动构造函数或者移动赋值运算符,能充分起到减少不必要拷贝的意义。

std::forward函数

  • 完美转发:指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另一个函数。
  • C++11 是通过引入一条所谓“引用折叠”的新语言规则,并结合新的模板推导规则来完成完美转发。
  • std::move(): 无条件地把它的参数转换成右值,实际没有move任何东西
  • std::forward< T >(u): 若u是左值,则传递之后仍然是左值;若u是右值,则传递之后仍然是右值。T为u的类型,输入右值时, std::forward< T >(u)相当于 std::move(),也会把所有权转移走
#include<iostream>
template<typename T>
void Print(T& t) { std::cout << "Left value." << std::endl;}
template<typename T>
void Print(T&& t) { std::cout << "Right value." << std::endl;}
template<typename T>
void Test(T&& t)    
{
    Print(t);   //t无论时左值还是右值,都会转为左值(右值引用参数作为函数的形参,在函数内部再转发该参数的时候会变成一个左值)
    Print(std::forward<T>(t));    //t为右值输入右值,t为左值输入左值
    Print(std::move(t));           // t无论时左值还是右值,都会转为右值
}
int main() 
{
    Test(1); //1是右值
    std::cout << "--------------------------------" << std::endl;
    int x = 1;
    Test(x);  //x是左值
    std::cout << "--------------------------------" << std::endl;
    Test(std::forward<int>(x));  //x是左值,但x表示的是1,而1为右值,传入右值
    return 0;
}
Left value.
Right value.
Right value.
--------------------------------
Left value.
Left value.
Right value.
--------------------------------
Left value.
Right value.
Right value.

代码copy自:c++11 完美转发 std::forward()

移动构造函数

  • 在 C++ 11 标准之前(C++ 98/03 标准中),若想用一个对象初始化一个同类的新对象,只能使用拷贝构造函数;
  • 创建和拷贝时需要对对象的每个成员进行初始化,若成员占内存比较大(指针成员指向非常大的堆内存数据),每次申请内存,消耗比较大。具体可参考 复制构造函数
  • C++11标准后,可使用移动构造函数省去临时变量申请内存的开销。
#include <iostream>
class Demo 
{
public:
    Demo() { std::cout << "Constructor" << std::endl; }
    Demo(const Demo&) { std::cout << "Copy Constructor" << std::endl; }
    Demo(const Demo&&) { std::cout << "Move Constructor" << std::endl; }
    virtual ~Demo() {std::cout << "Destructor Constructor" << std::endl;}
};
Demo  GetDemo()
{
    Demo  objDemo;     //调用构造函数创建对象objDemo
    return objDemo;    //调用移动构造函数把objDemo的内存所有权移给临时对象,然后调用析构函数析构objDemo,只是把objDemo析构了,里面已转移所有权的参数的内存中的内容还在(转移给了临时对象)
}
int main()
{
    Demo  objDemo = GetDemo();      //因为GetDemo()返回值是临时对象,属于右值,因此将调用移动构造函数,会把临时对象的资源转交给对象objDemo,若没有移动构造,会默认调用复制构造
    return 0;    //调用析构函数析构对象objDemo1
}
Constructor
Move Constructor
Destructor Constructor
Destructor Constructor
  • 类内有移动构造函数时,会优先调用移动构造函数;若无移动构造会默认调用拷贝构造。因此,通常由移动构造函数时都会显示声明复制构造函数。

带有指针成员的类时移动构造函数

#include <iostream>
struct Test
{
    int i = 0;
    std::string szT = "123";
};
class Demo
{
public:
    Demo() /*:m_pTest(new Test())*/
    {
        m_pTest = new Test();
        std::cout << "Constructor,m_pTest address: "<< m_pTest << std::endl;
    }
    Demo(const Demo& obj) /*:m_pTest(new Test(*(obj.m_pTest)))*/
    {
        m_pTest = new Test(*(obj.m_pTest));
        //m_pTest = new Test(std::move(*(obj.m_pTest)));   //添加std::move()和不添加这是一样的效果
        std::cout << "Copy Constructor,m_pTest address: " << m_pTest << std::endl;
    }
    Demo(Demo&& obj)/* :m_pTest(obj.m_pTest)*/
    {
        m_pTest = obj.m_pTest;
        obj.m_pTest = nullptr;
        std::cout << "Move Constructor,m_pTest address: " << m_pTest << std::endl;
    }
    virtual ~Demo()
    {
        std::cout << "Destroy Constructor,m_pTest address: " << m_pTest << std::endl;
        if (nullptr != m_pTest)
        {
            std::cout << "Release Memory"<< std::endl;
            delete m_pTest;
            m_pTest = nullptr;
        }
    }

private:
    Test* m_pTest = nullptr;
};
Demo GetDemo()
{
    Demo obDemo;    //调用构造函数,创建对象obDemo,obDemo中m_pTest的地址是A
    std::cout << "================================================" << std::endl;
    return obDemo;   //调用移动构造函数把对象objDemo中的m_pTest的所有权转移给临时对象,然后调用析构函数析构objDemo,因为m_pTest是所有权转移了,所以析构时m_pTest的地址是空的,不需要释放内存,而临时对象的m_pTest的地址是A(若移动构造函数不存在,会默认调用复制构造函数)
}
int main()
{
    Demo objDemo = GetDemo();           //因为GetDemo()返回值是临时对象,属于右值,因此将调用移动构造函数,会把临时对象的资源转交给对象objDemo,对象objDemo中m_pTest的地址是A,
    std::cout << "================================================" << std::endl;
    Demo objDemo1(objDemo);      // 调用复制构造函数,用对象objDemo 初始化objDemo1,obDemo1中m_pTest的地址是B
    std::cout << "================================================" << std::endl;
    Demo objDemo2(std::move(objDemo));    //std::move语句将左值变为右值而避免拷贝构造,调用移动构造函数把对象objDemo中的m_pTest所有权转移给objDemo2,对象objDemo中m_pTest的地址是空,对象objDemo2中m_pTest的地址是A
    std::cout << "================================================" << std::endl;
    return 0;   
    //调用析构函数,析构objDemo2对象,因为objDemo的所有权转移给了objDemo2,所以objDemo2需要释放内存
    //调用析构函数,析构objDemo1对象,因为objDemo1复制了一份objDemo,所以需要释放内存
    //调用析构函数,析构objDemo对象,因为objDemo的所有权转移给了objDemo2,所以不需要释放内存
}

结果:

Constructor,m_pTest address: 0000021CB34BF900
================================================
Move Constructor,m_pTest address: 0000021CB34BF900
Destroy Constructor,m_pTest address: 0000000000000000
================================================
Copy Constructor,m_pTest address: 0000021CB34BF4B0
================================================
Move Constructor,m_pTest address: 0000021CB34BF900
================================================
Destroy Constructor,m_pTest address: 0000021CB34BF900
Release Memory
Destroy Constructor,m_pTest address: 0000021CB34BF4B0
Release Memory
Destroy Constructor,m_pTest address: 0000000000000000

拷贝构造需要对成员变量进行深拷贝,而移动构造不需要,所以移动构造效率更高。

移动构造函数使用示例

自定义字符输出流,重载<<,每个输入字符流自动添加[]包裹


#include <iostream>
#include <sstream>
class CCustomStream
{
public:
    CCustomStream(): m_ssLog(new std::stringstream)
    {
        std::cout << "Constructor,m_ssLog address: " << m_ssLog << std::endl;
    }
    CCustomStream(const CCustomStream& obj)
    {
        m_ssLog = new std::stringstream(std::move(*(obj.m_ssLog)));
        std::cout << "Copy Constructor,m_ssLog address: " << m_ssLog << std::endl;
    }
    CCustomStream(CCustomStream&& obj)
    {
        m_ssLog = obj.m_ssLog;
        obj.m_ssLog = nullptr;
        std::cout << "Move Constructor,m_ssLog address: " << m_ssLog << std::endl;
    }
    virtual ~CCustomStream()
    {
        std::cout << "Destroy Constructor,m_ssLog address: " << m_ssLog << std::endl;
        if (nullptr != m_ssLog)
        {
            std::cout << "Release Memory" << std::endl;
            delete m_ssLog;
            m_ssLog = nullptr;
        }
    }
    std::string str() const
    {
        return m_ssLog->str();
    }
    template<typename T>
    CCustomStream& operator<<(T&& t)
    {
        std::cout << "operator" << std::endl;
        *m_ssLog << "[" << t << "]";
        return *this;
    }
private:
    std::stringstream* m_ssLog;
};

int main()
{
    CCustomStream objCS;        //调用构造函数,创建对象objCS
    std::cout << "objCS: " << objCS.str() << std::endl;
    objCS << "dkjn"<<"152";          //输入值
    std::cout << "objCS: "<<objCS.str() << std::endl;
    std::cout <<"================================================"  << std::endl;
    CCustomStream objCS1(objCS);       //调用复制构造函数,初始化对象objCS1
    std::cout << "objCS1: " << objCS1.str() << std::endl;    //objCS1把objCS内存中的值也复制过来了
    std::cout << "objCS: " << objCS.str() << std::endl;      //objCS内存中的值没了
    objCS1 << "dfh" << "188";
    std::cout << "objCS1: " << objCS1.str() << std::endl;
    std::cout << "objCS: " << objCS.str() << std::endl;    
    std::cout << "================================================" << std::endl;
    CCustomStream objCS2(std::move(objCS));    //调用移动构造函数,初始化对象objCS2
    std::cout << "objC2: " << objCS2.str() << std::endl;    //objCS2和objCS使用同一块内存,但objCS2并没有要objCS的值
    objCS2 << "sfsf" << "65151";
    std::cout << "objC2: " << objCS2.str() << std::endl;
   // std::cout << "objCS: "<< objCS.str() << std::endl;      //因为objCS2是移动的objCS的所有权,所以objCS不还在,会发生异常
    std::cout << "================================================" << std::endl;
    return 0;
}

结果:

Constructor,m_ssLog address: 0000025FE49EAAE0
objCS:
objCS: [dkjn][152]
================================================
Copy Constructor,m_ssLog address: 0000025FE49E5690
objCS1: [dkjn][152]
objCS:
objCS1: [dkjn][152][dfh][188]
objCS:
================================================
Move Constructor,m_ssLog address: 0000025FE49EAAE0
objC2:
objC2: [sfsf][65151]
================================================
Destroy Constructor,m_ssLog address: 0000025FE49EAAE0
Release Memory
Destroy Constructor,m_ssLog address: 0000025FE49E5690
Release Memory
Destroy Constructor,m_ssLog address: 0000000000000000

参考资料

复制构造函数
理解std::move和std::forward
C++:浅谈右值引用
C++右值引用(std::move)
C++移动构造函数
C++深拷贝和浅拷贝(深复制和浅复制)完全攻略
C++的std::move与std::forward原理大白话总结
C++11 std::move和std::forward
c++11 特性之std-move的使用和原理
c++11 之右值引用、移动语义、std::move
C++11右值引用和std::move语句实例解析
深入理解C++11 --C++11新特性解析与应用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qzy0621

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值