17-10 C 风格字符串
在第17.7节——C风格数组简介中,我们介绍了C风格数组,它允许我们定义一组顺序排列的元素:
int testScore[30] {}; // an array of 30 ints, indices 0 through 29
在第5.2节——字面量中,我们将字符串定义为连续字符的集合(例如“Hello, world!”),并介绍了C风格字符串字面量。我们还指出,C风格字符串字面量“Hello, world!”的类型为const char[14](13个显式字符加1个隐藏的空终止符)。
若此前尚未领悟,此刻应已明了:C 风格字符串本质上就是元素类型为 char 或 const char 的 C 风格数组!
尽管 C 风格字符串常量在代码中仍可使用,但 C 风格字符串对象在现代 C++ 中已逐渐失宠——因其使用困难且存在风险(现代替代方案为 std::string 和 std::string_view)。尽管如此,在旧代码中仍可能遇到C风格字符串对象的使用场景,若完全忽略它们则有失偏颇。
因此本节课将重点解析现代C++中关于C风格字符串对象的核心要点。
定义C风格字符串
要定义C风格字符串变量,只需声明一个C风格的char(或const char/constexpr char)数组变量:
char str1[8]{}; // an array of 8 char, indices 0 through 7
const char str2[]{ "string" }; // an array of 7 char, indices 0 through 6
constexpr char str3[] { "hello" }; // an array of 6 const char, indices 0 through 5
请注意,我们需要额外添加一个字符来表示隐式空终止符。
在使用初始化器定义C风格字符串时,我们强烈建议省略数组长度,让编译器自动计算长度。这样即使初始化器在未来发生变化,您也无需记得更新长度,同时避免了忘记添加额外元素来容纳空终止符的风险。
C 风格字符串会衰变
在第 17.8 课——C 风格数组衰变中,我们讨论了 C 风格数组在大多数情况下会衰变为指针。由于C风格字符串本质是C风格数组,它们同样会衰变——C风格字符串常量会衰变为const char,而C风格字符串数组则根据数组是否为const,衰变为const char或char*。当C风格字符串衰变为指针时,字符串长度(编码在类型信息中)将丢失。
这种长度信息的丢失正是C风格字符串采用空终止符的原因。字符串长度可通过统计字符串开头至空终止符之间的元素数量来(低效地)重建。
输出C风格字符串
输出C风格字符串时,std::cout会持续输出字符直至遇到空终止符。该空终止符标记字符串结束位置,从而使衰变字符串(即已丢失长度信息的字符串)仍可被打印出来。
#include <iostream>
void print(char ptr[])
{
std::cout << ptr << '\n'; // output string
}
int main()
{
char str[]{ "string" };
std::cout << str << '\n'; // outputs string
print(str);
return 0;
}

若尝试打印未以空字符结尾的字符串(例如因空字符被覆盖所致),则会导致未定义行为。这种情况下最可能的结果是:字符串中的所有字符都被打印出来后,程序会继续将相邻内存单元中的内容(被解释为字符)持续输出,直到遇到包含字节值为0的内存单元(该值将被解释为空终止符)为止!
输入C风格字符串
假设我们要求用户掷骰子任意多次,并将掷出的数字连续输入(例如524412616)。用户会输入多少个字符?我们无法预知。
由于C风格字符串是固定大小的数组,解决方案是声明一个远大于实际需求的数组:
#include <iostream>
int main()
{
char rolls[255] {}; // declare array large enough to hold 254 characters + null terminator
std::cout << "Enter your rolls: ";
std::cin >> rolls;
std::cout << "You entered: " << rolls << '\n';
return 0;
}


在 C++20 之前,std::cin >> 操作符会尽可能多地提取字符到 rolls 数组(在遇到第一个非首字符空格时停止)。没有任何机制阻止用户输入超过 254 个字符(无论是无意还是恶意)。若发生这种情况,用户的输入将导致 rolls 数组溢出,从而引发未定义行为。
关键洞察:
数组溢出Array overflow或缓冲区溢出buffer overflow是计算机安全问题,指存储空间被超容量数据覆盖的情况。此时存储区后方的内存将被覆盖,导致行为未定义。恶意攻击者可能利用此漏洞覆盖内存内容,试图以某种有利方式改变程序行为。
在C++20中,>>运算符被修改为仅支持输入未衰变的C风格字符串。这使得>>运算符只能提取不超过C风格字符串长度的字符,从而防止溢出。但这也意味着您无法再使用>>运算符向衰变的C风格字符串输入数据。
使用std::cin读取C风格字符串的推荐方式如下:
#include <iostream>
#include <iterator> // for std::size
int main()
{
char rolls[255] {}; // declare array large enough to hold 254 characters + null terminator
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更为安全,因为该类型会自动调整以容纳所需字符数量。
修改C风格字符串
需要注意的一点是,C风格字符串遵循与C风格数组相同的规则。这意味着你可以创建字符串时进行初始化,但之后不能使用赋值运算符向其赋值!
char str[]{ "string" }; // ok
str = "rope"; // not ok!]
这使得使用C风格字符串有些不方便。
由于C风格字符串本质上是数组,你可以使用[]运算符来修改字符串中的单个字符:
#include <iostream>
int main()
{
char str[]{ "string" };
std::cout << str << '\n';
str[1] = 'p';
std::cout << str << '\n';
return 0;
}
该程序输出:

获取C风格字符串的长度
由于C风格字符串本质是C风格数组,可使用std::size()(或C++20中的std::ssize())获取字符串作为数组的长度。需注意两点:
此方法对衰变字符串无效。
返回的是C风格数组的实际长度,而非字符串本身的长度。
#include <iostream>
int main()
{
char str[255]{ "string" }; // 6 characters + null terminator
std::cout << "length = " << std::size(str) << '\n'; // prints length = 255
char *ptr { str };
std::cout << "length = " << std::size(ptr) << '\n'; // compile error
return 0;
}

另一种解决方案是使用strlen()函数,该函数位于
#include <cstring> // for std::strlen
#include <iostream>
int main()
{
char str[255]{ "string" }; // 6 characters + null terminator
std::cout << "length = " << std::strlen(str) << '\n'; // prints length = 6
char *ptr { str };
std::cout << "length = " << std::strlen(ptr) << '\n'; // prints length = 6
return 0;
}

然而,std::strlen() 的运行效率较低,因为它需要遍历整个数组,逐个计数字符直至遇到空终止符。
其他C风格字符串操作函数
由于C风格字符串是C语言中的主要字符串类型,C语言提供了大量用于操作C风格字符串的函数。这些函数作为
以下是旧代码中常见的几个实用函数:
- strlen() -- 返回C风格字符串的长度
- strcpy()、strncpy()、strcpy_s() -- 将一个C风格字符串覆盖到另一个字符串
- strcat()、strncat() () -- 将一个C风格字符串追加到另一个字符串末尾
- strcmp(), strncmp() -- 比较两个C风格字符串(相同时返回0)
除strlen()外,我们通常建议避免使用
避免使用非const C风格字符串对象
除非存在特定且充分的理由,否则应尽量避免使用非const C风格字符串。这类字符串操作不便且易引发溢出,导致未定义行为(并可能引发安全隐患)。
在极少数需要处理C风格字符串或固定缓冲区大小的场景(例如内存受限设备),我们建议使用经过充分测试的第三方固定长度字符串库。
最佳实践:
避免使用非const C风格字符串对象,优先采用std::string。
测验时间
问题 #1
编写一个函数,逐字符打印C风格字符串。使用指针和指针运算遍历字符串的每个字符并打印该字符。编写主函数,使用字符串常量“Hello, world!”测试该函数。

显示解决方案
#include <iostream>
// str will point to the first letter of the C-style string.
// Note that str points to a const char, so we can not change the values it points to.
// However, we can point str at something else. This does not change the value of the argument.
void printCString(const char str[])
{
// While we haven't encountered a null terminator
while (*str != '\0')
{
// print the current character
std::cout << *str;
// and use pointer arithmetic to move str to the next character
++str;
}
}
int main()
{
printCString("Hello world!");
std::cout << '\n';
return 0;
}

问题 #2
重复测验 #1,但本次函数应将字符串倒序打印。

显示解决方案
#include <iostream>
void printCStringBackwards(const char str[])
{
// We can't modify str this time (we need it later)
// So we'll define a new pointer with the same address as str
const char *ptr{ str };
// Find the null terminator
while (*ptr != '\0')
++ptr;
// Now walk backwards and print characters until ptr reaches str again
while (ptr-- != str)
{
std::cout << *ptr;
}
}
int main()
{
printCStringBackwards("Hello world!");
std::cout << '\n';
return 0;
}

浙公网安备 33010602011771号