C++---String的模拟实现

不是为了去造更好的轮子,只是为了能够更好的理解和使用,所以对于string内部的成员函数如何实现是非常值得学习的。

1. 实现简单的string

对于一个简单的string来说,就是实现他的默认成员函数,且成员变量值给一个char* _str
,这里还是用了一个命名空间,是因为害怕自己所写的和库里面自带的分不清楚,所以在调用的时候,也要把命名空间带上例如 wzy::string s("hello");

#include<iostream>
using namespace std;
namespace wzy
{
	class string
	{
	public:
		//这个是调用无参的对象
		//string(const char* str)
		//	:_str(new char[1])
		//{
		//	_str[0] = '\0';
		//}
		//①建议写一个全缺省的构造函数
		//②对于任意的一个字符串即使是空字符串""里面也有一个默认的‘\0’
		string(const char* str = "")
			:_str(new char[strlen(str)+1])
		{
			strcpy(_str, str);
		}

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

		//s3(s1)
		//拷贝构造
		string(const string& s)
			//对于拷贝构造函数来说,如果只是实现简单的浅拷贝,那么就会在析构的时候出错,所以需要深拷贝
			//什么是深拷贝呢:自己重新开辟一个空间,然后在指向他
			:_str(new char[strlen(s._str) + 1])
		{
			strcpy(_str, s._str);
		}

		//在写这个的时候要想清楚,你的operator[]到底想要什么
		char& operator[](size_t i)
		{
			return _str[i];
		}

		//如果只是简单的浅拷贝,也会出现程序崩的情况
		//赋值运算符(这两个对象都已经被创建出来了) s3 = s1
		//此时你的s3对象需要和你的s1对象一样大,但是我哪知道他们两个哪个大,所以我直接把s3的空间释放掉,然后和s1对象开一样大就好了
		//但是此时还需要考虑是否你自己会给自己赋值呢?
		//那么为什么会考虑这种情况呢:是因为你如果自己给自己赋值的话,代码一上来就已经把你的这段空间给释放了
		
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				delete[] _str;
				_str = new char[strlen(s._str) + 1];
				strcpy(_str, s._str);
			}
			return *this;
		}

		//返回C格式字符串
		const char* c_str()
		{
			return _str;//再次说明,对于成员变量来说,成员函数可以随意的调用
		}
	private:
		char* _str;
	};
};

2. 深浅拷贝

对于上述的拷贝构造函数和赋值运算符来说,如果只是使用简单的浅拷贝(默认生成的成员函数),那么在使用的时候就会报错。
在这里插入图片描述
拿s1去拷贝构造s3,但是如果只是使用浅拷贝也叫值拷贝,那么s3会得到s1的数据和地址,然后他们两个的char*指针都会指向同一块空间,此时修改s1的数据,s3的数据也会跟着修改,他们两个对象之间的独立性出现了问题。最致命的问题出现在析构函数的时候,先定义出来的后析构,此时你会去先析构s3,那么s3指向的空间将会被释放掉,然后你再去析构s1的时候,s1指向的空间已经没有了,然后就随机的释放了一块空间,然后就会报错。

所以这里引入了深拷贝,也就是我重新的给你的s3开一个和我s1一样大的空间,然后再把我s1上有的数据拷贝给你,此时他们两个之间就具有独立性了,提示:这里的空间都是在堆上所开辟的,然而对空间的地址都是自下向上逐渐变大的(但也不一定)
在这里插入图片描述

3. 实现一个完整的string

对于完整的string来说,就是要尽可能的模拟出string类里面常用到的成员函数

#include<iostream>
using namespace std;
namespace wzy

{
	class string
	{
	public:

		typedef char* iterator;
		typedef const char* const_iterator;

		string(const char* str = "");

		~string();

		string(const string& s);

		string& operator=(const string &s);

//
            // iterator

		iterator begin();

		iterator end();

		const_iterator begin()const;

		const_iterator end()const;

/
			// modify

		void push_back(char c);

		string& operator+=(char c);

		void append(const char* str);

		string& operator+=(const char* str);

		void clear();

		void swap(string& s);

		const char* c_str()const;



/
		// capacity

		size_t size()const;

		size_t capacity()const;

		bool empty()const;

		void resize(size_t n, char ch = '\0');

		void reserve(size_t n);

/
		// access

		char& operator[](size_t i);

		const char& operator[](size_t i)const;

/
		// 返回c在string中第一次出现的位置

		size_t find(char ch, size_t pos = 0);

		// 返回子串s在string中第一次出现的位置

		size_t find(const char* sub, size_t pos = 0);

		// 在pos位置上插入字符c/字符串str,并返回该字符的位置

		string& insert(size_t pos, char c);

		string& insert(size_t pos, const char* str);



		// 删除pos位置上的元素,并返回该元素的下一个位置

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

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

		static const size_t npos;
	}
	const size_t string::npos = -1;

	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);
};

①构造函数
你会发现,如果你使用初始化列表,那么就要多次的写strlen(str),对于内置类型其实写在内部更好,这里的成员变量_capacity表示“能存多少个效字符” ,其中’\0’是标识符,不能算在里面

提示:初始化列表的顺序并不是真的顺序,声明的顺序才是初始化列表的真实顺序

string(const char* str)
	{
		_size = strlen(str);
		_capacity = _size;//这里的容量代表“能存多少个效字符” ‘\0’是标识符
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

②析构函数

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

③拷贝构造
这里将会提出传统写法和现代写法的不同,当本质是不变的,就是使代码更加的简洁了,在一定的程度上其实可读性变差了。

传统写法
开和s一样大的空间,然后再把s中的数据拷贝过去

string(const string& s)
	:_str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}

现代写法
现代写法就像是一个资本家一样,活都不是它来干的,而是别人干完之后,他直接把别人的劳动成果拿过来的感觉。

wzy::string s3(s1)此时我创建了一个tmp的对象,然后让他去调s1的构造函数,帮tmp进行开一样大的数组空间,然后在把数据拷贝过去,此时这个tmp就是我s3想要的,那么就直接交换就好了(但是交换完以后,_str就是指向的tmp所开辟空间的位置,那么tmp就指向了原先_str的位置,所以这里必须要对开始的_str进行初始化,不然交换完以后tmp就指向了一个随机值,然后tmp出了作用域就会调用自身的析构函数,对随机值进行释放就会报错)

string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			string tmp(s._str);
			std::swap(_str, tmp._str);
			std::swap(_size, tmp._size);
			std::swap(_capacity, tmp._capacity);
		}

④operator=
赋值运算符和拷贝构造一样,如果只是使用简单的浅拷贝,也会出现报错的问题
对于传统写法来说是要考虑:

  1. 禁止自己给自己赋值
  2. 释放自己的原来的空间在和s开一样大的空间,再把空间的内容拷贝过来

传统写法

string& operator=(const string& s)
		{
			if (this != &s)
			{
				delete[] _str;
				_str = new char[strlen(s._str) + 1];
				strcpy(_str, s._str);
			}
			return *this;
		}

现代写法
s3 = s1这里厉害在是一个传值,不再是引用,对于对象s来说,本身就是调用了s1的拷贝构造函数,那么你的s就是我这里s3想要的,直接交换就好。并且出了作用域s自己会调用析构函数,进行释放(这里其实更加的狠,我不但咬你所创造出来的东西,并且我还把我不想要的东西甩给你)

string& operator=(string s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
			return *this;
		}

⑤operator[]
对于operator来说是有一个断言的,一旦越过了数组,就直接崩掉

char& operator[](size_t i)
		{
			assert(i < _size);//越界会直接崩掉
			return _str[i];
		}
const char& operator[](size_t i)const
		{
			assert(i < _size);//越界会直接崩掉
			return _str[i];
		}

⑥size()

size_t size()const
		{
			return _size;
		}

⑦reserve()
这里的n表示预留的有效字符的个数,但是一定要想要应该给’\0’留一个位置

void reserve(size_t n)
		{
			if (n > _capacity)
			{
				//char* tmp = realloc(_str, n);
				char* tmp = new char[n+1];//这里最好写为new,第一是匹配前面的new,第二就是对于new来说是不需要检查的,开辟失败,会抛异常
				strcpy(tmp, _str); 
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}

⑧resize()
既然有了reserve()那么就要实现一下resize(),但是这个接口是不常使用的

void resize(size_t n,char ch = '\0')
{
	//为了避免不停的因为预留空间缩小放大而申请释放空间,发生的抖动问题
	if (n < _capacity)
	{
		_str[n] = '\0';
		_size = n;
	}
	else
	{
		if (n > _capacity)
		{
			reserve(n);
		}

		for (size_t i = _size; i < n; ++i)
		{
			_str[i] = ch;
		}
		_str[n] = '\0';
		_size = n;
	}
}
在这里插入代码片

⑨push_back()

不管在任何时候都得记住,只要添加数据,就要考虑增容的问题.

void push_back(char ch)
{
	if (_size == _capacity)
	{
		//这里不要直接的扩二倍,要考虑长远一些,应为还需要实现一个reserve()接口
		reserve(2 * _capacity);
	}
	_str[_size] = ch;
	++_size;
	_str[_size] = '\0'; //如果不加这一句,那么你放入的字符就会覆盖掉\0,那么他就不知道字符串何时结束,就会崩了
}

⑩append()
相当于C语言的strcat,区别就是不用在考虑空间是否足够,因为他会自己增扩容

void append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	strcpy(_str + _size, str);
	_size += len;
}

⑪operator+=
但是一般情况下是不建议使用push_back()和append()的,建议直接使用‘+=’

string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}

string& operator+=(const char* str)
{
	append(str);
	return *this;
}

string& operator+=(const string& s)
{
	append(s._str);
	return *this;
}

⑫swap()成员函数
就会很奇怪,为什么已经有一个全局的swap()函数了,还需要自己在定义一个成员函数呢? swap(s1,s2); s1.swap(s2); 这两个哪一个更好一些?其实是作为成员函数来说更好,因为在调用成员函数的时候,只是把指向s1._str和s2._str的指针进行了一个交换,就完成了。但是对于你的swap(s1,s2)来说是一个函数模板,template < class T> void swap(T& a, T& b),对于这个模板会实例化出来具体的函数,需要使用到拷贝构造和赋值运算符,刚好这两个都是深拷贝,所以其实代价还是很大的

void swap(string& s)
		{
//			::swap(_str,s._str); 前面没有给的时候,表示去全局域找,当然这里给一个std更加清楚
			std::swap(_str,s._str); 
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

所以有了这个内部的成员函数swap(),其实还可以对拷贝构造operator=的现代写法进行一个简化,直接调用这个接口。

⑬迭代器—iterator
迭代器是像指针一样的类型,但不一定是指针,但是他们的“用法”大多都一样,我们所认为的迭代器非常的高大上,其实不然,就是一种很好的封装,上层隐藏了具体信息

namespace wzy  //对于同一个命名空间,里面的类是会合并的
{
	class string
	{
	public:
		//迭代器是像指针一样的类型,但不一定是指针,但是他们的“用法”大多都一样
		//我们所认为的迭代器非常的高大上,其实不然,就是一种很好的封装,上层隐藏了具体信息
		typedef char* iterator;//目前在string里面就可以简单的认为迭代器就是char*的指针
		typedef const char* const_iterator;

		iterator begin() 
		{
			return _str;
		}

		iterator end() //可以理解为你的end就是指向的‘\0’的位置
		{
			return _str + _size;
		}

		const_iterator begin()const
		{
			return _str;
		}

		const_iterator end()const //可以理解为你的end就是指向的‘\0’的位置
		{
			return _str + _size;
		}
private:
		//你会发现在windows下这里的成员变量还有一个char buff[16],其实这里就是使用了一种空间换时间的方式,如果你的字符串比较短
		//就会直接的存储在buff里面,而不用在进行下面的开空间步骤了,但是如果字符串比较长,那么你的这个buff空间就直接浪费掉
		char* _str;
		int _size;
		int _capacity;
	};
};

这里可以理解为你的begin()就是指向数组的起始位置,而你的end()指向_size的位置,也就是字符串的’\0’的位置

int main(0
{
	wzy::string::iterator it = s.begin();
	//auto it = s.begin(); 可以使用auto去推类型,但是你不知道他返回的具体类型,其实并不是很好
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

⑭范围for
对于范围for的底层其实就是使用迭代器去帮他遍历整个字符串

⑮insert()
实现了insert就可以对push_back进行改造了 intsert(_size,ch);

void insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(2 * _capacity);
	}
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		--end;
	}
	_str[pos] = ch;
	_size++;
}
void insert(size_t pos, const char* str)
{
	assert(pos <= size); //=pos的时候相当于尾插
	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(_str + pos, str, len);
	_size += len;
}

⑯earse()
如果这里不单独判断这个len可能就会发生溢出的问题,因为如果你的npos是-1,那么它转换为无符号的整形就已经是最大值了,然后你在加len,就有可能会溢出。

void earse(size_t pos, size_t len = npos)
{
	assert(pos < _size);
	if (len == npos || pos + len >= _size) //如果这里不单独判断这个len可能就会发生溢出的问题
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
}

⑰c_str()—以C格式返回字符串
给一个const,那么都可以调用这个const函数

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

⑱find
找到了就返回索引,没找到就返回-1

size_t find(char ch,size_t pos = 0) //因为只能从右往左缺省
{
	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 char* ret = strstr(_str + pos, sub);
	if (ret == nullptr)
	{
		return npos;
	}
	else
	{
		return ret - _str;
	}
}

⑲operator>和operator==
对于其他的都可以使用这两个接口来复用

bool operator>(const string& s1, const string& s2)
{
	//这里需要按照ASCII值去比较
	size_t i1 = 0, i2 = 0;
	while (i1 < s1.size() && i2 < s2.size())
	{
		if (s1[i1]> s2[i2])
		{
			return true;
		}
		else if (s1[i1] < s2[i2])
		{
			return false;
		}
		else
		{
			++i1;
			++i2;
		}
	}
	if (i1 < s1.size())
	{
		return true;
	}
	else if (i2 < s2.size())
	{
		return false;
	}
	else
	{
		//此时是相等的情况
		return false;
	}
}

bool operator==(const string& s1, const string& s2)
{
	//这里需要按照ASCII值去比较
	size_t i1 = 0, i2 = 0;
	while (i1 < s1.size() && i2 < s2.size())
	{
		if (s1[i1]> s2[i2])
		{
			return false;
		}
		else if (s1[i1] < s2[i2])
		{
			return false;
		}
		else
		{
			++i1;
			++i2;
		}
	}
	if (i1 == s1.size() && i2 == s2.size())
	{
		return true;
	}
	else
	{
		return false;
	}
}

⑳operator<< 和operator>>

ostream& operator<<(ostream& out, const string& s)
	{
		for (size_t i = 0; i < s.size(); ++i)
		{
			out << s[i];
		}
		return out;
	}

istream& operator>>(istream& in, string& s) //对于cin来说遇见换行和空格都会停止
{
	s.resize(0);
	char ch;
	while (1)
	{
		in.get(ch);
		if (ch == ' ' || ch == '\n')
		{
			break;
		}
		else
		{
			s += ch;
		}
	}
	return in;
}

operator>和operator==
只需实现这两个,剩下的几个都可以采用复用的方式

	bool operator>(const string& s1, const string& s2)
	{
		//这里需要按照ASCII值去比较
		size_t i1 = 0, i2 = 0;
		while (i1 < s1.size() && i2 < s2.size())
		{
			if (s1[i1]> s2[i2])
			{
				return true;
			}
			else if (s1[i1] < s2[i2])
			{
				return false;
			}
			else
			{
				++i1;
				++i2;
			}
		}
		if (i1 < s1.size())
		{
			return true;
		}
		else if (i2 < s2.size())
		{
			return false;
		}
		else
		{
			//此时是相等的情况
			return false;
		}
	}

	bool operator==(const string& s1, const string& s2)
	{
		//这里需要按照ASCII值去比较
		size_t i1 = 0, i2 = 0;
		while (i1 < s1.size() && i2 < s2.size())
		{
			if (s1[i1]> s2[i2])
			{
				return false;
			}
			else if (s1[i1] < s2[i2])
			{
				return false;
			}
			else
			{
				++i1;
				++i2;
			}
		}
		if (i1 == s1.size() && i2 == s2.size())
		{
			return true;
		}
		else
		{
			return false;
		}
	}
  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值