C++深浅拷贝详解(传统现代写法)

●浅拷贝:

        顾名思义,浅拷贝只是将对象的值拷贝过来。

       如果没有显示实现构造函数或是拷贝构造函数,那么编译器会默认以浅拷贝的方式自动生成。

#define _CRT_SECURE_NO_WARNINGS

#include<iostream>
#include<Windows.h>
#include<string.h>
using namespace std;

class String
{
public:
	String()
		:_str(new char[1])
	{
		_str[0] = '\0';
	}

	String(const char* str)
		:_str(new char[strlen(str)+1])
	{
		strcpy(_str, str);
	}

	~String()
	{
		delete[] _str;
	}
private:
	char* _str;
};

void test1()
{
	String s1;
	String s2("hello");
	String S3(s2);

}

int main()
{
    test1();
    system("pause");
    return 0;
}

我们自己模拟一个string类显示实现其构造函数和析构函数,运行程序,test函数中,编译器会调用s1和s2我们自己模拟实现的构造函数,默认生成S3的拷贝构造函数,而编译器默认实现的是浅拷贝,也就是说,S3和S2拥有相同的一块空间,这有点像我们学过的引用,可以把它理解为S3是s2的别名,但是这会引起严重的后果——一块空间由于多次释放而程序崩溃。在test函数调用完成时,编译器要依次析构S3,s2和s1,当S3析构完成,s2以为这块空间还在,去释放的时候就会导致系统崩溃。

调试程序,可以看到s2和S3地址相同,这就验证了前面说的编译器默认是浅拷贝去调用它的拷贝构造函数,其实调用构造函数也是如此。

如果不写析构函数的话,浅拷贝是没有什么问题的,但是一旦自己实现了析构函数,系统就会崩溃。

●深拷贝

1>为了解决上述问题,引入深拷贝。

2>有的类中涉及到资源管理,就要用深拷贝的方式实现,并且要显式给出其构造函数,拷贝构造函数,析构函数和赋值运算符的重载。

3>不难想到,深拷贝就是为每个对象分配自己独有的资源,这样就不会由于多个对象共有一块空间而引起违规访问的问题了,不管析构多少次,都是没有问题的。

  ●下面是深拷贝的传统写法

             传统写法符合我们的正常思路,先创建相同大小的空间,再把内容拷过去。    

class String
{
public:
	//构造
	String(const char* str = "")
		:_str(new char[strlen(str) + 1])
	{
		strcpy(_str, str);
	}

	//拷贝构造
	String(const String& s)
		:_str(new char[strlen(s._str)+1])
	{
		strcpy(_str, s._str);
	}

	//重载等号
	//s1(this)=s2(s)
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			//释放旧空间
			delete[] _str;
			//开辟新空间并拷贝s内容
			char* newstr = new char[strlen(s._str) + 1];
			strcpy(newstr, s._str);
			//this指向新空间
			_str = newstr;
		}
		return *this;
	}

	~String()
	{
		if(_str)
			delete[] _str;
	}
private:
	char* _str;
};
void test2()
{
	String s1;
	String s2("hello");
	String s3(s2);
	String s4 = s2;
}
int main()
{
	//test1();
	test2();
	system("pause");
	return 0;
}

1>>s3由s2拷贝构造得到——>为s3开辟与s2同样大小的空间,再将s2中内容赋到这块空间中。

2>>s4由s3赋值得到——>将s4空间释放后,新开辟一块与s2相同大小的空间并将s2中内容赋给它,然后让s4指向这块空间。

通过监视看到s2,s3,s4地址都不相同,说明它们各自拥有不同的空间。

 ●深拷贝的现代写法:

class String
{
public:
	//构造
	String(const char* str = "")
		:_str(new char[strlen(str) + 1])
	{
		strcpy(_str, str);
	}

	//拷贝构造
	String(const String& s)
		:_str(nullptr)
	{
		//用s构造新的String
		String tmp(s._str);
		//交换this和tmp
		swap(_str, tmp._str);
	}

	//重载=
	//s1=s2
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			String tmp(s._str);
			swap(_str, tmp._str);
		}
		return *this;
	}
        /*
	String& operator=(String& s)
	{
	
		swap(_str, s._str);
		return *this;
	}
       */


	~String()
	{
		if (_str)
			delete[] _str;
	}
private:
	char* _str;
};
void test3()
{
	String s1;
	String s2("hello");
	String s3(s2);
	String s4 = s2;
}
int main()
{
	//test1();
	//test2();
	test3();
	system("pause");
	return 0;
}

1>>s3由s2拷贝构造得到——>由s2构造一个新tmp,再将这个tmp和s3交换,实际上是交换了它指针的指向。

        一开始s3并不知道指向哪里,但是通过交换我们可以拿到我们想要的内存,至于tmp最终指向哪里我们并不关心,最后编译器会释放它的。

2>>s4由s2赋值得到——>由s2构造一个新tmp,再将这个tmp和s4交换,返回s4。

       s4一开始已经有自己的内存,s2和s4交换指针指向,出了作用域tmp会自动调用它的析构函数,也就把原来s4指向的那块内存释放了。

现代写法是在传统写法的方式进行改进,虽然逻辑变得复杂,但是更加高效

猜你喜欢

转载自blog.csdn.net/a_struggling_monkey/article/details/84451714