C++深拷贝、浅拷贝 以及默认拷贝构造函数的缺陷

1、深拷贝 和 浅拷贝

1.1 深拷贝

//深度拷贝
int a = 5;
int b = a;
//深度拷贝
int a = 8;
int *p = new int;
*p = a;
char* str1 = "HelloWorld";
int len = strlen(str1);
char *str2 = new char[len];
memcpy(str2, str1, len);

1.2 浅拷贝

浅拷贝是简单的指针指向。逐字节拷贝指针的值(而不去开辟新的内存空间)。

//浅拷贝
int a = 8;
int *p;
p = &a;
char* str1 = "HelloWorld";
char* str2 = str1;

综上所述:拷贝者和被拷贝者若是同一个地址,则为浅拷贝,反之为深拷贝。

2、C++的默认拷贝构造函数

1.1 拷贝构造函数

class cls
{
public:
    //...
}

int main(void)
{
    cls c1;
    cls c2 = c1;    //初始化类,还可以写成 cls c2(c1);
    
    cls c3;
    c3 = c1;        //赋值类
    return 0;
}

在上述代码中,初始化类需要调用cls类的默认拷贝构造函数,而赋值类需要调用cls类的默认赋值操作符重载函数,他们都是浅拷贝。

当我们没写拷贝构造函数,运行如下代码时,编译器能够通过,但是实际运行时会报错。

class TestCls
{
public:
    int a;
    int *p;
public:
    TestCls()   //无参构造函数
    {
        std::cout<<"调用无参构造函数"<<std::endl;
        p = new int;
    }

    ~TestCls()     //析构函数
    {
        delete p;   
        std::cout<<"调用析构函数"<<std::endl;
    }
};

int main(void)
{
    TestCls t1;
    TestCls t2 = t1;  //效果等同于TestCls t2(t1),这里调用的是默认拷贝构造函数
    return 0;
}

为什么编译能通过,但是会报错呢?
这就涉及到了默认拷贝构造函数的缺陷了。

2.2 默认拷贝构造函数的弊端

类的默认拷贝构造函数只会用被拷贝类的成员的值为拷贝类简单初始化,也就是说二者的p指针指向的内存空间是一致的。以前面TestCls类为例,编译器为我们默认定义的默认拷贝构造函数为:

TestCls(const TestCls& testCls)
{
    a = testCls.a;
    p = testCls.p;      //两个类的p指针指向的地址一致。
}

堆区内存重复释放:
main函数将要退出时,拷贝类t2的析构函数先得到执行,它把自身p指向的堆空间释放了;然后,t1的析构函数得到调用,它同样要去析构自身的p指向指向的堆空间,但是该空间和t2类中p指向的空间一样,造成重复释放,程序运行崩溃。

2.3 解决办法:

如果属性有在堆区开辟的,一定早自定义拷贝构造函数,防止浅拷贝带来的问题。

TestCls(const TestCls& testCls)
{
    std::cout<<"调用拷贝构造函数"<<std::endl;
    a = testCls.a;
    //p = testCls.p;  这是错误的,这个还跟默认拷贝构造函数一样了
    p = new int;
    *p = *(testCls.p);      //为拷贝类的p指针分配空间,实现深度拷贝
}

2.4 后记

  • C++拷贝构造函数小妙招:
    自定义一个拷贝构造函数,并设置为private属性,其实现体可以什么都不写,那么这个类将变成一个不可被复制的类了。

3、补:拷贝构造函数的调用场景

3.1 使用一个一创建完毕的对象初始化一个新对象

这是最常见的情景。

class cls
{
public:
    tmp = 0;
    cls(int a)
    {
    	tmp = a;
    }
}

void test()
{
	cls p1(10);
	cls p2(p1);    // 也可以写成 cls p2 = p1;
}

3.2 值传递的方式给函数参数传值

void do_work(cls p)
{
}

void test()
{
	cls p1(0);
	do_work(p1);
}

上面的例子中,在调用do_work函数时进行值传递时,是调用了默认拷贝构造函数,拷贝了一个新的对象传给do_work,而不是直接把p1作为参数传进去。
(可通过打印p和p1的地址来验证)

3.3 以值方式返回局部对象

cls test()
{
	cls p1(0);
	return p1;
}

int main()
{
	cls p = test();
	return 0;
}

上面代码中,test函数返回的对象并不是p1,而是通过调用默认拷贝构造函数,拷贝了一个新的对象作为返回值。(可通过打印p1和p的地址来验证)

4、补:构造函数调用规则

  • C++默认情况下提供,(无参)构造函数,析构函数,拷贝构造函数
  • 若用户自定义了有参构造函数,则C++不提供默认构造函数,但还会提供默认拷贝构造函数
  • 若用户定义了拷贝构造函数,则不会提供其他构造函数


————————————————

版权声明:本文参考自CSDN博主「mybright_」的原创文章
原文链接:https://blog.csdn.net/qq_29344757/article/details/76037255

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值