C++STL之string类:基本使用及模拟实现

目录

一,前言

为什么要学习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 主要侧重于一些常见的数据结构和算法组件,如 vectorlist 等,而较少单独提及 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,迭代器具有很强的迁移性,vectorlist 也是这样使用的

        在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 只能用于stringvetctor,后面的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 函数交换 _strtmp._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的实现

我们可以把sizecapacityoperator[] ,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++ 知识,还提升了问题解决能力和编程思维,为我们日后打下了基础。加油~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值