《Beginning C++20 From Novice to Professional》第七章Working with Strings

发布于:2024-05-08 ⋅ 阅读:(26) ⋅ 点赞:(0)

字符串处理是非常令人关注的领域,因为大部分情况下我们的程序不是在处理数字而是在处理字符串,对于字符串的表示和操作成为编程语言中非常重要的一部分

书里也强调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'; // unspecified
 
    std::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 constant
 
    auto const range = {0x43, 43, 43};
#ifdef __cpp_lib_containers_ranges
    std::string sc(std::from_range, range); // tagged constructor (19)
    std::cout << "c) string(std::from_range, range) is called: ";
#else
    std::string sc(range.begin(), range.end()); // fallback to overload (12)
    std::cout << "c) string(range.begin(), range.end()) is called: ";
#endif
    std::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 searched
    std::cout << "Enter some text terminated by *:\n";
    std::getline(std::cin, text, '*');
    const std::string separators{" ,;:.\"!?'\n"};     // Word delimiters
    std::vector<std::string> words;                   // Words found
    size_t start{text.find_first_not_of(separators)}; // First word start index
    while (start != std::string::npos)                // Find the words
    {
        size_t end = text.find_first_of(separators, start + 1); // Find end of word
        if (end == std::string::npos)                           // Found a separator?
            end = text.length();                                // No, so set to end of text
        words.push_back(text.substr(start, end - start));       // Store the word
        start = 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 output
    for (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这样应该边界很明显了