C++ -- STL【string的使用】

目录

一、STL介绍

二、C/C++中的字符串

1、C语言中的字符串

2、C++中的字符串

三、string的接口

1、string的迭代器

[1.1 begin()与end()函数](#1.1 begin()与end()函数)

[1.2 rbegin()与rend()函数](#1.2 rbegin()与rend()函数)

2、string的初始化与销毁

3、string的容量操作

[3.1 有效长度与容量大小](#3.1 有效长度与容量大小)

[3.2 有效长度与容量操作](#3.2 有效长度与容量操作)

[3.3 string的访问操作](#3.3 string的访问操作)

[3.3.1 string类对象的三种遍历方式](#3.3.1 string类对象的三种遍历方式)

[3.3.2 auto关键字](#3.3.2 auto关键字)

[3.4 string的修改操作](#3.4 string的修改操作)

[3.4.1 字符串的增加](#3.4.1 字符串的增加)

[3.4.2 字符串的替换](#3.4.2 字符串的替换)

[3.4.3 字符串的删除](#3.4.3 字符串的删除)

[3.4.4 字符串的交换](#3.4.4 字符串的交换)

[3.4.5 string的其他操作](#3.4.5 string的其他操作)


一、STL介绍

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

STL 共有六大组件:


二、C/C++中的字符串

1、C语言中的字符串

在 C 语言中,字符串是由字符组成的字符数组,以空字符 '\0' 作为结束标志。由于数组特点,字符串的大小在定义数组时就已经确定,无法更改。

cpp 复制代码
	//数组大小为20
	char str[20] = "hello betty!\n";

当然我们可以通过动态内存分配来来解决这个问题,但无疑非常繁琐。

cpp 复制代码
void Test1()
{
	char* str = NULL;
	int len = 0;
	// 初始分配一些内存
	str = (char*)malloc(10 * sizeof(char));
	if (str == NULL) {
		perror("malloc fail");
		return 1;
	}
	strcpy(str, "Hello");
	len = strlen(str);
	// 根据需要扩展字符串
	str = (char*)realloc(str, (len + 6) * sizeof(char));
	if (str == NULL) {
		perror("realloc fail");
		return 1;
	}
	strcat(str, " World");
	printf("%s\n", str);
	//最后释放内存
	free(str);
}

2、C++中的字符串

虽然 C++ 兼容 C 语言,在 C++ 中仍然可以使用 C 语言的字符串,但是 C++ 自己实现了一个关于处理字符串的类 ------ string ,它提供了许多方便的操作和功能,使得字符串的处理更加安全和高效。下面是一个简单的 string 的使用:

cpp 复制代码
void Test2()
{
	string str = "hello betty!";
	cout << str << endl;
	//改变第一个字符
	str[0]++;
	cout << str << endl;
	//在末尾添加一个字符
	str += 'e';
	cout << str << endl;
	//在末尾添加一个字符串
	str += " hello";
	cout << str << endl;
}

相较于 C 语言的字符串,C++ 的字符串明显方便的多。接下来我们将详细介绍 C++ string 类的特点与用法。


三、string的接口

C++ 为我们提供了丰富的 string 接口,我们可以通过对象来调用,为了方便我们学习我们可以通过查询相关文档辅助 ------ string类的接口介绍

1、string的迭代器

迭代器(Iterator)是一种用于遍历容器中元素的工具。它提供了一种统一的方式来访问容器中的元素,而无需关心容器的具体实现细节。对于我们迭代器,我们在使用时将其当做指针使用即可。

在 string 类中,我们就可以通过迭代器来访问其具体元素,并且也为我们提供了相应的调用函数。

1.1 begin()与end()函数

begin() 与 end() 函数的使用方法具体如下:

  1. 函数声明:
  • iterator begin();
  • const_iterator begin() const;
  1. 作用:返回指向字符串第一个字符的迭代器。

  2. 返回值:普通对象返回 iterator 迭代器,const 对象返回 const_iterator 迭代器。

  1. 函数声明:
  • iterator end();
  • const_iterator end() const;
  1. 作用:返回指向字符串最后一个字符下一个位置的迭代器。
  2. 返回值:普通对象返回 iterator 迭代器,const 对象返回 const_iterator 迭代器。

begin() 、 end() 具体指向情况如下图所示:

然后我们可以通过以下代码来演示效果:

cpp 复制代码
void Test3()
{
    string s1 = "hello tata!";
    // 普通迭代器
    string::iterator it = s1.begin(); // 指向第一个位置
    while (it != s1.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
    const string s2 = "hello tata!"; // const对象
    // const 反向迭代器 不能改变字符串中的值
    string::const_iterator itt = s2.begin(); // 指向最后一个字符的下一个位置
    while (itt != s2.end())
    {
        cout << *itt << " ";
        ++itt;
    }
}

1.2 rbegin()与rend()函数

rbegin() 与 rend() 函数的使用方法具体如下:

  1. 函数声明:
  • reverse_iterator rbegin();
  • const_reverse_iterator rbegin() const;
  1. 作用:返回指向字符串最后一个字符位置(即其反向开头)的反向迭代器。
  2. 返回值:普通对象返回 iterator 迭代器,const 对象返回 const_iterator 迭代器。
  3. 函数声明:
  • reverse_iterator rend();
  • const_reverse_iterator rend() const;
  1. 作用:返回指向字符串第一个字符前面一个位置的反向迭代器。
  2. 返回值:普通对象返回 iterator 迭代器,const 对象返回 const_iterator 迭代器。

rbegin() 与 rend() 具体指向情况如下图所示:

然后我们可以通过以下代码来演示效果:

cpp 复制代码
void Test4()
{
    string s1 = "hello tata!";
    // 普通反向迭代器
    string::reverse_iterator rit = s1.rbegin(); // 指向最后一个字符位置
    while (rit != s1.rend())
    {
        cout << *rit << " ";
        ++rit;
    }
    cout << endl;
    const string s2 = "hello tata!"; // const对象
    // const 反向迭代器 不能改变字符串中的值
    string::const_reverse_iterator ritt = s1.rbegin(); // 指向最后一个字符的位置
    while (ritt != s1.rend())
    {
        cout << *ritt << " ";
        ++ritt;
    }
}

2、string的初始化与销毁

因为 string 是一个类,所以我们在初始化时肯定调用其构造函数初始化。以下就是我们常见初始化的接口:

  1. 第一个使我们的默认构造函数,不需要传参。
  2. 第二个使用的是拷贝构造来初始化。
  3. 第三个是使用一个 string 的某段区间初始化,其中 pos 是字符串下标,npos 是指无符号整数的最大值。
  4. 第四个使用的是某个字符数组初始化。
  5. 第五个使用的是某个字符数组前 n 个字符来初始化
  6. 第六个使用的是 n 个 c 字符初始化。
  7. 第七个使用的是某段迭代器区间初始化。
  8. 最后也能通过赋值运算符重载初始化。

下面是具体的代码示例:

cpp 复制代码
void Test5()
{
    // 1. 使用我们的默认构造函数,不需要传参。
    string s1;
    s1 = "hello tata!";
    // 2. 使用的是拷贝构造来初始化。
    string s2(s1);
    // 3. 使用一个string的某段区间初始化,其中pos是字符串下标,npos是指无符号整数的最大值。
    string s3(s2, 1, 7);
    // 4. 使用的是某个字符数组初始化。
    string s4("hello world!");
    // 5. 使用的是某个字符数组前n个字符来初始化
    string s5("hello world!", 5);
    // 6. 使用的是n个c字符初始化。
    string s6(7, 'a');
    // 7. 使用的是某段迭代器区间初始化。
    string s7(s1.begin(), s1.end());
    // 赋值运算符重载初始化
    string s8 = "hello tata!";
    cout << s1 << endl;
    cout << s2 << endl;
    cout << s3 << endl;
    cout << s4 << endl;
    cout << s5 << endl;
    cout << s6 << endl;
    cout << s7 << endl;
    cout << s8 << endl;
}

而由于 string 是一个类,出了作用域会自动调用它的析构函数,所以不用显示调用。

注意:用另一个 string 的 pos 位置开始后面的 len 个字符构造时,如果给的长度过大会怎么样呢?会越界吗?

3、string的容量操作

接下来我们将学习关于 string 类常见的容量操作:

函数名称 功能
size 返回字符串的有效长度
length 返回字符串的有效长度
capacity 返回字符串的容量大小
max_size 返回字符串的最大长度
clear 清空字符串
empty 检查是否为空串,是则返回 ture,否则返回 false
reserve 请求改变字符串的容量
resize 重新设置有效字符的数量,超过原来有效长度则用 c 字符填充
shrink_to_fit 收缩字符串容量

3.1 有效长度与容量大小

在 string 类中,我们可以通过 size(),length() 返回字符串的有效长度;capacity() 返回字符串的容量,其具体效果如下图:

我们也可以通过代码来验证:

cpp 复制代码
void Test6()
{
    string s("hello tata!");
    cout << s.size() << endl;     // 有效长度
    cout << s.length() << endl;   // 有效长度
    cout << s.capacity() << endl; // 容量大小
    cout << s.max_size() << endl; // 最大大小
}

其中有效长度 size 以及容量大小 capacity 不包括 \0。而 max_szie 返回字符串最大容量,不同平台下大小可能不一样。而在 vocode 下大小为 4611686018427387903,也就是 INT_MAX 的大小。

接下来我们可以来探究一下 string 的扩容机制

cpp 复制代码
void TestCapacity()
{
	string s;
	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 << "capacity changed: " << sz << '\n';
		}
	}
}

在 VS2022 编译器中,string 大概是以 1.5 扩容,但是在 g++ 编译器中却是 2 扩容。所以扩容倍数是不确定的,具体由不同编译器决定。

最后我们来谈谈 empty() 函数,它主要用来判断字符串是否为空:

cpp 复制代码
void TestEmpty()
{
	string s1("");//空串
	string s2("hello ");
	if (s1.empty())
	{
		cout << "s1为空串" << endl;
	}
	else
	{
		cout << "s1不为空串" << endl;
	}
	if (s2.empty())
	{
		cout << "s2为空串" << endl;
	}
	else
	{
		cout << "s2不为空串" << endl;
	}
}

3.2 有效长度与容量操作

首先我们要介绍的就是 clear() 函数,他能清空字符串,也就是改变有效长度 size ,但不会改变容量 capacity 。

cpp 复制代码
void TestClear()
{
	string s1("hello world!");
	cout <<"s1的有效长度为:"<< s1.size() << endl;
	cout <<"s1的容量大小为:"<< s1.capacity() << endl;
	s1.clear();
	cout << "s1的有效长度为:" << s1.size() << endl;
	cout << "s1的容量大小为:" << s1.capacity() << endl;
	if (s1.empty())
	{
		cout << "s1是空串" << endl;
	}
}

接下来我们将介绍两个可以改变容量的函数 reverse,resize。它们之间不小的差别,首先它们的接口如下:

我假设字符串原来有效长度为 sz,那么如果 n<sz,sz<n<capacity,n>capasity。两个函数的效果有何不同呢?

cpp 复制代码
void Test8()
{
	string s1("hello world!");
	cout << "reserve测试:" << endl;
	cout << s1 << endl;
	cout << "s1的有效长度为:" << s1.size() << endl;
	cout << "s1的容量大小为:" << s1.capacity() << endl;
	s1.reserve(5);
	cout << s1 << endl;
	cout << "s1的有效长度为:" << s1.size() << endl;
	cout << "s1的容量大小为:" << s1.capacity() << endl;
	s1.reserve(13);
	cout << s1 << endl;
	cout << "s1的有效长度为:" << s1.size() << endl;
	cout << "s1的容量大小为:" << s1.capacity() << endl;
	s1.reserve(25);
	cout << s1 << endl;
	cout << "s1的有效长度为:" << s1.size() << endl;
	cout << "s1的容量大小为:" << s1.capacity() << endl;
	cout << endl;
	cout << "resize测试:" << endl;
	string s2("hello world!");
	cout << s2 << endl;
	cout << "s2的有效长度为:" << s2.size() << endl;
	cout << "s2的容量大小为:" << s2.capacity() << endl;
	s2.resize(5);
	cout << s2 << endl;
	cout << "s2的有效长度为:" << s2.size() << endl;
	cout << "s2的容量大小为:" << s2.capacity() << endl;
	s2.resize(10,'x');
	cout << s2 << endl;
	cout << "s2的有效长度为:" << s2.size() << endl;
	cout << "s2的容量大小为:" << s2.capacity() << endl;
	s2.resize(25, 'x');
	cout << s2 << endl;
	cout << "s2的有效长度为:" << s2.size() << endl;
	cout << "s2的容量大小为:" << s2.capacity() << endl;
}

通过上述实验,我们可以总结出以下规律:

  1. 当 n<sz 时,reserve 并不会发生任何改变,resize 会删除有效字符到指定大小。
  2. 当 sz<n<capcity 时,reserve 并不会发生任何改变,resize 会补充有效字符(默认为 \0 )到指定大小。
  3. 当 n>capacity 时,reserve 会发生扩容,resize 会补充有效字符(默认为 \0 )到指定大小。
  • 注意:不同平台下扩容效果可能会不同,但是这个规律是不会改变的。

最后我们来介绍一个 C++11 引入的一个可以缩容 的函数 shrink_to_fit 它的主要目的就是让有效长度 size 与容量 capasity 适配。

cpp 复制代码
void Test9()
{
	string s1("hello world!");
	cout << s1 << endl;
	cout << "s1的有效长度为:" << s1.size() << endl;
	cout << "s1的容量大小为:" << s1.capacity() << endl;
	s1.reserve(100);//先扩容
	cout << "扩容后s1的有效长度为:" << s1.size() << endl;
	cout << "扩容后s1的容量大小为:" << s1.capacity() << endl;
	s1.shrink_to_fit();//再缩容
	cout << "缩容后s1的有效长度为:" << s1.size() << endl;
	cout << "缩容后s1的容量大小为:" << s1.capacity() << endl;
}

3.3 string的访问操作

接下来我们就来介绍 string 常见的访问函数:

函数名称 功能
operator [ ] 返回指定位置的字符,越界则报错
at 返回指定位置的字符,越界则抛异常
back 返回字符串最后一个字符(不是 '\0' )
front 返回字符串第一个字符

首先是 operator [ ] 这个运算符重载与 at 函数,它们的功能类似都是返回指定下标字符,并且 char* 类型返回 char* 类型,const char* 类型返回 const char* 类型。

cpp 复制代码
void Test10()
{
    string s1("hello tata!");
    for (int i = 0; i < s1.size(); i++)
    {
        cout << s1[i] << " ";
    }
    cout << endl;
    for (int i = 0; i < s1.size(); i++)
    {
        cout << s1.at(i) << " ";
    }
}

然后就是 C++11 引入的 front 与 back 函数,但实用性不是特别大,大家只需要了解。

cpp 复制代码
void Test11()
{
    string s1("hello tata!");
    cout << s1.front() << endl;
    cout << s1.back() << endl;
}
3.3.1 string类对象的三种遍历方式
  • 下标 + [ ]

这里因为 string 的底层是字符数组,所以可以用 下标 + [ ] 直接访问到 pos 位置的字符,从而遍历字符串,同时返回的是 pos 位置字符的引用,这样还可以修改 pos 位置的字符。

  • 迭代器:迭代器支持所有容器的访问。
  • 范围 for 遍历

这个范围 for 看起来好像很厉害,可以自动迭代、自动判断结束、自动读取数据、自动推导类型 (自定义类型也可以),但实际底层就是迭代器,汇编转到底层来看,范围 for 和迭代器是一样的。

总结:这三种遍历方式都差不多,但是下标 + [ ] 并不是所有的容器都支持,因为可能有些容器空间不连续,但是迭代器和范围 for 所有的容器都支持。

3.3.2 auto关键字

**# auto 关键字:**在这里补充2个 C++11 的小语法,方便我们后面的学习。

在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11 中,标准委员会变废为宝赋予了 auto 全新的含义即:auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。

用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用 auto 声明引用类型时则必须加 &,当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

注意:

auto 不能作为函数的参数,可以做返回值,但是建议谨慎使用。
auto 不能直接用来声明数组。

auto 的价值就是自动推导类型,如果类型过长就很方便,但是一定程度牺牲了可读性。

3.4 string的修改操作

string 关于修改的函数的接口都比较多,一一列举比较麻烦,这里我们只重点介绍常用的接口,剩下的大家具体使用时查官方文档即可。下面是常见的关于 string 修改的函数接口:

函数名称 功能
push_back 在字符串后追加字符
operator+= 在字符串后追加字符或字符串
append 在字符串后追加字符串
insert 在指定位置追加字符或者字符串
assign 使用指定字符串替换原字符串
replace 用新字符串替换原字符串指定区间
pop_back 删除字符串最后一个字符
erase 删除字符串指定部分
swap 交换两个字符串
3.4.1 字符串的增加

首先我们来介绍字符串的增加操作,在末尾添加字符我们可以使用 push_back,在末尾添加字符串我们可以使用 append,而 operator+= 既可以在末尾添加字符,也可以添加字符串,insert 可以在任意位置追加字符或者字符串。

cpp 复制代码
void Test12()
{
    string s("hello tata!");
    // 追加一个!
    s.push_back('!');
    cout << s << endl;
    s += '!';
    cout << s << endl;
    s.insert(0, 1, '!');
    cout << s << endl;
    // 追加一个字符串
    s.append("hello ");
    cout << s << endl;
    s += "world!";
    cout << s << endl;
    // 在下标0处追加字符串
    s.insert(0, "hello ");
    cout << s << endl;
}

但注意要谨慎使用 insert 因为在头部或中间位置插入必然要挪动数据。大量使用会降低程序效率。

当然 append 与 insert 的接口不止这些,下面是具体的的接口,需要时直接查文档即可。

3.4.2 字符串的替换

接下来我们来介绍两个字符串替换的函数 assign 以及 replace,其中 assign 是直接替换掉原字符串,而 replace 是替换原字符串的某段区间。

cpp 复制代码
void Test13()
{
	string s1("hello world!");
	string s2;
	//直接用s1替换s2
	s2.assign(s1);
	cout << s2 << endl;
	//用s1的某段区间替换s2(第三个参数省略,默认为string::npos)
	s2.assign(s1, 6);
	cout << s2 << endl;
	string s3("i am tata!");
	//用s1替换掉2下标长度为2的区间
	s3.replace(2, 2, s1);
	cout << s3 << endl;
	//用字符数组前n个字符替换
	s3.replace(0, 2, "hhhh", 2);
	cout << s3 << endl;
}

replace 也会挪动数据所以也不要大量使用。

当然 assign 与 replace 的接口不止这些,下面是具体的的接口,需要时直接查文档即可。

3.4.3 字符串的删除

字符串的删除有支持删除最后一个字符的 pop_back,也有支持删除任意区间的 erase。

cpp 复制代码
void Test14()
{
	string s("hello tata!");
	cout << s << endl;
	//删除最后一个字符
	s.pop_back();
	cout << s << endl;
	//删除迭代器所指字符
	s.erase(s.begin());
	cout << s << endl;
	//删除0下标长度为3的一段区间
	s.erase(0, 3);
	cout << s << endl;
	//删除一段迭代器区间
	s.erase(s.begin(), s.end() - 2);
	cout << s << endl;
}

注意 erase 也需要谨慎使用,因为删除也要挪动数据。

3.4.4 字符串的交换

最后我们来介绍一个字符串的交换函数 swap,这个 swap 函数与算法库中的 swap 函数并不相同,算法库中的 swap 函数将 string 中每个具体值都交换。而 string 中的 swap 函数实现的是指针交换,效率明显高的多。

cpp 复制代码
void TestSwap()
{
	string s("hello tata!");
	string p("hello world!");
	cout << s << endl;
	cout << p << endl;
	s.swap(p);
	cout << s << endl;
	cout << p << endl;
}
3.4.5 string的其他操作

除了上面操作外,string 还有一些格外补充操作,这里值挑几个常用的函数为大家介绍。·

函数名称 功能
c_str 返回 C 格式的字符串
substr 从字符串 pos 位置开始,截取 n 个字符,然后将其返回
find 从字符串 pos 位置开始往后找字符 c,返回该字符在字符串中的位置
rfind 从字符串 pos 位置开始往前找字符 c,返回该字符在字符串中的位置
find_first_of 在原字符串中,从前往后找匹配串中第一个匹配的字符
find_last_of 在原字符串中,从后往前找匹配串中第一个匹配的字符
find_first_not_of 在原字符串中,从前往后找匹配串中第一个不匹配的字符
find_last_not_of 在原字符串中,从后往前找匹配串中第一个不匹配的字符
getline 获取一行字符串
operator<< 流提取重载
operator>> 流插入重载

首先是 substr 与 find 函数,这两个函数可以结合使用。

cpp 复制代码
void Test15()
{
	string s("hello tata!");
	// 在字符串s中寻找b
	size_t pos = s.find('t');
	// 从下标6开始截取长度为6的字符串
	string str = s.substr(pos, 6);
	cout << str << endl;
}

需要注意的是如果 find 不存在查找目标,那就返回 npos。

find_first_of,find_last_of,find_first_not_of 与 find_last_not_of 的用法非常类似,我们就只以其中一种举例:

cpp 复制代码
void Test16()
{
	string str("Please, replace the vowels in this sentence by asterisks.");
	//从str字符串中任意匹配aeiou
	size_t found = str.find_first_of("aeiou");
	//找不到返回npos
	while (found != string::npos)
	{
		str[found] = '*';
		//从下一个位置找
		found = str.find_first_of("aeiou", found + 1);
	}
	cout << str << endl;
}

这里是 find 与 find_last_of 详细的接口,大家可以根据实际需求参考使用。

最后我们来介绍以下 getline,它与流插入都是读取字符串,但是区别就是 getline 遇见空格不会停止,而流插入会停止。这区别和 C 语言中 scanf 与 gets 类似

相关推荐
zmzb01031 小时前
C++课后习题训练记录Day38
开发语言·c++
福尔摩斯张1 小时前
Linux进程间通信(IPC)机制深度解析与实践指南
linux·运维·服务器·数据结构·c++·算法
lijiatu100862 小时前
C++ 类成员变量声明语法错误
java·开发语言·c++
zore_c2 小时前
【C语言】带你层层深入指针——指针详解2
c语言·开发语言·c++·经验分享·笔记
cookies_s_s2 小时前
项目--协程库(C++)前置知识篇
linux·服务器·c++
zmzb01032 小时前
C++课后习题训练记录Day39
数据结构·c++·算法
qq_310658512 小时前
mediasoup源码走读(二)环境搭建与 Demo 运行
服务器·c++·音视频
XiaoCCCcCCccCcccC3 小时前
多路复用 select -- select 的介绍,select 的优缺点,select 版本的 TCP 回显服务器
服务器·c++
XiaoCCCcCCccCcccC3 小时前
多路复用 poll -- poll 的介绍,poll 的优缺点,poll 版本的 TCP 回显服务器
服务器·网络·c++