[C++]string(下)(模拟实现,引用计数)

我们在上半部分,简单的讲述了一下string类的具体用法,我们本节就从底层模拟实现string,能够更加清晰的学习string的底层。我会将模拟实现的代码放到文章最后。

目录

string成员变量

string的构造函数

无参构造函数

        带参构造函数

拷贝构造

赋值重载 

c_str 

string的析构函数

string中频繁调用的短小函数

clear

string中的迭代器实现

string中的其他函数

reserve

push_back

 append

operate+=

 insert

插入一个字符

 插入一个字符串

erase

​编辑

  find

查找一个字符

查找一个字符串 

substr

重载运算符

 流插入流输出

引用计数的写实拷贝


string成员变量

上一节,我们已经了解到string的底层就是一个字符数组,因此我们很容易定义出string的类成员

string的构造函数

我们先来实现无参的构造函数,利用初始化列表

无参构造函数

注意 ,这个无参的构造函数有潜在的风险,还需要进行改进,在下面的c_str函数我们会具体的讲

再实现带参的构造函数

带参构造函数

这里有一个问题,我们能按照下面的方式进行初始化吗?

		string(const char* str)
			:_size(strlen(str))
			,_str(new char[_size+1])
			,_capacity(_size)
		{}

如果你还没有发现问题,不妨再去看看string成员变量的顺序

我们在类和对象的章节讲过初始化列表,并不按照初始化列表的顺序进行构造,而是按照类变量的定义顺序进行构造的,像上面的代码就会先初始化_str,而此时_size是一个随机值,那么就完蛋了,因此这里不太适合用初始化列表,我们还是老老实实的正常写即可。 

拷贝构造

拷贝构造如果不手动实现的话,编译器会自动生成一份浅拷贝的版本,但是对于string类对象,浅拷贝显而是不够用的,很容易出现问题,因此,我们手动写一份深拷贝的版本。

拷贝构造的逻辑也并不复杂,如下所示

这种拷贝构造的实现是传统的写法,现在有现代的写法 

利用标准库中的swap函数

我们构造一个临时的对象,利用构造函数进行构造,再利用swap交换二者的数据,就像是掠夺资源一样,将其的数据全部转移到我身上,然后在函数结束后,会自动销毁临时对象,而自身的数据还是保存的。

同样的思路还可以使用在operate= 

赋值重载 

赋值操作,要首先将原有的空间释放掉,不然会导致内存泄漏

还要提前判断是否二者相等,不然自己给自己赋值,就会删掉原有的数据,直接就变成随机值了

现代写法

c_str 

c_str就是返回字符串的首地址,直到遇到 \0 才会终止

如果我们直接按照上面的写法,会有一个风险

如果我们创建了一个字符串对象,并用无参的构造函数进行构造,这样它的_str就是nullptr进行初始化的,如果用c_str()直接解引用的话,就直接导致程序崩溃。因此,我们可以改变一下我们的无参构造函数,将其的_str修改为带一个字符'\0'的字符串,_size和_capacity都不用修改

这里我们只需要传入缺省参数,就可以将带参和无参的构造函数合并

string的析构函数

在类里面定义的函数,默认是内联展开的,因此,我们可以将这种频繁调用且代码量小的函数,直接放到类里面来定义,而代码量比较大的函数则可以进行声明定义的分离。

string中频繁调用的短小函数

这些函数都是我们直接定义在类中的

我们可以看到operate[],我们模拟的和库里面的实现一样,定义了两种,一种是const类,另一种是正常的情况,并且是用断言来判断越界的情况的,然后直接返回访问元素的引用,可以进行直接的修改。

clear

只清掉数据,不清空间

string中的迭代器实现

我们看看目前我们实现的函数是否能用范围for进行遍历

 从这里我们就可以看到没有办法使用范围for,而原因就是我们没有实现迭代器,这也就再次间接的证明了,范围for的底层就是用迭代器来实现的。

这里我们要注意,实现迭代器的时候必须严格按照库里面的命名风格,不然范围for是走不过去的

迭代器的底层实现还是有些复杂的,但是在这里,我们可以用一种简单的方式对迭代器进行实现,复杂的迭代器,我们在之后的list部分再进行切入。

当然简单的实现,就避免不了它有所缺陷,不过目前够用。

我们直接用原生指针就能简单的实现迭代器,我们也可以将迭代器和库中的迭代器一致,实现一个const迭代器,一个正常的迭代器,如下图所示

反向迭代器,要复杂的多,要用适配器模式来实现,这里我们先不实现。

其实就是将char*进行了typedef

其实所有的iterate都是由typedef过来的,但是并不一定全是指针,还有一些是自定义类型的

比如 

list中的迭代器就是由自定义类型,typedef过来的 

迭代器的实现就体现了封装的特性

屏蔽了底层的实现细节,提供了统一的类似访问方式,不需要关心容器底层的结构和实现细节

string中的其他函数

下面的函数,我们都进行定义和声明的分离,将其在类内进行声明,在其他文件中进行定义。

reserve

我们传入参数n,如果大于_capacity,那么我们就开辟一段新的空间,注意是n+1,因为要给'\0'留一个空间,然后拷贝数据到新空间,再进行交换删除即可。不要忘记修改_capacity的值。

push_back

尾插元素,先检查空间是否需要扩容,然后再进行插入,不过一定要记得在插入元素的后面加上'\0',因为我们插入的新元素,占据了原来的'\0',如果不添加一个新的'\0',就会在打印时出现烫烫烫或者屯屯屯。

 append

实现append函数时,也是先检查容量,这里我们有两种实现思路,第一种就是需要多少扩容多少,还有一种方式,也就是我们实现的方法,对齐去实现,如果你需要的空间,为了避免频繁的扩容,大于_capacity的二倍,你要多少,我给多少,如果小于2倍,我就按2倍扩。

关于追加字符串部分

我们可以用C语言种的函数strcat,但是不推荐,因为strcat还要自己去找原有字符串str的'\0'在哪,然后再追加,太麻烦了,不如我们直接用strcpy,直接添加。

最后不要忘记修改_size的值

operate+=

 

两种实现方式,我们都直接调用我们写的push_back和append直接实现,十分的方便  

 insert

插入一个字符

在某个位置插入一个字符,意味着我们要挪动数据

首先用断言判断是否越界,然后判断是否需要扩容。

然后挪动数据,插入元素,最后修改_size即可

这里注意while()的循环条件,如果写成end>=pos ,对于大部分情况都可以通过,但是如果是头插的话,pos = 0,而end是size_t的类型,会导致死循环。

那有人可能会说,那我把end的类型修改成int不就好了吗?

通过调试我们发现,当end为-1,pos为0 居然还能进入这个循环,是编译器出错了吗?

实际上,这里是C语言中埋下的一个坑,当操作符两端是不同类型时,C语言会让其中一个隐式类型转换,end在比较的时候就悄悄的提升成了size_t类型。

我们可以再把pos改成int,也可以解决问题,但是库里面的pos就是size_t的类型,因为pos肯定不为负数。

所以我们还是老老实实的改变while的终止条件

 插入一个字符串

大体逻辑就是先挪动数据,腾开空间,再插入数据

注意边界情况

erase

由于缺省值,只能在声明中定义,在定义函数这里就没法写npos,大家知道即可,声明头为

		void erase(size_t pos, size_t len = npos);

删除操作,先判断是否越界,再判断len是否大于_size-pos,如果大于就说明将pos后的元素全部都删了,只需要将pos位置改为'\0',修改_size的值即可。

否则就把数据从前往后向前挪动即可,最后修改_size,注意是从前往后挪动,如果是从后往前挪动会导致数据覆盖

  find

find函数是给定一个字符或字符串,然后从pos位置向后找

这两个函数pos的缺省值都为0,声明为

	size_t find(char ch, size_t pos = 0);
	size_t find(const char* str, size_t pos = 0);
查找一个字符

 从pos位置依次寻找,找到就返回下标,找不到就返回npos

查找一个字符串 

匹配字符串有好多种写法,第一种就是暴力查找,第二种是KMP算法,第三种还有BF算法,后两种都比较复杂,这两个算法的效率都很高,有兴趣的可以自己去了解一下,还是有一定难度的。 

我们在这里就简单一点来实现,用一下C的strstr算法直接匹配过掉即可 

substr

 如果len大于_size-pos就更新一下有效字符

将len改成有效的长度

然后我们就创一个变量,开好空间,将从pos位置开始的len个字符全都放进去,直接返回即可

重载运算符

有这些接口

	bool operator<(const string& s1, const string& s2);
	bool operator<=(const string& s1, const string& s2);
	bool operator>(const string& s1, const string& s2);
	bool operator>=(const string& s1, const string& s2);
	bool operator==(const string& s1, const string& s2);
	bool operator!=(const string& s1, const string& s2);

	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& in, string& s);

 上面的六种重载比较符,我们只需要手动实现两个,其他的再复用这两个的逻辑就可以达到目的

如图所示,我们用C语言种的strcmp函数,实现了==和<

其他的运算符,我们就直接复用这两个的逻辑。

 流插入流输出

流输出只需要将字符一个一个的提取出来,再依次输出即可

流提取的实现,首先清掉字符串的数据,然后也是一个字符一个字符的输入读取,判断是否为空格和换行符,就终止循环,最后直接接到字符串后面即可。 

这里读取字符要用get方法,不然在缓冲区中会自动忽略掉空格和换行符,就导致没法正常的读取数据,而get方法恰好就能解决这个问题。

这里有一个优化,如果我们按照一个字符一个字符的提取,直接加到原有字符串的后面,如果我们输入了很长一段数据,那么就会不断的+=,也会不断的有异地扩容,再拷贝,效率十分的低。

因此,我们不如先开一个大一点字符数组buff,然后将读取的数据都放到数组里,当buff存满,就统一直接+=给原字符串s,如果还有数据,就再次走这个逻辑,大大提升了效率。

不仅如此,buff是一个临时变量,并不会长期占用内存,也能提升效率。

代码如图所示

流插入流提取并不需要全都设为友元函数,因为我们并不需要访问类的私有对象,我们用迭代器和重载[]就能达到目的。

引用计数的写实拷贝

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该
资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,
如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有
其他对象在使用该资源。

引用计数主要来解决两个问题,如图所示

这个引用计数写实拷贝的实际作用,要用博弈的思想来看待,如果我们只是拷贝,但是没有去修改,这里我们就只需要进行浅拷贝,节省空间。

如果要修改我们再进行深拷贝,也不影响什么。

博弈就在于,如果我们拷贝后,不修改,那就是浅拷贝,我们就赚了。

大致意思就算是这样,这里作了解即可。

模拟实现的代码

string.h

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace study 
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin() const
		{
			return _str;
		}

		const_iterator end() const
		{
			return _str + _size;
		}

		string(const char* str = "")
		{
			_size = strlen(str);
			// _capacity不包含\0
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
	
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}


		string& operator=(const string& s)
		{
			if (this != &s)
			{
				delete[] _str;

				_str = new char[s._capacity + 1];
				strcpy(_str, s._str);
				_size = s._size;
				_capacity = s._capacity;
			}

			return *this;
		}
		

		~string()
		{
			if (_str)
			{
				delete[] _str;
				_str = nullptr;
				_size = _capacity = 0;
			}
		}

		const char* c_str() const
		{
			return _str;
		}

		size_t size() const
		{
			return _size;
		}
		 
		size_t capacity() const
		{
			return _capacity;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);

			return _str[pos];
		}

		const char& operator[](size_t pos) const
		{
			assert(pos < _size);

			return _str[pos];
		}

		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		void reserve(size_t n);
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);
		string substr(size_t pos = 0, size_t len = npos);


		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		void erase(size_t pos, size_t len = npos);
		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);


	private:
		char* _str;
		size_t _size;
		size_t _capacity;

		const static size_t npos = -1;
	};
}

string.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include"string.h"
namespace study 
{
	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}

	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}

		_str[_size] = ch;
		++_size;
		_str[_size] = '\0';
	}

	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			// 大于2倍,需要多少开多少,小于2倍按2倍扩
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}

		strcpy(_str + _size, str);
		_size += len;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}
	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}

	void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);

		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}

		// 挪动数据
		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}

		_str[pos] = ch;
		++_size;
	}

	void string::insert(size_t pos, const char* s)
	{
		assert(pos <= _size);

		size_t len = strlen(s);
		if (_size + len > _capacity)
		{
			// 大于2倍,需要多少开多少,小于2倍按2倍扩
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}

		size_t end = _size + len;
		while (end > pos + len - 1)
		{
			_str[end] = _str[end - len];
			--end;
		}

		for (size_t i = 0; i < len; i++)
		{
			_str[pos + i] = s[i];
		}

		_size += len;
	}

	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);

		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			for (size_t i = pos + len; i <= _size; i++)
			{
				_str[i - len] = _str[i];
			}

			_size -= len;
		}
	}
	size_t string::find(char ch, size_t pos)
	{
		assert(pos < _size);

		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
			{
				return i;
			}
		}

		return npos;
	}

	size_t string::find(const char* str, size_t pos)
	{
		assert(pos < _size);

		const char* ptr = strstr(_str + pos, str);
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}

	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);

		// len大于剩余字符长度,更新一下len
		if (len > _size - pos)
		{
			len = _size - pos;
		}

		string sub;
		sub.reserve(len);
		for (size_t i = 0; i < len; i++)
		{
			sub += _str[pos + i];
		}

		return sub;
	}

	bool operator<(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) < 0;
	}

	bool operator<=(const string& s1, const string& s2)
	{
		return s1 < s2 || s1 == s2;
	}

	bool operator>(const string& s1, const string& s2)
	{
		return !(s1 <= s2);
	}

	bool operator>=(const string& s1, const string& s2)
	{
		return !(s1 < s2);
	}

	bool operator==(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}

	bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);
	}

	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		s.clear();

		const int N = 256;
		char buff[N];
		int i = 0;

		char ch;
		//in >> ch;
		ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;

				i = 0;
			}

			//in >> ch;
			ch = in.get();
		}

		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值