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

news/2024/12/22 9:26:19/

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

书里也强调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这样应该边界很明显了


http://www.ppmy.cn/news/1452764.html

相关文章

【Java】java实现文件上传和下载(上传到指定路径/数据库/minio)

目录 上传到指定路径 一、代码层级结构 二、文件上传接口 三、使用postman进行测试&#xff1b; MultipartFile接收前端传递的文件&#xff1a;127.0.0.1:8082/path/uploadFile part接收前端传递的文件&#xff1a;127.0.0.1:8082/path/uploadFileByRequest 接收前端传递…

C# 实现格式化文本导入到Excel

目录 需求 Excel 的文本文件导入功能 范例运行环境 配置Office DCOM 实现 组件库引入 OpenTextToExcelFile 代码 调用 小结 需求 在一些导入功能里&#xff0c;甲方经常会给我们一些格式化的文本&#xff0c;类似 CSV 那样的纯文本。比如有关质量监督的标准文件&…

【C++】二叉树的进阶

二叉树的进阶 二叉搜索树概念操作实现创建树形结构拷贝构造函数构造函数析构函数赋值运算符重载循环版本查找插入删除 递归版本查找插入删除 应用K模型KV模型性能分析 二叉树进阶面试题二叉树创建字符串二叉树的分层遍历I最近公共祖先二叉搜索树与双向链表前序遍历与中序遍历构…

面试题分享之Java集合篇(三)

注意&#xff1a;文章若有错误的地方&#xff0c;欢迎评论区里面指正 &#x1f36d; 系列文章目录 面试题分享之Java基础篇&#xff08;二&#xff09;面试题分享之Java基础篇&#xff08;三&#xff09; 面试题分享之Java集合篇&#xff08;一&#xff09;、 面试题分享之Ja…

【代码随想录】day48

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、198打家劫舍二、213打家劫舍II三、337打家劫舍III 一、198打家劫舍 class Solution { public:int rob(vector<int>& nums) {vector<int> dp(n…

JET毛选学习笔记:如何利用《矛盾论》从做实验到做科研vol. 2

上一节讲完矛盾的普遍性和特殊性都已经5000字了&#xff0c;为了不影响阅读观感&#xff08;多水几篇&#xff09;&#xff0c;把他们进行了拆分&#xff0c;那我就继续侃大山吧。 五、矛盾的同一性和斗争性 先做名词解释&#xff1a; 矛盾的同一性&#xff08;统一&#xf…

neo4j 的插入速度为什么越来越慢,可能是使用了过多图谱查询操作

文章目录 背景描述分析解决代码参考neo4j 工具类Neo4jDriver知识图谱构建效果GuihuaNeo4jClass 背景描述 使用 tqdm 显示&#xff0c;处理的速度&#xff1b; 笔者使用 py2neo库&#xff0c;调用 neo4j 的API 完成节点插入&#xff1b; 有80万条数据需要插入到neo4j图数据中&am…

[Linux][守护进程]详细讲解 + 自主实现

目录 0.预备知识1.守护进程概念2.进程组概念3.会话概念4.守护进程化的方式5.实现daemon() 0.预备知识 **前台进程&#xff1a;**和终端关联的进程任何一次会话&#xff0c;只允许有一个前台进程和多个后台进程守护进程不能直接向显示器打印消息&#xff0c;一旦打印&#xff0…