C++高级使用技巧:左/右值及其引用类型、std::move函数

1.左值、右值

        数据类型(data type)和值类别(value category),是不同的两个概念。

        数据类型指的是数据在存储时的所占的字节大小。除了内置类型外,还有自定义类型,自定义类型表示了一种特定且具体的数据结构,比如类、结构体等。

        值类别,一般来说就是变量的左/右值属性。具体地,值类别表征了数据的存储位置,可以通过是否能取地址来判断左右值。如果能取地址,说明这个变量是左值,我们可以通过地址修改它,如果不能取地址,则变量是右值,我们不能通过地址修改它。

int a = 100;

        在上面表达式 中,100赋值给到变量a,a对应一个内存地址,因为可以获取/修改a的地址,所以a为左值(a在表达式的左边,这是左的含义);表达式右边的100就是一个右值,因为我们既无法获取字面量的内存地址,又无法通过其地址修改指向值。本质上,a对应的地址是位于进程地址空间上的栈区的地址,数据类型表示从该地址往后固定数目N个字节的空间被进程使用了,要以N字节为一个整体修改该地址上的内容。

        右值不能取地址是基于系统安全的,它不是因为没有地址,而是因为这个地址位于只读数据区或者说该地址上的数据只有在程序运行后才能被系统读取

        而栈区,堆区,静态区都是系统允许访问的区域,这些区域拥有被写入的权限。但是像什么字面常量,临时变量(隐式类型转换表达式产生的中间值,函数返回产生的中间值...),匿名对象就是右值,因为程序编译后,它们位于代码区或者没有修改这些数据的必要,只有在程序运行后,为了运行程序,系统才会访问这些空间。

        只要数据所在的区域没有权限访问,这些数据就是右值;有权限访问的区域上存储的数据就是左值。并且这个权限不是语言限制的,而是系统限制的,语言位于系统之上,我们可以突破语言的限制,但是底层系统的限制我们无法突破,也不能突破。

        常见的右值如下所示:

//1、字面值
"c++"; true; 99; 123.456; nullptr; 

//2、算数/逻辑表达式
a * b; (a > 0) || (b != -1); 1+2; 

//3、函数返回值
f();

//4、lambda表达式
[=](int a)->int{return ++a;} 

//5、栈上的匿名对象
Object(); 

//6、后置自增和自减表达式
num++;

2.左值引用、右值引用

        左值引用和右值引用都是变量的别名而已,左值引用就是左值的别名,右值引用就是右值的别名。所以左值引用只能绑定左值,右值引用只能绑定右值,我们不能将一个右值引用类型变量绑定到一个右值引用。因为变量都是左值!我们可以对右值引用类型变量取地址!但是,const左值引用可以绑定到右值。

int i = 42; //i是左值,可以对i取地址
int &r = i; //r是左值引用,绑定左值i

int &&rr = i; //ERROR!i是左值,不能绑定到右值引用rr
int &&rr3 = rr; //ERROR!rr是一个右值引用类型的变量,是一个左值。

int &r2 = rr; //rr是右值引用类型变量,变量都是左值。
int &r3 = r; //r是左值引用类型变量,变量都是左值。

int &&rr_result = Add(1, 2); //不能对表达式取地址,所以表达式结果是一个右值,可以绑定到右值引用
int &&rr2 = i * 42; //不能对表达值取地址,所以表达式结果是右值,可以绑定到右值引用
int &&rr1 = 42; //不能对字面常量取地址,所以字面常量是右值,可以绑定到右值引用

const int &r2 = i * 42; //不能对表达值取地址,所以表达式结果是右值,可以绑定到const左值引用

3.std::move

        移动和拷贝两者最大的区别是:拷贝会产生新的内存,而移动不会。通过拷贝获得的对象状态改变,不会影响到源对象,而通过移动获得的对象状态改变,会影响到源对象,而且被移动的源对象失去所有资源的控制权!拷贝会增加内存申请和数据复制的开销,而移动不会。移动语义主要是性能优化:将昂贵的对象从内存中的一个地址移动到另外一个地址的能力,同时窃取源资源以便以最小的代价构建目标。

        move作用主要可以将一个左值转换成右值引用,在对象拷贝的时候,它们不会产生一行代码同时原对象也会清空, 这样可以减轻资源创建和释放所带来的资源消耗。

class TestObj
{
private:
    std::string m_data;
public:
    TestObj(std::string data): m_data(data) {}
    TestObj(const TestObj& rhs): m_data(rhs.m_data) 
    {
        std::cout << "Copy Constructor" << std::endl;
    }
    TestObj(TestObj&& rhs): m_data(std::move(rhs.m_data)) 
    {
        std::cout << "Move Constructor" << std::endl;
    }
};

void foo(TestObj obj)
{
    std::cout << "foo" << std::endl;
}

int main()
{
    TestObj obj1("Hello World!");
    foo(obj1); // 复制构造函数
    foo(std::move(obj1)); // 移动构造函数
    return 0;
}

        在上面的例子中,定义了一个名为TestObj的类,其中包含了复制构造函数和移动构造函数。在main函数中,我们先使用obj1调用foo函数,会触发复制构造函数的调用。然后,我们使用std::move(obj1)调用foo函数,会触发移动构造函数的调用,这种情况下数据被“移动”到了新的对象中,避免了不必要的数据复制。需要注意的是,在调用完std::move后,obj1的状态已经被移动到了新的对象中,其值已经不再可用。

        move函数应该被广泛使用,特别是在以下情况下:

        1)需要从一个对象中“移动”大量数据到另一个对象中

        2)需要将一个对象传递给另一个函数,但是不需要保留该对象的状态

        需要注意的是,在使用move函数的过程中需要谨慎,因为它具有破坏性。一旦一个对象被移动,原对象的状态就不再可用。因此,在使用move函数时应该遵循以下原则:

        1)只有在需要移动对象时才使用move函数

        2)在移动对象之后,避免使用原对象

        3)避免多次移动同一个对象

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

弘毅—至善

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

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

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

打赏作者

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

抵扣说明:

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

余额充值