C++ 左值右值引用梳理(一)

C++ 左值右值引用梳理(一)

左值与右值的区别

在参考资料上看到这样一句话
https://www.internalpointers.com/post/understanding-meaning-lvalues-and-rvalues-c

In C++ an lvalue is something that points to a specific memory location. On the other hand, a rvalue is something that doesn’t point anywhere. In general, rvalues are temporary and short lived, while lvalues live a longer life since they exist as variables. It’s also fun to think of lvalues as containers and rvalues as things contained in the containers. Without a container, they would expire.

总的来说,左值可以获取地址,而右值不能。lvalue指的是可以放在赋值表达式左边的事物——在栈上或堆上分配的命名对象,或者其他对象成员——有明确的内存地址。rvalue指的是可以出现在赋值表达式右侧的对象——例如,文字常量和临时变量。

同样的,在《现代C++语言核心特性解析》中有这句话:

在C++中所谓的左值一般是指一个指向特定内存的具有名称的值(具名对象),它有一个相对稳定的内存地址,并且有一段较长的生命周期。而右值则是不指向稳定内存地址的匿名值(不具名对象),它的生命周期很短,通常是暂时性的。基于这一特征,我们可以用取地址符&来判断左值和右值,能取到内存地址的值为左值,否则为右值。

右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。左值可以在左边也可以在右边,右值只能在右边,举例说明

int x = 666;   // ok
int* y = &x;   // ok
666 = y; // error!

这里666是一个右值;一个字面常量,没有特定的内存地址。这个数字被分配给x,这是一个变量。一个变量有一个特定的内存位置,所以它是一个左值。C++声明赋值需要左值作为其左操作数。

第二句话,通过取地址运算符&将x的地址取出,&x是一个临时变量,是一个右值。
第三句话,666是一个右值,不能放在左边。

返回左值和右值的函数

对比这两个例子

int setValue()
{
    return 6;
}

// ... somewhere in main() ...

setValue() = 3; // error!

第一个例子,setValue()返回一个右值(临时数字6),它不能是赋值的左操作数。

int global = 100;

int& setGlobal()
{
    return global;    
}

// ... somewhere in main() ...

setGlobal() = 400; // OK

第二个例子,setGlobal返回一个引用,不像上面的setValue()。
参考链接:
【C 语言】变量本质 ( 变量概念 | 变量本质 - 内存空间别名 | 变量存储位置 - 代码区 | 变量三要素 )
关于引用为什么可以做左值,可以参考:,其实你看底层,就是指针的简化,理解后就可以明白为什么可以做左值。
从外到内理解c++引用,一文清晰解读。
C++引用在本质上是什么,它和指针到底有什么区别?

左值引用和右值引用的区别

左值引用

顾名思义,左值引用就是给左值的引用,给左值取别名。右值引用就是对右值的引用,给右值取别名。而C++11中新增了的右值引用语法特性。
在赋值运算符左侧是左值引用符,右侧必须是左值;在赋值运算符左侧是右值引用符,右侧必须是右值。
看下面的例子:

int y = 10;
int& yref = y;
yref++;        // y is now 11
int& yref = 10;  // wrong
void fnc(int& x)
{
}

int main()
{
    fnc(10);  // Nope!
    // This works instead:
    // int x = 10;
    // fnc(x);
}

但是下面的例子成功:

void fnc(const int& x)
{
}

int main()
{
    fnc(10);  // OK!
}

常量引用(const T&)可以绑定到右值(right-value),这是C++11引入的新特性之一。下面举例:

const int &r = 10;

实际上,编译器会创建一个临时对象,并让常量引用 r 绑定到这个临时对象上。

const int _temp = 10;

const int &r = _temp;

这个临时对象 _temp 是由编译器隐式创建的,并且它的生命周期至少持续到引用 r 的生命周期结束。为什么这个临时对象是const类型。回答是这样的:
1.字面量值:字面量值 10 本身就是一个不可修改的值。
2.常量引用:常量引用 const int &r 确保了引用的不可修改性,即不能通过引用 r 来修改所引用的对象。

临时对象具有常性,因此也需要常量引用来绑定。

右值引用

右值引用有什么优点呢:
1.绑定临时对象,正如上面提到
2.移动语义,当一个类有移动构造和复制构造时,会优先选择移动构造:
下面的链接很好的说明了这一点:

https://stackoverflow.com/questions/68184575/why-does-c-give-preference-to-rvalue-reference-over-const-reference-while-func

const T&& 有什么用呢,为什么还需要const的右值引用呢?是在你没有const T& 的时候
下面的例子回答了这一点:
https://www.sandordargo.com/blog/2021/08/18/const-rvalue-references

#include <iostream>

struct T {};

void f(T&) { std::cout << "lvalue ref\n"; }  // #1
void f(const T&) { std::cout << "const lvalue ref\n"; }  // #2
void f(T&&) { std::cout << "rvalue ref\n"; } // #3
void f(const T&&) { std::cout << "const rvalue ref\n"; } // #4

const T g() { return T{}; }

int main() {
    f(g()); // #4, #2
}

可以看到2和4是一样的效果。
在文章中出现cv的字眼,意思是“const”和“volatile”限定符,它们可以用来修饰类型,以表明该类型的某些特性。
注意:如果返回const value,意味着你不能再使用移动语义。

#include <iostream>

class MyString {
public:
    MyString(const char* str) {
        m_length = strlen(str);
        m_data = new char[m_length + 1];
        strcpy(m_data, str);
    }

    // 拷贝构造函数
    MyString(const MyString& other) {
        m_length = other.m_length;
        m_data = new char[m_length + 1];
        strcpy(m_data, other.m_data);
        std::cout << "Copy constructor called." << std::endl;
    }

    // 移动构造函数
    MyString(MyString&& other) noexcept {
        m_length = other.m_length;
        m_data = other.m_data;
        other.m_length = 0;
        other.m_data = nullptr;
        std::cout << "Move constructor called." << std::endl;
    }

    // 拷贝赋值运算符
    MyString& operator=(const MyString& other) {
        if (this != &other) {
            delete[] m_data;
            m_length = other.m_length;
            m_data = new char[m_length + 1];
            strcpy(m_data, other.m_data);
        }
        return *this;
    }

    // 移动赋值运算符
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] m_data;
            m_length = other.m_length;
            m_data = other.m_data;
            other.m_length = 0;
            other.m_data = nullptr;
        }
        return *this;
    }

    ~MyString() {
        delete[] m_data;
        std::cout << "Destructor called." << std::endl;
    }

    // 其他成员函数...
private:
    int m_length;
    char* m_data;
};

void print(MyString s) {
    std::cout << "String: " << s.m_data << std::endl;
}

int main() {
    MyString s1("Hello");

    // 使用临时对象调用移动构造函数
    MyString s2 = MyString("World");
    print(s2);

    // 使用右值引用调用移动构造函数
    MyString s3 = std::move(s1);
    print(s3);

    return 0;
}

3.完美转发,在后面会提到。
4.避免不必要的拷贝

完美转发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值