字符串处理是非常令人关注的领域,因为大部分情况下我们的程序不是在处理数字而是在处理字符串,对于字符串的表示和操作成为编程语言中非常重要的一部分
书里也强调C++中对于字符串的处理要好过C风格的char数组,更高效也更安全
本章我们可以学到的是:
A Better Class of String
<cstring>这里定义了关于C风格的以\0结尾的字符串的处理函数集,比如连接、搜索、比较等等
但是一切都基于\0这个标记字符串结束的字符,这带来了很多安全问题
Standard library header <cstring> - cppreference.com
C++标准库中定义了string这个模板类来表示字符串,注意他不是基本类型,除了字符外还包含了指针、字符数量以及其他很多操作
Defining string Objects 定义
string有很多构造函数,默认构造、字面量构造、重复字符构造、复制构造、范围构造
但是使用构造函数的时候一定要区分小括号和大括号,小括号使用的才是带参数的构造函数,大括号里只含一个字符串才会得到正常的初始化结果
这是变量phrase的初始化情况,0-13表示的是proverb下标为0开始的13个字符,13表示的不是范围右边界(这是经常出错的地方)
下面对书里提到的初始化方式做了一个总结
注意4、6两个用的都是大括号,这是我不理解的地方,我把大括号换成小括号后,结果没有改变,但是为了不导致混淆,用小括号调用构造函数不是更直观的写法吗?大括号也会调用带参数的构造函数吗?
包含字符串和字面量+一个数字的初始化方式有两种,这样看来设计是非常不统一的,建议都用小括号,使用cppreference中的语法来进行string的创建
std::basic_string<CharT,Traits,Allocator>::basic_string - cppreference.com
#include <cassert>
#include <cctype>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <string>int main()
{std::cout << "1) string(); ";std::string s1;assert(s1.empty() && (s1.length() == 0) && (s1.size() == 0));std::cout << "s1.capacity(): " << s1.capacity() << '\n'; // unspecifiedstd::cout << "2) string(size_type count, CharT ch): ";std::string s2(4, '=');std::cout << std::quoted(s2) << '\n'; // "===="std::cout << "3) string(const string& other, size_type pos, size_type count): ";std::string const other3("Exemplary");std::string s3(other3, 0, other3.length() - 1);std::cout << std::quoted(s3) << '\n'; // "Exemplar"std::cout << "4) string(const string& other, size_type pos): ";std::string const other4("Mutatis Mutandis");std::string s4(other4, 8);std::cout << std::quoted(s4) << '\n'; // "Mutandis", i.e. [8, 16)std::cout << "5) string(CharT const* s, size_type count): ";std::string s5("C-style string", 7);std::cout << std::quoted(s5) << '\n'; // "C-style", i.e. [0, 7)std::cout << "6) string(CharT const* s): ";std::string s6("C-style\0string");std::cout << std::quoted(s6) << '\n'; // "C-style"std::cout << "7) string(InputIt first, InputIt last): ";char mutable_c_str[] = "another C-style string";std::string s7(std::begin(mutable_c_str) + 8, std::end(mutable_c_str) - 1);std::cout << std::quoted(s7) << '\n'; // "C-style string"std::cout << "8) string(string&): ";std::string const other8("Exemplar");std::string s8(other8);std::cout << std::quoted(s8) << '\n'; // "Exemplar"std::cout << "9) string(string&&): ";std::string s9(std::string("C++ by ") + std::string("example"));std::cout << std::quoted(s9) << '\n'; // "C++ by example"std::cout << "a) string(std::initializer_list<CharT>): ";std::string sa({'C', '-', 's', 't', 'y', 'l', 'e'});std::cout << std::quoted(sa) << '\n'; // "C-style"// before C++11, overload resolution selects string(InputIt first, InputIt last)// [with InputIt = int] which behaves *as if* string(size_type count, CharT ch)// after C++11 the InputIt constructor is disabled for integral types and calls:std::cout << "b) string(size_type count, CharT ch) is called: ";std::string sb(3, std::toupper('a'));std::cout << std::quoted(sb) << '\n'; // "AAA"// std::string sc(nullptr); // Before C++23: throws std::logic_error// Since C++23: won't compile, see overload (18)
// std::string sc(0); // Same as above, as literal 0 is a null pointer constantauto const range = {0x43, 43, 43};
#ifdef __cpp_lib_containers_rangesstd::string sc(std::from_range, range); // tagged constructor (19)std::cout << "c) string(std::from_range, range) is called: ";
#elsestd::string sc(range.begin(), range.end()); // fallback to overload (12)std::cout << "c) string(range.begin(), range.end()) is called: ";
#endifstd::cout << std::quoted(sc) << '\n'; // "C++"
}
Operations with String Objects 字符串操作
这里是用字符串和字面量赋值的操作
Concatenating Strings 字符串连接
concatenate就是join、connect的意思,表示连接
我们可以进行的连接操作是string与string、string与literal,但是我们不能进行对两个字面量的连接,因为+这个运算符是我们对操作符的重载(这一点后面会讲)
字面量类型本质还是const char 指针,这种类型不支持+的操作
下面是一个使用的例子
还有一个接口叫append也可以用来拼接字符串,但是如果只是简单的拼接不涉及字符串截取,+=显然比append更简单
但是append也不是随便设计的,当我们不需要完整参数而是想拼接它的子串时,append可以这么使用:
和string其他操作类似,我们可以拼接重复字符、普通字符串、某个下标开始的子串、字面量、范围、字符列表等等
Concatenating Strings and Characters
前面提到两个字面量不可以连接,同样的两个char字符也不能连接,我们看下面一个例子:
两个char字符连接不会得到我们意想之中的结果,此时char被转为int进行ASCII整数运算,得到的字符是大写的L
所以作连接操作的时候一定要保证一侧至少有一个string对象,否则大概率会出错
Concatenating Strings and Numbers
C++不允许将string与数字相连接
会报错,显示没有匹配到+运算符用来连接string与double
有这么几个解决办法,要么我们把数字转换为字符串,要么使用format组合不同类型的对象
数字转换为字符串可以使用to_string方法,format已经讲过不再赘述
Accessing Characters in a String 访问字符串里的字符
C++中的字符串string是可变对象,不像Java和Go等其他语言不可变,我们可以通过下标直接访问其中的字符并进行修改
注意我们这里使用了getline输入了一行字符串到text,我们也可以更改分隔符,getline默认以换行符作为界限从输入流中获取数据
我们把delim参数加上(delimeter)
这样的话我们从输入流中读取字符直到#,把读取到的所有字符都存进text
Accessing Substrings 获取子串
使用substr这个方法,返回的子串是从下标pos开始的n个字符构造的新string对象
如果n越界了,那会返回后面的所有字符,和我们不写n参数是一样的效果
但是如果pos越界了,那函数就会抛出异常
Comparing Strings 字符串比较
大于小于这些运算符都是基于字符串的字典序比较
Three-Way Comparisons
C++20中有两种方法用于字符串比较的三种结果判断,一种是<=>箭头,一种是compare成员函数
is_lt就是less than,和右边的compare返回值<0是一种结果,表示第一个字符串小于第二个
总之两种方法都可以比较字符串,但是都不要写到if里面
Comparing Substrings Using compare()
之所以还要介绍compare这个成员函数,是因为我们不仅可以使用他比较两个完整的字符串,我们也可以使用重载版本去比较一个字符串与另外一个子串甚至C字符串
可以看到重载版本非常多std::basic_string<CharT,Traits,Allocator>::compare - cppreference.com
这里简单举一个例子,使用的应该是(2)版本
也就是说我们比较的是第一个字符串的子串与第二个字符串
我们也可以用compare来搜索子串
Comparing Substrings Using substr()
Checking the Start or End of a String 前后缀子串
但是可能由于这个功能很常用,C++20引入了两个函数专门用来查找前后缀子串
这样变得简洁了很多,目的也更明确,可读性增强
而且这两个函数在空字符串上也可以使用,不像front(),back()等函数有大小限制
Searching Strings 字符串内字符查找
首先介绍一个基本的方法:find()
As you can tell from this output, std::string::npos is defined to be a very large number. More
specifically, it is the largest value that can be represented by the type size_t. For 64-bit platforms, this value equals 2^64-1, a number in the order of 10^19—a one followed by 19 zeros.
我们会这样使用npos来作函数返回值检查:
所以不要想当然认为没找到字符就返回false,find也不要写进if条件里
Searching Within Substrings
std::basic_string<CharT,Traits,Allocator>::find - cppreference.com
find本身也有很多个重载版本
Finds the first substring equal to the given character sequence.
这个方法的设计目标就是寻找当前字符串从pos开始包不包含这样一个子串,子串的形式可以是string也可以是C串或者字符;find的返回值都是无符号整型,不要误认为是bool
这种接口设计风格也很像自然语言,把重要参数放在前面
同样的我们可以查找目标字符串的子串,此时要使用两个参数,包括子串的起始位置和查找字符
下面是查找字符串中不重复的子串出现次数的一段小程序,很实用
并且字符串的处理都是区分大小写的,在输入的一句话中出现了10次had,我们的程序使用while循环每次都在上一次查找的结果后面查找had,这样能保证不重复
之前还以为while循环里也能进行初始化操作,现在看来是不行的,带初始化的时候还是用for循环吧,不过这里用for写就很冗长了
Searching for Any of a Set of Characters
假设我们想要对字符串按照一些标点符号进行分割,此时我们要在字符串中不断查找这些分隔符
Finds the first character equal to one of the characters in the given character sequence.
std::basic_string<CharT,Traits,Allocator>::find_first_of - cppreference.com
首先介绍find_first_of这个方法,它负责找到一个字符串中第一次出现参数中字符的位置并返回
不过可以看到我们不仅可以查找第一次出现的位置,范围以外的元素可以用find_first_not_of
最后一次出现的位置可以用find_last_of
下面是一个单词提取的小程序:
#include <iostream>
#include <format>
#include <vector>using namespace std;int main() {std::string text; // The string to be searchedstd::cout << "Enter some text terminated by *:\n";std::getline(std::cin, text, '*');const std::string separators{" ,;:.\"!?'\n"}; // Word delimitersstd::vector<std::string> words; // Words foundsize_t start{text.find_first_not_of(separators)}; // First word start indexwhile (start != std::string::npos) // Find the words{size_t end = text.find_first_of(separators, start + 1); // Find end of wordif (end == std::string::npos) // Found a separator?end = text.length(); // No, so set to end of textwords.push_back(text.substr(start, end - start)); // Store the wordstart = text.find_first_not_of(separators, end + 1); // Find first character of next word}std::cout << "Your string contains the following " << words.size() << " words:\n";size_t count{}; // Number outputfor (const auto& word: words) {std::cout << std::format("{:15}", word);if (!(++count % 5))std::cout << std::endl;}std::cout << std::endl;
}
这个小程序也很有用,做到了按照标点符号分割后单词的提取,保留了大小写,而且以表格化形式输出
具体思路是先找到第一个单词所在的位置,使用find_first_not_of;然后找到第一个标点符号,使用find_first_of,这样一个单词的边界就确定了,注意左闭右开,然后用substr提取单词子串,更新下一个单词的开头,直到查找到字符串末尾
Searching a String Backward
从末尾倒序查找子串,不过不是子串倒过来的查找,返回的下标也是从前往后数的正向下标
Note that this is an offset from the start of the string, not the end.
std::basic_string<CharT,Traits,Allocator>::rfind - cppreference.com
上图其实包括了三个rfind重载版本,而且我们也可以加参数表示搜索起始位置
但是我个人觉得这个函数不如把string翻转过来再正向搜索,反正有点反直觉,个人不爱用
Modifying a String 字符串修改
一般的流程应该是查找修改的位置再修改,也就是先用find系列,再用修改方法
Inserting a String 插入字符串
这里的insert指定了插入的位置,即下标14开始插入words,不过下标插入的意思是插入在这个位置之前的空隙,从下标14开始替换(这里有歧义需要说明一下)
类似其他的查找函数,我们也可以插入C串,或者插入一个子串
我们还可以插入重复的若干个字符
Replacing a Substring
文章用的小标题都是方法的名字,接口设计很直观,基本不用特别搜索
首先介绍的版本是:
As always, the second argument of replace() is a length, and not a second index.
也就是说我们只需要记住replace第二个参数是需要被替换掉的字符数量,而不是替换内容的字符数;再次强调,第二个参数是被替换的字符数
那么这个方法一般怎么用呢,我们一般是先查找需要被替换的位置,再进行替换
我们找到了Jones的起始位置start,又在它的后面找到了第一个分隔符的位置end,end-start就是Jones的长度,这个长度作为replace的第二个参数
当然我们也可以替换一个子串
这样写的话效果是一样的,5个参数分别是被替换位置、被替换长度;替换对象、被替换位置、被替换长度
如果替换对象是C串的话,4参数版本的n2表示的是替换的字符数
我们还有一个替换重复字符的版本
这个我就不拿代码举例子了
std::basic_string<CharT,Traits,Allocator>::replace - cppreference.com
replace版本很多,没事可以去上面的ref看一看
Removing Characters from a String
需要移除字符的时候我们可以用replace在某位置替换为一个空字符串,但是C++有一个专用移除字符的方法叫erase
先举一个简单例子:
字符串的所有函数逻辑都类似,一个起始位置加上要处理的长度
不过更常见的使用方法应该是先查找要删除的起始位置:
我们看看erase其他参数的作用
所以我们不要认为一个参数的erase是用来删除某个位置的字符,他会把后面的所有字符都删除
正确的应该使用erase(i,1)来删除下标i的字符
还有一个clear方法用来删除所有字符,它的用法相当于erase(0)
std::basic_string<CharT,Traits,Allocator>::erase - cppreference.com这里补充其他的erase版本
C++20还加了两个非成员函数用来删除字符
std::erase, std::erase_if (std::basic_string) - cppreference.com
第一个是删除value字符,第二个是删除符合谓词pred的字符
我们来看看用法
这显然比我们用循环或者STL算法写起来简单
std::string vs. std::vector<char>
string比vector<char>好用很多,提供的方法也更多
不赘述,无脑使用string
Strings of International Characters
string由于是个类模板,不仅支持普通的char类型还支持存储其他字符
不过限于我对这些类型的引入还不是很了解,这一部分不在此详细介绍
Raw String Literals
一般的字符串字面值不允许我们换行和回车,要包含这些字符我们必须使用C语言中留下来的转义字符,但是像文件路径等使用转义字符非常多的时候代码可读性就很差
而且正则表达式也有很多反斜线,我们不希望转义字符造成歧义
那么raw string literal就是解决这些问题才设计的,我们不再需要转义字符
R("")包住的部分就是我们原本输入的字符串,不再需要对字符特殊处理
但是如果我们的字符串里本来就有括号和引号呢,这么做会导致下面的问题
编译器会认为b)"这里就结束了,后面的算作多余字符
其实我们的分隔符很灵活,不一定需要括号,R"..."省略号里只要有独特的字符标志边界就可以了,有括号的时候我们的边界可以像上面这样处理,使用*紧接双引号
或者更明显一点:
hhh这样应该边界很明显了