第二章 使用string和string_view

发布时间 2023-11-28 18:33:56作者: Mesonoxian

第二章 使用string和string_view


C风格字符串

在C语言中,字符串为字符类型的数组.字符串中的最后一个字符是 null('\0') 字符,官方将这个字符定义为 NUL .目前,程序员使用C字符串最常犯的错误是忘记为NUL分配空间

C++中有一些从C语言的字符串操作函数,它们在 <cstring> 中定义.

主要的来自C语言的字符串函数:

  • strlen():获得对应字符串的长度(而非需要的内存空间)
  • strcpy():用于复制一个字符串到另一个字符串
  • strstr():用于在一个字符串里找另一个字符串,如果找到了,返回子串在字符串中的地址
  • strcmp():一般用于比较两个字符串的长度,大于返回正数;小于返回负数;相等返回0
  • strcat():用于拼接两个字符串为一个字符串
  • strtok():用来按照子串分割字符串,多次调用可以将字符串多次分割
  • memset():对内存进行统一设置值,在不涉及class的情况下,通常用于清空内容
  • memcpy():用于复制某块内存到另一块内存

需要注意的,由于NUL存在的缘故,通常而言,strlen()与sizeof()的结果相差1

char text1[]{"abcdefg"};
size_t s1{sizeof(text1)};//7
size_t s2{strlen(text1)};//6

同时注意到,若将上面代码的数组类型改为指针, sizeof()将不再返回字符串占字节数,而是返回指针占用字节!

const char* text1{"abcdefg"};
size_t s1{sizeof(text1)};//sizeof(char*)
size_t s2{strlen(text1)};//6

警告
在Microsoft Visual Studio中使用C语言风格字符串时,编译器可能会提示unsafe function
在这种情况下,一种退而求其次的策略是使用它们的安全版本,如strcpy_s()等
然而最好的方法还是使用std中的std::string,执意要使用可以考虑声明宏_CRT_SECURE_NO_WARNINGS为1来解决这个警告.(scanf与printf同理)

通常而言,strtok()的使用一直是字符串处理的一个难点.在这里给出strtok的使用示例.
首先,strtok()的声明原型为:

char* strtok(char* _String,const char* _Delimiter)

其中,一般对于这两个参数,我们给予 _String 一个字符串 str,这个字符串是我们处理的对象;而给予 _Delimiter 的参数 sep 则是用于分割这个字符串的子串.

举例:

char* sep = "-";
char str[]{"mesonoxian-yao"};
char* ret = strtok(str,sep);
printf("%s",ret);//输出mesonoxian

而关于strtok的运行机理,这里简要介绍:

  • strtok函数找到str中的下一个标记,并将其用NUL结尾,返回一个指向这个标记的指针(这也意味着strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改)
  • strtok函数的第一个参数不为NULL,函数将找到str中第一个标记,strtok函数将保存它在字符串的位置
  • strtok函数的第一个参数为NULL,函数将在同一个字符串中被保存的位置开始,查找下一个标记
  • 如果字符串中不存在更多的标记,则返回nullptr

给出一段代码解释:

char* sep = ".";
char str[40] = "john.wilson.yao@gmail.com";
char* ret = NULL;
for (ret = strtok(str, sep); ret != NULL; ret = strtok(NULL, sep))
	printf("%s\n", ret);
//输出结果
/*john
wilson
yao@gmail
com*/

这段代码中很好的体现了strtok这种机理.为了更进一步地证明其改变了原始字符串,下面再给出一个例子:

const char* sep1 = ".";
const char* sep2 = "@";
char str[]{ "john.wilson.yao@gmail.com" };
std::cout << strtok(str, sep1) << std::endl;//john
std::cout << strtok(NULL, sep1) << std::endl;//wilson
std::cout << strtok(NULL, sep2) << std::endl;//yao
std::cout << strtok(NULL, sep1) << std::endl;//gmail
std::cout << strtok(NULL, sep1) << std::endl;//com
std::cout << strtok(str, sep2) << std::endl;//john

其中最后一行输出的结果为 "john" ,这说明strtok改变了原始字符串,并在识别到NUL后函数终止.

字符串字面量

对于程序中的字符串临时变量,C++有一套特殊的处理方式,将其视为 字符串字面量(string literal) .字符串字面量实际上存储在内存的只读部分,也就是说,即使一个程序使用了500次"hello"字符串自变量,编译器也只在内存中创建一个hello实例.这种技术成为 字面量池(literal pooling).

这也意味着,字符串字面量必须为 const char* 而非 char* ,因为如果一个指向字符串字面量的指针被通过其改变了其内存,将直接试图对字面量池中的只读内存进行修改.

char* prt{"hello"};
ptr[1]='a';//这是未定义行为

这样的行为是迷惑而又危险的,因而成为了一种未定义行为.

然而,由于数组开辟了一块连续的内存对来存储字符串,因而通过数组存储来存储字符串,而再对其进行修改是合法的.

char[] prt{"hello"};
ptr[1]='a'//这是合法的

一般而言,经常使用python的程序员通常会对 原始字符串字面量 这个概念不陌生,就像他们在python中所做的那样,C++也通过 前缀大写R 的方式来标记字符串字面量为原始字符串字面量.

原始字符串字面量中所有的转义符都不会被对应解释.

const char* str1{"Hello \"world\""};
const char* str2{R"(Hello "world")"};
//在原始字符串字面量中的转义符不被解释
const char* str3{"Hello\nworld"};
const char* str4{R"(Hello
world)"};

与之相似的,C++存在另一个字符串有关的标记, 长字符串字面量 通过一个 前缀大写L 或者宏函数 TEXT() 来实现.

长字符串字面量的每一个字符都占两个字节大小.这是UNICODE编码的.

_T 宏可以把一个引号引起来的字符串,根据你的环境设置,使得编译器会根据编译目标环境选择合适的(Unicode或ANSI)字符处理方式

L"string"
TEXT("string")

考虑到很多api都需要用到UNICODE编码的字符串,C++从C语言那继承来了一套配套的类字符串结构

其中 LP 的含义是长指针(long pointer), C 的含义是常量(constant), W 的含义为宽字符(wchar_t), T 的含义为_T宏.

  • LPSTR:一般长指针,可以与char*互换使用
  • LPTSTR:根据是否定义UNICODE来解释的长指针
  • LPWSTR:宽字符长指针,使用UNICODE编码
  • LPCSTR:常量长指针,表明这种实例的内容无法被改变
  • LPCWSTR:常量宽字符长指针,结合了LPCSTR,LPWSTR
  • LPCTSTR:结合了类型解释的常量长指针

上述相关类型的定义都在 windows.h 中.

与一般C风格字符串字面量相对,C++提供了一套用户自定义的标准库字符串字面量.

using namespace std::literals::string_literals;
using namespace std::literals::string_view_literals;

auto str1{"Hello World"};//将被解释为const char*
auto str2{"Hello World"s};//将被解释为std::string
auto str3{"Hello World"sv};//将被解释为std::string_view

通过如此的显式声明,可以避免由于类模板参数推导(CTAD)造成的数据类型问题.

std::vector names{"John","Wilson","Yao"};
//错误,将会导致解释为const char*,带来不可预料的后果
std::vector names{"John"s,"Wilson"s,"Yao"s};
//正确,CTAD将会使该std::vector存储std::string型对象

std::string简介

尽管C++中string只是个类,但是几乎总可以将string当成内建类型使用.

字符串的基本使用:

std::string str1{"Hello"};
std::string str2{" world"};
std::string str3=str1+str2;

std::string相关的操作

  • std::string::size(): 用于获得字符串的字符数
  • std::string::length(): 用于获得字符串长度
  • std::string::capacity(): 用于获得字符串容量
  • std::string::empty(): 用于判断字符串是否为空
  • std::string::data(): 用于返回字符串内部的字符数组
  • std::string::c_str(): 用于返回字符串内部的C风格字符串
  • std::string::substr(): 用于返回从给定位置开始的给定长度的子串
  • std::string::assign(): 用于从另一字符串赋值给该字符串
  • std::string::insert(): 用于插入子串至字符串中
  • std::string::append(): 用于在末尾拼接字符串
  • std::string::push_back(): 用于在末尾添加字符
  • std::string::pop_back(): 用于弹出末尾字符
  • std::string::erase(): 用于删除某段字符串
  • std::string::at(): 用途与operator[]相同,但是会检查范围
  • std::string::swap(): 将字符串与另一个字符串进行swap操作
  • std::string::find(): 用于在字符串查找某个子串
  • std::string::replace(): 用于用一个字符串替换该字符串的部分
  • std::string::reserve(): 用于翻转一个字符串
  • std::string::starts_with(): 用于判断字符串是否为给定前缀
  • std::string::ends_with(): 用于判断字符串是否为给定后缀

std::string作为一种标准库容器,在上面的操作外,也支持相关迭代器的操作

注意
自C++20开始,std::string被定义为constepr类.这意味着其可以在编译器使用

考虑到一般使用情景,事实上, std::string::find() 的使用通常是频繁而又让人摸不着头脑的,下面我们将会通过一系列例子来解释该函数的用法.

一般而言,为了查找到字符串中的某个子串,我们会考虑使用标准库提供的 std::find() 函数,或者标准库提供的 std::string::find() 方法.在实际的使用上,二者存在一定的不同.

std::find为通用算法函数,可以用于任何容器的查找

下面给出二者的函数/方法原型:

template<class InputIt, class T>
constexpr InputIt find(InputIt first, InputIt last, const T& value);
//std::find()
size_t find(const std::string& str, size_t pos = 0) const noexcept;
//std::string::find()

一般在子串的查找时,我们更倾向于使用 std::string::find() 方法.

std::string str{"john.wilson.yao@gmail.com"};
unsigned int i = str.find("-");
if (i==std::string::npos())
	std::cout<<"Not Found";
else
	std::cout<<"Found";
std::string str{"john.wilson.yao@gmail.com"};
unsigned int i = str.find("wilson");
std::cout<<i;//5(为首字符w的下标索引)
//std::string::find()得到子串首字符下标

std::string::iterator i = std::find(strin.begin(), strin.end(), 'e');
std::cout<<*++i;//l(为字符e后的一个字符)
//std::find()得到一个指向目标字符的迭代器
//std::find()无法对子串进行查找

通过上面的例子,我们不难发现,std::string::find()方法在找到目标子串后返回 目标子串首字母的下标;在没找到目标子串时,返回 std::string::npos()

字符串有关类型转换

在实际解决问题时,我们常常遇到一些需求,要求我们进行数据类型的转换

C++专门提供了一些函数与方法对字符串进行处理

数值转换为字符串

通常来说数值转换为字符串的需求都可以使用 std::to_string 函数来完美解决,不论是整数,负数还是浮点数.

std::cout << std::format("{}\n", std::to_string(1));//1
std::cout << std::format("{}\n", std::to_string(-1));//-1
std::cout << std::format("{}\n", std::to_string(4.5));//4.500000
std::cout << std::format("{}\n", std::to_string(-1.4e2));//-140.000000

而在C++17引入的低级数值转换库 <charconv> 中,C++给出了更为高效的解决方案,即 std::to_chars()

下面给出该函数及相关结构体原型.

//to_chars()
to_chars_result to_chars(char* first,char* last,IntegerT value,int base=10);
to_chars_result to_chars(char* first,char* last,FloatT value,chars_format format,int precision);

//to_chars_result
stuct to_chars_result{
	char* ptr;
	errc ec;
};
//chars_format
enum class chars_format{
	scientific,//(-)d.ddde±dd
	fixed,//(-)ddd.ddd
	hex,//(-)h.hhhp±d
	general = fixed|scientific//默认,将根据哪种最短而决定
}

而下面再给出一个使用 std::to_chars_result 解决类型转换问题的例子

当result中ec与std::errc{}相等时,表明转换成功

double num {-12.5};
char arr[30];
memset(arr, 0, sizeof(arr));
auto result { std::to_chars(arr, arr + 30, num) };
if (result.ec == std::errc{})
    printf("%s", arr);
//12.5

也可以借助 std::string::data() 来将其存储在 std::string

double num {-12.5};
std::string out{ "temp_string" };
auto result { std::to_chars(out.data(), out.data() + out.size() , num)};
if (result.ec == std::errc{})
    std::cout << out;
//-12.5string

不过很明显,通过这种方式获得的结果似乎与我们所希望的存在一些不同.

字符串转换为数值

考虑到数值通常有多种类型与情况,字符串转换为数值则有较多相关函数,它们通常直接定义在std下,如std::atoi()

  • int stoi(const string& str,size_t idx=0, int base=10)*
  • long stol(const string& str,size_t idx=0, int base=10)*
  • unsigned long stoul(const string& str,size_t idx=0, int base=10)*
  • long long stoll(const string& str,size_t idx=0, int base=10)*
  • unsigned long long stoull(const string& str,size_t idx=0, int base=10)*
  • float stof(const string& str,size_t idx=0)*
  • double stod(const string& str,size_t idx=0)*
  • long double stold(const string& str,size_t idx=0)*

str为目标字符串;idx是一个指针,接受第一个未转换的字符的索引;base表示转换过程用的进制

若base被设为0,则函数会按下面的规则自动计算

  1. 如果以0x开头,则解析为十六进制
  2. 如果以0开头,则解析为八进制
  3. 其他情况下被解析为十进制数字

上述函数在进行转换时,如果不能执行任何转换,这些函数会抛出 invalid_argument异常 ;如果转换的值超出返回类型的范围,则抛出 out_of_range异常.

同时,如同数值转换为字符串有低级方案,字符串转换为数值也同样有一个更为高效的方案: <charconv> 中的 std::from_chars

下面给出该函数及相关结构体原型.

//fram_chars
from_chars_result from_chars(const char* first,const char* last,IntegerT& value,int base=10);
from_chars_result from_chars(const char* first,const char* last,FloatT& value,chars_format format=std::chars_format::general);

//from_chars_result
struct from_chars_result{
	const char*ptr;
	errc ec;
};

于是下面就可以就 std::from_charsstd::to_chars 的对偶关系进行验证了

const int BufferSize = 64;
double value1{ 0.314 };
std::string out( BufferSize,' ' );
auto [ptr1, error1] {std::to_chars(out.data(), out.data() + out.size(), value1)};
if (error1 == std::errc{})
std::cout << out << std::endl;
//0.314

double value2;
auto [ptr2, error2] {std::from_chars(out.data(), out.data() + out.size(), value2)};
if (error2 == std::errc{})
    if (value1 == value2)
        std::cout << "Perfect roundtrip" << std::endl;
    else
        std::cout << "No perfect roundtrip?!?" << std::endl;
//Perfect roundtrip

结果很好地说明了我们的这两种操作并不存在问题.

std::string_view类

在C++17前,为接收只读字符串的函数选择形参类型是进退两难的一件事情,但是 std::string_view 的出现很好的解决了这个问题.

作为 const std::string& 的替代品,定义在 <string_view> 中的 std::string_view 很好避免了复制导致的开销.

下面以一个例子来引入 std::string_view

std::string_view extractExtension(std::string_view filename)
{
	return filename.substr(filename.rfind('.'));
}

该函数可以用于所有类型的不同字符串,体现了std::string_view强大的适配能力

std::string filename{R"(c:\temp\my file.ext)"};
std::cout<<std::format{"C++ string:{}",extractExtension(filename)}<<std::endl;

const char* cString{R"(c:\temp\my file.ext)"};
std::cout<<std::format{"C string:{}",extractExtension(cString)}<<std::endl;

std::cout<<std::format{"Literal:{}",extractExtension(R"(c:\temp\my file.ext)")}<<std::endl;

std::cout<<std::format("Raw:{}",extractExtension(std::string_view{raw,lenght}))<<std::endl;

注意
无法从std::string_view隐式构建一个std::string,药没用一个显式的std::string构造函数,要么使用std::string_view::data()成员.
返回字符串的函数应该返回const std::string&或者std::string,但是不应该返回std::string_view,返回std::string_view会导致回放的string_view对象无效的风险
将const std::string&对象或者std::string_view对象作为类的数据成员时,需要确保它们指向的字符串在对象生命周期保持有效状态
std::string_view不应该保存一个临时字符串的视图

字符串格式化

直到C++20前,字符串的格式化一般是通过 printf(),sprtintf(),std::stringstraem 来实现的
C++20引入了 std::format() ,定义在 <format> 中.

std::format基本用法

std::format的基础用法如下:

int num = 10;
std::cout<<std::format("the num is {}",num);//10

std::format除自动索引外,还可以使用显式索引,如下:

std::cout<<std::format("Read {1} bytes,from {0}","file1.txt",n);
//Read n bytes,from file1.txt

格式说明符

格式说明符用于控制值在输出中的格式,前缀为冒号,格式为:

{[手动索引]:[[填充字符]对齐方向][符号选项][#][0][宽度][.小数精度][L][数据类型]}

下面对每个可选的参数进行说明:

  • 手动索引: 手动选择索引的参数下标
  • 对齐方向:
    • <: 表示左对齐
    • ^: 表示居中对齐
    • >: 表示右对齐
    • 填充字符: 对齐过程中长度不够补充的字符
  • 符号选项:
    • -: 表示只显示负数的符号(默认)
    • +: 表示显示正数与负数的符号
    • space:: 表示对于负数使用符号,对于正数使用空格
  • #: 启用所谓的备用格式规则(即对特殊进制使用0x,0)
  • 0: 0表示,对于数字,将0插入格式化结果中,以达到[width]指定的最小宽度
  • 宽度: 指定的字符串的宽度
  • 小数精度: 仅用于浮点与字符串类型,表示输出小数位数
  • L: 与L宏相似,用于UNICODE编码
  • 数据类型:
    • b: 二进制输出整数
    • B: 当指定#时用0X替代0x
    • d: 十进制输出整数
    • o: 八进制输出整数
    • x: 小写形式的十六进制整数
    • X: 大写形式的十六进制整数
    • e: 以小写e标明的科学表示法
    • E: 以大写E标明的科学表示法
    • f: 浮点数输出
    • c: 字符输出
    • s: 字符串输出
    • p: 指针输出

std::format的格式说明符需要遵循严格的规则,如果格式说明符包含错误,将抛出 std::format_error 异常