C++之string类详解(超详细)

这节我们来学习一下之前C语言没有的,但是我们却一直想要使用的一样东西——string 

文章目录

目录

前言

一、C/C++对字符串的区别

1.C语言中的字符串

2.C++中的字符串

3.C/C++字符串主要区别

二、标准库中的string类

1.auto和范围for

1.1 auto

2.范围for

2.string类的引入

3.string类的接口说明

3.1  构造函数(Construct)

3.2 析构函数 (destructor)

3.3  字符串赋值重载(std::string::operator=)

3.4 迭代器相关函数

3.5 容量相关函数

3.6 元素访问相关函数

3.7  修饰符相关函数

3.8 字符串相关函数

3.9 非成员函数重载

3.10 成员函数(npos)

总结


前言

在我们C语言阶段,我们只接触了一些基础的变量,当时我们如果想要存储一个字符串变量,我们需要使用一个字符串数组来进行存储,然后以'\0'作为结束的标志,而且确定后,字符串的大小是不可再更改的。在后面的学习与实践中,开始对字符串进行一些操作,尽管C语言中提供了一些处理字符串的函数但是使用起来并不方便,于是C++引入了string类。


一、C/C++对字符串的区别

1.C语言中的字符串

在C语言中,字符串并没有专门的string类。C语言使用字符数组来表示一个字符串。字符数组的最后一个元素通常是空字符('\0),表示的是字符串的结束。

在C语言中没有提供直接的字符串类型,而是通过字符数组和标准库函数来处理字符串,常用的字符串操作函数包括:

  • strlen(str):返回字符串的长度(不包括末尾的空字符)。
  • strcpy(dest, src):将一个字符串复制到另一个字符串。
  • strcmp(str1, str2):比较两个字符串。
  • strcat(dest, src):连接两个字符串。

2.C++中的字符串

C++中引入了标准库<string>,提供了更为强大更为灵活的字符串类型——std::string。这个类封装了字符串的管理,并提供了许多便利的操作方法。

std::string是C++标准库的一部分,它提供了对字符串的内存管理和操作的封装。C++中的字符串大小是可以进行变化的,因为它是由一个类实例化产生的对象,并非是一个固定的字符数组,而且它的大小是可以自己手动调整的。std::string中还提供了许多成员函数来简化对字符串操作,常用的操作包括:

  • length() 或 size():返回字符串的长度。
  • c_str():返回C风格的字符串(即const char*)。
  • substr(start, length):提取子字符串。
  • find():查找子字符串的位置。
  • += 或 append():字符串连接。

3.C/C++字符串主要区别

特性C语言字符串C++字符串 (std::string)
类型字符数组 (char[])类 (std::string)
内存管理手动管理(需自己分配、调整大小)自动管理(动态调整大小)
操作简便性通过标准库函数进行操作提供丰富的成员函数,操作简便
字符串大小固定大小(或通过手动管理)动态大小(自动扩展)
内存分配静态或手动动态分配自动分配和回收

二、标准库中的string类

上图是来自C++Reference(搜索C/C++知识点的网站),介绍string是表示字符串序列的对象。这个标准的string类还提供了一些了类似于标准库容器的接口来处理那些字符/字符串。我们在标题的下面可以看到typedef basic_string<char> string。string其实是由basic_string这个模板类实例化出来的,使用char作为它的类成员类型。

1.auto和范围for

我们在开始介绍string类之前,首先来介绍两个C++11的补充知识。

1.1 auto

我们之前在定义一个变量时,我们都要明确这个变量的数据类型,我们不能够盲目地写上一个数据类型并进行赋值初始化。情况好的编译器会帮我们进行一下隐式类型转换,情况差的话,可能直接给你弹出一个报错了。为了避免这种尴尬的情况,C++提供了一个强大的数据类型——auto类型。我们看它的中文意思(自动的),就可以猜出来个大概。在C++中,auto是一个关键字,用于自动推导变量的类型。它让编译器根据变量的初始化值来推断该变量的类型,从而避免显式地指定类型。这使得代码更加简洁、易于维护,尤其在处理复杂类型时非常有用。

auto对于指针类型与引用类型同样适用。但是这两个的类型推导有点特殊,在对于指针进行类型推导时,auto与auto*是没有任何区别的,都能够推导出来指针类型的。

但是对于引用的类型推导,如果我们想要它自动推导出来引用类型的话,我们就需要使用auto&来进行推导,因为auto是根据等号右边所给的变量类型在编译器编译时期自动推导的,我们如果使用auto来推导一个引用类型(我们在之前就已经学习过了引用就是对一个变量取别名,还是指的是哪个变量),那么auto就会去那个最初的变量的数据类型。

如果在同一行中一次声明多个变量来使用auto来进行推导的话,必须确保它们是同一种数据类型,否则编译器会发生报错。实际上我们在使用auto来推导一行变量时,它只会推导出来第一个变量的数据类型,然后使用这个数据类型来定义其他的变量。auto不可以直接声明数组的,因为数组的数据类型是数组元素类型加上数组元素个数,auto无法推导出来数组个数的,因此不能直接声明数组,但是我们可以间接通过一个指针来声明一个已经定义的数组。

int arr[5] = {1, 2, 3, 4, 5};
auto ptr = arr;  // 这里,ptr 被推导为 int* 类型

auto不仅可以推导出来数据类型,还可以推导函数返回类型和函数参数类型(后面两种我们一般建议不要多使用,可以适当使用,我们这里介绍一下使用场景)

1. 作为返回值类型

auto 允许编译器根据返回的值推导出函数的返回类型。例如:

auto add(int a, int b) 
{
    return a + b;  // 返回类型会根据表达式推导出来,int 类型
}

在这个例子中,add 函数的返回类型由编译器推导为 int,因为 a + b 的结果是整数。

2. 作为函数参数类型

auto 不能直接作为函数参数类型(例如 auto x),但可以用于 模板函数推导类型的参数(C++14及之后支持)。一个常见的用法是用 auto 来推导参数类型(特别是在 lambda 表达式中):

// 使用 auto 在模板函数中推导参数类型
template <typename T>
auto multiply(T a, T b) 
{
    return a * b;  // 返回值类型由编译器推导
}

在 C++14 以后,你还可以通过 泛型参数推导来使用 auto 作为函数参数类型:

auto sum(auto a, auto b)
 {
    return a + b;
}

这个例子中,auto 会根据传入的实际类型推导参数 ab 的类型。

3. 作为 Lambda 表达式参数类型

在 lambda 表达式中,auto 可以用于推导参数类型:

auto add = [](auto a, auto b)
 {
    return a + b;
};

在这个例子中,auto 用于 lambda 的参数,允许传入任意类型的参数。

2.范围for

C++中的 范围-based for 循环(也叫 range-based for loop)是 C++11 引入的一种简化的遍历容器元素的语法。它通过自动遍历一个容器(例如数组、std::vectorstd::array 等),让你无需显式地写出迭代器或索引。我们之前在C语言中就已经学习过了for循环,但是我们每次写一个for循环都要写一大堆,要写初始条件,循环条件,变量的修改等等,有时候不知道数组或者容器的大小和里面的数据类型还头疼。然后C++中就很好地解决了这些问题,我们使用范围for就可以直接遍历那个数组或者容器,我们也不必去考虑起始位置在哪里,循环条件是什么了,至于数据类型,我们可以使用上面我们刚刚学习的auto,这个可以在范围for中一起使用,它可以直接推导出来容器或者数组里面元素的数据类型。这样一看,for循环是不是就简单多了,而且代码简洁明了。

基本语法如下:

for (declaration : container) 
{
    // 代码块
}

//declaration:每次迭代时,container 中的元素会被赋给 declaration,可以是元素的引用或者值。
//container:这是要遍历的容器,可以是数组、std::vector、std::map 等支持迭代的类型。

1.遍历数组

int arr[]={1,2,5,6,4,8}
for(auto i:arr)
{
    cout<<i<<" ";
}

2.遍历容器

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> vec = {10, 20, 30, 40, 50};
    for (int num : vec)
    {
        std::cout << num << " ";
    }
    
    return 0;
}

注意:我们如果想要使用这个范围for来进行一些修改的话,我们要使用引用。因为,范围for每次都是在:左边来获取值赋给右边的,如果我们在一个范围for中修改了,但是没有使用一个数组或者容器来进行存储,我们再通过范围for去遍历访问数组或容器里面的值的话,那样输出的还是原来的那些内容。除此之外,对于一些比较长的字符串,我们也建议使用引用,那样可以减少传值时的消耗。

2.string类的引入

我们现在使用的string都是前人实现好的,我们直接拿来使用就好了。在C++标准库中,就有string类,如果我们想要使用string的话,我们要先在头文件那里包一个string,意思就是调用了标准库中的类了,注意我们也要引入标准库的命名空间,否则我们需要逐个去指定命名空间来调用string类。

#include<iostream>  //标准库中的输入输出流库
#include<string>    //标准库中的string类
using namespace std;//引入标准库的命名空间

3.string类的接口说明

std::string 提供了非常丰富的成员函数来操作字符串。通过这些函数,你可以方便地进行字符串的创建、修改、查找、截取等操作,能够极大地提高 C++ 字符串处理的效率和便利性。接下来,让我给大家一一来介绍一些一些常见的函数。

(接下来的内容来自于C library - C++ Reference

首先我们来介绍几个string类中的成员函数,我们在之前类与对象中就已经学习过了,一个类一定会有构造函数,析构函数,拷贝构造函数等等。同样地,string类也配备了这些函数

3.1  构造函数(Construct)

这个函数是用来对string类进行初始化操作的,我们实例化一个string类对象后,我们要对这些对象赋予一些内容,而构造函数就是进行这些工作的。而string类提供丰富类型的构造函数原型如下:

第一种构造函数就是初始化一个空字符串;

第二种构造函数是一个普通字符串对象初始化给对应字符串对象;

第三种构造函数是将一个字符串对象的第pos位置之后的len个字符初始化给对应的字符串对象,注意这里不是尾插,而是将原来的那个字符串擦除了,然后给它初始化;

第四种构造函数是将const修饰的字符串初始化给对应字符串对象;

第五种构造函数是将一个const修饰的字符串的前n个字符初始化给对应字符串对象;

第六种构造函数是将n个字符初始化给对应字符串对象;

第七种构造函数是使用了迭代器的模板,使用迭代器来将另一个字符串初始化给对应字符串对象;

第八种构造函数使用初始化列表来进行初始化,是按照string类中成员变量声明的顺序进行初始化的;

第九种构造函数是移动构造函数,获取一个未指定但有效的状态的字符串,我们从上面的格式可以看出来我们传递的参数是地址的引用(有两个&)(这种构造函数我们了解一下即可,暂时不必掌握);

	string s1("hello world");
	string s2;//我们如果想要定义一个空的字符串,我们直接定义一个对象即可,不用加(),否则会发生报错
	string s3("x");//对于string实例化对象的s3只能够初始化字符串,不能够初始化字符。如果想要初始化字符不可以使用‘’,要使用“”。
	string s4(10, 'x');
	string s5(s4, 5, 3);//将s4中第5个位置之后的后面三个字符初始化给s5
	string s6 (s5.begin(),s5.end());

3.2 析构函数 (destructor)

这个函数是对string类销毁后的一个内存清理,由于string类是一个申请了内存空间的类,所以这个析构函数是需要释放空间的,至于其具体实现我们在后面会详细介绍的,析构函数的原型如下:

3.3  字符串赋值重载(std::string::operator=)

这个函数是对类中赋值运算符进行一个重载,使其能够进行string类中的赋值,为字符串赋予一个新的内容从而来替换原来的内容,这个函数也有多种原型如下:

第一种字符串赋值时将一个const修饰的字符串对象赋值给对应的字符串对象,由于我们要确保要赋值过去的那个字符串是不可以进行修改的,因此我们将其使用const进行修饰,那样这个字符串就是只读的;

第二种字符串赋值是将一个const修饰的字符串赋值给对应的字符串对象,同上面一样我们要确保它是不可以进行修改的;

第三种字符串赋值是将一个普通的字符赋值给对应的字符串对象,我们一般只将字符串进行const修饰,单个字符是不进行const修饰的;

第四种字符串赋值是使用初始化列表进行赋值的,如果初始项给了缺省值,那么就用缺省值来进行初始化,没有给就根据相应编译器的初始化规则来进行初始化;

第五种字符串赋值是将未指定但有效的状态的字符串str赋值给对应的字符串对象。

	string s1("123456");
	string s2("abcdef");
	string s3("XXX");
	string s4("oooo");

	s1 = s2;
	s3 = "yyy";
	s4 = 'a';

3.4 迭代器相关函数

在C++中,迭代器是一种对象,它能够访问容器中的元素,就像指针一样(但它不是指针)。迭代器提供了一种统一的方式来遍历容器,支持不同类型的容器,如vectorlistmap等。使用迭代器可以让你以容器无关的方式操作数据。无论使用什么类型的容器,迭代器都提供了一些常见的操作:

  • 解引用(*:通过解引用迭代器来访问当前元素。
  • 迭代器加法(++:迭代器可以通过递增来移动到容器中的下一个元素(对于双向和随机访问迭代器,还可以使用递减操作符--)。
  • 比较操作(== 和 !=:用于判断两个迭代器是否相等,通常用于循环终止条件。

接下来,我们就来介绍一下,string类中关于迭代器的一些函数。

这个函数是返回一个迭代器,这个迭代器返回第一个有效字符,可以进行解引用操作,上面的有两种函数原型,一种是针对普通迭代器,另一种是用来针对const修饰的迭代器(其实const修饰的是迭代器所指的内容)。

	string s1("abcdef");
	string::iterator it = s1.begin();
	//我们使用迭代器来获取值时,我们要使用*解引用一下,才能获取真正的值
	cout << *it << endl;

这个函数时返回一个迭代器,这个迭代器指向的是最后一个有效字符的后面一个理论元素(迭代器的结束标识),我们不能够进行解引用操作,如果你非要进行解引用的话,就会发出报错(越界了)。由于在标志库中是不包括结束迭代器所指向的内容,因此我们一般和begin迭代器一起使用,以此来指定包含此字符的所有元素,如果为空字符串,那样和begin迭代器所指向的内容相同。如果字符串对象是一个const修饰的那么就使用const_iterator,否则使用iterator。


	string s1("abcdef");
	string::iterator it = s1.begin();
	while (it!=s1.end())
	{
		cout << *it << " ";
		it++;//++我们要移动迭代器,不用加*
	}

这个函数返回一个反向迭代器,迭代器所指向的内容是字符串的最后一个有效字符(即反向开头),可以进行解引用,与上面的begin函数一样的,也有两个函数原型。这个函数与上面的begin函数刚好对着来,一个顺序迭代,一个逆序迭代。rbegin前面的那个前缀r这里指的就是reverse(逆置的)。反向迭代器的迭代方向是由后往前进行迭代的。

	string s1("abcdef");
	string::reverse_iterator it = s1.rbegin();
	cout << *it << endl;

这个函数返回一个反向迭代器,迭代器所指向的内容的是字符串第一个有效字符的之前一个理论元素(迭代器的结束标识)同样地,这个和上面的end函数很像,它所指向的内容同样是不可以解引用的。它一般和rbegin一块使用,来表示字符串的所有元素。移动迭代器同样是++,不过这个迭代器是反向移动的。

	string s1("abcdef");
	string::reverse_iterator it = s1.rbegin();
	while (it != s1.rend())
	{
		cout << *it << " ";
		it++;
	}

这个函数返回一个const_iterator,这个迭代器返回的是字符串的第一个有效字符,这个迭代器所指向的字符串是由const修饰,这个迭代器可以增加或减少,但是我们不能对其内容进行修改,即使它指向的字符串对象不是const修饰的。对于const修饰的字符串只能够使用这个迭代器来进行访问。

	const string s("123");
	string::const_iterator it = s.cbegin();
	cout << *it << endl;

这个函数返沪一个const_iterator,这个迭代器返回的是字符串的最后一个有效字符之后的一个理论元素,与上面一样的是针对const的字符串。我们同样搭配着cbegin一块使用。

	const string s("123");
	string::const_iterator it = s.cbegin();
	while (it!=s.cend())
	{
		cout << *it << " ";
		it++;
	}

这个函数返回一个迭代器,迭代器所指向的是字符串的最后一个有效数据(反向开端),是可以进行解引用的,这个函数是上面cbegin和rbegin函数的一个结合版,将两者的特点结合在一起了。

const string s("123");
string::const_reverse_iterator it = s.crbegin();
cout << *it << endl;
 

这个函数返回一个迭代器,迭代器所指向的内容是字符串的第一个有效字符之前的一个理论元素,是不可以进行解引用操作的,这个函数是上面的rend,cend函数的一个结合版。它要搭配着crbegin一块使用。

	const string s("123");
	string::const_reverse_iterator it = s.crbegin();
	while (it!=s.crend())
	{
		cout << *it << " ";
		it++;
	}

3.5 容量相关函数

string类实例化出来的是一个字符串,那么我们就要对它的空间好好研究研究,在string类中也提供了许多相关的函数。

上面两个函数的功能可以说是一模一样的,都是用来求取字符串的长度(单位为字节,不包含终止符’’\0'),那为什么要设计两个函数呢?这就要从string刚开始设计的时候开始说起来了,string类的设计是在STL之前的,那时候前人们为了计算string对象的大小就写出来length这个函数,后来开始设计STL了,它们同样需要求取大小,但是对于一些容器比如tree而言,length表示大小就不太合适了,于是就又设置了一个函数size,后面一般求取大小都是使用size的,length只是顺延下来了。这里我们还要注意的是,我们求取的只是字符串的实际长度,不一定等于字符串的容量大小。

这两个函数都是返回无符号整型(size_t),因为个数不可能为负数的。这个函数是无参函数,我们不用传递任何参数,我们只要通过相应容器调用即可。

	string s("hello world");
	cout << s.size() << endl;
	cout << s.length() << endl;

这个函数返回的是一个字符串对象所能够达到的最大长度(单位是字节),这个值其实与字符串内容没有多大关系,而是与编译器的环境有关,不同环境下的max_size是不同的。

这个函数是用来返回为当前字符串分配的存储空间大小用字节来表示,这个空间大小不一定等于字符串长度,它可以大于还可以等于,对于额外的空间可以允许字符串添加新字符来进行优化存储空间。这个capacity是根据字符串的size所决定的,如果size发生改变了, 编译器也会自动进行扩容(重新为其分配存储空间,不同的编译器分配的方式也有所不同),这个存储空间的限制由max_size所决定。

上面我们写了一个程序,逐个往里面加入字符,使字符串的size不断变大,然后我们再来观察一下,capacity的变化。我们要先提前知道一个点:size和capacity都是不包括终止符'\0'的。我们从上面的运行结果我们,我们可以看到除了第一次是2倍的扩容,其他都是大约1.5倍的进行扩容,这是在vs2022下运行的结果,在Linux中的扩容方式也有所不同,在Linux中都是2倍的进行扩容的。

这个函数是用来对一个字符串的有效数据进行修改的,上面有两种函数原型,第一种传递的参数是一个无符号整型,即传递一个参数将其的size值进行修改,第二种,多了一个参数char c,这个参数是为了我们后面如果修改的size比原来的size大的话用来补充的字符。

对于resize有三种情况:

1)n < size :我们想要修改的size值小于原来的size值,那么我们就要删除一些数据,来缩小size值,对于字符串而言就是将‘\0'放前移动了而已;

2)size < n < capacity: 如果我们修改的size值处于这个区间里面,那么size就要增大,但是capacity还不用改变,我们就要增加一些数据,如果我们使用上面第一种函数原型来进行修改size的话,它增加的内容就是' ',以此来补充缺少的size值,如果我们使用的是上面的第二种函数原型来进行修改size的话,它增加的内容就是'c'(c是我们任意指定的字符)来进行补充;

3)n > size :如果我们想要修改的值已经大于了capacity了,那么我们size值与capacity都要进行修改了,capacity编译器自己会进行扩容,size和2)一样增加相应的字符来进行补充。

这个函数可以根据请求的大小来修改字符串的容量大小,长度最多为n个字节,如果n大于字符串长度的话,那么这个函数就会要求使其容量增加到n(或者更大)。这个函数对于字符串的内容没有要求,也不能改变它的内容(不会对字符串进行缩减)因为即使我们reserve的值小于字符串的长度,编译器自己也会进行扩容,确保字符串都能容下的。我们使用这个函数可以在已知一个字符串的前提下使用,那样我们就可以减少不断扩容所带来的消耗。

string s;
s.reserve(1024);  //预留了1024字节的空间

这个函数是对一个字符串里面的内容进行清空,注意:我们只是对字符串的内容进行清空(将'\0'移到前面去了),使字符串的size值置为0,但是字符串的容量是不会改变的,即使我们之前已经经过了几次扩容了。

	string s1("hello world");
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

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

这个函数的返回类型是布尔型,那么返回值为true或者false,但是在vs2022中,返回真假用1,0来代替。这个函数是判断一个字符串是否为空(字符串的长度是否为0),仅此而已,它不能够对字符串做任何修改。

这个函数是请求其字符串减少其容量来适应字符串的大小,这个函数也不能够修改字符串的内容,仅仅是修改capacity。但是这个请求并非是一定的,只是一个请求,最终决定权还是在于编译器,容器实现可以自由优化,它可以保留容量大于其字符串的大小。如下代码所示,它并没有修改capacity值,还是使用原来的那个capacity,可能在某些方面编译器所需求的吧。

3.6 元素访问相关函数

string类对象本质还是一个字符串,而字符串也是一个字符串数组,如果我们想要访问字符串中的某个字符时,我们就要使用一些函数,当然这个string类早已经为我们安排好了已经。

首先这个函数就是我们之前在数组中经常使用的一个运算符[ ],在string类中它又进行了一次运算符重载,使我们也能在字符串中通过这个来访问其中某个字符。这个函数有两种函数原型,两个函数的参数都是相同的,不过一个没有使用const修饰,一个没有使用const修饰,其所针对的对象应该也挺明显的,一个是针对于普通的字符串,一个针对于有const修饰的字符串。这样我们就可以根据索引值来找字符串中的某个字符了。这里有一个注意的是字符串的第一个位置是0不是1。

这个函数返回对字符串中位置 pos 处的字符的引用。该函数会自动检查 pos 是否是字符串中字符的有效位置(即 pos 是否小于字符串长度),如果不是,则抛出 out_of_range 异常。我们可以通过这个函数来判断某个位置是否是这个字符串的有效位置。

	try
	{
		string s1("hello world");
		s1.at(20);
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	//try{}catch(){}是用来捕获异常的方法,后面我们会学习到的

这两个函数的功能差不多,第一个是返回最后一个有效字符的引用(即内容),第二个是返回第一个有效字符的引用,两者都不能对空字符串使用。front函数于迭代器中的begin函数有所不同,front函数返回的是字符的直接引用,并不需要解引用,而begin需要进行解引用。这两个函数都是直接调用即可,并未其他要求。

3.7  修饰符相关函数

我们对字符串不能仅仅只为了创建访问它,我们还要对它进行修改,string类中提供了一些函数可以进行删除,插入,替换等等。

这个函数是+=运算符的重载,我们将这个算术运算符重载为可以+=字符/字符串了,但是这个函数只能够进行尾插,不能指定任意位置插入。这个函数有上面多种函数原型。

第一种表示可以+=上一个字符串对象到对应的字符串对象上去;

第二种表示可以+=上一个字符串到对应的字符串对象上去;

第三种表示可以+=一个字符到对应的字符串对象上去;

第四种表示从初始值设定项列表声明符自动构造的,然后字符将按相同的顺序附加到字符串中。

这个函数使用起来十分方便,我们想要在字符串对象后面插入什么,直接+=什么就可以了。

	string s1;
	string s2("abcd");
	s1 += s2;
	cout << s1 << endl;
	s1 += 'a';
	cout << s1 << endl;
	s1 += "123";
	cout << s1 << endl;

这个函数通过在字符串的当前值末尾附加其他字符来扩展字符串,它也是尾插,不过它插入的花样可就多了,它上面的函数原型有七个之多,都快赶上了我们最开始学的构造函数了,接下来我来介绍一下上面七种函数原型分别表示什么意思。

第一种表示追加一个字符串对象到对应字符串对象的末尾;

第二种表示将一个字符串对象从第pos位置开始的len个长度的子串追加到对应字符串的末尾;

第三种表示将一个字符串追加到对应字符串对象的末尾;

第四种表示将一个字符串的前n个字符追加到对应字符串对象的末尾;

第五种表示将n个字符c追加到对应字符串对象的末尾;

第六种表示的是一个迭代器模板,通过迭代器的begin函数和end函数将,整个字符串都追加到对应字符串对象末尾;

第七种表示初始值设定项列表声明符自动构造的,然后字符将按相同的顺序附加到字符串对象的末尾。

这个函数相比于上面那个函数就简单多了,这个函数只能够将一个字符插入到对应字符串对象的末尾并且使字符串长度加1,它进行的也是尾插。

string s;
s.push_back('a');

这个函数是分配一个新的值来替换当前字符串中的内容,这个函数也有许多形式,它的这些函数参数我在上面基本都有讲过了,这里就不再一一赘述了,我直接给大家展示它的使用。这个函数,我们需要注意的是:它分配的新值会将原来的内容完全替换掉,不是插入。

//1.assign() 分配,即将新的内容分配给对应的字符串(会取代原来的字符串)
//可以是字符串,也可以是字符串的一部分,但是不可以是单个字符
string s1("hello world");
string s2("123456");
cout << s1 << endl;
s1.assign("xxx");
s1.assign(s2, 2, 3);
s1.assign(s2, 2);
s1.assign("abcdefsg", 3);
s1.assign("x");//使用双引号括起来的,是一个字符串
cout << s1 << endl;

这个函数同样是插入的意思,但是这个插入的形式更加多样化,它能够在任意位置上插入,不再局限于尾插了,它可以头插,可以在中间某个位置进行插入。但是我们在使用这个函数时,我们要明确插入的位置,不要弄错(字符串的起始位置是0)。另外,这个函数不宜多用,因为这个函数不像上面那些函数那样只在字符串末尾进行插入,那样不会对字符串内容造成多大影响,这个函数如果要在中间插入的话,那么后面那部分字符串就要进行移动了,那样字符串移动就会造成一些消耗了。如下图代码所示,我们可以这样使用insert函数,如果我们想在指定位置插入字符/字符串,我们可以直接使用具体位置,也可以使用迭代器来找相应位置。

这个函数是用来删除指定位置的数据并减少字符串长度,上面有三种函数原型。

第一种的表示在指定的位置(pos位置)删除len长度的数据;

第二种表示将迭代器p添加到要删除的字符中,注意这个迭代器表示的是一个字符;

第三种使用迭代器将某个字符串对象的所有内容都添加到要删除的字符串中。

这个erase函数也有检查报错的能力,如果我们越界删除的话,它不会发出assert断言报错,而是抛出异常。

​这个函数将字符串中从字符 pos 开始并跨越 len 字符的部分(或 [i1,i2) 之间范围内的字符串部分)替换为新内容。这里我们有一个计算区间长度的小技巧就是如果是左闭右开的区间,我们直接使用右边界减去左边界即这个区间的长度。这个函数也是不宜多用的,因为如果我们不能确保要替换的内容中的字符个数相同的话,就会造成一些字符串的移动,一段字符串还好,如果是一大段字符串的话,那样的消耗是极其巨大的。上面的函数原型咋一看是不是感到头皮发麻,冷静下来,我们仔细观察可以发现,上面那些参数,第一个参数都是表示要替换的起始位置,第二个参数都是要替换的长度(迭代器除外)。如果我们使用迭代器来表示位置来替换某些字符时 ,我们注意我们要只能使用我们原来字符串对象的迭代器,不可以使用其他对象的迭代器

这个swap函数是不是很熟悉,对的在C++标准库中也有一个swap函数,但是那个swap函数与这个函数有所区别,标准库中的swap函数的实现原理是对一个要替换的对象创建一个和它一样大的空间,然后将将其内容拷贝过去,最后再将原来的内容拷贝给替换给我们内容的对象的空间中。但是string类中的swap函数仅仅将str中的内容与容器中的内容进行替换,实际上两者只是改了一个指针的事儿,这么一说,是不是觉得string类中的swap函数方便多了。

string s1("123456");
string s2("abcef");
s1.swap(s2);
cout<<s1<<endl; //abcdef
cout<<s2<<endl; //123456

这个函数擦除字符串中最后一个有效字符,并有效地将有效长度减1。这个就是我们之前顺序表中的尾删,由于字符串的结束标志是'\0',我们只有将'\0‘往前移动一位,然后size--,这应该就是pop_back的实现原理。

string s1("123456");
s1.pop_back();
cout<<s1<<endl; //12345

3.8 字符串相关函数

在string类中还提供了一些对字符串操作的接口:

返回一个指向数组的指针,该数组包含以 null 结尾的字符序列(即 C 字符串),表示字符串对象的当前值。此数组包含构成字符串对象值的相同字符序列,以及末尾的附加终止 null 字符('\0')。这个函数就是将C++中的字符串对象等效转化为C语言风格的字符串。有时候,我们只能使用一些C语言风格的字符串比如调用C语言中的文件,那时我们就可以调用这个函数将其转化。这个函数我们需要注意的是,我们返回的是一个const修饰的字符串数组,我们是不可以对其进行修改的,返回的指针还可能会因为进一步对其他函数进行修改而失效,这个要记住。

string s1("hello world");//这是一个字符串对象
cout<<s1.c_str()<<endl;  //我们将其转化为C语言风格的字符串输出

返回一个指向数组的指针,该数组包含以 null 结尾的字符序列(即 C 字符串),表示字符串对象的当前值。此数组包含构成字符串对象值的相同字符序列,以及末尾的附加终止 null 字符 ('\0')。返回的指针指向string对象当前使用的内部数组,用于存储符合其值的字符。这个函数与上面的c_str的功能是一样的,也是返回一个字符串数组。

string s1("hello world");//这是一个字符串对象
cout<<s1.data()<<endl;  //我们将其转化为C语言风格的字符串输出

这个函数是将字符串对象当前值的子字符串复制到 s 指向的数组中。此子字符串包含从位置 pos 开始的 len 字符。该函数不会在复制内容的末尾附加 null 字符,如果想要得到一个C语言风格的字符串可以手动地添加一个'\0'。这个函数返回的并不是一个字符串,而是复制到 s 指向的数组的字符数。这可能等于 len 或 length()-pos (如果字符串值短于 pos+len)。

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, C++ World!";
    char buffer[20];  // 目标字符数组

    // 从位置 0 开始拷贝 5 个字符
    size_t copied = str.copy(buffer, 5, 0);

    buffer[copied] = '\0';  // 手动添加 '\0',使其成为 C 风格字符串

    std::cout << "Copied string: " << buffer << std::endl;
    std::cout << "Number of characters copied: " << copied << std::endl;

    return 0;
}
Copied string: Hello
Number of characters copied: 5

这个函数的功能是在字符串中查找内容。它在字符串中搜索由其参数指定的序列的第一个匹配项。
指定 pos 时,搜索仅包含位置 pos 或之后的字符,而忽略任何可能出现的包括 pos 之前的字符。
请注意,与成员函数find_first_of不同,每当搜索多个字符时,仅匹配其中一个字符是不够的,而是整个序列必须匹配。

如上面的函数原型所示,我们可以查找字符可以字符串对象还可以查找一个字符串的子串。

int main() {
    std::string str = "Hello, C++ World!";
    char ch = 'C';
    std::string subStr = "C++";
    std::string subStr = "o";

    size_t pos = str.find(ch);
    size_t pos = str.find(subStr);
    size_t pos = str.find(subStr, 5); 

     if (pos != std::string::npos) 
    {
        std::cout << "Substring '" << subStr << "' found at position: " << pos << std::endl;
    }
     else 
    {
        std::cout << "Substring not found!" << std::endl;
    }

    return 0;
}

这个函数也是查找某个字符/字符串,在字符串中搜索由其参数指定的序列的最后一次出现。指定 pos 时,搜索仅包括从位置 pos 或之前开始的字符序列,而忽略从 pos 之后开始的任何可能的匹配项。这个函数与上面的find有所区别但又很像,上面那个find是顺序查找,这个函数是逆序查找的。其使用方法和上面的find函数是一样的,这里就不作演示了。

这个函数的功能是在字符串中查找字符,但是这个函数与上面的find函数有所区别,上面的find函数如果是查找一个子串,需要全部匹配才可以,而这个函数只有任意一个字符匹配即可find_first_of 是 C++ 中 std::string 类的成员函数,它用于在字符串中查找第一个出现的指定字符集中的任意字符,并返回该字符的位置。它的基本功能是搜索一个或多个字符在字符串中第一次出现的位置。上面的pos值,我们要注意如果pos值大于字符串长度,那样就永远都找不到字符位置了。

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";
    
    // 查找第一个出现的字母 'o', 'l', 'W' 中的任意一个字符
    size_t pos = str.find_first_of("olW");
    
    if (pos != std::string::npos) 
    {
        std::cout << "Found at position: " << pos << std::endl;
    } else 
    {
        std::cout << "No match found." << std::endl;
    }
    
    return 0;
}

//1.查找单个字符
std::string str = "hello world";
size_t pos = str.find_last_of('o');
std::cout << "The last occurrence of 'o' is at position: " << pos << std::endl;
//The last occurrence of 'o' is at position: 7


//2.查找多个字符(例如查找 "h", "e", "o" 中最后一次出现的字符):
std::string str = "hello world";
size_t pos = str.find_last_of("heo");
std::cout << "The last occurrence of any of 'h', 'e', or 'o' is at position: " << pos << std::endl;
//The last occurrence of any of 'h', 'e', or 'o' is at position: 7

//3.指定查找的起始位置:
std::string str = "hello world";
size_t pos = str.find_last_of('l', 5);  // 从位置 5 开始查找
std::cout << "The last occurrence of 'l' from position 5 is at position: " << pos << std::endl;
//The last occurrence of 'l' from position 5 is at position: 3

这两个函数分别与上面那两个函数的功能相反,上面两个是找到的第一个和最后一个字符,这里是找不到的第一个和最后一个字符。find_first_not_offind_last_not_of 是 C++ 标准库中 std::string 类的成员函数,分别用于查找从字符串开始或从字符串末尾开始,第一个或最后一个不属于指定字符集合的位置。

//1. find_first_not_of
std::string str = "aaaabbbb";
size_t pos = str.find_first_not_of("ab");  // 查找第一个不属于'a'或'b'的字符
std::cout << "First character not in 'ab' is at position: " << pos << std::endl;
//First character not in 'ab' is at position: 4


//2. find_last_not_of
std::string str = "aaaabbbb";
size_t pos = str.find_last_not_of("ab");  // 查找最后一个不属于'a'或'b'的字符
std::cout << "Last character not in 'ab' is at position: " << pos << std::endl;
//Last character not in 'ab' is at position: 3

这个函数的功能是生成一个子字符串对象。返回一个新构造的对象,其值初始化为此对象的子字符串的副本。子字符串是对象中从字符位置开始并跨越字符(或直到字符串末尾,以先到者为准)的部分。

参数:

  • pos:起始位置,表示子字符串的起始位置。默认值为 0,即从字符串的开始处提取。
  • len:要提取的子字符串的长度。默认值是 std::string::npos,表示从起始位置提取直到字符串的末尾。

返回值:

  • 返回一个新创建的字符串,包含从原字符串中提取的子字符串。如果 pos 超出了原字符串的范围,或者 len 超出了可用的字符数,函数会抛出一个异常 std::out_of_range
#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";
    
    // 从位置 7 开始,提取 5 个字符
    std::string substr1 = str.substr(7, 5);  // 提取 "World"
    
    std::cout << "Extracted substring: " << substr1 << std::endl;
    
    return 0;
}


//Extracted substring: World

这个函数与我们C语言中学习的strcmp有点像,这个函数是比较两个字符串对象的,将字符串对象(或子字符串)的值与其参数指定的字符序列进行比较。比较的字符串是字符串对象的值,或者(如果使用的签名具有 pos 和 len 参数)从位置 pos 中的字符开始并跨越 len 字符的子字符串。此字符串与比较字符串进行比较,后者由传递给函数的其他参数确定。它们比较的方式是分别比较ASCII码值。它们的返回值与strcmp中的一样

参数:

  • str:要与当前字符串进行比较的字符串。
  • pos:用于指定当前字符串中开始比较的位置。
  • len:指定当前字符串中要比较的长度。
  • subpos:指定 str 字符串中要开始比较的位置。
  • s:C风格的字符串(const char*)来进行比较。

#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello";
    std::string str2 = "World";
    
    int result = str1.compare(str2);  // 比较两个字符串
    
    if (result == 0) {
        std::cout << "Strings are equal." << std::endl;
    } else if (result < 0) {
        std::cout << "str1 is less than str2." << std::endl;
    } else {
        std::cout << "str1 is greater than str2." << std::endl;
    }
    
    return 0;
}

//str1 is less than str2.

3.9 非成员函数重载

上面那些都是string类中的函数,即成员函数,接下来我们再来介绍一些非成员函数的。

这个函数的功能用于连接两个字符串,其返回值是参数中的两个字符串连接而成的一个字符串。

	string s1("hello world hello world");
	string s2("123456");

	string s = s1 + s2;
	cout << s << endl;//hello world hello world123456

这个函数是字符串的关系运算符,用于比较两个字符串的大小,上面提供了这个多函数原型,本质就是比较两个字符串对象的大小。这个函数的返回值是true/false。

这个swap函数是string类的非成员函数,但它与C++标志库中的swap函数也不一样,它可以说是string类中的成员函数swap的一个封装函数,它的参数是两个string类对象的一个引用,我们可以直接使用两个类对象,调用它们的成员函数来进行交换。

这个函数是对流提取运算符的一个重载,使其能够对字符串对象进行输入。我们在之前类与对象中就已经介绍过了。从输入流中提取字符串,将序列存储在 str 中,该序列将被覆盖(替换 str 的前值)。此函数重载 operator>> 的行为如 c-string中的istream::operator>> 中所述,但应用于字符串对象。每个提取的字符都附加到字符串中,就像调用了其成员push_back一样。请注意,istream提取作使用空格作为分隔符;因此,此作只会从流中提取可被视为单词的内容。

这个函数是对流插入运算符的一个重载,使其能够对字符串对象进行输出。将符合 str 值的字符序列插入 os 中。此函数重载 operator<< 的行为与 c-string中的ostream::operator<< 中所述,但应用于字符串对象。

这个函数与上面的operator>>的功能是一样的,都是对字符串的输入,但是上面那个的字符串输入,当遇到" " 或者“\n" 后就输入结束了,但是这个函数只会遇到”\n"才会结束输入,这个函数的名字是获取一行字符串。上面参数中的delim的意思是分隔符的意思,我们还可以自己设置输入结束的分隔符。

3.10 成员函数(npos)

最后我再来介绍一下string类中的一个公开的成员变量,这个变量在上面许多函数中都有着大用处。

这个变量是一个const修饰的静态变量,一般来说,静态变量是不可以在类中进行初始化的,因为我们在类中赋值就会走类中的初始化列表,而静态变量是不属于类的,因此它不能走初始化列表。但是这个是一个特例,有const修饰的无符号整型的静态变量可以直接在类中进行初始化。我们为什么要将这个无符号整型赋值一个-1呢?其实这个-1表示的是最大值(转为二进制补码为32个1即2^32).

总结

这节我们介绍了string的引入,以及它丰富的接口,其实在后面的STL学习与string十分相像的,其中好多函数都是相似的,希望我们能够触类旁通,举一反三。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值