字符串、内存、字符分类与本地化
本节目标
查询 C23 字符串、内存操作、字符分类、locale、多字节与宽字符接口。
C 的字符串接口建立在字符数组和空字符约定上,内存接口操作的则是对象表示中的字节。进入字符分类、locale、多字节和宽字符接口后,还要同时追踪参数值域、当前 locale 与转换状态。本章按“数据是什么、接口要求什么、结果如何判断”的顺序给出速查边界,不把某一种 C 字符类型直接当作完整的 Unicode 文本模型。
空字符结尾字符串与数组边界
字符数组只有在可访问范围内存在空字符时才可作为空字符结尾字符串传给字符串接口。数组的对象大小、当前字符串长度和剩余容量是三个不同量;拥有 char buffer[32] 不表示其中已经存在有效字符串,也不表示后续拼接一定容纳得下。
判断一个缓冲区能否接收文本时,先取得目标数组容量,再为内容和结尾 \0 分别留出空间。字符串字面量提供带结尾空字符的数组,但修改它的行为未定义;需要修改时,把内容复制进可写数组。读取外部数据后,只有接口合同或程序自己的检查能证明终止空字符存在。
| 任务 | 要确认的事实 | 常用接口或表达式 |
|---|---|---|
| 判断是否有容量 | 内容长度加一不超过目标数组大小 | length + 1 <= sizeof buffer |
| 取得已知有效字符串长度 | 可访问范围内已有 \0 | strlen |
| 保存可修改文本 | 目标是可写数组且容量已核对 | 受控复制或 memcpy |
长度、复制与拼接
strlen 返回字符数而不是目标数组容量。这里的字符数是终止空字符前的 char 元素数(也就是相同数量的字节),不是多字节文本中的 Unicode 码点数或用户可见字符数,并且不计结尾空字符。它会一直查找到 \0;若参数不是有效字符串,调用本身就越过合同,不能用 strlen 去“探测”任意未终止缓冲区。
strcpy 和 strcat 不接收目标容量。strcat 的目标必须已有有效字符串且剩余容量足够,还要为拼接结果的 \0 留位。strncpy 不保证在源过长时写入终止空字符:它按给定数量复制或填充,不是通用的“安全 strcpy”。需要固定容量协议时,应先明确是否允许截断、如何报告截断以及由谁写入终止空字符。
| 任务 | <string.h> 接口 | 关键前置条件 |
|---|---|---|
| 取长度 | strlen | 参数已经是有效字符串 |
| 复制字符串 | strcpy、strncpy | 目标足够且对象不违规重叠 |
| 拼接字符串 | strcat | 目标已有 \0 且剩余容量足够 |
比较、搜索与子串定位
strcmp 按无符号字符值的字典序关系返回小于、等于或大于零的结果;只判断符号,不依赖具体的负数或正数。strchr、strrchr 和 strstr 返回指向原字符串内部的指针,找不到时返回空指针。传入的对象必须是有效字符串,返回指针的生存期和可修改性也不超出原数组。
| 查询任务 | 接口 | 成功结果 |
|---|---|---|
| 比较两个字符串 | strcmp | 以结果符号表示顺序 |
| 查第一个或最后一个字符 | strchr、strrchr | 指向匹配位置的指针 |
| 查子串 | strstr | 指向首个匹配子串的指针 |
搜索到的位置适合计算同一数组内的偏移或在原数组上继续处理;不要在原数组生存期结束后保留该指针,也不要用来自无关数组的指针做减法或顺序比较。
分词与有状态接口
strtok 修改输入,并使用库维护的状态。它把选定分隔符改写为 \0,首次传入可修改字符串,后续传入空指针继续同一分词序列。因此不能传入字符串字面量,也不能假设原文本保持不变。
交错或嵌套调用会干扰已有分词序列;并发调用也不能依赖独立状态或数据竞争安全。需要多条独立解析路径时,优先写一个由调用方显式保存当前位置和边界的解析器,或使用目标实现明确提供且合同满足需求的接口。不要仅凭相似名称把某个平台扩展当作 ISO C 保证。
内存操作与对象重叠
<string.h> 的内存接口按字节处理对象表示,不要求数据以 \0 结尾。调用者负责证明对象范围有效、长度单位正确,并区分重叠与不重叠。
| 任务 | 接口 | 重叠与结果边界 |
|---|---|---|
| 复制不重叠字节范围 | memcpy | 源和目标重叠会违反前置条件 |
| 移动可能重叠的字节范围 | memmove | 像经由临时缓冲区复制 |
| 把每个字节设为同一值 | memset | 值按 unsigned char 转换 |
| 比较两段对象表示 | memcmp | 按前若干字节的无符号值比较 |
对象重叠时必须使用 memmove;memcpy 的对象重叠违反接口前置条件。代表程序删除连字符时,源范围与目标范围属于同一数组且发生重叠,所以用 memmove 连同结尾 \0 一起左移。
memcmp 比较对象表示的字节序列,不能普遍替代结构体或抽象值比较。填充字节、同一值的不同表示以及指针表示都可能让逐字节相等与语义相等分离;抽象数据应按字段合同比较。
字符分类、大小写与合法参数
<ctype.h> 中 isalpha、isdigit、toupper 等分类和转换接口接收 int,但参数必须是 EOF 或可表示为 unsigned char 的值。普通 char 可能有符号;从字符串取得字符后,应先转换为 unsigned char,再提升传入:toupper((unsigned char)text[index])。
| 任务 | <ctype.h> 接口 | 结果使用方式 |
|---|---|---|
| 判断字母或数字 | isalpha、isdigit | 只把零与非零作为真假 |
| 转为大写 | toupper | 无对应转换时返回原值 |
| 转为小写 | tolower | 返回值按接口合同再缩窄 |
分类和大小写规则受当前 locale 影响。不要把分类函数的某个非零数值当作固定标志,也不要把 EOF 先缩窄到 char 后再分类。代表程序固定使用 "C" locale,只处理 ASCII 输入,从而得到跨实现稳定结果。
locale 类别与 setlocale
程序启动时使用 "C" locale。setlocale 改变程序使用的 locale 状态;LC_ALL 一次指定全部类别,LC_CTYPE 影响字符分类与多字节解释,LC_NUMERIC 影响数值格式,其他标准类别分别覆盖排序、时间和货币规则。传入空指针可查询当前设置,传入空字符串请求实现由宿主环境选择的 locale。
| 任务 | <locale.h> 接口或宏 | 主要边界 |
|---|---|---|
| 查询或切换 locale | setlocale、LC_ALL | 返回空指针表示请求失败 |
| 查询数值与货币约定 | localeconv | 返回对象由库管理,不由调用方释放 |
保存并恢复 locale 字符串时,应先把需要长期保存的内容复制到调用方存储,再进行可能改变 locale 的调用;不能假设 setlocale 返回指针永远保持不变。locale 是会影响库函数的程序级状态,并发调用及与其他库函数交互都需要整体设计,不能把一次切换当作线程私有操作。需要稳定协议时,尽量把 locale 依赖限制在明确边界内。
多字节转换与 mbstate_t
多字节转换受当前 locale 和 mbstate_t 状态影响。mbrtowc 从多字节序列产生宽字符,wcrtomb 执行反向转换;调用方用清零的 mbstate_t 建立初始转换状态,并为每条独立转换链维护独立状态对象。
| 任务 | <wchar.h> 接口或类型 | 返回值重点 |
|---|---|---|
| 多字节转宽字符 | mbrtowc、mbstate_t | 字节数、零、(size_t)-1 或 (size_t)-2 |
| 宽字符转多字节 | wcrtomb、mbstate_t | 字节数或 (size_t)-1 |
mbrtowc 的 (size_t)-2 表示给定字节不足以完成当前多字节字符;代表程序以 mbrtowc(&wide, "A", 0, &state) 检查这一可移植的不完整输入分支。非法字节序列取决于当前 locale,失败时以 (size_t)-1 报告并关联 EILSEQ;正文不构造一个声称在所有 locale 都非法的固定字节。
宽字符、宽字符串与字符分类
宽字符串是以宽空字符终止的 wchar_t 数组。wcslen 计算宽字符元素数量,仍不等于数组容量;宽字符分类与转换由 <wctype.h> 提供。不能把 wchar_t 等同于 Unicode 码点:它的取值范围、执行宽字符集和编码关系都由 C 实现及当前 locale 的合同共同决定。
| 任务 | <wctype.h> 接口或类型 | 说明 |
|---|---|---|
| 查询分类描述符 | wctype | 按名称取得 wctype_t 描述符 |
| 分类宽字符 | iswctype | 使用有效描述符判断 |
| 宽字符转大写 | towupper | 规则受当前 locale 影响 |
<wchar.h> 还提供 wcslen 等宽字符串接口和宽字符 I/O;使用宽 I/O 时还要遵守流定向规则。用户可见字符可能由多个 Unicode 标量值、组合序列或更复杂的字素簇构成,宽数组元素数不能直接当作界面字符数。
C23 编码字符类型与 Unicode 边界
C23 的编码字符类型与 <uchar.h> 接口用于表达特定编码的代码单元和在多字节序列之间转换。char8_t、char16_t、char32_t 服务于 UTF-8、UTF-16、UTF-32 相关代码单元;mbrtoc8、c8rtomb、mbrtoc16、c16rtomb、mbrtoc32 和 c32rtomb 仍通过当前 locale 与 mbstate_t 连接到执行多字节编码。
| 任务 | <uchar.h> 类型或接口 | 不保证的事情 |
|---|---|---|
| 保存 UTF-8 代码单元 | char8_t、mbrtoc8、c8rtomb | 一个代码单元就是完整字符 |
| 保存 UTF-16 代码单元 | char16_t、mbrtoc16、c16rtomb | 一个代码单元独立表示所有码点 |
| 保存 UTF-32 代码单元 | char32_t、mbrtoc32、c32rtomb | 一个值就是一个用户可见字符 |
这些类型描述代码单元或与实现字符集合相关的值,不自动提供 Unicode 规范化、字素簇分割、区域化大小写或文本渲染。即使某个 32 位代码单元可以承载一个 Unicode 标量值,也仍不能把它等同于用户眼中的一个字符。
Annex K 边界检查接口
Annex K 的 _s 边界检查接口是可选且实现支持有限的扩展,不是可移植默认方案。实现通过 __STDC_LIB_EXT1__ 声明支持后,__STDC_WANT_LIB_EXT1__ 为 1 时必须提供声明,为 0 时不得提供;未定义该宏时,是否提供声明由实现定义。需要有意使用这些接口的程序,应在首次包含有关头文件前把请求宏定义为 1,并处理接口规定的运行时约束及失败结果。
set_constraint_handler_s 用于注册当前运行时约束处理器;某个 Annex K 接口检测到运行时约束违反时,会调用当前注册的处理器。处理器可能不返回;只有处理器返回后,该接口才会按照自身 Returns 小节的规定返回失败指示。
| 判断步骤 | 宏或接口 | 结论 |
|---|---|---|
| 实现是否声明支持 | __STDC_LIB_EXT1__ | 未声明时不能假定 Annex K 可用 |
| 翻译单元是否请求声明 | __STDC_WANT_LIB_EXT1__ | 为 1 时请求,为 0 时禁止;未定义时由实现决定 |
| 头文件是否实际提供接口 | strcpy_s、memcpy_s 等 _s 接口 | 依实现支持范围编译验证 |
这些接口不能自动修复错误的容量、对象生存期或重叠判断,也不能与名称相似的平台安全 CRT 混成一个跨平台保证。可移植代码应先使用 ISO C 主体中可用的接口并显式检查长度、容量和返回值;只在部署目标和工具链合同明确时封装可选接口。
字符串、内存与本地化陷阱
| 症状 | 常见根因 | 修正方向 |
|---|---|---|
strlen 越界读取 | 缓冲区内没有可访问的 \0 | 让产生数据的接口保证终止,或在已知边界内验证 |
| 复制后偶发乱码 | 把 strncpy 当作必然终止的复制 | 明确截断策略并自行保证终止 |
| 删除数组中一段后结果异常 | 对重叠范围调用 memcpy | 使用 memmove 并计算含 \0 的长度 |
isalpha 在扩展字节上出错 | 负 char 直接传入 | 先转换为 unsigned char |
| 同一文本在不同机器转换不同 | 隐式依赖宿主 locale | 固定协议 locale 或把差异纳入输入合同 |
| 宽字符数量不等于界面字符数 | 把代码单元当作字素簇 | 使用符合文本需求的 Unicode 处理层 |
按头文件反查常用任务:
| 头文件 | 主要接口组 | 适用任务 |
|---|---|---|
<string.h> | strlen、strcpy、strncpy、strcat、strcmp、strchr、strrchr、strstr、strtok、memcpy、memmove、memset、memcmp | 窄字符串与对象表示字节操作 |
<ctype.h> | isalpha、isdigit、toupper、tolower | 窄字符分类和大小写转换 |
<locale.h> | setlocale、localeconv、LC_ALL | 选择和查询程序 locale |
<wchar.h> | mbstate_t、mbrtowc、wcrtomb、wcslen | 多字节转换与宽字符串 |
<wctype.h> | wctype、iswctype、towupper | 宽字符分类与转换 |
<uchar.h> | char8_t、char16_t、char32_t 及编码转换接口 | UTF 编码代码单元与执行多字节编码转换 |
下面的代表程序固定使用 "C" locale,把 core-lab 保存到容量明确的数组,用 memmove 删除重叠范围中的连字符,再按合法参数规则转换大小写。它还用清零的 mbstate_t 验证 ASCII 多字节转换和 n == 0 的不完整输入分支;传入空字符串时,相同路径会安全得到长度零。
#include <ctype.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
int main(int argc, char *argv[]) {
if (argc > 2 || setlocale(LC_ALL, "C") == NULL) {
fputs("setup-error\n", stderr);
return EXIT_FAILURE;
}
const char *input = argc == 2 ? argv[1] : "core-lab";
char text[32];
size_t input_length = strlen(input);
if (input_length >= sizeof text) {
fputs("capacity-error\n", stderr);
return EXIT_FAILURE;
}
memcpy(text, input, input_length + 1);
char *dash = strchr(text, '-');
if (dash != NULL) {
memmove(dash, dash + 1, strlen(dash));
}
char upper[32];
size_t text_length = strlen(text);
for (size_t index = 0; index < text_length; ++index) {
upper[index] = (char)toupper((unsigned char)text[index]);
}
upper[text_length] = '\0';
mbstate_t state = {0};
wchar_t wide = L'\0';
if (mbrtowc(&wide, "A", 1, &state) != 1 || wide != L'A') {
fputs("conversion-error\n", stderr);
return EXIT_FAILURE;
}
mbstate_t incomplete_state = {0};
if (mbrtowc(&wide, "A", 0, &incomplete_state) != (size_t)-2) {
fputs("incomplete-check-error\n", stderr);
return EXIT_FAILURE;
}
printf("text=%s\n", text);
printf("length=%zu\n", text_length);
printf("upper=%s\n", upper);
puts("wide=A");
puts("locale=C");
return EXIT_SUCCESS;
}
输出:
text=corelab
length=7
upper=CORELAB
wide=A
locale=C