C++11中的右值引用和移动语义

文章目录

1.左值和右值

  • 左值是一个表示数据的表达式(如变量名,解引用的指针),有持久状态,存储在内存中,我们能够取到它的地址。左值可以出现在赋值号左侧,也可以出现在右侧。在定义时被const修饰后的左值,不能再给它赋值了,但是依然能取它的地址。
  • 右值也是一个表示数据的表达式,可以是常量、临时对象等,右值可以出现在赋值号右侧,但不能出现在赋值号的左侧,也不能取地址。
  • 左值可以简写为lvalue(left value/locator value ),可以理解为存在内存中,有明确地址的对象,可以取地址;右值可以简写为rvalue(right value/read value),可以理解为只能提供数据,而不能取地址的对象
cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
	//下面的a, b, *p, str, str[0]就是常见的左值 可以取到它们的地址
	int a = 1;
	const int b = a;
	int* p = new int(1);
	string str("xxxxx");
	str[0] = '0';
	cout << &a << endl;
	cout << &b << endl;
	cout << &(*p) << endl;
	cout << &str << endl;
	cout << (void*)&str[0] << endl;

	//右值不能取地址
	double x = 3.14, y = 4.55;
	1234;
	x + y;
	fmin(x, y);
	string("hello cpp11");
	/*cout << &1234 << endl;
	cout << &(x + y) << endl;
	cout << &fmin(x, y) << endl;
	cout << &string("hello cpp11") << endl;*/

	return 0;
}

2.左值引用和右值引用

  • Type& r1 = x; Type&& rr1 = y;第一个语句就是左值引用,即给左值取别名,第二个语句就是右值引用,即给右值取别名
  • 左值引用不能直接引用右值,加const修饰后可以引用右值
cpp 复制代码
int& = 1;//报错
const int& x = 1;//正确形式
  • 右值引用不能直接引用左值,但是可以引用move(左值)
cpp 复制代码
int x = 1;
int&& rrx = x;//报错
int&& rrx = move(x);//正确形式
  • template <class T> typename remove_reference<T>::type&& move (T&& arg);
  • move是库里面的一个函数模板,本质进行类型的强制转换
  • 变量表达式的属性都是左值,取地址
cpp 复制代码
int x = 1;
double y = 3.14;
int&& rr1 = move(x);
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
//rr1 rr2 rr3属性都是左值 可以取地址
cout << &rr1 << endl;
cout << &rr2 << endl;
cout << &rr3 << endl;

3.引用延长生命周期

右值引用const左值引用 可以为临时对象 延长生命周期,但这些对象无法被修改

cpp 复制代码
string s = "cpp11";
//通过const左值引用延长生存期
const string& r1 = s + s;
//r1 += "xxxx";//error: 不能修改const引用
//通过右值引用延长生存期
string&& r2 = s + s;
r2+="xxxx"//OK

4.左值和右值的参数匹配

  • C++98中实现一个const左值引用作为参数的函数,实参传左值和右值都可以匹配
  • 从C++11开始,分别重载了左值引用、const左值引用、右值引用作为形参的f函数,会根据实参的类型,匹配对应的函数
  • 右值引用变量在用于表达式时属性是左值
cpp 复制代码
#include <iostream>
using namespace std;
void f(int& x)
{
	std::cout << "左值引⽤重载 f(" << x << ")\n";
}
void f(const int& x)
{
	std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
void f(int&& x)
{
	std::cout << "右值引⽤重载 f(" << x << ")\n";
}

int main()
{
	int i = 2;
	const int ci = 2;
	f(i);//调用f(int&)
	f(ci);//调用f(const int&)
	f(3);//调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)
	f(move(i));//调用f(int&&)

	//右值引用变量在用于表达式是左值
	int&& x = 1;
	f(x);//调用f(int& x)
	f(move(x));//调用f(int&& x)

	return 0;
}

5.移动构造与移动赋值

  • 移动构造是一种构造函数,与拷贝构造相似,要求第一个参数是该类类型的引用,但不同的是要求这个参数是右值引用,若还有其他参数,额外的参数必须有缺省值
  • 移动赋值实际上就是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,与拷贝赋值函数相似,移动赋值要求第一个参数是该类类型的引用,但不同的是要求这个参数是右值引用
  • 对于深拷贝的类(如string/vector等)或包含深拷贝成员变量的了类,移动构造和移动赋值才有意义,因为第一个参数是右值引用的类型,它的本质就是"窃取"所引用右值对象的资源,避免进行昂贵的深拷贝操作,从而提高效率
cpp 复制代码
#include <iostream>
#include <cstring>
#include <utility> // 包含 std::move

namespace my {
    class string {
    public:
        // 1. 默认构造函数
        string() noexcept
            : data_(nullptr), size_(0), capacity_(0) {
            std::cout << "Default Constructor called." << std::endl;
        }

        // 2. 构造函数 (深拷贝)
        string(const char* str) {
            std::cout << "Constructor (Deep Copy) called." << std::endl;
            size_ = std::strlen(str);
            capacity_ = size_;
            data_ = new char[capacity_ + 1]; // +1 为了存放 null 结束符
            std::strcpy(data_, str);
        }

        // 3. 拷贝构造函数 (深拷贝)
        string(const string& other) {
            std::cout << "Copy Constructor (Deep Copy) called." << std::endl;
            size_ = other.size_;
            capacity_ = other.capacity_;
            data_ = new char[capacity_ + 1];
            std::strcpy(data_, other.data_);
        }

        // 4. 移动构造函数 (窃取资源)
        string(string&& other) noexcept
            : data_(other.data_), size_(other.size_), capacity_(other.capacity_) {
            std::cout << "Move Constructor called." << std::endl;
            // 将源对象置于一个安全的、可销毁的状态
            other.data_ = nullptr;
            other.size_ = 0;
            other.capacity_ = 0;
        }

        // 5. 拷贝赋值运算符 (深拷贝)
        string& operator=(const string& other) {
            std::cout << "Copy Assignment (Deep Copy) called." << std::endl;
            if (this != &other) { // 防止自赋值
                // 释放当前对象的资源
                delete[] data_;

                // 深拷贝 other 的资源
                size_ = other.size_;
                capacity_ = other.capacity_;
                data_ = new char[capacity_ + 1];
                std::strcpy(data_, other.data_);
            }
            return *this;
        }

        // 6. 移动赋值运算符 (窃取资源)
        string& operator=(string&& other) noexcept {
            std::cout << "Move Assignment called." << std::endl;
            if (this != &other) { // 防止自赋值
                // 释放当前对象的资源
                delete[] data_;

                // 窃取 other 的资源
                data_ = other.data_;
                size_ = other.size_;
                capacity_ = other.capacity_;

                // 将源对象置于一个安全的、可销毁的状态
                other.data_ = nullptr;
                other.size_ = 0;
                other.capacity_ = 0;
            }
            return *this;
        }

        // 析构函数
        ~string() noexcept {
            std::cout << "Destructor called for \"" << (data_ ? data_ : "nullptr") << "\"." << std::endl;
            delete[] data_; // 如果 data_ 是 nullptr,delete[] 是安全的
        }

        // 辅助函数:返回 C 风格字符串
        const char* c_str() const noexcept {
            return data_ ? data_ : "";
        }

    private:
        char* data_;
        size_t size_;
        size_t capacity_;
    };
}

// 一个返回 my::string 的函数,用于产生右值
my::string create_string(const char* str) {
    return my::string(str);
}

int main() {
    std::cout << "--- Scenario 1: Move Construction ---" << std::endl;
    // create_string 返回一个临时对象 (右值)
    // 这个临时对象会被移动构造函数 "窃取"
    my::string s1 = create_string("Hello, Move Semantics!");
    std::cout << "s1: " << s1.c_str() << std::endl;
    std::cout << std::endl;

    std::cout << "--- Scenario 2: Move Assignment ---" << std::endl;
    my::string s2; // 默认构造
    // create_string 返回一个临时对象 (右值)
    // 这个临时对象会被移动赋值运算符 "窃取"
    s2 = create_string("Another move example.");
    std::cout << "s2: " << s2.c_str() << std::endl;
    std::cout << std::endl;

    std::cout << "--- Scenario 3: Using std::move to cast lvalue to rvalue ---" << std::endl;
    my::string s3("I am an lvalue."); // 普通构造
    std::cout << "s3 before move: " << s3.c_str() << std::endl;

    // std::move 将 s3 (一个左值) 强制转换为右值引用
    // s4 通过移动构造函数窃取 s3 的资源
    my::string s4 = std::move(s3);

    std::cout << "s4 after move: " << s4.c_str() << std::endl;
    // 注意:此时 s3 的状态是不确定的,不应再使用
    std::cout << "s3 after move (now invalid): " << s3.c_str() << std::endl;
    std::cout << std::endl;

    std::cout << "--- End of main ---" << std::endl;
    // 所有对象在这里被销毁
    return 0;
}

6.右值引用和移动语义在传参中的提效

  • STL中C++11以后容器的push和insert系列接口中增加了右值引用版本
  • 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器中的对象
  • 当实参是一个右值时,容器内部则调用移动构造,右值对象的资源移动到容器空间的对象上

实现一个自定义vector容器,其中的相应接口中新增了右值引用版本:

cpp 复制代码
#include <iostream>
#include <utility> // 包含 std::move 和 std::forward

// 一个简化的 string 类,包含移动语义,用于测试
class MyString {
public:
    MyString(const char* s = "") {
        std::cout << "MyString Constructor: " << s << std::endl;
        size = std::strlen(s);
        data = new char[size + 1];
        std::strcpy(data, s);
    }

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

    // 移动构造函数 (浅拷贝,窃取资源)
    MyString(MyString&& other) noexcept {
        std::cout << "MyString Move Constructor: " << other.data << std::endl;
        // 窃取 other 的资源
        data = other.data;
        size = other.size;
        // 将 other 置于一个安全的、可销毁的状态
        other.data = nullptr;
        other.size = 0;
    }

    ~MyString() {
        std::cout << "MyString Destructor: " << (data ? data : "nullptr") << std::endl;
        delete[] data;
    }

    const char* c_str() const { return data ? data : ""; }

private:
    char* data;
    size_t size;
};

// 手动实现的简化 Vector 容器
template <typename T>
class MyVector {
public:
    MyVector() : data(nullptr), size(0), capacity(0) {}

    ~MyVector() {
        // 销毁所有元素
        for (size_t i = 0; i < size; ++i) {
            data[i].~T();
        }
        // 释放内存
        operator delete(data);
    }

    // 1. 左值引用版本的 push_back
    // 当实参是左值时,调用此版本
    void push_back(const T& value) {
        std::cout << "MyVector::push_back (lvalue)" << std::endl;
        ensure_capacity();
        // 在已分配的内存上构造新元素,使用拷贝构造函数
        new (data + size) T(value); // placement new
        ++size;
    }

    // 2. 右值引用版本的 push_back
    // 当实参是右值时,调用此版本
    void push_back(T&& value) {
        std::cout << "MyVector::push_back (rvalue)" << std::endl;
        ensure_capacity();
        // 在已分配的内存上构造新元素,使用移动构造函数
        // std::forward 用于完美转发,这里它会保留 value 的右值属性
        new (data + size) T(std::forward<T>(value)); // placement new
        ++size;
    }

    // 辅助函数:打印容器内容
    void print() const {
        std::cout << "MyVector content: ";
        for (size_t i = 0; i < size; ++i) {
            // 假设 T 是 MyString,有 c_str() 方法
            std::cout << data[i].c_str() << " ";
        }
        std::cout << std::endl;
    }

private:
    T* data;
    size_t size;
    size_t capacity;

    void ensure_capacity() {
        if (size == capacity) {
            size_t new_capacity = (capacity == 0) ? 1 : capacity * 2;
            // 分配新内存,不进行初始化
            T* new_data = static_cast<T*>(operator new(new_capacity * sizeof(T)));

            // 移动(或拷贝)现有元素到新内存
            for (size_t i = 0; i < size; ++i) {
                // 使用移动构造函数来"移动"元素
                new (new_data + i) T(std::move(data[i]));
                // 销毁旧位置的元素
                data[i].~T();
            }

            // 释放旧内存
            operator delete(data);

            data = new_data;
            capacity = new_capacity;
        }
    }
};

// --- 主函数,演示使用 ---
int main() {
    std::cout << "--- Creating MyVector ---" << std::endl;
    MyVector<MyString> vec;

    std::cout << "\n--- Pushing an lvalue ---" << std::endl;
    MyString s1("Hello"); // s1 是一个左值
    vec.push_back(s1);    // 调用左值版本的 push_back,内部会拷贝 s1
    vec.print();

    std::cout << "\n--- Pushing an rvalue (temporary object) ---" << std::endl;
    vec.push_back(MyString("World")); // MyString("World") 是一个右值临时对象
    // 调用右值版本的 push_back,内部会移动这个临时对象
    vec.print();

    std::cout << "\n--- Pushing an lvalue cast to rvalue with std::move ---" << std::endl;
    MyString s2("Move Me");
    vec.push_back(std::move(s2)); // std::move 将 s2 转为右值
    // 调用右值版本的 push_back,内部会移动 s2
    // 注意:此后 s2 的状态变得不确定,不应再使用
    vec.print();

    std::cout << "\n--- End of main ---" << std::endl;
    // MyVector 和其内部的 MyString 对象会在这里被销毁
    return 0;
}

运行结果:

7.类型分类与引用折叠

7.1.类型分类

  • C++11以后进一步对类型进行了划分,右值被划分为纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)
  • 纯右值就是字面值常量(42, true, nullptr等)、求值结果为字面量常量的表达式(a+b, ++a, str1+str2等)或者是一个无名的临时变量(str.substr(1, 2)等)。纯右值和将亡值是C++11中提出的,这里的纯右值就相当于C++98中的右值。
  • 将亡值是指返回右值引用 的函数的调用表达式和转换为右值引用 的转换函数的调用表达(如move(x), static_cast<X&&>(x))
  • 泛左值(generalized value, 简称glvalue),泛左值包含左值和将亡值

7.2.引用折叠

  • 由于C++98中不能直接定义引用的引用,因此在C++11中引入了引用折叠操作,帮助我们间接实现引用的引用的定义
  • 引用折叠可以借助模板来实现
  • 核心规则:左值引用的右值引用折叠成右值引用,其他组合均为左值引用
  • 万能引用:就是下面的f2函数,折叠后能够实现左值引用、const修饰的左值引用和右值引用
cpp 复制代码
#include <iostream>
using namespace std;
//根据引用折叠核心规则 f1实例化后一定是左值引用
template<class T>
void f1(T& x)
{}

//根据引用折叠核心规则 f2实例化后可能是左值引用,也可能是右值引用
//由于能实现两种引用 所以我们把这种情况又称为万能引用
template <class T>
void  f2(T&& x)
{}
int main()
{
	typedef int& lref;
	typedef int&& rref;
	int n = 0;
	lref& r1 = n;//传左值 折叠后为左值引用 r1类型为int&
	lref&& r2 = n;//传左值 折叠后为左值引用 r2类型为int&
	rref& r3 = n;//传左值 折叠后为左值引用 r3类型为int&
	rref&& r4 = 1;//传右值 折叠后为右值引用 r4类型为int&&

	//没有折叠 直接实例化为void f1(int& x)
	f1<int>(n);//OK
	f1<int>(0);//error: 左值引用不能传右值

	//折叠 实例化为void f1(int& x)
	f1<int&>(n);//OK
	f1<int&>(0);//error: 左值引用不能传右值
	f1<int&&>(n);//OK
	f1<int&&>(0);//error: 左值引用不能传右值

	//折叠 实例化为void f1(const int& x)
	f1<const int&>(n);//OK
	f1<const int&>(0);//OK const修饰后可以传右值给左值引用

	//没有折叠 直接实例化为void f2(int&& x)
	f2<int>(n);//error: 右值引用 不能传左值
	f2<int>(0);//OK

	//折叠 实例化为void f2(int& x)
	f2<int&>(n);//OK
	f2<int&>(0);//error: 左值引用 不能传右值

	//折叠 实例化为void f2(int&& x)
	f2<int&&>(n);//error: 右值引用 不能传左值
	f2<int&&>(0);//OK

	return 0;
}

8.完美转发

  • func(T&& t)函数模板中,传左值实例化后是左值引用的函数,传右值实例化后是右值引用的函数
  • 但是我们前面学到过,变量表达式的属性都是左值,即右值引用变量表达式的属性也是左值(如int&& rr1 = 1, 这里rr1属性是左值),因此func函数中无论实参传左值还是右值,t的属性都是左值,因此func函数调用的都是左值版本
  • 此时,如果想要保证t的属性不丢失 ,就需要使用完美转发来实现

完美转发通过一个函数模板foward 来实现,它的底层 实际上就是引用折叠
template <class T> T&& forward (typename remove_reference<T>::type& arg);
template <class T> T&& forward (typenameremove_reference<T>::type&& arg);

传递给func的实参是右值,则T被推导为int,没有折叠,forward内部T被强转为右值引用返回;

传递给func中的实参是左值,则T被推导为int&,引用折叠为左值引用,forward内部T被强转为左值引用返回

cpp 复制代码
#include <iostream>
#include <utility> // 包含 std::forward 和 std::move

// 下层函数:三个重载版本,用于展示匹配结果
void process(int& x) {
    std::cout << "  -> process(int&): 处理左值" << std::endl;
}

void process(const int& x) {
    std::cout << "  -> process(const int&): 处理 const 左值" << std::endl;
}

void process(int&& x) {
    std::cout << "  -> process(int&&): 处理右值" << std::endl;
}

// 上层函数模板:不使用完美转发
template <typename T>
void func_without_forward(T&& t) {
    std::cout << "func_without_forward: ";
    // t 是一个变量,无论它绑定的是左值还是右值,t本身都是左值
    process(t);
}

// 上层函数模板:使用完美转发
template <typename T>
void func_with_forward(T&& t) {
    std::cout << "func_with_forward:   ";
    // 使用 std::forward<T> 来转发 t,保持其原始属性
    process(std::forward<T>(t));
}

int main() {
    std::cout << "--- 传递左值 ---" << std::endl;
    int a = 10;
    func_without_forward(a); // a 是左值
    func_with_forward(a);    // a 是左值

    std::cout << "\n--- 传递 const 左值 ---" << std::endl;
    const int b = 20;
    func_without_forward(b); // b 是 const 左值
    func_with_forward(b);    // b 是 const 左值

    std::cout << "\n--- 传递右值 ---" << std::endl;
    func_without_forward(30); // 30 是右值
    func_with_forward(30);    // 30 是右值

    std::cout << "\n--- 传递 std::move 转换后的左值 ---" << std::endl;
    int c = 40;
    func_without_forward(std::move(c)); // std::move(c) 将 c 转为右值
    func_with_forward(std::move(c));    // std::move(c) 将 c 转为右值

    return 0;
}

运行结果:

相关推荐
有梦想的攻城狮2 小时前
初识Rust语言
java·开发语言·rust
程序猿_极客2 小时前
【2025 最新】 Python 安装教程 以及 Pycharm 安装教程(超详细图文指南,附常见问题解决)
开发语言·python·pycharm·python安装以及配置
2501_941235732 小时前
C++中的装饰器模式变体
开发语言·c++·算法
2501_941111252 小时前
基于C++的爬虫框架
开发语言·c++·算法
小欣加油2 小时前
leetcode 429 N叉树的层序遍历
数据结构·c++·算法·leetcode·职场和发展
b***66612 小时前
Python 爬虫实战案例 - 获取社交平台事件热度并进行影响分析
开发语言·爬虫·python
ModestCoder_2 小时前
Tokenization的演进:从NLP基石到多模态AI的“通用翻译器”
开发语言·人工智能·自然语言处理·机器人·具身智能
hweiyu002 小时前
GO的优缺点
开发语言·后端·golang
小龙报2 小时前
《算法通关指南C++编程篇 --- 初阶函数递归专题》
c语言·开发语言·c++·算法·创业创新·学习方法·visual studio