C++深浅拷贝浅析

C++中深拷贝和浅拷贝的问题是很值得我们注意的知识点,如果编程中不注意,可能会出现疏忽,导致bug。本文就详细讲讲C++深浅拷贝的种种。

       对于一般的对象,如:

C++代码 int a = 10;   int b = 20; 

       它们之间的赋值、复制过程是很简单的。但是对于类对象来说,其内部存在各种类型成员变量,在拷贝过程中会出现问题。如下:

C++代码 #include<iostream>    #include<cstring>    using namespace std;    class String {    public:        String (const char* psz=NULL) : m_psz(strcpy((new char[strlen(psz?psz:"")+1]),psz?psz:"")){            cout << "String构造" << endl;        }         ~String () {            if(m_psz) {                 delete[] m_psz;                 m_psz = NULL;            }            cout << "String析构" << endl;        }        char* c_str(void) {            return m_psz;        }    private:        char* m_psz;    };    int main(void) {        String s1("hello");        String s2(s1);        cout << "s1    " << s1.c_str() << endl;        cout << "s2    " << s2.c_str() << endl;        s1.c_str()[0] = 'H';        cout << "s1    " << s1.c_str() << endl;        cout << "s2    " << s2.c_str() << endl;        return 0;    } 

       运行结果:

 

       编译通过了,运行后出现一堆的错误,为什么?!这就是浅拷贝带来的问题。

       事实是,在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。原型如下:

C++代码 String (const String& that) {} 

       但凡是编译系统提供的缺省函数,总不是十全十美的。

       缺省拷贝构造函数在拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标--浅拷贝。

       用下图来解释这个问题:

 

       在进行对象复制后,事实上s1、s2里的成员指针m_psz都指向了一块内存空间(即内存空间共享了),在s1析构时,delete了成员指针m_psz所指向的内存空间,而s2析构时同样指向(此时已变成野指针)并且要释放这片已经被s1析构函数释放的内存空间,这就让同样一片内存空间出现了“double free” ,从而出错。而浅拷贝还存在着一个问题,因为一片空间被两个不同的子对象共享了,只要其中的一个子对象改变了其中的值,那另一个对象的值也跟着改变了,正如程序中只改变了s1.c_str()[0] = 'H',然而输出的s1,s2均为hello,所以这并不是真正意义上的复制。

       为了实现深拷贝,往往需要自己定义拷贝构造函数,在源代码里,我们加入自定义的拷贝构造函数如下:

C++代码 String (const String& that) : m_psz(strcpy((new char[strlen(that.m_psz)+1]),that.m_psz)){       cout << "String拷贝构造" << endl;   } 

       这样再运行就没有问题了。

       在程序中,还有哪些情况会用到拷贝构造函数呢?当函数存在对象型的参数或对象型的返回值时都会用到拷贝构造函数。

       而拷贝赋值的情况基本上与拷贝复制是一样的。只是拷贝赋值是属于操作符重载问题。例如在主函数若有:String s3;s3 = s2;这样系统在执行时会调用系统提供的缺省的拷贝赋值函数,原型如下:

C++代码 void operator = (const String& that) {} 

       我们可以自定义拷贝赋值函数如下:

C++代码 void operator=(const String& that) {       m_psz = strcpy (new char[strlen(that.m_psz)+1],that.m_psz);   } 

       但是这只是新手级别的写法,考虑的问题太少。我们知道对于普通变量来讲a=b返回的是左值a的引用,所以它可以作为左值继续接收其他值(a=b)=30,这样来讲我们操作符重载后返回的应该是类对象的引用(否则返回值将不能作为左值来进行运算),如下:

C++代码 String& operator=(const String& that){       m_psz = strcpy (new char[strlen(that.m_psz)+1],that.m_psz);   } 

       而 m_psz = strcpy (new char[strlen(that.m_psz)+1],that.m_psz);这种写法其实也有问题,因为在执行语句时,m_psz已经被构造已经分配了内存空间,但是如此进行指针赋值,m_psz直接转而指向另一片新new出来的内存空间,而丢弃了原来的内存,这样便造成了内存泄露。应更改为:

C++代码 String& operator=(const String& that) {       delete[] m_psz;       m_psz = strcpy (new char[strlen (that.m_psz)+1],that.m_psz);   } 

       这样就行了吗?在这个世界上不怕没好事就怕没好人,万一他跟你搞一个自赋值(s3=s3)怎么办?

       操作符左右两边都是同一个对象,这样先delete[] m_psz,后面又有that.m_psz,这就出现了问题。所以为了防止自赋值,我们一般的写法为:

C++代码 String& operator=(cosnt String& that) {       if(&that != this) {           delete[] m_psz;           m_psz = strcpy (new char[strlen(that.m_psz)+1],that.m_psz);          }       return *this;   } 

       可是这样写就完善了吗?是否要再仔细思索一下,还存在问题吗?!其实我可以告诉你,这样的写法也顶多算个初级工程师的写法。前面说过,为了保证内存不泄露,我们前面delete[] m_psz,然后我们在把new出来的空间给了m_psz,但是这样的问题是,你有考虑过万一new失败了呢?!内存分配失败,m_psz没有指向新的内存空间,但是它却已经把旧的空间给扔掉了,所以显然这样的写法依旧存在着问题。一般高级工程师的写法会是这样的:

C++代码 String& operator=(cosnt String& that) {        if(&that != this) {            char *psz = strcpy (new char[strlen(that.m_psz)+1],that.m_psz);//如果失败会抛出异常,m_psz最后在析构函数里释放            delete[] m_psz;            m_psz = psz;           }        return *this;    } 

       这样考虑的问题便比较全面了。 转载请注明来源写论文的方法 http://www.400qikan.com/news/1448.html

 

原文链接: https://www.cnblogs.com/rtrur/archive/2013/06/08/3127131.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    C++深浅拷贝浅析

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/91604

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月10日 上午1:15
下一篇 2023年2月10日 上午1:15

相关推荐