一、string类的由来
在C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列
的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户
自己管理,稍不留神可能还会越界访问。
面向过程编程OPP:Procedure Oriented Programming,是一种以事物为中心的编程思想。主要关注“怎么做”,即完成任务的具体细节。
面向对象编程OOP:Object Oriented Programming,是一种以对象为基础的编程思想。主要关注“谁来做”,即完成任务的对象。
于是在C++设计过程中,就添加了string类,用来专门管理字符串。
string这个类是被typedef出来的,basic_string是一个类模板,string内部原理可以理解为是一个动态开辟的顺序表,每个元素是一个char类型的字符。我们知道一说到字符串一定牵扯到编码问题,比如我们汉字的编码(unicode)和英文字母(ascii)的编码方式可能就会不同,常见的编码方式有ASCII,UTF-8,UTF-16,UTF-32等等,string是用模板实例化出来的一个类,它的编码方式就是常见的UTF-8。UTF-8编码的一个主要优点是它向后兼容ASCII,即任何ASCII字符在UTF-8中都使用相同的单字节表示。所以,它的成员变量都是char类型。有人会问为什么不直接搞个ASCII编码的字符串类出来,那就忽略了一个问题,汉字字符串怎么办?
u16string也是通过basic_string模板实例化出来的一个类,它的编码方式是UTF-16。
u32string同样是通过basic_string模板实例化出来的一个类,它的编码方式是UTF-32。
这里我们主要讲string这个类。
二、string类的基本使用
在使用string类时,必须包含#include<string>这个头文件以及using namespace std;如果不加using namespace std;创建对象时必须指明命名空间(std::string)。本篇主要讲如何使用,深层的东西不过多涉及。
1、构造函数
C++98版本下有7个构造函数,我们这里只说C++98,不谈论C++11。
接下来我们来看看它们是怎么使用的:
int main()
{
string s1; //(1)默认构造函数,空串
string s2("hello world"); //(4)传参构造
string s3(s2); //(2)拷贝构造
cout << s1 << endl; //重载了流插入,能够打印输出string类型的对象
cout << s2 << endl;
cout << s3 << endl;
//cin >> s1; //也重载了流提取,能够向string类型的对象中输入值
//cout << s1 << endl;
string s4(s2, 6, 5);//(3)s2中,下标为6的字符向后拷贝5个字符初始化给s2
//假设s2下标为6的字符后的字符不够5个,则拷贝到结尾即可
cout << s4 << endl;
string s5(s2, 6);//(3)
//我们可以看到库中第三个参数有个缺省值npos(size_t类型)
//它是类中静态成员变量,值为-1,-1在内存中存储就是32个1,
//因为npos是size_t类型也就是无符号整形,即npos就是整数的最大值
//它表示的意思就是从某个下标位置开始拷贝到结尾
cout << s5 << endl;
string s6("hello world",5);//(5)拷贝第一个参数字符串的前5个字符初始化给s6
cout << s6 << endl;
string s7(3,'x'); //(6)用3个'x'字符初始化给s7
cout << s7 << endl;
return 0;
}
运行结果:
2、析构函数
析构函数我们不需要使用,因为编译器会自动帮我们调用来释放空间。构造函数需要我们写是因为初始化的形式是多样的。
3、赋值重载
int main()
{
string s1("hello world");
string s2("xxx");
cout << s1 << endl;
cout << s2 << endl;
s1 = s2; //(1)对象参数类型重载
cout << s1 << endl;
cout << s2 << endl;
s1 = "hah"; //(2)字符串参数类型重载
cout << s1 << endl;
cout << s2 << endl;
s2 = 'q'; //(3)字符参数类型重载
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
4、重载[]
string可以像其他内置类型一样直接用下标引用操作符[]来访问内部元素。像下面这样:
int main()
{
string s1("hello world");
cout << s1[0] << endl; //h
cout << s1[1] << endl; //e
return 0;
}
我们知道自定义类型不能直接通过下标引用操作符来访问内部元素,而string的底层其实是在类内对下标引用操作符[]进行了重载。我们可以简单想象一下它的实现:
class string
{
public:
char& operator[](int i)
{
assert(i <= _size);
return _str[i];
}
private:
char* _str; //指向空间的起始位置
int _size; //记录元素个数
int _capacity; //由于要扩容,所以这里需要记录容量大小
};
能用引用返回吗?为什么要引用返回呢?
开辟空间是在堆上开辟的,调用[]结束后,空间还在,所以能用引用返回。至于为什么要用引用返回,第一,减少一次拷贝构造;第二,也是最重要的一点,可以修改变量的值。这也和下标引用操作符的功能进行了重合。
int main()
{
string s1("hello world");
cout << s1[0] << endl; //h
cout << s1[1] << endl; //e
s1[0] = 'x'; //支持修改,也印证了我们的想象
s1[1] = 'x';
cout << s1[0] << endl; //x
cout << s1[1] << endl; //x
return 0;
}
内置类型越界访问数组程序不会崩溃,也不会报错。
int main()
{
int a[3] = { 1,2,3 };
cout << a[5] << endl; //越界访问
return 0;
}
运行结果:
说明了越界访问,编译器也不管,但我们在类中重载下标引用操作符时,如果越界访问,直接报错,这样就会更好。所以我们加了一句"assert(i < _size);",可以避免发生越界情况。
我们可以来验证一下我们的猜想是否正确:
由此可见,string底层就是有这一机制的。
在此,介绍几种遍历成员的方式:
int main()
{
string s("hello world");
//方式1
for (size_t i = 0;i < s.size();i++)
{
cout << s[i] << " ";
}
cout << endl;
//方式2
//iterator是STL六大组件之一的迭代器
//使用迭代器必须指明在哪个容器的类域,每个容器都有自己的迭代器,它们的名字相同,用法相同但内部结构可能"天差地别",用法相同说明了在其他容器中也可以用这种方式来遍历成员
//用迭代器定义出来的对象,功能上像指针,可能是指针也可能不是指针,这里暂且理解为指针,也可以理解为像指针的东西
//begin()是返回这段空间开始位置的迭代器,end()是返回最后一个有效元素的下一个位置的迭代器
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " "; //这里可能就有人说了,不是指针怎么解引用,不是指针可能会对*进行重载,这里的*it如果改变就会修改s中字符的值
it++; //这里同样也是,若不是指针,就会对++进行重载
}
cout << endl;
//方式3(C++11)
//范围for:从s这个容器中自动取值给e,直到取完为止
//auto自动推导类型,这里的auto也可以写成char,但一般都写auto
//自动赋值,自动迭代,自动判断结束
//它的底层其实是迭代器,*it的值赋给e,支持迭代器就支持范围for
for (auto e : s) //写起来更简单
{
cout << e << " "; //这里的e只是s中每个字符的拷贝,修改e的值不影响s中字符的值,若想修改在auto后面加上引用&
}
cout << endl;
return 0;
}
这3种方式在性能上没有区别,都是遍历,只是写法上不同。
(1)、auto(C++11语法)
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,会自动释放,后来局部变量都能自动释放,所以auto在这里就不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
如果一个对象的类型太长,我们就可以用auto来简化代码。比如:
map<string, string> dict;
map<string, string>::iterator mit = dict.begin();
auto mit = dict.begin();
虽然auto可以简化代码,但在一定程度上"牺牲了"可读性。
int main()
{
int a = 10;
auto b = a;
cout << typeid(b).name() << endl; //打印int,typeid可以查看对象类型
return 0;
}
auto不能去定义数组。auto不能做参数但可以做返回值,auto做返回值建议谨慎使用。auto可以同时定义两个对象,但对象的类型必须一致,否则会报错。
auto后跟*,代表是指针,必须给地址,否则会报错。
(2)、范围for(C++11语法)
范围for主要用于容器。
for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
范围for可以作用到数组和容器对象上进行遍历,范围for的底层很简单,容器遍历实际就是替换为迭代器。
范围for用来遍历数组也是很方便的:
int main()
{
int arr[] = { 1,2,3,4,5 };
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
return 0;
}
(3)、迭代器
int main()
{
string s1("hello world");
//1、正向迭代器
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
//2、反向迭代器
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
rit++; //逆向走,++被重载了
}
cout << endl;
const string s2("day day up");
//3、const迭代器,只能读,不能写(修改指针指向的内容)
//cbegin和cend专门用于const迭代器,它们的功能和begin/end一样
//这里用begin/end来替代cbegin/cend也可以
string::const_iterator cit = s2.cbegin();
while (cit != s2.cend())
{
cout << *cit << " ";
cit++;
}
cout << endl;
//4、const反向迭代器,只能读,不能写(修改指针指向的内容)
//crbegin和crend专门用于const反向迭代器,它们的功能和rbegin/rend一样
//这里用rbegin/rend来替代crbegin/crend也可以
//string::const_reverse_iterator crit = s2.crbegin();
auto crit = s2.crbegin();
while (crit != s2.crend())
{
cout << *crit << " ";
crit++;
}
cout << endl;
return 0;
}
begin指向第一个有效元素,end指向最后一个有效元素的后一个位置。
rbegin指向最后一个有效元素,rend指向第一个有效元素的前一个位置。
5、成员函数
string类中成员函数有许多,在这里只写一部分常用到的,对于所写的每个成员函数我会写一些用法代码帮助大家理解,但有些成员函数有多个重载,我不会把每个重载的用法都写一遍,只挑选一些来写,希望大家理解。
(1)、size() / length()
这两个成员函数的功能都是返回字符串的长度,但不包括'\0';
(2)、max_size()
它的功能是返回最大字符串的长度(这里是整形的最大值)。
(3)、capacity()
它的功能是返回申请容量大小。这个大小不包括'\0',假设capacity的初始值是15字节,字符串中有15个字符,它是不会扩容的,当字符串中有16个字符它才扩容。也就是说实际的空间是比容量多一个字节的,这一个字节用来存放'\0'。
int main()
{
string s1("a");
cout << s1.capacity() << endl; //容量为15字节
s1 = "aaaaaaaaaaaaaaa"; //15个字符
cout << s1.capacity() << endl; //容量为15字节
s1 = "aaaaaaaaaaaaaaaa"; //16个字符
cout << s1.capacity() << endl; //容量为31字节
}
运行结果:
我们可以用一个例子来测试容量的变化:
void TestPushBack()
{
string s;
size_t sz = s.capacity();
cout << "capacity of start:" << sz << endl;//扩容前容量
cout << "making s grow:" << endl;
for (int i = 0;i < 100;++i) //循环控制插入100个字符
{
s.push_back('c'); //尾插一个字符'c'
if (sz != s.capacity())
{
sz = s.capacity(); //扩容后新的容量给sz
cout << "capacity changed:" << sz << endl;
}
}
}
int main()
{
TestPushBack();
return 0;
}
运行结果:
我们知道容量是只包含有效数据不包含'\0',但实际空间要比capacity的值多1用来存放'\0',我们在运行结果的基础上每个值都加1才是实际的空间。
加1后我们发现第一次空间是扩二倍,接下来差不多都是1.5倍左右扩容。
这里的原因是什么呢?
VS2019在这里自己做了单独的处理,当size小于16时,它将元素存放在一个buff数组中而不是直接存放在堆上。
//VS下多了一个类似_buff的一个数组
class string
{
private:
char _buff[16];
char* _str;
int _size;
int _capacity;
};
如果_size小于16,就会存放在_buff中,在没有数据时先给你开16字节的空间,capacity大小是15(加上'\0'就是16);大于16就全部存放在_str指向的堆中,同时清空_buff,但_buff这个空间还在,_size大于16首次扩容就会扩到32,这是单独处理的。后续扩容就是1.5倍左右。
我们可以看一下一个string类对象的大小是多少:
int main()
{
string s;
cout << s.capacity() << endl;
cout << sizeof(s) << endl;
return 0;
}
运行结果:
这里的15就是没有数据时,capacity的大小,验证了我们上面说的在没有数据时先给你开16字节的空间,capacity大小是15(加上'\0'就是16)。
而28就是_buff占16字节,char*占4字节,_size占4字节,_capacity占4字节,一共占28字节。
同样一段代码在Linux环境下的结果是不同的:
我们可以看到,在Linux下它是严格的二倍扩容,它没有_buff这一说,因为开始时capacity的值为0。
为什么在两种不同的环境下会有差异呢?
C++标准规定,string类必须实现什么功能,但怎么实现的是靠编译器来决定的。比如扩容,C++标准规定string类要实现自动扩容,但怎么扩容,扩多大是每个编译器自己实现的,这里VS和Linux下的扩容机制就不大相同。
(4)、reserve()
它的功能是改变容量的,也就是改变capacity的大小,它可以避免频繁扩容。
假设参数是n(就是改变后的容量大小),分3种情况:
1、n < size
首先会不会缩容,这个问题是根据编译器的,有些编译器会缩容,有些编译器不会缩容。如果缩容,则最多缩到size,不能把我的size也给缩没了。
在VS2019下是不缩容的,即容量保持不变,当然size也不会改变,其中的元素也不会改变。
在Linux下可能缩容。
2、size < n < capacity
有些编译器会缩容,有些编译器不会缩容。
在VS2019下是不缩容的,即容量保持不变,当然size也不会改变,其中的元素也不会改变。
在Linux下可能缩容。
3、n > capacity
会扩容,至少扩到n,也可能更多,这是不确定的。在vs下通常会扩的更多一些,而在Linux下通常就扩到n。
int main()
{
string s1("aaaaa");
cout << s1.size() << endl; //元素个数为5个
cout << s1.capacity() << endl; //容量为15字节
//1、n < size
s1.reserve(3);
cout << s1.size() << endl; //元素个数为5个
cout << s1.capacity() << endl; //容量为15字节
//2、size < n < capacity
s1.reserve(10);
cout << s1.size() << endl; //元素个数为5个
cout << s1.capacity() << endl; //容量为15字节
//3、n > capacity
s1.reserve(50);
cout << s1.size() << endl; //元素个数为5个
cout << s1.capacity() << endl; //会扩容,容量为63字节
}
(5)、resize()
功能就是将元素个数设置为n。分3种情况:
int main()
{
string s1("hah");
cout << s1.size() << endl; //3
cout << s1.capacity() << endl; //15
//1.n < size
s1.resize(1);
cout << s1.size() << endl; //1
cout << s1.capacity() << endl; //15
//2.size < n <capacity
s1.resize(10);
cout << s1.size() << endl; //10
cout << s1.capacity() << endl; //15
//3. n > capacity
s1.resize(30);
cout << s1.size() << endl; //30
cout << s1.capacity() << endl; //31
return 0;
}
在VS2019下运行的结果:
总结一句话,通过resize,设置n为多少,size就跟着变为多少,如果n > capacity,则capacity跟size一起变化,否则只有size变为n,capacity通常不变(取决于编译器)。
如果n小于size,则size个数就会变成n,size中多余的数据被删除。如果n大于size,则size扩大到n,新增加的数据初始化为'\0'。resize也可以传第二个参数,指定一个字符,新增加的数据初始化为你传过去的字符。
(6)、clear()
功能是清除数据,但通常不清除容量。
int main()
{
string s("hah");
cout << s.size() << endl;
cout << s.capacity() << endl;
s.clear();
cout << s.size() << endl;
cout << s.capacity() << endl;
return 0;
}
运行结果:
(7)、empty()
功能是判断字符个数是否为空。
int main()
{
string s1("hah");
if (s1.empty())
cout << "s1 -> null" << endl;
else
cout << "s1 -> not null" << endl;
string s2;
if (s2.empty())
cout << "s2 -> null" << endl;
else
cout << "s2 -> not null" << endl;
return 0;
}
运行结果:
(8)、shrink_to_fit()
功能是缩容,将capacity减小到适应它的size。这不是强制的。
int main()
{
string s1("hello");
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.shrink_to_fit();
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
运行结果:
这里并没有缩容。
(9)、at()
at的功能和重载[]几乎一样,只不过重载[]如果越界会断言报错,at越界会抛出out_of_range的异常。
int main()
{
string s("hah");
cout << s.at(0) << endl;
cout << s.at(1) << endl;
cout << s.at(2) << endl;
return 0;
}
运行结果:
(10)、front()
功能是返回第一个字符。
int main()
{
string s1("hello");
cout << s1.front() << endl; //h
return 0;
}
(11)、back()
功能是返回最后一个有效字符。
int main()
{
string s1("hello");
cout << s1.back() << endl; //o
return 0;
}
(12)、push_back()
功能是在原有字符串的基础上追加一个字符。
int main()
{
string s("hello");
cout << s << endl;
s.push_back(' ');
s.push_back('w');
s.push_back('o');
cout << s << endl;
return 0;
}
运行结果:
(13)、pop_back()
功能是删除字符串的末尾的一个有效字符。
int main()
{
string s("hello");
cout << s << endl;
s.pop_back();
cout << s << endl;
return 0;
}
运行结果:
(14)、append()
它的功能是在原有字符串的基础上追加字符串,但不能追加字符。
这里以第三个重载函数为例:
int main()
{
string s("hello");
cout << s << endl;
s.append(" world");
cout << s << endl;
return 0;
}
运行结果:
(15)、重载+=
它的功能也是在原有字符串的基础上追加字符串,也能追加字符。
这里以第二个重载函数为例:
int main()
{
string s("hello");
cout << s << endl;
s += " world";
cout << s << endl;
return 0;
}
运行结果:
(16)、assign()
它的功能是给一个字符串对象赋值,若之前字符串有内容则就覆盖掉原来的内容,同时size也会改变。
这里以第三个重载函数为例:
int main()
{
string s1 = "hah";
cout << s1.size() << endl; //size为3
cout << s1.capacity() << endl; //capacity为15
s1.assign("x");
cout << s1.size() << endl; //这里让size小于原来的size,size=1
cout << s1.capacity() << endl; //capacity保持不变,capacity=15
string s2 = "xix";
cout << s2.size() << endl; //size为3
cout << s2.capacity() << endl; //capacity为15
s2.assign("xxxxxxxxxxxxxxxxxxxxxxxxxx");
cout << s2.size() << endl; //这里让size大于原来的capacity,size=26
cout << s2.capacity() << endl;//这里的capacity就会扩容到31
string s3 = "pip";
cout << s3.size() << endl; //size为3
cout << s3.capacity() << endl; //capacity为15
s3.assign("xxxxx");
cout << s3.size() << endl; //这里让size大于原来的size,小于原来的capacity,size=5
cout << s3.capacity() << endl;//capacity保持不变,capacity=15
return 0;
}
运行结果:
(17)、insert()
它的功能是在指定位置前(这个位置必须有效,否则运行时会崩溃)插入字符或字符串。
这里以第三个重载函数为例:
int main()
{
string s("world");
cout << s << endl;
s.insert(0, "hello "); //在字符串中下标为0的位置插入"hello "
cout << s << endl;
return 0;
}
运行结果:
(18)、erase()
它的功能是删除指定位置的字符串或字符。
这里以第一个重载函数为例:
int main()
{
string s("hello world");
cout << s << endl;
s.erase(6, 1); //在下标为6的位置删除1个字符,如果不写第二个参数,默认值是npos,它是int类型最大值,可以理解为从第一个参数位置开始后面全删
cout << s << endl;
return 0;
}
运行结果:
(19)、replace()
它的作用是将字符串中某一段替换成另外一段。
这里以第三个重载函数为例:
int main()
{
string s("hello world");
cout << s << endl;
s.replace(5, 1, "%%"); //从下标为5的位置开始往后1个字符替换成"%%"
cout << s << endl;
return 0;
}
运行结果:
用2个字符替换1个字符也是可以的。多替换少也是可以的。
replace()尽量不要频繁使用,因为它底层牵扯到扩容问题,有时需要挪动大量数据。
(20)、find()
它的功能是从某个位置开始查找某个字符或字符串,若找到则返回第一个被找到的位置的起始位置下标,否则返回npos。npos是string类中的静态成员变量。
这里以第四个重载函数为例:
int main()
{
string s("hello wor ld");
size_t pos1 = s.find(' '); //从下标为0的位置开始找' ',若有多个' ',则返回第一个位置的下标
cout << pos1 << endl;
size_t pos2 = s.find(' ', pos1 + 1); //从下标为pos1 + 1的位置开始找' ',若有多个' ',则返回第一个位置的下标
cout << pos2 << endl;
return 0;
}
运行结果:
我们可以结合replace()来实现一个小功能:将一个字符串中所有空格换成'%'
int main()
{
string s("h el lo wo r ld");
cout << "replace before:" << s << endl;
size_t pos = s.find(' ');
while (pos != string::npos)
{
s.replace(pos, 1, "%");
pos = s.find(' ', pos + 1);
}
cout << "replace after: " << s << endl;
return 0;
}
运行结果:
还用另外一种实现方法:
int main()
{
string s("h el lo wo r ld");
cout << s << endl;
string tmp;
for (auto e : s)
{
if (e == ' ')
tmp += '%';
else
tmp += e;
}
cout << tmp << endl;
return 0;
}
运行结果:
(21)、swap()
它的功能是交换两个string类型对象的成员变量的值。
int main()
{
string s1("hah");
string s2("pip");
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
s1.swap(s2);
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
return 0;
}
运行结果:
通过调试时的监视窗口,也可以看出它们的交换情况:
交换前:
交换后:
不难看出只有buff没变,其余都交换了。
(22)、c_str()
它的功能是返回底层字符串的指针。它的出现就是兼容C语言的,比如一些C语言的函数参数是char*类型的,不能直接传string类型,必须传char*,我们就可以调用c_str()。转换为char*后末尾会放一个‘\0’。
int main()
{
string file;
cin >> file;
FILE* fout = fopen(file.c_str(), "r"); //fopen第一个参数是const char*,所以这里必须要转换一下
char ch = fgetc(fout);
while (ch != EOF)
{
cout << ch;
ch = fgetc(fout);
}
fclose(fout);
return 0;
}
(23)、substr()
它的功能是从某个位置开始,取长度为n的字串,构造一个string类型的对象进行返回。
int main()
{
string s1("hello world");
string s2 = s1.substr(0, 5); //拷贝构造,取下标为0的位置开始向后5个字符给s2
cout << s2 << endl;
return 0;
}
运行结果:
(24)、rfind()
它的功能是从某个位置开始从后往前找。找到第一个符合条件的就返回对应的下标。
这里以第四个重载函数为例:
int main()
{
//获取文件名的后缀
string s("Test.txt");
size_t pos = s.rfind('.');//从后向前找
string tmp = s.substr(pos);
cout << tmp << endl;
return 0;
}
运行结果:
这时候就有人说了,将rfind换成find也行啊,rfind感觉没什么用,那么请看下面一种情况:
int main()
{
//目的:打印.zip
string s("Test.txt.zip");
size_t pos = s.rfind('.');//从后向前找,如果这里是find,就不行了
string tmp = s.substr(pos);
cout << tmp << endl;
return 0;
}
运行结果:
(25)、 find_first_of()
它的功能是从指定位置开始查找任意个字符(查找的范围是参数中的所有字符),找到返回对应下标。
int main()
{
string str("Please, replace the vowels in this sentence by asterisks.");
size_t found = str.find_first_of("aeiou"); //找出串中的'a','e','i','o','u'任意首个出现的字符的下标
while (found != string::npos)
{
str[found] = '*';
found = str.find_first_of("aeiou", found + 1);
}
std::cout << str << '\n';
return 0;
}
运行结果:
这段代码的功能是将串中的所有'a','e','i','o','u'替换成'*'。
(26)、find_last_of()
它的功能与find_first_of()一样,只不过是从后往前找。这里就不赘述了。
(27)、find_first_not_of()
它的功能是从指定位置开始查找任意个字符(查找的范围是除了参数之外的所有字符),找到返回对应下标。
int main()
{
string str("Please, replace the vowels in this sentence by asterisks.");
size_t found = str.find_first_not_of("aeiou"); //找出串中不是'a','e','i','o','u'任意首个出现的字符的下标
while (found != string::npos)
{
str[found] = '*';
found = str.find_first_not_of("aeiou", found + 1);
}
std::cout << str << '\n';
return 0;
}
运行结果:
这段代码的功能是将串中的所有不是'a','e','i','o','u'的字符替换成'*'。
(28)、find_last_not_of()
它的功能与find_first_not_of()一样,只不过是从后往前找。这里就不赘述了。
6、非成员函数
(1)重载+
这里以第二个重载函数为例:
int main()
{
string s1("hello");
string s2 = s1 + " world"; //(2)
string s3 = "world " + s1; //(2)
cout << s2 << endl;
cout << s3 << endl;
return 0;
}
运行结果:
(2)重载关系运算符
支持两个字符串比较大小,比较规则是根据ASCII码值的大小。这里就不举例了。
(3)重载流插入(<<)和流提取(>>)
重载后就支持输入和输出string类型的字符串了。
(4)getline()
当cin一个字符串对象时,它是取不到空格的:
int main()
{
string s;
cin >> s;
cout << s << endl;
return 0;
}
当我们输入AAA B时,它只会取到AAA:
因为cin默认在遇到换行或空格时停止在缓冲区中继续读数据。
getline就可以解决这个问题,cin默认在遇到换行时停止在缓冲区中继续读数据。
int main()
{
string s;
getline(cin,s);
cout << s << endl;
return 0;
}
当我们输入AAA B时,它会取到AAA B:
它有两个重载函数:
它默认是以换行为终止符,我们也可以自己设置终止符,对应的是第一个构造函数。
三、总结
本篇到这里就结束了,主要讲了string类的基本使用,希望对大家有帮助,祝大家天天开心!