C++的移动构造函数和移动赋值运算符

本文通过C++Primer中的HasPtr类样例代码,详细介绍了如何实现类似值和类似指针的行为,并对构造、析构、赋值操作进行了深入探讨。

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

C++ Primer的样例代码,我改了改,又加了注释,记录一下。
注释里有我对输出结果的分析,重点看main函数里的注释。

#include <string>
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 行为像值的类
class HasPtr {
    friend void swap(HasPtr&, HasPtr&);
    friend bool operator<(const HasPtr &lhs, const HasPtr &rhs);
public:
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0) { cout << "I'm constructor!" << endl; }
    HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i){
        cout << "I'm copy constructor!" << endl; 
    }
    HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i)
    {
        cout << "I'm move constructor!" << endl;
        p.ps = 0;
    }
    // 有意思的地方来了,这个拷贝赋值运算符同时也是一个移动赋值运算符
    HasPtr& operator=(HasPtr rhs){
        cout << "Operator = is running!" << endl;
        // 使用临时变量是防范自赋值的情况的
        // std::string *temp_ps = new std::string(*rhs.ps);
        // delete ps;
        // ps = temp_ps;
        // i = rhs.i;
        // return *this;
        // 现在有swap函数了,一般有swap函数的都用swap实现赋值运算符
        // 注意,参数不再是const引用了,而是用实参拷贝构造的临时对象
        // 你会在上面的输出语句之前看到拷贝构造函数被调用了,就是这个原因
        // 这种技术叫“拷贝并交换”,我愿称之为“二王换血”
        // 因为交换后临时变量就带着没用的原值被销毁
        swap(*this, rhs);
        return *this;
    }
    ~HasPtr() {// ps这个指针是这个类的成员,但指针指向的内容不是
        cout << "I'm destructor!" << endl;
        delete ps;// 所以析构默认只能析构这个指针本身,要想删内容,得手动写
    }
private:
    std::string *ps;
    int i;
};
inline
void swap(HasPtr &lhs, HasPtr &rhs)
{
    cout << "I am swapping!" << endl;
    using std::swap;
    swap(lhs.ps, rhs.ps);
    swap(lhs.i, rhs.i);
}
bool operator<(const HasPtr &lhs, const HasPtr &rhs)
{
    return *lhs.ps < *rhs.ps;
}

// 行为像指针的类
// class HasPtr {
// public:
//     HasPtr(const std::string &s = std::string()):
//         ps(new std::string(s)), i(0), use(new std::size_t(1)) { cout << "I'm constructor_"<< *use << "!" << endl; }
//     HasPtr(const HasPtr &p):
//         ps(p.ps), i(p.i), use(p.use){
//             ++*use;
//             cout << "I'm copy constructor_" << *use << "!" << endl;
//         }
//     HasPtr& operator=(const HasPtr&);
//     ~HasPtr() {
//         cout << "I'm destructor_" << *use << "!" << endl;
//         if (--*use == 0) {
//             delete ps;
//             delete use;
//         }
//     }
// private:
//     std::string *ps;
//     int i;
//     std::size_t *use;
// };
// HasPtr& HasPtr::operator=(const HasPtr &rhs)
// {
//     cout << "Operator = is running!" << endl;
//     ++*rhs.use;
//     if (--*use == 0) {
//         delete ps;
//         delete use;
//     }
//     ps = rhs.ps;
//     i = rhs.i;
//     use = rhs.use;
//     return *this;
// }

int main()
{
    cout << "test begin" << endl;
    HasPtr test(*new std::string("hi"));
    cout << "test1 begin" << endl;
    HasPtr test1 = test;
    cout << "declare test2" << endl;
    HasPtr test2;
    cout << "definate test2" << endl;
    // 和拷贝赋值一样,移动构造也会先创造一个临时变量,
    // 再对临时变量使用移动赋值运算符,把临时变量交给test2
    test2 = std::move(test1);
    // 赋值结束,临时变量立刻被析构,可以看到下面这行打印前,先打印了一次析构
    cout << "initialize a vector" << endl;
    // 把test1注释掉是因为对它用了移动赋值,在上面对它调用std::move后,
    // 语句结束时,程序就以为它被当成临时变量析构掉了,而实际上,还是在它的
    // 生存周期结束时才会真正调用它的析构函数。但程序已经认为test1不能用了。
    // 所以不注释的话,程序就自动退出且不报错
    // vector<HasPtr> a{test,/*test1,*/test2};
    // cout << "sorting" << endl;
    // sort(a.begin(),a.end());
    return 0;
}

以下是输出结果

test begin
I'm constructor!
test1 begin
I'm copy constructor!
declare test2
I'm constructor!
definate test2
I'm move constructor!
Operator = is running!
I am swapping!
I'm destructor!
initialize a vector
I'm destructor!
I'm destructor!
I'm destructor!

PS:markdown对于c++的代码段,得写cpp,而不是c++。

<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]: 移动操作实现规范要求
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值