string类模拟实现

经过诸多等待,我们也是终于来到了string类的模拟实现环节,接下来就跟着我一起学习,了解string的底层实现吧!
由于我们水平并未那么高,所以我们并不会去实现basic_string,那样的话会有很多问题无法理解,毕竟我们只是了解,而不是创建一个更加好的

string.h

基本构造

//创建命名空间bit是为了隔绝std,避免命名冲突,并且封装性更好
namespace bit {
	class string 
	{
	public :
		string()//无参构造函数
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{}

		string(const char* str)//带参构造函数
			:_str(new char[strlen(str) + 1])
			, _size(strlen(str))
			, _capacity(strlen(str))
		{
			strcpy(_str, str);
		}

		//拷贝构造(深拷贝)
		//传统写法,s1和s2的空间不在同一个地方
		//就相当于我们自己做一碗红烧牛肉面
		string(const string& s) {
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

	private:
		char* _str;//字符串首字符地址
		size_t _size;//有效字符个数(不包含\0)
		size_t _capacity;//空间大小,需要比size至少多开一个,因为要容纳\0,但是capacity理论上与size相等
	};
}

以上的带参构造有很大的问题,大家知道为什么吗?
因为中间strlen调用了三次,而要用strlen获得字符串长度,每次都需要遍历字符串,这是极其浪费效率的;有些同学会耍小聪明,把capacity的strlen改成_size,但是这里是因为我们的定义顺序和初始化顺序刚好一样,假如我们把size和capacity的位置互换,因为初始化顺序就是类中成员变量定义的顺序,那么就会出现这个时候明明size还没有初始化,把一个随机数给了capacity初始化,这就也会出现错误,所以,为了避免这些问题,我们如下所示:

		//string()
		//	:_str(nullptr)
		//	,_size(0)
		//	,_capacity(0)
		//{}

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

我们一般建议全缺省,这样的话无论不放参数还是放参数都可以使用,比我们先前再创建一个无参数构造好;而且我们会发现,我们注释掉的无参数和全缺省参数是不一样的,一个_str=nullptr,一个则是"“,这又是为什么呢?nullptr不仅会让strlen无用,在后续返回c_str的时候也会出现问题(因为c_str返回的就是_str,是指向字符串的指针,而nullptr是无法使用的),而且我们这里为什么给的是个空串,而不是”\0"呢,这是因为const char* str常量字符串本来就是有"\0"在最后面的,如果我们给的不是空串,就重复填入了一个\0,这样的话是无意义的,所以给一个空串是刚刚好的
如果不是全缺省可以这么写:
在这里插入图片描述
但这只是一个参考,我们一般的官方写法都是全缺省

析构

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

c_str()、size()、capacity()

		const char* c_str() {
			return _str;
		}
		
		size_t size() const{
			return _size;
		}
		//因为其不会改变成员变量,所以后面加上const
		size_t capacity() const{
			return _capacity;
		}

operator[]

		//大家可能会觉得我们一直调用这个函数会造成负担,但实际上我们知道类里面调用的函数就是内联函数
		//而内联函数可以提升函数使用的效率(inline)
			char& operator[](size_t pos) {
			assert(pos < _size);//因为pos本来就大于0,所以只需要判断其小于size
			//这里用assert断言可以避免数组越界的问题
			//因为C语言的数组查越界只是抽查
			//就像我们查酒驾,你不一定会查到,但是如果车上设置一个程序,直接探测坐驾驶位的人有没有喝酒,就可以完全避免

			return _str[pos];//这里可以用_str[pos]的值是因为其是在堆上开辟的空间,不会在这里被销毁
			//返回引用的话可以提高效率,不怕多次创建对象
		}

		//可读不可写(重载)
		const char& operator[](size_t pos) const{
			assert(pos < _size);
			return _str[pos];
		}

迭代器(iterator)和范围for

		typedef char* iterator;//这里我们直接用指针版本

		iterator begin() {
			return _str;
		}

		iterator end() {
			return _str + _size;
		}

我们会发现写了iterator之后,我们直接按照规范写范围for便可以直接运行,这是因为范围for的底层相当于就是迭代器,只是进行了一个替换,但是需要注意的是:迭代器的名字一定要叫iterator,不然的话,范围for就无法成功替换

这里补充一些东西:我们知道每个容器都有各自的迭代器,它们都有统一的名字叫做iterator,那么为什么各种iterator不会冲突呢?一个是因为typedef将所有的迭代器都命名为iterator,还有一个则是因为我们并未定义在全局域中,而是定义在每个类域中,在使用时在前面包含类域名即可。

范围for是从python拿过来的(以前C++有个for_each是想达成这种目的,但是没范围for好用)

		typedef const char* const_iterator;//也需要有const版本的iterator

		const_iterator begin() const{
			return _str;
		}

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

这里也别忘了还有const类型的iterator

增删查改

push_back与append

		void push_back(char ch) {
			//扩容2倍
			if (_size == _capacity) {
				reserve(_capacity == 0 ? 4 : 2 * _capacity);//防止一开始capacity是0的情况
			}

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

		void append(const char* str) {
			//append是不能扩容2倍的,不知道字符串多长
			size_t len = strlen(str);
			if (_size + len > _capacity) {
				reserve(_size + len);
			}

			strcpy(_str + _size, str);//_str+_size就是末尾的地方,把str塞到后面
			_size += len;//有效个数要加上
		}

operator+=

		//这个才是用的最多的
		string& operator+=(char ch) {
			push_back(ch);
			return *this;
		}

		//重载一下
		string& operator+=(const char* str) {
			append(str);
			return *this;
		}

insert和erase

		void insert(size_t pos,char ch) {
			assert(pos <= _size);//==就是尾插

			if (_size == _capacity) {
				reserve(_capacity == 0 ? 4 : 2 * _capacity);//防止一开始capacity是0的情况
			}

			int end = _size;
			while (end >= (int)pos) {
				_str[end + 1] = _str[end];
				end--;
			}
			_str[pos] = ch;
			++_size;
		}
//其实以上写法是经过改良之后的写法,我们一开始会把int都改成size_t
//而这样的话,当end==-1(头插)的时候就会因为size_t是>0的数从而导致end的值反而变为最大的正数
//而如果我们只把end改为int也不可以,因为那样的话会发生整型提升
//两边操作数类型不同会发生类型提升,一般是范围小的向范围大的提升
//这里则是有符号向无符号提升

//另一种更简单的方法
		void insert(size_t pos, char ch) {
			assert(pos <= _size);//==就是尾插

			if (_size == _capacity) {
				reserve(_capacity == 0 ? 4 : 2 * _capacity);//防止一开始capacity是0的情况
			}

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

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

			size_t len = strlen(str);
			if (_size + len > _capacity) {
				reserve(_size + len);
			}

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

			//这里用strncpy的原因在于,如果我们直接用strcpy(_str+pos,str)
			//就会出现我们用范围for输出字符串和c_str()的结果不一样
			//因为strcpy会拷贝源字符串直到遇到空字符,并且包括空字符在内。
			//这自然导致了我们c_str()遇到\0就停止输出,于是原本后面还有字符串的内容,却无法被输出了
			//且一个字符串中含有多个\0也是一个问题
			strncpy(_str + pos, str, len);
			_size += len;
		}

		void erase(size_t pos, size_t len = npos) {
			assert(pos < _size);//不可能删除\0

			//如果是pos+len>=_size就会有溢出风险,左边数字容易过大
			if (len == npos || len >= _size - pos) {
				_str[pos] = '\0';
				_size = pos;
			}

			else {
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
		}

写完insert之后,我们还可以更改append和push_back使其更简单

在这里插入图片描述

其他成员函数

resize

		//直接把官方的合二为一,变为半缺省
		void resize(size_t n, char ch = '\0') {
			if (n <= _size) {
				_str[n] = '\0';
				_size = n;
			}

			else {
				reserve(n);//不管大还是小,最好都reserve一下
				for (size_t i = _size; i < n; i++) {
					_str[i] = ch;
				}
				_str[n] = '\0';
				_size = n;
			}
		}

swap

在这里插入图片描述

由于库里的swap使用代价太大,所以我们建议自己造一个
并且库里面的swap的话,原来的旧空间甚至都还会变化,并不只是两个交换(深拷贝的原因,要创建新空间)

		void swap(string& s) {
			//加上std是因为我们指定了std里面找,如果这里不进std的话,就会直接进bit找,会说参数类型不匹配
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

find

		//找一个字符
		size_t find(char ch, size_t pos = 0) const {
			assert(pos < _size);
			for (size_t i = pos; i < _size; i++) {
				if (_str[i] == ch) {
					return i;
				}
			}
			return npos;
		}

		//找一个子串
		size_t find(const char* sub, size_t pos = 0) const {
			assert(pos < _size);
			const char* p = strstr(_str, sub);//每次默认从头开始搜索,暴力匹配
			if (p) {
				return p - _str;
			}

			else {
				return npos;
			}
		}

substr

		string substr(size_t pos = 0, size_t len = npos) const{
			string sub;
			if (len == npos || len >= _size - pos) {
				for (size_t i = pos; i <= _size; i++) {
					sub += _str[i];
				}
			}

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

			return sub;//sub的拷贝返回后sub本体即被销毁,所以返回string
		}

relational operators

	//重载成全局的原因主要还是因为要让常量字符串和string作比较(不管两个的传入参数顺序如何)
	//我们不用像string里面写三个函数,因为我们传入的常量字符串会发生隐式类型转换
	//就比如string s1="hello world",就是编译器会创建一个 string 对象,并使用字符串字面值的内容初始化该对象(拷贝构造的感觉)
	bool operator==(const string& str1, const string& str2) {
		int ret = strcmp(str1.c_str(), str2.c_str());
		return ret == 0;
	}
	
	bool operator<(const string& str1, const string& str2) {
		int ret = strcmp(str1.c_str(), str2.c_str());
		return ret < 0;
	}

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

	bool operator<=(const string& str1, const string& str2) {
		return str1 == str2 || str1 < str2;
	}

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

	bool operator!=(const string& str1, const string& str2) {
		return !(str1 == str2);//前面的代码可以复用的
	}

赋值

		//相当于我自己买东西做红烧牛肉面,做完了还要自己洗锅
		string& operator=(const string& s) {
			char* tmp = new char[s._capacity + 1];
			strcpy(tmp, s._str);
			delete[] _str;
			_str = tmp;
			_size = s._size;
			_capacity = s._capacity;

			return *this;
		}

流插入和流提取

提供流插入和流提取的原因很简单,我们在C++的库里已经为内置类型提供了所有的输出函数,但是自定义类型是无法输出的,C语言的printf输出时需要有%d这样的格式控制字符,但是不可能每个自定义类型都有一个格式控制字符,所以创造了C++的流插入,可以自己定义

	//流插入
	//为了支持连续输出,所以必须返回osteam&对象类型
	//为什么现在我们不需要像日期类一样使用友元,因为我们不再直接使用私密成员变量
	ostream& operator<<(ostream& out, const string& s) {
		for (auto ch : s) {
			out << ch;
		}

		return out;
	}

	//流提取
	istream& operator>>(istream& in, string& s){
		s.clear();//这里是为了清除原本字符串里的内容,因为cin是覆盖内容
		char ch;
		//in >> ch;
		//这里我们不能用in>>ch,不然的话会死循环
		//因为拿不到空格和回车,只会认为是值的分割
		//C语言我们会用getchar(),而C++用in.get()方法
		ch = in.get();
		s.reserve(128);//这个很重要,不然运行会说堆出错
		while (ch != '\n' && ch != ' ')
		{
			s += ch;
			ch = in.get();
		}

		return in;
	}

但其实我们这个也还没有完全正确,我们直接用reverse(128),如果我们输入的少,那么就会浪费一大块堆上的空间,而且是无法被释放的;但是如果我们用下列的方法,你就会发现十分巧妙

std::istream& operator>>(std::istream& in, string& s)
	{
		s.clear();
		char ch;
		//in >> ch;
		ch = in.get();
		char buff[128];
		size_t i = 0;
		while (ch != '\n' && ch != ' ')
		{
			buff[i++] = ch;
			if (i == 127) {
				buff[127] = '\0';
				s += buff;
				i = 0;
			}

			ch = in.get();

			//如果没有等于127,就会只有以下代码
			//buff[i++]=ch;
			//ch = in.get();
			//就只是往数组里面塞输入的内容,然后继续输入
			//如果i超过127,那么又会重新计数,并把先前的内容塞进字符串
			//扩容不频繁,很好
		}

		if (i > 0) {
			buff[i] = '\0';
			s += buff;
		}
		//如果i没有超过128,那么到后面就是在数组的最后一个位置塞\0
		//然后最后把所有的数组内容插入进去字符串

		return in;
	}

getline(与>>类似)

	std::istream& getline(std::istream& in, string& s)
	{
		s.clear();

		char ch;
		//in >> ch;
		ch = in.get();
		char buff[128];
		size_t i = 0;
		while (ch != '\n')
		{
			buff[i++] = ch;
			// [0,126]
			if (i == 127)
			{
				buff[127] = '\0';
				s += buff;
				i = 0;
			}

			ch = in.get();
		}

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

		return in;
	}

赋值和拷贝构造现代写法

		//赋值现代写法
		//跟拷贝构造差不多
		//这里就相当于我让你给我做红烧牛肉面,做完了之后厨房还留给你收
		//因为tmp是局部对象
		string& operator=(const string& s) {
			string tmp(s);
			swap(tmp);
			return *this;
		}
		//拷贝构造(现代写法)
		//我们还要把this指针指向的字符串初始化一下
		//不然会是随机数(_str=nullptr;_size=0;_capacity=0)
		//相当于找个别人做红烧牛肉面(跟上面传统方式相比)
		//假他人之手
		string(const string& s) {
			string tmp(s._str);//用的是上面带有缺省值的构造(只能调用构造)
			swap(tmp);
		}
		//拷贝构造函数通过创建一个临时字符串对象,并将其内容与原始字符串对象的内容进行交换
		//从而实现了拷贝构造的目的。通过使用 swap,可以避免不必要的内存分配和释放,提高代码性能和效率。

这里可以用图看一下:
在这里插入图片描述
(拷贝构造)上面是传统,下面是现代
我们一开始初始化s2的原因在于,如果在析构函数中访问已经释放的或未初始化的内存,就可能导致出现问题,可能表现为读取随机值、崩溃或其他不可预测的行为。

需要注意的是,这里没有效率高低的说法,传统写法开空间拷贝数据,现代写法也开空间拷贝数据,两者只是方法不同,结果都一样

更简单的赋值写法

		//本质跟上面没什么区别
		//去掉const,变成传值传参,调用拷贝构造
		//要记住拷贝构造不传值的原因是因为拷贝构造调拷贝构造会死循环
		string& operator=(string s) {
			swap(s);
			return *this;
		}

其实这里我们总结一下我们为什么要写现代写法,究其原因在于其是可以复用的,如果是传统写法,那么每次写的方法都是不一样的,并且需要我们琢磨其过程,是没有现代写法方便效率的

这里还做一个补充,我们发现用sizeof(string)的变量的时候,其大小是28,但明明我们按照成员变量来算的话,是只有12的,这又是为什么呢?我们观察底层,发现其实string的底层是这样的
在这里插入图片描述
这个buff数组,占据了16个字节的空间,其作用是什么呢?是我们的字符串如果小于16,就会存进buff里面;如果超过16,就会存进_str里面,但是buff存入的数据不会销毁,是一种以空间换时间的方法。因为我们的string类其实一般存入的字符都比较小,如果在堆上开空间的话,效率是没有栈上好的,所以在字符串较小的情况下,我们可以选择存入buff,提高效率;而超过16,也不会有任何影响

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值