章节目录

C样式字符串

本节阅读量:

在前面,我们介绍了C样式数组,它允许我们定义元素的顺序集合:

1
    int testScore[30] {}; // 有30个元素的数组,从0到29

同时在前面学习字面值时,我们将字符串定义为字符的序列(例如“Hello,world!”),并引入了C样式的字符串字面值。我们还注意到,C样式字符串“Hello,world!”的类型为constchar[14](13个显式字符加1个隐藏的空终止符字符)。

如果您以前没有连接这些只是点,那么现在应该很明显,C样式的字符串只是C样式的数组,其元素类型是char或const char!

尽管C样式的字符串字面值可以在我们的代码中使用,但在现代C++中已经不受欢迎,因为它们很难使用,而且很危险(std::string和std::string_view是现代的替代品)。无论如何,您可能仍然会在较旧的代码中遇到C样式字符串对象的使用,我们根本没有涵盖它们。

因此,在本课中,我们将了解有关现代C++中C样式字符串对象的最重要的几点。


定义C样式字符串

要定义C样式字符串变量,只需声明C样式char数组变量(或const char/constexpr char):

1
2
3
4
char str1[8]{};                    // 含 8 个 char 的数组,从 0 到 7

const char str2[]{ "string" };     // 含 7 个 const char 的数组,从 0 到 6
constexpr char str3[] { "hello" }; // 含 6 个 const char 的数组,从 0 到 5

请记住,我们需要隐式添加一个额外的空终止符。

在使用初始值定义C样式字符串时,强烈建议省略数组长度,并让编译器计算长度。这样,如果初始值设定项在将来发生更改,则不必记住更新长度,并且不会有忘记额外的空终止符的风险。


C样式字符串的退化

在上节C样式数组退化中,我们讨论了C样式数组在大多数情况下如何退化为指针。因为C样式的字符串是C样式的数组,所以它们将退化——C样式的字符字面值退化为const char*,C样式的字符串数组退化为const char*或char*,具体取决于数组是否为常量。并且当C样式的字符串退化为指针时,字符串的长度(编码在类型信息中)丢失。

这种长度信息的丢失是C样式字符串具有空终止符的原因。通过计算字符串开头和空终止符之间的元素数,可以重新生成字符串的长度。


输出C样式字符串

输出C样式字符串时,std::cout输出字符,直到遇到空终止符。该空终止符标记字符串的结尾,以便仍然可以打印退化的字符串(已丢失其长度信息)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>

void print(char ptr[])
{
    std::cout << ptr << '\n'; // 打印 string
}

int main()
{
    char str[]{ "string" };
    std::cout << str << '\n'; // 打印 string

    print(str);

    return 0;
}

如果尝试打印没有空终止符的字符串(例如,因为以某种方式覆盖了空终止符),结果将是未定义的行为。在这种情况下,最可能的结果是打印字符串中的所有字符,然后它将只继续打印相邻内存中的所有内容(解释为字符),直到碰巧碰到包含0的内存字节(它将被解释为空终止符)!


输入C样式字符串

考虑这样的情况,我们要求用户根据需要滚动骰子多次,并输入滚动的数字,但不带空格(例如524412616)。用户将输入多少个字符?我们不知道。

由于C样式字符串是固定大小的数组,因此解决方案是声明一个比我们可能需要的更大的数组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>

int main()
{
    char rolls[255] {}; // 声明足够大的数组,可以放254个字符 + 空终止符
    std::cout << "Enter your rolls: ";
    std::cin >> rolls;
    std::cout << "You entered: " << rolls << '\n';

    return 0;
}

在C++20之前,std::cin » rolls将提取尽可能多的字符到rolls(在第一个非前导空格处停止)。没有什么可以阻止用户输入超过254个字符(无意或恶意)。如果发生这种情况,用户的输入将溢出rolls数组,并导致未定义的行为。

在C++20中,更改了运算符»,使其仅适用于输入非退化的C样式字符串。这允许操作符»仅提取C样式字符串长度允许的尽可能多的字符,防止溢出。但这也意味着您不能再使用操作符»来输入退化的C样式字符串。

使用std::cin读取C样式字符串的建议方法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>
#include <iterator> // for std::size

int main()
{
    char rolls[255] {}; // 声明足够大的数组,可以放254个字符 + 空终止符
    std::cout << "Enter your rolls: ";
    std::cin.getline(rolls, std::size(rolls));
    std::cout << "You entered: " << rolls << '\n';

    return 0;
}

对cin.getline()的调用将最多读取254个字符(包括空格)到rolls中。任何多余的字符都将被丢弃。因为getline()需要一个长度,所以我们可以提供可接受的最大字符数。对于非退化数组,这很容易——我们可以使用std::size()来获得数组长度。对于退化数组,我们必须用其他方法确定长度。如果我们提供了错误的长度,程序可能会出现故障或安全问题。

在现代C++中,当存储来自用户的输入字面值时,使用std::string更安全,因为std::string将自动调整以容纳所需数量的字符。


修改C样式字符串

需要注意的一点是,C样式字符串遵循与C样式数组相同的规则。这意味着您可以在创建时初始化字符串,但在此之后不能使用赋值运算符为其赋值!

1
2
char str[]{ "string" }; // ok
str = "rope";           // not ok!

这使得使用C样式的字符串有点尴尬。

由于C样式字符串是数组,因此可以使用[]运算符更改字符串中的单个字符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>

int main()
{
    char str[]{ "string" };
    std::cout << str << '\n';
    str[1] = 'p';
    std::cout << str << '\n';

    return 0;
}

该程序打印:

1
2
string
spring

获取C样式字符串的长度

因为C样式的字符串是C样式的数组,所以可以使用std::size()(或在C++20中,std::ssize())来获取字符串的长度作为数组。这里有两个警告:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>

int main()
{
    char str[255]{ "string" }; // 6 个字符 + 空终止符
    std::cout << "length = " << std::size(str) << '\n'; // 打印 length = 255

    char *ptr { str };
    std::cout << "length = " << std::size(ptr) << '\n'; // 编译失败

    return 0;
}

另一种解决方案是使用strlen()函数,该函数位于<cstring>头文件中。strlen()将在退化数组上工作,并返回所保留的字符串的长度,不包括空终止符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <cstring> // for std::strlen
#include <iostream>

int main()
{
    char str[255]{ "string" }; // 6 个字符 + 空终止符
    std::cout << "length = " << std::strlen(str) << '\n'; // 打印 length = 6

    char *ptr { str };
    std::cout << "length = " << std::strlen(ptr) << '\n';   // 打印 length = 6

    return 0;
}

然而,std::strlen()很慢,因为它必须遍历整个数组,计算字符数,直到到达空终止符。


其他C样式字符串操作函数

因为C样式字符串是C中的主要字符串类型,所以C语言提供了许多用于操作C样式字符串的函数。这些函数已被C++继承为<cstring>头文件的一部分。

下面是在旧代码中可以看到的一些最有用的代码:

  1. strlen()–返回C样式字符串的长度
  2. strcpy(),strncpy(),strcpy_s() –用一个C样式的字符串覆盖另一个
  3. strcat(),strncat() –将一个C样式的字符串附加到另一个字符串的末尾
  4. strcmp(),strncmp() –比较两个C样式的字符串(如果相等,则返回0)

除了strlen(),我们通常建议避免使用这些函数。


避免非常量的C样式字符串对象

除非您有特定的、令人信服的理由使用非常量C样式的字符串,否则最好避免使用它们,因为它们很难使用,并且容易溢出,这将导致未定义的行为(并且是潜在的安全问题)。

在您确实需要使用C样式字符串或固定缓冲区大小(例如,对于内存有限的设备)的罕见情况下,我们建议使用为此目的而设计的经过良好测试的第三方固定长度字符串库。


17.8 指针运算和下标

上一节

17.10 常量C样式字符串

下一节