自学内容网 自学内容网

c++ 左值、右值、左值引用(&)、右值引用(&&),移动构造和std::move

左值和右值 不是等于号的左边和右边 !!(一部分场景下是这样)

右值可以描述成一个临时值

左值

英文简写为“lvalue”,是“locator value”

最初指的是可出现在赋值语句左边的实体(就是等于号左边的值)

本质上是是 可以取到它的地址的值的内存区域(一般可以通过&)

常见的左值有:
1、变量(对象);

int i = 0;
i = 1;// i是变量(对象),左值

2、const 变量(对象);

const int ci = 5;
//ci = 6;// ci是const变量(对象),是左值但不能出现在赋值号左边

3、对指针解引用,*(指针);*pi = 1;
4、数组元素,a[1]
5、结构体成员、类成员,s.m_aps->ma

st.m_a = 1;// 结构体成员,左值
pst->m_b = 2;

右值

英文简写为“rvalue”,是"read value"的缩写

指一种表达式,其结果是值而非值所在的位置。一般是没有明确的内存位置,无法使用&获取地址,值不可被修改的。

另外,很多时候左值可以当右值使用。

常见的右值有:
1、字面常量(立即数),引号括起的字符串除外(它们由其地址表示),42、‘a’;
2、算术运算符(+、-、*、/、%、正号、负号)的求值结果,1+2;
3、逻辑运算符(&&、||、!)的求值结果,a!=10;
4、关系运算符(>、>=、<、<=、==、!=)的求值结果,a>10 && a<100;
5、函数的非引用返回值。这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在;

i = get100();// 函数的非引用返回值,右值

6、当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值;否则运算的结果是右值。

左值引用

为了与右值引用区分开,把C++11之前出现的引用成为左值引用。

左值引用就是C++中的引用类型,是己定义的变量的别名,主要用作函数形参或返回值。

// g++ 13_lvalue_reference.cpp
int get100()
{
return 100;
}
int main()
{
int i = 0;
int &ri = i;// 左值引用
//int &*pri = &ri;// 不能定义引用的指针。报错:cannot declare pointer to ‘int&’
//int &&rri = ri;// 不能定义引用的引用。在C++11标准里,这是一个右值引用

int *pi = &i;
int *&rpi = pi;// pi是左值,rpi是指针的引用
int &r_pi = *pi;// *pi是左值,可以初始化左值引用

int arr[5] = {0};
int &rarr = arr[0];// arr[0]是左值,可以初始化左值引用

// const 左值引用可以被初始化为一些左值
const int &cri = i;// const 左值引用
//const int *&crpi = pi;// 报错:用 ‘int*’ 初始化 ‘const int*&’ 无效
const int &cr_pi = *pi;// const 左值引用
const int &crarr= arr[0];

// const 左值引用可以被初始化为右值   去掉const不行
const int &ri0 = 42;
const int &ri1 = 'a';
const int &ri2 = 1+2;
const int &ri3 = (ri1 != 'a');
const int &ri4 = (ri1 >= 'a');
const int &ri5 = get100();// 函数的非引用返回值,右值
const int &ri6 = i>0?1:0;
//ri0 = 1;// 报错,const引用是只读的,其值不能修改
return 0;
}

右值引用

右值引用:这是为了支持移动操作,C++11标准引入的一种新的引用类型

所谓右值引用就是必须绑定到右值的引用。

通过 && 而不是 & 来获得右值引用。

怎样定义右值引用?右值引用使用&&来定义,也是必须初始化,初始化后无法改变其绑定的对象。定义右值引用格式:类型 &&引用名称 = 右值;。定义一个右值引用可以参考下面代码:

int &&rl = 13;
int *pi = &rl;

将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符&用于 13,但可将其用于 rl。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。

右值引用只能绑定到一个右值,右值要么是字面常量,要么是在表达式求值过程中创建的临时对象;

而临时对象有两个特点:
1、该对象将要被销毁;
2、该对象没有其他用户再使用它。
这就意味着,右值引用的代码是最后使用这个对象的了,可以自由地接管所引用的对象的资源。

在这里插入图片描述

如上图,变量a、b相加之后会产生一个值,这个值就是临时量,但它在内存中肯定是存在某个地址的,没有右值引用之前,这个值使用完就会被销毁,我们也不会知道它的内存地址。现在,这块内存可以被右值引用关联,关联后,右值引用甚至可以改变内存的内容,等右值引用使用完再销毁。

// g++ 13_rvalue_reference.cpp  -std=c++11
#include <iostream>
using namespace std;
int get100()
{
return 100;
}

void fun(int &&rri)// 右值引用作为函数形参
{
rri = 0;
}
int main()
{
// 1、右值引用必须被初始化为右值
int &&ri0 = 42;// 将 42 存到一个临时量,然后引用这个临时量
int &&ri1 = 'a';// 将 'a' 存到一个临时量,然后引用这个临时量
int &&ri2 = 1+2;// 将 1+2 存到一个临时量,然后引用这个临时量
int &&ri3 = (ri1 != 'a');
int &&ri4 = (ri1 >= 'a');
int &&ri5 = get100();// 函数的非引用返回值,右值
int &&ri6 = ri0>0?1:0;

// 2、右值引用的内容可以被修改
ri0 = 1;
cout << "ri0=" << ri0 << endl;

// 3、虽然没办法获取右值的地址,但可以获取右值引用的地址,并改变该地址的值
int *pi = &ri0;
*pi = 2;
cout << "pi=" << pi << ", *pi=" << *pi << ", ri0=" << ri0 << endl;

// 4、传入右值
fun(1+2);
return 0;
}

gpt补充:

  1. ri0 = 1;

    :

    • 这是直接使用右值引用 ri0 进行赋值操作。尽管 ri0 引用了一个右值临时变量,但 ri0 本身是一个左值,可以直接给它赋值。
    • 此操作将临时变量的值直接修改为 1,且没有解引用操作,简单直接。
  2. *pi = 1;

    :

    • 这里 pi 是一个指向 ri0 的指针,*pi = 1; 解引用指针来访问临时变量的值。
    • 从结果上看,*pi = 1;ri0 = 1; 会导致相同的效果,即右值临时变量的值被修改成 1
    • 但使用指针间接访问对象相对稍微多了一层解引用操作。

结论

虽然这两种操作可以达到相同的效果,但直接使用 ri0 赋值(ri0 = 1;)在语法上更直接,性能上可能稍微优于通过指针赋值。

第二弹~ 你可以完全不看上面的解释

#include <iostream>
#include <string>

void printA(const int& s)
{//const的左值引用可以接受 左值和右值

    std::cout << s << std::endl;
}
void printB(int&& s)
{//右值引用只能接受右值

    std::cout << "右值" ;
    std::cout << s << std::endl;

}
//如果printB 名字改成printA 也就是重载,代码printB(1); 也会先执行printA(int&& s) 优先级
int main(int argc, char const *argv[])
{
    // demo1 说明左值引用
    int i = 1;
    int &ii = i;
    ii = 2;                       // 这是修改 不是初始化赋值
    std::cout << i << std::endl;  // 2
    std::cout << ii << std::endl; // 2
    // 补充: 左值引用指向的地址不会变化,但是值会变化!

    // demo2 说明左值引用是不能用右值的 除非.....
    //  int &a=1; 报错,右值是不能初始化赋值給左值引用的
    //  int const &a=1;   //   编译报错'const int& a' previously declared here
    const int &b = 1; // 除非是常量引用() const
    // b=i;  b=2  常量不可修改
    std::cout << b << std::endl; // 1
//demo3 // 左值引用在调用方法时的使用
    printA(i);
    printA(1); 
//demo4  右值引用在调用方法时的使用
    //printB(i);//报错  
    printB(1); 
    return 0;
}

移动语义

本质上允许我们移动对象,而不是复制对象到其他地方


为什么有移动语义

class Person {
public:
  string name;
  int age;

  Person(const Person& other) { // 拷贝构造函数
    name = other.name;
    age = other.age;
  }
};
 
int main() {
  Person p1("Alice", 30); // 创建 Person 对象
  Person p3=p1;//!!!这样也会调用拷贝构造函数,这是全新的对象 
  cout << p2.name << ", " << p2.age << endl; // 输出:Alice, 30

  return 0;
}

那么为什么不能直接把p1给p3 假如p1是一个临时量 ,或者是当它是一个右值的时候

移动构造和move
// g++ 14_Copy_Date.cpp -std=c++11
#include <iostream>
#include <stdio.h>
#include <string.h>

using namespace std;

#define MAX_NEW_MEM (64 * 1000 * 1000)

class CDate
{
public:
    CDate(int year, int mon, int day)
    {
        m_year = year;
        m_mon = mon;
        m_day = day;
        str = new char[MAX_NEW_MEM];
        sprintf(str, "%4d.%02d.%02d", year, mon, day);
        cout << "Calling Constructor" << ", this=" << this << endl;
    }
    // 拷贝构造函数定义
    CDate(const CDate &date)
    {
        m_year = date.m_year;
        m_mon = date.m_mon;
        m_day = date.m_day;
        str = new char[MAX_NEW_MEM];
        memcpy(str, date.str, MAX_NEW_MEM);
        cout << "Calling Copy Constructor" << ", this=" << this << ", Copy Data" << endl;
    }
 //移动构造
    CDate(CDate&& date) noexcept //加上noexcept,用于通知标准库不抛出异常。提高性能
    {
        m_year = date.m_year;
        m_mon = date.m_mon;
        m_day = date.m_day;
        str = date.str;//直接指向了 省去了copy
        date.str = NULL;//把原来指针的指向null 防止悬挂指针
        cout << "Calling Move Constructor" << ", this=" << this <<endl;
    }
    // 析构函数定义
    ~CDate()
    {
        cout << "Calling Destructor" << ", this=" << this << endl;
        delete[] str;
    }

    CDate operator+(int day)
    {
        CDate temp = *this;
        temp.m_day += day;
        cout << "Calling operator+" << ", this=" << &temp << endl;
        return temp;
    }
   

    void show()
    {
        cout << "Date: " << m_year << "." << m_mon << "." << m_day << ", this=" << this << endl;
        // cout << "Date: " << str << endl;
    }

private:
    int m_year;
    int m_mon;
    int m_day;
    char *str;
};

int main()
{
    CDate date(2024, 06, 07);
    cout << endl;
    CDate date1 = date;
    cout << endl;
    /* 
        Calling Constructor, this=0x5ffe70
        Calling Copy Constructor, this=0x5ffe50, Copy Data
        Calling Destructor, this=0x5ffe50
        Calling Destructor, this=0x5ffe70
    */
    return 0;
}


int main2()
{
    CDate date(2024, 06, 07);
    cout << endl;
    CDate date2 = move(date);
    cout << endl;
    /* 
        Calling Constructor, this=0x5ffe70
        Calling Move Constructor, this=0x5ffe50
        Calling Destructor, this=0x5ffe50
        Calling Destructor, this=0x5ffe70
    */
    return 0;
}

原文地址:https://blog.csdn.net/beginnerdzz/article/details/143801769

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!