C++容器——string的使用(上)

一.STL简介

STL(standard template libaray- 标准模板库 ) : C++ 标准库的重要组成部分 ,不仅是一个可复用的
组件库,而且 是一个包罗数据结构与算法的软件框架
STL包含六大组成部分,如下图所示:

其中最主要的部分为容器和算法。容器可以理解为之前C语言中的各种数据结构,算法就是对这些数据的处理方法,包含常见的例如交换、查找、排序等。
接下来的博客,将会按照:基本容器的使用方法+容器的基本底层实现来展开。
容器的使用将围绕 cplusplus.com - The C++ Resources Network这个网址中的容器函数来展开,会介绍其最基本的类的默认成员函数、容器的增删查改等

二.string的使用

string类,可以理解为是一个存放字符串的数组。

2.1string的构造函数(包括拷贝构造)

如上图所示,string的构造函数有7种。当前阶段重点应用到前6种。

cpp 复制代码
	//string的几种构造

	string s1;//不含参数

	string s2("Hello world");//常量字符串构造

	string s3("Hello world", 5);//用常量字符串的前n个字符构造

	const char* str = "Hello world";
	string s4(str, 5);//与上面相同,常量字符串构造

	string s5(10, 'c');//n个单字符构造

	//拷贝构造
	string s6(s2);
	string s7(s2, 1, 5);//从下标1开始,到下标5结束的位置构造
	string s8(s2, 1);//最后一个参数缺省默认为npos,默认取到最后

1.不含参数的默认构造

2.利用常量字符串的构造

3.利用常量字符串的前n个字符的构造

4.利用n个单字符构造

5.拷贝构造

6.利用某个下标段的位置拷贝构造,如果超过最后一个字符的下标,也是取到最后一个字符

7.如果最后一个参数缺省,那么默认取到最后一个字符的位置,缺省参数为npos(size_t -1)。

2.2赋值运算符重载

cpp 复制代码
	//赋值运算符重载
	s8 = s3;
	s8 = str;
	s8 = 'x';

赋值运算符重载的三种类型:

1.两个string对象直接赋值

2.常量字符串的赋值

3.单字符的赋值

2.3string类的几种遍历方式

2.3.1 \[\]运算符重载

\[\]重载了两种,第二种主要目的是为了针对const string类型的对象实现取下标。

第一种返回类型为char&,表示其可支持\[\]取出元素的修改。

第二种返回类型为const char& ,仅可取出,不可修改。

cpp 复制代码
	string s1("Hello world");
	cout << s1<<endl;//cout运算符重载,可以直接输出string类对象

	//[]运算符重载,可以进行修改
	for (size_t i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << " ";
		++s1[i];
	}
	cout << endl;

	for (size_t i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << " ";
	}
	cout << endl;

	const string s2("hello world!");

	for (size_t i = 0; i < s1.size(); i++)
	{
		cout << s2[i] << " ";
		//++s2[i];//s2为const的string类型对象,会调用const的[]运算符重载,返回类型为const char&,不能进行修改
	}
	cout << endl;
	

\[\]重载后的调用和基本使用方法与数组的\[\]下标类似。值得注意的是,由于非const对象的返回值类型为引用,所以可对\[\]取出的值进行修改。const string类型的返回值为const char&类型,不可进行修改。这一点需要格外注意。

2.3.2 迭代器遍历

迭代器在此处不做详细讲解,先说明其基本用法,后续具体实现的过程中再具体讲解。

基本用法如下:

cpp 复制代码
	//迭代器进行遍历
	string s3("Hello world");

	string::iterator it1 = s3.begin();
	//可以看作类似于指针的用法
	while (it1 != s3.end())
	{
		cout << *it1 << " ";
		it1++;
	}
	cout << endl;

	//迭代器也可对其进行修改
	it1 = s3.begin();
	while (it1 != s3.end())
	{
		cout << ++(*it1) << " ";
		it1++;
	}
	cout << endl;

迭代器的使用:string::iterator it = s1.begin();需要指明迭代器所属的类,然后再调用string类中的begin函数返回一个迭代器对象并赋值给it。

在使用迭代器的时候,一版通过*it来实现取出类中的元素,所以可以将其理解为类似于指针一样的东西。不同的容器对于迭代器的调用方式相似,但其底层实现并不相同。从这点也能看出迭代器的几大优点:

复制代码
 迭代器的意义:
1:统一类似的方式遍历修改容器
2:算法脱离具体底层结构,和底层结构解耦,
3:算法独立模板实现针对多个容器处理

迭代器是类似于指针的用法,*迭代器表示取到该对象的内容。如果针对于const对象,则需要利用const_iterator,注意写法。

cpp 复制代码
	//普通对象用普通迭代器iterator
	//const对象用const迭代器 const_iterator
	string s1("hello world");

	const string s2(s1);

	string::iterator it1 = s1.begin();
	string::const_iterator it2 = s2.begin();
	
	//string::iterator it3 = s2.begin();//err

普通对象用普通迭代器,const对象用const迭代器。const_iterator本质是不能修改内容,而非不能修改迭代器本身。

反向遍历就需要用到反向迭代器,reverse_itetator

cpp 复制代码
	//如果想反向遍历,用反向迭代器
	string::reverse_iterator rit1 = s1.rbegin();
	while (rit1 != s1.rend())
	{
		cout << *rit1 << " ";
		rit1++;
	}
	cout << endl;

2.3.3 范围for

在讲解范围for之前,先说明一个C++中引入的新语法,auto。

auto可以自动识别类型,如下所示:

cpp 复制代码
//auto类型
//可自动识别类型
auto x = 1;
auto y = 1.1;
auto z = 'c';

//如果是地址,下面这两种写法均可
auto a = &x;
auto* pa = &x;

//如果是引用,需要是auto&
auto& rx = x;

auto自动识别类型的过程中,要注意对于指针和引用的方式。下面说明范围for的使用方式:

cpp 复制代码
	//范围for
	//范围for本质是将其转换为*it。
	string s4("hello world");
	for (auto e : s4)
	{
		cout << e << " ";
	}
	cout << endl;
// 自动取容器数据赋值给e
// 自动判断结束
// 自动迭代

	for (auto& e1 : s4)
	{
		e1++;
	}
	cout << endl;

使用方式:auto e(可以自由起的一个名字) : s4(类对象)

范围for的本质是将e 转换为迭代器的 *it 来实现。要注意其本质是一个拷贝出来的临时对象,对它进行修改并不能影响原string对象中的内容。

如果要通过范围for对其进行修改,需要引用类型。

总结范围for的优点:

自动取容器数据赋值给e
自动判断结束
自动迭代

2.4string类的一些常用函数

2.4.1查询容量相关函数,size,capacity等

cpp 复制代码
string s1("hello world");

cout << s1.size()<<endl;
cout << s1.length() << endl;
cout << s1.max_size() << endl;//理论值,一般用不到

cout << s1.capacity() << endl;

string s2("123456");
int s2_capacity = s2.capacity();
cout << "s2_capacity = " << s2_capacity << endl;

//除了第一次是二倍扩容,往后几乎是1.5倍的扩容
for (size_t i = 0; i < 100; i++)
{
	s2.push_back(i);
	if (s2.capacity() != s2_capacity)
	{
		s2_capacity = s2.capacity();
		cout << "New Capacity = " << s2_capacity << endl;
	}
}
cout << endl;

s2.clear();//清空内容
cout << s2 << endl;
cout << s2.size() << endl;
cout << s2.capacity() << endl;

s2.shrink_to_fit();//缩小容量,不具有约束力,一般不用。
cout << s2.capacity() << endl;

size函数,返回当前string对象有多少字符。capacity函数返回当前string对象的容量有多大。

当string中的存储空间不够时,就会继续申请新的空间,申请方式为除了第一次为二倍扩容,其余均为1.5倍扩容。

clear函数用来清空当前string中的内容,但并不改变容量大小。

shirink_to_fit函数,主要目的时缩小容量。不过缩小容量的操作效率低下,且容易影响数组中的存储内容,一般不用。运行结果如下图所示:

2.4.2reserve函数

reserve函数主要目的是用来改变capacity的容量大小。主要用法为提前开辟空间,尽可能减少string对象由于增加字符而导致的频繁扩容

cpp 复制代码
	string s1;
	s1.reserve(100);//直接开辟100个字符的空间
	cout << s1.capacity() << endl;

	for (size_t i = 0; i < 100; i++)
	{
		s1.push_back(i);
	}
	s1.reserve(50);//如果尝试缩小容量,那么也是不具有约束力的。况且如果真要缩小,用shrink_to_fit
	cout << s1.capacity() << endl;

同样的,如果尝试用reserve来减少容量,其也是不具有约束力的,尽可能不用。

2.4.3resize函数

resize函数是用来改变string对象内部数据多少。

改变string对象的数据长度,会有三种情况:

cpp 复制代码
	string s1("123456");
	cout << s1.size() << endl;

	//resize函数,可以改变string类中存储数据的内容
	//如果要变小,回去出一些元素直到resize中所写的内容。
	//如果要变大,则会补充单字符
	//第二个参数缺省,则会默认补充\0
	s1.resize(5);
	cout << s1 << endl;

	s1.resize(10, 'x');//如果第二个字符缺省,那么会自动补充\0.
	cout << s1 << endl;

1.当resize后的长度小于原长度时,就会强制去除一些数据,缩小到要resize的长度。

  1. 当resize后的长度大于原长度时,如果resize函数的第二个参数明确写了需要补充什么单字符,那么就会在不足的长度中补充该字符。

3.如果resize的第二个参数缺省,那么就会补充\0直到规定长度。

2.4.4operator +=和append函数

+=运算符的重载本质和append函数的功能相同,都是在string对象后添加字符。

可以+=一个string类对象,也可以+=一个常量字符串,同时可以+=一个单字符。

append功能类似,仅仅多了可以规定长度的功能。

列表中的第二个表示为在string类对象中添加的类对象,指定添加的部分下标,包括该下标之后的sublen个字符。

列表中的第四个表示为,添加该常量字符串中的前n个字符

第五个表示为添加n个单字符到该对象中。

2.4.5 insert和erase函数

insert函数主要功能为在指定位置插入字符/字符串。

erase函数主要为删除指定下标之后多少长度的字符

主要用法示例如下:

cpp 复制代码
string s1("123456");

s1.insert(1, "xxx");
cout << s1 << endl;

s1.insert(0, 1, 'a');
cout << s1 << endl;

string s2("hello world");
s1.insert(3, s2,5,10);//第三个参数为下标,最后一个参数为拷贝的长度
cout << s1 << endl;

s1.erase(0, 5);
cout << s1 << endl;

2.4.6assign和replace函数(以及find)

' assign函数是将当前string对象的内容指定为另一内容,其形式具体包括:1.指定为另一个string对象;2.指定为具体多少长度的另一个string对象;3.指定为某一常量字符串;4.指定为某一长度的常量字符串;5.指定为n个单字符

replace函数主要是将给定下标位置后的1个或几个字符替换为给定字符,主要搭配find使用

find用来寻找string类对象内某个string类对象,或者某个常量字符串/常量字符,具体用法如下例程序:

这个程序主要实现了将空格替换为两个百分号的功能。

cpp 复制代码
string s1("hello  world");

size_t pos = s1.find(' ');
while (pos != string::npos)
{
	s1.replace(pos,1, "%%");
	pos = s1.find(' ', pos + 2);
}
cout << s1 << endl;


string s2("hello    world");//如果继续用上面这种算法,效率就会很低

string s3;

string::iterator it2 = s2.begin();
while (it2 != s2.end())
{
	if (*it2 == ' ')
		s3 += "%%";
	else
		s3 += *it2;

	it2++;
}
cout << s3 << endl;

第一种思路为利用find函数寻找空格位置,并进行替换,但这种方法效率较低,因为涉及到数组数据前移问题。

第二种思路为创建一个新的string对象,,遍历原string对象,如果是空格,那就在新的string对象中添加两个百分号;如果是字符,那就直接添加。

2.4.7substr函数

substr函数为在该string对象内部,在规定的下标和字符串长度后,找出并返回该字串,并构建一个string对象。其搭配find和rfind函数能写出一些小程序:

cpp 复制代码
void test10()
{
	string s1("hello world");
	//substr用来分割一个string对象的子串,并返回一个string对象
	string s2 = s1.substr(0, 5);//第一个表示下标,第二个表示要多少长度
	cout << s2 << endl;
	//利用substr和find就可以写一个基础的分割网址的小程序。
}

void FindSubfix(const string& s)
{
	size_t pos1 = s.find('/');

	if (pos1 != string::npos)
	{
		cout << s.substr(0, pos1) << endl;;

		size_t pos2 = s.find( '/',pos1+2);
		cout << s.substr(pos1 + 2, pos2 - pos1 - 2) << endl;
		cout << s.substr(pos2 + 1, s.size() - pos2 - 1) << endl;
	}
	else
	{
		cout << "The URL is wrong!" << endl;
		exit(EXIT_FAILURE);
	}
}

void test11()
{
	string s1("https://legacy.cplusplus.com/reference/string/string/substr/");
	string s2("https://www.bilibili.com/video/BV1Vm4y1r7jY/");

	FindSubfix(s1);
	FindSubfix(s2);
}

//也可以写一个识别后缀的小程序

void searchfix(const string& s)
{
	size_t pos = s.rfind('.');//rfind为从后向前找
	if (pos != string::npos)
	{
		cout << s.substr(pos) << endl;
	}
	else
	{
		cout<<"The file have not the fix.";
	}
}
void test12()
{
	string s1("test.cpp");
	string s2("abcd.zip");
	string s3("abcd,ziop.7z.rar");

	searchfix(s1);
	searchfix(s2);
	searchfix(s3);
}

上面代码的test11()实现了一个基础的分割网址的功能,将其分为协议、域名以及之后的部分内容。实现思路为:先利用find函数找出第一个 '/',就可以用substr函数分割出协议。之后以该下标为基础,向后继续寻找 ' / ',就可以找出第二段内容。利用该下标和npos,即可分割出最后的内容。

test12()的分割后缀,也是类似的思路:通过rfind从后向前寻找 '.',以该下标为第一个参数,利用默认参数npos即可分割出后缀。

2.4.8返回 const char*的C语言接口

该函数的主要目的是返回一个指向字符串数组内容的const char* 类型的指针。利用该函数可以对接一些C语言的函数。

cpp 复制代码
void test13()
{
	//c_str函数用来返回针对于C语言应用的接口
	string file ( "string的基本使用.cpp");

	FILE* FILENAME = fopen(file.c_str(), "r");

	char ch = getc(FILENAME);
	while (ch!= EOF)
	{
		cout << ch;
		ch = getc(FILENAME);
	}
}

例如该段代码,fopen函数第一个参数需要传递一个char*的指针,就可以利用c_str()函数来实现该功能。这段代码实现了读取并打印出这个cpp文件内的字符内容。

2.4.9find_first_of等函数

以find_first_of为例

该函数主要实现了在类对象中寻找任何(所有)指定的内容。如下端代码所示:

cpp 复制代码
void test14()
{
	string s1("abcerfsasdwanjklwajdioanbkjwdnjkasda");
	string s2("abcerfsasdwanjklwajdioanbkjwdnjkasda");

	size_t pos1 = s1.find_first_of("aeiou");
	//size_t pos = s1.find_last_of("aeiou");//从后向前找
	size_t pos2 = s2.find_first_not_of("aeiou");

	while (pos1 != string::npos)
	{
		s1[pos1] = ' ';
		pos1 = s1.find_first_of("aeiou",pos1+1);
	}
	while (pos2 != string::npos)
	{
		s2[pos2] = ' ';
		pos2 = s2.find_first_of("aeiou", pos2 + 1);
	}
	cout << s1 << endl;
	cout << s2 << endl;
}

s1和s2为两个内容相同的对象。s1利用find_first_of函数找出其内容中所有的aeiou,并将其替换为空格。s2调用find_first_not_of函数,找出s2中所有不是aeiou的字符,并将其替换为空格。

这涵盖了string类一些常用的函数,下一篇博客将带来一个简单的string类的底层实现