(超详细 + 逐个解读) C++的STL学习 3 : string类的简单实现(深浅拷贝问题+基本功能的实现)

经过上一次的string类学习, 想必大家都对string容器有一定的了解了吧. 大家都有在用string吗? 个人觉得是非常好用的…无论是平时写代码还是刷题, 都是非常好用的容器~
当面试的时候有非常大的概率面试官就会让你当场实现一个string类, 那我们今天就来看看怎么简单实现一个有基本功能的string类~, 耐心看完, 你一定有所收获.

一. 深浅拷贝的问题

其实定义一个类, 无非就是写出他的几个成员函数, 比如构造函数, 析构函数.

拷贝构造和赋值运算符重载编译器会自动生成.

但是大家还记不记得前面说过的深浅拷贝的问题呢?

string类的拷贝构造和赋值运算符重载就涉及到深浅拷贝的问题, 
接下来我会向大家说明深浅拷贝的问题

涉及深浅拷贝, 首先这个类中必须有资源.

那问题又来了, 什么是资源?

资源啊, 资源就是不属于类模型中的东西, 都叫资源

比如类A中有一个成员变量是指针char*, char指向了堆上的一片空间,
那么很显然, char
指向的空间并不属于A类, A类只占4个字节, 也就是一个指针的大小
那么当A类调用拷贝构造的时候, 我们知道编译器自动生成的是浅拷贝, 浅拷贝就是我啥也不干, 就把这个地址也给你的指针, 然后创建了一个对象
这时没有什么问题, 但是当程序结束时, 两个对象都要调用析构函数释放资源, 在这里也就是释放空间, 那么现在, 问题来了: 这两个对象指向的空间是一块空间, 释放第一次没问题, 但是第二个对象析构要释放空间的时候就会产生二次释放的问题

举个例子: 你写完了作业, 我想借鉴一下你的, 但是我没写, 我直接把你的作业名字一改, 老师要收的时候我先交了, 那你不就没作业交了? 那这出大问题啊~

这下大家都能明白了吧?

然后我再画个图就更清晰了:

在这里插入图片描述
OK, 理解了深浅拷贝的问题, 下面我们就进行string类的简单实现吧~

二. string类的简单实现

首先要说的是, string类的本质就是一个指针char* 的封装

1. 构造函数

构造函数分为两种情况:
	1. 空的string类: 并不是不做任何操作, 而是要开一个空间放一个'\0'
	2. 给一个字符串: 给指针开一个空间, 把字符串拷贝即可

下面给出代码 :

	String()
		:_ptr(new char[1])
	{
		*_ptr = '\0';
	}

	String(char* ptr)
		:_ptr(new char[strlen(ptr) + 1])
	{
		strcpy(_ptr, ptr);
	}

这里我们其实可以把他俩合并一下, 给一个缺省值为空 ""

代码如下:

	//全缺省的构造函数
	String(const char* ptr = "")
		:_ptr(new char[strlen(ptr) + 1])
	{
		strcpy(_ptr, ptr);
	}

2. 析构函数

上来我们先把两个最简单的写了, 析构函数不用多说, 为了释放资源, 资源就是char* 所指向的空间, 释放这个空间就可

为了代码的安全性, 要进行指针判空

	//析构
	~String() {
		if (_ptr) {
			delete[] _ptr;
			_ptr = nullptr;
		}
	}

3. 拷贝构造

首先我们要知道, 拷贝构造是用一个已经存在的对象去创建另一个对象
这里我们要显式定义拷贝构造: 就要拷贝对象的内容+资源
即给新的对象开辟自己的空间, 然后吧内容拷贝进去~

注意: 拷贝构造参数一定要给引用类型, 不然会引发无穷递归, 详细见我之前的博客, 有兴趣的小伙伴可以去看看C++六大成函数

代码如下:

	String(const String& str)
		:_ptr(new char[strlen(str._ptr) + 1])
	{
		//深拷贝: 开自己的空间, 拷贝str的内容
		strcpy(_ptr, str._ptr);
	}

4. 赋值运算符重载函数

赋值运算符重载函数: 用一个已经存在的对象的值给另外一个已经存在的对象赋值
实现深拷贝要把内容拷贝到自己的空间中
这时为了防止空间不够, 我们直接给他开辟新的合适的空间, 释放原有的空间

这里有一个小小的优化就是判断是否给自己赋值, 是的话直接跳过

代码如下:

	String& operator=(const String& str) {
		if (this != &str) {
			//开空间   
			//因为原来内容不用了, 为了保证空间足够, 所以要新开空间
			char* tmp = new char[strlen(str._ptr) + 1];
			//拷贝资源
			strcpy(tmp, str._ptr);
			//释放原有空间
			delete[] _ptr;
			//指向新的空间
			_ptr = tmp;
		}
		return *this;
	}

到这里我们就已经实现了一个简单的string类了~

下面给出整体的代码:

class String {
public:
	//全缺省的构造函数
	String(const char* ptr = "")
		:_ptr(new char[strlen(ptr) + 1])
	{
		strcpy(_ptr, ptr);
	}


	//系统默认的拷贝构造
	//浅拷贝, 会造成析构时二次释放的问题
	//String(const String& str)
	//	:_ptr(strcpy(_ptr, str._ptr))
	//{}

	//显式定义的拷贝构造, 完成深拷贝: 拷贝对象的内容 + 资源
	String(const String& str)
		:_ptr(new char[strlen(str._ptr) + 1])
	{
		//深拷贝: 开自己的空间, 拷贝str的内容
		strcpy(_ptr, str._ptr);
	}


	//赋值运算符重载函数
	String& operator=(const String& str) {
		if (this != &str) {
			//开空间   因为原来内容不用了, 为了保证空间足够, 所以要新开空间
			char* tmp = new char[strlen(str._ptr) + 1];
			//拷贝资源
			strcpy(tmp, str._ptr);
			//释放原有空间
			delete[] _ptr;
			//指向新的空间
			_ptr = tmp;
		}
		return *this;
	}


	//析构
	~String() {
		if (_ptr) {
			delete[] _ptr;
			_ptr = nullptr;
		}
	}


private:
	char* _ptr;
};

当你以为这篇到此结束了, 你就大错特错了…

接下来给大家来一钵秀的, 拷贝构造和赋值运算符重载的"现代"写法

5. 拷贝构造和赋值运算符的现代写法

不多说, 先直接上代码,

		//拷贝构造  现代写法

		String(const String& str)
			:_ptr(nullptr)  
		{	
			String tmp(str._ptr);
			swap(_ptr, tmp._ptr);
		}

下面说明为什么这样子做可以

  1. 创建一个局部对象tmp: 调用构造函数 --> 开空间 + 内容拷贝
    此时内容已经拷贝完成
  2. 交换了一下指针, 把nullptr给了局部对象tmp, 而把我们需要的字符串交换了过来, 函数结束后局部对象带着交换给他的nullptr就被销毁了, 留下了我们需要的东西
    这钵 ===> 杀人诛心

要注意的是: 一定要给_ptr初始化, 如果不初始化的话, _ptr是一个随机值, 交换给tmp之后, tmp调用析构释放空间时, 随机值的释放是错误的..程序会崩掉

这个, 说实话, 操作一般, 下面这个才是真的秀

来, 上菜

		//赋值运算符重载  现代写法
		String& operator=(String tmp) {
			swap(_ptr, tmp._ptr);
			return *this;
		}

我们可以看到, 代码非常的简短, 但是细节很到位, 我们来品一下

  1. 首先是参数, 给的是值传递而不是引用传递, 这里就调用拷贝构造创建了临时对象tmp, 此时我们就已经完成了拷贝操作
  2. 然后直接一波交换, 把你的指针拿来, 此时我的指针就指向了我们需要的字符串, 赋值的任务完成. 临时变量tmp就带着原来对象的内容被销毁…
这钵直接窃取劳动果实...不可谓不狠啊....tmp老工具人了...呜呜心疼1秒钟

下面给出现代写法的完整代码:

//简单的实现.
class String {
public:
	//全缺省的构造函数
	String(const char* ptr = "")
		:_ptr(new char[strlen(ptr) + 1])
	{
		strcpy(_ptr, ptr);
	}

		//拷贝构造  现代写法
		String(const String& str)
			:_ptr(nullptr)  
		{
			String tmp(str._ptr);
			swap(_ptr, tmp._ptr);
		}


		//赋值运算符重载  现代写法
		String& operator=(String tmp) {
			swap(_ptr, tmp._ptr);
			return *this;
		}

	//析构
	~String() {
		if (_ptr) {
			delete[] _ptr;
			_ptr = nullptr;
		}
	}


private:
	char* _ptr;
};

这就是string类的简单实现了…下一次我们实现完整的string类~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殇&璃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值