从C语言到C++_13(string的模拟实现)深浅拷贝+传统/现代写法

news2024/11/24 15:19:07
前面已经对 string 类进行了简单的介绍和应用,大家只要能够正常使用即可。
在面试中,面试官总喜欢让学生自己 来模拟实现string类,
最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。
为了更深入学习STL,下面我们就自己来模拟实现一下string的常用接口函数:

目录

1.  string默认成员函数

1.1 构造和析构

1.2 深浅拷贝介绍

1.3 拷贝构造的实现

1.4 赋值的实现

1.5 写时拷贝(了解)

2. string 的部分函数实现

2.1 完整默认成员函数代码:

2.2 c_str() 的实现

2.3 全缺省构造函数的实现

2.4 size() 和 operator[] 的实现

3. string的迭代器

3.1 string迭代器的实现

3.2 迭代器和范围for再思考

4. string的增删查改函数实现

4.1 reserve() 的实现

4.2 push_back() 的实现

4.3 append() 的实现

4.4 operator+= 的实现

4.5 insert() 的实现

4.6 resize() 的实现

4.7 find() 的实现

4.8 erase() 的实现

5. 传统写法和现代写法

5.1 拷贝构造的现代写法

5.2 赋值重载的现代写法

6. operator 运算符重载

6.1 六个比较运算符重载

6.2 流插入和流提取重载

7. 完整代码:

string.h:

Test.c:

本章完。


1.  string默认成员函数

1.1 构造和析构

我们先试着来实现 string 的构造和析构:

整体框架:

string.h

#pragma once

#include<iostream>
#include<string>
#include<assert.h>
using namespace std;

namespace rtx
{
	class string
	{
	public:
		string(const char* s)
		{

		}

		~string() 
		{

		}
	private:
		char* _str;
	};

	void test_string1()
	{
		string s1("hello world");
	}
}

Test.c:

#include "string.h"

int main()
{
	try
	{
		rtx::test_string1();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

这里为了和原有的 string 进行区分,我们搞一个命名空间给它们括起来。

我们的测试就放在简单提到的try catch上,然后该序号就能测试了。

构造函数是这样写吗?这样写的话拷贝构造能直接用默认生成的吗

		string(const char* s)
			: _str(new char[strlen(s) + 1])// 开strlen大小的空间(多开一个放\0)
		{
				strcpy(_str, str);
		}

然后我们先实现析构,用 new[] 对应的 delete[] 来析构:

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

放到上面的框架:编译通过

此时我们改一下测试用例 test_string1,如果我们要用 s1 拷贝构造一下 s2:

 详细解析:

说明:上述string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用 s1 s2 时,编译器会调用默认的拷贝构造。最终导致的问题是, s1 s2 共用同一块内存空间,在释放时同一块 空间被释放多次而引起程序崩溃 这种拷贝方式,称为浅拷贝

1.2 深浅拷贝介绍

如何解决这样的问题呢?

我们 s2 拷贝构造你 s1,本意并不是想跟你指向一块空间!

我们的本意是想让 s2 有一块自己的空间,并且能使其内容是 s1 里的 hello world

这就是深拷贝。

 所以这里就涉及到了深浅拷贝的问题,我们下面就来探讨一下深浅拷贝的问题。

浅拷贝:(直接把内存无脑指过去) 也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

深拷贝(开一块一样大的空间,再把数据拷贝下来,指向我自己开的空间) 如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

1.3 拷贝构造的实现

 我们之前实现日期类的时候,用自动生成的拷贝构造(浅拷贝)是可以的,

所以当时我们不用自己实现拷贝构造,让它默认生成就足够了。

但是像 string 这样的类,它的拷贝构造我们不得不亲自写:

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

 这就实现了深拷贝。

1.4 赋值的实现

 现在有一个 s3,如果我们想把 s3 赋值给 s1:

	void test_string1()
	{
		string s1("hello world");
		string s2(s1);

		string s3("!!!");
		s1 = s3;
	}

如果你不自己实现赋值,就和之前一样,会是浅拷贝,也会造成崩溃。

所以,我们仍然需要自己实现一个 operator= ,首先思路如下:

		string& operator=(const string& s)
		{
			if (this != &s)
			{
				delete[] _str;// 释放原有空间
				_str = new char[s._capacity + 1];// 开辟新的空间
				strcpy(_str, s._str);// 赋值
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}

根据我们的实现思路,首先释放原有空间,然后开辟新的空间,

最后把 s3 的值赋值给 s1。为了防止自己给自己赋值,我们可以判断一下。

这时我们还要考虑一个难以发现的问题,如果 new 失败了怎么办?

抛异常!失败了没问题,也不会走到 strcpy,但问题是我们已经把原有的空间释放掉了,

走到析构那里二次释放可能会崩,所以我们得解决这个问题。

可以试着把释放原有空间的步骤放到后面:

		string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[s._capacity + 1];// 开辟新的空间
				strcpy(tmp, s._str);// 赋值到tmp
				delete[] _str;// 释放原有空间

				_str = tmp;// tmp赋值到想要的地方,出去tmp就销毁了
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}

这样一来,就算是动态内存开辟失败了,我们也不用担心出问题了。

这是更标准的实现方式,我们先去开辟空间,放到临时变量 tmp 中,tmp 翻车就不会执行下面的代码,tmp 没有翻车,再去释放原有的空间,最后再把 tmp 的值交付给 s1,

这是非常保险的,有效避免了空间没开成还把 s1 空间释放掉的 "偷鸡不成蚀把米" 的事发生。

1.5 写时拷贝(了解)

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成 1 ,每增加一个对象使用该资源,就给计数增加1 ,当某个对象被销毁时,先给该计数减 1 ,然后再检查是否需要释放资源,如果计数为 1 ,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其它对象在使用该资源。

写时拷贝技术实际上是运用了一个 “引用计数” 的概念来实现的。在开辟的空间中多维护四个字节来存储引用计数。
有两种方法:
①:多开辟四个字节(pCount)的空间,用来记录有多少个指针指向这片空间。
②:在开辟空间的头部预留四个字节的空间来记录有多少个指针指向这片空间。
  当我们多开辟一份空间时,让引用计数+1,如果有释放空间,那就让计数-1,但是此时不是真正的释放,是假释放,等到引用计数变为 0 时,才会真正的释放空间。如果有修改或写的操作,那么也让原空间的引用计数-1,并且真正开辟新的空间。

写时拷贝涉及多线程等不好的问题,所以了解一ha就行

2. string 的部分函数实现

刚才我们为了方便讲解深浅拷贝的问题,有些地方所以没有写全。

我们知道string有这几个接口函数:

我们实现只是实现常用的,且length和size是一样的,我们现在增加一些成员:

private:
	char*  _str;
	size_t _size;
	size_t _capacity;   // 有效字符的空间数,不算\0

2.1 完整默认成员函数代码:

#pragma once

#include<iostream>
#include<string>
#include<assert.h>
using namespace std;

namespace rtx
{
	class string
	{
	public:
		string(const char* s)
		{
			_size =strlen(s);// 因为要算多次strlen 效率低 且放在初始化列表关联到声明顺序 所以不用初始化列表
			_capacity = _size;
			_str = new char[_size + 1];// 开_size+1大小的空间(多开一个放\0)
			strcpy(_str, s);
		}

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

		string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[s._capacity + 1];// 开辟新的空间
				strcpy(tmp, s._str);// 赋值到tmp
				delete[] _str;// 释放原有空间

				_str = tmp;// tmp赋值到想要的地方,出去tmp就销毁了
				_size = s.size();
				_capacity = s._capacity;
			}
			return *this;
		}

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

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

	void test_string1()
	{
		string s1("hello world");
		string s2(s1);

		string s3("!!!");
		s1 = s3;
	}
}

2.2 c_str() 的实现

 c_str() 返回的是C语言字符串的指针常量,是可读不写的:

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

返回const char*,因为是可读不可写的,所以我们需要用 const 修饰。

c_str 返回的是当前字符串的首字符地址,这里我们直接 return _str 即可实现。

测试一下:

	void test_string1()
	{
		string s1("hello world");
		string s2(s1);

		string s3("!!!");
		s1 = s3;

		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
		cout << s3.c_str() << endl;
	}

2.3 全缺省构造函数的实现

还要考虑不带参的情况,比如下面的 s4:

	void test_string1()
	{
		string s1("hello world");
		string s2(s1);

		string s3("!!!");
		s1 = s3;

		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
		cout << s3.c_str() << endl;

		string s4;
	}

无参构造函数:

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

一般的类都是提供全缺省的,值得注意的是,这里缺省值给的是 " "

有人看到指针 char* 可能给缺省值一个空指针 nullptr:

string(const char* str = nullptr)

也就相当于直接对这个字符串进行解引用了,这里的字符串又是空,所以会引发空指针问题。

所以我们这里给的是一个空的字符串 " ",常量字符串默认就带有 \0,这样就不会出问题:

		string(const char* s  = "")
		{
			_size =strlen(s);// 因为要算多次strlen 效率低 且放在初始化列表关联到声明顺序 所以不用初始化列表
			_capacity = _size;
			_str = new char[_size + 1];// 开_size+1大小的空间(多开一个放\0)
			strcpy(_str, s);
		}

这样达到的效果和无参构造函数是一样的,且无参编译器不知道调用哪个,

所以我们就需把无参构造函数删了。

2.4 size() 和 operator[] 的实现

 size()的实现:

		size_t size() const
		{
			return _size;
		}

size() 只需要返回成员 _size 即可,考虑到不需要修改,我们加上 const。

operator[] 的实现:

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

直接返回字符串对应下标位置的元素,

因为返回的是一个字符,所以我们这里引用返回 char。

我们来测试一下,遍历整个字符串,这样既可以测试到 size() 也可以测试到 operator[] :

	void test_string2() 
	{
		string s1("hello world");
		string s2;

		for (size_t i = 0; i < s1.size(); i++) 
		{
			cout << s1[i] << " ";
		}
		cout << endl;

		s1[0] = 'x';
		for (size_t i = 0; i < s1.size(); i++)
		{
			cout << s1[i] << " ";
		}
		cout << endl;
	}

 普通对象可以调用,但是 const 对象呢?所以我们还要考虑一下 const 对象。

我们写一个 const 对象的重载版本:

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

因为返回的是 pos 位置字符的 const 引用,所以可读但不可写。

3. string的迭代器

在上上篇中,我们首次讲解迭代器,为了方便理解,我们当时解释其为像指针一样的类型。

实际上,有没有一种可能,它就是一种指针呢?

遗憾的是,迭代器并非指针,而是类模板。 只是它表现地像指针,模拟了指针的部分功能。

string迭代器的实现非常简单,它就是一个 char* 的指针罢了。

后面我们讲解 list 的时候它的迭代器又不是指针了,又是自定义类型了。

所以迭代器是一个像指针的东西,有可能是指针有可能不是指针。

3.1 string迭代器的实现

实现迭代器的 begin() 和 end() :

		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}

 const 迭代器就是可以读但是不可以写的迭代器,实现一下 const 迭代器:

		typedef const char* const_iterator;
		const_iterator begin() const 
		{
			return _str;
		}
		const_iterator end() const
		{
			return _str + _size;
		}

3.2 迭代器和范围for再思考

迭代器的底层是连续的物理空间,给原生指针++解引用能正好贴合迭代器的行为,能做到遍历。

但是对于链表和树型结构来说,迭代器的实现就没有这么简单了。

但是,强大的迭代器通过统一的封装,无论是树、链表还是数组……

它都能用统一的方式遍历,这就是迭代器的优势,也是它的强大之处。

我们上一章提到过范围for遍历string,我们能不能直接用在我们写的迭代器上:

	void test_string3()
	{
		string s1("hello world");

		string::iterator it = s1.begin();
		while (it != s1.end())
		{
			cout << *it << " ";// 读
			it++;
		}
		cout << endl;

		it = s1.begin();
		while (it != s1.end())
		{
			(*it)++;// 写,++的优先级比*高,+=就低,可以 *it += 1;
			cout << *it << " ";
			it++;
		}
		cout << endl;

		for (auto& e : s1)
		{
			cout << e << " ";
		}
		cout << endl;
	}

 我们也妹写范围 for 啊,怎么就能直接用了?

所以范围 for 根本就不需要自己实现,你只要把迭代器实现好,范围 for 直接就可以用。

范围 for 的本质是由迭代器支持的,编译时范围 for 会被替换成迭代器。

这么一看,又是自动加加,又是自动判断结束的范围 for,好像也没那么回事儿。

4. string的增删查改函数实现

4.1 reserve() 的实现

我们先实现一下 reserve 增容:

这里可以检查一下是否真的需要增容,万一接收的 new_capacity 比 _capacity 小,就不动。

		void reserve(size_t new_capacity)
		{
			if (new_capacity > _capacity)
			{
				char* tmp = new char[new_capacity + 1];// 开新空间
				strcpy(tmp, _str);// 搬运
				delete[] _str; // 释放原空间

				_str = tmp;// 没问题,递交给_str
				_capacity = new_capacity;// 更新容量
			}
		}

这里我们之前讲数据结构用的是 realloc,现在我们熟悉熟悉用 new,

 还是用申请新空间、原空间数据拷贝到新空间,再释放空间地方式去扩容。

我们的 _capacity 存储的是有效字符,没算 \0,所以这里还要 +1 为 \0 开一个空间。

4.2 push_back() 的实现

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

			_str[_size++] = ch;// 在_size位置放字符后++
			_str[_size] = '\0';// 易漏
		}

首先检查是否需要增容,如果需要就调用我们上面实现的 reserve 函数,

参数传递可以用三目操作符,防止容量是0的情况,0乘任何数都是0从而引发问题的情况。

测试一下效果如何:

4.3 append() 的实现

append 是追加字符串的,首先我们把要追加的字符串长度计算出来,

然后看容量够不够,不够我们就交给 reserve 去扩容,扩 _size + len,够用就行。

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

			strcpy(_str + _size, str);// 首字符+_size大小就是\0位置
			_size += len;
		}

这里我们甚至都不需要用 strcat,因为它的位置我们很清楚,不就在 _str + _size 后面插入吗。

用 strcat 还需要遍历找到原来位置的 \0,麻烦且效率低,strcat 函数我们以后尽量都不用。

4.4 operator+= 的实现

这就是我们一章说的 "用起来爽到飞起" 的 += ,因为字符和字符串都可以用 += 去操作。

所以我们需要两个重载版本,一个是字符的,一个是字符串的。

我们不需要自己实现了,直接复用 push_back 和 append 就好了:

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

测试:

	void test_string4() 
	{
		string s1("hello world");
		cout << s1.c_str() << endl;

		s1.push_back('!');
		cout << s1.c_str() << endl;

		s1.push_back('R');
		cout << s1.c_str() << endl;

		s1.append("abcd");
		cout << s1.c_str() << endl;

		s1 += 'e';
		s1 += "fgh";
		cout << s1.c_str() << endl;
	}

4.5 insert() 的实现

insert:字符

		string& insert(size_t pos, const char ch)
		{
			assert(pos < _size);
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			for (size_t i = _size + 1;i > pos; --i)// 挪动数据,+1是挪动\0
			{
				_str[i] = _str[i - 1];
			}

			_str[pos] = ch;
			++_size;
			return *this;
		}

insert:字符串

		string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			int len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

			for (size_t i = _size + len ;i > pos + len - 1; --i)// 挪动数据,画图注意边界,参考上面inser字符的len == 1
			{
				_str[i] = _str[i - len];// 首先看\0 _size+len-len就是\0的位置
			}

			strncpy(_str + pos, str, len);
			_size += len;
			return *this;
		}

 测试:

	void test_string4() 
	{
		string s1("hello world");
		cout << s1.c_str() << endl;

		s1.push_back('!');
		cout << s1.c_str() << endl;

		s1.push_back('R');
		cout << s1.c_str() << endl;

		s1.append("abcd");
		cout << s1.c_str() << endl;

		s1 += 'e';
		s1 += "fgh";
		cout << s1.c_str() << endl;

		s1.insert(0, 'x');
		s1.insert(6, 'T');
		cout << s1.c_str() << endl;

		s1.insert(6, "PPPPPPPPPP");
		cout << s1.c_str() << endl;
		s1.insert(0, "PPPPPPPPPP");
		cout << s1.c_str() << endl;
	}

 (测试后push_back 和 append 直接复用还能测试一波)

+=又是复用push_back 和 append 的,直接套娃开始:

		void reserve(size_t new_capacity)
		{
			if (new_capacity > _capacity)
			{
				char* tmp = new char[new_capacity + 1];// 开新空间
				strcpy(tmp, _str);// 搬运
				delete[] _str; // 释放原空间

				_str = tmp;// 没问题,递交给_str
				_capacity = new_capacity;// 更新容量
			}
		}

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

			_str[_size++] = ch;// 在_size位置放字符后++
			_str[_size] = '\0';// 易漏

			//insert(_size, ch);
		}

4.6 resize() 的实现

我们为了扩容,先实现了 reverse,现在我们再顺便实现一下 resize。

这里再提一下 reverse 和 resize 的区别:

resize 分给初始值和不给初始值的情况,所以有两种:

库里面也是这么实现的。

但是我们上面讲构造函数的时候说过,我们可以使用全缺省的方式,这样就可以二合一了。

resize 实现的难点是要考虑种种情况,我们来举个例子分析一下:

 如果欲增容量比 _size 小的情况:

因为标准库是没有缩容的,所以我们实现的时候也不考虑去缩容。我们可以加一个 \0 去截断。

如果预增容量比 _size 大的情况:

resize 是开空间 + 初始化,开空间的工作我们就可以交给已经实现好的 reserve,

然后再写 resize 的初始化的功能,我们这里可以使用 memset 函数。

		void resize(size_t new_capacity, const char ch = '\0')
		{
			if (new_capacity > _size)// 插入数据
			{
				reserve(new_capacity);
				//for (size_t i = _size; i < new_capacity; ++i)
				//{
				//	_str[i] = ch;
				//}
				memset(_str + _size, ch, new_capacity - _size);// 上面的for循环即memset的功能
				_str[new_capacity] = '\0';
				_size = new_capacity;
			}
			else// 删除数据
			{
				_str[new_capacity] = '\0';
				_size = new_capacity;
			}
		}

4.7 find() 的实现

 find:查找字符

如果遍历完整个字符串都没找到,就返回 npos(找到库的来)。

这个 npos 我们可以在成员变量中定义:

		size_t find(const char ch) const
		{
			for (size_t i = 0; i < _size; i++) 
			{
				if (ch == _str[i])// 找到了
				{
					return i;    // 返回下标
				}
			}
			return npos;// 找不到
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	public:
		const static size_t npos = -1;// const static 语法特殊处理,直接可以当成定义初始化

 find:查找字符串

这里我们可以用 strstr 去找子串,如果找到了,返回的是子串首次出现的地址。

如果没找到,返回的是空。所以我们这里可以做判断,如果是 nullptr 就返回 npos。

如果找到了,就返回对应下标,子串地址 - 开头,就是下标了。

		size_t find(const char* str, size_t pos = 0) const
		{
			const char* ptr = strstr(_str + pos, str);
			if (ptr == nullptr) 
			{
				return npos;
			}
			else 
			{
				return ptr - _str;  // 减开头
			}
		}

4.8 erase() 的实现

		string& erase(size_t pos, size_t len = npos) 
		{
			assert(pos < _size);
			if (len == npos || pos + len >= _size)// 如果pos后面的都删完了,注意len == npos 不能忽略,因为npos + len 有可能重回到 1
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}

测试find 和 erase:

5. 传统写法和现代写法

对于拷贝构造的深拷贝,传统写法就是本本分分分地去完成深拷贝:

		string(const string& s)/传统写法
			:_str(new char[s._capacity + 1])
			, _size(s._size)
			, _capacity(s._capacity)
		{
			strcpy(_str, s._str);
		}

5.1 拷贝构造的现代写法

现在我们来介绍一种现代写法,它和传统写法本质工作是一样的,即完成深拷贝。

现代写法的方式不是本本分分地去按着 Step 一步步干活,而是 "投机取巧" 地去完成深拷贝。

		void swap(string& s)// s和*this换
		{
			::swap(s._str, _str);//注意这里要加域作用符,默认是全局的,不然就是自己调自己了
			::swap(s._size, _size);
			::swap(s._capacity, _capacity);

		}
		string(const string& s)/现代写法
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			string tmp(s._str);
			swap(tmp);// tmp和*this换
		}

现代写法的本质就是复用了构造函数。

我想拷贝,但我又不想自己干,我把活交给工具人 tmp 来帮我干。

值得注意的是如果不给原_str赋空指针,那么它的默认指向会是个随机值。和tmp交换后,tmp 是一个局部对象,我们把 s2 原来的指针和 tmp 交换了,那么 tmp 就成了个随机值了。tmp 出了作用域要调用析构函数,对随机值指向的空间进行释放,怎么释放?都不是你自己的 new / malloc 出来的,你还硬要对它释放,就可能会引发崩溃。但是 delete / free 一个空,是不会报错的,因为会进行一个检查。所以是可以 delete 一个空的,我们这里初始化列表中把 nullptr 给 _str,是为了交换完之后, nullptr 能交到 tmp 手中,这样 tmp 出了作用域调用析构函数就不会翻车了。

5.2 赋值重载的现代写法

赋值重载的传统写法:

		string& operator=(const string& s)/传统写法
		{
			if (this != &s)
			{
				char* tmp = new char[s._capacity + 1];// 开辟新的空间
				strcpy(tmp, s._str);// 赋值到tmp
				delete[] _str;// 释放原有空间

				_str = tmp;// tmp赋值到想要的地方,出去tmp就销毁了
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}		

赋值重载的现代写法:

		string& operator=(const string& s)/现代写法
		{
			if (this != &s)
			{
				string tmp(s);
				swap(tmp);// tmp和*this换
			}
			return *this;
		}

比上面的拷贝构造的现代写法还要压榨tmp,交换完之后,正好让 tmp 出作用域调用析构函数,属实是一石二鸟的美事。把 tmp 压榨的干干净净,还让 tmp 帮忙把屁股擦干净(释放空间)。

用上面的测试简单测一下:

总结:

现代写法在 string 中体现的优势还不够大,因为好像和传统写法差不多。

 但是到后面实现 vector、list 的时候,会发现现代写法的优势真的是太大了。

现代写法写起来会更简单些,比如如果是个链表,传统写法就不是 strcpy 这么简单的了,

你还要一个一个结点拷贝过去,但是现代写法只需要调用 swap 交换一下就可以了。

现代写法更加简洁,只是在 string 这里优势体现的不明显罢了,我们后面可以慢慢体会。

6. operator 运算符重载

6.1 六个比较运算符重载

学日期类的时候就说过,只需实现 > 和 ==,剩下的都可以复用解决:

而且> 和 ==可以直接用strcmp ,剩下的复用,你不想用strcmp时剩下的也不用改了:

	    bool operator>(const string& s) const
		{
			return strcmp(_str, s._str) > 0;
		}
		bool operator==(const string& s) const
		{
			return strcmp(_str, s._str) == 0;
		}		
		bool operator>=(const string& s) const// 养成this指针写在前面的习惯
		{
			return *this > s || *this == s;
		}
		bool operator<(const string& s) const
		{
			return !(*this >= s);
		}		
		bool operator<=(const string& s) const
		{
			return !(*this > s);
		}		
		bool operator!=(const string& s) const
		{
			return !(*this == s);
		}

6.2 流插入和流提取重载

 我们当时实现日期类的流插入和流提取时,也详细讲过这些,当时讲解了友元。

 在友元那一章我们说过 "占参问题" ,这里就不再多做解释了。

如果我们重载成成员函数,第一个位置就会被隐含的 this 指针占据。

这样实现出来的流插入必然会不符合我们的使用习惯,所以我们选择在全局实现。

在全局里不存在隐含的 this 指针了。

而且我们已经有operator [ ] 可以访问私有成员了,所以不需要设置成友元函数:

流插入很简单:

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

但是流提取是这么简单吗?下面的代码有什么问题?:

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

我们发现这样输入空格和换行也终止不了程序,因为cin会自动忽略空格和换行。

有什么办法?cin有一个get的成员函数,可以获取每一个字符,现在我们查下文档会用就行,

后面我们还会详细的讲解IO流,流提取普通实现:

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

这样实现的流提取有一个缺陷:频繁的 += 效率低,能想到什么办法优化?

以下是类似库里面的实现:(思路类似缓冲区)

	istream& operator>>(istream& in, string& s)// 流插入优化(类似库里面的)
	{
		char ch;
		ch = in.get();

		const size_t N = 32;
		char buff[N];// C++11支持的变长数组
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == N - 1)// 如果buff的容量满了
			{
				buff[i] = '\0';// 在后面放\0,
				s += buff;// += 到 s 上
				i = 0;// 把 i 重新变成0 用来再次使用buff数组
			}
			ch = in.get();
		}
		buff[i] = '\0';// 处理一下buff剩余的
		s += buff;

		return in;
	}

简单测试下:

	void test_string6()
	{
		string s1;
		string s2;
		cin >> s1 >> s2;
		cout << s1 << endl << s2 << endl;

		cout << (s1 > s2) << endl;
		cout << (s1 == s2) << endl;
		cout << (s1 >= s2) << endl;
		cout << (s1 < s2) << endl;
		cout << (s1 <= s2) << endl;
		cout << (s1 != s2) << endl;
	}

7. 完整代码:

string.h:

#pragma once

#include<iostream>
#include<string>
#include<assert.h>
using namespace std;

namespace rtx
{
	class string
	{
	public:
		string(const char* s  = "")
		{
			_size =strlen(s);// 因为要算多次strlen 效率低 且放在初始化列表关联到声明顺序 所以不用初始化列表
			_capacity = _size;
			_str = new char[_size + 1];// 开_size+1大小的空间(多开一个放\0)
			strcpy(_str, s);
		}

		//string(const string& s)/传统写法
		//	:_str(new char[s._capacity + 1])
		//	, _size(s._size)
		//	, _capacity(s._capacity)
		//{
		//	strcpy(_str, s._str);
		//}
		void swap(string& s)// s和*this换
		{
			::swap(s._str, _str);//注意这里要加域作用符,默认是全局的,不然就是自己调自己了
			::swap(s._size, _size);
			::swap(s._capacity, _capacity);

		}
		string(const string& s)/现代写法
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			string tmp(s._str);
			swap(tmp);// tmp和*this换
		}

		//string& operator=(const string& s)/传统写法
		//{
		//	if (this != &s)
		//	{
		//		char* tmp = new char[s._capacity + 1];// 开辟新的空间
		//		strcpy(tmp, s._str);// 赋值到tmp
		//		delete[] _str;// 释放原有空间

		//		_str = tmp;// tmp赋值到想要的地方,出去tmp就销毁了
		//		_size = s._size;
		//		_capacity = s._capacity;
		//	}
		//	return *this;
		//}		
		string& operator=(const string& s)/现代写法
		{
			if (this != &s)
			{
				string tmp(s);
				swap(tmp);// tmp和*this换
			}
			return *this;
		}

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

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

		size_t size() const
		{
			return _size;
		}

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

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

		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}

		typedef const char* const_iterator;
		const_iterator begin() const 
		{
			return _str;
		}
		const_iterator end() const
		{
			return _str + _size;
		}

		void reserve(size_t new_capacity)
		{
			if (new_capacity > _capacity)
			{
				char* tmp = new char[new_capacity + 1];// 开新空间
				strcpy(tmp, _str);// 搬运
				delete[] _str; // 释放原空间

				_str = tmp;// 没问题,递交给_str
				_capacity = new_capacity;// 更新容量
			}
		}

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

			_str[_size++] = ch;// 在_size位置放字符后++
			_str[_size] = '\0';// 易漏

			//insert(_size, ch);
		}

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

			strcpy(_str + _size, str);// 首字符+_size大小就是\0位置
			_size += len;

			//insert(_size, str);
		}

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

		string& insert(size_t pos, const char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			for (size_t i = _size + 1;i > pos; --i)// 挪动数据,+1是挪动\0
			{
				_str[i] = _str[i - 1];
			}

			_str[pos] = ch;
			++_size;
			return *this;
		}
		string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			int len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

			for (size_t i = _size + len ;i > pos + len - 1; --i)// 挪动数据,画图注意边界,参考上面inser字符的len == 1
			{
				_str[i] = _str[i - len];// 首先看\0 _size+len-len就是\0的位置
			}

			strncpy(_str + pos, str, len);
			_size += len;
			return *this;
		}

		void resize(size_t new_capacity, const char ch = '\0')
		{
			if (new_capacity > _size)// 插入数据
			{
				reserve(new_capacity);
				//for (size_t i = _size; i < new_capacity; ++i)
				//{
				//	_str[i] = ch;
				//}
				memset(_str + _size, ch, new_capacity - _size);// 上面的for循环即memset的功能
				_str[new_capacity] = '\0';
				_size = new_capacity;
			}
			else// 删除数据
			{
				_str[new_capacity] = '\0';
				_size = new_capacity;
			}
		}

		size_t find(char ch) const
		{
			for (size_t i = 0; i < _size; i++) 
			{
				if (ch == _str[i])// 找到了
				{
					return i;    // 返回下标
				}
			}
			return npos;// 找不到
		}
		size_t find(const char* str, size_t pos = 0) const
		{
			const char* ptr = strstr(_str + pos, str);
			if (ptr == nullptr) 
			{
				return npos;
			}
			else 
			{
				return ptr - _str;  // 减开头
			}
		}

		string& erase(size_t pos, size_t len = npos) 
		{
			assert(pos < _size);
			if (len == npos || pos + len >= _size)// 如果pos后面的都删完了,注意len == npos 不能忽略,因为npos + len 有可能重回到 1
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}

		bool operator>(const string& s) const
		{
			return strcmp(_str, s._str) > 0;
		}
		bool operator==(const string& s) const
		{
			return strcmp(_str, s._str) == 0;
		}		
		bool operator>=(const string& s) const// 养成this指针写在前面的习惯
		{
			return *this > s || *this == s;
		}
		bool operator<(const string& s) const
		{
			return !(*this >= s);
		}		
		bool operator<=(const string& s) const
		{
			return !(*this > s);
		}		
		bool operator!=(const string& s) const
		{
			return !(*this == s);
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	public:
		const static size_t npos = -1;// const static 语法特殊处理,直接可以当成定义初始化
	};

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

	//istream& operator>>(istream& in, string& s)// 流插入普通实现
	//{
	//	char ch;
	//	ch = in.get();
	//	while (ch != ' ' && ch != '\n')
	//	{
	//		s += ch;
	//		ch = in.get();
	//	}
	//	return in;
	//}
	istream& operator>>(istream& in, string& s)// 流插入优化(类似库里面的)
	{
		char ch;
		ch = in.get();

		const size_t N = 32;
		char buff[N];// C++11支持的变长数组
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == N - 1)// 如果buff的容量满了
			{
				buff[i] = '\0';// 在后面放\0,
				s += buff;// += 到 s 上
				i = 0;// 把 i 重新变成0 用来再次使用buff数组
			}
			ch = in.get();
		}
		buff[i] = '\0';// 处理一下buff剩余的
		s += buff;

		return in;
	}

	void test_string1()
	{
		string s1("hello world");
		string s2(s1);

		string s3("!!!");
		s1 = s3;

		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
		cout << s3.c_str() << endl;

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

		for (size_t i = 0; i < s1.size(); i++) 
		{
			cout << s1[i] << " ";
		}
		cout << endl;

		s1[0] = 'x';
		for (size_t i = 0; i < s1.size(); i++)
		{
			cout << s1[i] << " ";
		}
		cout << endl;
	}

	void test_string3()
	{
		string s1("hello world");

		string::iterator it = s1.begin();
		while (it != s1.end())
		{
			cout << *it << " ";// 读
			it++;
		}
		cout << endl;

		it = s1.begin();
		while (it != s1.end())
		{
			(*it)++;// 写,++的优先级比*高,+=就低,可以 *it += 1;
			cout << *it << " ";
			it++;
		}
		cout << endl;

		for (auto& e : s1)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void test_string4() 
	{
		string s1("hello world");
		cout << s1.c_str() << endl;

		s1.push_back('!');
		cout << s1.c_str() << endl;

		s1.push_back('R');
		cout << s1.c_str() << endl;

		s1.append("abcd");
		cout << s1.c_str() << endl;

		s1 += 'e';
		s1 += "fgh";
		cout << s1.c_str() << endl;

		s1.insert(0, 'x');
		s1.insert(6, 'T');
		cout << s1.c_str() << endl;

		s1.insert(6, "PPPPPPPPPP");
		cout << s1.c_str() << endl;
		s1.insert(0, "PPPPPPPPPP");
		cout << s1.c_str() << endl;

		s1.resize(100,'x');
		cout << s1.c_str() << endl;
	}

	void test_string5()
	{
		string s1("hello world");
		string s2(s1);
		string s3 = s1;
		cout << s2.c_str() << endl << s3.c_str() << endl;

		cout << s1.find('d') << endl;// 打印d的下标:6
		cout << s1.find("world") << endl;// 打印了w的下标:10
		cout << s1.find("wold") << endl;// 打印了npos:4294967295
		cout << s1.find("world", 9) << endl;// 打印了npos:4294967295

		s1.erase(9, 2);// 从下标9开始删除2个字符
		cout << s1.c_str() << endl;

		s1.erase(s1.find('o'), 2);// 找到o,其下标为4,从下标4开始删除2个字符
		cout << s1.c_str() << endl;

		s1.erase(5);// 从下标5开始删完
		cout << s1.c_str() << endl;
	}

	void test_string6()
	{
		string s1;
		string s2;
		cin >> s1 >> s2;
		cout << s1 << endl << s2 << endl;

		cout << (s1 > s2) << endl;
		cout << (s1 == s2) << endl;
		cout << (s1 >= s2) << endl;
		cout << (s1 < s2) << endl;
		cout << (s1 <= s2) << endl;
		cout << (s1 != s2) << endl;
	}
}

Test.c:

#define _CRT_SECURE_NO_WARNINGS 1

#include "string.h"

int main()
{
	try
	{
		rtx::test_string6();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

本章完。

这篇博客两万多个字了......刚开始接触确实有点累der

不过STL的实现都是类似的,以后的学习就轻松多了,

下一步:了解vector的接口函数,写写vector的OJ题,模拟实现vector。

想笑~ 来伪装掉下的眼泪~

23年5月27,早起把博客发出去,我又行了。

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

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

相关文章

JetBrains的Python集成开发环境Pycharm 2023版本在Win10系统的下载与安装配置教程

目录 前言一、PyCharm安装二、使用配置总结 前言 PyCharm是一款专为Python语言开发人员设计的集成开发环境&#xff08;IDE&#xff09;。它提供了丰富的功能和工具&#xff0c;可以帮助开发人员更高效地编写、调试和部署Python应用程序。 PyCharm的主要特点&#xff1a; —…

chatgpt赋能python:Python按键的SEO优化

Python按键的SEO优化 Python是一种广泛使用的编程语言之一&#xff0c;它在机器学习、数据科学和网络编程等领域中广泛应用。在Python中&#xff0c;按键是一个重要的概念&#xff0c;它可以让你控制程序的流程。在本文中&#xff0c;我们将介绍Python中按键的基本概念&#x…

JetBrains的Python集成开发环境Pycharm 2023版本在Linux系统的下载与安装配置教程

目录 前言一、PyCharm安装二、使用配置总结 前言 PyCharm是一款专为Python语言开发人员设计的集成开发环境&#xff08;IDE&#xff09;。它提供了丰富的功能和工具&#xff0c;可以帮助开发人员更高效地编写、调试和部署Python应用程序。注&#xff1a;已在CentOS7.9和Ubuntu…

vue3的api解读-VUE Reactivity

目录 响应式 命令式程序 vs 响应式程序 Be Reactive&#xff1a;将响应式的值绑定到UI 代理模式&#xff08;CODING&#xff09; /src/App.tsx /src/main.ts /vite.config.ts /src/demo/ProxyExample.ts Reactivity是Vue3新加的一个库&#xff0c;新的package 响应式 …

【软考系统规划与管理师笔记】第2篇 信息技术知识1

目录 1 软件工程 1.1 软件需求分析与定义 1.2 软件设计、测试与维护 1.3 软件质量保证及质量评价 1.4 软件配置管理 1.5 软件过程管理 1.6 软件复用 2 面向对象系统分析与设计 2.1 面向对象设计的基本概念 2.2统一建模语言与可视化建模 3. 应用集成技术 3.1 数据库与…

低代码平台中的分布式 RPC 框架 (约 3000 行代码)

RPC 是分布式系统设计中不可或缺的一个部分。国内开源的 RPC 框架很多&#xff0c;它们的设计大都受到了 dubbo 框架的影响&#xff0c;核心的抽象概念与 dubbo 类似。从今天的角度上看&#xff0c;dubbo 的设计已经过于繁琐冗长&#xff0c;如果基于现在的技术环境&#xff0c…

Go语言WEB开发[html/template包]

Go语言Web开发 Go语言提供了html/template包来支持模板渲染。Go提供的html/template包对HTML模板提供了丰富的模板语言&#xff0c;主要用于Web应用程序。 模板中的变量 模板中的变量通过{{.}} 来访问。 {{.}} 称为管道和root。 在模板文件内&#xff0c;{{.}}代表当前变量&…

【ROS】ROS2命令行工具详解

1、简介 ROS1中每个功能都使用ros开头的命令行工具&#xff0c;如&#xff1a;roscore、rosrun等 ROS2中只有一个命令行工具ros2&#xff0c;各个功能模块通过参数子命令来实现 ~$ ros2 -h 用法: ros2 [-h] [--use-python-default-buffering] <command> ……选项:-h, …

二叉树的认识

愚昧将使你达不到任何成果&#xff0c;并在失望和忧郁之中自暴自弃。 --达芬奇 目录 &#x1f341;一.二叉树的概念 &#x1f341;二.二叉树的特点&#xff0c;结构 &#x1f341;三.三种特殊的二叉树 &#x1f341;1.斜树 &#x1f341;2.满二叉树 …

Learning C++ No.24 【哈希/散列实战】

引言&#xff1a; 北京时间&#xff1a;2023/5/20/7:30&#xff0c;周六&#xff0c;可惜有课&#xff0c;而且还是早八&#xff0c;说明我们现在没有多少的学习时间啦&#xff01;得抓紧把该博客的引言给写完&#xff0c;我们距离期末考越来越近啦&#xff01;再过一个星期就…

客服系统搭建,这些技能你必须知道

作为客服系统领域的专家&#xff0c;我来为大家介绍一些客服系统搭建的技能。在本文中&#xff0c;你将了解到如何搭建一个高效的客服系统以及如何应对可能出现的问题。 选择合适的客服系统 在选择客服系统时&#xff0c;需要考虑以下几个因素&#xff1a; 处理的工单量用户…

《人月神话》译文修订明细(3)-读者可以对照修改

《人月神话》译文修订明细&#xff08;1&#xff09;-读者可以对照修改 《人月神话》译文修订明细&#xff08;2&#xff09;-读者可以对照修改 《人月神话》译文修订如下&#xff0c;读者可以对照自己手上的书修改。 相关阅读 这回真要动刀子-征集《人月神话》中译本的翻译…

java开发学习框架

Java基础 1.1. Java简介与安装 1.2. Java基本语法 1.3. 数据类型与变量 1.4. 运算符与表达式 1.5. 流程控制&#xff08;分支与循环&#xff09; 1.6. 数组 面向对象编程 2.1. 类与对象 2.2. 继承与多态 2.3. 接口与抽象类 2.4. 封装与访问控制 2.5. 重载与覆盖 2.6. Java内存管…

【面试题】 ES6中将非数组转换为数组的三种方法

大厂面试题分享 面试题库 前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 web前端面试题库 VS java后端面试题库大全 我们常常想使用数组的方法&#xff0c;比如forEach&#xff0c;filter&#xff0c;又或者so…

ChatGPT、Java 8 文档、MySQL都说 JDBC 没必要 `Class.forName()`,结果报错了……

文章目录 回顾 Tomcat 部署 WAR 应用报错找不到数据库驱动的问题ChatGPT、Javadoc 和 MySQL 驱动都说没必要 Class.forName()实验创建一个最小复现问题的 Demo不调用 Class.forName("com.mysql.cj.jdbc.Driver")调用 Class.forName("com.mysql.cj.jdbc.Driver&q…

chatgpt赋能python:Python的输出功能及其应用

Python的输出功能及其应用 Python是一种高级编程语言&#xff0c;它具有动态类型的解释性能力&#xff0c;是一种简洁、易于学习和易于阅读的编程语言。Python拥有非常强大的输出功能&#xff0c;使得开发者可以以多种形式输出数据结果&#xff0c;这对于数据分析、数据处理和…

人工智能值不值得学习?人工智能就业方向及前景

人工智能值不值得学习? 一、人工智能值得学吗&#xff1f; 很多同学想要知道人工智能值得学吗&#xff1f;小编认为是值得的&#xff0c;具体原因有以下两点&#xff1a; 1、人工智能专业前景好&#xff0c;但人才紧缺 根据人工智能行业的专家预计&#xff0c;到2020年&am…

华为CE12808/S9700交换机istack/CSS堆叠主备倒换操作命令步骤

一、华为CE12808交换机&#xff0c;istack堆叠状态 1、设备型号&#xff1a; 交换机一&#xff1a; HUAWEI CE12808 交换机二&#xff1a; HUAWEI CE12808 2、istack堆叠主备倒换操作步骤&#xff1a; 2.1、设备当前配置保存并进行备份。 2.2、切换所用命令。 执行命令display…

红黑树的插入。

一&#xff0c;一颗红黑树满足的性质 ①每个结点或是红色&#xff0c;或是黑色。 ②根结点是黑色的。 ③叶结点&#xff08;虚构的外部节点NULL结点&#xff09;都是黑色的。 ④不存在两个相邻的红结点。 ⑤对每个结点&#xff0c;从该结点到任一结点的简单路径上&#xff0c;…

【JavaSE】Java基础语法(二十):多态

文章目录 1. 多态的概述2 .多态中的成员访问特点3. 多态的好处和弊端4. 多态中的转型5. 多态中转型存在的风险和解决方案 (应用)6. 多态的实用价值 1. 多态的概述 什么是多态 同一个对象&#xff0c;在不同时刻表现出来的不同形态 多态的前提 要有继承或实现关系要有方法的重写…