《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++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给