C++ 引用与右值引用

引用

引用在底层是指针实现的

int *p = &a;
int &b = a;

*p = 20;
b = 30;

定义一个引用变量,和定义一指令是一模一样的;通过引用变量修改所引用内存的值,和通过指针解引用修改指针指向的内存的值,其底层指令也是一模一样的。

int x = 10;
int &ref = x;
ref = 20;

会被翻译成

int x = 10;
int *const ref = &x;
*ref = 20;

但是相比指针,有更严格的语法要求,引用是一种更安全的指针

  1. 引用是必须初始化的,指针可以不初始化
  2. 引用的必须是实例,有内存地址
int &c = 20; // 引用的必须是实例,有内存地址
  1. 引用只有一级引用,没有多级引用,指针可以有一级指针也可以有多级指针

引用的使用

void swap(int &x,int &y){
	int temp = x;
	x = y;
	y = temp;
}

对数组的引用

int array[5] = {};
int *q = array;
int (&q)[5] = array;

右值引用

int &&c = 20;
// 汇编
dword ptr [ebp-30h], 14h  ; 将20存储到ebp-30h
lea eax, [ebp-30h]        ; 将ebp-30h的地址加载到eax
mov dword ptr [c], eax  ; 将eax的值存储到c,也就是将20的内存地址存到c

翻译成C代码就是

int temp = 20; // 会产生临时量
int *c = &temp;

c在底层是一个指针变量,存放14h的临时栈地址。

等效于,汇编指令相同

const int &c = 20; // 区别是这个不能修改

右值被右值引用绑定时,会自动生成临时量来存储,然后直接引用临时量。
右值引用本身是左值,保存着临时量的地址,只能使用左值引用来引用他。

右值引用用来生成临时量保存纯右值,对于在内存中有空间的左值变量,无需再生成临时量去存储,也就不能引用。

函数栈帧与RVO返回值优化

#include <cstring>
#include <iostream>

using namespace std;

class CMyString {
public:
    CMyString(const char *str = nullptr) {
        cout << "CMyString(const char*)" << endl;
        if (str != nullptr) {
            mptr = new char[strlen(str) + 1];
            strcpy(mptr, str);
        } else {
            mptr = new char[1];
            *mptr = '\0';
        }
    }
    ~CMyString() {
        cout << "~CMyString" << endl;
        delete[] mptr;
        mptr = nullptr;
    }
    CMyString(const CMyString &str) { // 
        cout << "CMyString(const CMyString &str)" << endl;
        mptr = new char[strlen(str.mptr) + 1];
        strcpy(mptr, str.mptr);
    }
    CMyString &operator=(const CMyString &str) { // 4. 赋值构造
        cout << "CMyString& operator=(const CMyString &str)" << endl;
        if (this == &str)
            return *this;

        delete[] mptr;

        mptr = new char[strlen(str.mptr) + 1];
        strcpy(mptr, str.mptr);
        return *this;
    }

    const char *c_str() const {
        return mptr;
    }

private:
    char *mptr;
};

CMyString GetString(CMyString &str) {
    const char *pstr = str.c_str();
    CMyString tmpStr(pstr); // 3.拷贝构造
    return tmpStr; // 4. 拷贝构造到main栈帧,一般会被编译器优化掉,直接将局部对象构造到要返回的函数栈帧上
}

int main() {
    CMyString str1("aaaaaaaaa"); // 1. 普通构造
    CMyString str2;              // 2. 普通构造
    str2 = GetString(str1); // 5. 赋值构造
    cout << str2.c_str() << endl;
}

未优化时,返回局部对象,会调用对象的拷贝构造将局部对象构造到上级函数栈帧上。

CMyString(const char*)
CMyString(const char*)
CMyString(const char*)
CMyString(const CMyString&) => tmpStr拷贝构造到main栈帧上的临时对象
~CMyString
CMyString& operator=(const CMyString &str) => main函数栈帧上的临时对象给t2赋值
~CMyString
aaaaaaaaa
~CMyString
~CMyString

优化后

CMyString(const char*)
CMyString(const char*)
CMyString(const char*)
CMyString& operator=(const CMyString &str)
~CMyString
aaaaaaaaa
~CMyString
~CMyString

优化后,返回的局部对象直接构造在上级函数栈帧上,不需要再次拷贝构造,这种优化称为返回值优化(Return Value Optimization, RVO)​

如果不使用RVO,如何自定义优化这一过程呢。

移动构造

对于将亡值(出了作用域将要销毁的值),可以直接将其资源转移到当前对象,而不用调用拷贝构造(拷贝到上级栈帧,再拷贝到赋值对象)。

#include <cstring>
#include <iostream>
#include <ostream>

using namespace std;

class CMyString {
public:
    CMyString(const char *str = nullptr) {
        cout << "CMyString(const char*)" << endl;
        if (str != nullptr) {
            mptr = new char[strlen(str) + 1];
            strcpy(mptr, str);
        } else {
            mptr = new char[1];
            *mptr = '\0';
        }
    }
    ~CMyString() {
        cout << "~CMyString" << endl;
        delete[] mptr;
        mptr = nullptr;
    }
    CMyString(const CMyString &str) {
        cout << "CMyString(const CMyString &str)" << endl;
        mptr = new char[strlen(str.mptr) + 1];
        strcpy(mptr, str.mptr);
    }
    CMyString(CMyString&& str){
        cout << "CMyString(CMyString&& str)" << endl;
        mptr = str.mptr;
        str.mptr = nullptr;
    }

    CMyString &operator=(const CMyString &str) { // 4. 赋值构造
        cout << "CMyString& operator=(const CMyString &str)" << endl;
        if (this == &str)
            return *this;

        delete[] mptr;

        mptr = new char[strlen(str.mptr) + 1];
        strcpy(mptr, str.mptr);
        return *this;
    }

    const char *c_str() const {
        return mptr;
    }



private:
    char *mptr;

    friend CMyString operator+(const CMyString &lhs,const CMyString &rhs);
    friend ostream& operator<<(ostream &out,const CMyString &str);
};

CMyString GetString(CMyString &str) {
    const char *pstr = str.c_str();
    CMyString tmpStr(pstr); 
    return tmpStr;
}

CMyString operator+(const CMyString &lhs,const CMyString &rhs){
    CMyString tmpStr; // 3. 默认构造
    strcpy(tmpStr.mptr, lhs.mptr);
    strcat(tmpStr.mptr,rhs.mptr);
    return std::move(tmpStr); // 显示指定为移动构造
}

ostream& operator<<(ostream &out,const CMyString &str){
    out << str.mptr;
    return out;
}

int main() {
    CMyString str1 = "hello"; // 1. 赋值构造
    CMyString str2 = "world"; // 2. 赋值构造
    CMyString str3 = str1 + str2; // 4. 移动构造,不调用赋值构造
    cout << str3 << endl;
}

使用等号运算符,如果等式右边的值是右值,默认会调用移动构造


对于右值引用,只是将浅拷贝和深拷贝在语义上分开,在对象成为将亡值时触发浅拷贝,也就是只拷贝动态资源的指针。返回右值时不会将对象拷贝到上级函数栈也不会

// 函数返回过程
返回局部对象 --> 拷贝到上级函数栈匿名对象 --> 拷贝构造到目标对象

上述函数返回过程原来需要两次深拷贝构造。
现在我们明确其是右值,触发浅拷贝构造。
第一次深拷贝构造被RVO直接优化,第二次直接调用浅拷贝构造(移动构造)
如果没有RVO就是两次浅拷贝构造(移动构造),在其拷贝的对象成为将亡值时触发。

函数返回引用对象,本质是返回对象的指针,在使用时自动解引用


右值本质是那些无名的对象或临时量,之后无法再被使用,就可以使用右值引用对其重复利用。
匿名对象被识别为右值。

将右值转为左值时,有指针使用其指针,常量为其开辟内存空间

引用折叠

在使用模板时,可以使用&& 来自动推导形参使用左值接受左值的实参还是右值引用接受右值的实参;

template<typename Ty>
// CMyString& + && = CMyString&
// CMyString&&右值 + && = CMyString&&右值引用
// CMyString&&这里表示右值而不是右值引用
void push_back(Ty &&val){
	
}

实参是右值就会用形参是右值引用去接受。
实参是左值就会用形参是引用去接受。

对于右值引用或者引用,都是左值,在函数内部的使用其实都一样,所以可以使用引用折叠将其合并。只有在是否触发移动构造时有所区别,这时可以使用完美转发来还原

除了在函数传参时使用匿名对象构造将亡值成为右值,也可以使用move将对象强转为右值。右值只能使用右值引用去传递,而右值引用是左值。

由于右值在函数传参时会成为右值引用的左值,使用模板递归调用时,无法正确匹配真正的函数(例如不再能触发移动构造),这时候就需要自动将右值引用还原为右值,也就是forward

完美转发 forward

forward 类型完美转发,能够识别左值和右值类型

右值在被右值引用形参接受后,右值引用是左值,forward通过推导变量原始定义来判断是否使用move将其恢复为右值。

#include <iostream>
#include <utility>  // for std::forward

// 目标函数
void foo(int a, double b, const std::string& c) {
    std::cout << "a="<< a << ", b="<< b << ", c="<< c << std::endl;
}

// 包装函数(完美转发 + 可变参数模板)
template<typename... Args>
void wrapper(Args&&... args) {
    foo(std::forward<Args>(args)...);  // 完美转发
}

int main() {
    std::string s = "Hello";
    wrapper(42, 3.14, s);       // 传递左值
    wrapper(100, 2.71, "World"); // 传递右值(字面量)
    return 0;
}

右值可以被绑定到const的左值引用。
你知晓其是右值引用,你知道应当不修改内容,修改后果你自行负责。
其他类型可以绑定右值时,应当不能修改右值。故而const的左值引用可以绑定

posted @ 2025-04-24 22:06  丘狸尾  阅读(37)  评论(0)    收藏  举报