
文章目录
1. 为什么学习 string 类
1.1 C 语言中的字符串
C 语言中,字符串是以 '\0' 结尾的一些字符的集合,为了操作方便,C 标准库中提供了一些 str 系列的库函数,比如 strlen、strcpy、strcat、strcmp 等等。
但是这些库函数和字符串是分离开的,不太符合面向对象的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
举个例子,你用 strcpy 拷贝字符串的时候,如果目标空间不够大,就会越界写入,编译器不会帮你检查,全靠程序员自己小心。
1.2 为什么需要 string 类
正是因为 C 语言字符串有这些问题,C++ 中就设计了 string 类。
-
string 类把字符串和操作字符串的方法封装在了一起,符合面向对象的思想。
-
string 类自动管理底层空间,不需要我们自己 new 和 delete,大大降低了内存泄漏和越界的风险。
-
在 OJ 中,有关字符串的题目基本以 string 类的形式出现,在常规工作中,为了简单、方便、快捷,基本都使用 string 类,很少有人去使用 C 库中的字符串操作函数。
2. string 类的使用
2.1 string 类的介绍
在使用 string 类时,必须包含:
#include <string>
头文件以及:
using namespace std;
string 类其实是 basic_string<char> 的 typedef,是标准库提供的一个管理字符数组的类。
2.2 补充语法:auto 和范围 for
在这里补充两个 C++11 的小语法,方便我们后面的学习。
2.2.1 auto 关键字
在早期 C/C++ 中 auto 的含义是:
使用 auto 修饰的变量,是具有自动存储器的局部变量(入函数创建,出函数销毁)。
后来这个不重要了。
C++11 中,标准委员会变废为宝赋予了 auto 全新的含义:
auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。
#include<iostream>
using namespace std;
int func1()
{
return 10;
}
int main()
{
int a = 10;
auto b = a; // 推导为int
auto c = 'a'; // 推导为char
auto d = func1(); // 推导为int
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
typeid(b).name(),typeid 运算符可以求出变量的类型。
auto 使用时的注意点:
1. 用 auto 声明指针和引用
用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别。
但是用 auto 声明引用类型时则必须加 &。
int x = 10;
auto y = &x; // 推导为int*
auto* z = &x; // 推导为int*,和上面没有区别
auto& m = x; // 推导为int&,引用必须加&
2. 同一行声明多个变量时必须类型一致
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错。
因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
错误代码:
auto cc = 3, dd = 4.0;
编译报错:
auto 必须始终推导为同一类型。
3. auto 不能作为函数参数
错误代码:
void func2(auto a)
{}
因为参数是接口,调用前必须确定类型,编译器无法推导。
4. auto 可以做函数返回值,但是建议谨慎使用
auto func3()
{
return 3;
}
返回值用 auto 的坏处是,你可能要不断往前看才能知道返回的类型是什么,对我们不太友好。
对编译器没有什么影响,因为编译器调用完函数以后顺带推导的,倒是没有什么消耗。
5. auto 不能直接用来声明数组
错误代码:
auto array[] = { 4, 5, 6 };
因为 auto 只能推出类型,推不出类型加数量,而初始化列表也没有推导数量的作用。
6. auto 的用武之地
当类型名特别长的时候,auto 就非常好用了。
#include<iostream>
#include<string>
#include<map>
using namespace std;
int main()
{
std::map<std::string, std::string> dict =
{
{ "apple", "苹果" },
{ "orange", "橙子" },
{ "pear", "梨" }
};
// 不用auto,类型名超长
// std::map<std::string, std::string>::iterator it = dict.begin();
// 用auto,简洁明了
auto it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
return 0;
}
当然凡事都有弊端的,这里也是牺牲了一部分可读性的。
2.2.2 范围 for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11 中引入了基于范围的 for 循环。
for 循环后的括号由冒号 : 分为两部分:
第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
自动取数据,自动迭代,自动判断结束。
#include<iostream>
#include<string>
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
// C++98的遍历
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] *= 2;
}
// C++11的范围for遍历
for (auto& e : array)
e *= 2;
for (auto e : array)
cout << e << " ";
cout << endl;
string str("hello world");
for (auto ch : str)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
范围 for 的注意点:
- 范围 for 的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
- 迭代器可以直接修改值,但是范围 for 不是这样的,加了引用才可以修改值,否则只是拷贝。
string s2("hello");
// 不加引用,ch是拷贝,修改不影响原字符串
for (auto ch : s2)
{
ch -= 2; // 改的是临时拷贝
}
cout << s2 << endl; // 还是hello
// 加引用,ch是引用,修改影响原字符串
for (auto& ch : s2)
{
ch -= 2; // 改的是原字符串
}
cout << s2 << endl; // 变成了fcjjm
- 范围 for 可以作用到数组和容器对象上进行遍历。
2.3 string 类对象的常见构造
| 函数名称 | 功能说明 |
|---|---|
| string() | 构造空的 string 类对象,即空字符串 |
| string(const char* s) | 用 C-string 来构造 string 类对象 |
| string(size_t n, char c) | string 类对象中包含 n 个字符 c |
| string(const string& s) | 拷贝构造函数 |
#include<iostream>
#include<string>
using namespace std;
void Teststring()
{
string s1; // 构造空的string类对象s1
string s2("hello bit"); // 用C格式字符串构造string类对象s2
string s3(10, 'x'); // 构造包含10个'x'的string对象
string s4(s2); // 拷贝构造s4
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
}
int main()
{
Teststring();
return 0;
}
2.4 string 类对象的容量操作
| 函数名称 | 功能说明 |
|---|---|
| size | 返回字符串有效字符长度 |
| length | 返回字符串有效字符长度 |
| capacity | 返回空间总大小 |
| empty | 检测字符串是否为空串,是返回 true,否则返回 false |
| clear | 清空有效字符 |
| reserve | 为字符串预留空间 |
| resize | 将有效字符的个数改成 n 个,多出的空间用字符 c 填充 |
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s("hello bit");
cout << s.size() << endl;
cout << s.length() << endl;
cout << s.capacity() << endl;
cout << s.empty() << endl;
s.clear();
cout << s << endl;
cout << s.capacity() << endl;
s.reserve(100);
cout << s.capacity() << endl;
s.resize(5, 'a');
cout << s << endl;
return 0;
}
容量操作的注意点:
-
size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。 -
clear()只是将 string 中有效字符清空,不改变底层空间大小。 -
resize(size_t n)与resize(size_t n, char c)都是将字符串中有效字符个数改变到 n 个。
不同的是当字符个数增多时:
resize(n)用'\0'来填充多出的元素空间。resize(size_t n, char c)用字符 c 来填充多出的元素空间。
注意:
resize 在改变元素个数时,如果是将元素个数增多,会改变底层容量的大小;
如果是将元素个数减少,底层空间总大小不变。
reserve(size_t res_arg=0):为 string 预留空间,不改变有效元素个数。
当 reserve 的参数小于 string 的底层空间总大小时,VS2022中reserve 不会改变容量大小,但是linux会改变,即缩容,就是因为文档在这种情况下写的是没有约束力的请求,即请求缩容,但是不算强制的。
对 string 操作时,如果能够大概预估到放多少字符,可以先通过 reserve 把空间预留好,避免频繁扩容。
2.5 string 类对象的访问及遍历操作
| 函数名称 | 功能说明 |
|---|---|
| operator[] | 返回 pos 位置的字符,const 和非 const 版本都有 |
| begin + end | begin 获取第一个字符的迭代器,end 获取最后一个字符下一个位置的迭代器 |
| rbegin + rend | 反向迭代器,从后往前遍历 |
| 范围 for | C++11 支持更简洁的遍历方式 |
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s("hello bit");
// 1. operator[] 下标访问
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i] << " ";
}
cout << endl;
// 2. 迭代器遍历
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 3. 反向迭代器遍历
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
// 4. 范围for遍历
for (auto ch : s)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
迭代器使用的注意点:
不能直接对 end() 进行 -- 操作,因为 end() 返回的是临时右值,不能对临时右值进行自减。
错误代码:
auto it = --container.end();
错误原因:
不能对临时右值进行自减。
正确做法:
先把 end() 拿到,存在一个变量里,此时它就变成了左值,可以安全地进行 -- 操作。
auto it = s.end(); // 先用变量存储end()迭代器
--it; // 对变量进行自减,安全指向最后一个有效元素
cout << *it << endl;
2.6 string 类对象的修改操作
| 函数名称 | 功能说明 |
|---|---|
| push_back | 在字符串后尾插字符 c |
| append | 在字符串后追加一个字符串 |
| operator+= | 在字符串后追加字符串 str(最常用) |
| c_str | 返回 C 格式字符串 |
| data | 返回字符串数据 |
| find + npos | 从字符串 pos 位置开始往后找,返回位置 |
| rfind | 从字符串 pos 位置开始往前找,返回位置 |
| find_first_of | 查找字符集合中任意一个字符的第一个匹配 |
| find_last_of | 从后往前查找字符集合中任意一个字符 |
| substr | 从 pos 位置开始,截取 n 个字符返回 |
| copy | 把字符串中的字符拷贝到 buffer 中 |
2.6.1 尾插操作
在 string 尾部追加字符时:
push_back(c) / append(1, c) / += 'c'
三种的实现方式差不多。
一般情况下 string 类的 += 操作用的比较多。
+= 操作不仅可以连接单个字符,还可以连接字符串。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s;
s.push_back('h'); // 尾插字符
s.append("ello"); // 追加字符串
s += ' '; // +=追加字符
s += "bit"; // +=追加字符串
cout << s << endl; // hello bit
return 0;
}
2.6.2 c_str 和 data
c_str() 返回 C 格式字符串,即 const char*,末尾保证有 '\0'。
data() 和 c_str() 功能类似,返回 const char*,指向字符串内部字符数组的首地址。
注意:
C++17 之前:
data()不保证末尾带
'\0'C++17 及以后:
data()和
c_str()完全一样,末尾保证有
'\0'空终止符。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s("hello");
const char* p1 = s.c_str(); // 返回C格式字符串
const char* p2 = s.data(); // 返回字符串数据
cout << p1 << endl;
cout << p2 << endl;
// 用C语言的函数操作string
printf("%s\n", s.c_str());
return 0;
}
2.6.3 find 系列和 npos
find 从字符串 pos 位置开始往后找子串或字符,返回找到的位置;找不到返回 npos。
npos 是 string 类的静态成员常量,值为 -1。
因为是 size_t 无符号类型,所以实际表示的是最大值。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s("hello bit hello");
// find找子串
size_t pos = s.find("bit");
if (pos != string::npos)
{
cout << "找到了,位置是:" << pos << endl;
}
// rfind从后往前找
pos = s.rfind("hello");
cout << "rfind找到的位置:" << pos << endl;
// find_first_of:找字符集合中任意一个字符的第一个匹配
// 在s中找"aeiou"中任意一个元音字母第一次出现的位置
pos = s.find_first_of("aeiou");
cout << "第一个元音字母的位置:" << pos << endl;
// find_last_of:从后往前找字符集合中任意一个字符
pos = s.find_last_of("aeiou");
cout << "最后一个元音字母的位置:" << pos << endl;
return 0;
}
注意区分:
find和
find_first_of
find是找整个子串完全匹配。
find_first_of是找字符集合中任意一个字符匹配,只要有一个字符匹配就算找到了。
2.6.4 substr 和 copy
substr(pos, len):
在 str 中从 pos 位置开始,截取 len 个字符,然后将其作为一个新的 string 对象返回。
copy 一般和 substr 功能类似,但是 copy 要开一个 buffer 空间,把 string 的值拷贝到 buffer 中去。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s("hello bit");
// substr截取子串
string sub = s.substr(6, 3); // 从位置6开始,截取3个字符
cout << sub << endl; // bit
// copy拷贝到buffer
char buffer[10] = { 0 };
s.copy(buffer, 5, 0); // 从位置0开始,拷贝5个字符到buffer
cout << buffer << endl; // hello
return 0;
}
不是迭代器接口的函数,参数都是起始位置加长度,比如:
substr(pos, len)
erase(pos, len)
copy(buf, len, pos)等。
2.7 string 类的非成员函数
| 函数 | 功能说明 |
|---|---|
| operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
| operator>> | 输入运算符重载 |
| operator<< | 输出运算符重载 |
| getline | 获取一行字符串 |
| relational operators | 大小比较 |
2.7.1 operator + 尽量少用
operator+ 是传值返回,会产生深拷贝,效率低。
能使用 += 就不要用 +。
string s1("hello");
string s2(" bit");
string s3 = s1 + s2; // 产生临时对象,深拷贝,效率低
s1 += s2; // 直接在s1上追加,效率高
2.7.2 operator>> 和 getline
cin >> s 遇到空格和换行符就会终止,所以不能读取带空格的字符串。
getline(cin, s) 可以读取一整行,包括空格,遇到换行符终止,还有一种形式是自己确定是以什么为终止符getline(cin, s, '*') 。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s;
// cin >> s 遇到空格就结束
cin >> s;
cout << s << endl; // 输入"hello bit",只输出"hello"
// getline可以读取一整行
getline(cin, s);
cout << s << endl; // 输入"hello bit",输出"hello bit"
// 这里getline可以读取到*才截止
getline(cin, s);
cout << s << endl; // 输入"hello bit(回车换行)adsd*",
//输出"hello bit(回车换行)adsd"
return 0;
}
那么
cin.get()是干什么的呢?不管是字符还是字符串的
>>输入,它们都不会读取到空格或者换行符。为了解决这个问题,就有了
cin.get()。这个东西可以读取到空格和换行符。
输入字符串的时候,因为字符串是以空格和换行符作为分隔符的,所以遇到这些就会终止。
输入字符的时候,因为字符是一个一个读取的,读完一个就终止,所以空格和换行符在字符输入时不起作用的。
2.7.3 operator>> 的底层实现(如何解决频繁扩容)
我们来思考一下 operator>> 是怎么实现的。
如果每读取一个字符就 push_back 一次,那么当输入字符串很长的时候,就会频繁扩容,效率很低。
那能不能用 reserve 提前预留空间呢?
不行。
因为 reserve 小了,效率没有提升;
reserve 大了,效率提升了,但是如果输入小了就浪费了。
解决办法是:
开一个 buff 数组,先把字符读到 buff 数组中,buff 数组满了再一次性追加到 string 中。
因为字符放在 buff 数组中是没有扩容的,而且 buff 数组在栈上,随函数栈帧释放,效率高,出了operator>>就销毁了,不会一直占用空间,用reserve的话可能会有多个类同时存在很久,就导致空间一直被占用。
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 256;
char buff[N];
int i = 0;
char ch;
ch = in.get(); // 用get读取,因为>>读不到空格和换行
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == N - 1) // buff满了
{
buff[i] = '\0';
s += buff; // 一次性追加
i = 0;
}
ch = in.get();
}
if (i > 0) // 处理最后不满buff的部分
{
buff[i] = '\0';
s += buff;
}
return in;
}
这也是 string 类这样设计的原因之一:
通过 buff 数组减少扩容次数,同时栈上的 buff 随函数结束自动释放,不会造成长期的空间浪费。
2.7.4 关系运算符
string 类重载了关系运算符,可以直接用:
>、<、==、!=
等比较字符串大小。
比较的是字典序。
string s1("abc");
string s2("abd");
if (s1 < s2)
cout << "s1 < s2" << endl;
2.8 string 的两个 swap
string 有两个 swap:
一个是成员函数 swap,
一个是全局的 std::swap(string)。
string s1("hello");
string s2("world");
s1.swap(s2); // 成员swap,交换三个成员,高效
std::swap(s1, s2); // 全局swap,也可以用
成员 swap 的实现:
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
为什么需要全局的
std::swap(string)呢?全局的是为了防止调用库里面的那个通用 swap。
库里面的通用 swap 在面对深拷贝的情况下,会产生 3 次深拷贝,导致效率骤降。
而 string 专门提供了全局的 swap 特化版本,内部调用的是成员 swap,只交换指针和整数,效率很高。
3 总结
这篇博客我们学习了:
- 为什么需要 string 类(C 语言字符串的痛点)
- string 类的常用接口(构造、容量、访问、修改、非成员函数)
预告:
下一篇博客会讲到:
-
string 的底层结构(VS 的 SSO 和 g++ 的写时拷贝)
-
string 的模拟实现(浅拷贝问题 → 深拷贝传统写法 → 现代写法 → 写时拷贝)
-
编码问题和一些细节注意点
欲知后事如何,请听下回分解!
希望这篇博客对你有帮助!
谢谢观看!
—— string类初识与常用接口详解&spm=1001.2101.3001.5002&articleId=165097232&d=1&t=3&u=1303d9619b3447b4ae8346e60caedd4e)
2万+

被折叠的 条评论
为什么被折叠?



