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

2024-04-30 05:44

本文主要是介绍《Beginning C++20 From Novice to Professional》第七章Working with Strings,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

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

这篇关于《Beginning C++20 From Novice to Professional》第七章Working with Strings的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/948039

相关文章

关于C++中的虚拟继承的一些总结(虚拟继承,覆盖,派生,隐藏)

1.为什么要引入虚拟继承 虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如下: class A class B1:public virtual A; class B2:pu

C++对象布局及多态实现探索之内存布局(整理的很多链接)

本文通过观察对象的内存布局,跟踪函数调用的汇编代码。分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等 文章链接:http://dev.yesky.com/254/2191254.shtml      论C/C++函数间动态内存的传递 (2005-07-30)   当你涉及到C/C++的核心编程的时候,你会无止境地与内存管理打交道。 文章链接:http://dev.yesky

C++的模板(八):子系统

平常所见的大部分模板代码,模板所传的参数类型,到了模板里面,或实例化为对象,或嵌入模板内部结构中,或在模板内又派生了子类。不管怎样,最终他们在模板内,直接或间接,都实例化成对象了。 但这不是唯一的用法。试想一下。如果在模板内限制调用参数类型的构造函数会发生什么?参数类的对象在模板内无法构造。他们只能从模板的成员函数传入。模板不保存这些对象或者只保存他们的指针。因为构造函数被分离,这些指针在模板外

C++工程编译链接错误汇总VisualStudio

目录 一些小的知识点 make工具 可以使用windows下的事件查看器崩溃的地方 dumpbin工具查看dll是32位还是64位的 _MSC_VER .cc 和.cpp 【VC++目录中的包含目录】 vs 【C/C++常规中的附加包含目录】——头文件所在目录如何怎么添加,添加了以后搜索头文件就会到这些个路径下搜索了 include<> 和 include"" WinMain 和

C/C++的编译和链接过程

目录 从源文件生成可执行文件(书中第2章) 1.Preprocessing预处理——预处理器cpp 2.Compilation编译——编译器cll ps:vs中优化选项设置 3.Assembly汇编——汇编器as ps:vs中汇编输出文件设置 4.Linking链接——链接器ld 符号 模块,库 链接过程——链接器 链接过程 1.简单链接的例子 2.链接过程 3.地址和

C++必修:模版的入门到实践

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:C++学习 贝蒂的主页:Betty’s blog 1. 泛型编程 首先让我们来思考一个问题,如何实现一个交换函数? void swap(int& x, int& y){int tmp = x;x = y;y = tmp;} 相信大家很快就能写出上面这段代码,但是如果要求这个交换函数支持字符型

20.Spring5注解介绍

1.配置组件 Configure Components 注解名称说明@Configuration把一个类作为一个loC容 器 ,它的某个方法头上如果注册7@Bean , 就会作为这个Spring容器中的Bean@ComponentScan在配置类上添加@ComponentScan注解。该注解默认会扫描该类所在的包下所有的配置类,相当于之前的 <context:component-scan>@Sc

C++入门01

1、.h和.cpp 源文件 (.cpp)源文件是C++程序的实际实现代码文件,其中包含了具体的函数和类的定义、实现以及其他相关的代码。主要特点如下:实现代码: 源文件中包含了函数、类的具体实现代码,用于实现程序的功能。编译单元: 源文件通常是一个编译单元,即单独编译的基本单位。每个源文件都会经过编译器的处理,生成对应的目标文件。包含头文件: 源文件可以通过#include指令引入头文件,以使

C++面试八股文:std::deque用过吗?

100编程书屋_孔夫子旧书网 某日二师兄参加XXX科技公司的C++工程师开发岗位第26面: 面试官:deque用过吗? 二师兄:说实话,很少用,基本没用过。 面试官:为什么? 二师兄:因为使用它的场景很少,大部分需要性能、且需要自动扩容的时候使用vector,需要随机插入和删除的时候可以使用list。 面试官:那你知道STL中的stack是如何实现的吗? 二师兄:默认情况下,stack使

剑指offer(C++)--孩子们的游戏(圆圈中最后剩下的数)

题目 每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去