C++ string类的模拟实现 -> 浅拷贝与深拷贝

在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数,所以我们实现几个简单的接口即可。

大家看一下以下string类的实现是否有问题?

//为了和标准库区分,此处用String
class String
{
public:
	//无参的构造函数
	//String()
	//	//:_str(nullptr)//错误写法,在计算strlen的时候,会解引用空指针发生崩溃
	//	:_str(new char[1])
	//{
	//	_str[0] = '\0 ';
	//}
	带参的构造函数
	//String(const char* str)
	//	:_str(new char[strlen(str)+1])//多开辟一个空间存放'\0'
	//{
	//	strcpy(_str, str);
	//}

	//无参的和带参的构造函数,写一个全缺省的构造函数即可
	//String(const char* str="\0")//错误示范.
	//String(const char* str=nullptr)//错误示范。strlen的时候解引用空指针引发崩溃

	String(const char* str = "")
		:_str(new char[strlen(str) + 1])
	{
		strcpy(_str, str);
	}
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
	size_t size()
	{
		return strlen(_str);
	}
	char& operator[](size_t i)
	{
		return _str[i];
	}
private:
	char* _str;//这里模拟实现简单的string类,没有定义_size,_capacity成员变量
};
//测试
void Test_String()
{
	String s1("hello world");
	String s2(s1);
}

首先说一下构造函数为什么这样实现:构造函数在初始化列表的时候对_str 开辟一段空间,并进行拷贝,是因为string类的实现,通常要完成增删查改等操作,而字符串是一个常量,放在代码段,不能被修改,此外还要考虑增容问题,s1存放在栈区,所以s1要在堆上申请空间,并把字符串拷贝过去,在堆上进行相关操作,这样才能解决问题。

上面代码测试运行之后,程序崩溃。

说明:上述String类的实现我们没有显示定义其拷贝构造赋值运算符重载编译器会合成默认的,当用s1构造s2时,编译器调用默认的拷贝构造。最终导致s1和s2指向的同一块内存空间释放两次,从而引发崩溃。这种拷贝方式,称为浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项时,就会发生发生了访问违规。

深拷贝:如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

深拷贝与浅拷贝的区别:

浅拷贝(Shallow Copy)

浅拷贝只是简单地将源对象的所有成员值(包括指针成员的地址值)复制到新对象中。如果对象中有指针指向动态分配的内存,那么浅拷贝后的对象将与原对象共享同一块内存这意呀着,如果原对象或新对象中的任何一个释放了这块内存(使用delete),那么另一个对象将指向一个无效的内存地址,这可能导致未定义行为,如程序崩溃

深拷贝(Deep Copy)

深拷贝在复制对象时,除了复制对象的所有成员值外,还会为对象中的动态分配的内存创建新的副本,并更新指针成员以指向这些新的副本。这样,原对象和新对象将完全独立,互不影响即使其中一个对象释放了动态分配的内存,也不会影响到另一个对象。

总之深拷贝和浅拷贝的主要区别在于拷贝的层次和对象间的独立性。在选择使用哪种拷贝方式时,需要根据具体需求和场景来决定。

深拷贝两种实现方法:

一、深拷贝->传统写法

传统写法通常通过手动分配内存,并逐个复制对象的成员变量来实现深拷贝。这种方法需要程序员显式地管理内存,包括使用new和delete。

//深拷贝->传统写法
String(const String& s)
    :_str(new char[strlen(s._str)+1])
{
}
String(const String& s)
	:_str(new char[strlen(s._str)+1])
{
	strcpy(_str, s._str);
}
String& operator=(const String& s)
{
	if (this != &s)
	{
		char* tmp = new char[strlen(s._str )+ 1];
		strcpy(tmp, s._str);
		delete[] _str;
		_str = tmp;
	}
		return *this;
}
~String()
{
	if (_str)
	{
		delete[] _str;
		_str = nullptr;
	}
}
private:
char* _str;
};

在这个例子中,构造函数、拷贝构造函数和赋值操作符都通过new来分配内存,并通过strcpy来复制字符串。析构函数则负责释放内存。

二、深拷贝->现代写法

现代写法通常利用C++的某些特性来简化深拷贝的实现,比如使用智能指针(如std::unique_ptr或std::shared_ptr)、标准库中的容器(如std::vector、std::string)或者通过交换指针来实现。以下是一个使用交换指针的方式实现深拷贝。

//深拷贝->现代写法
class String
{
public:
	String(const char* str = "")
		:_str(new char[strlen(str) + 1])
	{
		strcpy(_str, str);
	}
	//深拷贝->现代写法
	//s2(s1)
	String(const String& s)
		:_str(nullptr)//如果不初始化,_str为随机值,函数结束时,临时对象tmp调用析构函数释放空间,释放随机值就会有问题。
	{
		String tmp(s._str);//临时对象tmp调构造函数初始化,tmp就拥有s1一样大的空间和值。
		swap(_str,tmp._str);//此时tmp就是s2想要的,交换。
		//交换完成以后,临时对象tmp调用析构函数对s2的_str指向的空间进行释放。
	}
	//s3=s1
	String& operator=(String s)//参数传值,临时对象s就是s1调用拷贝构造构造出来的,s就拥有s1一样大的空间和值。
	{
		swap(_str, s._str);//此时s就是s3想要的,交换。
		//交换完成以后,临时对象s会调用析构函数对本来s3的_str指向的空间进行释放(发生交换了)
		return *this;
	}
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
	
private:
	char* _str;
};

在这个例子中,拷贝构造函数通过创建一个临时对象tmp并交换指针来实现深拷贝。赋值操作符则采用了所谓的“拷贝并交换”技术(Copy-and-Swap),它首先通过按值传递的方式创建一个新对象s(这会自动调用拷贝构造函数),然后交换指针。这种方法的好处是简洁且易于管理内存。

深拷贝传统写法与现代写法的特点对比:

一、深拷贝传统写法

  • 手动管理内存,包括分配和释放。
  • 需要显式地复制对象的所有成员变量。
  • 代码相对复杂,容易出错,特别是在处理复杂的对象结构时。

二、深拷贝现代写法

  • 减少或避免手动管理内存。
  • 利用C++的高级特性来简化实现。
  • 代码更简洁,更易于理解和维护。

模拟实现简单的string类:

class String
{
public:

	//传统版String写法
	/*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);
	}
	string& operator=(const string& s)
	{
		if (this != &s)
		{
			char* tmp = new char[strlen(s._str )+ 1];
			strcpy(tmp, s._str);
			delete[] _str;
			_str = tmp;
		}
			return *this;
	}*/

	//现代版String写法
	String(const char* str = "")
		:_str(new char[strlen(str) + 1])
	{
		strcpy(_str, str);
	}
	String(const String& s)
		:_str(nullptr)
	{
		String tmp(s._str);
		swap(_str, tmp._str);
	}
	String& operator=(String s)
	{
		swap(_str, s._str);
		return *this;
	}
	size_t size()
	{
		return strlen(_str);
	}
	char& operator[](size_t i)
	{
		return _str[i];
	}
	~String()
	{
		delete[] _str;
		_str = nullptr;
	}
private:
	char* _str;
};
//测试
void Test_String()
{
	String s1("hello world");
    String s2(s1);
    for (size_t i = 0; i < s2.size(); i++)
    {
     	cout << s2[i];
    }
	cout << endl;
	String s3;
	s3 = s1;
	for (size_t i = 0; i < s3.size(); i++)
	{
		 cout << s3[i];
	}
	cout << endl;
}

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值