在《C 风格数组简介》中,我们介绍了 C 风格数组,它允许我们定义一段连续的元素集合:
int testScore[30] {}; // 一个包含 30 个 int 的数组,下标 0 至 29
在《字面量》中,我们将字符串定义为“一串连续字符”(如 "Hello, world!"
),并引入了 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 风格字符串变量,只需声明一个 char
(或 const char
/ constexpr char
)的 C 风格数组:
char str1[8]{}; // 8 个 char 的数组,下标 0–7
const char str2[]{ "string" }; // 7 个 char 的数组,下标 0–6
constexpr char str3[] { "hello" }; // 6 个 const char 的数组,下标 0–5
注意需额外留出一个字符存放隐式空字符终止符。
当初始化 C 风格字符串时,强烈建议省略数组长度而让编译器推导。这样若初始值日后变动,无需手动更新长度,也绝不会忘记为终止符预留空间。
C 风格字符串会退化
在《C 风格数组退化》中,我们讨论过 C 风格数组在大多数情境下会退化为指针。由于 C 风格字符串即 C 风格数组,它们同样会退化:
- 字符串字面量退化为
const char*
; - C 风格字符串数组依其是否
const
退化为const char*
或char*
。
退化后,字符串长度(原本编码在类型中)丢失。
正因长度信息丢失,C 风格字符串才需要空字符终止符:通过计数从字符串开始到空字符之间的字符数,可(低效地)重新获得长度。
输出 C 风格字符串
使用 std::cout
输出 C 风格字符串时,遇到空字符即停止。空字符标记字符串结束,使得已退化、失去长度信息的字符串仍可被打印。
#include <iostream>
void print(char ptr[])
{
std::cout << ptr << '\n'; // 输出字符串
}
int main()
{
char str[]{ "string" };
std::cout << str << '\n'; // 输出 string
print(str);
return 0;
}
若字符串缺少空字符(如空字符被覆盖),则行为未定义。最可能的结果是:打印完字符串后继续把相邻内存解释为字符,直到遇到值为 0 的字节(视作空字符)!
输入 C 风格字符串
假设要求用户掷骰子若干次并连续输入所有结果(如 524412616)。我们无法预知用户会输入多少字符。
由于 C 风格字符串为定长数组,解决方法是声明一个足够大的数组:
#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 字符(无论无意或恶意),数组将溢出,导致未定义行为。
关键洞察
数组溢出(或称缓冲区溢出)是一种安全漏洞:当写入数据超出存储容量时,相邻内存被覆盖,可能造成未定义行为。攻击者可利用此漏洞篡改内存,从而改变程序行为。
自 C++20 起,operator>>
仅能对未退化的 C 风格字符串使用,从而最多提取数组可容纳的字符数,防止溢出;退化后的字符串已无法再用 >>
直接输入。
推荐做法如下:
#include <iostream>
#include <iterator> // std::size
int main()
{
char rolls[255] {};
std::cout << "Enter your rolls: ";
std::cin.getline(rolls, std::size(rolls));
std::cout << "You entered: " << rolls << '\n';
return 0;
}
cin.getline()
最多读取 254 字符(含空格),多余字符被丢弃。由于 getline
接受长度参数,我们可传入数组大小。对未退化数组,可用 std::size()
;退化数组则需另行获知长度;若长度有误,程序可能出错或出现安全隐患。
现代 C++ 中,若需存储用户输入文本,使用 std::string
更安全,因其可自动扩容。
修改 C 风格字符串
C 风格字符串遵循与 C 风格数组相同的规则:创建时可初始化,之后不可再用赋值运算符整体赋值!
char str[]{ "string" }; // 合法
str = "rope"; // 非法!
这使得使用 C 风格字符串颇为不便。
由于它们是数组,可用 []
修改单个字符:
#include <iostream>
int main()
{
char str[]{ "string" };
std::cout << str << '\n';
str[1] = 'p';
std::cout << str << '\n';
return 0;
}
输出:
string
spring
获取 C 风格字符串长度
由于 C 风格字符串即 C 风格数组,可使用 std::size()
(C++20 起亦可用 std::ssize()
)获取数组长度,但有两点注意:
- 仅对未退化字符串有效。
- 返回的是数组总长度,而非字符串逻辑长度。
#include <iostream>
int main()
{
char str[255]{ "string" }; // 6 字符 + 空字符
std::cout << "length = " << std::size(str) << '\n'; // 输出 255
char *ptr { str };
std::cout << "length = " << std::size(ptr) << '\n'; // 编译错误
return 0;
}
另一方案是使用 <cstring>
中的 strlen()
,它对退化数组亦有效,返回不含空字符的字符串长度:
#include <cstring> // std::strlen
#include <iostream>
int main()
{
char str[255]{ "string" };
std::cout << "length = " << std::strlen(str) << '\n'; // 输出 6
char *ptr { str };
std::cout << "length = " << std::strlen(ptr) << '\n'; // 输出 6
return 0;
}
但 strlen()
效率较低,需遍历整个数组直到空字符。
其他 C 风格字符串操作函数
由于 C 风格字符串是 C 的主要字符串类型,C 提供了大量操作函数,C++ 通过 <cstring>
继承:
strlen()
—— 返回字符串长度strcpy(), strncpy(), strcpy_s()
—— 复制字符串strcat(), strncat()
—— 拼接字符串strcmp(), strncmp()
—— 比较字符串(相等返回 0)
除 strlen()
外,其余函数在现代 C++ 中建议避免使用。
避免非常量 C 风格字符串对象
除非有明确且充分的理由,否则应避免使用非常量 C 风格字符串对象:它们使用不便,且极易溢出,导致未定义行为及安全隐患。
若确实需要在固定缓冲区中操作字符串(如内存受限设备),建议使用经过充分测试的第三方定长字符串库。
最佳实践
避免非常量 C 风格字符串对象,优先使用 std::string
。
测验
问题 1
编写函数,逐一字符打印 C 风格字符串,使用指针及指针运算步进。在 main
中用字面量 "Hello, world!"
测试。
问题 2
重做问题 1,但要求反向打印字符串。