拷贝构造函数、拷贝赋值操作符、移动构造函数,移动赋值操作符

当我们使用拷贝构造函数的时候,如果我们不小心使用的是浅拷贝,那完了,危险了就。

一、拷贝构造函数

1.1 什么是拷贝构造函数

首先我们说一下什么是拷贝构造函数。\

拷贝构造函数(Copy Constructor)是c++中的一种特殊的构造函数,它用于创建一个对象并将其初始化为同一类中另一个对象的副本。拷贝构造函数通常以引用方式(const MyClass& other)接受一个同类对象作为参数,然后根据这个参数的值创建一个新的对象,使新对象与参数对象的内容相同。\

说的简单点就是用同一个类的实例构造一个新的实例。\
注意: 拷贝结束后被复制对象的资源依旧存在。

下面是一个拷贝构造函数的简单例子。

class MyClass {
public:
    // 拷贝构造函数
    MyClass(const MyClass& other) {
        // 复制 other 的成员变量到当前对象
        // 通常使用深拷贝来确保新对象独立于原对象
    }

    // 其他成员和构造函数...
};

1.2 拷贝构造函数中的浅拷贝和深拷贝

浅拷贝

浅拷贝其实就是通过拷贝构造函数构建的实例和传入参数的那个实例中的属性共用内存,当一个类的属性改变了另一个也会改变,而且如果delete了一个类,就是出现内存泄漏的问题。\

比如下面的代码。

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

class Str{
    public:
    char *value;
    Str(char s[])
    {
        cout<<"调用构造函数..."<<endl;
        int len = strlen(s);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,s);
    }
    Str(Str &v)
    {
        cout<<"调用拷贝构造函数..."<<endl;
        this->value = v.value;
    }
    ~Str()
    {
        cout<<"调用析构函数..."<<endl;
        if(value != NULL)
            delete[] value;
    }
};

int main()
{

    char s[] = "I love BIT";
    Str *a = new Str(s);
    Str *b = new Str(*a);
    delete a;
    cout<<"b对象中的字符串为:"<<b->value<<endl;
    delete b;
    return 0;
}

上面类的value是一个指针,两个类的指针都指向一个地方,delete了a,b的内存也会释放,后面又delete了一次b,属于重复释放了,会有安全的问题。\

深拷贝

深拷贝就是重新为构造的实例进行内存分配,使两个实例拥有不同的内存,互不影响。\

比如说下面的代码。

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

class Str{
    public:
    char *value;
    Str(char s[])
    {
        cout<<"调用构造函数..."<<endl;
        int len = strlen(s);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,s);
    }
    Str(Str &v)
    {
        cout<<"调用拷贝构造函数..."<<endl;
        int len = strlen(v.value);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,v.value);
    }
    ~Str()
    {
        cout<<"调用析构函数..."<<endl;
        if(value != NULL)
        {
            delete[] value;
            value = NULL;
        }
    }
};

int main()
{

    char s[] = "I love BIT";
    Str *a = new Str(s);
    Str *b = new Str(*a);
    delete a;
    cout<<"b对象中的字符串为:"<<b->value<<endl;
    delete b;
    return 0;
}

这样才不会出现资源泄漏和悬挂指针等问题。

1.3 拷贝构造注意事项

最后要注意的是,如果你没有显式定义拷贝构造函数,c++编译器会给你生成一个默认的拷贝构造函数,该构造函数会执行浅拷贝(即简单地复制成员变量的值)。\

在某些情况下,特别是当类中包含指针等动态分配的资源时,你可能需要显式定义自己的拷贝构造函数以执行深拷贝,以避免资源泄露和悬挂指针等问题。

二、拷贝赋值运算符

如果你理解了拷贝构造函数,那么拷贝赋值运算符就很急简单了。\
就是重载了=运算符,新实例化一个类时候可以直接等于一个现有的实例(通过拷贝)。\
传入的参数const Foo&是为了防止不必要的开销,而又不能更改传入的类。

// 拷贝赋值运算符
// 返回值必须为引用值
Foo& operator=(const Foo& f) {
        s_ = f.s_;
        v_ = f.v_;
        std::cout << "Copy Assignment: " << Info() << std::endl;
        return *this;
}

// 使用
Foo f1("hello", v);
Foo f2 = f1;  // 调用拷贝赋值运算符
 ```

 ## 三、移动构造函数
 移动构造函数是基于c++11的一个新特性``std::move()``实现的,具体的我也不太懂。它是强制将一个左值转换为右值,然后进行构造,避免构造过程中的拷贝造成性能损失。\
**作用:** 用于创建一个新对象,该对象获得另一个已存在对象的资源,并使原对象进入有效但未定义的状态。\
 **参数:** 通常接受一个同类对象的右值引用(&&)作为参数,该引用是被移动的对象。\
 偷走了什么资源,看你函数怎么写的,如果你新构建的实例的属性等于了一个被强制转换成右值的资源,那这个资源就被偷走了。例如``s_(std::move(f.s_))``,就相当于``s=std::move(f.s_))``,这就偷走了``s_``资源。
 ```cpp
 // 移动构造函数
// 传入的是右值引用&&  std::move()是将左值转换为右值
Foo(Foo&& f) : s_(std::move(f.s_)), v_(std::move(f.v_)) {
        std::cout << "Move Constructor: " << Info() << std::endl;
}

// 使用
Foo f1("hello", v);
Foo f2(std::move(f1));   // 调用移动构造函数 调用后 f1中的资源都没了

四、移动赋值操作符

懂了移动构造函数,移动赋值运算符也是挺简单的。\
也是重载=,传入一个右值引用(&&),然后返回对象本身。
这里偷走了什么资源就看你的函数里面了,这里是偷走了s_v_。如果你把v_ = std::move(f.v_);删去,那么传入的对象是还拥有这个v_资源的。

// 移动赋值操作符
Foo&  operator=(Foo&& f) {
        s_ = std::move(f.s_);
        v_ = std::move(f.v_);
        std::cout << "Move Assignment: " << Info() << std::endl;
        return *this;
}

// 使用
Foo f1("hello", v);
f3 = std::move(f1); // 调用移动赋值运算符  f1的资源也会没有

五、最后

上面代码完整版。

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

class Foo {
public:
        // 默认构造函数
        Foo() { std::cout << "Default Constructor: " << Info() << std::endl; }

        // 自定义构造函数
        Foo(const std::string& s, const std::vector<int>& v) : s_(s), v_(v) {
                std::cout << "User-Defined Constructor: " << Info() << std::endl;
        }

        // 析构函数
        ~Foo() { std::cout << "Destructor: " << Info() << std::endl; }

        // 拷贝构造函数
        // 就是用同类的其他对象进行初始化
        // 传入的参数类型:引用指向的数据是一个不可修改的常量
        Foo(const Foo& f) : s_(f.s_), v_(f.v_) {
                std::cout << "Copy Constructor: " << Info() << std::endl;
        }

        // 拷贝赋值运算符
        // 返回值必须为引用值
        Foo& operator=(const Foo& f) {
                s_ = f.s_;
                v_ = f.v_;
                std::cout << "Copy Assignment: " << Info() << std::endl;
                return *this;
        }

        // 移动构造函数
        // 传入的是右值引用 std::move()是将左值转换为右值
        Foo(Foo&& f) : s_(std::move(f.s_)), v_(std::move(f.v_)) {
                std::cout << "Move Constructor: " << Info() << std::endl;
        }

        // 移动赋值操作符
        Foo&  operator=(Foo&& f) {
                s_ = std::move(f.s_);
                v_ = std::move(f.v_);
                std::cout << "Move Assignment: " << Info() << std::endl;
                return *this;
        }

        std::string Info() {
                return "{" + (s_.empty() ? " 'empty'" : s_) + ", " + std::to_string(v_.size()) + "}";
        }

private:
        std::string s_;
        std::vector<int> v_;

};

int main() {

    std::vector<int> v(1024);

    std::cout << "================== Copy ==================" << std::endl;
    Foo f1("hello", v);    // 自定义运算符
    Foo f2(std::move(f1));   // 调用移动构造函数
    Foo f3;         // 调用默认构造函数
    f3 = std::move(f2); // 调用移动赋值运算符
    Foo f4 = f3;    // 调用拷贝赋值运算符
    return 0;

}

参考博客1--现代 C++:右值引用、移动语意、完美转发 \
参考博客2--C++移动构造函数以及move语句简单介绍

THE END