【C++初阶:STL —— string】string类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现

【写在前面】

到这里我们就要学会能自己能看文档了,因为就这个 string 类来说就有一百多个接口函数,那肯定记不住呀,我们平时用的大概就二十个,一般我们都是学习它比较常用的,其它的大概熟悉它们有哪些功能,真要用到时再去查文档。可以看到其实 string 就是一个管理字符数组的顺序表,因为字符数组的使用广泛,C++ 就专门给了一个 string 类,由于编码原因,它写的是一个模板。针对 string,一般情况它有三个成员 —— char* _str、size_t _size、size_t _capacity,我们在下面模拟实现 string 时就会看到,其次在学习深浅拷贝时我们只用 char* _str。
在这里插入图片描述

C++ 的文档库严格来说有两个:注意前者并不是 C++ 的官网文档,后者才是。这里我们对于官方文档就作为参考,因为它比较乱,平时就用 cplusplus 也够了。

  1. cplusplus
  2. cppreference

此外对于类和对象还有一个深浅拷贝的知识我们将会再这里补充。

一、为什么学习string类

💦 C语言中的字符串

C 语言中,字符串是以 ‘\0’ 结尾的一些字符的集合,为了操作方便,C 标准库中提供了一些 str 系列的库函数,
但是这些库函数与字符串是分离开的,不太符合 OOP 的思想,而且底层空间需要用户自己管理,稍不留神可
能还会越界访问。

💦 两个面试题(暂不讲解)

字符串转整型数字

字符串相加

在 OJ 中,有关字符串的题目基本以 string 类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用 string 类,很少有人去使用 C 库中的字符串操作函数。

二、标准库中的string类

💦 string类(了解)

string类的文档介绍

  1. 字符串是表示字符序列的类。
  2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
  3. string 类是使用 char(即作为它的字符类型,使用它的默认 char_traits 和分配器类型 (关于模板的更多信息,请参阅 basic_string)。
  4. string 类是 basic_string 模板类的一个实例,它使用 char 来实例化 basic_string 模板类,并用 char_traits 和 allocator 作为 basic_string 的默认参数(根于更多的模板信息请参考 basic_string)。
  5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如 UTF-8) 的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

总结:

  1. string 是表示字符串的字符串类。
  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作。
  3. string 在底层实际是:basic_string 模板类的别名,typedef basic_string<char, char_traits, allocator> string;。
  4. 不能操作多字节或者变长字符的序列。
  5. 在使用 string 类时,必须包含 string 头文件以及 using namespace std;。

在这里插入图片描述

#include<string>
#include<iostream>
using namespace std;
int main()
{
	cout << sizeof(char) << endl;
	cout << sizeof(wchar_t) << endl;

	char arr1[] = "hello bit";
	char arr2[] = "比特";
	return 0;
}

📝说明

对于字符,C++ 里提出了两个类型,现阶段接触的是第一个。

可以看到结果一个是 1 和 2,这个东西与编码有关系。

编码 ❓

计算机中存储只有二进制 0 和 1,如何去表示文字呢 ???

这时就建立出对应的编码表:

  1. ASCII > 支持英文
    1Byte = 8Bit,0 - 255 ASCII 编码表就是对 256 个值建立一个对应的表示值
    在这里插入图片描述

  2. GBK > 我国制定的,常用于 windows 下
    早期 windows 为了打入中国市场,就使用这个编码

  3. utf-8 > 通用,兼容 ASCII,常用于 Linux 下
    世界各国都开始使用计算机了,而早期计算机中只能表示英文,不能表示其它国家的文字,所以需要建立自己的编码表,但各自搞各自的就非常乱,所以就出现了 utf-8,早期是 UniCode

所以据不完全统计汉字大约有十万个左右,所以我们这里用 2 个字节,大概表示 6 万种状态

💦 string类的常用接口说明(只讲最常用的)
1、string类对象的常见构造
(constructor) 函数名称功能说明
string() —— 重点构造空的 string 类对象,即空字符串
string(const char* s) —— 重点用 C-string 来构造 string 类对象
string(size_t n, char c)string 类对象中包含 n 个字符 c
string(const string& s) —— 重点拷贝构造函数
#include<string>
#include<iostream>
using namespace std;   
//大概原理
template<class T>
class basic_string
{
	T* _arr;
	int _size;
	int _capacity;
};
//typedef basic_string<char> string;
int main()
{
	string s1;
	string s2("hello");  
	string s3("比特");
	string s4(10, 'a');
	string s5(s2);
	//对于string它重载了流提取和流插入等,这里就可以直接用
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	cout << s4 << endl;
	cout << s5 << endl;
	
	//赋值 ———— 深拷贝
	s1 = s5;
	cout << s1 << endl;
}

📝说明

basic_string ❗
在这里插入图片描述

string s1; || string s2(“hello”); ❗

通过调试我们发现 s1 虽然是无参的,但并不是啥都没有,且会在第一个位置放一个 \0。
在这里插入图片描述

2、string类对象的容量操作
函数名称功能说明
size —— 重点返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty —— 重点检测字符串释放为空串,是返回 true,否则返回 false
clear —— 重点清空有效字符
reserve —— 重点为字符串预留空间
resize —— 重点将有效字符的个数改成 n 个,多出的空间用字符 c 填充
在这里插入图片描述
#include<string>
#include<iostream>
using namespace std;
void test_string1()
{
	//1、size | length
	string s1("hello world");
	cout << s1.size() << endl;
	cout << s1.length() << endl;
	cout << "----------cut----------" << endl;
	//2、max_size
	string s2;
	cout << s1.max_size() << endl;
	cout << s2.max_size() << endl;	
	cout << "----------cut----------" << endl;
	//3、capacity
	cout << s1.capacity() << endl;
	cout << "----------cut----------" << endl;
	//4、resize
	string s3("hello world");
	cout << s3.size() << endl;
	cout << s3 << endl;
	//s3.resize(20);//n大于当前的字符串的长度且没有指定c,所以hello world\0\0\0\0...   
	//s3.resize(5);//n小于当前的字符串的长度, 它会删除掉从n开始的这些字符
	s3.resize(20, 'x');//n大于当前的字符串的长度且指定c,所以hello worldxxxx...
	cout << s3.size() << endl;
	cout << s3 << endl;
	cout << "----------cut----------" << endl;
	//5、reserve
	string s4("hello world");
	s4.reserve(20);
	cout << s4 << endl;
	cout << s4.size() << endl;
	cout << s4.capacity() << endl;
	s4.reserve(10);
	cout << s4 << endl;
	cout << s4.size() << endl;
	cout << s4.capacity() << endl;
	cout << "----------cut----------" << endl;
	//6、clear | empty
	string s5("hello world");
	cout << s5 << endl;
	cout << s5.empty() << endl;;
	s5.clear();
	cout << s5 << endl;
	cout << s5.empty() << endl;
	cout << "----------cut----------" << endl;
	//7、shrink_to_fit 暂且不演示
}   
void test_string2()
{
	string s;
	size_t sz = s.capacity();
	cout << "making s grow:\n" << sz << endl;
	for(int i = 0; i < 500; ++i)
	{
		s.push_back('c');
		if(sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed:" << sz << '\n';
		}
	}
	cout << "----------cut----------" << endl;
}
int main()
{
	test_string1();
	test_string2();

	return 0;
}

📝说明

size || length ❗
在这里插入图片描述
两者的功能相同,一般我们比较常用的是 size。

对于 string 是在 STL 这个规范前被设计出来的,因此在 Containers 下并没有 string:
在这里插入图片描述
早期说要算字符串字符的长度,所以早期提供的接口就叫 length,至于后面要加 size 的原因是后面增加了 map、set 这样的树,所以用 length 去表示它的数据个数就不合适了。

max_size ❗

它也是早期设计比较早的,属于一个没用的接口 —— 从操作系统中获取最大的长度。本意是想告诉你这个字符串最大你能定义多长, 这个接口在设计的时候其实不好实现,它没有办法标准的去定义这个接口,因为它有很多不确定的因素。所以它这个地方是直接给你 232 ,也就是 4G。所以没什么价值,以后再遇到就直接跳过了。
在这里插入图片描述
capacity ❗

对于 string 对象而言,如果 capacity 是 15,意味着它有 16 个字节的空间,因为有一个位置是 \0 的。对于 capacity 它会随着字符串的大小而增容,这里默认是 15。
在这里插入图片描述

resize ❗

resize 的前者版本可以让字符串的长度变成 n;后者可以让字符串的 n 个长度变成 c。

如果 n 是小于当前的字符串的长度, 它就会缩减到 n 个字符,删除掉从 n 开始的这些字符。

如果 n 是大于当前的字符串的长度,通过在末尾插入尽可能多的内容来扩展当前内容,倘若指定了 c,则新元素被初始化为 c 的副本,否则它们就是值初始化字符 (空字符 \0)。

对于 s3.resize(20); s3[19] 是有效位置,因为对于 operator[] 里它会 assert(pos < _size)。
在这里插入图片描述

reserve ❗

请求 capacity。

注意这里不是你要多少 capacity 它就给多少 capacity,它在增容时还要对照不同编译器下自己的增容规则,最终容量不一定等于字符串长度,它可能是相等的,也可能是更大的。

如果 n 大于当前字符串的 capacity,那么它会去扩容。

如果 n 小于当前字符串的 capacity,这里跟不同的平台有关系。文档里是这样说的:其他情况下 (小于或等于),有可能它会缩容(开一块新空间,将数据拷贝,释放原有空间),也有可能不对当前空间进行影响,只是变换 capacity 的值。已证,VS 和 Linux 下不会缩容,STL 的标准也是这样规定的,这是实现 STL 的人决定的。

对于 s4.reserve(20); s4[19] 是无效位置,因为对于 operator[] 里它会 assert(pos < _size)。
在这里插入图片描述
string 增容是怎么增的 ❓

test_string2():
在这里插入图片描述
可以看到 Windows VS 下初始容量是 15 ,除了第一次,其余的大概是以 1.5 倍增容。

g++ ???
在这里插入图片描述
可以看到在不同的编译器下增容规则也不同,Linux g++ 下初始容量是 0,其余是以 2 倍增容的。

clear | empty ❗

清理字符串 | 判空字符串
在这里插入图片描述
在这里插入图片描述
resize 和 reserve 有什么价值 ❓

对于 resize,既要开空间,还要对这些空间初始化,就可以用 resize —— s.resize(20, ‘x’);

对于 reserve,明确知道需要多大空间的情况,可以提前把空间开好,以减少增容所带来的代价 —— s.reserve(500);

3、string类对象的访问及遍历操作
函数名称功能说明
operator[] —— 重点返回 pos 位置的字符,const string 类对象调用
begin + endbegin 获取一个字符的迭代器,end 获取最后一个字符下一个位置的迭代器
rbegin + rendbegin 获取一个字符的迭代器,end 获取最后一个字符下一个位置的迭代器
范围 forC++11 支持更简洁的范围 for 的新遍历方式
在这里插入图片描述
#include<assert.h>
#include<string>
#include<iostream>
using namespace std;   
//大概原理
template<class T>
class basic_string
{
public:
	char& operator[](size_t pos)
	{
		assert(pos < _size);//检查越界:s2[20] -> err
		return _arr[pos];	
	}
private:
	T* _arr;
	int _size;
	int _capacity;
};
//typedef basic_string<char> string;
int main()
{
	//逐一遍历字符串
		//1、operator[]
	string s1("hello world");
	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;
	
		//2、范围for
	string s2("hello world");
	for(auto& e : s2)
	{
		e += 1;//写	
	}
	for(auto e : s2)
	{
		cout << e << " ";//读
	}
	cout << endl;
	
		//3、迭代器
	string s3("hello world");
	string::iterator it = s3.begin();
	while(it != s3.end())
	{
		*it += 1;//写
		++it;
	}
	it = s3.begin();
	while(it != s3.end())
	{
		cout << *it << " ";//读
		++it;
	}
	cout << endl;
	
	return 0;
}

📝说明

operator [] | at ❗

operator[] 和 at 的价值是一样的,但是不一样的地方是对于越界:operator[] 直接使用断言报错,比较粗暴;而 at 则会抛异常。
在这里插入图片描述
在这里插入图片描述

范围 for ❗

这是 C++11 中提出的新语法,比较抽象,对于它的原理,后面会再模拟实现它。

注意 auto e : s3 只能读,不能写;auto& e : s3 可读可写。

迭代器 ❗

它是 STL 中六大组件中的核心组件,这里先见个面。
在这里插入图片描述

begin() 返回第一个有效数据位置的迭代器;

end() 返回最后一个有效数据的下一个位置的迭代器;

目前可以认为它类似于指针,—— 有可能是指针,有可能不是指针,因为不仅仅 string 有迭代器,其它容器里也有迭代器。

while(it != s3.end()) 也可以 while(it < s3.end()),但不建议。这里是数组,换成小于它依然没问题,但是如果是链表,小于就不行了。所以这里统一就用不等于。

迭代器的好处是可以用统一类似的方式去访问修改容器 (也就意味着会了 string 的迭代器就等于会了其它容器的迭代器):
在这里插入图片描述
为什么这里要提供三种遍历方式 ❓

其实只提供了两种,因为范围 for 本质是迭代器,也就是说一个容器支持迭代器才会支持范围 for,后面会演示。

operator [] 可以像数组一样去访问,这种方式对于 vector 连续的数据结构是支持的,但是不支持 list。

迭代器这种方式是任何容器都支持的方式,所以迭代器才是容器通用的访问方式。

再简单了解迭代器 ❗

#include<string>
#include<iostream>
using namespace std;
void Print1(const string& s)//不需要写
{
	string::iterator it = s.begin();//err,普通迭代器
	//string::const_iterator it = s.begin();//ok,const迭代器
	//string::const_iterator it = s.cbegin();//ok,C++11提供的cbegin
	while(it != s.end())
	{
		cout << *it << " ";
		++it;	
	}
	cout << endl;
}
void Print2(const string& s)
{
	string::reverse_iterator rit = s.rbegin();//err,普通反向迭代器
	//string::const_reverse_iterator rit = s.rbegin();//ok,const反向迭代器
	//auto rit = s.rbegin();//ok,auto的体现
	while(rit != s.rend())
	{
		cout << *rit << " ";
		++rit;//注意这里不是--
	}
	cout << endl;
}
void test_string()
{
	string s1("hello");
	Print1(s1);

	string s2("hello");
	Print2(s2);	
}
int main()
{
	test_string();
	return 0;
}

📝说明

这里利用迭代器打印 s1:
在这里插入图片描述
查文档:begin 有两个版本
在这里插入图片描述

所以报错的原因是:s 是 const 对象,返回的是 const 迭代器,而这里使用普通迭代器来接收。解决方法就是用 const 迭代器接收。

在文档中看到 C++11 中又提供了一个 cbegin ???
在这里插入图片描述
为什么 C++11 把 const 迭代器单独拎出来,大概原因应该是 begin 里有普通 / const 迭代器不太明确,容易误导。不知道大家是怎么想的,我是觉得有点画蛇添足了。

rbegin:

rbegin 就是反向迭代器,顾名思义就是反向遍历。
在这里插入图片描述
在这里插入图片描述
auto 的价值 ❗

到了迭代器这里我们就可以用 auto 来替代比较长的类型,auto 的价值得以体现。

4、string类对象的修改操作
函数名称功能说明
push_back在字符串后尾插字符 c
append在字符串后追加一个字符串
operator+= —— 重点在字符串后追加字符串 str
c_str —— 重点返回 c 格式字符串
find + npos —— 重点从字符串 pos 位置开始往后找字符 c,返回该字符在字符串中的位置
rfind从字符串 pos 位置开始往前找字符 c,返回该字符在字符串中的位置
substr在 str 中从 pos 位置开始,截取 n 个字符,然后将其返回
在这里插入图片描述
#include<string>
#include<iostream>
using namespace std;
void test_string1()
{
	//1、push_back | append | operator +=
	string s1("hello");
	string s2("world");
	s1.push_back('+');
	s1.append("world");
	cout << s1 << endl;
	s1 += '+';
	s1 += "bit+";
	s1 += s2;
	cout << s1 << endl;
	cout << "----------cut----------" << endl;
	//2、insert | erase
	string s3("hello");
	s3.insert(0, "bit ");
	cout << s3 << endl;
	//s3.erase(5);//从5删到nops
	s3.erase(5, 2);//从5往后删2个
	cout << s3 << endl;
	cout << "----------cut----------" << endl;
}
void test_string2()
{
	//要求取出文件的后缀
	string file1("test.txt");
	string file2("test.c");
	string file3("test.txt.zip");//对test.txt文件打了个包
	size_t pos1 = file1.find('.');
	if(pos1 != string::npos)//当find的返回值不等于npos就找到了
	{
		//1、substr
		string sub1 = file1.substr(pos1);//同substr(pos1, file1.size()-pos1);,但没必要,因为这里是取后缀,而substr有缺省参数npos
		cout << sub1 << endl;
	}
	size_t pos2 = file2.find('.');
	if(pos2 != string::npos)
	{
		string sub2 = file2.substr(pos2);
		cout << sub2 << endl;
	}
	//由此可见对于file3,以上代码不适用
	size_t pos3 = file3.find('.');
	if(pos3 != string::npos)
	{
		string sub3 = file3.substr(pos3);
		cout << sub3 << endl;
	}
	//更适用的来说取文件的后缀应当使用另一个接口rfind
	size_t pos4 = file3.rfind('.');
	if(pos4 != string::npos)
	{
		string sub4 = file3.substr(pos4);
		cout << sub4 << endl;
	}
	cout << "----------cut----------" << endl;
}
void test_string3()
{
	//取出url中的protocol、domain、uri ———— 网址 = 协议 + 域名 + 资源定位
	string url("http://www.cplusplus.com/reference/string/string/find/");
	size_t i1 = url.find("://");//find也可以找串
	if(i1 != string::npos)
	{
		string protocol = url.substr(0, i1 - 0);
		cout << "protocol:" << protocol << endl;
	}
	size_t i2 = url.find('/', i1 + 3);//find的第四个版本
	if(i2 != string::npos)
	{
		string domain = url.substr(i1 + 3, i2 - (i1 + 3));  
		cout << "domain:" << domain << endl;
	}
	size_t i3 = url.find('/', i2);
	if(i3 != string::npos)
	{
		string uri = url.substr(i3);
		cout << "uri:" << uri << endl;	
		printf("uri:%s\n", uri.c_str());//C语言去输出string做不到,但是提供了c_str
	}
}
int main()
{
	test_string1();
	//3、find
	test_string2();
	test_string3();
	return 0;
}

📝说明

push_back | append | operator+= ❗

如果你要尾插一个字符用 push_back,如果你要尾插一个字符串用 append。个人感觉这是 string 设计的不好的地方,为什么不再重载一个 push_back 呢,非要去搞一个 append 来恶心人,而 append 又重载了六个版本,也没啥太大的价值,比较常用的也就三、四版本。

相对于 push_back、append 更推荐使用 operator+=,因为它可以追加一个字符,也可以追加一个字符串,还可以追加一个对象。

这里我们发现 string 里面没有头插、头删,但它有中间插入、删除。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
insert | erase ❗

可以看到实现了七个版本,这里就了解了解较为常用的,其余简单提一下。

它不仅仅可以中间插入、也可以在头上插入,还可以在尾部插入。

但是对于 insert 能不用就不用,因为效率比较低,它需要挪动数据。
在这里插入图片描述
与 insert 对应的是 erase。
在这里插入图片描述
erase 的第一个版本参数中的 npos 比较特殊,它给的是一个缺省参数,nops 是这个类里的静态成员变量,它是 -1,但实际上不是 -1,因为这个 -1 给了 size_t,所以它转换为补码就是整型的最大的值。
在这里插入图片描述
find | rfind ❗

对于 find 的返回值,一般是 int,找到返回下标,找不到返回 -1。这里用 size_t 的想法是,找到返回下标,找不到会返回 nops,但是正常的字符不可能有那么长。
在这里插入图片描述
查找字符串中内容的最后一次出现:
在这里插入图片描述

substr ❗

生成子串。
在这里插入图片描述
c_str ❗

它可以和 C语言配合使用。
在这里插入图片描述

5、string类非成员函数
函数功能说明
operator+尽量少用,因为传值返回,导致深拷贝效率低
operator>> —— 重点输入运算符重载
operator<< —— 重点输出运算符重载
getline —— 重点获取一行字符串
relational operators —— 重点大小比较
在这里插入图片描述
#include<string>
#include<iostream>
using namespace std;
int main()
{
	string s1("hello");
	s1 + "world";
	cout << s1 << endl;
	s1 += "world";
	cout << s1 << endl;
	cout << "----------cut----------" << endl;
	return 0;
}

📝说明

operator+ 和 operator+= ❗

它们的作用都是尾插,但是 operator+= 会改变当前字符串,而 operator+ 不改变当前字符串。
在这里插入图片描述
relational operators ❗

string 还提供了比较大小的接口,可以看到非常多的版本,比如对象跟对象比、字符串跟对象比,自我感觉比较冗余。

因为字符串是可以被隐式类型转换成 string 的:

string s1("hello");//正常写法,string支持单参数的构造函数
string s2 = "world";//隐式类型转换,先将字符串构造一个string,再将string拷贝构造s2。最后优化成直接构造

所以这里是想说就算是字符串跟对象比,字符串也可以隐式转换成对象,没必要再实现一个版本。

在这里插入图片描述

6、补充
函数功能说明
to_string数值转字符串
stoi字符串转数值
#include<string>
#include<iostream>
using namespace std;
int main()
{
	int i = 1234;
	string str = to_string(i);
	cout << str << endl;
	int j = stoi(str);
	cout << j << endl;
	return 0;
}

📝说明

to_string | stoi ❗

将数值转换为字符串(注意它不是 string 的成员函数,而是全局函数):
在这里插入图片描述
在 C语言中有类似的函数 itoa,它需要自己提供空间,且有些 OJ 上还不支持,因为它不是标准的 C库,所以不够好用。

可以看到它还提供了将字符串转成整数:
在这里插入图片描述

💦 小试牛刀

1、仅仅反转字母<难度系数⭐>

📝 题述:给你一个字符串 s ,根据下述规则反转字符串:

  • 所有非英文字母保留在原有位置。
  • 所有英文字母 (小写或大写) 位置反转。

返回反转后的 s 。

💨示例1:

输入:s = “ab-cd”
输出:“dc-ba”

💨示例2:

输入:s = “a-bC-dEf-ghIj”
输出:s = “a-bC-dEf-ghIj”

💨示例3:

输入:s = “Test1ng-Leet=code-Q!”
输出:“Qedo1ct-eeLg=ntse-T!”

⚠ 提示

  • 1 <= s.length <= 100
  • s 仅由 ASCII 值在范围 [33, 122] 的字符组成
  • s 不含 ‘"’ 或 ‘\’

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

  1. 这里利用快排的思想 —— 下标完成
  2. 逻辑不变 —— 将下标改造成迭代器

leetcode原题

1、

class Solution 
{
public:
    bool isLetter(char ch)//判断是否为字母,如果是返回treu,否则返回false
    {
        if(ch >= 'a' && ch <= 'z')
            return true;
        if(ch >= 'A' && ch <= 'Z')
            return true;
        return false;
    }
    string reverseOnlyLetters(string s) 
    {
        int begin = 0;
        int end = s.size() - 1;
        while(begin < end)
        {
            while(begin < end && !isLetter(s[begin]))//非英文字母,且保证最后begin和end重合
            {
                ++begin;
            }
            while(begin < end && !isLetter(s[end]))//非英文字母,且保证最后begin和end重合
            {
                --end;
            }
            swap(s[begin], s[end]);//交换
            //交换后还要调整
            ++begin;
            --end;
        }
        return s;
    }
};

2、

class Solution 
{
public:
    bool isLetter(char ch)//判断是否为字母,如果是返回treu,否则返回false
    {
        if(ch >= 'a' && ch <= 'z')
            return true;
        if(ch >= 'A' && ch <= 'Z')
            return true;
        return false;
    }
    string reverseOnlyLetters(string s) 
    {
        auto begin = s.begin();
        auto end = s.end() - 1;
        while(begin < end)
        {
            while(begin < end && !isLetter(*begin))//非英文字母,且保证最后begin和end重合
            {
                ++begin;
            }
            while(begin < end && !isLetter(*end))//非英文字母,且保证最后begin和end重合
            {
                --end;
            }
            swap(*begin, *end);//交换
            //交换后还要调整
            ++begin;
            --end;
        }
        return s;
    }
};
2、字符串中第一个唯一字符<难度系数⭐>

📝 题述:给定一个字符串 s ,找到它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1 。

💨示例1:

输入:s = “leetcode”
输出:0

💨示例2:

输入: s = “loveleetcode”
输出: 2

💨示例3:

输入: s = “aabb”
输出: -1

⚠ 提示

  • 1 <= s.length <= 105
  • s 只包含小写字母

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:如果一个字符一个字符的去比较,效率很慢,最坏的情况是 O(N2),所以我们这里建立一个映射关系去统计次数,原理同计数排序

leetcode原题

class Solution 
{
public:
    int firstUniqChar(string s) 
    {
        int count[26] = {0};
        //统计每个字符出现的次数
        for(auto ch:s)
        {
           count[ch - 'a']++;
        }
        //找出字符串中第一个不重复的字符
        for(int i = 0; i < s.size(); ++i)
        {
            if(count[s[i] - 'a'] == 1)
            {
				return i;
            }
        }
        return -1;
    }
};
3、字符串里面最后一个单词的长度<难度系数⭐⭐>

📝 题述:计算字符串最后一个单词的长度,单词以空格隔开,字符串长度小于 5000。(注:字符串末尾不以空格为结尾)。

输入描述:输入一行,代表要计算的字符串,非空,长度小于 5000。

输出描述:输出一个整数,表示输入字符串最后一个单词的长度。

💨示例1:

输入:hello nowcoder
输出:8

⚠ 说明

最后一个单词为 nowcoder,长度为 8。

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:无

nowcoder原题

#include<string>
#include<iostream>
using namespace std;
int main()
{
    string s;
    //cin >> s;
    getline(cin, s);
    size_t pos = s.rfind(' ');
    if(pos != string::npos)//多个单词
    {
        cout << s.size() - (pos + 1) << endl;
    }
    else//一个单词
    {
        cout << s.size() << endl;
    }
    return 0;
}

📝说明

scanf 和 cin 都有一个特点,默认遇到空格或换行,它会认为空格前后是两个独立的变量。

针对这种情况我们就不能用 cin 了,在 string 里提供了一个 getline:

它遇到换行才结束。
在这里插入图片描述

4、验证一个字符串是否是回文<难度系数⭐>

📝 题述:给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。本题中,我们将空字符串定义为有效的回文串。

💨示例1:

输入:“A man, a plan, a canal: Panama”
输出:true
解释:“amanaplanacanalpanama” 是回文串

💨示例2:

输入: “race a car”
输出: false
解释:“raceacar” 不是回文串

⚠ 提示

  • 1 <= s.length <= 2 * 105
  • 字符串 s 由 ASCII 字符组成

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

这道题数字是一个坑,对于字母比较我们可以用小的 + 32 或者全部转小写或大写。

  1. 常规写法,暴力求解,稍不注意就会写得比较复杂,然后过不了
  2. 先将所有的字母转大写或小写

leetcode原题

1:

class Solution {
public:
    bool IsLetterOrNun(char ch)
    {
        if((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9'))
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    bool isPalindrome(string s) {
        int begin = 0, end = s.size() - 1;
        while(begin < end)
        {
            while(begin < end && !IsLetterOrNun(s[begin]))//跳过非字母数字
            {
                ++begin;
            }
            while(begin < end && !IsLetterOrNun(s[end]))//跳过非字母数字
            {
                --end;
            }
            if(s[begin] != s[end])
            {
                //有一个是数字,就不存在大小写比较问题
                if(s[begin] < 'A' || s[end] < 'A')
                {
                    return false;
                }
                else if(s[begin] < s[end] && s[begin] + 32 == s[end])
                {
                    ++begin;
                    --end;
                }
                else if(s[end] < s[begin] && s[end] + 32 == s[begin])
                {
                    ++begin;
                    --end;
                }
                else
                {
                    return false;
                }
            }
            else
            {
                ++begin;
                --end;
            }
        }
        return true;
    }
};

2:

class Solution {
public:
    bool IsLetterOrNun(char ch)
    {
        if((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9'))
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    bool isPalindrome(string s) {
        //大写转小写
        for(auto& ch : s)
        {
            if(ch >= 'A' && ch <= 'Z')
            {
                ch += 32;
            }
        }
        int begin = 0, end = s.size() - 1;
        while(begin < end)
        {
            while(begin < end && !IsLetterOrNun(s[begin]))
            {
                ++begin;
            }
            while(begin < end && !IsLetterOrNun(s[end]))
            {
                --end;
            }
            if(s[begin] != s[end])
            {
                return false;
            }
            else
            {
                ++begin;
                --end;
            }
        }
        return true;
    }
};
5、 字符串相加<难度系数⭐>

📝 题述:给定两个字符串形式的非负整数 num1 和 num2 ,计算它们的和并同样以字符串形式返回。你不能使用任何內建的用于处理大整数的库 (比如 BigInteger:大数运算), 也不能直接将输入的字符串转换为整数形式。

💨示例1:

输入:num1 = “11”, num2 = “123”
输出:“134”

💨示例2:

输入:num1 = “456”, num2 = “77”
输出:“533”

💨示例3:

输入:num1 = “0”, num2 = “0”
输出:“0”

⚠ 提示

  • 1 <= num1.length, num2.length <= 104
  • num1 和 num2 都只包含数字 0 - 9
  • num1 和 num2 都不包含任何前导零

🧷 平台:Visual studio 2017 && windows

🍳 拓展:

有些语言里会提供一个库叫 BigInteger(大数运算):顾名思义,就是很大的数值的数进行一系列的运算。由于编程语言提供的基本数值数据类型表示的数值范围有限,不能满足较大规模的高精度数值计算,因此需要利用其他方法实现高精度数值的计算,于是产生了大数运算。大数运算主要有加、减、乘三种方法。

我们平时表示较大整型时用的是 long long 或 int64,它们都是 8Byte,能表示的范围是 264,已经很大了,大概是一个 20 位的整数(严格来说是 19 位),但是在一些高科技领域要进行科学计算,所以还远远不够,那么怎么处理呢???

这时有人就写出了 BigInteger,它用字符串来表示整数 —— 意味着无论多大的整数都可以表示。

可以看到这道题用 C语言去做,两个数字相加之和是多大,不好开空间,但是用 string 的话就可以不用管了,string 的底层实现了自动增容。

🔑 核心思想 1:
在这里插入图片描述

leetcode原题

class Solution {
public:
    string addStrings(string num1, string num2) {
        string retStr;
        int end1 = num1.size() - 1;
        int end2 = num2.size() - 1;
        int carry = 0;
        while(end1 >= 0 || end2 >= 0)//只要还有一位那么就继续
        {
            int val1 = 0, val2 = 0;
            if(end1 >= 0)
            {
                val1 = num1[end1] - '0';
                --end1;
            }
            if(end2 >= 0)
            {
                val2 = num2[end2] - '0';
                --end2;
            }
            int ret = val1 + val2 + carry;
            if(ret > 9)//进位
            {
                ret -= 10;
                carry = 1;
            }
            else
            {
                carry = 0;
            }
            //头插
            retStr.insert(0, 1, '0' + ret);
            //retStr.insert(retStr.begin(), '0' + ret);//同上
        } 
        if(carry == 1)//还有一位的情况:"1","9"
        {
           retStr.insert(retStr.begin(), '1');
        }
        return retStr;
    }
};

📝说明

假设 num1 和 num2 的长度为 n,则如上代码的时间复杂度是多少 ❓

O(N) ???

就思路来说时间复杂度是 O(N),但是这里使用了 insert,所以原本是 O(N) 的时间复杂度硬是写成了 O(N2)
在这里插入图片描述

🧿 优化版本

🔑 核心思想 2:

每次将结果尾插,最后再逆置。

C++ 在算法这个头文件里提供了此类函数:
在这里插入图片描述
只要有迭代器,它就可以针对任何容器去使用:
在这里插入图片描述

class Solution {
public:
    string addStrings(string num1, string num2) {
        string retStr;
        int end1 = num1.size() - 1;
        int end2 = num2.size() - 1;
        int carry = 0;
        while(end1 >= 0 || end2 >= 0)//只要还有一位那么就继续
        {
            int val1 = 0, val2 = 0;
            if(end1 >= 0)
            {
                val1 = num1[end1] - '0';
                --end1;
            }
            if(end2 >= 0)
            {
                val2 = num2[end2] - '0';
                --end2;
            }
            int ret = val1 + val2 + carry;
            if(ret > 9)//进位
            {
                ret -= 10;
                carry = 1;
            }
            else
            {
                carry = 0;
            }
            //尾插
            retStr += ('0' + ret);
        } 
        if(carry == 1)//还有一位的情况:"1","9"
        {
           retStr += '1';
        }
        //逆置
        reverse(retStr.begin(), retStr.end());
        return retStr;
    }
};

📝说明

在这里插入图片描述

三、string类的模拟实现

💦 经典的string类问题

💨 string.h

#pragma once
namespace bit
{
	class string
	{
	public:
		string(char* str)
			//:_str(str)
			:_str(new char[strlen(str) + 1]
		{
			strcpy(_str, str);
		}
		~string()//当生命周期结束,程序会自动调用析构函数释放资源。
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			return _str[pos];//_str[pos]同*(_str + pos)
		}
	private:
		char* _str;
	};
	void test_string1()
	{
		string s1("hello");
		s1[0] = 'x';

		string s2(s1);
		s2[0] = 'y';
	}
}

💨 string.cpp

#include"string.h"
int main()
{
	bit::test_string1();
	return 0;
}

📝说明
在这里插入图片描述
注意这里对于新一点的编译器会报错(这里是 VS2017),需要使用 const 来修饰 _str。这里因为要演示,所以就不使用了。
在这里插入图片描述
这里 hello 是常量字符串,存储在常量区,然后调了构造函数初始化 _str,然后 s1 [0] = ‘x’ 就会报错(运行时错误)。
在这里插入图片描述
所以这里我们要改就要把常量字符串拷贝到 new 的一块空间,s1[0] = ‘x’ 才对。
在这里插入图片描述
这里没有写拷贝构造函数,编译器会默认生成,对于内置类型,字节序的浅拷贝;自定义类型,会去调用它的拷贝构造完成拷贝,程序结束,调用析构函数,对同一块空间释放两次,程序崩溃,这就是浅拷贝所带来的问题。其中析构两次空间只是其中一个问题,拷贝构造 s2 后,再去 s2[0] = ‘y’; ,s1 也会跟着修改。

解决方法就是 s2 必需是一块独立的空间,也就是深拷贝。

💦 浅拷贝

由如上代码我们了解了浅拷贝会带来两个问题:

  1. 析构两次空间
  2. 其中一个去修改,会影响另一个

浅拷贝也称为位拷贝,编译器只是将对象中的值拷贝过来,如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一个对象不知道该资源已经释放,以为该空间还有效,所以继续对资源进行操作时,就会发生访问违规。所以要解决浅拷贝问题,C++ 引入了深拷贝。同时要明白一个问题,string 做拷贝时,其实是深拷贝,也就意味着要尽量减少它的拷贝构造,因为它除了拷贝值过来,还需要开空间,代价较大,只有迫不得已的时候才用。

浅拷贝只关注美人鱼的上半身,而深拷贝探索到了美人鱼不为人知的下半身:
在这里插入图片描述
在这里插入图片描述

💦 深拷贝

深拷贝会新开辟一块与原对象一样大的空间,再把原对象空间上的值拷贝过来。

1、深拷贝的传统版写法的string类

💨 string.h

#pragma once
namespace bit
{
	class string
	{
	public:
		string(char* str)
			:_str(new char[strlen(str) + 1]
		{
			strcpy(_str, str);
		}
		//s2(s1)
		string(const string& s)
			:_str(new char[strlen(s.str) + 1])
		{
			strcpy(_str, s._str);
		}
		//s1 = s3
		string operator=(const string& s)
		{
			if(this != &s)//防止自己赋值
			{
				/*delete[] _str;//this->_str
				_str = new char[strlen(s._str) + 1];*/
				char* tmp = new char[strlen(s._str) + 1];
				delete[] _str;
				_str = tmp;
				strcpy(_str, s._str);
			}
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			return _str[pos];
		}
	private:
		char* _str;
	};
	void f1(string s)
	{}
	void f2(const string& s)
	{}
	template<class T>
	void f3(T x)
	{}
	void f3(const T& x)
	{}
	void test_string1()
	{
		string s1("hello");
		s1[0] = 'x';

		string s2(s1);
		s2[0] = 'y';
		
		string s3("hello bit");
		s1 = s3;
	
		f1(s1);
		f2(s2);
	}
}

💨 string.cpp

#include"string.h"
int main()
{
	try
	{
		bit::test_string1();	
	}
	catch(exception& e)
	{
		cout << e.what() << endl;	
	}
	return 0;
}

📝说明
在这里插入图片描述
引用的价值更进一步得以体现:f1 是传值传参,这里使用 s1 构造 s,是一个拷贝构造,并且这个拷贝构造是深拷贝;f2 是引用传参,s 是 s2 的别名,不需要拷贝构造。
在这里插入图片描述
对于模板也是一样:f3 里 T 是 int、double 都无所谓,但如果它是一个 string、vector、map 呢,那这要走深拷贝,代价是极大的。所以对于 f3 这种写法是极其不推荐的。
在这里插入图片描述
注意 s1 和 s2 所指向的空间大小不一定相同。实现:直接拷贝不一定对,因为它们各自所指向的空间大小不一定相同,比如 s1 是 6 个有效字符的空间,s2 是 10 个有效字符的空间,直接拷贝就会导致越界。比如 s1 是 100 个有字符的空间,s2 是 5 个有效字符的空间,可以直接拷贝,但是浪费空间。最好的方法就是把 s1 指向的空间释放掉,重新开辟一块与 s2 所指向的空间一样大的空间,再把 s2 所指向的数据拷贝至新空间,最后让 s1 指向新空间
在这里插入图片描述
目前我们写的赋值重载仍有问题:对于如上代码中所有的 new,当 new 失败时,我们没有去捕获异常,对于赋值重载中的 new,如果失败了,还把 s1 给破坏了,赔了夫人又折兵,所以这里可以先 new 空间,再 delete。虽然异常我们还没涉及,但是这里先完善下,不然显然咱不专业。

2、深拷贝的现代版写法的string类
//s2(s1)
string(const string& s)
	:_str(nullptr);
{
	string tmp(s._str);
	swap(_str, tmp._str);	
}
//s1 = s3//版本一
string& operator=(const string& s)
{
	if(this != &s)
	{
		string tmp(s._str);
		swap(_str, tmp._str);	
	}
	return *this;
}
//s1=s3//版本二
string& operator=(string s)
{
	swap(_str, s._str);
	return *this;
}

📝说明
在这里插入图片描述
在这里插入图片描述
至此如上代码还有一点问题:虽然已经达到深拷贝的效果,但是这里的 tmp 是一个局部对象,出了作用域,生命周期结束,它会调用析构函数,而此时 tmp 是一个随机值,这里再释放就会报错,所以说我们在一开始得把它指向空(free 和 delete 释放空都不会有问题)。

现代写法的精髓就是让别人替自己干活,有什么特别的价值呢 ❓

实际上在以后我们写深拷贝时,大部分用的都是现代写法 —— 这里拷贝的是一个数组,如果拷贝的是更复杂的东西时,自己去开空间、拷贝,代价就很大。
在这里插入图片描述
相比版本一这是更正宗的现代写法,版本一已经说过了,就不理解了。版本二是使用传值传参,s3 传给 s,s 就充当了 tmp 的作用 —— string s(s3) 拷贝构造。为什么这里不判断自己赋值给自己呢 ???因为已经深拷贝出来了,判断也已经没有意义了。注意这里并没有改变 s3,因为这里是传值。也就是说,后面在写赋值时,所有的深拷贝都可以用这样的一段代码解决。

传统写法 | 现代写法 ❓

它们俩的效率是一样的。但是:

  1. 传统写法,可读性高,便于理解,但操作性较低
  2. 现代写法,代码更加简洁高效,但是逻辑更加复杂

在以后我们都更倾向于现代写法。

💦 写时拷贝(了解)

在这里插入图片描述

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。

引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源,如果计数为 1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
在这里插入图片描述

当然这种方案也有不好的地方,具体可以参考如下的文章,所以说可以认为这种技术已经脱轨了。

写时拷贝

写时拷贝在读取时的缺陷

证明标准库里没有使用这种技术 ❓
在这里插入图片描述
这里有个函数 c_str,它返回指向字符串的指针,如果 _str 不一样的就是深拷贝,一样就是写时拷贝(除非去修改它,触发深拷贝)。注意这里输出地址时不能用 cout,要用 printf,因为 c_str 返回的是 char*。

Visual Studio 2017:
在这里插入图片描述

Linux g++:
在这里插入图片描述

写时拷贝是在需要修改时在去深拷贝 ❗

Visual Studio 2017:
在这里插入图片描述

Linux g++:
在这里插入图片描述


📝小结

早期 Linux 选择了写时拷贝的技术,而 VS 下选择了直接深拷贝的技术。它们本质都是深拷贝,只是说 Linux 下先做浅拷贝,如果不写就不做深拷贝,写了再去做深拷贝,并且是谁写谁做。我这里 g++ 的版本是 gcc version 4.8.5 20150623,之前好像听说最新的版本已经放弃写时拷贝了,有兴趣的可以去验证下。

💦 string类的模拟实现

💨 string.h

#pragma once
namespace bit
{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;	
		}
		const_iterator begin() const
		{
			return _str;	
		}
		const_iterator end() const
		{
			return _str + _size;	
		}
		/*string()
			:_str(new char[1])
			,_size(0)
			,_capacity(0)
		{
			*_str = '\0';
		}*/
		//string(char* str = "\0")
		string(char* str = "")
			:_size(strlen(str))
			,capacity(_size);
		{
			_str(new char[_capacity + 1]);//预留\0
			strcpy(_str, str);
		}
		//s1.swap(s2)
		void swap(string& s)
		{
			//注意这里的swap不算重载,重载要求的是在同一作用域,就像string和vector里都有push_back,它们函数名、返回值、参数可能都是相同的,那它们俩能同时存在的原因就是它俩在不同的作用域
			::swap(_str, s._str); 
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		//s2(s1)
		string(const string& s)
			:_str(nullptr);
		{
			string tmp(s._str);
			swap(tmp)//this->swap(tmp);	
		}
		//s1=s3
		string& operator=(string s)
		{
			swap(s);//this->swap(s);
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		//读写
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		//读
		const char& operator[](size_t pos) const 
		{
			assert(pos < _size);
			return _str[pos];
		}
		//增容
		void reserve(size_t n)
		{
			if(n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void resize(size_t n, char ch = '\0')
		{
			if(n <= _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				if(n > _capacity)//一次性增容,避免频繁增容所带来的消耗
				{
					reserve(n);
				}
				for(size_t i = _size; i < n; i++)
				{
					_str[i] = ch;	
				}
				_size = n;
				_str[_size] = '\0';
			}		
		}
		void push_back(char ch)
		{
			/*if(_size >= _capacity)//扩容
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				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)//扩容
			{
				reserve(_size + len);
			}	
			strcpy(_str + _size, str);
			_size += len;*/
			//复用insert
			insert(_size, str);
		}
		string& operator+=(char ch)
		{
			push_back(ch);//this->push_back(ch);
			return *this;	
		}
		string& operator+=(const char* str)
		{
			append(str);
			return *this;	
		}
		string& operator+=(const string& s)
		{
			*this += s._str;
			return *this;
		}
		size_t size() const
		{	
			return _size;
		}
		size_t capacity() const
		{
			return _capacity;	
		}
		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* sub, size_t pos = 0)
		{
			const char* p = strstr(_str + pos, sub);
			if(p == nullptr)
			{
				return npos;	
			}
			else
			{
				return p - _str;
			}
			return npos;
		}
		string& insert(size_t pos, char ch)
		{
			assert(pos <= _size);
			if(_size == _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);	
			}
			/*int end = _size;
			while(end >= (int)pos)
			{
				_str[end + 1] = _str[end];
				--end;	
			}*/
			size_t end = _size + 1;
			while(end > pos)
			{
				_str[end] = _str[end - 1];
				--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 == 0)//空串直接返回
			{
				return *this;
			}
			if(len + _size > _capacity)
			{
				reserve(len + _size);
			}
			size_t end = _size + len; 
			while(end >= pos + len)
			{
				_str[end] = str[end - len];	
				--end;
			}
			for(size_t i = 0; i < len; ++i)
			{
				_str[pos + i] = str[i];
			}
			_size += len;
			return *this;
		}
		const char* c_str()
		{
			return _str;	
		}
		string& erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);
			//1、pos后删完
			//2、pos后删一部分
			if(len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);//这里直接调用strcpy即可
				_size -= len;
			}
			return *this;
		}
		void print(const string& s)
		{
			for(size_t i = 0; i < s.size(); ++i)
			{
				//s[i] = 'x'//err
				cout << s[i] << " ";
			}
			cout << endl;
			string::const_iterator it = s.begin();
			while(it != s.end())
			{
				cout << *it << " ";
			}
			cout << endl;
		}
		void clear()
		{
			_str[0] = '\0';
			_size = 0;	
		}
		
	private:
		char* _str;
		size_t _size;
		size_t _capacity;//不包含最后做标识的\0
		static const size_t npos;
	};
	static size_t string::npos = -1;
	string operator+(const string& s1, char ch)
	{
		string ret = s1;
		ret += ch;
		return ret;  
	}
	string operator+(const string& s1, const char* str)
	{
		string ret = s1;
		ret += str;
		return ret;  
	}
	ostream& operator<<(ostream& out, const string& s)
	{	
		for(size_t i = 0; i < s.size(); ++i)
		{
			out << s[i];
		}
		return out;
	}
	istream& operator>>(istream& in, string& s)
	{	
		s.clear();
		char ch;
		//in >> ch;
		ch = in.get();
		while(ch != ' ' || ch != '\n')
		{
			s += ch;	
			//in >> ch;
			ch = in.get();
		}
		return in;
	}
	istream& getline>>(string& s)
	{
		s.clear();
		char ch;
		ch = in.get();
		while(ch != '\n')
		{
			s += ch;	
			ch = in.get();
		}
		return in;	
	}
	bool operator>(const string& s1, const string& s2)
	{
		size_t i1 = 0, i2 = 0; 
		//“abc" "aa" 
		//"aa" "abc"
		while(i1 < s1.size() && i2 < s2.size())
		{
			if(s1[i1] > s2[i2])
			{
				return true;	
			}	
			else if(s1[i1] < s2[i2])
			{
				return false;	
			}
			else
			{
				++i1;
				++i2;	
			}
		}		
		//"abc" "abc" -> false
		//"abcd" "abc" -> true
		//"abc" "abcd" -> false
		if(i1 == s1.size())
		{
			return false;	
		}
		else
		{
			return true;	
		}
	}
	bool operator==(const string& s1, const strint& s2)
	{
		size_t i1 = 0, i2 = 0;
		while(i1 < s1.size() && i2 < s2.size())
		{
			if(s1[i1] != s2[i2])
			{
				return false;	
			}
			else
			{
				++i1;
				++i2;	
			}
		}	
		if(i1 == s1.size() && i2 == s2.size())
		{
			return true;
		}
		else
		{
			return false;	
		}
	}
	inline bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);
	}
	inline bool operator>=(const string& s1, const string& s2)
	{
		return s1 > s2 || s1 == s2;
	}
	inline bool operator<(const string& s1, const string& s2)
	{
		return !(s1 >= s2) 
	}
	inline bool operator<=(const string& s1, const string& s2)
	{
		return !(s1 > s2) 
	}
	
	void test_string1()
	{
		string s1("hello");
		s1.push_back(' ');
		s1.append("world");
		s1 += ' ';
		s1 += "world";
		string s2(s1);
		s2.resize(3);
		s2.resize(8, 'x');
		s2.resize(20, 'x');
	}
	void test_string2()
	{
		//遍历打印
		//1、
		string s1("hello world");
		for(size_t i = 0; i < s1.size(); ++i)
		{
			s1[i] += 1;//普通迭代器可读写
			cout << s1[i] << " ";
		}
		cout << endl;
		//2、
		string::iterator it = s.begin();
		while(if != s1.end())
		{
			*it -= 1;
			cout << *it << endl;
			++it;
		}
		cout << endl;
		//3、
		for(auto e : s1)
		{
			cout << e << " ";
		}
		cout << endl;
	
		print(s1);
	}
	void test_string3()
	{
		string s1("hello world"):
		s1.insert(5, 'x');
		cout << s1.c_str() << endl;
		string s2("helloworld");
		s2.insert(5, "bit");
		cout << s2.c_str() << endl;
		s2.insert(0, "hello");
		cout << s2.c_str() << endl; 
		string s3("helloworld");
		s3.erase(5, 3);
		cout << s3.c_str() << endl;
		s3.erase(5);
		cout << s3.c_str() << endl;
	}
	void test_string4()
	{
		string s1("hello");
		cin >> s1;
		cout << s1 << endl;
		cout << s1.c_str() << endl;
		//以上两种cout的差别点
		string s2("hello");
		s2.resize(20);
		s2[19] = 'x';
		cout << s1 << endl;
		cout << s1.c_str() << endl;
		
		string s3;
		cin >> s3;
		cout << s3 << endl;
	}
	void test_string5()
	{
		//string s1;
		//cin >> s1;
		//cout << s1 << endl;	
		getline(cin, s1);
		cout << s1 << endl;
	}
}

💨 string.cpp

#include"string.h"
int main()
{
	try
	{
		bit::test_string1();	
		bit::test_string2();
		bit::test_string3();	
		bit::test_string4();
		bit::test_string5();
	}
	catch(exception& e)
	{
		cout << e.what() << endl;	 
	}
	return 0;
}

📝说明
在这里插入图片描述

对于默认构造函数,我们之前说过写无参的不如写全缺省的,这里的缺省值可以给 “” 或 “\0”。_str 需要支持增删查改,所以得把原来的空间拷贝到新开的一块空间上进行操作,需要注意的是 _capacity 不包含 \0,也就是说 10 个字符需要有 11 个空间。
在这里插入图片描述
对于普通数组而言,越界读一般是检查不出来的,越界写是抽查,可能会检查出来,可能的意思是数组后面有一个标志位,如果修改了它就会报错,但不可能数组之后都是标志位,所以就可能就会出现往后越界一个位置会报错,往后越界二个位置不会报错的情况。这个在之前有说过,感兴趣的可以去测试下。 但是在 string 里无论是越界读还是越界写都会报错。

为什么到了 string,[ ] 就查的这么严 ???因为 [ ] 是一个函数调用,它对这里的参数进行了严格的检查。
在这里插入图片描述
当 _capacity 满了,push_back 直接扩二倍是可以的,但是 append 扩二倍不一定能满足,因为 append 尾插的是一个字符串。
在这里插入图片描述

对于 resize,string 里有两个版本,第一个版本如果没给值,它默认是用 \0 初始化,第二个版本是用指定字符初始化。其实第一个版本有点累赘了,这里我们直接把第二个版本的第二个参数设为缺省值为 \0 即可,其中我们在实现 resize 时又分为几种情况:

💨 hello\0:_size = 5

  1. n <= _size:_size = 3
  2. n > _size && n < _capacity:_size = 8
  3. n > _capacity:_size = 15

对于第 3 种,我们也有很多种方法实现,比如去循环复用 +=,当 n 远大于当前空间时效率就很低,可能只需要一步到位的代码,却需要频繁增容,如果你不知道 string 的底层你根本不知道要这样写。这也就是我们为什么要去模拟实现 string 的原因。

对于第 2、3 种,我们的循环里的结束条件不能为 i <= n,因为这里要分情况,如果没传 ch,这里写 i <= n 是没问题的,但是如果传的是 x,就不行了,需要在循环后面补 \0,所以干脆 i < n,再在循环后被 \0。
在这里插入图片描述
可以看到迭代器的实现并不难,其实相比 string 和 vector 的迭代器,list 的迭代器就要复杂许多了。string::iterator 就是告诉编译器 iterator 去全局去找是找不到的,要到 string 这个域里去找,迭代器它是一个内嵌类型,所以说它要指定类域。注意这里是一个左闭右开的区间,它是一个普通迭代器,支持读写。
在这里插入图片描述
可以看到我们也没实现范围 for,为什么它依然可以完成呢,其实范围 for 的原理就是被替换成迭代器,范围 for 的语法是说取 s1 里的每个数据,赋值给 e,然后它会自动走,自动判断结束。所谓的这些自动其实没这么神奇,就像模板好像很牛逼,写一个 swap,其它地方都可以用,其实是把一些工作交给编译器去做了。这里也是一样,你可以认为这里的 e 是定义的一个变量,然后把 *it 赋值给 e,所以对 e 的改变不会影响 s1,需要加上引用才行。所以范围 for 的原理就是被替换成迭代器。
在这里插入图片描述
当然空口无凭,这里我们把 begin 改成了 Begin,报错了,以上就能说明范围 for 会被原模原样的替换成迭代器,但是迭代器得跟着规范去走,得用 begin。
在这里插入图片描述
print 就会用到 const 迭代器,对于 const 对象还要去实现一个 const 版本的 operator[],迭代器也要去实现 const 迭代器。注意对于普通对象和 const 对象,operator[] 和迭代器就必须实现两份,而像 size() 实现一份 const 版本就可以满足了。
在这里插入图片描述

注意这里实现的 insert 有问题,当 s1.insert(0, ‘x’) 时,当 end 减到 -1 时,本来应该结束了,但是它还会走,因为这里当无符号和有符号比较时,进行整型提升了,这里的 -1 会被提升为一个很大的正数。解决方法:

  1. 将参数的 size_t pos 改成 int pos,但不符合设计的意义
  2. 在 while 循环里比较时,将 pos 强转为 int
  3. 将 end 的值改成 _size + 1,具体见详细代码

这里就非常考验 C语言的基本功了,如果你不知道这个提升机制,甚至你在调试的时候都会怀疑是编译器的 bug。
在这里插入图片描述
对于erase 的实现,它的第二个参数我们给的是一个缺省值 npos,它是无符号的 -1。
在这里插入图片描述

一般我们重载流提取、流插入都是全局的。在实现日期类时,我们也写过,那里需要使用到友元,但是流提取和流插入必须是友元这句话是不对的,日期类里的友元,是要突破封装访问日期类的年月日,这里不一定要去访问。比如流插入的 operator[] 是公有的,流提取提供的 += 是公有的。
对于上面的 operator<<,我们发现它并不能提取我们输入的字符。其实这里的 in 是拿不到换行符或空格的,C语言中的 scanf 也是这样的,对于 C语言它可以使用 getchar 来解决,这里我们可以看到 C++ 中 cin 对象里也有类似的接口 get。
在这里插入图片描述
其次 operator>> 里还有个问题,当对 s1 字符串继续 cin >> s1 时,新输入的字符串会补在原字符串的后面,所以这里我们还要提供一个 clear 接口,在 operator>> 的第一行 clear。
在这里插入图片描述
以上两种 cout 有无区别 ???

一般情况下没有啥区别,但在某种场景下有区别,这种场景很不容易被发现。这里对 s2 resize 后,它会变成 hello\0\0\0\0…x,此时第一个 cout 会全部输出,因为它是按 _size 走的;第二个 cout 不会输出,因为它遇到 \0 就终止。所以大部分情况我们都用第一种 cout,因为它有保障。
在这里插入图片描述
这里我们输入 hello world,但只获取到了 hello,这个原因是 cin 是以空格或换行结束的,它认为 hello 和 world 是给两个对象的,所以这里就实现了 getline>>。注意这里的实现和 operator>> 是一样的,不同的地方是 getline 遇到换行才结束。
在这里插入图片描述
对于 operator> 写成成员或全局都可以,我们这里根据库里来实现写成全局。注意这里比较的是 ASCII 码。这里比较有如下情况:

  1. “abc”  “aa”
  2. “aa”    “abc”
  3. “abc”  “abc”
  4. “abcd” “abc”
  5. “abc”   “abcd"

当我们实现了 operator>、operator==,那么其它的就可以直接复用了,且实现为 inline,避免建立函数栈帧所带来的开销。
在这里插入图片描述
注意 operator+ 尽量少用,因为它内部需要两次深拷贝。
在这里插入图片描述
对于 find 我们这里只实现查找字符和查找字符串

💦 补充

如下 s1 和 s2 对象的大小为什么是 28 字节 ❓
在这里插入图片描述
在这里插入图片描述
根据我们实现的 string 类,应该只有 12 字节,凭空多出的 16 字节是怎么来的 ?
这其实是 VS 下面的一个技术优化(也可以说是 PJ 版本自己考虑的),调试发现除了 size、capacity、指针,还多了很多东西。
在这里插入图片描述
在这里插入图片描述
也就是说你的字符个数小于 16,它不会去堆上开空间,而是存储在 _Buf 数组里;如果字符个数大于等于 16,它就会存储在 _str 指向的堆上。这种方式的好处就是对于小空间不用去系统去申请了,减少内存碎片。

Linux 下 s1 和 s2 的大小 ❗
在这里插入图片描述

四、扩展阅读

面试中string的一种正确写法

STL的string类怎么啦?

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

跳动的bit

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值