文章目录
- 前言
- 一、介绍
- 二、string类的常用接口说明
- 2.1 常见的构造接口
- 2.2 与容量有关的接口
- 2.3 与对象访问及遍历有关的操作
- 2.4 与对象修改有关的操作
- 2.5 与查找有关的接口
- 2.6 string类的非成员函数
- 2.7 与类型转换有关的接口
前言
在C语言中,字符串是以\0
结尾的一些字符的集合,为了方便操作,C标准库中提供了一些str
系列的库函数。但是这些库函数与字符串是分离开的,不太符合面向对象(OOP)的思想,而且底层空间需要用户自己去管理,稍不留神可能就会出现越界访问。为了解决上面的这些问题,C++中引入了string
类,它给我们带来了极大的便利。
一、介绍
-
string
是表示字符串的字符串类。 -
该类的接口与常规容器(vector、list等)的接口基本相同,再添加了一些专门用来操作
string
的常规操作。 -
string
在底层实际是:用basic_string
模板类实例化出来的一个类,typedef basic_string<char> string;
。 -
不能操作多字节或者变长字符的序列。
小Tips:在使用string
类时,需要包含头文件#include <string>
,以及使用using namespace std
展开命名空间。
二、string类的常用接口说明
2.1 常见的构造接口
📖string()
该类的默认构造函数,用于构造空的string
类对象,即空字符串。
int main()
{
string s1;
cout << s1 << endl;
return 0;
}
小Tips:string
类对象支持流插入和流提取,下文将进行介绍,这里大家直接使用即可。
📖string(const char* s)
用C-string
来构造string
类对象,即用一个C的字符串(或字符数组)来构造一个string
类的对象。
int main()
{
string s1("Hello C++!");
cout << s1 << endl;
return 0;
}
📖string(size_t n, char c)
用n个字符c
来构建一个string
类对象。
int main()
{
string s1(5, 'x');
cout << s1 << endl;
return 0;
}
📖string(const string& s)
string
类的拷贝构造,用于构建一个和已存在的string
类对象s
一模一样的对象。
int main()
{
string s1(5, 'x');
string s2(s1);
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
📖string (const string& str, size_t pos, size_t len = npos)
复制str
中从字符位置pos
开始并跨越len
字符的部分(如果 str 太短或 len 是字符串npos
,则直到 str 的末尾)。简单来说就是使用一个已存在的string
类对象的一部分来创建一个新的string
类对象。
小Tips:nops
是string
类里面的一个静态成员变量,它是size_t
类型,初始化为-1
,即表示整型的最大值。此值如果在string
的成员函数中作为形参len
的缺省值,表示到字符串结束。如果作为string
类中成员函数的返回值,一般表示没有匹配项。
int main()
{
string s1("Hello C++!");
string s2(s1, 0, 5);//用s1的部分来初始化创建s2
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
注意:对一个string
类对象来说,它的第一个有效字符的下标是0。
📖string (const char* s, size_t n)
用s
所指向字符串(或字符数组)的前n
个来初始化创建一个string
类对象。
int main()
{
char str[] = "Hello C++!";
string s1(str, 5);//用字符数组str的前5个字符来构建一个string类对象
cout << str << endl;
cout << s1 << endl;
return 0;
}
2.2 与容量有关的接口
📖size()
返回字符串的有效字符长度。
int main()
{
string s1("Hello C++!");
string s2("Good morning!");
cout << "s1的size:" << s1.size() << endl;
cout << "s2的size:" << s2.size() << endl;
return 0;
}
📖length()
返回字符串的有效字符长度。
int main()
{
string s1("Hello C++!");
string s2("Good morning!");
cout << "s1的length:" << s1.length() << endl;
cout << "s2的length:" << s2.length() << endl;
return 0;
}
小Tips:从上面的打印结果可以看出,size()
和length()
接口的功能一模一样,甚至底层实现原理也完全相同,都是返回字符串的有效字符个数,最初只有length()
接口,引入size()
接口的原因是为了与其他容器的接口保持以致,一般情况下基本都使用size()
接口。这也从侧面说明string
诞生的时间比STL
要早。
📖capacity()
返回一个string
对象中空间的大小。
int main()
{
string s1("Hello C++!");
string s2("Good morning chunren!");
cout << "s1的capacity:" << s1.capacity() << endl;
cout << "s2的capacity:" << s2.capacity() << endl;
return 0;
}
小Tips:同一个string
对象,在不同平台下的capacity()
(空间容量)可能不同,因为string
在底层就是一个存储字符的动态顺序表,空间不够了要进行扩容,而不同平台底层的扩容机制可能有所不同,这就导致了最终capacity()
的结果不同。将上面的代码放到Linux环境下使用g++编译器再来试试:
小Tips:capacity()
返回一个string
对象中空间的大小,这个空间指的是可以存储有效字符的空间,底层实际上的空间会多一个,因为还要存储\0
。
📖VS下的扩容机制
int main()
{
string s1("Hello!");
size_t old = s1.capacity();
cout << s1.capacity() << endl;
for (size_t i = 0; i < 100; i++)
{
s1 += 'v';
if (old != s1.capacity())
{
cout << "扩容:" << s1.capacity() << endl;
old = s1.capacity();
}
}
return 0;
}
VS下一上来会有15个空间用来存储数据(本质上是开16个空间,因为还要存\0
),第一次扩容是2倍,后面都是以1.5倍的大小去扩容。
📖Linux下的扩容机制
int main()
{
string s1("Hello!");
size_t old = s1.capacity();
cout << s1.capacity() << endl;
for (size_t i = 0; i < 100; i++)
{
s1 += 'v';
if (old != s1.capacity())
{
cout << "扩容:" << s1.capacity() << endl;
old = s1.capacity();
}
}
return 0;
}
将上面的代码放到Linux环境下使用g++编译器再来试试:
可见在Linux下,最初对象需要多少空间就开多少,后面一次按照2倍的大小进行扩容。
📖empty()
检测字符串是否为空串,是返回true
,否则返回false
。
int main()
{
string s1;//
if (s1.empty())
{
cout << "s1是一个空串" << endl;
}
return 0;
}
📖clear()
清空有效字符。
int main()
{
string s1("Hello C++!");
cout << "清空之前的size:" << s1.size() << endl;
cout << "清空之前的capacity:" << s1.capacity() << endl;
s1.clear();//清空
cout << "清空之后的size:" << s1.size() << endl;
cout << "清空之后的capacity:" << s1.capacity() << endl;
return 0;
}
小Tips:从打印结果可以看出,clear()
清空操作不会影响capacity()
容量,也就是说string
对象并不会主动的去缩容。
📖reserve (size_t n = 0)
为字符串预留空间。直接一次申请n
个空间,可以用来存储n
个有效字符,避免了每次都要去扩容。大部分的扩容都是异地扩容,扩容次数过多会影响效率。
int main()
{
string s1;
s1.reserve(100);//知道要尾插100个字符就先直接申请100个,避免后面再去扩容
size_t old = s1.capacity();
cout << s1.capacity() << endl;
for (size_t i = 0; i < 100; i++)
{
s1 += 'v';
if (old != s1.capacity())
{
cout << "扩容:" << s1.capacity() << endl;
old = s1.capacity();
}
}
s1.reserve(20);
cout << "第二次执行reserve(20):" << s1.capacity() << endl;
return 0;
}
小Tips:实际申请到的有效空间可能比我们需要的多,但是一定不可能比我们需要的少,如s1.reserve(100)
去申请100个有效空间,但实际上申请了111个有效空间。当n
小于当前对象的容量时,一般的编译器都不会执行缩容。(视具体情况而定)
📖resize
将字符串大小调整为n
个长度的大小,当n
小于当前字符串的长度size()
,会保留前n
个字符,将第n
个字符后面的所以字符删除;当n
大于当前字符串的长度size()
,先会进行扩容,以满足存储n
个有效字符的需求,如果指定了字符c
,会将新元素初始化为c
的副本,否则全部初始化为空字符,即\0
。
int main()
{
string s1("Hello C++!");
cout << "最初的s1.size():" << s1.size() << endl;
cout << "最初的s1.capacity():" << s1.capacity() << endl;
string s2 = s1;//将s1拷贝一份
s1.reserve(100);
cout << "reserve(100)后的s1.size():" << s1.size() << endl;
cout << "reserve(100)后的s1.capacity():" << s1.capacity() << endl;
s2.resize(100);
cout << "resize(100)后的s1.size():" << s2.size() << endl;
cout << "resize(100)后的s1.capacity():" << s2.capacity() << endl;
return 0;
}
小Tips:reserve
和resize
的区别可总结为:前者只会影响容量,负责申请空间,不会改变字符串的有效字符个数,即会改变capacity
,不会改变size
;后者在申请空间的基础上还会对空间进行初始化,这样就会对有效字符的个数产生影响,所以它即会改变capacity
也会改变size
📖shrink_to_fit()
将capacity
容量缩至合适,一般不会缩小到和size
一样大,可能会比size
大一点。
int main()
{
string s2("Hello C++!");
cout << "最初的s2.size():" << s2.size() << endl;
cout << "最初的s2.capacity():" << s2.capacity() << endl;
s2.reserve(100);//扩容
cout << "reserve(100)后的s2.size():" << s2.size() << endl;
cout << "reserve(100)后的s2.capacity():" << s2.capacity() << endl;
s2.shrink_to_fit();//缩容
cout << "缩容后的s2.size():" << s2.size() << endl;
cout << "缩容后的s2.capacity():" << s2.capacity() << endl;
return 0;
}
2.3 与对象访问及遍历有关的操作
📖operator[ ]
int main()
{
string s1("Hello C++!");//普通对象
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i];
}
cout << endl << "修改后:";
for (size_t i = 0; i < s1.size(); i++)
{
cout << ++s1[i];//因为返回值是引用所以可以用[]对其进行修改
}
cout << endl;
const string s2("Hello World!");
for (size_t i = 0; i < s2.size(); i++)
{
cout << s2[i];
}
cout << endl;
return 0;
}
小Tips:因为operator[]
的返回值是一个引用,所以可以通过[]
加下标的方式去访问和修改string
类对象,和内置类型的数组不同,这里的s1[i]
本质上是去调用函数,而内置类型的数组使用[]
本质是解引用。如果发生越界访问,程序会直接报错,at接口和operator[]
接口的功能类似,只不过at
接口在发生越界访问的时候会抛出异常。
📖迭代器
迭代器是一种抽象的设计概念,它提供一种方法,使之能够依序巡访某个容器所含的各种元素,而又无需暴露该容器的内部表述方式。迭代器是一种行为类似指针的对象,它是容器与算法的桥梁,算法需要去访问容器中的数据,但是容器的数据都是私有的,并且有多种容器,针对不同的容器,某一算法的具体实现可能不同,例如对链表逆置和对顺序表逆置,具体过程当然是不同的,但是迭代器的出现却将它们统一了起来,对于一个算法,无论是什么容器,只需要将它的迭代器区间传过来即可,算法只使用统一的逻辑。因此任何容器的迭代器类型都用iterator
来表示,它是一个类的内置类型,通过typedef
得到,关于迭代器更具体地内容我将在后续模拟实现的文章中为给大家分享,今天我们只需要知道如何使用即可。
📖begin、end
int main()
{
string s1("Hello C++!");//普通对象
string::iterator it = s1.begin();
while (it < s1.end())
{
cout << *it;//解引用迭代器
it++;//++迭代器,让迭代器向后走
}
cout << endl;
it = s1.begin();
while (it < s1.end())
{
++(*it);//通过迭代器去修改
it++;
}
it = s1.begin();
while (it < s1.end())
{
cout << *it;
it++;
}
cout << endl;
return 0;
}
从上面的代码可以看出,一个迭代器对象和一个指针类型的变量十分相似,都可以通过*
解引用,并且都可以++
,还可以解引用后去修改。但本质上对迭代器的这些操作都是通过运算符重载来实现的,具体实现我将在后续文章中为大家介绍。
小Tips:迭代器区间永远都是左闭右开,迭代器类型作为类的内置类型可以直接通过类名::iterator
访问,例如:string::iterator
就表示string
类里面的迭代器类型。普通迭代器可读可写,const
迭代器限制的是其指向的内容,只能读不能写,而const
迭代器本身可以修改。
📖rbegin、rend
int main()
{
string s1("Hello C++!");//普通对象
string::reverse_iterator it = s1.rbegin();
while (it < s1.rend())
{
cout << *it;//解引用迭代器
it++;//++迭代器,让迭代器向后走
}
cout << endl;
return 0;
}
小Tips:reverse_iterator
一般被叫做反向迭代器,因为它可以倒着去遍历。
📖范围for
int main()
{
string s1("Hello C++!");//普通对象
for (auto it : s1)
{
cout << it;
}
cout << endl;
for (auto& it : s1)//用引用就可以进行修改
{
it--;
}
for (auto it : s1)
{
cout << it;
}
cout << endl;
return 0;
}
小Tips:范围for就是基于迭代器实现的,在底层范围for会转化成正向迭代器。换言之,一个容器如果不支持迭代器,那它必定也不支持范围for。
2.4 与对象修改有关的操作
📖operator=
赋值运算符重载是string
类的一个默认成员函数,该函数有三个重载形式,如下图所示:
int main()
{
string s1("Hello C++!");
string s2("你好,C++!");
cout << s2 << endl;//原始的s2
s2 = s1;//string类对象
cout << s2 << endl;//第一次赋值后的s2
s2 = "春人.";
cout << s2 << endl;//第二次赋值后的s2
s2 = 'a';
cout << s2 << endl;//第三次赋值后的s2
return 0;
}
📖void push_back (char c)
将一个字符c
追加到string
类对象的末尾,它的长度增加1。
int main()
{
string s1("Hello C++!");
cout << "追加前:" << s1 << endl;
s1.push_back('s');
cout << "追加后:" << s1 << endl;
return 0;
}
📖append
append
是在源字符串的后面进行追加操作的成员函数,它有七种重载实现形式,如下图所示:
int main()
{
string s1("Hello C++!");
string s2("aaaa");
cout << "追加前:" << s2 << endl;
s2.append(s1);
cout << "追加一个string对象:" << s2 << endl;
s2 = "aaaa";
s2.append(s1, 6, 3);
cout << "追加一个string对象的一部分:" << s2 << endl;
s2 = "aaaa";
s2.append("你好");
cout << "追加一个C类型的字符串:" << s2 << endl;
s2 = "aaaa";
s2.append("Hello!", 2);
cout << "追加一个C类型字符串的前两个字符:" << s2 << endl;
s2 = "aaaa";
s2.append(5, 'b');
cout << "追加五个字符b:" << s2 << endl;
s2 = "aaaa";
s2.append(s1.begin()+2, s1.begin()+4);
cout << "追加一个迭代器区间:" << s2 << endl;
return 0;
}
📖operator+=
通过重载运算符+=
实现追加,该运算符重载有三种重载实现形式,如下图所示:
int main()
{
string s1("Hello C++!");
string s2("aaaa");
cout << "追加前:" << s2 << endl;
s2 += s1;
cout << "追加一个string类对象:" << s2 << endl;
s2 = "aaaa";
s2 += "bcde";
cout << "追加一个C类型的字符串:" << s2 << endl;
s2 = "aaaa";
s2 += 'o';
cout << "追加一个字符:" << s2 << endl;
return 0;
}
小Tips:除了上面介绍的一些常用的字符串修改接口外,还有一些不太常用的,例如:assign(内容替换)、insert(指定位置插入)、erase(删除)、replace(部分替换)、swap(交换两个字符串)。它们的使用方法都大同小异。
2.5 与查找有关的接口
📖c_str()
该接口的返回值类型是const char*
,即返回一个C格式的字符串,该接口起到桥梁作用。
int main()
{
string s2("Hello C++!");
const char* str = s2.c_str();
cout << str << endl;
return 0;
}
📖find
从字符串的pos
位置开始往后查找字符或字符串,返回其在当前字符串中的位置。
int main()
{
string s1("https://www.csdn.net/?spm=1011.2124.3001.4476");
size_t pos1 = s1.find("csdn");
cout << pos1 << endl;
size_t pos2 = s1.find("www.csdn.net", 7, 3);
cout << pos2 << endl;
return 0;
}
小Tips:一般在没有找到的情况下会返回npos
,即整型最大值。
📖rfind
在字符串pos
位置开始往前查找字符或字符串,返回其在当前字符串中的位置。
int main()
{
std::string str("The sixth sick sheik's sixth sheep's sick.");
std::string key("sixth");
std::size_t found = str.rfind(key);
if (found != std::string::npos)
str.replace(found, key.length(), "seventh");
std::cout << str << '\n';
return 0;
}
📖substr (size_t pos = 0, size_t len = npos)
在源字符串中,从pos
位置开始,截取n
个字符,以string
的形式返回。
int main()
{
string s1("https://www.csdn.net/?spm=1011.2124.3001.4476");
size_t pos1 = s1.find("csdn");
string s2 = s1.substr(pos1, 8);
cout << s2 << endl;
return 0;
}
小Tips:除了上面介绍的一些常用接口,还有一些不常用的,比如:find_first_of(在字符串中搜索与其参数中指定的任何字符匹配的第一个字符)、find_last_of(查找最后一个匹配的)、find_first_not_of(查找第一个不匹配的)、find_last_not_of(查找最后一个不匹配的)。
2.6 string类的非成员函数
有些运算符重载函数存在竞争左操作数的问题,所以它们写在string
类的外面,是类的非成员函数,主要有下面几种:
函数 | 功能说明 |
---|---|
operator+ | 尽量少用,因为是传值返回,会进行深拷贝导致效率降低 |
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
relational operators | 大小比较运算符重载 |
getline | 获取一行字符串 |
小Tips:operator>>
和getline
的区别在于,前者遇到空格' '
和换行\n
会截止,而后者默认只有遇到换行\n
才截止,因此当我们需要从键盘读取一个含有空格的字符串是,只能用getline
。
2.7 与类型转换有关的接口
📖string类型转成其他内置类型
📖to_string将内置类型转成string类型
🎁结语:
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!