STL库——string(类函数学习)

发布于:2025-08-19 ⋅ 阅读:(18) ⋅ 点赞:(0)

ʕ • ᴥ • ʔ

づ♡ど

 🎉 欢迎点赞支持🎉

个人主页:励志不掉头发的内向程序员

专栏主页:C++语言


文章目录

前言

一、STL简介

二、string类的优点

三、标准库中的string类

四、string的成员函数

4.1、构造函数

 4.2、析构函数

4.3、赋值重载

4.4、运算符重载

4.5、迭代器(iterator)

4.6、Capacity相关函数

4.6.1 size/length函数

4.6.2、max_size函数

4.6.3、capacity函数

4.6.4、reserve/resize函数

4.6.5、clear函数

4.6.6、empty函数

4.6.7、shrink_to_fit函数

4.7、插入删除相关函数

4.7.1、push_back函数

4.7.2、append函数

4.7.3、operator+=函数

4.7.4、insert函数

4.7.5、pop_back函数

4.7.6、erase函数

4.7.7、replace函数

4.8、查找函数

4.8.1、find函数

4.8.2、rfind函数

4.8.3、find_first_of函数

4.8.4、find_last_of函数

4.8.5、find_first_not_of/find_last_not_of函数

4.9、其他函数

4.9.1、c_str函数

4.9.2、copy函数

4.9.3、substr函数

五、非成员函数

5.1、operator+函数

5.2、<>函数

5.3、getline函数

总结


前言

本章节我们就来说说我们C++与C语言又一个巨大的区别,那就是STL库,在这个库中收录了我们很多经常使用的数据结构和算法,我们之后的章节会来具体的讲解和学习,接下来我们来一起看看吧。


一、STL简介

STL是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构和算法的软件框架。

STL的六大组件

二、string类的优点

与C语言相比,我们STL容器中(严格来说应该是C++标准库)引入了我们的string,它的出现帮助我们去管理字符串,这使得我们对字符串的使用更加规范以及更加简单快捷,出错率也与自己去实现相比大大的降低了。

三、标准库中的string类

我们可以看看我们string类中有哪些成员函数,string - C++ Reference

我们可以粗略的看到我们string类的成员变量是很多的,我们为了完全搞懂string类,我们就来一个一个成员函数的学习和模拟实现吧。

四、string的成员函数

我们string类都保存在我们string的头文件中

#include <string>

4.1、构造函数

我们学习类第一步就应该先去查看它的构造函数

我们string设计时间比STL早,有很多设计没有参考,所以我们学习时就会觉得它有些设计是十分冗余的,就比如光是string的构造函数就有7种,但是没有关系,只有string是这样的,我们只要把经常要用的学会就行,我也只会模拟实现经常要用的成员函数。

#include <string>

int main()
{
	string s1;
	string s2("11111111");
	string s3(s2);

    // 重载了流输入输出
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;

	cin >> s1;
	cout << s1 << endl;
	return 0;
}

上面3个是我们主要使用的构造函数,接下来讲讲剩下的几个。

第3个构造函数是用来构造一个字符串的一部分的。

第一个参数是用那个字符串构造,第二个则是从哪个位置,第三个参数是拷贝多少字符(如果剩下的不够就有多少拷贝多少)。

#include <string>

int main()
{
	string s1("hello world");
	string s2(s1, 6, 5);
	string s3(s1, 6, 100);

	cout << s2 << endl;
	cout << s3 << endl;

	return 0;
}

当然如果我们不传最后一个参数,那就是默认拷贝到结尾,它的缺省参数是一个npos

它是一个静态成员变量。我们可以试着突破类域去看看它的值是多少。

#include <string>

int main()
{
	cout << string::npos << endl;
	return 0;
}

由于我们的字符串不可能有这么长,所以这就可以默认拷贝到最后一个位置了。

第5个构造是用来构造字符串前面一部分的。

第一个参数是用来输入字符串的,第二个参数则是确定拷贝前面几个字符,要是字符数目不够则是全拷贝。

#include <string>

int main()
{
	string s1("hello world", 5);
	string s2("hello world", 100);

	cout << s1 << endl;
	cout << s2 << endl;

	return 0;
}

第6个构造就是用n个字符c去初始化。

第一个参数就是要几个字符,第二个参数就是字符是什么。

#include <string>

int main()
{
	string s1(10, 'x');

	cout << s1 << endl;
	return 0;
}

还有最后一个暂时不讲,之后再说明。

 4.2、析构函数

析构函数没有什么必要去看,因为它是系统自动调用的,我们无需手动调用。

4.3、赋值重载

第一种就是把一个string赋值给另外一个string。

int main()
{
	string s1("xxxxxxxxxxxxx");
	string s2;
	s2 = s1;

	cout << s2 << endl;
	return 0;
}

第二种是把我们的字符串赋值给我们的string。

int main()
{
	string s1;
	s1 = "xxxxxxxxxx";

	cout << s1 << endl;
	return 0;
}

第三种则是把我们字符赋值给我们string。

int main()
{
	string s1;
	s1 = 'X';

	cout << s1 << endl;
	return 0;
}

我们一般使用第一种和第二种即可,第三种赋值重载没有必要,因为一个字符也算字符串,也可以用第二种代替。

4.4、运算符重载

[ ]符重载:

我们string类将[]符进行了重载,这无疑是一个高明的操作,因为这样就使得我们的字符串和数组一样可以非常方便的修改和查询了。

int main()
{
	string s1("hello world");

	//修改指定字符
	s1[3] = '*';
	s1[5] = '#';

	cout << s1 << endl;

	//查看指定字符
	cout << s1[4] << " " << s1[7] << endl;
	return 0;
}

同时我们还可以通过[]符来遍历我们的字符串。

int main()
{
	string s1("hello world");
	
	//size()是string中返回我们字符串大小的函数
	for (int i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << " ";
	}
	cout << endl;
	return 0;
}

这样操作主要是通过我们的运算符通过返回我们类型的引用去修改指定位置的字符来实现的,我们下一章节再来细聊。
除了这种遍历变量的方式,我们还有两种,下面会讲。

4.5、迭代器(iterator)

我们的迭代器是STL的一大组件,其作用主要就是用来遍历和访问我们的容器。我们先来看看它的操作。

int main()
{
	string s1("hello world");
	
	//size()是string中返回我们字符串大小的函数
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	return 0;
}

这样写就实现了我们遍历的效果。我们仔细观察这些代码,感觉就很像指针。我们迭代器的实现千变万化,只要能够达到我们所需要的效果即可,所以我们也不确定它到底是怎么实现的,可能有的就是用指针实现的。但是我们可以想象成指针。

我们的begin()是返回string空间开始位置的迭代器,而end()是我们返回最后一个有效字符的位置,也就是'\0'的位置的迭代器。

大致就是这个样子,我们从begin到end,一个一个遍历过去,如果是指针,那就是一个一个解引用后再++,一直到end后停止,这个很好理解,如果不是指针,那我们就得运算符重载了,把我们'*'、'++'两个运算符重载成我们和指针那样的操作了。所以我们的迭代器可能是指针,也可能不是。

iterator给我们STL库中所有的容器提供了一个通用的遍历和访问的方法,让它们统一起来,这就是迭代器的厉害之处。

当然,我们除了这种正向迭代器,还有反向迭代器

int main()
{
	string s1("hello world");
	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout << *rit << " ";
		rit++;
	}
	cout << endl;
	return 0;
}

它的写法和刚才的正向迭代器很像。

它也是使用++,只不过是倒着走的。我们可以这样子理解

我们封装了++后使其倒着运行。

当然,有的时候我们会碰到const string,但是我们的普通迭代器可读可写,会导致权限放大不能调用怎么办呢?这个时候就得用const_iterator迭代器了。用法和iterator是一样的,只是不能修改,只能遍历。

int main()
{
	string s1("hello world");
	string::const_iterator cit = s1.cbegin();
	while (cit != s1.cend())
	{
		cout << *cit << " ";
		cit++;
	}
	cout << endl;
	return 0;
}

当然,也有const反向迭代器

int main()
{
	string s1("hello world");
	string::const_reverse_iterator crit = s1.crbegin();
	while (crit != s1.crend())
	{
		cout << *crit << " ";
		crit++;
	}
	cout << endl;
	return 0;
}

用法都是类似的,大家记住就好了,等下一章节讲解模拟实现是在来细讲这些内容。

我们还有一种遍历变量的方法,就是范围for

int main()
{
	string s1("hello world");
	
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;
	return 0;
}

这个代码的含义是我们从我们的s1中去取我们的每一个变量,然后传给我们的ch(自定义的,叫啥无所谓)中去。

我们再来聊聊auto,它代表的是自动推导类型,它会自动去推导我们s1传过来的是什么类型的变量,auto就是什么类型的,这里是char,所以auto就是char,当然我们自己写char也可以。

我们auto的主要作用是来简化我们的代码而使用的,例如map<string, string>::iterator可以直接用auto代替,是不是简单多了。但是它的代价是牺牲了可读性。

当然我们auto必须要初始化,不然它就不知道它因该推导成什么类型。也不能一行定义不同类型的变量。也不能定义数组。

// 不能这样写
auto a;
auto a = 1, b = 1.1;
auto array[] = { 1, 2, 3, 4, 5, 6 };

我们的auto不能做参数,但是可以做返回值,虽然不建议这么做。

我们接着说范围for,

	for (char ch : s1)
	{
		cout << ch << " ";
	}

但是我们这里一般都用auto。范围for可以自动赋值,自动迭代,自动判断结束,它的底层其实是迭代器,虽然看上去很厉害,但其实就是迭代器套了一层壳。

同时他不会去改变我们s1的内容,它相当于是传*it给我们的ch而非it。

int main()
{
	string s1("hello world");
	
	for (auto ch : s1)
	{
		ch -= 2;
		cout << ch << " ";
	}
	cout << endl;
	cout << s1;
	return 0;
}

我们可以看出它是没有修改s1的。

如果是想要修改得在auto后加&,相当于从之前传输过来的char类型变成了char&类型

int main()
{
	string s1("hello world");
	
	for (auto& ch : s1)
	{
		ch -= 2;
		cout << ch << " ";
	}
	cout << endl;
	cout << s1;
	return 0;
}

我们所讲的3种遍历方式在性能方面是没有任何区别的,可以随便使用。

4.6、Capacity相关函数

4.6.1 size/length函数

这两个函数都是用来返回我们的字符串中字符的数量的,使用方法也很简单

int main()
{
	string s1("hello world");
	cout << s1.size() << endl;
	cout << s1.length() << endl;
	return 0;
}

它们就是两个不同名字但是作用完全一样的函数。我们length不具有通用性,只属于string容器,但是我们的size是所有容器都有的。

4.6.2、max_size函数

这个函数的作用就是和你说说我们的string最大能开多大,实际就是整型的最大值。没有什么用处,而且过于理想化,实际上根本开不了这么大。

int main()
{
	string s1("hello world");
	cout << s1.max_size() << endl;
	return 0;
}

4.6.3、capacity函数

返回我们string容量的函数。

int main()
{
	string s1("hello world");
	cout << s1.capacity() << endl;
	return 0;
}

我们可以先来看看我们的string是怎么扩容的

int main()
{
	string s;
	// 得到初始内存
	size_t sz = s.capacity();
	cout << "capacity changed: " << sz << endl;
	cout << "make s grow:" << endl;
	for (int i = 0; i < 100; i++)
	{
		// 循环插入c去看它扩容
		s.push_back('c');

		// 如果扩容了就打印看看新扩容的大小
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << endl;
		}
	}
}

我们可以看到,我们第一次是2倍扩容,其他的时候就是1.5倍扩容。这是因为我们vs的工程师为了防止我们总是使用不满string,为了效率所以在string上增加了一个buffer[16]的字符串,当我们的字符串没有满16个字符时我们的编译器就不会在堆上开辟空间,而是在栈上的buffer的字符串中。

所以我们从第一次扩容是2倍扩容是特殊情况,后面都是1.5倍扩容。当然不同编译器扩容方式不一样。

4.6.4、reserve/resize函数

reserve是用来开辟空间的,它能提前开辟好我们指定用到的空间,从而减少扩容次数。

int main()
{
	string s;
	s.reserve(100);
	cout << s.capacity() << endl;
	return 0;
}

但是其实这个只是让我们编译器至少开辟100的空间,具体是多多少得看编译器操作,因为编译器会做整数倍对齐。

当然,我们的reserve是一般是不会缩容的,但是还是要看具体的服务器。

int main()
{
	string s("0123456789");
	cout << s.capacity() << endl;
	s.reserve(5);
	cout << s.capacity() << endl;
	s.reserve(20);
	s.reserve(15);
	cout << s.capacity() << endl;

	return 0;
}

resize则是可以扩容插入,如果它比size小会删除数据,如果比size大但是capacity小的话会在后面插入数据,如果你没给会插入默认字符' \0 ',如果比capacity大则会扩容。

4.6.5、clear函数

这个函数是用来清空我们的字符串的数据,但是一般不会清理容量

int main()
{
	string s("0123456789");
	cout << s << endl;
	s.clear();
	cout << s << endl;
	cout << s.capacity() << endl;

	return 0;
}

4.6.6、empty函数

判断我们string是不是空的,有没有字符。

int main()
{
	string s("0123456789");
	if (s.empty()) cout << "Yes" << endl;
	else cout << "No" << endl;
	s.clear();
	if (s.empty()) cout << "Yes" << endl;
	else cout << "No" << endl;

	return 0;
}

4.6.7、shrink_to_fit函数

这个函数也是用来缩容的,但是和reserve一样也不一定会成功。

4.7、插入删除相关函数

4.7.1、push_back函数

这个函数的功能是尾插一个字符,它只能插入一个字符,不能插入字符串,设计的蛮鸡肋的。

int main()
{
	string s("1234");

	// 如果用双引号就会报错,因为不能插入字符串
	s.push_back('5');
	cout << s << endl;
	return 0;
}

4.7.2、append函数

想要插入字符串就可以用append,但是一般我们就使用第三个,别的没有必要使用,这里简单介绍一下把

第一个:

单纯的在末尾插入一个string字符串。

int main()
{
	string s1("hello ");
	string s2("world");

	s1.append(s2);
	cout << s1 << endl;
	return 0;
}

第二个:

在末尾插入字符串str从subpos位置开始的sublen个字符。

int main()
{
	string s1("hello ");
	string s2("world");

	s1.append(s2, 1, 3);
	cout << s1 << endl;
	return 0;
}

如果剩余数不足那就取到末尾。

第三个:

直接在后面插入一个字符串。

int main()
{
	string s1("hello ");
	s1.append("world");
	cout << s1 << endl;
	return 0;
}

第四个:

在末尾插入s前n个字符,如果剩下的不够,则取到末尾。

int main()
{
	string s1("hello ");
	s1.append("world", 3);
	cout << s1 << endl;
	return 0;
}

第五个:

就是在末尾插入n给字符c。

第六个后面再将。

4.7.3、operator+=函数

这个是我们string中最常用的尾插用法,它可以兼容字符和字符串,而且可读性也很好。

int main()
{
	string s1("hello");
	string s2("world");
	s1 += ' ';
	s1 += s2;
	s1 += "################";
	cout << s1 << endl;
	return 0;
}

4.7.4、insert函数

我们前面就学了尾插,其他位置的插入就用这个函数,这个函数也十分的冗余,主要用的就2个,如果我们要插入我们的字符串,可以用第1种和第3种。

int main()
{
	string s1("hello");
	string s2("world");

	// 第一种方法
	s1.insert(4, s2);
	cout << s1 << endl;

	// 第二种方法
	s1.insert(0, "########");
	cout << s1 << endl;

	return 0;
}

插入字符也可以把字符当字符串,但是如果一定得是字符的话那我们就只能用第五种或者第六种办法了。

int main()
{
	string s1("hello");
	char c = '#';

	// 第五种方法,在第0位置插入1个字符c
	s1.insert(0, 1, c);
	cout << s1 << endl;

	// 第六种方法
	s1.insert(s1.end(), c);
	cout << s1 << endl;

	return 0;
}

其他的不常用,大家可以自己去试试看看怎么用,很简单。

4.7.5、pop_back函数

这个就不用多说了,就是尾删。

4.7.6、erase函数

主要就是使用第1个,在pos位置删除len个字符,如果没写len或者剩下的不足那就在pos后面全删了。

int main()
{
	string s1("hello world");
	s1.erase(2, 2);
	cout << s1 << endl;

	return 0;
}

第二个则是迭代器删除,只能删除一个,

int main()
{
	string s1("hello world");
	s1.erase(s1.begin() + 2);
	cout << s1 << endl;
	return 0;
}

第3个则是删除一个区间的。

4.7.7、replace函数

这个函数的主要作用就是替换,设计的也很麻烦,就讲几个经常使用的,大家要是有需要的再查库就好了。

int main()
{
	string s1("hello world");
	// 第5个位置替换成"%%",替换3个字符
	s1.replace(5, 3, "%%");
	cout << s1 << endl;
	return 0;
}

4.8、查找函数

4.8.1、find函数

我们可以查找一个string字符串,或者一个字符,它如果找到了就会返回第一个匹配位置的字符下标,没找到就会返回npos。这里面的pos表示从哪里开始找起。

int main()
{
	string s1("hello world");
	string s2("wor");
	size_t pos = s1.find(s2);
	cout << pos << endl;

	// 从第7个位置开始找
	pos = s1.find("s2, 7");
	cout << pos << endl;

	pos = s1.find(' ');
	cout << pos << endl;

	return 0;
}

4.8.2、rfind函数

rfind和find用法相同,就是rfind是倒着查找的,这里就不多赘述了。返回值就是从后往前第一次出现的下标,如果没有就返回npos。

4.8.3、find_first_of函数

这个函数的意思不是说找第一个,因为我们find就是第一个,而是说找任意一个,就比如

int main()
{
	std::string str("Please, replace the vowels in this sentence by asterisks.");
	std::size_t found = str.find_first_of("abcd");
    // 一直查找,知道找不到为止
	while (found != std::string::npos)
	{
		str[found] = '*';
        // 从已经找过的位置接着往下找
		found = str.find_first_of("abcd", found + 1);
	}

	std::cout << str << '\n';

	return 0;
}

我们这个函数就是把我们传进去的字符串"abcd"中的每一个字符和我们的原文进行配对,如果有其中有其中一个字符可以配对上那就返回第一个配对上的下标,所以上面我们str字符串中的a、b、c、d字符全部替换成了我们的*号。

4.8.4、find_last_of函数

和我们的find_first_of一样,只不过是反向查找。

4.8.5、find_first_not_of/find_last_not_of函数

分别是find_first_of和find_last_of的对立,就是查找除了我们字符串中的字符其他的字符。

int main()
{
	std::string str("Please, replace the vowels in this sentence by asterisks.");
	std::size_t found = str.find_first_not_of("abcd");
	while (found != std::string::npos)
	{
		str[found] = '*';
		found = str.find_first_not_of("abcd", found + 1);
	}

	std::cout << str << '\n';

	return 0;
}

4.9、其他函数

4.9.1、c_str函数

此函数的作用就是返回底层字符串的指针。这个函数的意义就是兼容C语言的。

4.9.2、copy函数

这就是一个拷贝,一般用的很少用,而且我们还要手动的去给它增加' \0 ',不然他就无法检测到什么时候停下,它的返回值就是拷贝了多少个字符。

int main()
{
	string s1("hello world");
	char buffer[10];

	// 从s1中的第3个字符开始拷贝4个字符
	size_t len = s1.copy(buffer, 4, 3);

	// 手动增加\0,不然就会出错
	cout << buffer << endl;
	buffer[len] = '\0';
	cout << buffer << endl;

	return 0;
}

4.9.3、substr函数

这个是我们获取字串最常用的方法。是从pos开始的len个字符,如果不给len就拷贝到结尾。

int main()
{
	string s1("0123456789");
	string s2, s3;
	s2 = s1.substr(4, 7);
	s3 = s1.substr(5);

	cout << s2 << endl;
	cout << s3 << endl;

	return 0;
}

五、非成员函数

5.1、operator+函数

我们operator+没有重载成成员函数的原因是为了方便更加自由的操作

int main()
{
	string s1("hello");

	// 这个重载成成员函数就可以做到
	string s2 = s1 + "world";
	cout << s2 << endl;

	// 这样如果是重载函数那就做不到了
	string s3 = "world" + s1;
	cout << s3 << endl;
	return 0;
}

这样就自由了很多。

5.2、<</>>函数

不多赘述,前往都用了很多次了。

5.3、getline函数

这个函数的作用就是改变我们字符串的分割符,我们的字符串默认是以空格和换行符作为分割,这就使得我们有的时候想要记录一些句子却记录不了,因为有空格。

int main()
{
	string s1;
	cin >> s1;
	cout << s1;
	return 0;
}

为了避免这种情况,我们就可以用getline来改变分隔符。它的第一个参数是流输入,一般是cin,第二个参数就是我们要输入的字符串,第三个参数就是我们想要的分隔符,如果不写就默认是换行为分隔符。

int main()
{
	string s1;
	getline(cin, s1);
	cout << s1;
	return 0;
}

以*为分隔符

int main()
{
	string s1;
	getline(cin, s1, '*');
	cout << s1;
	return 0;
}


总结

以上便是我们string的各种常用函数的使用方法,虽然很多,但是都不是很难,我们也不用死记硬背,我们之后和日常使用会一次又一次的反复记忆,我们如果忘记了再回来或者查文档去看看就好了,下一章节我们回来试着模拟实现string,下一章节再见。

🎇坚持到这里已经很厉害啦,辛苦啦🎇

ʕ • ᴥ • ʔ

づ♡ど


网站公告

今日签到

点亮在社区的每一天
去签到