C++-string类的模拟实现

news2024/11/26 12:46:26

本博客基于C++官方文档当中给出的string类当中的主要功能实现,来作为参照,简单模拟实现 My-string 。

 对于C++当中的string类的介绍,在之前的几篇博客当中有说明,如有问题,请参照一下两个博客文章进行参考:

(2条消息) C++ string类-2_chihiro1122的博客-CSDN博客

(2条消息) C++ string类 迭代器 范围for_string类型迭代器_chihiro1122的博客-CSDN博客

string类

 为了与官方当中的string类做区分,在写的时候,My-string类在自定义的命名空间当中进行实现。

基础功能实现

 My-string 类的成员变量

 不想成员变量在类外部被修改,所以用 protected 关键字来修饰:

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

 构造函数和析构函数

 参考官方文档当中的构造函数有很多,但是没有必要都实现,只需要实现常用的两种,之间传入字符串和构建空对象两个方式的构造函数,实现如下所示:

		 //构造函数
		string()
			:_str(new char[1]),
			_size(0),
			_capacity(0)
		{
			_str[0] = '\0';
		}

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

注意上述使用的参数列表来对string类当中的成员变量进行赋值和开空间等操作,在使用参数列表的时候,参数列表当中参数的顺序是要和本类当中的成员变量的声明顺序要保持一致。因为在使用参数列表定义成员变量的时候,不是按照参数列表当中参数的顺序来定义的,是按照本类当中对成员的声明顺序来进行定义的。

如下例子,在使用参数列表的时候就会出错:

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

    protected:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

我们发现上述定义 _str 这个字符串数组的时候,本类当中的声明是第一声明的,而在构造函数的参数列表当中的位置是在最后的。但是在定义变量的时候不是先定义 _size 这个变量,而是按照本类当中对变量的声明顺序来进行定义,所以会先对 _str 这个字符串数组进行定义;又因为在上述构造函数的参数列表当中,定义的 _str 这个数组的大小是按照 _capacity 这个成员变量来进行计算的,但是此时 _capacity 这个成员变量没有定义,不是我们想要的值,所以这个程序可能出现问题。

 所以,我们在使用参数列表的时候,记住一定要保证成员变量在类当中的声明和 列表当中定义的顺序保持一致。

像上述我们想实现的两种构造函数,其实可以用一个 带缺省参数的构造函数实现:

		//string(const char* str = '\0') // 错误写法
		//string(const char* str = nullptr) // 错误写法
		//string(const char* str = "\0") // 可以但是没有必要下面更好
		string(const char* str = "")
			: _str(new char[strlen(str) + 1]),
			_size(strlen(str)),
			_capacity(strlen(str))
		{
			strcpy(_str, str);
		}

 拷贝构造函数实现:

		// 拷贝构造函数(深拷贝)
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_capacity = s._capacity;
			_size = s._size;
		}

 析构函数实现:

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

 一些简单的功能实现

		// 返回c语言形式的字符串
		const char* c_str() const
		{
			return _str;
		}

		// 返回有效的字符个数
		size_t Size() const
		{
			return _size;
		}

operator [] 

 我们在使用官方的string类的时候,这个[] 下标访问操作符的运算符重载函数非常好用,所以这里也实现一下,其实这个函数实现不难,但是需要注意两个接口,一个接口是可读可写的函数,也就是针对的是非const 对象;另一个是只可读的 const 对象:

可读可写(非const对象):

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

			return _str[pos];
		}

只可读(const对象):

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

			return _str[pos];
		}

上述的只可读接口函数当中使用的 const 修饰的是 函数参数当中所隐含的 this 指针,这个指针指向的是这个函数作用的对象,如果这个对象 是 const 的对象,且使用的函数是可读可写的函数,也就是用没有用 const 修饰的函数,那么此处就发生了权限的放大,就会编译报错。

 像上述就是函数参数的类型不同,构成了函数的重载,在使用这个 operator [] 这个函数的时候,使用的是普通对象,那么就调用第一个函数;如果使用的是 const对象,就调用第二个函数。

iterator 迭代器 和 范围for 的简单实现

 迭代器

 其实在string当中的迭代器就是一个 typedef 的实现,具体操作看如下对 string 迭代器 当中的 begin() 和 end() 函数的实现就会明白。

begin()和 end()在数组当中的关系: 

 那么其实实现也很简单:

	public:
		typedef char* iterator;

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

其实 iterator 就是对char* 类型的一个 重命名。

上述迭代器适用的是普通的对象,这个迭代器是可读可写的;对于 const 的对象,就要单独创建只可读的迭代器

		typedef const char* const_iterator;

		iterator begin() const
		{
			return _str;
		}

		iterator end() const
		{
			return _str + size();
		}

使用迭代器

	string_begin::string::iterator it = str.begin();
	while (it != str.end())
	{
		cout << *it << endl;
		it++;
	}

注意:使用很简单,需要注意的是,在使用的使用,我们要想找到迭代器的位置,应该像上述一样先声明命名空间然后再声明命名空间的当中的类空间,然后编译器才能找到 iterator 。因为编译器默认只在全局进行搜索。

或者是用 auto 自动推导类型。

 范围for

 当我们实现了上述迭代器之后,我们像官方string类当中使用范围for一样来使在 My-string 当中使用范围for,发现已经可以使用了。

 这是因为,范围for 的底层实际就是迭代器,所以我们在实现迭代器之后,就可以支持范围for了。

在我们看来 范围for 很智能,自动开始,自动判断结束等等,其实这都是编译器做的,在编译的时候会把范围for 傻瓜式的全部替换为迭代器,所以在我们看来很神奇,智能,其实都是编译器的功劳。

 使用:

	for (auto ch : str)
	{
		cout << ch << " ";
	}
	cout << endl; 

输出:

 怎么证明呢?其实很简单,因为范围for 是编译器的 傻瓜式替换,所以名字不对也不能使用 范围 for:

现在我们把 end()这个函数给注释掉,发现直接报错了:

 范围for 在使用的时候,编译器不能找到end()这个函数。

我们把end()函数的名字改成 Myend()之后也不行,也是直接报错找不到end()函数

 总结,范围for 是傻瓜式的替换,不仅对迭代器的功能有要求,对迭代器的命名都是有要求的

 如果我们查看范围for 的底层汇编,发现他也是在调用 begin(),end()这样的函数,和迭代器差不多的。

增删查改

增 

 对于字符串的增,实现两个函数,一个是 push_back() 一个是 append(),push_back()是尾差一个字符,append ()是尾差一个字符串。

当然,增,可能会有一个扩容的问题,对于 push_back()我们可以直接用 _capacity 的 2 倍的形式扩容;但是append()因为尾差的是字符串,我们不敢直接 扩容2倍,可能会有添加的字符串之后的有效字符数,大于 _capacity 的情况。所以,我们这里使用 strlen(str)计算要插入的字符串的有效字符 + _size 原本字符串数组当中的有效字符作为 扩容的条件和 扩容的大小。

在官方的string类当中,有一个 reserve ()扩容函数,在这这里我们就直接实现这个函数,那么在上述的  push_back() 一个是 append() 函数中我们就使用这个函数来进行扩容:

 reserve():

这个函数我们遵循的是 c 当中 realloc 函数的扩容规则之一,直接开辟另一个新的大空间,再把原本空间的当中的内容拷贝到 新空间当中:

		// 扩容函数
		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 ch)
		{
			// 如果有效字符个数超过了 容量
			if (_size >= _capacity)
			{
				// 2倍扩容
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			_str[_size] = ch;
			_size++;
			_str[_size] = '\0';
		}

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

当然上述两个函数都不是我们最常用的,最常用的是 operator += 这个运算符重载函数,当然这个函数的底层和上述差不多,只不过在使用的时候更加方便:

他同样有两个接口,一个是 尾插字符,一个是尾差字符串:
 

		// += 尾差字符
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

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

对于插入,还有就是在 指定位置(pos)位置插入字符或字符串(insert())函数的实现:

		// 指定位置插入一个或多个字符
		void insert(size_t pos, size_t n, char ch)
		{
			assert(pos <= _size);

			if (_size + n >= _capacity)
			{
				// 扩容
				reserve(_size + n);
			}

			//往后挪动数据
			size_t end = _size;
			while (pos <= end && end != npos)
			{
				_str[end + n] = _str[end];
				--end;
			}

			// 覆盖值
			for (size_t i = 0; i < n; i++)
			{
				_str[pos + i] = ch;
			}

			_size += n;
		}
		// 指定位置插入一个字符串
		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;
			while (pos <= end && end != npos)
			{
				_str[end + len] = _str[end];
				--end;
			}

			// 覆盖值
			for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = *(str + i);
			}

			_size += len;
		}

在实现的时候遇到的问题,如下图:

 end是int类型的,pos是size_t类型的,按照上述的调试,本次循环应该退出循环,但是实际上是进入了循环

 其原因是因为,在c语言的语法当中规定,在一个运算符的两边,如果左右两边的操作数的类型不相同,就会发生整形提升,通常是小类型像大类型发生转换,比如如果是 int类型和 double类型,那么int类型会转换为 double类型

 所以,看似上述end到了-1,按道理应该退出循环,但是int类型的 end 发生的整形提升,提升到了 size_t 无符号整形,所以,了解类型的值域的循环的小伙伴就知道,此时end就不会是-1,从无符号整形的视角来看,是全1,就是整形的最大值。自然就不会跳出循环。

 解决上述问题的方式有很多,可以强制类型转换:

 或者像官方string当中一样设置一个 npoe :

 如上设置之后,就可以在while循环当中多增加一个条件,当 end 走到 -1 的之后就停止:

 删

 erase函数,从pos位置删除一个或多个字符:

		// 从pos位置删除一个或多个字符
		void erase(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);

			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				size_t end = pos + len;
				while (end <= _size)
				{
					_str[pos++] = _str[end++];
				}
				_size -= len;
			}
		}

 clear()函数,删除字符串当中所有的有效字符:

		// 清除所有有效字符
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

 find()函数,在pos位置查找一个字符:

		// 从pos位置往后查找一个字符
		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;
		}

find()函数,从pos位置开始查找一个字符串:

		// 从pos位置往后查找一个字符
		size_t find(char ch, size_t pos = 0)
		{
			assert(pos < _size);
			for (size_t i = pos; i <= _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}

			return npos;
		}

		// 从pos位置开始查找一个字符串
		size_t find(const char* str, size_t pos = 0)
		{
			assert(pos < _size);
			const char* ptr = strstr(_str, str);
			if (ptr)
			{
				return ptr - _str;
			}
			else
			{
				return npos;
			}
		}

substr()函数,从字符串数组的pos位置开始取出len个字符大小的字符串:

		// 从pos位置开始,从字符串数组当中取出len个字符的字符串,返回string类
		string substr(size_t pos, size_t len = npos)
		{
			assert(pos < _size);

			size_t n = len;
			if (len == npos || len + pos >= _size)
			{
				n = _size - pos;
			}

			string tmp;
			tmp.reserve(n);
			for (int i = pos; i < pos + n; i++)
			{
				tmp += _str[i];
			}

			return tmp;
		}

其他

 resize()函数,在官方string类当中的resize()函数可以扩容也可以缩容(删除元素),同样我们MyString当中模拟实现的 resize()也应该具备上述功能,我们考虑出以下三种情况(如下图所示):

  • 对于 5 的情况,就是小于 _size 的情况,这时候就需要缩容,相当于是删除元素,直接修改 _size 即可;
  • 对于 15 的情况,就是 在 _size 和 _capacity 之间的情况,这时候空间是够的,只需要填写初始化覆盖的字符即可;
  • 对于 25 的情况,就是 超出了 _capacitt 容量,这时候需要扩容,然后初始化字符;
  • 对于上述 15 和 25 的情况,我们统一进行处理,我们上述实现的 reserve()扩容函数是会检测给定的空间是否超出 _capacity 原本的空间大小的,所以上述两种情况直接先进行 reserve()检测扩容,然后在进行初始化赋值操作;

代码实现:

		// resize 删除或扩容添加函数
		void resize(size_t n, char ch = '\0')
		{
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				// 先检测 扩容
				reserve(n);

				for (int i = _size; i < n; i++)
				{
					_str[i] = ch;
				}
				_size = n;
				_str[_size] = '\0';
			}
		}

输入输出(流插入和流提取)(operator<< 和 operator>>)

 注意1:上述两个函数当中的 返回的 ostream 返回值和 ostream 参数一定要用引用,如果不用引用的话,在使用的时候就会报错;因为 ostream 做了一个防拷贝操作:

 在定义的时候让 ostream 的拷贝构造函数 = delete 就做到了防拷贝操作;上述函数如果不做为引用返回的话,单独用 ostream 返回,就会产生拷贝,生成临时对象,所以就会报错!!!

例子:

ostream operator<< (ostream out, string& str)
{
	 方式一
	//for (int i = 0; i < str.size(); i++)
	//{
	//	out << str[i];
	//}

	// 方式二
	for (auto ch : str)
	{
		out << ch;
	}

	return out;
}

 上述这个例子没有进行引用就会报错:

 注意2:这个函数最好是定义在全局,而不建议定义为成员函数,因为成员函数的第一个参数被固定为当前对象的this指针,这样的话,实现出来的流输出等等在格式上不符合标准的我们经常使用的格式,所以,我们期望 类似 ostream 这样的参数作为函数的第一参数,这样的格式比较符合。

注意3:上述也提到了要定义在全局,但是,我们上述是使用了命名空间来和官方的String类做区别的,所以这个全局函数应该定义在命名空间当中,String类之外,也就是在命名空间的全局域当中;如果你不小心把这个全局函数定义来命名空间之外的最大的那个全局当中,那么编译器会认为这个是STL当中的string。

 operator<<(流插入)

 可以用有元来解决私有成员的问题,但是一般不建议使用有元,如下使用 operator[] 函数,或迭代器来访问:

	ostream& operator<< (ostream& out, string& str)
	{
		 方式一
		//for (int i = 0; i < str.size(); i++)
		//{
		//	out << str[i];
		//}

		// 方式二
		for (auto ch : str)
		{
			out << ch;
		}

		return out;
	}

 此处的 打印和 使用 c_str()函数来打印,两者其实有差别的,大多数情况下两者打印的结果都一样,但是有些特殊情况下就不一样了,如下例子所示:

 c_str()函数打印的是一个字符串,是一个内置类型,打印这个字符串是以 '\0' 作为终止符的,而流输入是插入多少就打印多少,所以遇见 '\0' 是不会停止的;而上述没有打印出 '\0' 是VS版本的问题,上述使用的是VS2019,如果是VS2013就会在 "hello world" 和 “!!!!!!!!!”之间打印一个空格。

 所以这也就引发出一个问题,在C库函数当中的strcpy函数也是按照 '\0" 作为停止符号的,如果字符串的有效字符当中包含了 '\0' 那么这个函数在拷贝的时候就会出现问题,所以我们应该使用 memcpy()这个函数。

  operator>>(流提取)

 这里的流提取(输入)不考虑空格的情况,如果需要像一个句子一样输入有空格的字符串的话,应该使用getline()函数,这个函数在下面会实现。

所以,只需要一次从缓冲区当中提取一个字符然后 += 到string类当中就行了。

而且,我们使用 流提取来对string类对象当中的字符串数组进行写入数据的话,我们希望的是覆盖,而不是像 operator+= 函数一样尾插,我们我们考虑先用 clear()函数对字符串进行清除。 

问题:

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

		return in;
	}

上述代码会陷入死循环,这是因为流提取在识别不同字符或字符串的时候,使用 空格换行(\n)来进行识别的,也就是说 istream 这个流提取本身就不会读到 空格 和 换行(\n),所以上面的ch就不能被赋值为 空格 和 换行(\n),所以循环就不会停止。

 解决方法,在 istream 这个流提取当中有一个接口 get(),它默认每次只读取一个字符不管这个字符是 空格 还是 换行

代码如下:

	istream& operator>> (istream& in, string& str)
	{
        str.clear();
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			str += ch;
			ch = in.get();
		}

		return in;
	}

上述代码还可以进行优化,我们发现我们给 str 对象当中插入字符,使用的是 operator+= 这个函数来实现的,而上述是一个一个字符的形式来进行插入的,这样就会导致一个问题,当我们输入的字符串很长的时候, 使用 operator+= 函数会进行很多次扩容,虽然影响不大,但是对于就这个代码来看,还是不太好,所以我们进行以下优化:

  • 法1:我们可以先 resercve()预先开辟一段空间,这样就可以一定程度上解决问题,但是这样解决一小部分问题,假设我们先开辟 1024 个空间,如果我们现在只需要10个字符串,那么之后的 1014个空间就浪费了,如果我们需要的空间是 1024的好几倍,那么 1024也不够用,还是需要多开辟几次空间,所以这个方案我们不采取,不适用很多场景。
  • 法2:开辟一个临时数组,这个数组的大小可以自己规定,这里我们规定大小为 128 个字符;相当于是把我们输入的字符串,以一组127个有效字符,分割成很多组,当一组的字符填满之后,在对str对象当中的字符串数组进行填写,这样就避免了 在 operator+= 当中很多次的扩容操作。

 还需要优化的是,上述我们的实现的流提取,遇到换行或者空格就会停止,那么如果我们在输入有效字符之前,有空格或者换行,那么就会直接停止,但是在官方的string当中的流提取是会把前面的空格和换行清除的,所以我们这加一个循环,把有效字符之前的空格和换行给删除了。

最终代码实现:

istream& operator>> (istream& in, string& str)
	{
		str.clear();
		char ch = in.get();

		while (ch == ' ' || ch == '\0')
		{
			ch = in.get();
		}

		char Buff[128];
		int i = 0;

		while (ch != ' ' && ch != '\n')
		{
			Buff[i++] = ch;

			if (i == 127)
			{
				Buff[i] = '\0';
				str += Buff;
				// 重置i
				i = 0;
			}
			ch = in.get();
		}

		// 如果此时 i 不是0,说明Buff 当中还有字符没有 += 完
		if (i != 0)
		{
			Buff[i] = '\0';
			str += Buff;
		}

		return in;
	}

比较大小

 operator<

 string的比较大小不按照长度来比,按照ascll码来比,比如 str1 = "bb" ; str2 = "aaa";那么要是 str2 > str1。

在这里实现可以直接使用 C 当中高度strcmp()这个库函数来实现,但是还是会出现和上述一样的问题,如果有效字符当中有 '\0' ,那么就会出现问题,所以我们应该使用 memcmp()。

但是,memcmp()还是有问题,如下两种情况,两个字符串字符个数不相等,还是比较麻烦的:

 所以还是要自己来实现,其实自己实现也不难:

  • 两个字符串一起走,如果当前哪一个字符串的字符的ascll值大,那么他就大,反之;如果当前两个字符串的字符相等,那么就继续往后走。
  • 之后到最后,有两种情况,一种是两个字符串字符相等,字符个数也相等,那么这两个字符串就相等;另一种就是上述说的两种情况,短的字符串走完了,长的字符串没有走完,那么长的字符串就是大的那一个字符串;

 代码实现:

		int operator< (const string& str)
		{
			//return strcmp(_str, str._str) < 0;

			size_t i1 = 0;
			size_t i2 = 0;

			while (i1 < _size && i2 < str._size)
			{
				if (_str[i1] < str._str[i2])
				{
					return true;
				}
				else if (_str[i1] > str._str[i2])
				{
					return false;
				}
				else
				{
					++i1;
					++i2;
				}
			}

			/*if (i1 == _size && i2 != str._size)
			{
				return true;
			}
			else
			{
				return false;
			}*/
			// 或者
			
			// return _size < str._size;

			// 或者

			return i1 == _size && i2 != str._size;
		}

复用 memcmp()函数实现的代码如下:

		int operator< (const string& str)
		{
			int Mybool = memcmp(_str, str._str, _size < str._size ? _size : str._size);

			return Mybool == 0 ? _size < str._size : Mybool < 0;
		}

operator== / <= / > / >= / !=

 写好一个之后,后面的就简单了,可以直接复用:

		bool operator== (const string& str) const
		{
			return _size == str._size &&
				memcmp(_str, str._str, _size) == 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 == s);
		}

operator= 

 复制拷贝有两种,一种是浅拷贝,一种是深拷贝:

  • 浅拷贝就是值拷贝,是直接复制,这样的话如果只是一些内置类型是没有问题的,但是,如果是一块空间的指针进行浅拷贝,那么这个指针只是指向了新的一块空间,那么原本的指针指向的空间就找不到了,就会发生内存泄漏;
  • 深拷贝就是上述的例子,假设要拷贝另一块空间,就新开辟一块空间,这块空间的大小和另一块空间的大小一样,然后再把另一快空间当中的值拷贝到新空间当中,然后再让指针指向这条新的空间,这就是深拷贝。
  • 对于深拷贝,上述描写的只是一种情况,就是当要拷贝的空间比原空间大;其实还有两种情况,一种拷贝的空间比原空间小,那么就直接进行拷贝,但是为了优化,多余的空间需要释放,所以还是要重新开一个更小的空间然后进行赋值;另一种是拷贝的空间和原空间相等,那么就直接对空间进行赋值。

 所以综上所述,建议 处了空间大小相等的情况下 都直接释放原空间,然后再进行赋值。

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

				_size = str._size;
				_capacity = str._capacity;
			}

			return *this;
		}

上述就是深拷贝的赋值操作符重载函数

其实还有一个更好的写法,先来看下面这个代码:

string& operator= (const string& str)
		{
			if (this != &str)
			{
				string tmp(str);

				std::swap(_str, tmp._str);
				std::swap(_size, tmp._size);
				std::swap(_capacity, tmp._capacity);
			}
			return *this;
		}

如上,新创建一个和 str 一样的对象(调用了拷贝构造函数)-tmp,然后把tmp和 this对象的数组指针和 所以成员交换一下,然后就可以实现生拷贝了,这是一个很妙的写法,看下图:

 如上所示,tmp新开辟了一块空间,s1想着,反正你tmp 的生命周期 就在这个函数当中,出了这个函数,tmp就需要调用析构函数,释放空间,那么s1就把他的空间给tmp,tmp就把新开辟的,和s3赋值好的空间给s1,然后tmp在最后释放空间的时候就释放的是s1原本的空间,相当于是tmp为s1把原空间给释放掉了。

 注意:在赋值操作符重载函数当中不能像如下一样写:

  string& operator= (const string& str)
    {
        string tmp(str);
        std::swap(tmp , *this);

        return *this;
    }

像上述一样写会造成递归死循环。

 这时候,swap()这个函数在两个参数都是对象的时候,它其中调用的就是 operator= 赋值操作符重载函数,如下所示:

 那像上述代码就会在 swap 和 operator= 之间来回跳,造成递归式的死循环

 所以在像上述一样使用swap 的时候,还是需要自己实现swap()函数:

就如上述string类的swap如下所示实现:

class string
{
`````````````

		void swap(string& str)
		{
			std::swap(_str, str._str);
			std::swap(_size, str._size);
			std::swap(_capacity, str._capacity);
		}

````````````
};

根据上述实现的swap()函数,上述的 operator= 函数可以如下优化:

		string& operator= (string& str)
		{
			swap(str);

			return *this;
		}

上述相当于把两个string对象的 所有成员 都给交换

		string& operator= (string str)
		{
			swap(str);

			return *this;
		}

而上述才是和之前一样,是传值拷贝,需要创建临时对象,也就是这个str就是局部变量,局部变量出了这个函数作用域就结束了生命周期,相当于是 str 帮s1 (*this)把s1原本的空间释放掉了。

 

 string类的完整代码
 

 

#pragma once
#include<assert.h>

namespace string_begin
{
	class string
	{

	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		iterator begin() const
		{
			return _str;
		}

		iterator end() const
		{
			return _str + size();
		}
		// 构造函数
		//string()
		//	:_str(new char[1]),
		//	_size(0),
		//	_capacity(0)
		//{
		//	_str[0] = '\0';
		//}

		//string(const char* str)
		//	: _str(new char[strlen(str) + 1]),
		//	_size(strlen(str)),
		//	_capacity(strlen(str))
		//{
		//	strcpy(_str, str);
		//}
		
		//string(const char* str = '\0') // 错误写法
		//string(const char* str = nullptr) // 错误写法
		//string(const char* str = "\0") // 可以但是没有必要下面更好
		string(const char* str = "")
			: _str(new char[strlen(str) + 1]),
			_size(strlen(str)),
			_capacity(strlen(str))
		{
			memcpy(_str, str, _size + 1);
		}

		// 拷贝构造函数(深拷贝)
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			//strcpy(_str, s._str);
			memcpy(_str, s._str, s._size + 1);
			_capacity = s._capacity;
			_size = s._size;
		}

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

		// 返回c语言形式的字符串
		const char* c_str() const
		{
			return _str;
		}

		// 返回有效的字符个数
		size_t size() const
		{
			return _size;
		}

		// 下标+引用返回的运算符重载函数
		// 要提供两个版本,一个是非const对象的,一个是const对象的
		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 reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				memcpy(tmp, _str, _size + 1);
				delete[] _str;
				_str = tmp;
				_capacity = n; 
			}
		}

		// 增
		void push_back(char ch)
		{
			// 如果有效字符个数超过了 容量
			if (_size >= _capacity)
			{
				// 2倍扩容
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			_str[_size] = ch;
			_size++;
			_str[_size] = '\0';
		}

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

		// += 尾插字符
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

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

		// 指定位置插入一个或多个字符
		void insert(size_t pos, size_t n, char ch)
		{
			assert(pos <= _size);

			if (_size + n >= _capacity)
			{
				// 扩容
				reserve(_size + n);
			}

			//往后挪动数据
			size_t end = _size;
			while (pos <= end && end != npos)
			{
				_str[end + n] = _str[end];
				--end;
			}

			// 覆盖值
			for (size_t i = 0; i < n; i++)
			{
				_str[pos + i] = ch;
			}

			_size += n;
		}

		// 指定位置插入一个字符串
		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;
			while (pos <= end && end != npos)
			{
				_str[end + len] = _str[end];
				--end;
			}

			// 覆盖值
			for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = *(str + i);
			}

			_size += len;
		}

		// 从pos位置删除一个或多个字符
		void erase(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);

			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				size_t end = pos + len;
				while (end <= _size)
				{
					_str[pos++] = _str[end++];
				}
				_size -= len;
			}
		}

		// 从pos位置往后查找一个字符
		size_t find(char ch, size_t pos = 0)
		{
			assert(pos <= _size);
			for (size_t i = pos; i <= _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}

			return npos;
		}

		// 从pos位置开始查找一个字符串
		size_t find(const char* str, size_t pos = 0)
		{
			assert(pos <= _size);
			const char* ptr = strstr(_str, str);
			if (ptr)
			{
				return ptr - _str;
			}
			else
			{
				return npos;
			}
		}

		// 从pos位置开始,从字符串数组当中取出len个字符的字符串,返回string类
		string substr(size_t pos, size_t len = npos)
		{
			assert(pos < _size);

			size_t n = len;
			if (len == npos || len + pos >= _size)
			{
				n = _size - pos;
			}

			string tmp;
			tmp.reserve(n);
			for (int i = pos; i < pos + n; i++)
			{
				tmp += _str[i];
			}

			return tmp;
		}

		// resize 删除或扩容添加函数
		void resize(size_t n, char ch = '\0')
		{
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				// 先检测 扩容
				reserve(n);

				for (int i = _size; i < n; i++)
				{
					_str[i] = ch;
				}
				_size = n;
				_str[_size] = '\0';
			}
		}

		// 清除所有有效字符
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		// 比较string大小
		//int operator< (const string& str) const
		//{
		//	//return strcmp(_str, str._str) < 0;

		//	size_t i1 = 0;
		//	size_t i2 = 0;

		//	while (i1 < _size && i2 < str._size)
		//	{
		//		if (_str[i1] < str._str[i2])
		//		{
		//			return true;
		//		}
		//		else if (_str[i1] > str._str[i2])
		//		{
		//			return false;
		//		}
		//		else
		//		{
		//			++i1;
		//			++i2;
		//		}
		//	}

		//	/*if (i1 == _size && i2 != str._size)
		//	{
		//		return true;
		//	}
		//	else
		//	{
		//		return false;
		//	}*/
		//	// 或者
		//	
		//	// return _size < str._size;

		//	// 或者

		//	return i1 == _size && i2 != str._size;
		//}

		// < 当中复用 memcmp
		bool operator< (const string& str) const
		{
			int Mybool = memcmp(_str, str._str, _size < str._size ? _size : str._size);

			return Mybool == 0 ? _size < str._size : Mybool < 0;
		}

		bool operator== (const string& str) const
		{
			return _size == str._size &&
				memcmp(_str, str._str, _size) == 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 swap(string& str)
		{
			std::swap(_str, str._str);
			std::swap(_size, str._size);
			std::swap(_capacity, str._capacity);
		}

		// 复制操作符重载函数
		//string& operator= (string& str)
		//{
		//	if (this != &str)
		//	{
		//		char* tmp = new char[str._capacity + 1];
		//		memcpy(tmp, str._str , str._size + 1);
		//		delete[] _str;
		//		_str = tmp;

		//		_size = str._size;
		//		_capacity = str._capacity;
		//	}

		//	return *this;
		//}

		//string& operator= (const string& str)
		//{
		//	if (this != &str)
		//	{
		//		string tmp(str);

		//		//std::swap(_str, tmp._str);
		//		//std::swap(_size, tmp._size);
		//		//std::swap(_capacity, tmp._capacity);
		//		swap(tmp);

		//	}
		//	return *this;
		//}

		string& operator= (string str)
		{
			swap(str);

			return *this;
		}

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

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

	ostream& operator<< (ostream& out, string& str)
	{
		 方式一
		//for (int i = 0; i < str.size(); i++)
		//{
		//	out << str[i];
		//}

		// 方式二
		for (auto ch : str)
		{
			out << ch;
		}

		return out;
	}

	istream& operator>> (istream& in, string& str)
	{
		str.clear();
		char ch = in.get();

		while (ch == ' ' || ch == '\0')
		{
			ch = in.get();
		}

		char Buff[128];
		int i = 0;

		while (ch != ' ' && ch != '\n')
		{
			Buff[i++] = ch;

			if (i == 127)
			{
				Buff[i] = '\0';
				str += Buff;
				// 重置i
				i = 0;
			}
			ch = in.get();
		}

		// 如果此时 i 不是0,说明Buff 当中还有字符没有 += 完
		if (i != 0)
		{
			Buff[i] = '\0';
			str += Buff;
		}

		return in;
	}
}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/764640.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CTF 1和0

一、 对于此类型数据&#xff0c;将其复制到excel中&#xff0c;将0所在位置背景色设置为白色&#xff0c;将1所在的位置设置为黑色 如图所示 二、添加定位符 对于定位符来说&#xff0c;同样可以在excel表中添加1&#xff0c;且1所在单元格为黑色表示定位符&#xff0c;如下…

07 - 线性表的类型定义 - 循环单向链表

前两节我们已经学习了单链表和双链表的概念以及具体的实现过程,其中有一个问题,值得注意,这样的链表有一个共同特征,就是尾节点指向 null,可以说是一次性的,像是糖葫芦。想象一下,如果此时,糖葫芦变成了手串,也就意味着头尾相连,形成闭环,这就是单向链表的另外一种形…

提高工作效率的文件管理软件实践方法

在现代社会中&#xff0c;高效的工作效率是保持竞争力的关键。随着信息技术的不断发展&#xff0c;文件管理软件成为提高工作效率的重要工具之一。 一个好的文件管理软件需要具备直观友好的用户界面。用户界面的清晰易操作让用户更便捷地找到所需文件。在软件开发的初期&#…

KMP 算法推演总结

title: KMP 算法推演总结 date: 2023-07-17 16:07:13 tags: 算法 categories:数据结构与算法 cover: https://cover.png feature: false KMP 算法推演 可先见 Fan’s Web 字符串匹配的 BF 算法、RK 算法部分&#xff0c;后面的 BM 算法及 KMP 算法可以搭配一起看 KMP 算法的…

ADB初识

ADB是Android Debug Bridge&#xff0c;是一个命令行程序。abd可以从计算机上通过USB控制Android手机设备。可以使用ADB复制文件、安装和卸载应用程序&#xff0c;运行shell命令等。 ADB的下载配置 Windows版本&#xff1a;https://dl.google.com/android/repository/platform…

ICV报告:确保城市未来产业发展成功的策略

近日&#xff0c;专注于前沿科技领域的国际咨询机构ICV TAnK发布了《确保城市未来产业发展成功的策略》报告。报告的主要内容包括&#xff1a;未来产业的背景情况和发展趋势&#xff1b;在城市中发展未来产业的重要性&#xff1b;对未来产业发展面临的五大困难和城市在发展未来…

Spring Batch之读数据库——JdbcCursorItemReader之自定义PreparedStatementSetter(三十八)

一、自定义PreparedStatementSetter 详情参考我的另一篇博客&#xff1a; Spring Batch之读数据库——JdbcCursorItemReader&#xff08;三十五&#xff09;_人……杰的博客-CSDN博客 二、项目实例 1.项目实例 2.代码实现 BatchMain.java&#xff1a; package com.xj.dem…

如何使用 SSH 远程控制一台 Windows 服务器

如何使用 SSH 远程控制一台 Windows 服务器 查了一下&#xff0c;Windows 上其实也是有 SSH 服务器的&#xff0c;只不过默认是没有装的&#xff0c;这里只需要安装一个 OpenSSH 服务器就好了。 Win10 的话&#xff0c;就在设置里面可以安装&#xff0c;从开始菜单打开“设置…

MYSQL表操作(DML,DDL)

建表并插入数据&#xff1a; mysql> create table worker(-> dept_id int(11) not null,-> emp_id int (11) not null,-> work_time date not null,-> salary float(8,2) not null,-> poli_face varchar(10) not null default 群众,-> name varchar(20) …

IIC的再认识

IIC介绍 关于IIC的基本概念&#xff0c;其实在学习89C52的时候已经大致了解过了&#xff0c;且由于STM32支持了IIC协议&#xff0c;所以在STM32中使用IIC可以直接调用HAL库的库函数&#xff1a; HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c,uint16_t DevAdd…

SAP ABAP 报表程序实现下载文件及上传 Excel 并解析

步骤1&#xff1a; 事务代码 SMW0 选择二进制数据选项点击上方按钮。 点击新建按钮输入名称和描述&#xff0c;上传模版文件。 案例传入 EXCEL 如下&#xff1a; 创建好资源库对象结果如下。 步骤2&#xff1a;报表效果展示 点击按钮选择上传的文件。 解析 Excel 文件结果…

酷开科技大屏营销,撬动营销新增量

5G、人工智能、元宇宙等技术的发展促使数字营销的内容、渠道、传播方式发生了一系列变化&#xff1b;存量竞争下&#xff0c;增长成为企业更加迫切、更具挑战的课题&#xff0c;品牌营销活动越来越围绕“生意增长”和“提效转化”的目标展开。 如今的市场环境下&#xff0c;产…

计算机毕业论文选题推荐|软件工程|信息管理|数据分析|系列一

文章目录 导文题目导文 计算机毕业论文选题推荐|软件工程|信息管理 (***语言)==使用其他任何编程语言 例如:基于(***语言)门窗账务管理系统的设计与实现 得到:基于JAVA门窗账务管理系统的设计与实现 基于vue门窗账务管理系统的设计与实现 等等 题目 基于requests多线程…

BERT系列算法解读:(RoBERTa/ALBERT/DistilBERT/Transformer/Hugging Face/NLP/预训练模型/模型蒸馏)

BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff0c;基于Transformers的双向编码器表示&#xff09;系列算法在自然语言处理任务中是必不可少的经典模型&#xff0c;当初第一代GPT模型发布的时候&#xff0c;坐了冷板凳&#xff0c;罪魁祸首…

【C++】-list的具体使用

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你 …

漏洞复现畅捷通CRM SQL注入

免责声明 术文章仅供参考,任何个人和组织使用网络应当遵守宪法法律,遵守公共秩序,尊重社会公德,不得利用网络从事危害国家安全、荣誉和利益,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失,均由使用…

.NET SqlSuger初次使用

文章目录 前言SqlSuger测试DB Fisrt和CodeFirst 前言 我之前在B站上面发视频说如何使用EF框架去生成实体类。我当时做了Mysql,Sql server,Sqlite的适配。但是下面评论区说SqlSuger很好用&#xff0c;而且很多公司都用SqlSuger。 B站视频&#xff1a;C#如何快速开发数据库业务…

VsCode连不上Server,但SSH可以

造成的原因可能是因为Client处Vscode更新与Server端"~/.vscode-server"文件夹内的版本不匹配&#xff0c;通常来说直接删除该文件夹即可。 但是有时该文夹内有几个问价被占用&#xff1a;“resource busy”,且该文件处于内核态&#xff0c; 通过&#xff1a;“lsof…

Nginx学习之一撸到底

一、Nginx环境搭建 ❶首先创建Nginx的目录并进入&#xff1a; [rootlocalhost]# mkdir /soft && mkdir /soft/nginx/ [rootlocalhost]# cd /soft/nginx/ ❷下载Nginx的安装包&#xff0c;可以通过FTP工具上传离线环境包&#xff0c;也可通过wget命令在线获取安装包…

Python与matlab 实现图像加密--扩散加密

1、图像加密 使用扩散加密 图像加密中,扩散处理是在不改变像素点位置的条件下,将任一明文像素点的信息隐藏在尽可能多的密文像素点中。 基于异或运算的扩散处理 正向(i从1到MN)的算法与其逆算法: 逆向(i从MN到1)的算法与其逆算法: C和S是密码向量,P是明文图像 Python…