✨✨ 欢迎大家来到贝蒂大讲堂✨✨
??养成好习惯,先赞后看哦~??
所属专栏:C++学习
贝蒂的主页:Betty’s blog
1. C/C++中的字符串
1.1. C语言中的字符串
在 C 语言中,字符串是由字符组成的字符数组,以空字符 '\0'
作为结束标志。由于数组特点,字符串的大小在定义数组时就已经确定,无法更改。
//数组大小为20char str[20] = "hello betty!\n";
当然我们可以通过动态内存分配来来解决这个问题,但无疑非常繁琐。
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);}
1.2. C++中的字符串
虽然C++兼容C语言,在C++中仍然可以使用C语言的字符串,但是C++自己实现了一个关于处理字符串的类–string
,它提供了许多方便的操作和功能,使得字符串的处理更加安全和高效。下面是一个简单的string
的使用:
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
类的特点与用法。
2. string的接口
C++为我们提供了丰富的string接口,我们可以通过对象来调用,为了方便我们学习我们可以通过查询相关文档辅助–string类的接口介绍
2.1. string的迭代器
迭代器(Iterator)是一种用于遍历容器中元素的工具。它提供了一种统一的方式来访问容器中的元素,而无需关心容器的具体实现细节。对于我们迭代器,我们在使用时将其当做指针使用即可。
在string
类中,我们就可以通过迭代器来访问其具体元素,并且也为我们提供了相应的调用函数。
2.1.1. begin()与end()函数
begin()
与end()
函数的使用方法具体如下:
作用:返回指向字符串第一个字符的迭代器。
返回值:普通对象返回iterator
迭代器,const 对象返回const_iterator
迭代器。
iterator
迭代器,const 对象返回const_iterator
迭代器。 begin()
,end()
具体指向情况如下图所示:
然后我们可以通过以下代码来演示效果:
void Test3(){string s1 = "hello betty!";//普通迭代器string::iterator it = s1.begin();//指向第一个位置while (it != s1.end()){cout << *it << " ";++it;}cout << endl;const string s2 = "hello betty!";//const对象//const 反向迭代器 不能改变字符串中的值string::const_iterator itt = s2.begin();//指向最后一个字符的下一个位置while (itt != s2.end()){cout << *itt << " ";++itt;}}
2.1.2. rbegin()与rend()函数
rbegin()
与rend()
函数的使用方法具体如下:
作用:返回指向字符串最后一个字符位置(即其反向开头)的反向迭代器。
返回值:普通对象返回iterator
迭代器,const 对象返回const_iterator
迭代器。
iterator
迭代器,const 对象返回const_iterator
迭代器。 rbegin()
,rend()
具体指向情况如下图所示:
然后我们可以通过以下代码来演示效果:
void Test4(){string s1 = "hello betty!";//普通反向迭代器string::reverse_iterator rit = s1.rbegin();//指向最后一个字符位置while (rit != s1.rend()){cout << *rit << " ";++rit;}cout << endl;const string s2 = "hello betty!";//const对象//const 反向迭代器 不能改变字符串中的值string::const_reverse_iterator ritt = s1.rbegin();//指向最后一个字符的位置while (ritt != s1.rend()){cout << *ritt << " ";++ritt;}}
2.2. string的初始化与销毁
因为string
是一个类,所以我们在初始化时肯定调用其构造函数初始化。以下就是我们常见初始化的接口:
string
的某段区间初始化,其中pos
是字符串下标,npos
是指无符号整数的最大值。第四个使用的是某个字符数组初始化。第五个使用的是某个字符数组前n
个字符来初始化第六个使用的是n
个c
字符初始化。第七个使用的是某段迭代器区间初始化。最后也能通过赋值运算符重载初始化。 下面是具体的代码示例:
void Test5(){//1. 使用我们的默认构造函数,不需要传参。string s1;s1 = "hello betty!";//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 betty!";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
是一个类,出了作用域会自动调用它的析构函数,所以不用显示调用。
2.3. string的容量操作
接下来我们将学习关于string
类常见的容量操作:
函数名称 | 功能 |
---|---|
size | 返回字符串的有效长度 |
length | 返回字符串的有效长度 |
capacity | 返回字符串的容量大小 |
max_size | 返回字符串的最大长度 |
clear | 清空字符串 |
empty | 检查是否为空串,是则返回ture,否则返回false |
reserve | 请求改变字符串的容量 |
resize | 重新设置有效字符的数量,超过原来有效长度则用c字符填充 |
shrink_to_fit | 收缩资字符串容量 |
2.3.1. 有效长度与容量大小
在string
类中,我们可以通过size()
,length()
返回字符串的有效长度;capacity()
返回字符串的容量,其具体效果如下图:
我们也可以通过代码来验证:
void Test6(){string s("hello betty!");cout << s.size() << endl;//有效长度cout << s.length() << endl;//有效长度cout << s.capacity() << endl;//容量大小cout << s.max_size() << endl;//最大大小}
其中有效长度size
以及容量大小capacity
不包括\0
。而max_size
返回字符串最大容量,不同平台下大小可能不一样。而在VS2022
下大小为2147483647
,也就是INT_MAX
的大小。
接下来我们可以来探究一下string
的扩容机制
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()
函数,它主要用来判断字符串是否为空:
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;}}
2.3.2. 有效长度与容量操作
首先我们要介绍的就是clear()
函数,他能清空字符串,也就是改变有效长度size
,但不会改变容量capacity
。
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;}}
接下来我们将介绍两个可以改变容量的函数reserve
,resize
。它们之间不小的差别,首先它们的接口如下
我假设字符串原来有效长度为sz
,那么如果n<sz
,sz<n<capcity
,n>capacity
。两个函数的效果有何不同呢?
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;}
通过上述实验,我们可以总结出以下规律:
当n<sz
时,reserve
并不会发生任何改变,resize
会删除有效字符到指定大小。当sz<n<capcity
时,reserve
并不会发生任何改变,resize
会补充有效字符(默认为’\0)到指定大小。当n>capacity
时,reserve
会发生扩容,resize
会补充有效字符(默认为’\0)到指定大小。 注意:不同平台下扩容效果可能会不同,但是这个规律是不会改变的。 最后我们来介绍一个C++11引入的一个可以缩容的函数shrink_to_fit
**,**它的主要目的就是让有效长度size
与容量capacity
适配。
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;}
2.4. string的访问操作
接下来我们就来介绍string
常见的访问函数:
函数名称 | 功能 |
---|---|
operator[] | 返回指定位置的字符,越界则报错 |
at | 返回指定位置的字符,越界则抛异常 |
back | 返回字符串最后一个字符(不是’\0’) |
front | 返回字符串第一个字符 |
首先是operator[]
这个运算符重载与at
函数,它们的功能类似都是返回指定下标字符,并且char*
类型返回char*
类型,const char*
类型返回const char*
类型。
void Test10(){string s1("hello betty!");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
函数,但实用性不是特别大,大家只需要了解。
void Test11(){string s1("hello betty!");cout << s1.front() << endl;cout << s1.back() << endl;}
2.5. string的修改操作
string
关于修改的函数的接口都比较多,一一列举比较麻烦,这里我们只重点介绍常用的接口,剩下的大家具体使用时查官方文档即可。下面是常见的关于string
修改的函数接口:
函数名称 | 功能 |
---|---|
push_back | 在字符串后追加字符 |
operator+= | 在字符串后追加字符或字符串 |
append | 在字符串后追加字符串 |
insert | 在指定位置追加字符或者字符串 |
assign | 使用指定字符串替换原字符串 |
replace | 用新字符串替换原字符串指定区间 |
pop_back | 删除字符串最后一个字符 |
erase | 删除字符串指定部分 |
swap | 交换两个字符串 |
2.5.1. 字符串的增加
首先我们来介绍字符串的增加操作,在末尾添加字符我们可以使用push_back
,在末尾添加字符串我们可以使用append
,而operator+=
既可以在末尾添加字符,也可以添加字符串,insert
可以在任意位置追加字符或者字符串。
void Test12(){string s("hello betty!");//追加一个!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;}
当然append
与insert
的接口不止这些,下面是具体的的接口,需要时直接插文档即可
2.5.2. 字符串的替换
接下来我们来介绍两个字符串替换的函数assign
以及replace
,其中assign
是直接替换掉原字符串,而replace
是替换原字符串的某段区间。
void Test13(){string s1("hello world!");string s2;//直接用s1替换s2s2.assign(s1);cout << s2 << endl;//用s1的某段区间替换s2s2.assign(s1, 6);cout << s2 << endl;string s3("i am betty!");//用s1替换掉2下标长度为2的区间s3.replace(2, 2, s1);cout << s3 << endl;//用字符数组前n个字符替换s3.replace(0, 2, "hhhh", 2);cout << s3 << endl;}
当然assign
与replace
的接口不止这些,下面是具体的的接口,需要时直接插文档即可
2.5.3. 字符串的删除
字符串的删除有支持删除最后一个字符的pop_back
,也有支持删除任意区间的erase
。
void Test14(){string s("hello betty!");//删除最后一个字符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;}
2.5.4. 字符串的交换
最后我们来介绍一个字符串的交换函数swap
,这个swap
函数与算法库中的swap
函数并不相同,算法库中的swap
函数将string
中每个具体值都交换。而string
中的swap
函数实现的是指针交换,效率明显高的多。
void TestSwap(){string s("hello betty!");string p("hello world!");cout << s << endl;cout << p << endl;s.swap(p);cout << s << endl;cout << p << endl;}
2.6. 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
函数,这两个函数可以结合使用
void Test15(){string s("hello betty!");//在字符串s中寻找bsize_t pos = s.find('b');//从下标6开始截取长度为6的字符串string str = s.substr(pos, 6);cout << str << endl;}
find_first_of
,find_last_of
,find_first_not_of
与find_last_not_of
的用法非常类似,我们就只以其中一种举例:
void Test16(){string str("Please, replace the vowels in this sentence by asterisks.");//从str字符串中任意匹配aeiousize_t found = str.find_first_of("aeiou");//找不到返回nposwhile (found != string::npos){str[found] = '*';//从下一个位置找found = str.find_first_of("aeiou", found + 1);}cout << str << endl;}
这里是find
与find_first_of
详细的接口,大家可以根据实际需求参考使用
最后我们来介绍以下getline
,它与流插入都是读取字符串,但是区别就是getline
遇见空格不会停止,而流插入会。这区别和C语言中scanf
与gets
类似