【C++初阶】模拟实现string的常见操作

👦个人主页:@Weraphael

✍🏻作者简介:目前学习C++和算法

✈️专栏:C++航路

🐋 希望大家多多支持,咱一起进步!😁

如果文章对你有帮助的话

欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


目录

一、准备工作

为了方便管理代码,分两个文件来写:

  • Test.cpp - 测试代码逻辑
  • string.h - 模拟实现string

二、string的结构

我们知道,string是一个管理字符数组的类,底层其实就是一个支持动态增长的字符数组,就像数据结构学的动态顺序表。

cpp 复制代码
namespace wj
{
	class string
	{
	public:

	private:
		char* _str; // 动态字符数组
		size_t _size; // 字符个数
		size_t _capacity; // 容量(不包含'\0')
	};
}

string类的成员变量有三个,一个字符指针_str指向开辟的动态数组,_size标识有效数据个数(不包含'\0'),_capacity记录容量的大小(不包含'\0'

还需要注意的是:我们新命名了命名空间域wj,就是避免和库中的string产生冲突。

三、模拟实现常见初始化操作

3.1 用C字符串构造

cpp 复制代码
namespace wj
{
	class string
	{
	public:
		// 用C字符串构造
		string(const char* s)
			:_str(new char[strlen(s) + 1]) // +1是为'\0'
			, _size(strlen(s)) // _size不包含'\0'
			, _capacity(_size)
		{
			// 拷贝数据
			memcpy(_str, s, _size + 1);
			// +1是为了拷贝'\0'
		}

	private:
		char* _str; // 动态字符数组
		size_t _size; // 字符个数
		size_t _capacity; // 容量
	};
}

以上代码有个易错点:要注意初始化列表的顺序,是按照成员变量的顺序来赋值的!

为了验证代码的正确性,需要打印出结果。由于自己模拟实现的string,还没有实现重载流插入<< ,所以不能直接打印string对象,而流插入<<是可以打印内置类型的。因此string是有提供转为内置类型的接口s_str

cpp 复制代码
const char* c_str() const
{
	return _str;
}

为什么会在函数后加个const ?在往期博客我们讲过:只要成员函数内部不修改成员变量,都应该加上const

接下来来测试代码:

【Test.cpp】

3.2 无参构造(默认构造)

无参构造的结果就是空字符,默认是有'\0'

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

【Test.cpp】

3.3 拷贝构造

自定义类型的拷贝必须先调用拷贝构造函数。但如果不手动编写,编译器会默认生成一个浅拷贝/值拷贝的拷贝构造函数,即将所有成员变量逐一拷贝到新对象中,这种拷贝方式对于基本数据类型或者是自定义类型的成员变量来说是没有问题的。但是,如果类中有动态分配内存的指针变量,则需要手动编写深拷贝的拷贝构造函数。

cpp 复制代码
// string s1("hello world");
// string s2(s1); // 拷贝构造
// 这里的str是s1的别名,其实就是s1

string(const string& str)
{
	// 1. 开一个和s1一样大的空间
	_str = new char[str._capacity + 1];
	// 2. 将s1的数据拷贝给s2
	memcpy(_str, str._str, str._size + 1);
	_size = str._size;
	_capacity = str._capacity;
}

注意:以上代码其实隐藏了一个this指针,这个this指针其实就是s2,因此以上代码本质就是:

cpp 复制代码
string(const string& str)
{
	// 1. 开一个和s1一样大的空间
	this->_str = new char[str._capacity + 1];
	// 2. 将s1的数据拷贝给s2
	memcpy(this->_str, str._str, str._size + 1);
	this->_size = str._size;
	this->_capacity = str._capacity;
}

但是高手是不会把this写出来的hh

【Test.cpp】

四、析构函数

由于成员变量含有动态内存开辟的空间,因此要手动写出析构函数

cpp 复制代码
~string()
{
	delete[] _str;
	_str = nullptr;
	_size = 0;
	_capacity = 0;
}

五、模拟实现常见遍历操作

5.1 下标访问[]

cpp 复制代码
size_t size() const 
{
	return _size;
}

// 可读可写
// 可以引用返回:因为返回的对象是在堆上的,出了此作用域不会被销毁
char& operator[](size_t pos)
{
	// 断言,防止越界
	assert(pos >= 0 && pos < _size); 
	return _str[pos];
}

// 可读不可写
const char& operator[](size_t pos) const 
{
	assert(pos >= 0 && pos < _size);
	return _str[pos];
}

【Test.cpp】

5.2 迭代器

string的迭代器iterator本质就是一个char*的指针。

cpp 复制代码
// 可读可写
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;
}

【Test.cpp】

在往期博客我们将过,范围for的底层就是迭代器 ,因此也可以使用范围for:

六、尾插

6.1 push_back - 尾插一个字符 + reserve - 扩容

要尾插字符之前,需要考虑当前_size是否大于_capacity,如果小于,则不用扩容;如果大于,则需要扩容。因此,string库里同样也提供了扩容操作:

注意:reserve一般不会缩容

cpp 复制代码
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倍扩容
		// 如果是空串,扩了2倍容量还是0,因此要考虑容量为0的情况
		reserve(_capacity == 0 ? 4 : _capacity * 2); 
	}
	_str[_size] = ch;
	_size++;
	_str[_size] = '\0';
}

【Test.cpp】

6.2 append - 尾插字符串

cpp 复制代码
string& append(const char* s)
{
	// 可能存在扩容
	size_t n = strlen(s);
	if (_size + n > _capacity)
	{
		// 至少扩容_size+n
		reserve(_size + n);
	}
	memcpy(_str + _size, s, n + 1);
	_size += n;

	return *this;
}

【Test.cpp】

6.3 运算符重载+= - push_back和append升级版

它既可以尾插一个字符,还可以尾插字符串 。因此,直接复用push_backappend即可

cpp 复制代码
// +=字符串
string& operator+=(const char* s)
{
	append(s);
	return *this;
}

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

【Test.cpp】

七、插入操作insert

  • . pos位置插入n个字符

【思路】

  1. 首先要判断下标的合法性。
  2. 其次还要判断插入的字符加上原有的字符是否超过当前容量,超过就扩容。
  3. 然后就是挪动数据和插入数据。注意挪动数据一定要从最后一个字符'\0'开始挪动;不能从pos位置开始挪,否则后面的内容就被覆盖了。以下是动图展示
cpp 复制代码
// pos - 下标
// n - 插入的字符个数
// x - 插入的字符
void insert(size_t pos, size_t n, char x) 
{
	// 判断下标pos的合法性
	assert(pos >= 0 && pos <= _size);

	// 可能存在扩容
	if (_size + n > _capacity)
	{
		reserve(_size + n);
	}

	// 挪动数据
	size_t end = _size;
	while (end >= pos)
	{
		_str[end + n] = _str[end];
		end--;
	}
	
	// 插入数据
	for (size_t i = 0; i < n; i++)
	{
		_str[pos + i] = x;
	}
	_size += n;
}

通过思路分析,不难可以写出以上代码。但是以上代码有一个bug,当头插时,程序就崩溃了(如下图)

通过走读代码我们发现,posend的类型是都是size_t。因此end最后自减到-1,由于类型是size_t,而无符号的-1是一个相当大的数,循环条件成立就会一直死循环下去。

因此这里有两种方法:

第一种:将posend的类型全部改为int

cpp 复制代码
void insert(int pos, size_t n, char x) 
{
	// 判断下标pos的合法性
	assert(pos >= 0 && pos <= _size);

	// 可能存在扩容
	if (_size + n > _capacity)
	{
		reserve(_size + n);
	}

	// 挪动数据
	int end = _size;
	while (end >= pos)
	{
		_str[end + n] = _str[end];
		end--;
	}
	// 插入数据
	for (int i = 0; i < n; i++)
	{
		_str[pos + i] = x;
	}
	_size += n;
}

以上这种方法虽然可以,但是和库里提供的类型是有所差别的,因此还是有些不好。

第二种:既然size_t类型的end自减到-1就会死循环,那么加个end != -1不就完事了。恰好,string库里提供了公共静态成员常量npos,这个常量使用值-1定义。

所以,最终代码如下:

cpp 复制代码
namespace wj
{
	class string
	{
	public:
		void insert(size_t pos, size_t n, char x) 
		{
			// 判断下标pos的合法性
			assert(pos >= 0 && pos <= _size);

			// 可能存在扩容
			if (_size + n > _capacity)
			{
				reserve(_size + n);
			}

			// 挪动数据
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				_str[end + n] = _str[end];
				end--;
			}
			// 插入数据
			for (size_t i = 0; i < n; i++)
			{
				_str[pos + i] = x;
			}
			_size += n;
		}
		
	private:
		char* _str; // 动态字符数组
		size_t _size; // 字符个数
		size_t _capacity; // 容量

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

需要注意的是:静态成员需要在类外定义

【Test.cpp】

  • pos位置插入字符串

思路类似,这里就不过多赘述了

cpp 复制代码
void insert(size_t pos, const char* str)
{
	// 判断下标pos的合法性
	assert(pos <= _size);
	
	// 可能存在扩容
	size_t length = strlen(str);
	if (_size + length > _capacity)
	{
		reserve(_size + length);
	}
	// 挪动数据
	size_t end = _size;
	while (end >= pos && end != npos)
	{
		_str[end + length] = _str[end];
		end--;
	}
	// 插入数据
	for (size_t i = 0; i < length; i++)
	{
		_str[pos + i] = str[i];
	}
	_size += length;
}

【Test.cpp】

八、删除操作erase

【思路】

  • 首先要检查下标的合法性
  • 删除要分情况讨论:
    第一种:当前下标往后的字符全都需要删除

    第二种:删除的字符是符合范围内的
cpp 复制代码
string& erase(size_t pos = 0, size_t len = npos)
{
	// 检查下标合法性
	assert(pos >= 0 && pos < _size);

	if (len == npos || len + pos >= _size)
	{
		// 说明pos(包括pos)后面的字符要全部删完
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		size_t end = pos + len;
		while (end <= _size)
		{
			_str[pos] = _str[end];
			pos++;
			end++;
		}
		_size -= len;
	}
	return *this;
}

【Test.cpp】

九、查找操作find

  • 查找字符
cpp 复制代码
// 从下标pos开始查找字符,如果实参不传第二个参数,默认从下标为0开始查找
size_t find(char x, size_t pos = 0)  const
{
	// 检查查找下标的合法性
	assert(pos >= 0 && pos < _size);
	// 遍历查找即可
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == x)
		{
			// 找到返回下标
			return i;
		}
	}
	// 没找到默认返回npos,其实就是-1
	return npos;
}

【Test.cpp】

  • 查找字符串

查找子串可以有很多方法,最简便就是使用C语言中的strstr函数

cpp 复制代码
size_t find(const char* str, size_t pos = 0) const
{
	// 检查查找下标的合法性
	assert(pos >= 0 && pos < _size);

	//strstr(const char * str1, const char * str2) - 从str1中查找str2
	const char* ptr = strstr(_str + pos, str);
	// 如果ptr为空说明没找到
	if (ptr == nullptr)
	{
		// 找不到返回npos
		return npos;
	}
	// 否则找到了
	else
	{
		// 指针-指针 --- 返回的是元素个数
		return ptr - _str;
	}
}

【Test.cpp】

十、截取操作substr

cpp 复制代码
// pos - 截取的下标
// len - 截取的长度
// npos - 截取的最大长度
string substr(size_t pos = 0, size_t len = npos) const
{
	// 判断下标的合法性
	assert(pos >= 0 && pos < _size);

	// 分两种情况,
	// 1. 可能需要截到尾
	// 2. 可能需要截取一部分
	
	size_t n = len;
	// 截取到尾的情况
	if (len == npos || pos + len >= _size)
	{
		n = _size - pos;
	}

	string tmp;
	tmp.reserve(n);
	for (size_t i = pos; i < pos + n; i++)
	{
		// 将截取的字符全部放去tmp
		tmp += _str[i];
	}
	return tmp;
}

注意:对象tmp并不是直接返回,返回的过程会中间会生成一个临时变量。因此,tmp在返回时会调用拷贝构造(深拷贝)。如果没有写深拷贝,程序会崩溃,因为tmp出了作用域就会调用析构函数销毁空间,这也就导致临时变量指向的空间被销毁。

十一、改变字符串的有效个数resize

三种情况:

  • n小于size,相当于删除数据,保留n个字符
  • n等于size,则保留原数据
  • n大于size,则会扩容,同时后面会补充n - size个字符(不指定默认是'\0'
cpp 复制代码
void resize(size_t n, char ch = '\0')
{
	// 相当于删除数据,只保留前n个字符
	if (n < _size) 
	{
		_size = n;
		_str[_size] = '\0';
	}
	
	else // 否则相当于插入数据
	{
	  	// 可能有扩容情况,n比_capacity大才会扩容,n=_capacity什么也不发生
		reserve(n);
		for (size_t i = _size; i < n; i++) // 填数据
		{
			_str[i] = ch;
		}
		_size = n;
		_str[_size] = '\0';
	}
}

十二、流插入<<

成员函数默认第一个形参都是对象的地址,也就是隐藏的this指针,由于cout抢占了对象的第一个位置,因此不能当做成员函数,就只能写在类外了

cpp 复制代码
// 注意:必须引用返回,不然会报错。因为ostream这个类做了一个防拷贝
ostream& operator<<(ostream& out, const string& s)
{
	// !注意:
	// C语言的字符串的打印是只打印到'\0'停止
	// 打印string时与'\0'无关与size有关

	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}

十三、流提取>>

由于输入的字符个数不确定,导致扩容多少也不确定,因此只能一个一个从键盘上拿,istream中有个get可以解决(每次读取一个字符)

cpp 复制代码
istream& operator>>(istream& in, string& s)
{
	char ch = in.get();
	// [cin >> 对象] 默认读取到空格或者换行就不会往下读了
	// 只有getline才可以
	while (ch != ' ' && ch != '\n')
	{
		s += ch;// += 自己就会扩容
		ch = in.get();
	}
	return in;
}

以上代码还是不够完善,当多次对一个对象输入的情况,要对之前形成一次覆盖,可以对比库里的string

自己实现的上一次输入的数据没有清理干净。因此要清理上次的内容

cpp 复制代码
istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char ch = in.get();
		// [cin >> 对象] 默认读取到空格或者换行就不会往下读了
		// 只有getline才可以
		while (ch != ' ' && ch != '\n')
		{
			s += ch;// += 自己就会扩容
			ch = in.get();
		}
		return in;
	}

【Test.cpp】

还有一个问题,当一开始输入连续空格或者换行时,可以对比自己实现的和库里的:

因此要过滤前面的空格或者换行

cpp 复制代码
istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char ch = in.get();
		// 处理前缓冲区前面的空格或者换行
		while (ch == ' ' || ch == '\n')
		{
			ch = in.get();
		}
		// [cin >> 对象] 默认读取到空格或者换行就不会往下读了
		// 只有getline才可以
		while (ch != ' ' && ch != '\n')
		{
			s += ch;// += 自己就会扩容
			ch = in.get();
		}
		return in;
	}

【Test.cpp】

十四、清空操作clear

直接将第一个字符改成'\0'即可

cpp 复制代码
void clear()
{
	_str[0] = '\0';
	_size = 0;
}

十五、比较操作

15.1 <

cpp 复制代码
bool operator<(const string& s) const
{
	// 不能用strcpy,因为它只会比到'\0'
	// 而string类是有多少字符比多少字符
	
	// 以短的字符串为基础来比较
	int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);

	// "hello" < "hello"   false ①
	// "helloxx" < "hello" false ②
	// "hello"  < "helloxx" true ③

	// ret为0,类似第②中情况,则长的那个字符串长
	// 否则,则ret<0说明s1<s2为真,ret>0,则s1>s2为假
	return ret == 0 ? _size < s._size : ret < 0;
}

15.2 ==

cpp 复制代码
bool operator==(const string& s) const
{
	return _size == s._size
		   && memcmp(_str, s._str, _size) == 0;
}

当写完<==,剩下的代码就可以复用了。

15.3 <=

cpp 复制代码
bool operator<=(const string& s) const
{
	return *this < s || *this == s;
}

15.4 >

cpp 复制代码
bool operator>(const string& s) const
{
	return !(*this <= s);
}

15.5 >=

cpp 复制代码
bool operator>=(const string& s) const
{
	return !(*this < s);
}

15.6 !=

cpp 复制代码
bool operator!=(const string& s) const
{
	return !(*this == s);
}

十六、交换操作swap

cpp 复制代码
// 16. swap
void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_capacity, s._capacity);
	std::swap(_size, s._size);
}

十七、赋值运算符重载=

默认不写是以值的方式逐字节拷贝(浅拷贝/值拷贝),因此内置类型成员变量是直接赋值的,而自定义类型成员会去调用它的默认函数,但要注意动态开辟的成员变量。如果不写深拷贝,两个对象会同时指向动态开辟的空间。

法一:传统写法:

cpp 复制代码
// s1 = s2
// s1是隐藏的this指针

// 方法:
// 首先开一个和s2同样大的空间并且把s2的数据拷贝
// 然后再释放掉s1指向的空间,最后再让s1指向新拷贝的那个空间
string& operator=(const string& s)
{
	if (this != &s)
	{
		// 首先开一个和s2同样大的空间并且把s2的数据拷贝
		char* tmp = new char[s._capacity + 1];
		memcpy(tmp, s._str, s._size + 1);
		// 然后再释放掉s1指向的空间
		delete[] _str;
		// 最后再让s1指向新拷贝的那个空间
		_str = tmp;

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

法二:现代写法

cpp 复制代码
// 方法:
// 用s2拷贝构造tmp的对象,然后再让tmp和s1交换
string& operator=(const string& s)
{
	if (this != &s)
	{
		string tmp(s);
		swap(tmp);
	}
	return *this;
}

法二延伸:

cpp 复制代码
//s1 = s2
// 传参时,s2调用拷贝构造给tmp(深拷贝),然后再和s1交换
// s1是隐藏的this指针
string& operator=(string tmp)
{
	swap(tmp);
	return *this;
}

十八、源码string.h

cpp 复制代码
#pragma once
#include <assert.h>

namespace wj
{
	class string
	{
	public:
		// 1. 用C语言字符串的方式初始化

		// 以下不是默认构造
		string(const char* str)
			:_str(new char[strlen(str) + 1]) // 开空间。+1算上'\0'
			, _size(strlen(str)) // 字符个数
			, _capacity(_size) // 容量只存有效字符

			// 注意初始化列表的顺序(按照声明的顺序)
		{
			memcpy(_str, str, _size + 1);
			// +1 是为了拷贝'\0'
		}

		// 2. 由于成员变量含有动态内存开辟的空间
		// 因此要写析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

		// 3. 观察结果
		// 无论是cout还是printf都不能直接打印string类型
		// 因此就需要将string类转换为char类型,可以用c_str
		const char* c_str() const
		{
			return _str;
		}

		// 4. 无参构造(默认构造)
		// 默认构造:无参、全缺省、编译器自动生成的
		// 注意:默认构造不能重载
		string()
			:_str(new char[1]) // 空字符串至少有一个'\0'
			, _size(0)
			, _capacity(0)
		{
			_str[0] = '\0';
		}

		// 5. string遍历

		// 元素个数
		size_t size() const // 加const的原因:只要成员函数内部不修改成员变量,都应该加上const
		{
			return _size;
		}

		// ① operator[]

		// 可读可写
		// 可以引用返回(因为返回的对象是在堆上的,出了此作用域不会被销毁)
		char& operator[](size_t pos)
		{
			assert(pos < _size); // 断言,防止越界
			return _str[pos];
		}

		// 只能读,不能写
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);
			return _str[pos];
		}


		// 迭代器的遍历
		// string的迭代器本质就是一个char*的指针
		
		// 可读可写
		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;
		}

		// 6. string增

		// Request a change in capacity - reserve
		// 一般不会缩容
		void reserve(size_t n) 
		{
			if (n > _capacity)
			{
				// +1是给'\0'
				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); // 如果是空串,扩了2倍容量还是0,因此要考虑容量为0的情况
				_str[_size] = ch;
				_size++;
				_str[_size] = '\0';
			}
		}

		// 尾插字符串
		void append(const char* s)
		{
			size_t length = strlen(s);
			// 可能存在扩容
			if (_size + length > _capacity)
			{
				// 至少扩容到_size+length
				reserve(_size + length);
			}
			memcpy(_str + _size, s, length + 1);
			_size += length;
		}

		// 运算符重载+=,push_back和append升级
		// 即可以尾插一个字符
		string& operator+=(char x)//+=完还是要返回该对象(string类型)且对象不会被销毁(引用)
		{
			push_back(x);
			return *this;
		}

		// 也可以尾插一个字符串
		string& operator+=(const char* s)
		{
			append(s);
			return *this;
		}

		// 7. string插入
		// pos - 下标
		// n - 插入的字符个数
		// x - 插入的字符

		string& insert(size_t pos, int n, char x) // 函数重载
		{
			// 判断下标pos的合法性
			assert(pos <= _size);

			// 可能存在扩容
			if (_size + n > _capacity)
			{
				reserve(_size + n);
			}

			// 挪动数据
			size_t end = _size;
			// while(end >= pos)
			// 如果不强制类型转化为int会死循环,因为pos类型是size_t,end的类型是int,
			// 他们在比较的时候会发生算术转换(低的类型向高的类型转换)
			// 当end减减为-1时,由于end的类型转化为size_t,是一个非常大的数
			// 所以可以将pos强制转化成int,避免算术转化

			// 如果不想强制类型转化,可以用npos。
			//while (end >= (int)pos && end != npos)

			while (end >= pos && end != npos)
			{
				_str[end + n] = _str[end];
				end--;
			}
			// 插入数据
			for (size_t i = 0; i < n; i++)
			{
				_str[pos + i] = x;
			}
			_size += n;
			
			return *this;
		}

		string& insert(size_t pos, const char* str)
		{
			// 判断下标pos的合法性
			assert(pos <= _size);

			// 可能存在扩容
			size_t length = strlen(str);
			if (_size + length > _capacity)
			{
				reserve(_size + length);
			}
			// 挪动数据
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				_str[end + length] = _str[end];
				end--;
			}
			// 插入数据
			for (size_t i = 0; i < length; i++)
			{
				_str[pos + i] = str[i];
			}
			_size += length;

			return *this;
		}

		// 7. string的删除erase
		string& erase(size_t pos, size_t len = npos) //size_t len = npos - 从坐标pos开始往后删完
		{
			// 检查pos的合法性
			assert(pos <= _size);

			if (len == npos || len + pos >= _size)
			{
				// 说明pos(包括pos)后面的字符要全部删完
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				size_t end = pos + len;
				while (end <= _size)
				{
					_str[pos] = _str[end];
					pos++;
					end++;
				}
				_size -= len;
			}
			return *this;
		}

		// 8. string的查找find

		// 查找字符
		size_t find(char x, size_t pos = 0) // 从下标pos开始查找字符,如果实参不传第二个参数,默认从下标为0开始查找
		{
			assert(pos < _size);

			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == x)
				{
					// 找到返回下标
					return i;
				}
			}
			// 没找到默认返回npos
			return npos;
		}

		// 查找字符串
		size_t find(const char* str, size_t pos = 0) 
		{
			assert(pos < _size);

			// BM算法orkmp算法?
			//strstr(const char * str1, const char * str2) - 从str1中查找str2
			const char* ptr = strstr(_str + pos, str);// strstr - 查找子串
			// 如果ptr为空说明没找到
			if (ptr == nullptr)
			{
				// 找不到返回npos
				return npos;
			}
			// 否则找到了
			else
			{
				// 指针-指针 --- 返回的是元素个数
				return ptr - _str;
			}
		}

		// 9. 截取子串

		// pos - 截取的下标
		// len - 截取的长度
		// npos - 截取的最大长度
		string substr(size_t pos = 0, size_t len = npos) const
		{
			// 判断下标的合法性
			assert(pos < _size);

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

			string tmp;
			tmp.reserve(n);
			for (size_t i = pos; i < pos + n; i++)
			{
				tmp += _str[i];
			}
			// 由于对象tmp是自定义类型,返回时会生成一个临时变量(可能是寄存器),然后tmp会拷贝给临时变量
			// 自定义类型的拷贝必须先调用拷贝构造函数。但如果不手动编写,编译器会默认生成一个浅拷贝/值拷贝的拷贝构造函数
			// 即将所有成员变量逐一拷贝到新对象中,这种拷贝方式对于基本数据类型或者是自定义类型的成员变量来说是没有问题的
			// 但是 如果类中有动态分配内存的指针变量,则需要手动编写深拷贝的拷贝构造函数
			// 因为如果不写,tmp和_str会指向同一块内存开辟的空间
			return tmp;
		}
		// 10. 拷贝构造函数
		string(const string& s)
		{
			// 深拷贝
			_str = new char[s._capacity + 1];
			memcpy(_str, s._str, s.size() + 1);
			_size = s._size;
			_capacity = s._capacity;
		}
		 
		// 11. resize
		// 改变字符串的有效个数,如果改变的个数大于容量,则会扩容,并且resize还能初始化
		void resize(size_t n, char ch = '\0')
		{
			if (n < _size) // 相当于删除数据,只保留前n个字符
			{
				_size = n;
				_str[_size] = '\0';
			}
			else // 否则相当于插入数据
			{
				reserve(n);// 可能有扩容情况,n比_capacity大才会扩容,比_capacity小什么也不发生
				for (size_t i = _size; i < n; i++) // 填数据
				{
					_str[i] = ch;
				}
				_size = n;
				_str[_size] = '\0';
			}
		}

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

		// 15. 比较大小(字符/字符串比较都是按ascII)
		bool operator<(const string& s) const
		{
			// 不能用strcpy,因为它只会比到'\0'
			// 而string类是有多少字符比多少字符
			int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);

			// "hello" < "hello"   false ①
			// "helloxx" < "hello" false ②
			// "hello"  < "helloxx" true ③

			// 如果等于0,
			return ret == 0 ? _size < s._size : ret < 0;
		}

		bool operator==(const string& s) const
		{
			return _size == s._size
				&& memcmp(_str, s._str, _size) == 0;
		}

		bool operator<=(const string& s) const
		{
			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);
		}

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

		// 17. 赋值运算符重载
		// 默认是以值的方式逐字节拷贝(浅拷贝/值拷贝)
		// 因此内置类型成员变量是直接赋值的,而自定义类型成员会去调用它的默认函数,
		// 但是要注意动态开辟的成员变量。如果不写(深拷贝)两个对象会同时指向动态开辟的空间
		 
		// s1 = s2
		// s1是隐藏的this指针

		// 法一(传统写法):
		// 首先开一个和s2同样大的空间并且把s2的数据拷贝,然后再释放掉s1指向的空间,最后再让s1指向新拷贝的那个空间

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

				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}*/

		// 法二(现代写法):
		// 用s2拷贝构造tmp的对象,然后再让tmp和s1交换
		/*string& operator=(const string& s)
		{
			if (this != &s)
			{
				string tmp(s);
				swap(tmp);
			}
			return *this;
		}*/

		// 法二的延伸
		
		//s1 = s2
		// 传参时,s2调用拷贝构造给tmp(深拷贝),然后再和s1交换
		string& operator=(string tmp)
		{
			swap(tmp);

			return *this;
		}


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

		static size_t npos;

		// 如果静态成员变量想在类里定义,加个const即可(但是只能整型这样做,其他类型会报错)
		//const static size_t npos; // 这种方式不建议!!!
	};

	// 静态成员变量在类里是声明,并且定义只能在类外 
	size_t string::npos = -1;

	// 12. 流插入<< (插入到屏幕)
	
	// 成员函数默认(隐藏)第一个形参都是对象的地址,也就是this指针
	// cout抢占了对象的第一个位置,因此不能当做成员函数,就只能写在类外了

	// 注意:必须引用返回,不然会报错。因为ostream这个类做了一个防拷贝
	ostream& operator<<(ostream& out, const string& s)
	{
		// !注意:
		// C语言的字符串的打印是只打印到'\0'停止
		// 打印string时与'\0'无关与size有关

		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}
	
	// 13. 流提取
	istream& operator>>(istream& in, string& s)
	{

		// 输入的字符不确定,导致扩容多少也不确定
		// 因此只能一个一个的拿,istream中有个get可以解决(每次读取一个字符)
		
		// ① 多次对一个对象输入的情况,对之前形成一次覆盖
		// 因此要清理上次的内容
		s.clear();
		
		char ch = in.get();
		// ② 一开始输入连续空格或者换行
		// 处理前缓冲区前面的空格或者换行
		while (ch == ' ' || ch == '\n')
		{
			ch = in.get();
		}

		while (ch != ' ' && ch != '\n')
		{
			s += ch;// += 自己就会扩容
			ch = in.get();
		}
		return in;
	}


	// 优化版
	//s.clear();

	//char ch = in.get();
	 处理前缓冲区前面的空格或者换行
	//while (ch == ' ' || ch == '\n')
	//{
	//	ch = in.get();
	//}

	in >> ch;
	//char buff[128];
	//int i = 0;

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

	//	//in >> ch;
	//	ch = in.get();
	//}

	 如果输入的字符不满127,就要在后补上'\0'
	//if (i != 0)
	//{
	//	buff[i] = '\0';
	//	s += buff;
	//}

	//return in;

	
}
相关推荐
AngeliaXue3 分钟前
Java集合(List篇)
java·开发语言·list·集合
世俗ˊ4 分钟前
Java中ArrayList和LinkedList的比较
java·开发语言
Mercury Random4 分钟前
Qwen 个人笔记
android·笔记
小立爱学习5 分钟前
Linux 给 vmlinux 添加符号
linux·c语言
顶呱呱程序6 分钟前
2-100 基于matlab的水果识别
开发语言·matlab·边缘检测·水果识别·特征提取·matlab-gui
wx2004110212 分钟前
Codeforces Round 973 (Div. 2) - D题
数据结构·c++·算法
攸攸太上16 分钟前
Docker学习
java·网络·学习·docker·容器
Milo_K24 分钟前
项目文件配置
java·开发语言
Crossoads44 分钟前
【数据结构】排序算法---基数排序
c语言·开发语言·数据结构·算法·排序算法
Ylucius1 小时前
JavaScript 与 Java 的继承有何区别?-----原型继承,单继承有何联系?
java·开发语言·前端·javascript·后端·学习