14_对象拷贝 – 拷贝构造函数

14 对象拷贝 - 拷贝构造函数

什么是对象拷贝

假设我在工作中需要存储一些对象, 那么通常我可以选择数组啊链表啊等等

比如说我现在用数组存储 3 个对象, Obj 1 2 3

O1 O2 O3

随着工作的扩展, 原来只能存储 3 个对象的数组可能不够用了

我们现在需要一个更大的数组

所以我就重新创建了一个新的数组, 比原来的大

O1 O2 O3 O4 ...

但是麻烦的问题是, 我必须要把原来的数据给复制过来

如果是 C 语言的话, 我们通常是用函数进行内存拷贝, 把这一块内存复制到另一块地方

其实我们在 C++ 里面做的事情和在 C 语言里面一模一样, 只不过它有自己的名字, 这个名字就是对象拷贝

C++ 编译器允许我们使用更加简单的方式来实现我们所谓的内存复制, 这就是对象拷贝

现在我们来看在 C++ 里面如何实现对象拷贝

拷贝构造函数

class CObject
{
    private:
        int x;
        int y;
    public:
        CObject()
        {

        }
        CObject(int x, int y)
        {
            this->x = x;
            thix->y = y;
        }
};
int main(int argc, char* argv[])
{
    CObject obj(1, 2);      // 创建一个对象, 叫 obj, 有两个成员

    CObject objNew(obj);    // 创建一个和 obj 一模一样的对象 objNew

    return 0;
}

C++ 编译器允许我们用这种方式, 比如你首先起个对象名, 然后后面直接调用一个构造函数, 这个构造函数传的参数就是对象类型. CObject objNew(obj);

你可能会有疑问, obj 这个构造函数有 1 和 2, 这个构造函数我们写了肯定没问题

但是 objNew(obj); 这个我没有定义一个 obj 类型的构造函数啊, 这样能编译过去吗 ?

能, 这个构造函数虽然你没有定义, 但是编译器会给我们提供的.

这个构造函数它有一个名字, 叫拷贝构造函数

换句话说, 我们所编写的类型, 它默认就有了一个拷贝构造函数

拷贝构造函数作用就是把上面对象复制一份到新创建出来的对象这里, 本质就是内存的复制, 只不过编译器替我们做了这个事情


现在大家可能有个疑问, 比如说我对象创建完了以后, 我可能把里面的属性和值已经修改了, 不是刚开始创建的那个值了, 这时候你再通过拷贝构造函数得到的那个新的对象是什么样的

我们做个实验

class CObject
{
    private:
        int x;
        int y;
    public:
        CObject()
        {

        }
        CObject(int x, int y)
        {
            this->x = x;
            thix->y = y;
        }
        void SetX(int x)
        {
            this->x = x;
        }
};

int main(int argc, char* argv[])
{
    CObject obj(1, 2);

    obj.SetX(100);

    CObject objNew(obj);

    return 0;
}

我们创建了一个对象, 初始值是 1 和 2, 然后我们通过 SetX() 函数把它给改了, 然后我们通过拷贝构造函数, 新创建了一个对象 objNew, 我们看一看新的对象和上面我们原来对象的值是不是一样的

结果是, 完全一样, x 都是 100, y 都是 2

其实这个原理已经说了, 这个其实就是编译器替我们做了内存复制的工作, 原来那块内存被改了, 被改了之后被复制过来, 新的这一块的内容自然是被改了之后的.

我们可以看看汇编

;CObject objNew(obj);
mov eax, dword ptr [ebp - 8]
mov dword ptr [ebp - 10h], eax
mov ecx, dword ptr [ebp - 4]
mov dword ptr [ebp - 0Ch], ecx

就是内存复制而已. 把第一个对象的值取出来放到 [ebp - 10h]


刚才是在栈里面创建对象, 复制对象

现在我们在堆里面搞事情

CObject* p = new CObject(obj);

这个就是新对象在堆中

反汇编代码如下 :

;CObject* p = new CObject(obj);
push    8
call    operator new (0040d570)
add     esp, 4
mov     dword ptr [ebp - 10h], eax
cmp     dword ptr [ebp - 10h], 0
je      main+57h (0040d547)
mov     eax, dword ptr [ebp - 10h]
mov     ecx, dword ptr [ebp - 8]
mov     dword ptr [eax], ecx
mov     edx, dword ptr [ebp - 4]
mov     dword ptr [eax + 4], edx
mov     eax, dword ptr [ebp - 10h]
mov     dword ptr [ebp - 14h], eax
jmp     main+5Eh (0040d54e)
mov     dword ptr [ebp - 14h], 0
mov     ecx, dword ptr [ebp - 14h]
mov     dword ptr [ebp - 0Ch], ecx

我们跟进去 call operator new (0040d570) new 看看

这个 new, 我们之前已经分析过了, 这个 new 就相当于 malloc 加上构造函数, 大家想看 new 的话可以去看以前的文章


现在我们来考虑一个新问题, 如果我们要拷贝的这个对象本身, 它本身有一个父对象怎么办, 就是这个时候是只拷贝这么一个对象, 还是把它的父对象一起拷贝过来呢

#include <stdio.h>
#include <windows.h>

class CBase
{
    private:
        int z;
    public:
        CBase()
        {

        }
        CBase(int z)
        {
            this->z = z;
        }
};

class CObject:public CBase
{
    private:
        int x;
        int y;
    public:
        CObject()
        {

        }
        CObject(int x, int y, int z):CBase(z)   // 在这个有参数的构造函数里, 告诉编译器用父类的这个构造函数进行初始化
        {
            this->x = x;
            this->y = y;
        }
};

int main(int argc, char* argv[])
{
    COBject obj(1, 2, 3);

    CObject objNew(obj);    // 拷贝构造函数

    CObject* p = new CObject(obj);

    return 0;
}

运行结果显示, 父类也被复制过来了

现在可以下结论了, 拷贝构造函数是编译器提供给我们的, 它不仅可以把子类的内容复制过来, 也可以把父类的内容复制过来. 无论你有多少个父类, 它这个拷贝的过程是全部包含的, 全部复制过来.

那么拷贝构造函数我们是否可以自己写一个呢 ? 可以但没必要, 因为编译器提供的这个已经非常完美了.

但是, 有且仅有一种情况, 拷贝构造函数无法满足我们的需求, 如果我们见到这种情况的话, 必须要我们自己重写拷贝构造函数

拷贝构造函数的问题

class CObject
{
    private:
        int m_nLength;
        char* m_strBuffer;
    public:
        CObject()
        {

        }
        CObject(const char* str)
        {
            m_nLength = strlen(str) + 1;        // 得到传进来字符串的长度
            m_strBuffer = new char[m_nLength];  // 根据你需要的长度在堆中申请了一段空间, 相当于 malloc
            memset(m_strBuffer, 0, m_nLength);  // 对申请的内存进行初始化
            strcpy(m_strBuffer, str);           // 拷贝字符串, 把传进来的字符串拷贝到我申请的这块内存里面去
        }
        ~CObject()                              // 当我们的对象不再需要的时候, 我们可以通过析构函数把这块空间释放掉
        {
            delete[] m_strBuffer;
        }
}

// 我在主函数里面创建的对象, 当我主函数执行完的时候, 这个析构函数会把空间释放掉
int main(int argc, char* argv[])
{
    CObject oldObj("编程达人");

    CObject newObj(oldObj);

    return 0;
}

它把指针也原封不动地复制过来了, 这就意味着两个对象, 指向的是同一个字符串, 那这样的话第一个对象如果释放了空间的话, 那第二个对象指向的地方就出问题了

指针的这个情况就不能使用拷贝构造函数了, 拷贝构造函数只复制对象成员的值, 并不复制对象成员所指向的值

上面所说的拷贝构造函数做的事情, 我们称为浅拷贝

如果你能把对象在堆中申请的空间, 重新申请一个, 并且里面内容都一样的话, 那么我们称之为深拷贝

我们一旦遇到这种情况, 必须自己重写一个拷贝构造函数

拷贝构造函数的实现

那么拷贝构造函数如何实现呢 ?

先看代码

class CObject
{
    private:
        int m_nLength;
        char* m_strBuffer;
    public:
        CObject()
        {

        }
        CObject(const char* str)
        {
            m_nLength = strlen(str) + 1;
            m_strBuffer = new char[m_nLength];
            memset(m_strBuffer, 0, m_nLength);
            strcpy(m_strBuffer, str);
        }
        CObject(const CObject& obj) // 添加了一个新的构造函数, 它比较特殊, 它的参数是固定的, 参数是一个引用类型, 并且是当前这个类的引用类型 ---- 自己定义的拷贝构造函数
        {
            m_nLength = obj.m_nLength;  // 把要拷贝对象的第一个成员直接拿过来
            m_strBuffer = new char[m_nLength];  // 创建一块新内存
            memset(m_strBuffer, 0, m_nLength);  // 初始化内存
            strcpy(m_strBuffer, obj.m_strBuffer);   // 把旧对象的值拷贝过来放到申请的内存中, 这样我就不是指针的拷贝了, 而是指针指向的内容的拷贝了
        }
        ~CObject()
        {
            delete[] m_strBuffer;
        }
}

当我们自己写了拷贝构造函数之后, 编译器就不再提供拷贝构造函数. 这个拷贝构造函数, 你要么不写, 要么就写全了, 如果不写全, 那你函数里复制几个成员, 新创建的对象就是几个成员

总结

  1. 如果不需要深拷贝, 不要自己添加拷贝构造函数

  2. 如果你添加了拷贝构造函数, 那么编译器将不再提供, 所有的事情都需要由新添加的函数自己来处理

如果当前对象有父类, 我们重写拷贝构造函数必须要在后面加上 :Base(obj), 明确告诉编译器替我调用父类的拷贝构造函数

MyObject(const MyObject& obj):Base(obj)
{
    // 哪些成员需要拷贝, 不能有遗漏
}