深浅拷贝剖析

【摘要】string类的拷贝构造和赋值拷贝函数一定会面临浅拷贝和深拷贝,要想合理使用这两种函数,那就必须要想出解决的办法。浅拷贝面临多次析构以及改动一个就会全部改变的问题,很容易造成内存泄漏,接下来我就先把浅拷贝的产生原因以及对应的解决办法分享给大家吧。

目录


1.简析拷贝构造和赋值构造

2.引用计数法

3.写实拷贝法


前言

在这里,我先说一下什么情况下是拷贝构造,什么情况下是赋值构造吧。

拷贝构造是在对象被创建并用另一个已经存在的对象来初始化它时调用的,而赋值函数只能把一个对象赋值给另一个已经存在的对象,使得那个已经存在的对象具有和源对象相同的状态。


但是,这两种函数如果不经过特殊处理的话都会面临一个很明显的问题,多次析构,这只是其中一个问题,另一个问题就是,如果修改了其中一个指针指向的地址的内容,其他的也会受到影响,这一定不是你想要的,所以,为了解决这些问题,才有了下面的两种解决办法。

浅拷贝的代码(方便进行对比)

class String
{
	String::String(const char* str="")//构造函数,开辟一段空间,然后给里面放上一个'/0',表示为空
		:_str(new char[strlen(str)+1])
	{
		strcpy(_str,str);
	}
}

String::String(const String&s)//构造拷贝函数,让一个指针指向这段内存空间
       :_str(s._str)
{}

String &String::operator=(const String&s)// = 操作符的重载,相当于直接让s1指向s2,指针的指向发生改变
{
	if(this !=&s)
	{
		_str=s._str;
	}
	return *this;
}

String::~String()//析构函数用delete[],析构一个字符串
{
    if (_str)
    {
        delete[] _str;
    }
}

一.引用计数法

引用计数法其实应用的地方很广泛,在哈希表中就使用过引用计数法来限制多次删除的问题发生,同样的,在linux的进程管理中,也出现了这种用法,总之,这种用法用处很多,下面我就简单介绍一下如何实现。

原理

       为了处理多次析构的问题,我们加入一个pcount ,每使用一个拷贝构造函数,就相当于多了一个指针只想这段内存空间,那么pcount就进行加1的操作。由于函数出了作用域会自动调用析构函数释放内存,假如说我们拷贝了两个对象,但是实际上我们并没有开辟空间,因此只有一段空间。所以,每次函数要调用析构函数时,我们就让pcount -1,直到pcount为0时,我们就使用delete []真正的释放这段空间,这样就可以避免多次释放内存空间从而造成内存泄漏

代码如下

/引用计数拷贝法
String::String(const char* str = "")//拷贝
        :_str(new char[strlen(str)+1])//加入一个pcount增加了计数功能
        ,_pCount(new int(1))//开辟1块空间并初始化为1 
{
    strcpy(_str, str);
}

String::String(const String&s)//拷贝构造
:_str(s._str)                 //拷贝构造一次,那么引用计数就加1
,_pCount(s._pCount)
{
    (*_pCount)++;
}

String s1("hello world")
{
    String s2(s1);
    s1[2] = 'x';
    cout << s1.c_str() << endl;
    cout << s2.c_str() << endl;
}

String& String:: operator=(const String&s)//赋值运算符重载
{
    if (this != &s)
    {
        if (--(*_pCount) == 0)//这里不调用析构函数,
            //因为析构函数一般都是在出了函数作用域才被调用
        {
            delete[] _str;
            delete _pCount;
        }
        _str = s._str;
        _pCount = s._pCount;
        ++(*_pCount);
    }
    return *this;
}

二.写实拷贝

           写实拷贝也可以解决浅拷贝带来的问题。它的想法其实也很简单,大概思路是这样的,我写在下面,如果你们看了还是不理解的话可以私信我哦

浅拷贝为什么会造成多次析构的问题呢?根本原因就是因为没有开辟空间,所以几个指针指向的都是同一段内存空间,所以就会互相影响,要想解决这个问题,只需要重新开辟空间就好了,这样它们就可以互不干涉,你可以进行你的析构和更改,我可以操作我的。


这样做,s1指向的是s1的内存空间,s2指向s2的内存空间,等到函数出了作用域就会自动调用析构函数,这里析构的顺序还是按照先构造的后析构的原则。

代码如下

String::String(const char* str = "")
:_str(new char[strlen(str)+5])
{
    _str += 4;
    strcpy(_str, str);
    *((int*)(_str-4)) = 1;
}
//s2(s1)
String::String(const String& s)
:_str(s._str)
{
    *((int*)(_str-4)) += 1;
}
//s1=s2
String& String ::operator=(const String& s)
{
    if (this!=&s)
    {
        if (--(*(int*)(_str - 4)) == 0)
        {
            delete[](_str - 4);
        }
        _str = s._str;
        ++(*(int*)(_str - 4));
    }
    return *this;
}
void String::CopyOnWrite()
{
    if ((*(int*)(_str-4))>1)
    {
        char* newstr = new char[strlen(_str) + 5];
        strcpy(newstr, _str);
        --(*(int*)(_str - 4));
        _str = newstr;
        ++(*(int*)(_str - 4));

    }

}
char &String::operator[](size_t pos)
{
    CopyOnWrite();
    return _str[pos];
}
总结
      深浅拷贝的问题不光会在string类中出现,在C++的链表,二叉树等等数据结构的操作中都会有所体现,因此,不管是什么样的场合下,我们都应该懂得如何去使用这两种方法,毕竟这两种方法各有优点。


猜你喜欢

转载自blog.csdn.net/zb1593496558/article/details/79976472