目录
一,前言
为什么要学习string类
C语言中的字符串
C++中的字符串
STL(Standard Template Library) 里面的 string 类
二,string类的基本使用
文档的阅读
常见接口的基本使用
1,构造函数(constructor)
2,拷贝构造(copy constructor)
3,运算符 operator= 重载
4,析构函数 (destructor)
5,push_back 插入一个字符
6, append 插入一个字符串
7,operator+= 插入一个字符或字符串
8,operator[ ] 的重载 与 size()
operator[] size() 的应用, 将字符串转成整型
9,iterator迭代器的使用
遍历字符串数组
正向、反向迭代器,const、非const 迭代器
10,size(), capacity(), length()的使用
11,resize(), reserve() 的使用
12,insert(),erase() 的使用
13,c_str() 的使用
14,find() 的使用
15, operator+ 和 operator+=(一般使用operator+=)
16,关系运算符(relational operators)
17,getline的使用
三,string类的模拟实现
简单的string类的实现
构造函数的实现
string.h
test.cpp
深浅拷贝问题
析构函数的实现
拷贝构造的实现
运算符重载operator= 的实现
以上整体代码:
string.h
test.cpp
深拷贝的现代写法
实现具有增删改查的string类
1.构造函数
2.析构函数
3.拷贝构造
4.赋值运算符
5.迭代器 iterator的实现
6. push_bcak
7. append
8. reserve
9.重载运算符(operator+=)
10.insert
11. erase
12. find
13. resize
14. 运算符重载
15. 重载 operator<< 与 operator>>
整体代码
string.h
test.cpp
四,小结
一,前言
为什么要学习string类
C语言中的字符串
C语言中,字符串是以 '\0' 结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数, 但是这些库函数与字符串是分离开的,不太符合面向对象的思想,而且底层空间需要用户自己管理,稍不留神可 能还会越界访问。
C++中的字符串
C++兼容C语言的语法,所以字符串也是和C语言一样以 ' \0 ’作为结束标志,而我们的实际中处理字符串是非常常见的操作。C++就引入了string 类,string 类提供了方便、高效和安全的方式来操作字符串,满足各种字符串处理的需求。
STL(Standard Template Library) 里面的 string 类
模板(Template)是 C++ 中的一种编程机制,指泛型编程,而标准模板库(Standard Template Library,STL)是 C++ 标准库的一部分,其中大量使用了模板这种机制。 可以说 STL 是基于模板构建起来的一系列通用的、可复用的数据结构和算法的集合。
例如,STL 中的容器(如 vector、list 、map 等)和算法(如 sort、find 等)都是通过模板来实现的,从而可以处理不同类型的数据。
需要注意的是,string 类是 C++ 标准库(包括 STL)的一部分。它与 STL 中的其他组件一样,遵循相同的设计理念和规范,为编程提供了方便、高效和可复用的字符串处理功能。
通常所说的 STL 主要侧重于一些常见的数据结构和算法组件,如 vector、list 等,而较少单独提及 string 类
STL 常被强调的是用于数据存储和操作的数据结构(如 vector 用于动态数组,list 用于链表)以及相关的算法(如排序、查找等)。string 类主要专注于字符串处理,相对来说功能较为特定。
二,string类的基本使用
文档的阅读
这里主要演示最为常见的接口,其它接口需要靠自己阅读文档学习,C++STL的学习需要到官网阅读文档,以下是文档的网址:
https://cplusplus.com/
必须学会看文档,如果英语水平不是很好,可以下一个翻译软件,但是尽量不要使用翻译软件,自己不会的单词就去学。
以下是我使用的翻译软件网址:
https://fanyi.youdao.com/download-Windows
进入手册, 找到 string 类的接口


常见接口的基本使用
在 C++ 中使用 string 类之前,需要引入 <string> 头文件。
1,构造函数(constructor)
先看文档,序号1-7都是构造函数的重载,这些序号与下面的说明一一对应。


构造函数的应用
#include <iostream>
#include <string>
int main()
{
	std::string s1(10, 'a'); //里面需要10个字符a
	std::cout << s1 << std::endl;
	std::string s2(3, 'b'); //里面需要10个字符b
	std::cout << s2 << std::endl;
	return 0;
}以上就是根据文档使用构造函数完成初始化字符串数组,接下来我就不逐次看文档了,因为这些都是以前学习过的基础,只不过现在用起来了。
构造函数的其它重载函数的使用
| default (1) | string(); | 
|---|---|
| copy (2) | string (const string& str); | 
#include <iostream>
#include <string>
int main()
{
	std::string s1;   //构造空字符串
	std::string s2("hello");  //构造hello字符串
	std::cout << s1 << std::endl;
	std::cout << s2 << std::endl;
	return 0;
}pos是起始位置,len 缺省参数是 npos,不填就是一个很大的值,意思就是拷贝到最后。
| substring (3) | string (const string& str, size_t pos, size_t len = npos); | 
|---|
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s("12345");
	string s1(s, 1, 2);   //打印 23
	string s2(s, 1, string::npos); //打印 2345  // npos是string类中的一个静态成员变量,size_t npos = -1;表示很大的一个值
	cout << s1 << endl;
	cout << s2 << endl;
	return 0;
}
2,拷贝构造(copy constructor)
文档中的第二点,拷贝构造也属于构造的一种,用于初始化对象。

拷贝构造的使用
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s1("hello"); // 构造
	string s2(s1);   //拷贝构造
	string s3 = s1;  //s1先构造给临时对象tmp, tmp(s1),  tmp拷贝构造, s3(tmp)
	                 //直接优化成拷贝构造s3(s1);
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	return 0;
}3,运算符 operator= 重载
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s1("hello"); // 构造
	string s2("dear"); //构造
	s2 = s1;           // 运算符=的重载
	cout << s2 << endl;
	return 0;
}4,析构函数 (destructor)
出了作用域自动调用
5,push_back 插入一个字符
void push_back(char c);
 
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s("hello");
	s.push_back('A');  //插入一个字符
	cout << s << endl;  //打印结果 helloA
	return 0;
}6, append 插入一个字符串
| c-string (3) | string& append (const char* s); | 
|---|
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s("hello");
	s.append(" world"); //插入一个字符串
	cout << s << endl;   //打印结果 hello world
	return 0;
}7,operator+= 插入一个字符或字符串
| c-string (2) | string& operator+= (const char* s); | 
|---|---|
| character (3) | string& operator+= (char c); | 
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s1;
	s1 += '1';     //插入一个字符
	s1 += "2222";  //插入一个字符串
	cout << s1 << endl;  // 打印结果 12222
	string s2;
	s2 += "hello";
	s2 += ".";
	s2 += "world";
	cout << s2 << endl;  // 打印结果 hello.world
	return 0;
}
8,operator[ ] 的重载 与 size()
char& operator[] (size_t pos); 可以修改其返回值
const char& operator[] (size_t pos) const; 不可以修改其返回值
size_t size() const; 返回字符串的大小,不包含 \0
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s("12345");
	//遍历字符串
	for (size_t i = 0; i < s.size(); ++i)  //size(),计算字符串数组中的个数
	{
		cout << s[i] << " ";         // s[i] 相对于 s.operator[](&s,i)这样调用函数; operator返回的是一个字符
	}
	return 0;
}
operator[] size() 的应用, 将字符串转成整型

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s("12345");
	int val = 0;
	for (size_t i = 0; i < s.size(); ++i) 
	{
		val *= 10;
		val += s[i] - '0';
	}
	cout << val << endl;
	return 0;
}9,iterator迭代器的使用
遍历字符串数组
- 1.利用operator[] 和 size() 遍历
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s1("hello");
	s1 += ' ';
	s1 += "world";
	cout << s1 << endl;   
	//除了可以读还可以写
	for (size_t i = 0; i < s1.size(); ++i)
	{
		s1[i] += 1;
	}
	//读
	for (size_t i = 0; i < s1.size(); ++i)
	{
		cout << s1[i] << " ";    
	}
	cout << endl;
	return 0;
}
- 2. 迭代器遍历:迭代器 iterator类型 属于string类,迭代器具有很强的迁移性,vector、list 也是这样使用的
在string类中(字符串数组中),迭代器有点类似于指针遍历数组,但是迭代器不一定是指针, 而是一个像指针一样的东西

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s1("hello world");
	string::iterator it = s1.begin();
	//写
	while (it != s1.end())
	{
		*it -= 1;
		++it;
	}
	//读
	it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	return 0;
}
- 3. 范围for原理: 会被替换成迭代器
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s1("hello world");
	for (auto ch : s1) 
	{
		cout << ch << " ";
	}
	cout << endl;
	return 0;
}
正向、反向迭代器,const、非const 迭代器
目前来看operator[] 和 size 很方便,迭代器看都不想看,但是operator[] 和 size 只能用于string、vetctor,后面的list等等就可以感受到迭代器带来的便利
#include <iostream>
#include <string>
using namespace std;
int string2int(const string& str)  // const 接收
{
	int val = 0;
	//const迭代器,只能读,不能写
	string::const_iterator it = str.begin();
	while (it != str.end())
	{
		//*it -= 1;  不能写
		val *= 10;
		val += (*it - '0');
		++it;
	}
	//反向的const迭代器
	val = 0;
	string::const_reverse_iterator rit = str.rbegin();
	while (rit != str.rend())
	{
		val *= 10;
		val += (*rit - '0');
		++rit;
	}
	return val;
}
//其它迭代器
int main()
{
	string s1("hello world");
	//1,反向迭代器,倒着遍历
	string::reverse_iterator rit = s1.rbegin();  // 指向 d
	while (rit != s1.rend())        // rend()  指向h 的前一个位置
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
	// 2,const迭代器
	string nums("12345");
	cout << string2int(nums) << endl;
	return 0;
}
10,size(), capacity(), length()的使用
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s1("hello world");
	string s2("hello");
	cout << s1.size() << endl; //求有多少个有效字符
	cout << s2.size() << endl;   
	cout << s1.length() << endl;
	cout << s2.length() << endl;//string比STL早一点点、字符串length长度,后面出现了树等等,就用size了有效个数的意思,现在一般使用size
	cout << s1.max_size() << endl;  // max_size返回字符串可以达到的最大长度 没什么用了解一下
	cout << s2.max_size() << endl;
	cout << s1.capacity() << endl; // 求容量
	cout << s2.capacity() << endl;
	s1 += "1111111";
	cout << s1.capacity() << endl; //增容到的目前容量
	s1.clear();  //只是把size清空了,也就是把有效字符清空了
	cout << s1 << endl;
	cout << s1.capacity() << endl;  //容量没有变化
}
11,resize(), reserve() 的使用
resize 除了增容还会改变size,因为有插入字符
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s("hello world");
	s.resize(5);//默认不给填\0,给了什么就填什么
	cout << s << endl;
	s.resize(20, 'x');   //除了会扩容还会填充 x ,如果不给值默认补\0
	cout << s << endl;
}- reserve就是开空间,如果给100不是空间开100而是1.5倍/2倍的开(容量大于100)
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s;
	s.reserve(100);
	size_t sz = s.capacity();
	cout << "making s grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capcaity changed:" << sz << endl;
		}
	}
	cout << s.capacity(); //capacity计算的是存储有效字符的空间容量,不包括\0, 打印111,本质是112个最后一个给 \0
}
12,insert(),erase() 的使用
insert的使用
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s;
	s += '1';
	s += "3456";
	cout << s << endl;
	s.insert(s.begin(), '0'); // iterator insert(iterator p, char c);
	cout << s << endl;
	s.insert(2, "2"); //string& insert (size_t pos, const char* s); 字符'2'不能插入,只能插入字符串"2"
	cout << s << endl;
	return 0;
}erase的使用
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s;
	s += "123456";
	// 删除字符 
	s.erase(2, 10);  //如果才读比字符串里面的长,直接删到结束的位置
	cout << s << endl;   //打印 12
	return 0;
}13,c_str() 的使用

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s1("hello");
	//获取字符数组的首地址,用C字符串的形式遍历
	const char* str = s1.c_str();
	while (*str)
	{
		cout << *str << " ";
		++str;
	}
	cout << endl;
	cout << s1 << endl;  //调用的string重载的operator<< 输出
	cout << s1.c_str() << endl;//直接输出 const char*
	return 0;
}c_str遇到 \0就会停止,而对象会把字符数组中的所有字符输出
#include <iostream>
#include <string>
using namespace std;
int main() 
{
	string s1("hello");
	s1 += '\0';
	s1 += "world";
	cout << s1 << endl;  //调用的string重载的operator<< 输出,将对象数组中的所有字符输出
	cout << s1.c_str() << endl;//直接输出 const char*,遇到\0就结束
	return 0;
}
14,find() 的使用
| character (4) | size_t find (char c, size_t pos = 0) const; | 
|---|
#include <iostream>
#include <string>
using namespace std;
int main() 
{
	string s1("string.cpp");
	string s2("string.c");
	string s3("string.txt");
	size_t pos1 = s1.find('.'); //如果没有找到返回npos,无符号 -1,不可能这么长,
	if (pos1 != string::npos)
	{
		cout << s1.substr(pos1) << endl; //substr 函数用于从一个字符串中提取子字符串。
        //它的作用是根据指定的起始位置和长度,返回原字符串的一部分。
	}
	size_t pos2 = s2.find('.'); //如果没有找到返回npos,无符号 -1,不可能这么长,
	if (pos2 != string::npos)
	{
		cout << s2.substr(pos1) << endl;
	}
	size_t pos3 = s3.find('.'); //如果没有找到返回npos,无符号 -1,不可能这么长,
	if (pos3 != string::npos)
	{
		cout << s3.substr(pos1) << endl;
	}
	return 0;
}但是呢还有一个问题、string.cpp.zip 取最后一个后缀,如何取到呢?
rfind,从后往前找,和 find一样使用。
#include <iostream>
#include <string>
using namespace std;
int main() 
{
	string s1("string.cpp.zip");
	size_t pos = s1.rfind('.');
	if (pos != string::npos) 
	{
		cout << s1.substr(pos) << endl;   //打印.zip
	}
	return 0;
}find 的使用场景:网址的分离,这种情况下我们可以把以下的代码写成分离函数,直接调用
#include <iostream>
#include <string>
using namespace std;
int main() 
{
	//协议 域名 资源名称
	string url("http://www.cp.uplus.com/reference/string/find/");
	//string url2("https://v.baidu.vip.logn");
	
	//分理出url的协议、域名、资源名称
	size_t i1 = url.find(':'); //返回 5
	if (i1 != string::npos)
	{
		cout << url.substr(0, i1) << endl;
	}
	size_t i2 = url.find('/', i1 + 3);
	if (i2 != string::npos)
	{
		cout << url.substr(i1 + 3, i2 - (i1 + 3)) << endl;
	}
	cout << url.substr(i2 + 1) << endl;
	return 0;
}
15, operator+ 和 operator+=(一般使用operator+=)
#include <iostream>
#include <string>
using namespace std;
int main() 
{
	string s1("hello");
	string ret1 = s1 + "world";   // s1不改变
	string ret2 = s1 += "world";  // s1改变
	cout << ret1 << endl;
	cout << ret2 << endl;
	return 0;
}16,关系运算符(relational operators)
string是可以直接比较大小的
#include <iostream>
#include <string>
using namespace std;
int main() 
{
	string s1("abcd");
	string s2("bbcd");
	cout << (s1 < s2) << endl;       // 1
	cout << (s1 < "bbcd") << endl;   // 1
	cout << ("abcd" < s2) << endl;   // 1
	return 0;
}17,getline的使用
| (1) | istream& getline (istream& is, string& str, char delim); | 
|---|---|
| (2) | istream& getline (istream& is, string& str); | 
cin 或者 scanf 遇到空格或者换行就结束了,如果多个值只能用空格换行间隔
geline就派上用场了,遇到空格不会结束
geline(cin,s); //遇到换行才结束
三,string类的模拟实现
模拟实现是指实现最核心的功能,不是实现一个一模一样的, 也不是吧每一个接口都实现,这样没有意义。
简单的string类的实现
实现一个简单的string类,限于时间,不可能要求具备 std::string的功能,但是至少要求能正确管理资源。-> 构造 + 析构 + 拷贝构造 + 赋值operator=,这些都是默认的成员函数。
将自定义的
string类放在特定的命名空间中,如my_string,能够有效地避免与标准库中的std::string产生混淆。当未展开std时,通过明确的命名空间限定来区分使用的是哪一个string类。如果不将自定义的
string类封装在命名空间中,又展开了std,那么当同时存在标准库的string和自定义的string时,编译器将无法明确您要使用的是哪一个,从而导致混淆和潜在的错误。例如,如果在同一个作用域内同时有
string str1;和my_string::string str2;,通过命名空间就能清晰地知道str1是标准库的,str2是自定义命名空间中的。这样的命名空间管理有助于提高代码的可读性、可维护性和可扩展性。
我们就写两个文件,因为代码比较少,实现也是头文件中实现了,如果你想分离也可以,把声明和定义分离
在写第一步构造函数的时候就出现了问题,以下代码就存在深浅拷贝的问题。
构造函数的实现
string.h
#pragma once
namespace my_string 
{
	class string 
	{
	public:
		//构造函数
		string(char* str)
			:_str(str)
		{}
		//求大小
		size_t size() 
		{
			return strlen(_str);
		}
		char& operator[](size_t i) 
		{
			return _str[i];
		}
	private:
		char* _str;
	};
	
}test.cpp
#include <iostream>
#include <string>
using namespace std;
#include "string.h"
int main() 
{
	my_string::string s1("hello");
	for (int i = 0; i < s1.size(); ++i)
	{
		cout << s1[i] << " ";
	}
	cout << endl;
	return 0;
}上面代码是实现就是一个浅拷贝,直接把 str 赋值给 _str, 两者指向的还是同一块空间,析构的时候也会析构两次,这是类和对象(中),提到的深浅拷贝问题,而且该数据在常量区,不能进行修改。
深浅拷贝问题
类和对象(中) ---- 拷贝构造函数,遗留的一个深浅拷贝问题,现在我们可以来解决了
我们要解决的是两次析构的问题,和不能修改的问题,怎么办呢???
只有一个办法就是进行深拷贝,如何操作?
就是堆区上开一个和 "hello"一样大小的空间,可以动态增长,str 指向自己的空间, _str 是另外开辟的一份空间,所以析构的时候析构的是自己的空间,堆上的数据可以进行修改。

针对上面的代码进行修改后的构造函数
栈上的空间是自动分配的,而堆上可以自由管理,空间不够了可以增容
String对象中存储指针,指针指向的数组中存储字符,字符最后必须保留\0
带参的需要这样给,在堆上开一份一样大的空间,把值赋值过去
 //构造函数
	string(const char* str)
		:_str(new char[strlen(str) + 1])  // hello \0,要多开一个存储\0.
	{
		strcpy(_str, str);   //把str的值拷贝给 _str,strcpy 会把 \0 也拷贝过去
	}Std::string 当为空串的时候,不打印任何的东西
当我们自己实现string的时候,不传参数也存在问题,当我们遍历无参的string的时候,程序直接崩溃了。

所以我们可以再实现一个构造函数(重载)。以下是无参的构造函数
//接受无参的构造函数,
string()
	:_str(new char[1])
{
	_str[0] = '\0';
}
进而优化代码:无参和带参的可以合并,写一个全缺省的。
写到这里,我们终于把构造函数搞定了
string(const char* str = "")
	:_str(new char[strlen(str) + 1]) 
{
	strcpy(_str, str);
}
析构函数的实现
~string()
{
	delete[] _str;
	_str = nullptr;
}拷贝构造的实现
拷贝构造也是构造函数的一种,如果直接赋值也会是浅拷贝,所以我们也是给 _str 开一份一样大的空间
//拷贝构造
// string s2(s1)
string(const string& s)
	:_str(new char[strlen(s._str) + 1]) 
{
	strcpy(_str, s._str);
}运算符重载operator= 的实现

//赋值运算符 =
string& operator=(const string& s)
{
	if (this != &s)  //防止自己给自己赋值
	{
		char* tmp = new char[strlen(s._str) + 1];
		if (tmp == nullptr)
		{
			return *this;
		}
		strcpy(tmp, s._str);
		delete[] _str;
		_str = tmp;
		return *this;
	}
}以上整体代码:
string.h
#pragma once
namespace my_string
{
	class string
	{
	public:
		/*string()
			:_str(new char[1])
		{
			_str[0] = '\0';
		}
		string(const char* str)
			:_str(new char[strlen(str) + 1])
		{
			strcpy(_str, str);
		}*/
		//构造函数
		string(const char* str = "")
			:_str(new char[strlen(str) + 1])
		{
			strcpy(_str, str);
		}
		//析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		//计算大小
		size_t size()
		{
			return strlen(_str);
		}
		//operator[]重载
		char& operator[](size_t i)
		{
			return _str[i];
		}
		//拷贝构造
		// string s2(s1)
		string(const string& s)
			:_str(new char[strlen(s._str) + 1])
		{
			strcpy(_str, s._str);
		}
		//赋值 operator=
		//支持返回值,是因为有连等
		string& operator=(const string& s)
		{
			if (this != &s)  //防止自己给自己赋值
			{
				char* tmp = new char[strlen(s._str) + 1];
				if (tmp == nullptr)
				{
					return *this;
				}
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;
				return *this;
			}
		}
		//c_str
		const char* c_str()
		{
			return _str;
		}
	private:
		char* _str;
	};
	//下面都是测试,可以写 test.c,使用命名空间的东西都是需要指定这个命名空间 my_string::
	void test_string1()
	{
		string s1("hello");
		string s2;
		for (int i = 0; i < s1.size(); ++i)
		{
			cout << s1[i] << " ";
		}
		cout << endl;
		for (int i = 0; i < s2.size(); ++i)
		{
			cout << s2[i] << " ";
		}
		cout << endl;
	}
	void test_string2()
	{
		string s1("hello");
		string s2(s1); //拷贝构造
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
		string s3("world");
		s1 = s3;  //赋值
		cout << s1.c_str() << endl;
		cout << s3.c_str() << endl;
	}
}
test.cpp
#include <iostream>
#include <string>
using namespace std;
#include "string.h"
int main() 
{
	my_string::test_string1();
	my_string::test_string2();
	return 0;
}深拷贝的现代写法
首先,在拷贝构造函数
string(const string& s)中,将_str初始化为nullptr,这是为了确保在后续的操作中,出作用域析构的时候析构随机值就会出错,或出现异常或者其他情况,不会因为使用未初始化的_str而导致错误。然后,创建一个临时对象
tmp,并通过调用普通的构造函数为其分配内存并进行数据的拷贝(string tmp(s._str))。接下来,使用
swap函数交换_str和tmp._str。这样,新创建的对象(即正在进行拷贝构造的对象)就拥有了与原始对象独立的内存空间和数据副本。通过这种方式,实现了对原始对象的深拷贝,使得新对象和原始对象在内存上相互独立,修改其中一个对象的数据不会影响到另一个对象。
赋值运算的现代写法也是同理

构造函数和拷贝构造现代写法
//深拷贝--现代写法
string(const string& s)
	:_str(nullptr)
{
	string tmp(s._str); //tmp去调构造函数,开空间拷贝数据
	swap(_str, tmp._str);
}
string& operator=(const string& s)
{
	if (this != &s)
	{
		string tmp(s._str);//调构造,如果写s就是调用拷贝构造
		swap(_str, tmp._str);
	}
	return *this;
}operator= 还可以进一步简写
string & operator=(string s)   // 传参给s取调拷贝构造 s就是外面传过来的副本,在有拷贝构造的基础是也是深拷贝
{
	swap(_str, s._str);
	return *this;
}
以上简单string的实现其实是一个面试题目 ->深浅拷贝的问题。
搞定了这么是一个面试题,接下来我们要实现一个支持增删查改的 string
实现具有增删改查的string类
实现一个具有增删改查的string类,就需要实现可动态增长,需要多增加两个变量来记录字符的有效个数和容量的大小,如果空间不够就需要增容,这和顺序表的实现类似。
在实现之前,我们还是先把默认的成员函数先实现(构造+析构+拷贝构造+赋值)
第一步先把string类架子搭建起来
namespace my_string
{
	class string 
	{
	public:
		//迭代器的实现
		//构造函数
		//析构函数
		//拷贝构造
		//......
	private:
		char* _str;
		size_t _size;  //已经有多少个有效字符
		size_t _capacity; 
	};
}1.构造函数
string(const char* str = "")
{
	_size = strlen(str);  // 因为每次使用strlen本质都是在变量字符串数组,所以我们就使用一次strlen,减少复用
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}2.析构函数
//析构函数
~string() 
{
	delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}3.拷贝构造
传统写法和现代写法,选择一种实现即可
//拷贝构造---传统写法
string(const string& s) 
{
	_size = s._size;
	_capacity = s._capacity;
	char* tmp = new char[_capacity + 1];
	strcpy(tmp, s._str);
	_str = tmp;
}
//拷贝构造---现代写法
void swap(string& s) //因为要交换三个值,赋值运算符的重载也会用到交换三个值这个函数,直接调用
{
	::swap(_str, s._str);            // ::表示调用的是全局的swap,标准库已经实现了的,我们不需要写
 	::swap(_size, s._size);
	::swap(_capacity, s._capacity);
}
//拷贝构造
string(const string& s) 
	:_str(nullptr)
	,_size(0)
	,_capacity(0)
{
	string tmp(s._str);
	this->swap(tmp);   //this通常省略,这是隐藏的this指针去调用swap函数
	//swap(tmp);
}
4.赋值运算符
传统写法和现代写法二选一
//传统写法
string& operator=(const string& s)  
{
	if (this != &s)  //防止自己给自己赋值
	{
		_size = s._size;
		_capacity = s._capacity;
		char* tmp = new char[_capacity + 1];
		strcpy(tmp, s._str);
		delete[] _str;
		_str = tmp;
		return *this;
	}
}
//现代写法
string& operator=(string s)  //调用拷贝构造
{
	if (this != &s)  //防止自己给自己赋值
	{
		this->swap(s);   //这是写拷贝构造实现的swap函数,可以直接调用完成三个变量的交换
		//swap(s);
		return *this;
	}
}5.迭代器 iterator的实现
我们可以把size,capacity,operator[] ,c_str 也顺便一起实现了
#include <assert.h>
namespace my_string
{
	class string 
	{
	public:
		//迭代器的实现 
		typedef char* iterator;
		iterator begin()
		{
			return _str;   // 像指针一样指向字符串数组的下标0
		}
		iterator end()
		{
			return _str + _size; //指向字符串数组的最后一个元素的下一个位置
		}
		//构造函数
		//析构函数
		//拷贝构造
		//赋值
		size_t size() const   //当外面是const的时候可以调用const,但是不能调用非const所以加上const
		{
			return _size;
		}
		size_t capcaity() const
		{
			return _capacity;
		}
		// operator[] 和const operator[]
		char& operator[](size_t i)
		{
			assert(i < _size);
			return _str[i];
		}
		const char& operator[](size_t i) const
		{
			assert(i < _size);
			return _str[i];
		}
		const char* c_str()
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;  //已经有多少个有效字符  
		size_t _capacity; 
	};
}6. push_bcak
push_back和顺序表中的尾插一样,尾插之前第一步都是先判断容量是否充足,如果不够就需要增容,同样的逻辑

void push_back(char ch) 
{
	//如果空间不够,增2倍
	if (_size == _capacity) 
	{
		size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
		char* newstr = new char[newcapacity + 1]; //\0不是有效数据,但是\0需要占用一个空间
		strcpy(newstr, _str);
		delete[] _str;
		_str = newstr;
		_capacity = newcapacity;
	}
	_str[_size] = ch;
	++_size;
	_str[_size] = '\0';   //这里必须在末尾加 \0
}7. append
append 和 push_back逻辑也是类似的

	void append(const char* str) 
	{
		size_t len = strlen(str);
		if (_size + len > _capacity) 
		{
			size_t newcapacity = _size + len;
			char* newstr = new char[newcapacity + 1];
			strcpy(newstr, _str);
			delete[] _str;
			_str = newstr;
			_capacity = newcapacity;
		}
		strcpy(_str + _size, str);
		_size += len;
	}8. reserve
push_back 和 append 的实现有逻辑一样的代码,就是在增容的时候都是同样的代码,我们就可以写成一个 reserve 改变容量,直接调用reserve

//reserve 把容量增到多少,消灭重复的代码,假设n == 100,容量不够的时候就增加
void reserve(size_t n) 
{
	if (n > _capacity) //检查是因为这是一个公有函数,不仅仅是插入删除会调用这个函数,还有其它会调用就做了一步检查
	{
		char* newstr = new char[n + 1]; 
		strcpy(newstr, _str);
		delete[] _str;
		_str = newstr;
		_capacity = n;
	}
}9.重载运算符(operator+=)
+=一个字符可以直接调用上面的push_back
+=一个字符串可以直接调用 append
单独实现也是同样的逻辑,推荐使用 +=。
// s1+= 'a';
string& operator+=(char ch) 
{
	this->push_back(ch);
	return *this;
}
// s1+= "aaaaa";
string& operator+=(const char* str) 
{
	this->append(str);
	return *this;
}10.insert
insert 插入一个字符
① 插入字符之前,先判断是否需要增容
② 把 pos 到 _size-1 的有效字符全部往后挪动一个字符单位
 我们发现最后还有补一个 \0 作为结束标志,所以我们直接把
 pos 到 \0 这些字符一起往后挪动一位
③ 我就是要让 pos 为 size_t 类型,当 pos = 0 的时候,end 不断减小,直到变成 -1, 如果不把 pos 强转成 int
 pos <= -1, int 和 size_t 比较,会把 -1 整型转成size_t类型,end会变成一个很大的数字,
 这样就会死循环了

//插入一个字符
string& insert(size_t pos, char ch) 
{
	assert(pos <= _size);
	if (_size == _capacity) 
	{
		size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
		reserve(newcapacity);
	}
	int end = _size;
	while ((int)pos <= end) 
	{
		_str[end + 1] = _str[end];
		--end;
	}
	_str[pos] = ch;
	++_size;
	return *this;
}insert 插入一个字符串
同样和 append 一样的逻辑,只不过多了个 pos,需要把位置空出来,插入字符串
和 insert 插入一个字符一样
①判断容量
②挪走空出位置
③把字符串拷贝过去
//插入一个字符串
string& insert(size_t pos, const char* str) 
{
	assert(pos <= _size); // = 指尾插
	size_t len = strlen(str);
	if(len + _size > _capacity )
	{
		size_t newcapacity = len + _size;
		reserve(newcapacity);
	}
	int end = _size;
	while (end >= (int)pos)  //当end是-1的时候会转成很大的数
	{
		//每次移动 len,空出 len 个位置插入
		_str[end + len] = _str[end];
		--end;
	}
	//不拷贝\0
	strncpy(_str + pos, str, len);
	_size += len;
	return *this;
}11. erase
定义npos
class string 
{
public:
	//.....
private:
	char* _str;
	size_t _size;  //已经有多少个有效字符  
	size_t _capacity; 
	static size_t npos;  //静态成员 npos声明
};
size_t string::npos = -1;  // npos 的定义erase的实现
以下是图解:

string& erase(size_t pos, size_t len = npos) 
{
	assert(pos < _size);
	if (len >= _size - pos) 
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else 
	{
		size_t i = pos + len;
		while (i <= _size) 
		{
			_str[i - len] = _str[i];
			++i;
		}
		_size -= len;
	}
	return *this;
}12. find
查找一个字符或者一个字符串
//查找一个字符
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;
}
//查找一个字符串
size_t find(const char* str, size_t pos = 0) 
{
	char* p = strstr(_str, str);  // 找字串 strstr的模拟实现必须要会,这是C语言基础
	if (p == nullptr) 
	{
		return npos;
	}
	else 
	{
		return p - _str;     //返回个数
	}
}13. resize
调整字符串的大小,如果调整成比原来的字符串小直接补 \0
如果调整的范围超出了字符串的范围,补插入的字符,如果不给默认就是补 \0 填充
void resize(size_t n, char ch = '\0') 
{
	if (n < _size) 
	{
		_str[n] = '\0';
		_size = n;
	}
	else 
	{
		if (n > _capacity) 
		{
			reserve(n);
		}
		for (int i = _size; i < n; ++i) 
		{
			_str[i] = ch;
		}
		_size = n;
		_str[_size] = '\0';
	}
}14. 运算符重载
// 两个字符的比较大小 s1 < s2
bool operator<(const string& s) 
{
	int ret = strcmp(_str, s._str);   //strcmp比较字符串模拟实现需要会
	return ret < 0;
}
bool operator==(const string& s) 
{
	int ret = strcmp(_str, s._str);
	return ret == 0;
}
bool operator<=(const string& s) 
{
	return *this == s || *this < s;
}
bool operator>(const string& s) 
{
	return !(*this <= s);
}
bool operator>=(const string& s) 
{
	return !(*this < s);
}
bool operator!=(const string& s) 
{
	return !(*this == s);
}15. 重载 operator<< 与 operator>>
operator<< 输出,可以使用友元函数,也可以直接遍历
//可以使用友元函数,重载输出 operator<<
//或者直接输出每一个字符
ostream& operator<<(ostream& out, const string& s)  // const 接受
{
	for (size_t i = 0; i < s.size(); ++i)  // s.size()  s是const去调用size,所以我们 size 也加了const
	{
		cout << s[i];    // operator[]函数 也加了const
	}
	cout << endl;
	return out;
}
//下面是友元函数
使用友元函数需要再类内部声明,这样才能访问私有成员
/*ostream& operator<<(ostream& out, const string& s)
{
	out << s._str << endl;
	return out;
}*/operator>>输入
// operator>>输入
//getline,空格不结束
istream& operator>>(istream& in, string& s) 
{
	while (1)
	{
		char ch;
		//in >> ch;
		ch = in.get();   //每次读取一个字符
		if (ch == ' ' || ch == '\n') 
		{
			break;
		}
		else 
		{
			s += ch;  //调用 += 把字符拼接起来
		}
	}
	return in;
}两者对比
 
 
整体代码
string.h
//实现一个支持增删查改的string
#include <assert.h>
namespace my_string
{
	class string 
	{
	public:
		//迭代器
		typedef char* iterator;
		iterator begin() 
		{
			return _str;
		}
		iterator end() 
		{
			return _str + _size;
		}
		//friend ostream& operator<<(ostream& out, const string& s);
		//构造函数
		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		//拷贝构造---传统写法
	/*	string(const string& s) 
		{
			_size = s._size;
			_capacity = s._capacity;
			char* tmp = new char[_capacity + 1];
			strcpy(tmp, s._str);
			_str = tmp;
		}*/
		//拷贝构造---现代写法
		string(const string& s) 
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			string tmp(s._str);
			this->swap(tmp);
			//swap(tmp);
		}
		void swap(string& s) 
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 赋值
		//s1 = s3
		//传统写法
		string& operator=(const string& s)  
		{
			if (this != &s)  //防止自己给自己赋值
			{
				_size = s._size;
				_capacity = s._capacity;
				char* tmp = new char[_capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;
				return *this;
			}
		}
		//现代写法
		string& operator=(string s)  //调用拷贝构造
		{
			if (this != &s)  //防止自己给自己赋值
			{
				this->swap(s);
				//swap(s);
				return *this;
			}
		}
		//析构函数
		~string() 
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}
		size_t size() const 
		{
			return _size;
		}
		size_t capcaity() const 
		{
			return _capacity;
		}
		// operator[] 和const operator[]
		char& operator[](size_t i) 
		{
			assert(i < _size);
			return _str[i];
		}
		const char& operator[](size_t i) const
		{
			assert(i < _size);
			return _str[i];
		}
		const char* c_str() 
		{
			return _str;
		}
		//reserve 把容量增到多少,消灭重复的代码,假设n == 100,容量不够的时候就增加
		void reserve(size_t n) 
		{
			if (n > _capacity) //检查是因为这是一个公有函数,不仅仅是插入删除会调用这个函数,还有其它会调用就做了一步检查
			{
				char* newstr = new char[n + 1]; 
				strcpy(newstr, _str);
				delete[] _str;
				_str = newstr;
				_capacity = n;
			}
		}
		void resize(size_t n, char ch = '\0') 
		{
			if (n < _size) 
			{
				_str[n] = '\0';
				_size = n;
			}
			else 
			{
				if (n > _capacity) 
				{
					reserve(n);
				}
				for (int i = _size; i < n; ++i) 
				{
					_str[i] = ch;
				}
				_size = n;
				_str[_size] = '\0';
			}
		}
		//增
		void push_back(char ch) 
		{
			如果空间不够,增2倍
			//if (_size == _capacity) 
			//{
			//	size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
			//	//char* newstr = new char[newcapacity + 1]; //\0不是有效数据,但是\0需要占用一个空间
			//	//strcpy(newstr, _str);
			//	//delete[] _str;
			//	//_str = newstr;
			//	//_capacity = newcapacity;
			//	reserve(newcapacity);
			//}
			//_str[_size] = ch;
			//++_size;
			//_str[_size] = '\0';
			 
			//转成直接调用insert
			insert(_size, ch);
		}
		void append(const char* str) 
		{
			//size_t len = strlen(str);
			//if (_size + len > _capacity) 
			//{
			//	size_t newcapacity = _size + len;
			//	/*char* newstr = new char[newcapacity + 1];
			//	strcpy(newstr, _str);
			//	delete[] _str;
			//	_str = newstr;
			//	_capacity = newcapacity;*/
			//	reserve(newcapacity);
			//}
			//strcpy(_str + _size, str);
			//_size += len;
			 
			//转成直接调用insert
			insert(_size, str);
		}
		// 运算符重载 operator+= 
		// s1+= 'a';
		string& operator+=(char ch) 
		{
			this->push_back(ch);
			return *this;
		}
		// s1+= "aaaaa";
		string& operator+=(const char* str) 
		{
			this->append(str);
			return *this;
		}
		//插入一个字符
		string& insert(size_t pos, char ch) 
		{
			assert(pos <= _size);
			if (_size == _capacity) 
			{
				size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
				reserve(newcapacity);
			}
			int end = _size;
			while ((int)pos <= end) 
			{
				_str[end + 1] = _str[end];
				--end;
			}
			_str[pos] = ch;
			++_size;
			return *this;
		}
		//插入一个字符串
		string& insert(size_t pos, const char* str) 
		{
			assert(pos <= _size); // = 指尾插
			size_t len = strlen(str);
			if(len + _size > _capacity )
			{
				size_t newcapacity = len + _size;
				reserve(newcapacity);
			}
			int end = _size;
			while (end >= (int)pos)  //当end是-1的时候会转成很大的数
			{
				//每次移动 len,空出 len 个位置插入
				_str[end + len] = _str[end];
				--end;
			}
		/*	int start = 0;
			while (start < len) 
			{
				_str[pos++] = str[start++];
			}*/
			//不拷贝\0
			strncpy(_str + pos, str, len);
			_size += len;
			return *this;
		}
		string& erase(size_t pos, size_t len = npos) 
		{
			assert(pos < _size);
			if (len >= _size - pos) 
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else 
			{
				size_t i = pos + len;
				while (i <= _size) 
				{
					_str[i - len] = _str[i];
					++i;
				}
				_size -= len;
			}
			return *this;
		}
		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;
		}
		size_t find(const char* str, size_t pos = 0) 
		{
			char* p = strstr(_str, str);
			if (p == nullptr) 
			{
				return npos;
			}
			else 
			{
				return p - _str;
			}
		}
		// 两个字符的比较大小 s1 < s2
		bool operator<(const string& s) 
		{
			int ret = strcmp(_str, s._str);
			return ret < 0;
		}
		bool operator==(const string& s) 
		{
			int ret = strcmp(_str, s._str);
			return ret == 0;
		}
		bool operator<=(const string& s) 
		{
			return *this == s || *this < s;
		}
		bool operator>(const string& s) 
		{
			return !(*this <= s);
		}
		bool operator>=(const string& s) 
		{
			return !(*this < s);
		}
		bool operator!=(const string& s) 
		{
			return !(*this == s);
		}
private:
		char* _str;
		size_t _size;  //已经有多少个有效字符
		size_t _capacity; // 能存多少个有效字符,\0不是有效字符
		static size_t npos;
};
size_t string::npos = -1;
	//可以使用友元函数,重载输出 operator<<
	//或者直接输出每一个字符
	ostream& operator<<(ostream& out, const string& s)
	{
		for (size_t i = 0; i < s.size(); ++i) 
		{
			cout << s[i];
		}
		cout << endl;
		return out;
	}
	//下面是友元函数
	/*ostream& operator<<(ostream& out, const string& s)
	{
		out << s._str << endl;
		return out;
	}*/
	// operator>>输入
	//getline,空格不结束
	istream& operator>>(istream& in, string& s) 
	{
		while (1)
		{
			char ch;
			//in >> ch;
			ch = in.get();
			if (ch == ' ' || ch == '\n') 
			{
				break;
			}
			else 
			{
				s += ch;
			}
		}
		return in;
	}
	//以下全是测试代码
	void test_string1() 
	{
		string s1;
		string s2("hello");
		cout << s1 << endl;
		cout << s2 << endl;
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
		//三种遍历方式
		for (size_t i = 0; i < s2.size(); ++i) 
		{
			s2[i] += 1; //没有调用+=,字符 += 1
			cout << s2[i] << " ";
		}
		cout << endl;
		
		//迭代器
		string::iterator it2 = s2.begin();
		while (it2 != s2.end()) 
		{
			*it2 -= 1;
			cout << *it2 << " ";
			++it2;
		}
		cout << endl;
		//范围for是由迭代器支持的,也就是说这段代码最终会被编译器替换成迭代器
		//iterator begin end
		for (auto e : s2) 
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test_string2() 
	{
		string s1("hello");
		s1.push_back('a');
		s1.push_back('b');
		s1.push_back('c');
		s1.push_back('e');
		s1.push_back('f');
		s1.push_back('g'); //增加到第六次就出现问题
		//s1.append(" dyyx1231");
		cout << s1 << endl;
		s1 += 'a';
		s1 += "dyyy";
		cout << s1 << endl;
	}
	void test_strign3() 
	{
		string s1("hello");
		s1.insert(1,'x');
		s1.insert(1,"xyz");
		cout << s1 << endl;
		string s2("hello");
		s2.reserve(11);
		cout << s2.size() << endl;
		cout << s2.capcaity() << endl;
		s2.resize(8, 'x');
		cout << s2.size() << endl;
		cout << s2.capcaity() << endl;
		s2.resize(2);
		cout << s2.size() << endl;
		cout << s2.capcaity() << endl;
	}
	void test_strign4() 
	{
		string s1("helloworld");
		s1.erase(5, 2);
		cout << s1 << endl;
		s1.erase(5, 4);
		cout << s1 << endl;
		string s2("abcdabcef");
		cout << s2.find("bce") << endl;
		cout << s2.find("sfa") << endl;
	}
	void test_strign5() 
	{
		/*string s;
		cin >> s;
		cout << s;*/
		string s("hello");
		s.insert(2, "xxx");
		cout << s << endl;
		s.insert(0, "xxx");
		cout << s << endl;
	}
}test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include "string.h"  
int main() 
{	
	//模拟实现string
	my_string::test_string1();
	//my_string::test_string2();
	//my_string::test_strign3();
	//my_string::test_strign4();
	//my_string::test_strign5();	
	return 0;
}四,小结
我们能够实现出个一个具有增删改查的 string 类,实现过程中我们把前面所学的知识都运用起来了,顺便把深浅拷贝问题也解决了,静态成员变量的使用、友元函数的使用等等,认识了迭代器的使用。
此外,我们还对代码进行了优化,提高了程序的性能和可读性。例如,在内存分配和释放时,采用了更高效的算法,减少了不必要的开销以及深浅拷贝的现代写法。
通过这次实现,我们不仅巩固和拓展了所学的 C++ 知识,还提升了问题解决能力和编程思维,为我们日后打下了基础。加油~



















