string的模拟实现

前言

为了加深对string类的理解,本节我们来学习一下string类的模拟实现,那么废话不多说,我们正式进入今天的学习。

(模拟实现的所有函数我都做了声明和定义分离,但其实string的构造等一些短小而频繁调用的函数其实可以不做声明和定义分离,可以直接在类中定义)

我们通过之前的学习知道,string的底层的核心是这三个私有成员变量(代码写在string.h中):

	class string
	{
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

1.实现无参数的string功能

(该代码可不做声明和定义分离)

我们知道,当我们不给string中传入参数时,string就会构造一个空的字符串,所以我们可以很轻松的写出如下的代码:

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

我们在这种情况下采用初始化列表让_size和_capacity变量为0,_str指向空指针

此时代码其实存在一点问题,假设我们在测试的时候想要打印字符串,因为没有重载流插入和流提取,所以我们先来模拟实现一下 c_str 用于打印字符串:

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

再来写一下测试代码:

	void test_string1()
	{
		string s1;
		string s2("hello world");
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
	}

我们运行代码 的时候发现打印打不出来,程序崩溃了

这是为什么呢?这是因为s1是一个空的string对象,它是用空指针来初始化的。当打印s1的时候它会去解引用空指针,此时就导致了错误,所以这就说明我们写的不含参数的string的代码是错误的,我们应该要改正一下代码,不能直接给空指针,此时也应该开辟一个字节的空间,里面给 "\0"

(代码写在string.h中)

	string();

(代码写在string.cpp中)

	string::string()
		:_str(new char[1]{'\0'})
		, _size(0)
		, _capacity(0)
	{}

此时再来运行一下测试代码: 

可以发现,更改完代码以后就能成功打印了


2.实现参数仅为字符指针时的string功能

(该代码可不做声明和定义分离)

当给string中传入参数时,并且参数仅为指向一个字符串的字符指针时,string类就会拷贝字符指针指向的字符串来初始化

这里我们用初始化列表来写就会有点不合理,理由如下:

当我们用初始化列表写的时候,我们要给_str来new一块新的空间用于存储拷贝的字符串

			:_str(new char[strlen(str)+1])

我们此时开辟的空间的大小应该是str指向的字符串的大小+1,+1是因为要给 " \0 " 留空间。在_capacity和_size的初始化中我们还需要使用strlen:

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

连续使用三次strlen会比较麻烦,我们可能会想到如下的处理方式:

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

我们会想到:先初始化_size,再用_size来初始化其他的成员变量。但这样做其实是不可行的,我们在学习初始化列表的时候提到了:初始化列表的顺序并不是变量出现的顺序,而是变量声明的顺序。所以此时先初始化的变量是_str,而此时的_size是一个随机值。

基于这些存在的隐患,我们在处理这一类代码的时候就最好不用初始化列表去初始化

(代码写在string.h中):

	string(const char* str);

(代码写在string.cpp中):

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

 需要注意的一点是:_capacity中不包含 " \0 " 所以不需要+1

**************************************************************************************************************

我们其实可以把1和2中的string用全缺省函数来合并

(代码写在string.h中):

	string(const char* str = " ");

(代码写在string.cpp中):

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

注意:常量字符串后面默认有 " \0 " ,所以这里不要自己写 " \0 "

**************************************************************************************************************


3.实现string的析构

(该代码可不做声明和定义分离)

这个代码非常的基础,所以不做解释了

(代码写在string.h中):

	~string();

(代码写在string.cpp中):

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

4.实现size、capacity函数

(该代码可不做声明和定义分离)

这个代码非常的基础,所以不做解释了

(代码写在string.h中):

	size_t size() const;
	size_t capacity() const;

(代码写在string.cpp中):

	size_t string::size() const
	{
		return _size;
	}

	size_t string::capacity() const
	{
		return _size;
	}

5.实现operator[]重载

(该代码可不做声明和定义分离)

我们首先应该要加上断言,必须保证pos小于_size,防止发生越界访问的问题:

		assert(pos < _size);

它还要提供一个const的版本,给const对象使用。

根据这些,我们可以写出代码如下

(代码写在string.h中)

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

(代码写在string.cpp中):

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

 我们来测试一下(代码写在string.cpp中):

	void test_string2()
	{
		string s1("hello world");
		for (size_t i = 0; i < s1.size(); i++)
		{
			s1[i] += 1;
		}
		cout << s1.c_str() << endl;
	}

6.实现迭代器、begin函数、end函数

那么同样的条件下,我们能不能使用范围for呢?

	void test_string2()
	{
		string s1("hello world");
		for (size_t i = 0; i < s1.size(); i++)
		{
			s1[i] += 1;
		}
		cout << s1.c_str() << endl;
		for (auto ch : s2)
		{
			cout << ch << " ";
		}
	}

我们可以发现它报错了。因为范围for的底层就是迭代器,那么我们该怎样简单的实现迭代器呢?

其实很简单(代码写在string.h中):

		typedef char* iterator;

因为迭代器模拟的是指针的行为,所以我们可以这样写代码。iterator在底层其实全部都是typedef过来的,帮助我们获取了一个统一的名字,但是其实这些类型不全是指针,还有可能是自定义类型。我们这里只是把迭代器代码简化了,真正的迭代器代码不是这样实现的。我们之所以能用原生的指针来实现string类中的迭代器,是因为string类的结构有优势,因为他是一个数组。本质是数组的都可以用原生指针来做迭代器(eg:链表不是一个数组,如果我们以原生指针作为迭代器,也就是节点作为迭代器,此时迭代器++无法走到下一个节点,这样就会出现问题)

迭代器的设计其实是一种封装的体现,因为迭代器的底层可能是数组、链表甚至是一串数。迭代器屏蔽了底层实现的细节,都提供了统一的方法去访问容器,不需要使用者关心底层结构和实现细节

接着再来实现一下begin和end函数:

		iterator begin() const
		{
			return _str;
		}
		iterator end() const
		{
			return _str + _size;
		}

当加上迭代器、begin、end的代码,此时范围for的程序就可以正常运行了(范围for在替换的时候,函数名必须和库中的函数名一致,例如begin函数不能写成Begin)

(反向迭代器暂时不做讲解,需要用到适配器)

7.实现push_back、reserve、append函数、重载+=

要实现push_back,首先需要先扩容,我们采取扩2倍的形式扩容,所以此时应该先来模拟实现reserve函数

因为reserve函数是只扩容不缩容的,所以首先就应该判断n是否大于_capacity

还要注意我们要多开一个字节的空间留给 " \0 "

	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}

接下来来实现push_bacak函数:

首先需要进行判断,如果_size等于_capacity,此时就说明空间已经满了,需要进行扩容。我们调用reserve来完成扩容2倍。

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

如果像上述代码那样写是错误的,因为_capacity有可能是0,所以要使用三目操作符判断一下_capacity并且修改:

扩完容了以后就很简单了:

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

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

实现完push_back以后,重载运算符+=也很容易了,先来解决参数是字符的+=:

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

我们来测试一下+=的代码:

	void test_string3()
	{
		string s("hello wor");
		s += 'l';
		s += 'd';
		cout << s.c_str() << endl;
	}

可以看到后面出现了乱码,因为我们此时没有去处理"\0",我们现在需要修改一下push_back的代码,加入"\0":

	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size] = ch;
		++_size;
		_str[_size] = '\0';
	}

此时代码就没有任何的错误了

接下来我们来实现一下append函数:

append函数此时肯定不能继续采用二倍扩容的形式,我们来举一个例子:假设原字符串是"hello world",我们需要插入一个很长很长的字符串,当插入的字符串长度加原字符串长度大于二倍扩容的空间时,就会出现问题。所以必须要根据需求扩容:

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

然后我们就可以使用strcpy拷贝字符串:

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

现在有了append函数,我们就可以完善+=的重载:

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

8.模拟实现insert、erase函数、npos

我们先来实现insert插入一个字符:

假设想要在某个位置插入一个字符,首先就必须要保证pos要小于等于_size(等于的时候相当于尾插),然后要把插入位置后面的所有字符向后挪动一位,最后在实现插入。

要注意的一点是,当空间不够的时候,我们还需要实现扩容

根据上面几点,我们可以写出代码如下:

	void string::insert(size_t pos, char ch)
	{
		assert(pos < _size);
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		//挪动数据
		size_t end = _size;
		while (end >= pos)
		{
			_str[end + 1] = _str[end];
			--end;
		}
		_str[pos] = ch;
		++_size;
	}

其实这样的写法存在一个bug,我们来测试一下:

	void test_string4()
	{
		string s1("hello world");
		s1.insert(5, '&');
		cout << s1.c_str() << endl;
	}

当我们在第五个数据的位置插入&时没有出现错误,此时我们再来看在边界位置处的情况:

	void test_string4()
	{
		string s1("hello world");
		s1.insert(5, '&');
		cout << s1.c_str() << endl;
		s1.insert(0, '&');
		cout << s1.c_str() << endl;
	}

此时程序崩溃了,我们来分析一下程序为什么会崩溃:

我们来看一下insert代码中的这一段:

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

因为我们给pos的值是0,所以循环结束的条件是end >= 0,而end的数据类型是size_t,是不可能会小于0的,此时它再执行--操作就会得到无符号整型的最大值

这里我们可能会想到把end的类型改成int来解决问题,实际上这样也是不行的,这是C语言留下的一个问题,为了更直观的看到变化,我们来调试一下:

**************************************************************************************************************

这里来讲解一个调试的小技巧,当调试的数据很大的时候,我们一次一次的按f10或者f11就会很麻烦,而且容易按错,我们可以这样去做来观察最后一两次的情况:

		while (end >= pos)
		{
			if (end == 0)
			{
				//..
			}
			_str[end + 1] = _str[end];
			--end;
		}

...里面的内容随便写什么都可以,目的是为了让它到条件的时候停下来

**************************************************************************************************************

可以看到,end都为-1了,而停下来的条件是end<0,但是此时程序并没有停止,这是为什么呢?

这就要涉及到我们之前在C语言阶段学习过的一个知识:当操作符两边的操作数类型不相同的时候,编译器会自动进行类型的转换,而对于这种大于和小于的比较时,通常会把范围小的变量向范围大的变量进行转换,所以这里end变量就会自动转换成pos变量的类型,而pos变量的类型是size_t,所以此时即使end小于0了,程序仍然没有停止。

要想解决这个问题,第一种办法就是把参数里面的pos类型也改成int,此时就不存在类型转换的问题了。

但是如果这样解决问题的话本身就不太合理。首先库中的pos变量的类型也是size_t,其次pos变量表示一个数据的位置,而位置不可能是负数,所以这种方法可以先排除。

第二种方法就是在while循环条件中把pos强制转换成int类型

		while (end >= (int)pos)

此时重新运行代码,发现就可以正常运行了:

第三种方法:可以不用下标进行访问,用指针进行访问,因为指针不可能为空,这种方法不作详细的讲解了

第四种方法:我们还是让pos和end的数据类型都为size_t,我们把挪动数据的代码改成把end-1处的数据挪到end处

			_str[end] = _str[end-1];

我们接下来实现在pos位置处插入字符串:

首先还是需要先判断pos位置有没有越界

		assert(pos <= _size);

接着我们就需要判断是否需要扩容,这里因为字符串的大小是不确定的,不能直接进行简单的二倍扩容,具体要扩容多少要看添加的字符串的长度。大于2倍,需要多少空间就扩容到多少空间;小于2倍就扩容2倍。(这里的代码细节就不讲解了,和之前的append的扩容模式一模一样

接下来的步骤就是挪动数据+插入数据

	void string::insert(size_t pos, const char* s)
	{
		assert(pos <= _size);
		size_t len = strlen(s);
		if (_size + len > _capacity)
		{
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}
		size_t end = _size + len;
		while ()
		{
			_str[end - len] = _str[end];
		}
		for (size_t i = 0; i < len; i++)
		{
			_str[pos + i] = s[i];
		}
		_size += len;
	}

我们来想想while循环结束的条件是什么?

1. end > pos 

先来看一下这个条件,当end小于或者等于pos的时候就不会继续循环了,我们假设原字符串是"hello world",假设这里的pos是5,len为3。故end的最后一个有效的位置就是在w位置,因为挪动的条件是_str[end - len] = _str[end],因为end-len = 5 - 3 = 2 ,此时w位置的前三个数据也会被挪动

2. end > pos + len

我们直接来测试一下:

	void test_string4()
	{
		string s("hello world");
		s.insert(5, "&&&");
		cout << s.c_str() << endl;
	}

通过运行结果,可以知道循环结束的条件是有问题的,我们来分析一下:

因为这里出现了乱码,所以我们可以猜测这个问题至少与'\0'有关,通过调试我们发现条件写反了,应该写成:

			_str[end] = _str[end - len];

解决完这个问题我们再来运行一下程序:

可以发现插入&&&以后,原来的空格变成了r了,所以还是有问题,接着来分析一下:

就拿刚才的举的例子为例,因为要插入的字符串有三个字节,所以pos位置之后的数据需要往后挪动三个字节,所以end的结束位置在字符r处,也就是说r的位置就是pos+len,因为r也要挪动,而循环结束的条件是小于或者等于,所以当end = pos + len的时候也需要挪动,所以我们一个修改一下循环结束的条件:

		while (end >= pos + len)

因为pos + len不会为0,所以这个条件不会存在错误,我们重新运行一下代码:

现在可以看到,代码运行成功了,所以循环条件是end >= pos + len

(或者写成end > pos + len - 1,但是这样写需要先判断右边的数加起来不能等于或者小于0,如果小于0就直接返回)

故insert的全部代码如下:

	void string::insert(size_t pos, char ch)
	{
		assert(pos < _size);
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		//挪动数据
		int end = _size;
		while (end >= (int)pos)
		{
			if (end == 0)
			{
				int i = 0;//调试断点
			}
			_str[end + 1] = _str[end];
			--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)
		{
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}
		size_t end = _size + len;
		while (end >= pos + len)
		{
			_str[end] = _str[end - len];
			--end;
		}
		for (size_t i = 0; i < len; i++)
		{
			_str[pos + i] = s[i];
		}
		_size += len;
	}

再来实现一下npos

要注意:npos不能写在头文件中,写在头文件中会报链接错误

	{
    private:
		char* _str;
		size_t _size;
		size_t _capacity;
		static const size_t npos;
	};
	const size_t string::npos = -1;

**************************************************************************************************************

这里在头文件中也可以直接这样写:

	{
    private:
		char* _str;
		size_t _size;
		size_t _capacity;
		static const size_t npos = -1;
	};

但是不建议这么写,因为这个语法比较奇怪。之前我们学习过”静态的成员变量不能直接在类中给缺省值,因为静态的成员变量不走初始化列表,只走声明和定义分离“。

但是这里可以这么写,因为这样的形式就相当于是一个定义了,static const才能这么写

然而最奇怪的一点在于只有整型(int、size_t)可以这么写,double等类型不能这么写

这样设计是因为方便定义数组:

**************************************************************************************************************


最后来实现erase函数:

首先我们需要保证pos不能越界

		assert(pos < _size);

erase需要两个参数pos和len,作用是把从pos开始的len个字符删除掉。如果给的len的长度大于pos后面剩余数据的长度时就直接删除pos后面的所有数据,这里需要注意,如果是要把pos位置后面的数据全部删除的话,不需要把pos后的所有数据给抹除掉,但是要把pos处的后一个位置加上'\0',同时改一下_size的大小

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

我们来测试一下代码:

	void test_string5()
	{
		string s1("hello world");
		s1.erase(6, 100000);
		cout << s1.c_str() << endl;
		
		string s2("hello world");
		s1.erase(6);
		cout << s2.c_str() << endl;

		string s3("hello world");
		s3.erase(6, 3);
		cout << s3.c_str() << endl;
	}


9.实现find、substr函数

find函数可以查找字符也可以查找字符串,我们先来从完成字符的查找:

find函数查找字符需要两个参数:ch和pos,作用是从pos位置开始寻找ch字符,如果找到了就返回ch字符的下标

find查找字符下标的代码非常的简单,就不做过多的讲解了:

	size_t string::find(char ch, size_t pos = 0)
	{
		for (size_t i = 0; i < _size; i++)
		{
			if (_str[i] == ch)
			{
				return i;
			}
		}
		return npos;
	}

下面我们来讲解一下find查找字符串:

要想实现find查找字符串,我们首先需要了解一下strstr函数

(符串匹配的BM算法来实现字符串的查找,有兴趣的可以自行了解)

strstr函数可以返回指向 str1 中第一次出现的 str2 的指针,如果 str2 不是 str1 的一部分,则返回 null 指针

我们首先还是需要判断pos位置是否越界

接着我们用strstr来查找有没有匹配的字符串,如果有就返回下标,没有就返回npos

	size_t string::find(const char* str, size_t pos = 0)
	{
		assert(pos < _size);
		const char* ptr = strstr(_str + pos, str);
		if (str == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}

现在find函数的代码已经全部写完了,我们来将代码整合一下:

	size_t string::find(char ch, size_t pos = 0)
	{
		for (size_t i = 0; i < _size; i++)
		{
			if (_str[i] == ch)
			{
				return i;
			}
		}
		return npos;
	}
	size_t string::find(const char* str, size_t pos = 0)
	{
		assert(pos < _size);
		const char* ptr = strstr(_str + pos, str);
		if (str == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}

接下来我们来实现一下substr函数:

首先我们需要判断pos有没有越界

		assert(pos < _size);

实现sbustr的过程中会出现两种情况:

第一种情况是len的长度已经大于pos位置后的字符串的长度,此时有多少数据就拷贝多少数据

第二种情况是len的长度不大于pos位置后字符串的长度,此时就拷贝len个数据

根据这个条件我们就可以写出代码如下:

	string string::substr(size_t pos = 0, size_t len = npos)
	{
		assert(pos < _size);
		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;
	}

我们结合之前写的find来测试一下:

测试条件是寻找一个文件名的后缀,并返回

	void test_string6()
	{
		string s("test.cpp.zip");
		size_t pos = s.find('.');
		string suffix = s.substr(pos);
		cout << suffix.c_str() << endl;
	}

虽然程序运行成功了,但是这里的代码本身是有问题的,因为VS2022编译器的优化,所以才没有出现问题,我们来分析一下问题吧:

假设目前的编译器环境不是VS2022,假设是VS2019。(VS2022的优化很激进,它会和三为一,把sub变量和临时变量都优化掉,用suffix去充当sub)因为这里的sub是一个传值返回,本来是会构造一个临时变量,用临时变量去拷贝suffix。但是经过编译器的优化,就会直接用sub去拷贝suffix,因为我们目前还没有写拷贝构造,所以这里的拷贝是一个浅拷贝。所以此时sub和sufiix的_str也相同,当sub销毁的时候suffix也被销毁了。所以此时suffix就会指向一个野指针,就会导致程序出现乱码的错误

为了解决这个可能会存在的隐患,我们就需要写一个拷贝构造函数

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

像这样写一个深拷贝就能避免程序出现意外

10.实现比较大小的符号重载

string类中的成员函数现在已经实现的差不多了,我们来实现string类中的大小比较符号的重载。比较大小符号的重载写成的是全局函数,因为它想要支持string类和string类比较、字符串和string类比较等。

我们还是使用复用的方法,这样我们就可以节省代码的空间

1.先来实现一下小于符号的重载

我首先需要知道的是:字符串之间的比较不是按照长度来比较的,而是按照ASCII码表来进行比较的,所以此时我们就可以使用到strcmp函数来比较大小

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

我们来测试一下代码:

	void test_string7()
	{
		string s1("hello world");
		string s2("hello world");
		cout << (s1 < s2) << endl;
		cout << (s1 == s2) << endl;
		cout << ("hello world" < s2) << endl;
		cout << (s1 == "hello world") << endl;
	}

我们在这里需要注意,在进行大于小于比较的时候需要加上括号,因为 << 的优先级更高

我们这里没有重载string和字符串、字符串和string的比较,但是为什么还是可以成功比较呢?这是因为走了隐式类型的转换

但如果是下面的这种形式就不会走隐式类型转换

		cout << ("hello world" == "hello world") << endl;

因为运算符的重载必须要有一个类类型的参数,这里的代码是两个常量字符串的比较,编译器在识别的时候就会识别为一个指针,相当于是两个指针的比较了

11.实现流插入和流提取的重载

因为流插入是读取string的值,并且将它的值插入到一个ostream类型的对象中去,所以要用以下以下的形式处理代码的const:

	ostream& operator<<(ostream& out, const string& s)

而流提取是要将istream类型的数据写入一个string类型的对象中去,所以此时情况就与流插入相反:

	istream& operator>>(istream& in, string& s)

我们先来处理一下流插入代码的细节:

string对象是不支持直接插入的,但是流插入库里面已经实现好了,对于内置类型的成员是支持插入的,所以此时我们就需要把string转换成一个一个的字符再插入,这里我们就可以使用范围for来取出所有的字符:

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

我们来测试一下代码:

	void test_string8()
	{
		string s1("hello world");
		cout << s1 << endl;
	}

接下来我们来实现流提取的代码:

我们首先需要一个字符一个字符的提取数据,遇到换行和空格就默认作为字符串的分割

	istream& operator>>(istream& in, string& s)
	{
		char ch;
		in >> ch;
		while (ch != ' ' && ch != '/n')
		{
			s += ch;
			in >> ch;
		}
		return in;
	}

接着来测试一下:

	void test_string8()
	{
		string s1;
		cin >> s1;
		cout << s1 << endl;
	}

此时我们就会发现一个问题:无论是空格还是换行都不会停止输入,程序一直在循环的输入,不会跳出:

出现这个问题的原因是:cin和scanf输入和提取任何类型的值默认都是以空格和换行为分隔符,所以我们在输入一个一个的字符的时候,也会自动忽略掉换行和空格

所以我们就不能使用流提取来提取这里的char数据,此时我们就需要调用到istream里面的一个叫做get的函数

这里的get不会管分隔符,也不涉及任何的类型,只会一个字符一个字符的读取,和C语言中的getc函数相似,所以我们使用get函数对代码进行修改:

	istream& operator>>(istream& in, string& s)
	{
		char ch;
		ch = in.get();
		while (ch != ' ' && ch != '/n')
		{
			s += ch;
			ch = in.get();
		}
		return in;
	}

此时重新运行代码,就可以看到代码没有出现任何的错误

我们再看一下,假设s1中原先就已经存在字符串

	void test_string8()
	{
		string s1("hello world");
		cin >> s1;
		cout << s1 << endl;
	}

我们可以发现:在输入数据以后,原来的数据依旧存在,这一点与库函数不同,库函数在输入以后不会保留原来的数据,我们需要保证模拟实现的函数功能要与库函数中的一致,所以我们此时对代码进行修改:

因为在输入之前我们先要清空原来的所有数据,所以我们此时还需要在string类中写一个clear函数:

clear函数的实现十分简单,我们给数组中的第一个数据赋予 \0 ,再把size改成0,capacity不修改就好了:

		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}
	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char ch;
		ch = in.get();
		while (ch != ' ' && ch != '/n')
		{
			s += ch;
			ch = in.get();
		}
		return in;
	}

此时我们重新运行代码,就不会出现问题了:

其实流提取的代码还有优化的空间:

假设我们输入的是一个很长很长的字符串时,此时s就会不断的去 += ch,不断的+=就会不断的去扩容,此时就会对程序的效率产生一定的影响。要是我们可以提前开辟好空间,就会提高程序的效率。单此时我们就会遇到一个麻烦:我们不知道要开辟多大的空间才合理

此时有人就提供了这样的一种解决方案:

创建一个buff数组,数组中的数据个数任定,每次提取的字符都先放到buff中去,假设输入数据的个数很大,buff满了,此时直接在s后面+=buff。一直这样循环,直到数据结束:

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		const int N = 256;
		char buff[N];
		int i = 0;
		char ch;
		ch = in.get();
		while (ch != ' ' && ch != '/n')
		{
			buff[i++] = ch;
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}

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

		return in;
	}

这样处理比起直接reserve的好处是可以减少空间浪费或者频繁的扩容

**************************************************************************************************************

这里来提出一个问题:我们之前实现日期类的时候将流插入和流提取定义为一个友元函数,那么流插入和流提取是不是必须写成友元函数呢?

答案是:否,假设我们不需要访问一个对象的私有成员就不需要写成友元函数,就像上面写的流插入的代码一样,可以直接使用迭代器或者下标+[ ]就能访问的就不用写成友元函数

**************************************************************************************************************

结尾

string类的主要功能到这里就实现的差不多了,自己去模拟实现string类不仅可以提升我们的代码水平,还可以加深我们对于库函数的理解。那么本节的的内容就到此结束了,希望可以给您带来帮助,谢谢您的浏览!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值