当前位置: 首页 > article >正文

使用string和string_view(一)——C风格字符串、字符串字面量和std::string

目录

  • 1. C风格字符串
  • 2. 字符串字面量
    • 2.1 简介
    • 2.2 原始字符串字面量
  • 3. std::string
    • 3.1 C风格字符串的问题
    • 3.2 使用string类
    • 3.3 字符串比较
    • 3.4 内存处理
    • 3.5 与C风格字符串的兼容
    • 3.6 std::string字面量
    • 3.7 std::vector和字符串的CTAD
  • 参考

1. C风格字符串

  在C语言中,字符串表示为字符的数组。字符串中的最后一个字符是null字符(\0),这样,操作字符串的代码就知道字符串在哪里结束。官方将这个null字符定义为NUL,这个拼写中只有一个L而不是两个L。NUL和NULL指针是两回事。尽管C++提供了更好的字符串抽象,但理解C语言中使用的字符串技术非常重要,因为在C++程序设计中仍可能使用这些技术。最常见的一种情况是C++程序调用某个第三方库或与操作系统交互时调用基于C语言的接口。
  目前,程序员使用C字符串时最常犯的错误是忘记为\0字符分配空间。例如,字符串"hello"看上去有5个字符长,但在内存中需要6个字符的空间才能保存这个字符串的值。
在这里插入图片描述
  C++包含一些来自C语言的字符串操作函数,它们在<cstring>头文件中定义。通常,这些函数不直接操作内存分配。例如,strcpy()函数有两个字符串参数。这个函数将第二个字符串复制到第一个字符串,而不考虑第二个字符串能否恰当地填入第一个字符串。下面的代码试图在strcpy()函数之上构建一个包装器,这个包装器能够分配正确数量的内存并返回结果,而不是接收一个已经分配好的字符串。这个最初的尝试会被证明是错误的,函数通过strlen()函数获取字符串的长度。调用者负责释放由copyString()分配的内存。

char* copyString(const char* str) {
	char* result { new char[strlen(str)] }; // bug! off by one!
	strcpy(result, str);
	return result;
}

  copyString()函数的代码这样写是不正确的。strlen()函数返回字符串的长度,而不是保存这个字符串所需的内存量。对于字符串"hello",strlen()返回的是5,而不是6。为字符串分配内存的正确方式是在实际字符所需的空间加1>。一开始看到到处都需要加1可能会感到有点奇怪,但这是其工作方式,所以在使用C风格字符串时要记住这一点。正确实现的代码如下:

char* copyString(const char* str) {
	char* result { new char[strlen(str) + 1] };
	strcpy(result, str);
	return result;
}

  要记住strlen()只返回字符串中实际字符数的一种方式是:考虑如果为一个由几个其他字符串构成的字符串分配空间,应该怎么做。例如,如果函数接收3个字符串参数,并返回一个由这3个字符串串联而成的字符串,那么这个返回的字符串应该有多大?为精确分配足够空间,空间的大小应该是3个字符串的长度相加,然后加上1留给尾部的\0字符。如果strlen()的字符串长度包括\0,那么分配的内存就会过大。下面的代码通过strcpy()和strcat()函数执行这个操作:

char* appendStrings(const char* str1, const char* str2, const char* str3) {
	char* result { new char[strlen(str1) + strlen(str2) + strlen(str3) + 1] };
	strcpy(result, str1);
	strcat(result, str2);
	strcat(result, str3);
	return result;
}

  C和C++中的sizeof()操作符可用于获得给定数据类型或变量的大小。例如,sizeof(char)返回1,因为字符的大小是1字节。但在C风格字符串中,sizeof()和strlen()是不同的。绝对不要通过sizeof()获取字符串的大小。它根据C风格的字符串的存储方式来返回大小。如果C风格字符串存储为char[],则sizeof()返回字符串使用的实际内存,包括\0字符。例如:

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

  但是,如果C风格字符串存储为char*,sizeof()就返回指针的大小!例如:

const char* text2 { "abcdef" };
size_t s3 { sizeof(text2) }; // is platform-dependent.
size_t s4 { strlen(text2) }; // is 6.

  在32位模式下编译期,s3的值为4;而在64位模式下编译期,s3的值为8,因为这返回的是指针const char*的大小。
  警告:在Microsoft Visual Studio中使用C风格字符串函数时,编译器可能会给出安全相关的警告甚至错误,说明这些函数已经被弃用了。使用其他C标准库函数可以避免这些警告,例如strcpy_s()和strcat_s(),这些函数是安全C库(ISO/IEC TR 24731)标准的一部分。然而,最好的解决方案是切换到C++的string类

2. 字符串字面量

2.1 简介

  注意,C++程序编写的字符串要用引号包围。例如,下面的代码输出字符串hello,这段代码包含这个字符串本身,而不是一个包含这个字符串的变量。
  std::cout << "hello" << std::endl;
  在上面的代码中,"hello"是一个字符串字面值,因为这个字符串以值的形式写出,而不是一个变量。字符串字面量实际上存储在内存的只读部分。通过这种方式,编译器可重用相同字符串字面量的引用,从而优化内存的使用。也就是说,即使一个程序使用了500次"hello"字符串字面量,编译器也只在内存中创建一个hello实例。这种技术称为字面量池。
  字符串字面值可赋值给变量,但因为字符串字面量位于内存的只读部分,且使用了字面量池,所以这样做会产生风险。C++标准正式指出:字符串字面量的类型为n个const char数组,然而为了向后兼容较老的不支持const的代码,大部分编译器不会强制程序将字符串字面量赋值给const char*类型的变量。这些编译器允许将字符串字面量赋值给不带有const的char*,而且整个程序可正常运行,除非试图修改字符串。一般情况下,试图修改字符串字面量的行为是没有定义的。可能会导致程序崩溃;可能使程序继续执行,看起来却有莫名其妙的副作用;可能不加通告地忽略修改行为;可能修改行为是有效的,这完全取决于编译器。例如,下面的代码展示了未定义的行为:

char* ptr { "hello" }; // assign the string literal to a variable.
ptr[1] = 'a'; // undefined behavior!

  一种更安全的编码方式是在引用字符串常量时,使用指向const字符的指针。下面的代码包含同样的bug,但由于这段代码将字符串字面量赋值给const char*,因此编译器会捕获到任何写入只读内存的企图。

const char* ptr { "hello" }; // assign the string literal to a variable.
ptr[1] = 'a'; // error! attempts to write to read-only memory.

  还可将字符串字面量用作字符数组char[]的初始值。这种情况下,编译器会创建一个足以放下这个字符串的数组,然后将字符串复制到这个数组。因此,编译器不会将字面量放在只读的内存中,也不会进行字面量池的操作。

char arr[] { "hello" }; // compiler takes care of creating appropriate sized
						// character array arr.
arr[1] = 'a'; 			// the contents can be modified.

2.2 原始字符串字面量

  原始字符串字面量是可横跨多行代码的字符串字面量,不需要转义嵌入的双引号,像\t和\n这种转义序列不按照转义序列的方式处理,而是按照普通文本的方式处理。如果像下面这样编写普通的字符串字面量,那么会收到一个编译器错误,因为字符串中包含了未转义的双引号。
  const char* str { "Hello "World"!" }; / error!
  对于普通字符串,必须转义双引号,如下所示。
  const char* str { "Hello \"World\"!" };
  对于原始字符串字面量,就不需要转义双引号了。原始字符串字面量以R“(开头,以)"结尾。
  const char* str { R"(Hello "World"!)" };
  如果需要一个包含多行的字符串,不使用原始字符串字面量的话,就需要在字符串中新行的开始位置嵌入\n转义序列。例如:
  const char* str { "Line 1\nLine 2" };
  如果将这个字符串输出到控制台,将看到以下结果。

Line 1
Line 2

  而使用原始字符串字面量,不使用\n转义序列来开始一个新行,只需要在源代码中按下Enter键以开始一个真正的新行。这与前面使用嵌入的\n的代码片段的效果相同。

const char* str { R"(Line 1
Line 2)" };

  在原始字符串字面量中忽略了转义序列。例如,在下面的原始字符串字面量中,\t转义序列没有替换为实际的制表符字符,而是按照字面形式保存。
  const char* str { R"(Is the following a tab character? \t)" };
  因此,如果将此字符串输出到控制台,将看到以下结果。

Is the following a tab character? \t

  因为原始字符串字面量以)“结尾,所以使用这种语法时,不能在字符串中嵌入)”。例如,下面的字符串是不合法的,因为在这个字符串的中间包含)“。
  const char* str { R"(Embedded )" characters)" }; // error!
  如果需要嵌入)”,则需要使用扩展的原始字符串字面量语法,如下所示。
  R"d-char-sequence(r-char-sequence)d-char-sequence"
  r-char-sequence是实际的原始字符串。d-char-sequence是可选的分隔符序列,原始字符串首尾的分隔符序列应该一致。分隔符序列最多能有16个字符。应选择未出现在原始字符串字面量中的序列作为分隔符序列。
  上例可改用唯一的分隔符序列。
  const char* str { R"-((Embedded )" characters)-" };
  在操作数据库查询字符串、正则表达式和文件路径时,原始字符串字面量可以令程序的编写更加方便。

3. std::string

  C++提供了一个得到极大改善的字符串概念,并作为标准库的一部分提供了这个字符串的实现。在C++中,std::string是一个类(实际上是basic_string模板类的一个实例),这个类支持<cstring>中提供的许多功能,还能自动管理内存分配。string类在std名称空间的<string>头文件中定义。

3.1 C风格字符串的问题

  为理解C++ String类的必要性,需要考虑C风格字符串的优势和劣势。
  优势:
  1. 很简单,底层使用了基本的字符类型和数组结构。
  2. 轻量级,如果使用得当,只会占用所需的内存。
  3. 可按操作原始内存的方式轻松操作和复制字符串。
  4. 如果你是一名C语言程序员,为什么还要学习新事物?
  劣势:
  1. 为了模拟一等数据类型字符串,需要付出很多努力。
  2. 很容易产生难以找到的内存bug,且难以解决。
  3. 没有利用C++的面向对象特性。
  4. 要求程序员了解底层的表示方式。
  C++的String类解决了C风格字符串的所有问题,并且证明C风格字符串相比一等数据类型的那些优势实际上是无关紧要的。

3.2 使用string类

  尽管string是一个类,但是几乎总可将string当成内建类型使用。事实上,把string想象成简单类型更容易发挥string的作用。通过运算符重载的神奇作用,C++的string使用起来比C字符串容易得多。例如,给string重新定义+运算符,以表示字符串串联。下面的例子会得到1234:

std::string a { "12" };
std::string b { "34" };
std::string c;
c = a + b; // c is "1234".

  +=运算符也被重载了,通过这个运算符可以轻松地追加一个字符串。
  a += b; // a is "1234".

3.3 字符串比较

  C风格字符串的另一个问题是不能通过==运算符进行比较。假设有以下两个字符串。

char* a { "12" };
char b[] { "12" };

  按照下述方式编写的比较操作始终返回false,因为它比较的是指针的值,而不是字符串的内容。

if ( a == b ) {
	/* ... */
}

  注意C数组和指针是相关的。可将C数组看成指向数组中第一个元素的指针。
  要比较C字符串,需要编写这样的代码:

if ( strcmp(a, b) == 0 ) {
	/* ... */
}

  此外,C字符串也无法通过<、<=、>=或>进行比较,因此需要通过strcmp()根据字符串的字典顺序返回-1、0和1的值判断。这样会产生非常笨拙且可读性低的代码,还很容易出错。
  在C++的String类中,操作符都被重载了,这些运算符可以操作真正的字符串字符。单独的字符可通过方括号运算符[]访问。
  C++ string类另外提供了一个compare()方法,它的行为类似于strcmp()并且具有类似的返回类型。实例如下:

std::string a { "12" };
std::string b { "34" };

auto result { a.compare(b) };
if ( result < 0 ) {
	std::cout << "less\n";
} else if ( result = 0 ) {
	std::cout << "equal\n";
} else {
	std::cout << "greater\n";
}

  与strcmp()一样,这样使用起来很麻烦,你需要记住返回值的确切含义。此外,由于返回值只是一个整数,很容易忘记这个整数的含义,写出以下的错误代码来比较相等性。

if ( a.compare(b) ) {
	std::cout << "equal\n";
}

  compare()为相等返回0,为不相等返回任何其他值。所以这行代码与它的意图相反,也就是说,它对不相等的字符串输出"equal"!如果只想检查两个字符串是否相等,不要使用compare(),只需要使用==。
  C++20通过三向比较运算符改进了这一切。字符串类完全支持这个运算符,示例如下:

auto result { a <=> b };
if ( std::is_lt(result) ) {
	std::cout << "less\n";
}
if ( std::is_gt(result) ) {
	std::cout << "greater\n";
}
if ( std::is_eq(result) ) {
	std::cout << "equal\n";
}

3.4 内存处理

  如下面的代码所示,当string操作需要扩展string时,string类能够自动处理内存需求,因此不会出现内存溢出的情况了。

std::string myString { "hello" };
myString += ", there";
std::string myOtherString { myString };
if ( myString == myOtherString ) {
	myOtherString[0] = 'H';
}

std::cout << myString << "\n";
std::cout << myOtherString << "\n";

  这段代码的输出如下所示:

hello, there
Hello, there

  在这个例子中有几点需要注意。一是注意即使字符串被分配和调整大小,也不会出现内存泄漏的情况。所有这些string对象都创建为栈中的变量。尽管string类肯定需要完成大量分配内存和调整大小的工作,但是string类的析构函数会在string对象离开作用域时清理内存。
  另外需要注意的是,运算符以预期的方式工作。例如,=运算符复制字符串,这是最有可能预期的操作。如果习惯使用基于数组的字符串,那么这种方式有可能带来全新的体验,也可能令你迷惑。不用担心,一旦学会信任string类能做出正确的行为,那么代码的编写会简单得多。

3.5 与C风格字符串的兼容

  string类支持一系列其他的操作,以下是一部分:
  1. substr(pos, len):返回从给定位置开始的给定长度的子字符串。
  2. find(str):如果找到了字符串,返回它的位置;如果没有找到,返回string::npos。
  3. replace(pos, len, str):将字符串的一部分(给定开始位置和长度)替换为另一个字符串。
  4. starts_with(str)/ends_with(str):如果一个字符串以给定的子串开始或结尾,则返回true。
  下面是一个小代码片段,展示了其中的一些操作。

std::string strHello { "Hello!!" };
std::string strWorld { "The World..." };
auto position { strHello.find("!!") };
if ( position != std::string::npos ) {
	// found the "!!" substring, now replace it.
	strHello.replace(position, 2, strWorld.substr(3, 6));
}
std::cout << strHello << "\n";

  输出如下:

Hello World

  注意:从C++20开始,std::string是constexpr类,这意味着string可用于在编译期执行操作,并可用于constexpr函数和类的实现。

3.6 std::string字面量

  源代码中的字符串字面量通常解释为const char*。使用用户定义的标准字面量s可以把字符串字面量解释为std::string。例如:

auto string1 { "Hello World" }; // string1 is a const char*.
auto string2 { "Hello World"s }; // string2 is an std::string.

在这里插入图片描述
  标准用户定义字面量s在std::literals::string_literals名称空间中定义。但是string_literals和literals名称空间都是所谓的内联名称空间。因此使用以下选项可以使这些字符串字面量可用于你的代码。

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

  基本上,在内联名称空间中声明的所有内容都会自动在父名称空间可用。要自己定义内联名称空间,可以使用inline关键字。例如,string_literals内联名称空间定义如下:

namespace std {
	inline namespace literals {
		inline namespace string_literals {
			// ...
		}
	}
}

3.7 std::vector和字符串的CTAD

  std::vector支持类模板参数推导CTAD,允许编译器根据初始化列表自动推导vector的类型,对字符串vector使用CTAD时必须小心。以vector的以下声明为例:

std::vector names { "John", "Sam", "Joe" };

  推导出的类型将是vector<const char*>,而不是vector<std::string>!这是一个很容易犯的错误,可能导致代码出现一些奇怪的行为,甚至崩溃。这取决于之后对vector的处理方式。
  如果你需要一个vector<std::string>,可以使用上面一节提到的std::string字面量。注意下例中每个字符串字面量后面的s:
  std::vector names { "John"s, "Sam"s, "Joe"s };

参考

[比] 马克·格雷戈勒著 程序喵大人 惠惠 墨梵 译 C++20高级编程(第五版)


http://www.kler.cn/a/612065.html

相关文章:

  • 批量将 PDF 转换为 Word/PPT/Txt/Jpg图片等其它格式
  • 开发DOM更新算法
  • [python]基于yolov8实现热力图可视化支持图像视频和摄像头检测
  • CentOS 7安装 mysql
  • 老是忘记package.json,备忘一下 webpack 环境下 Vue Cli 和 Vite 命令行工具对比
  • 【Pandas】pandas Series to_xarray
  • SpringBoot集成腾讯云OCR实现身份证识别
  • 【牛客网】数据分析笔试刷题
  • Charles抓HTTPS包
  • 数据结构:汉诺塔问题的递归求解和分析
  • 部分 Bash 内置命令的详解
  • 企业网站源码HTML成品网站与网页代码模板指南
  • 学习记录-Ajax-自封装axios函数
  • RAMS(区域大气建模系统)评估土地利用/覆被变化的气候与水文效应
  • 【Django】教程-3-数据库相关介绍
  • NVIDIA Megatron Core:大规模语言模型训练与部署框架详解
  • [250325] Claude AI 现已支持网络搜索功能!| ReactOS 0.4.15 发布!
  • 英语不好,可以考取Oracle OCP认证吗?
  • HO与OH差异之Navigation三
  • Android第六次面试总结(自定义 View与事件分发)