【C++】——string的模拟实现

前言:

在之前的学习中,我们已经对string类进行了简单的介绍,大家只要能够正常使用即可。但是在面试中,面试官总喜欢让学生自己 来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。因此,接下来我将带领大家手动模拟实现一下。


目录

(一)成员函数

1、构造函数

2、拷贝构造

3、赋值重载

4、析构函数

(二)容量

1、size()

2、capacity()

3、reserve()

4、resize()

5、clear()

(三)元素访问

1、 operator[]

(四)修改 

1、 operator+=

2、append()

3、push_back()

4、insert()

5、erase()

6、swap()

(五)字符串操作 

 1、c_str()

2、find()

(六)非成员函数重载

1、relational operators()

2、operator<<

3、operator>>

(七)代码汇总

(八)总结


(一)成员函数

1、构造函数

刚开始时,如果我们要实现构造函数,可能就需要分别实现带参的构造函数和无参的构造函数,但是有没有简单方法可以做到一步到位呢?

 💨  因此,为了更加的灵活方便,我们直接把带参的构造函数和无参构造函数集合,形成全缺省的构造函数,这样就省得再去写两个构造函数。

代码如下:

//全缺省的构造函数
//string(const char* str = nullptr)  //不可以,对其解引用如果遇到空指针就报错
//string(const char* str = '\0')      //类型不匹配,char 不能匹配为指针
//string(const char* str = "\0")      //可以
string(const char* str = "") 
	:_size(strlen(str))
{
	_capacity = _size == 0 ? 5 : _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

2、拷贝构造

编译器默认的实现的是浅拷贝,但是浅拷贝存在问题:

  • 如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

因此为了解决上述的问题,可以采用深拷贝解决浅拷贝问题:

  • 每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。

代码如下:

//深拷贝
// str3(str2)
string(const string& STR)
	:_size(STR._size)
	, _capacity(STR._capacity)
{
	_str = new char[STR._capacity + 1];
	strcpy(_str, STR._str);
}

3、赋值重载

注意:

  1. 当以拷贝的方式初始化一个对象时,会调用拷贝构造函数;
  2. 当给一个对象赋值时,会调用重载过的赋值运算符。

即使我们没有显式的重载赋值运算符,编译器也会以默认地方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量一一赋值给新对象,这和默认拷贝构造函数的功能类似。

代码如下:

string& operator=(const string& STR)
{
	if (this != &STR)
	{
		char* tmp = new char[STR._capacity + 1];
		strcpy(tmp, STR._str);
		delete[] _str;
		_str = tmp;

		_size = STR._size;
		_capacity = STR._capacity;
	}
	return *this;
}

4、析构函数

析构函数的实现就比较简单,只需将指针所指的空间进行释放并把置空即可(防止野指针)  ,最后把剩余的两个成员置为0即可。

代码如下:

       //析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

(二)容量

1、size()

顾名思义返回字符串的长度(以字符数为单位)

代码如下:

size_t size() const
{
	return _size;
}

2、capacity()

返回当前为basic_string分配的存储空间的大小,以字符表示。

代码如下:

size_t capacity() const
{
	return _capacity;
}

3、reserve()

表示请求更改容量,使字符串容量适应计划的大小更改为最多 n 个字符。

注意是有效字符,不包含标识字符,而在具体实现的时候,我们在底层多开一个空间给\0。

代码如下:

//扩容操作
void reserve(size_t N)
{
	if (N > _capacity)
	{
		char* tmp = new char[N + 1];
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;

		_capacity = N;
	}
}

4、resize()

其实它的情况大体上可以分为插入数据和删除数据两种情况。

  • 1.对于插入数据来说直接调用【reserve】提前预留好空间,然后搞一个for循环将字符ch尾插到数组里面去,最后再在数组末尾插入一个\0标识字符;
  • 2.对于删除数据就比较简单了,如果 n 小于当前字符串长度,则当前值将缩短为其第一个 n 个字符,删除第 n 个字符以外的字符然后重置一下_size的大小为n即可。

代码如下:


		//扩容+初始化
		void resize(size_t n, char STR = '\0')
		{
			if (n < _size)
			{
				// 删除数据--保留前n个
				_size = n;
				_str[_size] = '\0';
			}
			else if (n > _size)
			{
				if (n > _capacity)
				{
					reserve(n);
				}

				size_t end = _size;
				while (end < n)
				{
					_str[end] = STR;
					end++;
				}

				_size = n;
				_str[_size] = '\0';
			}
		}

5、clear()

顾名思义就是清除字符串,擦除basic_string的内容,该内容变为空字符串长度为 0 个字符)。

代码如下:

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

(三)元素访问

1、 operator[]

元素访问操作相对来说用的最多的就是operator[] ;

  1. 对它进行调用时可能进行的是写操作,也可能进行读操作,所以为了适应const和非const对象,operator[]应该实现两个版本的函数;
  2. 并且这个函数处理越界访问的态度就是assert直接断言,而at对于越界访问的态度是抛异常。

代码如下:

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

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

 


(四)修改 

1、 operator+=

追加到字符串,通过在当前值的末尾附加其他字符来扩展

在这里我们只实现添加字符和字符串的操作;

我们可以直接复用【push_back】的操作来实现。

代码如下:

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

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

 

2、append()

追加到字符串通过在当前值的末尾附加其他字符来扩展。
  1. 我们可以直接调用strcpy接口来进行字符串的尾插,但是需要注意一点,那就是【string】类的字符串函数是不会进行自动扩容的,所以我们需要判断一下是否需要进行扩容,在空间预留好的情况下进行字符串的尾插即可实现;
  2. 其次,如果已经实现了【insert】函数的情况下。我们可以直接复用【insert】函数也可实现对应的操作。
 

代码如下:

//追加字符串
void append(const char* STR)
{
	size_t len = strlen(STR);
	if (len + _size > _capacity)
	{
		reserve(_size + len);
	}
	strcpy(_str + _size, STR);
	_size += len;

	//insert(_size, STR);
}

3、push_back()

注意:

  1. 首先对于【push_back】有一个特别需要注意的地方就是当容量不够时的扩容操作。如果是一个空对象进行push_back的话,这时如果我们采取的二倍扩容就有问题,因为0*2还是0,所以对于空对象的情况我们应该给他一个初始的capacity值,所以上述构造函数的时候我给成了【5】,其他情况下进行二倍扩容即可;
  2. 其次,就是在尾插字符之后,要记得进行补【\0】操作,,否则在打印的时候就会有麻烦了。
  3. 最后跟【append】一样,如果已经实现了【insert】函数的情况下。我们可以直接复用【insert】函数也可实现对应的操作。

代码如下:

//尾插操作
void push_back(char STR)
{
	if (_size + 1 > _capacity)
	{
		reserve(_capacity * 2);
	}

	_str[_size] = STR;
	++_size;

	_str[_size] = '\0';

	//insert(_size, STR);
}

4、insert()

插入到字符串中,在 pos(或 p)指示的字符之前将其他字符插入

注意:

  1. 对于【insert】函数,有经常会引出错误的地方,那就是对于while循环里面的操作;
  2. 可能很多的小伙伴在while循环里面都是这样写的:_str[end + 1] = _str[end] ,那么这样写有没有问题呢?答案是会出问题的;
  3. 我们的end是size_t定义的,因为size_t是无符号数,那么-1会被认为是无符号整数,进行隐式类型转换,由于-1的补码是全1,此时就是恒大于0,程序会陷入死循环。所以我们可以不用size_t来定义end,防止发生隐式类型转换;
  4. 那么是不是只要把【size_t end = _size + len;】中的【end】用 int 定义就可以解决了呢?答案当然不是的 (是不是觉得很坑了呀!!!);
  5. 因为-1在和size_t定义的pos进行比较时,又会发生隐式类型转换。这是因为比较运算符也是运算符,只要进行运算就有可能出现隐式类型转换,因此此时又可能出现上述那样的情况,-1就又会被转为无符号整型,程序就又陷入死循环;
  6. 那么有没有解决方法呢?当然是有的,我们只需在比较时将【size_t】的pos强转为【int】类型,此时再去比较就没得问题了;
  7. 但当我们就想使用size_t类型,通过把【end-1】位置的元素挪到【end】位置上去,在while循环条件的判断位置,我们用end来和pos位置进行比较,end应该大于pos的位置,一旦end=pos我们就跳出循环,这样就可以了。

代码如下:

       //插入字符操作
		string& insert(size_t pos, char STR_1)
		{
			assert(pos < _size);
			if (_size + 1 > _capacity)
			{
				reserve(2 * _capacity);
			}

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

			_str[pos] = STR_1;
			++_size;

			return *this;
		}


		//插入字符串
		string& insert(size_t pos, const char* STR_2)
		{
			assert(pos < _size);
			size_t len = strlen(STR_2);
			if (_size + len > _capacity){
				reserve(_size + len);
			}

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

			// 拷贝插入
			strncpy(_str + pos, STR_2, len);
			_size += len;

			return *this;
		}

5、erase()

意思很简单,就是从字符串中删除字符

对于删除,思路很简单,分为两种情况下的删除:

  • 1.如果当前位置加上要删除的长度大于字符串的长度,即【 pos + len >= _size】,此时的意思即为删除pos之后的所有元素;
  • 2.除了上述情况,就是在字符串内正常删除操作。我们只需利用strcpy来进行,将pos+len之后的字符串直接覆盖到pos位置,这样实际上就完成了删除的工作。

注意:

  1. 对于【npos】这个参数,首先我们知道对于静态成员变量,它的规则是在类外定义,类里面声明,定义时不加static关键字
  2. 但如果静态成员变量有const修饰,这时它可以在类内直接进行定义,这样的特性只针对于整型,对于其他类型则是不适用的;
  3. npos就是const static修饰的成员变量,可以直接在类内进行定义。

代码如下:


		//删除操作
		string& erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}

			return *this;
		}

6、swap()

至于交换,这个就没有必要再多说什么了很简单,我相信大家肯定也会这个。

代码如下:

       //交换
		void swap(string& STR)
		{
			std::swap(_str, STR._str);
			std::swap(_capacity, STR._capacity);
			std::swap(_size, STR._size);
		}

 


(五)字符串操作 

 1、c_str()

获取等效的 C 字符串,返回指向一个数组的指针,该数组包含以 null 结尾的字符序列(即 C 字符串),表示basic_string对象的当前值
代码展示:
        const char* c_str()
		{
			return _str;
		}

2、find()

查找字符串中的第一个匹配项basic_string中搜索由其参数指定的序列的第一个匹配项。
对于这个函数不用多说,就是对其进行遍历查找即可。

代码展示:

        //查找
		size_t find(char STR, size_t pos = 0)
		{
			assert(pos < _size);

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

			return npos;
		}

		size_t find(const char* STR, size_t pos = 0)
		{
			assert(pos < _size);

			char* p = strstr(_str + pos, STR);
			if (p == nullptr)
			{
				return npos;
			}
			else
			{
				return p - _str;
			}
		}

 


(六)非成员函数重载

1、relational operators()

basic_string的关系运算符,以ascll码的方式比较大小

这个实现的过程,跟之前日期类的时间如出一辙,基本上都是一样的。

代码如下:

       //比较大小
		bool operator >(const string& STR) const
		{
			return strcmp(_str, STR._str) > 0;
		}

		bool operator == (const string & STR)const
		{
			return strcmp(_str, STR._str) == 0;
		}

		bool operator >= (const string & STR)const
		{
			return *this > STR || *this == STR;
		}

		bool operator <  (const string & STR)const
		{
			return !(*this >= STR);
		}

		bool operator <= (const string& STR)const
		{
			return !(*this > STR);
		}

		bool operator!=(const string& STR) const
		{
			return !(*this == STR);
		}

2、operator<<

将字符串插入流将符合 str 值的字符序列插入到 os 中。

代码如下:

	//operator<<
	ostream& operator<<(ostream& out, const string& STR)
	{
		for (auto e : STR)
		{
			out << e;
		}
		return out;
	}

3、operator>>

从流中提取字符串输入流中提取字符串,将序列存储在 str 中,该序列被覆盖(替换 str 的先前值)

注意:

  • 流提取是以空格和\n作为间隔标志的 ,而【getline】则是以【\0】就停止。

 代码如下:

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

		char ch = in.get();
        //如果输入到缓冲区里的字符串非常非常的长,那么+=就需要频繁的扩容,则效率就会降低
        //因此,在这里可以使用开辟一个数组,先将有效数据放入数组中,在进行操作,可有效提高效率
		char buff[128];   
		size_t i = 0;

		while (ch != ' ' && ch != '\0')
		{
			buff[i++] = ch;
			if (i == 127)      //最后得留一个位置给\0
			{
				buff[127] = '\0';
				STR += buff;
				i = 0;
			}

			ch = in.get();
		}
		if (i != 0)
		{
			buff[i] = '\0';
			STR += buff;
		}

		return in;

	}

 

(七)代码汇总

代码汇总如下:

        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 = nullptr)  //不可以,对其解引用如果遇到空指针就报错
		//string(const char* str = '\0')      //类型不匹配,char 不能匹配为指针
		//string(const char* str = "\0")      //可以
		string(const char* str = "") 
			:_size(strlen(str))
		{
			_capacity = _size == 0 ? 5 : _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		//深拷贝
		// str3(str2)
		string(const string& STR)
			:_size(STR._size)
			, _capacity(STR._capacity)
		{
			_str = new char[STR._capacity + 1];
			strcpy(_str, STR._str);
		}


	    //赋值操作	
		string& operator=(const string& STR)
		{
			if (this != &STR)
			{
				// str1 = str1 的情况不满足
				/*delete[] _str;
				_str = new char[s._capaicty + 1];
				strcpy(_str, s._str);
				_size = s._size;
				_capaicty = s._capaicty;*/

				char* tmp = new char[STR._capacity + 1];
				strcpy(tmp, STR._str);
				delete[] _str;
				_str = tmp;

				_size = STR._size;
				_capacity = STR._capacity;
			}
			return *this;
		}


		//析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}


		const char* c_str()
		{
			return _str;
		}

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

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

		size_t size() const
		{
			return _size;
		}

		size_t capacity() const
		{
			return _capacity;
		}
		//比较大小
		bool operator >(const string& STR) const
		{
			return strcmp(_str, STR._str) > 0;
		}

		bool operator == (const string & STR)const
		{
			return strcmp(_str, STR._str) == 0;
		}

		bool operator >= (const string & STR)const
		{
			return *this > STR || *this == STR;
		}

		bool operator <  (const string & STR)const
		{
			return !(*this >= STR);
		}

		bool operator <= (const string& STR)const
		{
			return !(*this > STR);
		}

		bool operator!=(const string& STR) const
		{
			return !(*this == STR);
		}

		//扩容+初始化
		void resize(size_t n, char STR = '\0')
		{
			if (n < _size)
			{
				// 删除数据--保留前n个
				_size = n;
				_str[_size] = '\0';
			}
			else if (n > _size)
			{
				if (n > _capacity)
				{
					reserve(n);
				}

				size_t end = _size;
				while (end < n)
				{
					_str[end] = STR;
					end++;
				}

				_size = n;
				_str[_size] = '\0';
			}
		}


		//扩容操作
		void reserve(size_t N)
		{
			if (N > _capacity)
			{
				char* tmp = new char[N + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;

				_capacity = N;
			}
		}

		//尾插操作
		void push_back(char STR)
		{
			if (_size + 1 > _capacity)
			{
				reserve(_capacity * 2);
			}

			_str[_size] = STR;
			++_size;

			_str[_size] = '\0';

			//insert(_size, STR);
		}


		//追加字符串
		void append(const char* STR)
		{
			size_t len = strlen(STR);
			if (len + _size > _capacity)
			{
				reserve(_size + len);
			}
			strcpy(_str + _size, STR);
			_size += len;

			//insert(_size, STR);
		}


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

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

		//插入字符操作
		string& insert(size_t pos, char STR_1)
		{
			assert(pos < _size);
			if (_size + 1 > _capacity)
			{
				reserve(2 * _capacity);
			}

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

			_str[pos] = STR_1;
			++_size;

			return *this;
		}


		//插入字符串
		string& insert(size_t pos, const char* STR_2)
		{
			assert(pos < _size);
			size_t len = strlen(STR_2);
			if (_size + len > _capacity){
				reserve(_size + len);
			}

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

			// 拷贝插入
			strncpy(_str + pos, STR_2, len);
			_size += len;

			return *this;
		}


		//删除操作
		string& erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}

			return *this;
		}


		//交换
		void swap(string& STR)
		{
			std::swap(_str, STR._str);
			std::swap(_capacity, STR._capacity);
			std::swap(_size, STR._size);
		}


		//查找
		size_t find(char STR, size_t pos = 0)
		{
			assert(pos < _size);

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

			return npos;
		}

		size_t find(const char* STR, size_t pos = 0)
		{
			assert(pos < _size);

			char* p = strstr(_str + pos, STR);
			if (p == nullptr)
			{
				return npos;
			}
			else
			{
				return p - _str;
			}
		}

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

    //operator<<
	ostream& operator<<(ostream& out, const string& STR)
	{
		for (auto e : STR)
		{
			out << e;
		}
		return out;
	}

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

		char ch = in.get();
		char buff[128];
		size_t i = 0;

		while (ch != ' ' && ch != '\0')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[127] = '\0';
				STR += buff;
				i = 0;
			}

			ch = in.get();
		}
		if (i != 0)
		{
			buff[i] = '\0';
			STR += buff;
		}

		return in;

	}


(八)总结

到此,关于string的模拟实现,在这里我们主要实现的是经常用得到的,对于其他的,我们并没有一一列举。如果后面有机会再给大家展示。

接下来,我们简单总结一下本文:

  1. 我们从文档的先后顺序入手,依次对各个板块的常用接口进行了模拟实现;
  2. 大家在上手操作的时候,一定要想明白为什么,做到真正的掌握string类它是非常重要的。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数

到此,便于string类的模拟实现便讲解完毕了。希望本文对大家有所帮助,感谢各位的观看!!!

 

  • 41
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 68
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 68
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

起飞的风筝

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

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

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

打赏作者

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

抵扣说明:

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

余额充值