美文网首页
C++<第二十九篇>:拷贝构造函数

C++<第二十九篇>:拷贝构造函数

作者: NoBugException | 来源:发表于2022-01-28 14:19 被阅读0次

拷贝构造函数其实就是对象的拷贝,在程序设计中,对象的拷贝其实很常见。

(1)拷贝构造函数的两种实现方式

【方式一】

Copy c1(10), c2;

c2 = c1; // 对象的默认拷贝方式

采用直接赋值的方式,Copy 是一个类,它定义了两个对象:c1 和 c2,将 c1 直接赋值给 c2。

【方式二】

Copy(const Copy& copy)
{
    this->a = copy.a;
}


Copy c1(10);
Copy c2(c1); // 拷贝构造函数

自定义一个 构造函数,参数时对象的引用,将已经建立实例的对象作为实参传递构造方法中,从而实现对象的拷贝。

(2)浅拷贝和深拷贝

浅拷贝 只是对指针的拷贝,浅拷贝后两个指针指向同一个内存空间;
深拷贝 不仅对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针;

当对一个已知对象进行拷贝时,编译系统会自动调用一种构造函数——拷贝构造函数,如果用户未定义拷贝构造函数,则会调用默认拷贝构造函数。

当拷贝一个基类指针到派生类时,如果调用系统默认的拷贝构造函数,这时只是对指针进行拷贝,两个指针指向同一个地址,这就会导致指针被分配了一次内存,但内存被释放了两次(两次调用析构函数),造成程序崩溃。所以在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。

到底是 浅拷贝 还是 深拷贝? 这个话题的讨论是针对于类中存在指针成员的前提之下的,所以我们

先看下如下演示代码:

class Copy 
{
public:
    Copy() 
    { 
        cout << "执行了无参构造函数" << endl; 
    }
    Copy(int a) 
    {
        cout << "执行了有参构造函数" << endl;
        this->a = a;
    }
    Copy(const Copy& copy)
    {
        cout << "执行拷贝构造函数" << endl;
        this->a = copy.a;
    }

    ~Copy() 
    {
        cout << "执行了析构函数" << endl;
    }
    int getA() 
    {
        return a;
    }
    void setA(int a) 
    {
        this->a = a;
    }
    int* getP() 
    {
        return p;
    }
    void setP(int* p)
    {
        this->p = p;
    }
private:
    int a;
    int* p;
};

void print(Copy& copy) // 打印数组
{
    cout << copy.getA() << endl;
    int* p = copy.getP();
    for (size_t i = 0; i < 10; i++) // 打印数组的值
    {
        cout << *p << " ";
        p++;
    }
    cout << endl;
}

Copy 有一个成员变量是一个指针p,对p的拷贝到底是 浅拷贝 还是 深拷贝 是根据多个Copy对象中的指针 p 指向的内存是 共享 还是 独立 来判断,如果多个Copy对象中的指针 p 指向的内存是同一块内存,那么是 浅拷贝,不是它们的内存都是独立存在的,那么是 深拷贝

现在在栈空间中创建c1和c2两个对象,并且c1初始化:

int* name = new int[10]; // 在堆内存开辟空间
for (size_t i = 0; i < 10; i++) // 数组的初始化
{
    name[i] = i;
}
Copy c1(10), c2; // 定义变量 c1 和 c2
c1.setP(name); // c1的成员变量p指向name

c2使用直接赋值的方式对c2进行初始化(将c1拷贝到c2):

c2 = c1; // 拷贝

c1初始化后,a = 10,p指向name数组,数组的值为:{0,1,2,3,4,5,6,7,8,9};
c2是通过赋值( c2 = c1)的方式初始化的,开始打印c1和c2的值:

cout << "打印 c1:" << endl;
print(c1); // 打印c1数组
cout << "打印 c2:" << endl;
print(c2); // 打印c2数组

打印结果是:

打印 c1:
10
0 1 2 3 4 5 6 7 8 9
打印 c2:
10
0 1 2 3 4 5 6 7 8 9

c1和c2的a的值都是10,a是非指针成员,非指针成员不用管它,因为非指针成员无法区分到底是 浅拷贝 还是 深拷贝
c1和c2的成员变量p指向的数组的值是一致的,但是此时依然无法确定到底是 浅拷贝 还是 深拷贝

为了区分 浅拷贝 还是 深拷贝,我们的策略是对c2的成员p指向的数组重新赋值,再次打印c1和c2指针p的数组的值。

下面,对c2的p指向的数组重新赋值:

for (size_t i = 0; i < 10; i++) // 数组的初始化
{
    name[i] = i * 2;
}
c1.setP(name);

输出代码如下:

c1.setA(20);
cout << "打印 c1:" << endl;
print(c1); // 打印c1数组
cout << "打印 c2:" << endl;
print(c2); // 打印c2数组

输出的结果为:

打印 c1:
20
0 2 4 6 8 10 12 14 16 18
打印 c2:
10
0 2 4 6 8 10 12 14 16 18

c1和c2的a的值分别是20、10,a是非指针成员,非指针成员不用管它,因为这只能说明c1和c2是独立内存,c1和c2在拷贝之前就已经开辟了空间,我们研究的对象是指针p是 浅拷贝 还是 深拷贝
c1和c2的成员变量p指向的数组的值是一致的,当c2的指针p指向数组的值改变时,c1的指针p指向的数组的值也发生了改变,此时已经可以给出结论:c1和c2的指针p所指向的是同一块内存空间,像这种只复制指针,没有复制指针所指向的空间和空间中的数据,就叫做 浅拷贝
所以,另一个结论就是:对象直接赋值的方式(默认拷贝方式)叫做浅拷贝

如果,我们给指针p所指向的数组重新分配内存空间,那么就做到了 完全拷贝,这种方式叫做 深拷贝

演示代码如下:

Copy(const Copy& copy)
{
    cout << "执行拷贝构造函数" << endl;
    this->a = copy.a;
    // this->p = new int[10]; // 将p指向重新分配的内存空间
    this->p = (int*)malloc(sizeof(int) * 10); // 将p指向重新分配的内存空间
    memcpy(this->p, copy.p, 10 * sizeof(int)); // 将 copy.p 所指向的内存区域复制 10 * sizeof(int)个字节到 this->p 中
}

写一个构造函数,形式参数是本身对象的引用。
将指针 p 指向新的内存空间,开辟新的内存空间常用方式有两种:

【第一种】 C++ 新增的方式

this->p = new int[10]; // 将p指向重新分配的内存空间

使用 new 关键字开辟内存。

【第二种】 C 的方式

this->p = (int*)malloc(sizeof(int) * 10); // 将p指向重新分配的内存空间

使用 malloc 或者 calloc 函数开辟内存。

最后,使用 memcpy 函数将一个内存空间的数据拷贝到另一个内存空间中;

我们替换掉直接赋值的拷贝代码:

Copy c2(c1); // 拷贝

执行的结果是:

打印 c1:
0 2 4 6 8 10 12 14 16 18
打印 c2:
0 1 2 3 4 5 6 7 8 9

c1和c2的指针p所指向的内存空间不一致,所以这种方式叫做 深拷贝*。

注意,我们并没有说 Copy c2(c1) 的赋值方式是 深拷贝*,如果将构造函数的代码改成直接赋值指针,而不重新开辟内存:

Copy(const Copy& copy)
{
    cout << "执行拷贝构造函数" << endl;
    this->a = copy.a;
    this->p = copy.p; 
}

那么,c1和c2的成员p所指向的内存空间是同一个,这个方式依然叫做 浅拷贝

所以,判断是哪种拷贝方式的核心思想是:两个指针指向的内存空间是否是同一个

(3)析构函数释放指针的注意点

为了防止内存泄漏,我们需要在析构函数中释放指针:

~Copy() 
{
    delete p;
}

我们知道,c1和c2对象创建时,都执行了构造函数,相应的,他们的析构函数要 执行两次,即 delete p 会 执行两次

如果是 浅拷贝,那么c1的指针p和c2的指针p指向同一块内存区域,第一此执行 delete p 后,p所指向的内存空间已经释放,当第二次执行 delete p 后,由于p指向的内存空间已经释放,所以程序会 直接崩溃

如果是 深拷贝,c1的p和c2的p指向不同的内存空间,执行两次析构函数分别释放两块内存空间,所以程序不会崩溃。

所以得出结论:如果类中含有指针,那么它的拷贝不能用直接赋值的方式,直接重写构造方法,给指针重新分配内存空间

[本章完...]

相关文章

网友评论

      本文标题:C++<第二十九篇>:拷贝构造函数

      本文链接:https://www.haomeiwen.com/subject/eijahrtx.html