C++ 移动构造与移动赋值运算符

本文深入探讨了C++中的左值与右值概念,详细解释了移动语义的必要性和实现方式,通过具体示例展示了如何利用移动构造函数来提高资源管理效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一,左值与右值

1,左值与左值引用

左值是一个表示数据的表达式,程序可以获取其地址。左值可以出现在赋值语句的左边,也可以出现在赋值语句的右边。左值引用就是对左值的引用。下面的变量都是左值

int a = 20;
const int b = 10;
int c = b;


2,右值与右值引用

右值即可出现在赋值表达式右边,但不能获取其地址。右值包括字面常量(C风格字符串除外,它表示的是地址),诸如x + y表达式以及返回值的函数(条件是该函数返回的不是引用)。C++11新增了右值引用,这是使用&&表示的,右值引用就是对右值的引用。下面的是右值的例子

int getValue(){
    return 50;
}

int main(){
    int x = 10;
    int y = 20;
    int &&r1 = 30;  //字面常量是右值
    int &&r2 = x + y;  //表达式是右值
    int &&r3 = getValue();  //函数返回的是int类型的值
    return 0;
}


二,为何需要移动语义?

我们定义了下面这个Useless类

class Useless{
private:
    string name;
    char *p;
public:
    Useless();
    Useless(string name);
    Useless(const Useless &f);
    Useless operator+(const Useless &f) const;
    void showData();
    ~Useless();
};

定义一个函数这个函数的返回值是Useless类对象

Useless getObject(){
    Useless temp("temp");
    return temp;
}

假设有下面这些代码

Useless three(getObject());

getObject()函数创建temp对象,temp对象管理着10000个字符。Useless的复制构造函数将创建这10000个字符的副本,然后删除getObject()函数返回的临时对象。这里的要点是,做了大量的无用功。考虑到临时对象被删除了,如果编译器将临时对象对数据的所有权直接转让给three对象,不是更好么?也就是说,不将临时对象管理的10000个字符复制到新的地方,再删除临时对象管理的字符串。而是将字符留在原来的地方,并将three对象与之相关联。这类似于在计算机中移动文件的情形,实际的文件还是留在原来的地方,而只修改记录。这种方法被称为移动语义。有点悖论的是,移动语义实际上避免了移动原始数据,而只是修改了记录。


三,使用移动构造函数实现移动语义

使用前面介绍的Useless类,这个类里面的很多函数都很常规,在这里只写出移动复制构造函数的定义。

class Useless{
private:
    string name;
    char *p;
public:
    Useless();
    Useless(string name);
    Useless(const Useless &f);
    Useless(Useless &&f);
    Useless operator+(const Useless &f) const;
    void showData();
    ~Useless();
};

移动复制构造函数

Useless::Useless(Useless &&f):name(f.name){
    p = f.p;
    f.p = NULL;
    showData();
}

应用实例

int main(){
    Useless one("one");
    Useless two("two");
    Useless three(one + two);
    return 0;
}

输出结果

one Object Data Address : 0xcc0d78
two Object Data Address : 0xcc0da8
one+two Object Data Address : 0xcc0e08
one+two Object Data Address : 0xcc0e08
one+two Object delete. 0x000000
one+two Object delete. 0xcc0e08
two Object delete. 0xcc0da8
one Object delete. 0xcc0d78

Process returned 0 (0x0) execution time : 0.012 s
Press any key to continue.

程序分析
在方法operator+中创建的对象的数据地址与对象three存储的数据地址相同(都是 0xcc0e08),其中对象three是由移动复制构造函数创建的。另外,注意到创建对象three之后,为临时对象调用了析构函数。之所以知道这是临时对象,是因为其数据地址是: 0x000000。使用移动复制构造函数直接交换了临时对象与three对象之间的数据的所有权。


四,移动构造函数解析
1,移动构造函数

Useless::Useless(Useless &&f):name(f.name){
    p = f.p;
    f.p = NULL;
    showData();
}

上面的代码使p直接指向现有的数据,以获取这些数据的所有权。此时,p与f.p指向相同的数据,调用析构函数时,这将带来麻烦,因为程序不能为同一个地址调用delete []两次。为避免这个问题,该构造函数随后将原来的指针设置为空指针,因为对空指针执行delete []没有问题。
注意:

由于修改了f对象,这要求不能在参数声明中使用const。


2,移动构造函数解析
要让移动语义发生,需要下面两个步骤。
(1),右值引用让编译器知道何时可使用移动语义

Useless one("one");
Useless two("two");
Useless three(one + two);

对象one与two是左值,与左值引用匹配,而表达式one + two是右值,与右值引用匹配。因此,右值引用让编译器使用移动复制构造函数初始化对象three。

 

(2),定义移动复制构造函数。


五,移动构造函数与移动赋值运算符

1,C++11在原有4个特殊成员函数的基础上,新增了两个:移动构造函数与移动赋值运算符。默认的移动构造函数与移动赋值运算符与复制版本类似,执行逐成员初始化并复制内置类型。如果成员是类对象,将使用相应类的构造函数和赋值运算符,就像参数为右值一样。如果定义了移动构造函数与移动赋值运算符,这将调用他们。否则将调用复制构造函数和赋值运算符。


2,如果你提供了析构函数、复制构造函数或赋值运算符,编译器将不会自动提供移动构造函数和移动赋值运算符。如果你提供了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数和赋值运算符。

<think>好的,我现在要仔细分析用户的问题,并准备一个全面且清晰的回答。用户想了解C++移动构造函数移动赋值运算符的使用及实现,特别是他们的具体应用代码示例。 首先,我需要回顾之前提供的回答引用资料,确保内容一致且准确。用户提到的引用[1]到[4]涵盖了移动语义的基本概念、实现方法、最佳实践以及注意事项。需要整合这些信息,同时结合之前的教程内容,确保回答结构合理。 接下来,需要明确移动构造函数移动赋值运算符的定义。根据引用[2][3],移动构造函数赋值运算符的作用是转移资源所有权,避免不必要的复制。它们的参数是右值引用,并且通常标记为noexcept,以避免异常带来的问题。此外,引用[4]提到移动后的对象应处于有效但可析构的状态,这点很重要。 然后,考虑如何展示代码示例。引用[2]中的代码示例展示了移动赋值运算符的实现,但可能不够完整。需要提供一个完整的类示例,包括移动构造函数移动赋值运算符,并解释每个步骤的作用。例如,在移动操作中,原对象的指针应置空,避免重复释放资源。 用户可能对何时需要自定义移动操作有疑问。根据引用[3],当类管理动态资源(如堆内存、文件句柄)时,需要定义移动操作以提高性能。否则,编译器可能不会自动生成,或者调用拷贝操作代替,影响效率。同时,引用[4]提到如果定义了拷贝控制成员(如析构函数),应遵循Rule of Five,定义所有相关函数。 此外,需要强调noexcept的重要性。引用[3]指出,不抛出异常的移动操作应标记为noexcept,否则编译器可能选择拷贝而非移动,特别是在STL容器中。例如,vector在扩容时,如果元素的移动构造函数不是noexcept,可能会使用拷贝而非移动,导致性能下降。 关于使用场景,引用[1]之前的回答提到了STL容器操作、函数返回值资源管理类。需要具体说明在这些情况下移动语义如何提升性能,比如在vector::push_back中使用std::move减少拷贝开销。 还要注意移动后的对象状态。引用[3][4]都提到,移动后的对象应处于有效但空的状态,例如将原指针置为nullptr,这样析构时不会出错。这一点在代码示例中必须体现,比如移动构造函数中转移数据后,原对象的data_设为nullptr。 可能用户会混淆移动语义完美转发。需要简要区分std::movestd::forward的作用,但主要聚焦在移动构造函数赋值运算符的实现上。引用[4]提到带模板的赋值运算符不应移动赋值混淆,这点可能需要提及,但作为高级话题,可以放在注意事项里。 最后,整理常见问题,如编译器何时自动生成移动操作、如何确保异常安全、性能优化的实际案例等。这些问题可以作为相关问题的延伸,帮助用户进一步学习。 总结回答结构:首先介绍基本概念,然后详细说明移动构造函数赋值运算符的实现步骤,提供完整代码示例,接着讨论使用场景最佳实践,最后注意事项常见问题。需要确保每个部分都有引用支持,并自然地插入引用标识。</think>## C++移动构造函数移动赋值运算符详解 ### 一、核心原理 移动语义通过转移资源所有权避免深拷贝,特别适用于管理动态内存、文件句柄等资源的类。复制操作$O(n)$的时间复杂度不同,移动操作的时间复杂度为$O(1)$[^3]。 数学表达式说明: $$ \text{移动成本} = O(1) $$ $$ \text{复制成本} = O(n) $$ ### 二、实现规范 #### 1. 移动构造函数 **语法格式**: ```cpp ClassName(ClassName&& other) noexcept; ``` **实现要点**: - 转移资源所有权(指针、文件描述符等) - 将原对象置于有效但空的状态 - 必须标记`noexcept`(引用[3]) **示例实现**: ```cpp class MemoryBlock { public: // 移动构造函数 MemoryBlock(MemoryBlock&& other) noexcept : size_(other.size_), data_(other.data_) { other.size_ = 0; // 原对象置空 other.data_ = nullptr; } private: size_t size_; int* data_; }; ``` #### 2. 移动赋值运算符 **语法格式**: ```cpp ClassName& operator=(ClassName&& other) noexcept; ``` **实现要点**: - 检查自赋值 - 释放当前资源 - 转移资源所有权 - 标记`noexcept` **示例实现**: ```cpp MemoryBlock& operator=(MemoryBlock&& other) noexcept { if (this != &other) { delete[] data_; // 释放原有资源 data_ = other.data_; // 资源转移 size_ = other.size_; other.data_ = nullptr; // 置空原对象 other.size_ = 0; } return *this; } ``` ### 三、触发场景 1. **STL容器操作** ```cpp vector<string> v; string s = "data"; v.push_back(std::move(s)); // 触发移动构造函数 ``` 2. **函数返回值优化** ```cpp vector<int> generate_data() { vector<int> temp(1000); return temp; // 自动触发移动语义 } ``` 3. **显式转移所有权** ```cpp unique_ptr<Resource> p1 = make_unique<Resource>(); unique_ptr<Resource> p2 = std::move(p1); // 触发移动赋值 ``` ### 四、最佳实践 1. **遵循Rule of Five** 如果定义了以下任一函数,应全部定义: - 析构函数 - 拷贝构造函数 - 拷贝赋值运算符 - 移动构造函数 - 移动赋值运算符 2. **异常安全保证** - 移动操作必须标记`noexcept`(引用[3]) - 禁止在移动操作中抛出异常(引用[4]) 3. **对象状态管理** ```cpp // 正确做法:原对象置空 other.data_ = nullptr; // 错误做法:保留指针将导致双重释放 // other.data_ = valid_pointer; ``` ### 五、完整示例 ```cpp class FileHandle { public: explicit FileHandle(const char* filename) : handle_(fopen(filename, "r")) {} // 移动构造函数 FileHandle(FileHandle&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } // 移动赋值运算符 FileHandle& operator=(FileHandle&& other) noexcept { if (this != &other) { close(); // 释放当前资源 handle_ = other.handle_; other.handle_ = nullptr; } return *this; } ~FileHandle() { close(); } private: void close() { if (handle_) fclose(handle_); } FILE* handle_ = nullptr; }; // 使用示例 FileHandle f1("data.txt"); FileHandle f2 = std::move(f1); // 移动构造 FileHandle f3("log.txt"); f3 = std::move(f2); // 移动赋值 ``` ### 六、注意事项 1. **自动生成条件**: - 没有用户声明的拷贝操作 - 没有用户声明的析构函数 - 类成员都可移动(引用[4]) 2. **禁用场景**: ```cpp class Immutable { public: Immutable(const Immutable&) = delete; Immutable& operator=(Immutable&&) = delete; // 禁止移动 }; ``` 3. **模板陷阱**: ```cpp template<typename T> class Wrapper { public: // 不会阻止编译器生成默认移动操作 template<typename U> Wrapper(U&& value); }; ``` --相关问题-- 1. 如何验证移动操作确实被调用? 2. 移动语义对STL容器性能的具体影响? 3. 如何处理不可移动的类成员? 4. 为什么移动构造函数参数不能是const右值引用?[^4] 5. 移动语义在多线程环境中的注意事项? [^1]: C++移动语义核心概念 [^2]: 移动赋值运算符实现示例 [^3]: noexcept关键作用及性能优化 [^4]: 移动操作实现规范要求
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值