您的位置:

深入理解C++拷贝构造函数的实现原理

一、什么是拷贝构造函数

拷贝构造函数是一种特殊的构造函数,它在对象作为参数传递给函数或函数返回对象时被调用。拷贝构造函数可以将一个对象作为另一个对象的副本来创建,即初始化。这个副本跟原对象有着一模一样的数据和属性。 可以用下面的代码来定义一个简单的类,并为它定义一个拷贝构造函数。
class MyString {
public:
    MyString(const char* str = NULL);    // 构造函数
    MyString(const MyString& other);    // 拷贝构造函数
    ~MyString();                        // 析构函数
private:
    char* m_data;
};
调用拷贝构造函数的方式有两种:一种显式调用,另一种隐式调用。 显式调用拷贝构造函数的方式是创建一个对象并将其初始化为另一个对象,比如下面的代码。
// 显式调用
MyString str1 = "Hello world";
MyString str2(str1);
在这里,我们创建了两个 MyString 类型的对象 str1 和 str2。第一个对象被显式初始化为字符串 "Hello world",这个过程会调用构造函数。第二个对象 str2 被显式初始化为第一个对象 str1 的副本,这个过程会调用拷贝构造函数。 隐式调用拷贝构造函数的方式有很多,比如函数参数传递、函数返回值等。比如下面的代码:
// 隐式调用
void func(MyString str)
{
    // do something...
}

MyString func2()
{
    MyString str = "Hello world";
    return str;
}

int main()
{
    MyString str = "Hello world";
    func(str);
    MyString str2 = func2();
    return 0;
}
在这里,我们定义了一个函数 func,它的参数是 MyString 类型的对象 str。当我们调用这个函数时,会创建一个 MyString 类型的对象并将其初始化为传入的参数对象,这个过程会调用拷贝构造函数。类似地,我们还定义了一个函数 func2,它返回一个 MyString 类型的对象。当我们将其赋值给一个 MyString 类型的对象 str2 时,也会调用拷贝构造函数。

二、浅拷贝和深拷贝

在定义拷贝构造函数时,需要考虑到对象内存的复制问题。如果只是简单地将一个对象的内存复制到另一个对象中,叫做浅拷贝。浅拷贝可能会导致问题,主要是因为多个对象共享同一块内存资源,导致其中一个对象的内存被释放后,其他对象仍然指向该内存地址,访问该地址的结果就可能是未定义的。 为了避免这种问题,我们需要使用深拷贝。深拷贝将不仅复制对象的内存,而且会分配一段新的内存,将原来对象的数据复制到这段新的内存中,从而避免多个对象共享同一块内存资源的问题。 下面是深拷贝和浅拷贝的示例代码。
class MyString {
public:
    MyString(char* str = NULL);               //构造函数
    MyString(const MyString& other);           //拷贝构造函数,进行深拷贝
    ~MyString();                              //析构函数
private:
    char* m_data;
};

MyString::MyString(char* str)
{
    if (str == NULL) {
        m_data = new char[1];
        *m_data = '\0';
    } else {
        int len = strlen(str);
        m_data = new char[len + 1];
        strcpy(m_data, str);
    }
}

MyString::MyString(const MyString& other)
{
    int len = strlen(other.m_data);
    m_data = new char[len + 1];
    strcpy(m_data, other.m_data);
}

MyString::~MyString()
{
    delete[] m_data;
}

int main()
{
    char* str = "Hello world";
    MyString myStr(str);            // 调用构造函数
    MyString myStr2(myStr);        // 调用拷贝构造函数
    return 0;
}
在这里,我们定义了一个 MyString 类,并为它定义了构造函数和拷贝构造函数。构造函数会根据传入的字符串分配内存,并将字符串复制到这段内存中。拷贝构造函数进行深拷贝,它会为新对象分配一段新的内存,并将原对象的数据复制到这段新的内存中。 在 main 函数中,我们调用了 MyString 的构造函数和拷贝构造函数,分别创建出两个对象 myStr 和 myStr2,这两个对象在内存中拥有不同的地址。

三、默认拷贝构造函数

如果我们没有定义自己的拷贝构造函数,编译器就会自动生成一个默认的拷贝构造函数。默认拷贝构造函数与复制构造函数的行为类似:将原对象的所有属性值都复制到新对象中。默认拷贝构造函数的实现非常简单,就是将原对象的内存复制到新对象中。 下面是默认拷贝构造函数的示例代码:
class MyString {
public:
    char* m_data;
};

int main()
{
    char* str = "Hello world";
    MyString myStr1;
    myStr1.m_data = new char[strlen(str) + 1];
    strcpy(myStr1.m_data, str);

    MyString myStr2(myStr1);            // 调用默认拷贝构造函数
    return 0;
}
在这里,我们定义了一个 MyString 类,并为它定义了一个属性 m_data。在 main 函数中,我们创建了一个对象 myStr1,并为它分配了一段内存,并将字符串 "Hello world" 复制到这段内存中。然后我们用 myStr1 创建了一个新对象 myStr2,这个过程会调用默认拷贝构造函数。 默认拷贝构造函数的实现就是逐个复制原对象的属性值到新对象中,代码如下:
MyString(const MyString& other)
{
    m_data = other.m_data;
}
可以看到,这个拷贝构造函数实现非常简单,只是将原对象的内存地址复制到新对象中。由于两个对象共享同一块内存,这就导致了上面提到的多个对象共享同一块内存资源的问题,因此必须自己定义拷贝构造函数,避免这个问题。

结论

拷贝构造函数是一种特殊的构造函数,它可以将一个对象作为另一个对象的副本来创建,即初始化。拷贝构造函数有两种调用方式:一种是显式调用,另一种是隐式调用。拷贝构造函数需要根据对象内存的复制问题来考虑如何实现,如果只是简单地将一个对象的内存复制到另一个对象中,叫做浅拷贝;否则,需要使用深拷贝避免共享同一地址的问题。如果没有定义自己的拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,但这个函数会导致多个对象共享同一块内存资源的问题,因此自己定义拷贝构造函数是必须的。