• c++ 类的特殊成员函数:移动构造函数(五)


    1. 简介

    移动构造函数是C++11中的新特性,它允许对象通过移动而不是复制来传递和初始化。移动构造函数通常用于提高性能,因为它避免了不必要的复制操作,特别是当处理大型对象或使用动态内存分配时。

    2. 来源

    拷贝构造函数出现函数返回值 (返回对象)时,代码如下:

    #include 
    #include 
    
    using namespace std;
    
    class stu {
    public:
        string* name = nullptr;
        int age;
    
        stu() {
            cout << "无参构造" << endl;
        }
    //    stu() : name(nullptr) {
    //        cout << "无参构造" << endl;
    //    }
    
        stu(const string& n, int a) : name(new string(n)), age(a) {
            cout << "有参构造" << endl;
        }
    
        stu(const stu& s) : name(new string(*s.name)), age(s.age) {
            cout << "拷贝构造" << endl;
        }
    
    
        ~stu() {
            delete name;  // 释放堆上分配的内存
            name = nullptr;
            cout << "析构函数" << endl;
        }
    };
    
    stu createstu() {
        stu s("华云飞", 240);
        return s;
    }
    
    int main() {
        stu s1 = createstu();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    g++ 编译时运行输出如下:

    有参构造
    析构函数
    
    • 1
    • 2

    MSVC 编译时运行输出如下:

    有参构造
    拷贝构造
    析构函数
    析构函数
    
    • 1
    • 2
    • 3
    • 4

    这种现象是编译器自动优化,编译器有时候为了避免拷贝生成临时对象而消耗内存空间,所以默认会有优化、避免发生过多的拷贝动作所以打印的日志可能不是我们所期望的,这时候,如果手动编译的话,可以添加参数:

    #如果手动编译 可以添加以下参数  -fno-elide-constructors 
    g++ -std=c++11  xxx.cpp -fno-elide-constructors
    
    # 如果使用cmake编译,可以添加配置   
    CMakeLists.txt 中前面添加:
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-elide-constructors")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这时运行结果:

    有参构造
    拷贝构造
    析构函数
    拷贝构造
    析构函数
    析构函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    原因解释:
    在这里插入图片描述

    先创建 对象 s stu s("华云飞", 240), 然后执行拷贝构造得到临时对象,接着执行析构函数销毁对象s , 再执行拷贝构造得到对象 s1,接着执行析构函数销毁临时对象,最后再销毁对象 s1,完成整个代码过程。

    2.1 tips:

    如果编译信息中有如下提示:

    cl: 命令行 warning D9002 :忽略未知选项“-fno-elide-constructors”
    
    • 1

    则:

    在 CLion 中使用 cl 编译器时,如果看到类似的警告消息 "warning D9002: ignoring unknown option '-fno-elide-constructors'",这是因为 -fno-elide-constructors 是一个 g++(GCC)编译器选项,而不是 Visual C++(cl)编译器选项。
    
    -fno-elide-constructors 选项用于禁用 C++ 编译器对构造函数进行优化的过程。然而,Visual C++ 的 cl 编译器默认情况下不支持此选项。
    
    如果想在 CLion 中使用 -fno-elide-constructors 选项,需要将项目配置更改为使用 g++ 编译器而不是 cl 编译器。可以按照以下步骤进行更改:
    
    打开 CLion 并导航到 File -> Settings(Windows/Linux)或 CLion -> Preferences(MacOS)。
    在设置面板中选择 Build, Execution, Deployment -> Toolchains。
    在右侧的 "CMake" 栏中,点击下拉箭头选择已安装的 g++ 工具链。
    确保勾选 "Use this toolchain for building" 复选框,并点击 Apply 或 OK 保存更改。
    通过这样的配置更改,CLion 将使用 g++ 编译器来构建项目,应该能够正常使用 -fno-elide-constructors 选项了。请注意,在切换编译器之后可能需要重新加载项目或重新生成 CMake 配置。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如图:
    在这里插入图片描述

    3. 移动构造

    拷贝构造有时会有一些弊端,数据拷贝太多,浪费内存,尤其是一些即将消亡的对象,这些对象销毁了,但是它们的数据还要拷贝出来,我们需要使用。而移动构造让新对象直接接管消亡对象的数据,不用重新开辟空间来拷贝数据。
    代码:

    #include 
    #include 
    
    using namespace std;
    
    class stu {
    public:
        string* name = nullptr;
        int age;
    
        stu(){
            cout << "无参构造" << endl;
        }
    //    stu() : name(nullptr) {
    //        cout << "无参构造" << endl;
    //    }
    
        stu(const string& n, int a) : name(new string(n)), age(a) {
            cout << "有参构造" << endl;
        }
    
        stu(const stu& s) : name(new string(*s.name)), age(s.age) {
            cout << "拷贝构造" << endl;
        }
    
    // 移动构造函数  移动构造函数操作的是右值,也就是说,它主要是针对数据来说的。
        stu(stu && s){
    //      让新对象持有数据的控制权
            name = s.name;
            age = s.age;
    //      让原对象放弃数据的控制权  
            s.name = nullptr;
            s.age = 0;
            cout << "移动构造" << endl;
        }
    
    //    stu(stu && s):name(s.name),age(s.age){
    //        s.name = nullptr;
    //        cout << "移动构造" << endl;
    //    }
    
    
        ~stu() {
            if(name!= nullptr){
                delete name;  // 释放堆上分配的内存
                name = nullptr;
            }
            cout << "析构函数" << endl;
        }
    };
    
    stu createstu() {
        stu s("华云飞", 240);
        cout << "s: " <<s.name << " " << *s.name<< " " << s.age << " " << &s.age <<  " " << &s<<endl;
        return s;
    }
    
    int main() {
        stu s1 = createstu();
        cout << "s1: " <<s1.name << " " << *s1.name<< " " << s1.age  << " " << &s1.age <<  " " << &s1 <<endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    运行结果:

    有参构造
    s: 000002452CA5FEA0 华云飞 240 000000F4F4F7FC60 000000F4F4F7FC58 
    移动构造
    析构函数
    移动构造
    析构函数
    s1: 000002452CA5FEA0 华云飞 240  000000F4F4F7FD00 000000F4F4F7FCF8
    析构函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    先执行有参构造 stu s("华云飞", 240) 生成对象s, 在执行移动构造把 对象s 中的数据的所有权递交给临时对象,同时让原对象放弃数据的控制权
    在这里插入图片描述
    同理 临时对象和 对象s1移动构造也是一样的。
    在这里插入图片描述

    4. std::move 函数

    4.1 左值转右值

    #include 
    #include 
    
    using namespace std;
    
    int add(int &&a , int  && b){
        return a + b;
    }
    
    
    int main() {
    
        int a = 20; // a : 左值  , 20 : 右值
    
        int & b = a ; // b : 左值引用 , a:左值
        int && c = 30 ; // c :右值引用 , 30 :右值
        //把左值变成右值。
        int && d = move(a); // d : 右值引用 , a :左值
    
        int num1 = 40 , num2 = 50;
        add(move(num1) ,move(num2));
        add(60 ,70);
    
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    4.2 对象转换为右值引用

    std::move 是一个函数模板,用于将对象转换为右值引用,并且表明该对象的所有权可以被移动。当使用 std::move 将一个对象作为参数传递给其他函数时,意味着放弃了对该对象的所有权,并允许接收函数直接获取并修改其内部状态。这在实现高效的移动语义和避免不必要的拷贝构造/赋值操作时非常有用。

    #include 
    #include 
    
    using namespace std;
    
    class stu {
    public:
        string* name = nullptr;
        int age;
    
        stu(){
            cout << "无参构造" << endl;
        }
    
    
        stu(const string& n, int a) : name(new string(n)), age(a) {
            cout << "有参构造" << endl;
        }
    
        stu(const stu& s) : name(new string(*s.name)), age(s.age) {
            cout << "拷贝构造" << endl;
        }
    
    
        stu(stu && s){
    
            name = s.name;
            age = s.age;
    
            s.name = nullptr;
            s.age = 0;
            cout << "移动构造" << endl;
        }
    
    
        ~stu() {
            if(name!= nullptr){
                delete name;  // 释放堆上分配的内存
                name = nullptr;
            }
            cout << "析构函数" << endl;
        }
    };
    
    
    int main() {
    
        stu s0;
        stu s("华云飞", 240);
        cout << "s: " <<s.name << " " << *s.name<< " " << s.age  << " " << &s.age <<endl;
        stu s2 = move(s);
        cout << "s2: " <<s2.name << " " << *s2.name<< " " << s2.age  << " " << &s2.age <<endl;
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    运行结果:

    无参构造
    有参构造
    s: 00000223323E3F70 华云飞 240 0000007C39CFF880
    移动构造
    s2: 00000223323E3F70 华云飞 240 0000007C39CFF8D0
    析构函数
    析构函数
    析构函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    5. 总结

    C++中的移动构造函数是一种特殊的构造函数,用于在对象被移动时执行高效的资源转移操作。它主要用于提高程序的性能和内存管理效率。

    在C++11及以上版本中引入了移动语义,通过右值引用(Rvalue Reference)来实现。移动构造函数使用 && 运算符作为参数类型标识,并且接受一个右值引用参数。通常情况下,它会将传入的参数对象的资源指针拷贝到当前对象,并将原始对象置为空状态,避免进行额外的资源拷贝或分配。

    移动构造函数常用于以下场景:

    1. 在容器类中进行元素插入或重新排序时,可以利用移动语义避免不必要的数据复制。
      当使用动态容器(如 std::vector、std::list)存储大量对象时,容器可能会不断地进行内存重新分配和数据复制。在这种情况下,移动构造函数可以通过将对象的所有权从旧容器移动到新容器,避免不必要的数据复制,提高性能。
    std::vector<BigObject> CreateBigObjects() {
        std::vector<BigObject> objects;
        // 添加大量对象到容器中
        // ...
        return objects;  // 移动构造函数被调用,避免了大量的数据复制
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    详细代码:

    std::vector<int> createVector() {
        std::vector<int> v{1, 2, 3, 4, 5};
        return std::move(v); // 使用移动构造返回v
    }
    
    void processVector(std::vector<int>&& v) {
        // 使用移动构造接受右值引用参数v
        // 这里可以直接使用v,无需进行额外的拷贝
        for (int i : v) {
            std::cout << i << " ";
        }
    }
    
    int main() {
        std::vector<int> vec = createVector(); // 使用移动构造初始化vec
        processVector(std::move(vec)); // 将vec作为右值引用传递给函数
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    1. 在函数返回值时,可以通过移动语义避免大量数据拷贝操作。
      临时对象的传递:当在函数间传递临时对象时,移动构造函数可以避免对临时对象进行深拷贝,提高效率。
    void ProcessBigObject(BigObject obj) {
        // 处理大对象
    }
    
    int main() {
        ProcessBigObject(BigObject());  // 临时对象通过移动构造函数传递,避免了深拷贝
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    详细代码:

    #include 
    #include 
    
    class Resource {
    public:
        int* data;
    
        Resource() : data(nullptr) {}
    
        Resource(int size) {
            data = new int[size];
            std::cout << "Resource allocated!" << std::endl;
        }
    
        // 移动构造函数
        Resource(Resource&& other) noexcept {
            data = other.data;     // 转移资源所有权
            other.data = nullptr;  // 清空原始指针
            std::cout << "Resource moved!" << std::endl;
        }
    
        ~Resource() {
            if (data != nullptr) {
                delete[] data;
                std::cout << "Resource deallocated!" << std::endl;
            }
        }
    };
    
    int main() {
        Resource res1(100);  // 创建一个资源对象
    
        Resource res2(std::move(res1));  // 使用移动构造将资源从res1转移到res2
    
        std::vector<Resource> vec;
        vec.push_back(std::move(res2));  // 使用移动构造将资源从res2转移到容器中的元素
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    1. 在使用智能指针等管理资源的类中,可以通过移动语义进行资源所有权的转移。
      动态内存管理:当使用动态分配的内存(如使用 new 运算符分配的内存)来存储对象时,移动构造函数可以在对象的所有权转移后,正确管理内存的释放,避免内存泄漏。
    std::unique_ptr<BigObject> CreateBigObject() {
        std::unique_ptr<BigObject> obj = std::make_unique<BigObject>();
        // 对对象进行初始化
        // ...
        return obj;  // 移动构造函数被调用,正确管理内存的释放
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 相关阅读:
    前后端结合解决Excel海量公式计算的性能问题
    BUUCTF Misc 来首歌吧 & 荷兰宽带数据泄露 & 面具下的flag & 九连环
    mybatis自定义类型控制器(TypeHandler)处理将字符串处理为集合
    【【萌新的FPGA学习之按键控制蜂鸣器之消抖介绍】】
    LLM大语言模型训练中常见的技术:微调与嵌入
    路径规划|多目标海洋捕食者算法(MOMPA)求解最短路径问题(Matlab代码实现)
    知错认错,但错难改!苹果的种种事件门确实是问题,要改也不是一蹴而就
    Servlet
    B. Permutation Chain
    【ARM Coresight SoC-400/SoC-600 专栏导读】
  • 原文地址:https://blog.csdn.net/weixin_40378209/article/details/133758959