C++类默认拷贝构造函数的弊端

C++类的中有两个特殊的构造函数,(1)无参构造函数,(2)拷贝构造函数。它们的特殊之处在于:
(1) 当类中没有定义任何构造函数时,编译器会默认提供一个无参构造函数且其函数体为空;
(2) 当类中没有定义拷贝构造函数时,编译器会默认提供一个拷贝构造函数,进行成员变量之间的拷贝。(这个拷贝操作是浅拷贝)

这里只讲拷贝构造函数。在C语言中,

int a = 5;  //初始化
int b;
b = 6;      //赋值

上面的初始化及赋值操作是最正常不过的语法,C++语言肩挑兼容C语言语法的责任,所以在类的设计上,也兼容这种操作:

class cls
{
pubic:
    //...
}

int main(void)
{
    cls c1;
    cls c2 = c1;    //初始化类,还可以 cls c2(c1);
    cls c3;

    c3 = c1;        //赋值类

    //...

    return 0;
}

如上的初始化类需要调用到cls类的默认实现的拷贝构造函数,为类赋值需要调用的是cls类的默认实现的赋值操作符重载函数,它们都是 浅度拷贝 的。前者其原型为:

cls(const cls& c)

默认的拷贝构造函数存在弊端,看如下类定义:

class TestCls{
public:
    int a;
    int *p;

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

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

类中的指针p在构造函数中分配的空间,在析构函数中释放。

int main(void)
{
    TestCls t;

    return 0;
}

编译运行确实不会出错:

https://ae01.alicdn.com/kf/U98b9a3c1b2f148b48f04958f6853fcc11.jpg

类在我们没有定义拷贝构造函数的时候,会默认定义默认拷贝构造函数,也就是说可以直接用同类型的类间可以相互赋值、初始化:

int main(void)
{
    TestCls t1;
    TestCls t2 = t1;   //效果等同于TestCls t2(t1);

    return 0;
}

编译通过,运行却出错了:

https://ae01.alicdn.com/kf/U411c05c9c540412aa9d7e75208d86975l.jpg
[tip type=”tip success” ]

原因就在于,默认的拷贝构造函数实现的是 浅拷贝

[/tip]

深度拷贝和浅拷贝

深度拷贝和浅拷贝在c语言中就经常遇到的了,在这里我简单描述。
一般的 赋值操作 是深度拷贝:

//深度拷贝
int a = 5;
int b = a;

简单的 指针指向,则是浅拷贝:

//浅拷贝
int a = 8;
int *p;
p = &a;

char* str1 = "HelloWorld";
char* str2 = str1;

将上面的浅拷贝改为深度拷贝后:

//深度拷贝
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);
[tip type=”tip info” ]

总而言之,拷贝者和被拷贝者若是 同一个地址 ,则为浅拷贝,反之为深拷贝。

[/tip]

例:以字符串拷贝为例,浅拷贝 后,str1和str2同指向0x123456,不管哪一个指针,对该空间内容的修改都会影响另一个指针。

https://ae01.alicdn.com/kf/U35c9966b27554611ad2d312426afeafbX.jpg

深拷贝 后,str1和str2指向不同的内存空间,各自的空间的内容一样。因为空间不同,所以不管哪一个指针,对该空间内容的修改都不会影响另一个指针。

https://ae01.alicdn.com/kf/U35c9966b27554611ad2d312426afeafbX.jpg

解决默认拷贝构造函数的弊端

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

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

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

[/tip]

解决办法十分简单,自定义拷贝构造函数,里面用深度拷贝的方式为拷贝类初始化:

class TestCls{
public:
    int a;
    int *p;

public:
    TestCls()
    {
        std::cout<<"TestCls()"<<std::endl;
        p = new int;
    }


    TestCls(const TestCls& testCls)
    {
        std::cout<<"TestCls(const TestCls& testCls)"<<std::endl;
        a = testCls.a;
        //p = testCls.p;
        p = new int;

        *p = *(testCls.p);      //为拷贝类的p指针分配空间,实现深度拷贝
    }

    ~TestCls()
    {
        delete p;   
        std::cout<<"~TestCls()"<<std::endl;
    }
};

int main(void)
{
    TestCls t1;
    TestCls t2 = t1;

    return 0;
}

编译运行正常:

https://ae01.alicdn.com/kf/Uffc45284b1a34fd7ae5d220dc04f1e9bm.jpg

关于c++拷贝构造函数的深度拷贝和浅拷贝的介绍到这里,其实还可以将它们的地址打印出来看看,不过这一步就不再赘述了。

拷贝构造函数其它妙用

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