string类

162 阅读23分钟

string类是什么

string类是用来存储字符的类(由类模板实例化出来),

底层是一个动态增长的字符数组

下图是cplusplus.com的内容:

image.png

--为什么要有basic_string类模板

实例化出来的string内部存的是char类型.

除此之外还有wstring、u16string、u32string.

以下是cplusplus.com里的截图:

image.png image.png image.png

char是字符类型,大小1byte,最多表示2^8 = 256种字符,

但世界上的字符远大于256种,为了表示世界上的文字,出现了各种编码表.

--编码表

内存只存储01序列,如何把这些0/1序列转换成各种符号,显示到屏幕上?

去查编码表,编码表上记录了 内存存的值 与 文字符号 的映射关系.

例:

image.png

image.png

内存中存的其实是符号对应的数字,字符/字符串的显示要去查编码表.

ASCII编码表

ASCII(American Standard Code For Information Interchange):

即美国标准信息交换码,最先产生,美国的文字只包含英文数字和标点符号,

ascii编码只编了0~127,负的没有编写,包含所有的美国字符.

char类型有1byte,256种状态足够表示所有的美国字符.

但为了表示多个国家的文字,出现了更多编码表.

其它编码表

utf-8:兼容ASCII,char仍然是8bit,但是像汉字一般用2个char来表示

【ASCII只编了0~127,一般以负数/头几个比特位以固定规律开始的就是汉字】

用ASCII没有编码的映射作为前缀

例:

image.png

最后打印tmp,内容还是“你好”.

utf-16:无法兼容ASCII编码,不管是什么字符,都用2个字节来映射.

utf-32:不管是什么字符,都用4个字节来映射.

gbk:中国出的针对汉字标准,对繁体字/生僻字的支持更好.

--小结

为了让计算机能用数字映射世界上各种字符,

出现了多种编码表:如ASCII、utf-8、utf-16、utf-32

因此也出现多种字符类型:如char、wchar_t、char16_t、char32_t

string类是由类模板basic_string实例化出来的,内部的字符类型是char.

string类基本使用

--构造函数

void test1()
{
	//1 默认构造string()
	string s1;
	
	//2 用字符串初始化string(const char *s)
	string s2 = "abcd";//先用"abcd"构造临时对象,再拷贝构造s2 + 编译器优化 -》直接构造
	//string s2("abcd");

	//3 拷贝构造string(const string& str)
	string s3(s2);//同string s3 = s2

	//string里面重载了<<和>>运算符,
	//可以直接用cout打印string类,用cin输入字符串
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	cin >> s1;
	cout << s1 << endl;
}

注意:它们都存有一个隐藏的'\0'. image.png

4 string (const string& str, size_t pos, size_t len = npos);//部分拷贝构造

从str的pos下标开始,数len个字符,用该子串进行拷贝构造;

另外,如果从pos下标开始第3个参数len超过了后面字符长度,拷贝到结尾

例:

	//从string类"hello world"下标为2位置开始数3个字符,用该子串进行拷贝构造
	string tmp("hello world");
	string s1(tmp, 2, 3);
	cout << s1 << endl;

	//从string类"hello world"下标为3位置开始数999个字符,超过该字符串,直接到结尾
	string s2(tmp, 3, 999);
	cout << s2 << endl;

image.png

cplusplus.com: image.png

npos是string类里的一个静态const变量,size_t是无符号整数,

size_t npos = -1,此时npos是无符号int整型的最大值,即2^32 - 1

说明如果不传最后一个参数,就是拷贝到字符串结尾,例:

	string tmp("newYear");
	string s3(tmp, 3);
	cout << s3 << endl;//输出Year
void test3()
{
	//5 string(const char* s, size_t n);
	//用s的前n个字符,来初始化字符串
	string s1("abcdef", 3);
	cout << s1 << endl; //输出abc
	
	//6 string(size_t n, char c);
	//用n个字符c,初始化字符串
	string s2(3, 'a');
	cout << s2 << endl; //输出aaa
}

--赋值运算符重载

void test4()
{
	string s1(5, 'x');//"xxxxx"
	cout << s1 << endl;
	string s2("abcd");

	//1 operator=(const string& str)
	s1 = s2;
	cout << s1 << endl; //"abcd"

	//2 operator=(const char* str)
	s1 = "hello";
	cout << s1 << endl;//"hello"

	//3 operator=(char x)
	s1 = 'a';
	cout << s1 << endl;//"a"
}

--遍历string里的字符

用重载的[ ]遍历string

string内部的成员变量char*_str(名字不定)指向一块动态开辟的空间,用于存储字符.

虽然它的成员变量char*str或str(名字不定)是私有的,但string类内部实现了[ ]的重载.

即char& operator[](size_t pos)和const char& operator[](size_t pos)const

const string类型会调用const类型operator[]成员函数

普通 string类型会调用普通的operator[]

void test5()
{
	string s1("hello world");
	//可以读写对应位置的字符,会转换成调用operator[]
	cout << s1[0] << endl;
	cout << s1.operator[](0) << endl;
	s1[0] = 'a';
	cout << s1 << endl;

	const string s2("ttt");
	//s2[0] = 'x';会报错
}

此时[ ]重载可以让string类对象像数组一样,访问内部动态空间指向的字符数组

void test6()
{
	string s1(6, 'a');//"aaaaaa"
	for (size_t i = 0; i < s1.size(); ++i)//size()会返回当前字符个数
	{
		cout << s1[i];
		++s1[i];
	}
	cout << endl;
	cout << s1 << endl; // 修改后"bbbbbb"

	//const string可以遍历,但不可以修改
	const string s2("abcd");
	for (size_t i = 0; i < s2.size(); ++i)
	{
		//s2[i]++;会报错
		cout << s2[i];
	}
}

用迭代器遍历string

在string类里有一个内嵌类型iterator,

迭代器就是该内嵌类型,它是用指针typedef出来的,或是定义的内部类.

在string类中,iterator就是char*,在string类内部如下:

typedef char* iterator

void test7()
{
	string s1("abc");
	string::iterator it = s1.begin();//begin()会返回第一个位置的迭代器,指向首元素
	while (it != s1.end())//end()返回最后一个迭代器位置的下一个位置,指向尾元素的下一个位置
	{
          //对于string,!=改成<也可以,因为它底层是连续的物理空间,同时迭代器是原生指针
          //但如果是其它容器不一定行,例list(链表)就不能
		cout << *it << " ";   //输出a b c
		++it;
	}
}

image.png

(1) 像指针一样,迭代器类型iterator通过解引用可以拿到string中的每个字符数据

同时++可以指向下一个数据

(2) begin()和end()返回的迭代器位置,遵循[ 左闭右开 )原则

(3)任何一个类型的迭代器,都是在它内部定义的.有些是typedef出来的,有些是内部类.

例如:要使用string的迭代器,要先指定string这个类域 —— string::iterator

(4)迭代器是所有容器的通用访问方式,屏蔽容器底层的细节,遍历容器内部的每个数据,

同时用法都是类似的

(5) 正向迭代器通过begin()和end()正向遍历,

反向迭代器通过rbegin()和rend()支持反向遍历

void test8()
{
	string s1("abcd");
	//反向迭代器类型名字——reverse_iterator,
	//提供rbegin()和rend()用于反向遍历容器
	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout << *rit << " ";
		++rit;//不管正向还是反向,都是++
	}
}

(6)普通对象调用begin()返回普通迭代器,const对象调用begin()会返回const迭代器

void test9()
{
	const string s1("abcd");
	//const正向迭代器类型:const_iterator
	string::const_iterator const_it = s1.begin();
	while (const_it != s1.end())
	{
		cout << *const_it << " ";
		const_it++;
	}

	//const反向迭代器类型:const_reverse_iterator
	string::const_reverse_iterator const_rit = s1.rbegin();
	while (const_rit != s1.rend())
	{
		cout << *const_rit << " ";
		++const_rit;
	}
}

因为库里的begin()有2种,构成函数重载:

1 iterator begin(); //普通对象调用

2 const_iterator begin()const;//const对象调用

end()、rend()、rbegin()同理.

(7) 总共4种迭代器类型:

iterator、reverse_iterator;

const_iterator、const_reverse_iterator

补充:

可以用一个迭代器区间构造一个string对象,注意这个区间都是左闭右开

	string s1("abcd");
	string s2(s1.begin(), s1.end());
	cout << s2 << endl;
        
	string s3(s1.rbegin(), s1.rend());
	cout << s3 << endl;

image.png

--插入字符/字符串

1 调用内部普通函数尾插

void test10()
{
	string s1("hello");
	//void push_back(char c) 尾插一个字符c
	s1.push_back('s');
	cout << s1 << endl;

	//string& append (const char* s); 尾插一个字符串
	s1.append("world");
	cout << s1 << endl;

	//string& append(const string& str)
	string tmp("aaa");
	s1.append(tmp);
	cout << s1 << endl;
}

2 调用+=运算符重载尾插

void test11()
{
	string s1("abc");
	//+=一个字符
	s1 += 'd';
	cout << s1 << endl;
	
	//+=一个常量字符串
	s1 += "efg";
	cout << s1 << endl;

	//+=一个string对象
	string s2("hijk");
	s1 += s2;
	cout << s1 << endl;
}

3 在pos位置插入字符串/字符

string& insert(size_t pos, const char* s)

string& insert(size_t pos, const string& str)

string& insert(size_t pos, size_t n, char c)//在pos位置插入n个字符c

底层是连续的数组空间,在任意位置插入数据要挪动数据,挪动一次是O(n)

void test14()
{
	string s1("it is sunny");
	//在每个空格处插入@_
	for (size_t i = 0; i < s1.size(); )
	{
		if (s1[i] == ' ')
		{
			//string& insert (size_t pos, const char* s);在pos位置,插入s
			s1.insert(i, "@_");
			//注意当前空格的下标改成了i+2
			i += 3;//避开已经遍历的空格
		}
		else
			i++;
	}
	cout << s1 << endl;
}

image.png

--删除

void test15()
{
	//string& erase(size_t pos = 0, size_t len = npos);
	//从pos位置开始删除len个数据,若len大于pos之后的字符数,删到结尾
	string s1("abcdefg");
	cout << s1.erase(2, 3) << endl;//"abfg"
	
  //iterator erase(iterator p);
	//删除迭代器某个位置的字符
	string s2("utf-8");
	s2.erase(s2.begin() + 1);//删除t
	cout << s2 << endl;

	//iterator erase(iterator first, iterator last);
	//删除某个迭代器区间的字符串[左闭右开),不会删除last对应位置的字符
	string s3("utf-16");
	s3.erase(s3.begin() + 1, s3.end() - 1);//删除"tf-1"
	cout << s3 << endl;
}

--查找

(1) 正向找

size_t find(const string& str, size_t pos = 0)const;//pos位置开始查找一个string对象

size_t find(const char* s, size_t pos = 0)const;//查找一个常量字符串

size_t find(char c,size_t pos = 0)const;//查找一个字符

如果找到,返回找到的第一个字符位置/字符串首位置

如果没找到,返回npos = -1,但由于返回类型size_t,所以实际返回的是无符号整型最大值.

string substr(size_t pos = 0, size_t len = npos)const //取当前string一部分,构造子串

例1:找到文件后缀,并打印出来

void test19()
{
	string fileName("test.cpp");
  //找到.的位置
	size_t ret = fileName.find('.');
	if (ret == -1)
		cout << "无后缀" << endl;
	else
	{
          //从ret位置开始,到结尾,就是文件后缀
		string suff = fileName.substr(ret, string::npos);
		cout << suff << endl;
	}
}

(2)反向找

size_t rfind(char c, size_t pos = npos)const//返回最后一次出现字符c的位置

例2:若文件有多个后缀,要求找到真后缀(最后一个后缀)

test.cpp.tar

void test20()
{
	string fileName("test.cpp.tar");
	size_t ret = fileName.rfind('.');
	if (ret == string::npos)
		cout << "无后缀" << endl;
	else
	{
		string suff = fileName.substr(ret);
		cout << suff << endl;
	}
}

(3) 应用

将任意的网址url分割成3个部分.

URL是由那3部分组成请列举_百度知道 (baidu.com)

网址url由3部分构成:协议名、存有该资源的主机IP地址、资源的具体地址

例:legacy.cplusplus.com/reference/s…

协议名:https

存有该资源的主机IP地址:legacy.cplusplus.com

资源的具体地址:reference/string/string/?kw=string

void splitUrl(const string& url)
{
	 //"https://legacy.cplusplus.com/reference/string/string/?kw=string";
	
	 //1 先找 :// 或 : 分割出协议
	size_t pos1 = url.find(':');
	if (pos1 == string::npos)
	{
		cout << "url非法" << endl;
		exit(-1);
	}
	string agreement = url.substr(0, pos1 - 1);
	cout << "协议名:" << agreement << endl;

	//2 在://位置之后,找到第一个/,分割出主机ip
	size_t begin = pos1 + 3;
	size_t pos2 = url.find('/', begin);
	if (pos2 == string::npos)
	{
		cout << "url非法" << endl;
		exit(-1);
	}
	string ip = url.substr(begin, pos2 - begin);
	cout << "主机IP地址:" << ip << endl;

	//3 分割出资源具体地址
	 string specificAddress = url.substr(pos2 + 1);
	 cout << "资源具体地址:" << specificAddress << endl;
}

image.png

--容量相关

1 size_t capacity() const; //返回当前string可存储有效字符的容量

2 void reserve(size_t n = 0);//提前开好可存储n个有效字符的容量

如果提前知道要插入的字符数目,可以用reserve提前开好空间,

从而减少扩容的消耗.

void test12()
{
	string s3("abcd");
	cout << "当前存储的字符数:"<< s3.size() << endl;
	cout << "当前容量:" << s3.capacity() << endl;
	s3.reserve(1000);//扩容到1000,但因为内存碎片一些问题,会对齐一下出现偏差
	cout << "扩容后存储的字符数:" << s3.size() << endl;
	cout << "扩容后容量:" << s3.capacity() << endl;
}

image.png

3 void resize(size_t n);//修改size的值为n,同时初始化新增的空间为0

void resize(size_t n, char c)//修改size的值为n,同时初始化新增的空间为c

void test13()
{
	string s1("abcd");
	s1.resize(99);

	string s2("aaaa");
	s2.resize(1);
	cout << s2 << endl;//"a"

	string s3("bbbb");
	s3.resize(7, 't');
	cout << s3 << endl;//"bbbbttt"
}

--c_str函数

1 网络上c接口函数,虽然c++兼容c,但c接口在传参时,不支持用string类对象传参.

const char* c_str()const//返回c形式的字符串,以'\0'结尾

例:

void test16()
{
	string fileName("源.cpp");
	//假设现在要用c接口读取当前文件 FILE* fopen(const char* _FileName, const char* _Mode)
	//fopen(fileName, "r");此时string类型的fileName不能直接传过去,要用c_str返回c形式字符串
	//c形式的字符串:const char*类型,指向一个字符串常量 或者 就是一个字符数组(都以'\0'结尾)
	FILE* fr = fopen(fileName.c_str(), "r");
	char ch = fgetc(fr);
	while (ch != EOF)
	{
		cout << ch;
		ch = fgetc(fr);
	}
}

image.png

2 【cout << string对象】 与 【cout << c字符串】区别

(1)调用不同的运算符重载

void test17()
{
	string s1("abcd");
	//调用重载的<<运算符 ostream& operator<<(ostream& os,const string& str)
	cout << s1 << endl;

	//调用重载的<<运算符 ostream& operator<<(ostream& os, const char* s) 
	cout << s1.c_str() << endl;
}

下图来自cplusplus.com:

image.png

image.png

(2) cout打印string对象 只认 size,cout打印c字符串 只认 '\0'.

void test18()
{
	string s1("abcd");
	s1 += '\0';
	s1 += "ttt";

	cout << "string类型的字符串打印:"<<s1 <<",s1.size()为" <<s1.size() << endl;
	cout << "c类型的字符串打印:" << s1.c_str() << endl;
}

image.png

--getline函数

当要输入一个包含 空格 的字符串时,要用getline才能输入

void test22()
{
	string s1;
	//istream& getline(istream & is, string & str),默认以换行符终止输入
	getline(cin, s1);
	cout <<"输入后:"<< s1 << endl << endl;

	string s2;
	//istream& getline(istream & is, string & str, char delim);自定义delim符号终止输入
	getline(cin, s2,'@');
	cout <<"输入后:" << s2 << endl;
}

image.png

--字符串转数字 与 数字转字符串

以下函数是文件下的全局函数,不是成员函数.

(1) to_string函数:用于把数字转换成字符串,重载了多种数字类型,如int、double、long long等

(2) 字符串转数字:stoi(字符串转整数)、stod(字符串转浮点数)等函数,以stoi为例:

int stoi (const string& str, size_t* idx = 0, int base = 10)

只需要传指定字符串,就能转换成10进制数字

idx:做输出型参数,可以置空,

若不为空,函数内部会把转换出的数字长度交给*idx

base:进制

void test23()
{
	int a = 0;
	double b = 3.3;
	
	//string to_string(int a) 
	string aStr = to_string(a);

	//string to_string(double a)
	string bStr = to_string(b);
	
	cout << "转换成功:" << aStr << " " << bStr << endl;

	string s1 = "123";
	string s2 = "13.2";
	int tmp1 = stoi(s1);
	double tmp2 = stod(s2);
	cout << "转换成功:" << tmp1 << "  " << tmp2 << endl;

	string s3 = "345699  tt";
	size_t idx = 0;
	int tmp3 = stoi(s3, &idx , 10);
	cout << tmp3 <<" 转换的数字长度为:" << idx;
}

image.png

string模拟实现

namespace yh
{
    class string
    {
     public:
     private:
           char* _str;//指向动态开辟的空间
           size_t _size;//当前有效字符的个数
           size_t _capacity;//当前存储有效字符的空间大小,不包含'\0'
    };
}

--默认构造函数和析构函数

(1) 无参构造函数

不能用nullptr初始化_str,因为cout打印c字符串,会首先对每个字符解引用

使得调用c_str()接口后,不一定可以用cout进行打印.

		string()
			:_str(nullptr)//如果用nullptr初始化会引发上述问题
			,_size(0)
			,_capacity(0)
		{}    
		string s1;
		cout << s1.c_str() << endl;//cout对于char*类型,都会先解引用再打印,如果是nullptr会崩溃
		char* tmp = nullptr;
		cout << tmp << endl;//都会崩溃      

对于string s1,要开1byte用来存储'\0'

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

(2) 用常量字符串构造string

string(const char* str)

首先,不能把str(const char* 类型)直接赋给_str(char* 类型),这会导致权限的扩大

第二,给_str开空间,要多开一个'\0'空间,再把str的数据拷贝过去

		string(const char* str)//用常量字符串初始化string
			//:_str(str) 权限的扩大
			:_str(new char[strlen(str)+1])//开空间,注意'\0'
			,_size(strlen(str))
			,_capacity(strlen(str))
		{
			strcpy(_str, str);
		} 
 

(3) 把上述2个构造函数合并,并改进为一个全缺省的构造函数

string(const char* str = "");

首先,空字符串""包含了'\0',把它作为缺省值

第二,strlen是O(n)的函数,可以先用size记录,然后再用来初始化初始容量和大小

		string(const char* str = "")
		{
			//只调用一次strlen
			int size = strlen(str);
    
                  //设置初始容量和大小
			_capacity = _size = size;
                  //开空间
			_str = new char[_capacity + 1];
                  //拷贝数据
			strcpy(_str, str);
		}    

(4) 析构函数

		~string()
		{
			delete[] _str;
		}

--拷贝构造函数和赋值运算符重载

(1) 浅拷贝出现的问题

默认的拷贝构造函数和赋值运算符重载,会进行值拷贝(浅拷贝),

此时它们内部的_str会指向同一块空间

image.png

问题一:s2修改,s1也会跟着修改,因为它们的_str指向同一块空间

问题二:同一块空间在析构(析构函数清理对象在堆区申请的空间)时,会释放2次,报错

(2) 使用深拷贝方式解决,s2单独申请一块新空间,把s1指向的内容拷贝到新空间里

拷贝构造函数1(传统写法——自己开空间):

		string(const string& str)
			:_size(str._size)
			,_capacity(str._capacity)
		{
			//开新空间,多开一个给'\0'
			_str = new char[_capacity + 1];

			//把str的内容拷贝到新空间
			strcpy(_str, str._str);
		}

拷贝构造函数2:

通过调用已有的构造函数,生成与待拷贝的string相同内容的对象,

最后交换tmp与*this,不需要手动拷贝.

		void swap(string& s)
		{
			//该交换成员函数,用来交换两个对象的所有成员
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
    
		string(const string& str)
		{
			//调用已有的string(const char* str = "")构造临时tmp
			string tmp(str._str);

			//然后*this和tmp所有成员进行交换
			this->swap(tmp);

			//因为tmp是局部对象,出了作用域调用析构函数,
			//与*this交换所得的空间是随机值,会崩溃
			//所以交换后要修改tmp,阻止上述情况发生
			tmp._str = nullptr;//delete[]内部有检查空指针,为空什么都不干
		}                

image.png

赋值运算符重载1(传统写法 —— 手动开空间,手动释放旧空间,手动拷贝):

注意判断自己给自己赋值

被赋值的对象,要注意清理原先申请的空间,否则会有内存泄漏

		string& operator=(const string& str)
		{
			if (this != &str)//如果不是自我赋值
			{
				//先开新空间拷贝数据,new失败后,不会破坏_str
				char* tmp = new char[str._capacity + 1];
				strcpy(tmp, str._str);

				//把this原来的空间释放
				delete[] _str;

				_str = tmp;
				_size = str._size;
				_capacity = str._capacity;
			}
			return *this;
		}

赋值运算符重载2:利用传参的拷贝构造出和s1数据相同的对象,

此时*this只需要与str交换成员即可

出了作用域,str自动销毁,帮助释放旧空间.

          //s2 = s1
		string& operator=(string str)//调用拷贝构造生成str
		{
                  //*this和str所有成员进行交换
			swap(str);
			//str是局部对象,出了作用域自动销毁,帮助释放原来s2的旧空间
			return *this;
		}

--string的迭代器实现

string里的迭代器iterator类型本质就是原生指针char*

在类内部:typedef char* iterator;

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

		typedef const char* const_iterator;
		const_iterator begin()const
		{
			return _str;
		}
		const_iterator end()const
		{
			return _str + _size;
		}
		string s1("abcdefg");
		string::iterator it = s1.begin();
		while (it != s1.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
		cout << "for循环遍历:";
		//for(:)底层调用的就是迭代器,一个类实现了迭代器,就能用for(:)遍历
		for (auto x : s1)
		{
			cout << x << " ";
		}

image.png

image.png

--插入字符/字符串

(1) 插入字符/字符串,一定会涉及扩容

void reserve(size_t n);//提前开好可存储n个有效字符的容量

		//开存储n个有效字符的空间,不会缩容
		void reserve(size_t n)
		{
			//如果需要的空间n大于当前容量
			if (n > _capacity)
			{
				//开新空间,把旧空间的数据拷贝过去
				char* tmp = new char[n+1];
				strcpy(tmp, _str);
				//释放旧空间
				delete _str;

				//更新当前对象*this的成员
				_str = tmp;
				_capacity = n;
			}
		}
		
		size_t capacity()const
		{
			return _capacity;
		}
 //该函数在命名空间yh中
	void test25()
	{
		string s1("abcd");
		cout << s1.capacity() << endl;
		s1.reserve(100);
		cout << s1.capacity();
	}

image.png

(2) 尾插字符

		void push_back(char x)
		{
			if (_capacity == _size)
			{
				//注意_capacity初始值为0的情况
				reserve(_capacity == 0 ? 5 : _capacity * 2);
			}
			//放数据
			_str[_size] = x;
			++_size;
			//注意'\0'
			_str[_size] = '\0';
		}
    
 		string& operator+=(char x)
		{
			//复用push_back
			push_back(x);
			return *this;
		}

(3) 尾插字符串

		void append(const char* str)
		{
			size_t length = strlen(str);

			//此时的容量必须能存下所有字符
			if (_capacity < _size + length)
			{
				reserve(_size + length);
			}
			//_str+_size 即string最后一个有效数据的下一个位置
			strcpy(_str + _size, str);
			_size += length;
		}
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

image.png (4) 在任意位置插入字符

		//在pos位置插入一个字符
		string& insert(size_t pos, char ch)
		{
			assert(pos <= _size);//=_size相当于尾插
			reserve(_size + 1);//判断扩容
			
			//从后往前挪数据
			//把前一个位置的字符赋值给当前位置的字符
			size_t pre = _size - 1;
			size_t  cur= _size;//初始位置为'\0'
			while(cur > pos)
			{
				//把 前一个位置数据 挪到 当前位置
				_str[cur] = _str[pre];
				pre--;
				cur--;
			}
			//修改pos位置数据,并++_size
			_str[pos] = ch;
			++_size;

			//最后一个字符的下一个位置为'\0'
			_str[_size] = '\0';
			
			return *this;
		}                                             
	void test27()
	{
		string s1("abc");
		cout << s1.c_str() << endl;
		s1.insert(0, 't');
		cout << s1.c_str() << endl;//"tabc"
		s1.insert(1, 'g');
		cout << s1.c_str() << endl;//"tgabc"		
	}

image.png

(5) 任意位置插入字符串

image.png

总思路

定义前后下标pre和cur指向_size和_size+length的位置,

_str[cur] = _str[pre]

然后同时--,将插入位置之后的值全部挪到后面.最后再放入字符串.

细节

1 判断挪动字符终止的条件

当pre < pos 位置时(即pre = pos - 1),

此时pos位置之后(包括pos位置)的字符已经全部挪动,

但是size_t pre,当头插时,pre会刚好减到无符号整型最大值,导致死循环.

所以判断条件用cur:

pre = pos - 1;

cur = pre + length(插入的字符串长度);

-》cur = pos+length - 1,

即当cur减到pos+length-1时,所有字符挪动完成,可以开始插入

2 插入的是空字符,不做处理

此时cur和pre是相等的,头插情况下cur也会一起减到无符号整型最大值.

               //在pos位置插入一个字符串
		string& insert(size_t pos, const char* str)
		{
			//插入位置判断
			assert(pos <= _size);

			//如果是空串,不进行操作
			if (strlen(str) == 0)
				return *this;
			//扩容判断
			size_t length = strlen(str);
			reserve(length + _size);

			//前后两个指针,
			size_t cur= _size + length;
			size_t pre = _size;
			while (cur > pos+length - 1)
			{
				_str[cur] = _str[pre];
				--cur;
				--pre;
			}
			for (int i = 0; i < length; i++)
			{
				_str[pos + i] = str[i];
			}
			_size += length;
			return *this;
		}

  void test28()
	{
		string s2("aaa");
		s2.insert(1, "bc");//"abcaa"
		cout << s2.c_str() << endl;
		s2.insert(5, "gg");//"abcaagg"
		cout << s2.c_str() << endl;
		s2.insert(0, "uu");//"uuabcaagg"
		cout << s2.c_str() << endl;

		string s3;
		s3.insert(0, "");//插入空串
		cout << s3.c_str();
	}

image.png

--删除字符/字符串

string& erase(size_t pos, size_t len = npos)

(1)npos是string类的const静态成员变量

对于该类成员,c++特殊处理,不需要在类外面定义,直接在声明地方就可以定义

image.png

(2)情况分类

情况1:当len = npos时,删到结尾

情况2:当pos + len >= _size时,删到结尾

情况3:删除0个字符,len = 0

情况4:删除某个区间的所有字符

image.png

	void test29()
	{
		string s1("abcdefgh");
		s1.erase(0, 2);//头删 "cdefgh"
		cout << s1.c_str() << endl;

		s1.erase(1, 2);//中间删"cfgh"
		cout << s1.c_str() << endl;
	}                                                  

--查找字符/子串

          //查找一个字符                          
		size_t find(char c, size_t pos = 0)const
		{
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == c)
					return i;
			}
			return npos;
		}
		size_t find(const char* sub, size_t pos = 0)const
		{
			//复用c语言库的strstr
			//const char* strstr(const char* str1, const char* str2);
			//char* strstr(char* str1, const char* str2);
			//如果str2是str1的子串,返回str1对应指针位置,否则返回NULL

			const char* tmp = strstr(_str + pos, sub);
			if (tmp == nullptr)
			{
				return npos;
			}
			else
			{
				return (tmp - _str);//指针-指针起始就是其下标位置,因为它们是连续物理空间
			}
		}

--string的流插入流提取运算符重载

(1) 关于全局对象cin和cout

库里定义了两个类型,分别叫ostream输出流类和istream输入流类.(在头文件iostream中)

而cout是ostream类型的全局对象,cin是istream类型的全局对象

在ostream类型里,重载了<<运算符;

在istream类型里,重载了>>运算符.

库里写好了运算符重载+各种内置类型构成函数重载.

cplusplus.com: image.png

image.png

void testn()
{
	int i = 0;
	double d = 1.0;
	cin.operator>>(i);  // cin >> i;  
	cin.operator>>(d); //cin >> d;
	
	cout.operator<<(i)<<endl; //cout << i << endl;
	cout.operator<<(d);           //cout << d;
}

image.png

(2) 实现string的流插入和流提取运算符

	//1 不能实现为成员函数,如果实现为成员函数,this指针抢占左操作数
	//2 不一定要实现为string的友元函数,因为不需要访问string的私有成员
	ostream& operator<<(ostream& out, const string& str)
	{
		for (int i = 0; i < str.size(); i++)
		{
			out << str[i];
		}
		//支持连续流插入
		return out;
	}

image.png

cout << s1 <<s2 << endl:从左向右执行,先执行cout<<s1,会返回一个ostream&对象,

然后继续用这个对象调用上面实现的string流插入运算符重载,这就实现了连续流插入.

  istream& operator>>(istream& in, string& str)
	{
		char ch;
		//in >> ch;
		//cin是会忽略掉输入的空格或者换行,不会把空格和换行交给ch
          //清空str,也可以调用clear()
		str = "";
		ch = in.get();//它有一个get()成员函数,可以逐个字符读取
		while (ch != ' ' && ch != '\n')
		{
			str += ch;
			ch = in.get();
		}
		//在接收输入的数据时,cin默认以' '或'\n' 作为数据的分割符,但cin不会去接收它们
		return in;
	}                                                         

image.png

但是逐个插入str中,扩容频繁,效率不高.

优化思路:解决扩容频繁问题,在内部定义一个buff数组,一批一批去加

  istream& operator>>(istream& in, string& str)
	{
         //先清空str,也可以调用clear()
		str = "";
		char ch;
		ch = in.get();
		char buff[64];
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 63)
			{
				buff[i] = '\0';
				//如果缓冲区已满,就一次性给str,可以有效减少扩容的消耗
				str += buff;
				i = 0;
			}
			ch = in.get();
		}
	
		buff[i] = '\0';
		str += buff;
		return in;
	}    

--resize函数

void resize(size_t n)

void resize(size_t n, char c)

1 特性

若n < 当前的数据量,尾删数据, 不做任何处理.

若n == 当前数据量,不做任何处理.

若n > 当前数据量,将多出的空间初始化为c或'\0'

  std::string s1("abcd");
	s1.resize(50, 'x');
	cout << "resize(50, 'x')后:s1的数据个数:" << s1.size() << endl;
	cout << s1 << endl<<endl;

	s1.resize(5);
	cout << "resize(5)后:s1的数据个数:"<<s1.size() << endl;
	cout << s1 << endl << endl;

	s1.resize(4, 'u');
	cout << "resize(4,'u')后:s1的数据个数:" << s1.size() << endl;
	cout << s1 << endl << endl;

	s1.resize(4);
	cout << "resize(4)后:s1的数据个数:" << s1.size() << endl;
	cout << s1 << endl;

image.png

2 模拟实现

		void resize(size_t n, char ch = '\0')
		{		
			if (n == _size)//不处理
				return;
			else if (n < _size)//删除数据
			{
				_size = n;
				_str[_size] = '\0';
			}
			else//扩容,同时初始化多出的空间
			{
				reserve(n);
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}
				_size = n;
				_str[_size] = '\0';
			}
		}