第 14 章:进阶处理与输入输出
本章涵盖:
- 使用指针
- 格式化输入
- 处理扩展字符集
- 使用二进制流进行输入和输出
- 检查错误并清理
既然已经了解指针及其工作方式,现在可以从新的角度审视 C 库的一些功能。C 的文本处理离不开指针,因此本章在第 14.1 节先考察一个更详尽的示例。随后介绍格式化输入函数(第 14.2 节);这些函数要求以指针作为实参,所以只能推迟到这里。接着引入一整套处理扩展字符集(第 14.3 节)和二进制流(第 14.6 节)的新函数。作为本章乃至这一整级的收尾,最后将讨论如何干净利落地处理错误(第 15.6 节)。
14.1 文本处理
先来看下面这个程序。它从 stdin 读取一系列文本行,每行含有若干个数,然后以规范化形式把这些数写到 stdout:各数表示成十六进制,并以逗号分隔。
numberline.c
int main (void) {
char lbuf[256];
for (;;) {
if (fgetline(sizeof lbuf, lbuf, stdin)) {
size_t n;
size_t* nums = numberline(strlen(lbuf)+1, lbuf, &n, 0);
if (nums) {
int ret = fprintnumbers(stdout, "%#zX", ",\t", n, nums);
if (ret < 0) return EXIT_FAILURE;
free(nums);
}
} else {
if (lbuf[0]) { /* a partial line has been read */
for (;;) {
int c = getc(stdin);
if (c == EOF) return EXIT_FAILURE;
if (c == '\n') {
fprintf(stderr, "line too long: %s\n", lbuf);
break;
}
}
} else break; /* regular end of input */
}
}
}253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
这个程序把工作分成三项不同任务:
- 用
fgetline读取一行文本; - 用
numberline把读到的行拆成一系列size_t类型的数; - 用
fprintnumbers打印这些数。
核心是函数 numberline。它把收到的 lbuf 字符串拆成若干数,分配一个数组来存储它们;如果提供了指针形参 np,还会通过该形参返回这些数的数量。
接口:numberline
把字符串 lbuf 解释为一系列以 base 为基数表示的数。
返回: 一个新分配的数组,其中存放在 lbuf 中找到的数。
形参:
lbuf应当是字符串;- 如果
np非空,数的数量存入*np; base是从 0 到 36 的值,其解释与strtoul及类似函数相同。
备注: 调用方负责释放返回的数组。
size_t* numberline(size_t size,
char const lbuf[restrict static size],
size_t* restrict np, int base);2
3
该函数本身又分成两部分,各自承担截然不同的任务。numberline_inner 负责解释文本行;numberline 只是包在它外面的一层,用来核实或保证它的先决条件。函数 numberline_inner 在循环中使用 C 库函数 strtoull,收集各个数并返回其数量。
现在可以看到 strtoull 第二个形参的用途。这里传入的是对象 next 的地址,next 用来记录字符串中每个数结束的位置。由于 next 是指向 char 的指针,传给 strtoull 的实参就是指向 char 指针的指针:
numberline.c
static
size_t numberline_inner(char const* restrict act,
size_t numb[restrict static 1],
int base) {
size_t n = 0;
for (char* next = nullptr; act[0]; act = next) {
numb[n] = strtoull(act, &next, base);
if (act == next) break;
++n;
}
return n;
}101
102
103
104
105
106
107
108
109
110
111
假设调用形式是 strtoull("0789a", &next, base)。根据形参 base 的值,同一个字符串会得到不同解释。例如,base 的值为 10 时,第一个非数字字符就是末尾的 'a':
base | 数字个数 | 得到的数 | *next |
|---|---|---|---|
| 8 | 2 | 7 | '8' |
| 10 | 4 | 789 | 'a' |
| 16 | 5 | 30,874 | '\0' |
| 0 | 2 | 7 | '8' |
请记住基数为 0 时的特殊规则。实际基数由字符串的第一个字符(或前两个字符)推导得出。这里第一个字符是 '0',所以字符串按八进制解释;解析会在该基数下的第一个非数字字符 '8' 处停止。
numberline_inner 对文本行的解析可能因以下两种条件而终止:
act指向字符串终止位置,也就是字符0;- 函数
strtoull没有找到数,此时next会被设为act的值。
这两个条件分别体现在 for 循环的控制表达式,以及循环内部的 if-break 条件中。
请注意,C 库函数 strtoull 有一项历史遗留弱点:第一个实参的类型是 char const*,第二个实参的类型却是没有 const 限定的 char**。正因如此,我们只能把 next 的类型写成 char*,而不能写成 char const*。这样一来,调用 strtoull 之后,我们可能会无意间修改只读字符串,导致程序崩溃。
要点 14.1 #1
字符串 strto... 转换函数不具备 const 安全性。
函数 numberline 负责 numberline_inner 外围的衔接工作:
- 如果
np为空,就让它指向一个辅助对象; - 检查输入字符串是否有效;
- 分配一个足够大的数组来存储各值,得知实际个数后再把数组调整到合适的大小。
这里使用了三个 C 库函数:memchr、malloc 和 realloc。与先前的示例一样,malloc 与 realloc 配合使用,确保数组具有所需的长度。
如果存在值为 0 的字节,memchr 就返回第一个这样的字节的地址;否则返回空指针。这里只用它来检查前 size 个字节中是否确实有字符 0。这样便能保证底层用到的所有字符串函数(特别是 strtoull)处理的都是以 0 终止的字符串。
在 C23 以前,memchr 的接口是有问题的:当时只有一个同名函数,它返回的 void* 可能指向只读对象。
要点 14.1 #2
memchr 和 strchr 搜索函数的函数接口不具备 const 安全性。
C23 新增的类型泛型宏接口修复了这项缺陷。第 18.1.7 节将更详细地讨论这些类型泛型接口。
要点 14.1 #3
memchr 和 strchr 搜索函数的类型泛型接口具备 const 安全性。
相比之下,返回字符串内部索引位置的函数则是安全的。
要点 14.1 #4
strspn 和 strcspn 搜索函数具备 const 安全性。
numberline.c
size_t* numberline(size_t size,
char const lbuf[restrict static size],
size_t* restrict np, int base) {
size_t* ret = nullptr;
size_t n = 0;
/* Check for validity of the string, first. */
if (memchr(lbuf, 0, size)) {
/* The maximum number of integers encoded.
To see that this may be as much look at
the sequence 08 08 08 08 ... and suppose
that base is 0. */
ret = malloc(sizeof(size_t[1+(2*size)/3]));
if (!ret) return nullptr;
n = numberline_inner(lbuf, ret, base);
size_t len = n ? n : 1;
size_t* ret2 = realloc(ret, sizeof(size_t[len]));
if (ret2) ret = ret2;
}
if (np) *np = n;
return ret;
}114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
遗憾的是,这些返回索引的函数无法检查一个 char 数组究竟是不是字符串,所以这里用不上它们。
现在来看示例中的第二个函数。
接口:fgetline
读取一个至多包含 size-1 个字节的文本行。
字符 '\n' 会替换为 0。
返回: 如果成功读入了完整的一行,就返回 s。否则返回 0,此时 s 中存放能读入的最长的不完整文本行。s 以空字符终止。
char* fgetline(size_t size,
char s[restrict size],
FILE* restrict stream);2
3
它与 C 库函数 fgets 十分相似。第一项差别在接口上:形参顺序不同,而且 size 形参的类型是 size_t 而不是 int。和 fgets 一样,从流中读取失败时,它会返回空指针,因此很容易检测 stream 上的文件结束条件。
更重要的是,fgetline 能更从容地处理另一种关键情形:它既能检测下一输入行是否过长,也能检测流的最后一行是否缺少结尾的 '\n' 字符。
numberline.c
char* fgetline(size_t size,
char s[restrict size],
FILE* restrict stream) {
s[0] = 0;
char* ret = fgets(s, size, stream);
if (ret) {
/* Uses non-const variant of strchr */
char* pos = strchr(s, '\n');
if (pos) *pos = 0;
else ret = nullptr;
}
return ret;
}137
138
139
140
141
142
143
144
145
146
147
148
函数的前两行保证 s 始终以空字符终止:如果 fgets 调用成功,终止由它完成;否则就强制把 s 设为空字符串。随后,如果确实读入了内容,就把 s 中找到的第一个 '\n' 字符替换为 0。如果找不到,就说明只读入了不完整的一行。调用方可以检测到这种情形,再次调用 fgetline,尝试读取该行的剩余部分,或者检测文件结束条件。[1]
除 fgets 外,这里还用到了 C 库中的 strchr。自 C23 起,函数接口缺乏 const 安全性已不成问题,因为类型泛型接口提供了相应的保证。
函数 fprintnumbers 涉及大量细致的错误处理,因此第 15.6 节会更深入地讨论它。这里仅讨论稍简单一些的 sprintnumbers:它只写入字符串而不写入流,而且直接假定传入的缓冲区 buf 空间足够。
接口:sprintnumbers
使用 printf 格式 form,把一系列数 nums 打印到 buf 中。各数之间用 sep 字符分隔,末尾添加换行字符。
返回: 打印到 buf 的字符数。
这里假定 tot 和 buf 足够大,而且 form 是适合打印 size_t 的格式。
int sprintnumbers(size_t tot, char buf[restrict tot],
char const form[restrict static 1],
char const sep[restrict static 1],
size_t len,
size_t nums[restrict static len]);2
3
4
5
函数 sprintnumbers 使用了一个尚未见过的 C 库函数:sprintf。它的格式化能力与 printf 和 fprintf 相同,只是不向流打印,而是写入 char 数组。
numberline.c
int sprintnumbers(size_t tot, char buf[restrict tot],
char const form[restrict static 1],
char const sep[restrict static 1],
size_t len,
size_t nums[const restrict static len]) {
char* p = buf; /* next position in buf */
size_t const seplen = strlen(sep);
if (len) {
size_t i = 0;
for (; i < len;) {
p += sprintf(p, form, nums[i]);
++i;
if (i >= len) break;
memcpy(p, sep, seplen);
p += seplen;
}
}
memcpy(p, "\n", 2);
return (p-buf)+1;
}156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
函数 sprintf 总会在字符串末尾放置字符 0,并返回该字符串的长度,也就是写入的字符 0 之前的字符数。示例利用这个返回值,更新指向缓冲区当前位置的指针。不过,sprintf 仍有一项重要漏洞。
要点 14.1 #5
sprintf 没有采取任何防止缓冲区溢出的措施。
也就是说,如果第一个实参传入的缓冲区空间不足,就会发生糟糕的事。与 sprintf 本身一样,sprintnumbers 内部也假定缓冲区足够大,容得下结果。如果拿不准缓冲区是否容得下结果,可以改用 C 库函数 snprintf:
int snprintf(char* restrict s, size_t n,
char const* restrict form, ...);2
该函数还保证写入 s 的字节数绝不超过 n。如果返回值大于或等于 n,说明字符串已被截短,以便装得下。特别地,当 n 为 0 时,不会向 s 写入任何内容。
要点 14.1 #6
格式化长度未知的输出时,请使用 snprintf。
总而言之,snprintf 有许多优良性质:
- 缓冲区不会溢出;
- 调用成功后,
s是字符串; - 以
n为0、s为空指针调用时,snprintf返回本应写入的字符串长度。
利用这一点,只需一个简单的 for 循环,就能算出一行中所有数打印出来共需多少字符:
numberline.c
/* Count the chars for the numbers. */
for (size_t i = 0; i < len; ++i)
tot += snprintf(nullptr, 0, form, nums[i]);189
190
后面会看到这段代码在 fprintnumbers 中的用法。
挑战 15:字符串中的文本处理
关于文本处理,我们已经介绍了不少内容。现在来看看你能否真正把它们用起来。
- 能否在字符串中搜索给定单词?
- 能否替换字符串中的一个单词,并返回包含新内容的副本?
- 能否为字符串实现一些正则表达式匹配函数?例如,寻找
[A-Q]或[^0-9]这样的字符类,并支持用*表示“任意内容”、用?表示“任意字符”的匹配? - 能否为
[[:alpha:]]、[[:digit:]]等 POSIX 字符类实现正则表达式匹配函数? - 能否把这些功能全部结合起来,在字符串中搜索正则表达式?
- 能否用正则表达式对特定单词进行查询替换?
- 能否为正则表达式增加分组?
- 能否为查询替换增加分组?
14.2 格式化输入
与用于格式化输出的 printf 函数族相似,C 库也提供了一系列格式化输入函数:fscanf 从任意流输入,scanf 从 stdin 输入,sscanf 则从字符串输入。例如,下面的代码会从 stdin 读取一行三个 double 值:
double a[3];
/* Read and process an entire line with three double values. */
if (scanf(" %lg %lg %lg ", &a[0], &a[1], &a[2]) < 3) {
printf("not enough input values!\n");
}2
3
4
5
表 14.1 到表 14.3 概述了格式说明。遗憾的是,这些函数比 printf 更难用,而且有些约定与 printf 存在细微差别。
- 为了能够返回各格式对应的值,实参必须是指向所扫描类型的指针。
表 14.1 scanf 及类似函数的格式说明,一般句法为 [XX][WW][LL]SS
| 名称 | 记号 | 说明 |
|---|---|---|
XX | * | 抑制赋值 |
WW | 字段宽度 | 最大输入字符数 |
LL | 修饰符 | 选择目标类型的宽度 |
SS | 说明符 | 选择转换 |
表 14.2 scanf 及类似函数的格式说明符。 二进制支持由 C23 引入。字符或字符集合说明符('c'、's'、'[')带有 'l' 修饰符时,会把输入中的多字节字符序列转换到宽字符 wchar_t 实参中;参见第 14.3 节。
SS | 转换 | 指向 | 跳过空白 | 类似函数 |
|---|---|---|---|---|
'd' | 十进制 | 有符号类型 | 是 | strtol,基数 10 |
'i' | 二进制、八进制、十进制或十六进制 | 有符号类型 | 是 | strtol,基数 0 |
'b' | 二进制 | 无符号类型 | 是 | strtoul,基数 2 |
'o' | 八进制 | 无符号类型 | 是 | strtoul,基数 8 |
'u' | 十进制 | 无符号类型 | 是 | strtoul,基数 10 |
'x' | 十六进制 | 无符号类型 | 是 | strtoul,基数 16 |
'aefg' | 浮点 | 浮点类型 | 是 | strtod |
'%' | 字符 '%' | 不赋值 | 否 | |
'c' | 字符 | char | 否 | memcpy |
's' | 非空白字符 | char | 是 | 使用 " \f\n\r\t\v" 的 strcspn |
'[' | 扫描集 | 字符串 | 否 | strspn 或 strcspn |
'p' | 地址 | void | 是 | |
'n' | 字符计数 | 有符号类型 | 否 |
- 空白处理非常微妙,有时还会出人意料。格式中的空格字符
' '会匹配任意空白序列:空格、制表符和换行符。这样的序列可以为空,也可以包含多个换行符。 - 字符串处理有所不同。由于
scanf函数的实参本来就是指针,格式"%c"和"%s"都对应char*类型的实参。"%c"读取长度固定(默认为1)的字符数组;"%s"则匹配任意非空白字符序列,并在末尾添加字符0。 - 格式中的类型说明与
printf存在细微差异,浮点类型尤其如此。为了让两者保持一致,最好在printf和scanf中都用"%lg"或类似格式表示double,用"%Lg"表示long double。 - 还有一种识别字符类的简易工具。例如,可以用格式
"%[aeiouAEIOU]"扫描拉丁字母表中的元音。在这种字符类说明中,如果开头是脱字符^,就表示对该字符类取反。因此,"%[^\n]%*[\n]"会扫描完整的一行(该行不得为空),随后丢弃行末的换行字符。
这些特殊之处让 scanf 函数族难以使用。例如,前面那个看似简单的示例有一项缺陷(或者说功能):它不限于读取单个输入行,而会欣然接受分布在多行中的三个 double 值。[2] 大多数情况下,如果输入具有规则的模式(例如一串数),最好不要用这些函数。
表 14.3 scanf 及类似函数的格式修饰符。 请注意,float 和 double 的处理方式与 printf 不同。
| 修饰符 | 类型 |
|---|---|
"L" | long double |
"hh" | char 类型 |
"h" | short 类型 |
"" | signed、unsigned、float、char 数组和字符串 |
"l" | long 整数类型、double、wchar_t 字符和字符串 |
"ll" | long long 整数类型 |
"j" | intmax_t、uintmax_t |
"z" | size_t |
"t" | ptrdiff_t |
"wN" | uintN_t、intN_t、uint_leastN_t 或 int_leastN_t(自 C23 起,N 通常为 8、16、32、64 或 128) |
"wfN" | uint_fastN_t 或 int_fastN_t(自 C23 起) |
14.3 扩展字符集
到目前为止,无论是编写程序,还是编写要在控制台上打印的字符串字面量,我们都只用了有限的一组字符:拉丁字母、阿拉伯数字和一些标点字符。这项限制是历史的偶然:一方面源于美国计算机产业早期的市场主导地位,另一方面源于当初必须用极少的位来编码字符。[3] 从基本数据单元的类型名叫 char 就可以看出,起初“文本字符”和“不可分割的数据组成单元”这两个概念并没有区分清楚。
我们的字符集继承自拉丁语,而拉丁语作为口语早已消亡。它的字符集不足以表达其他语言在语音上的特点。在欧洲语言中,英语的特殊之处在于,它用 ai、ou、gh(fair enough)之类的字母组合来表示缺失的语音,而不像大多数近亲语言那样使用附加符号、特殊字符或连字(fär ínóff)。因此,即便是同样使用拉丁字母表的其他语言,可用的手段也已经相当有限;至于使用完全不同文字(希腊语、俄语),甚至文字观念截然不同(日语、汉语)的语言和文化,这套受限的美式字符集显然远远不够用。
在市场向全球扩张的最初几年里,不同的计算机制造商、国家和组织近乎随意地为各自社群提供母语支持,还在毫无协调的情况下各自加入了对图形字符、数学排版、乐谱等的专门支持。那是一片彻底的混乱。结果,在不同系统、国家和文化之间交换文本信息往往十分困难,甚至根本做不到;要编写能在不同语言、不同计算平台下使用的可移植代码,简直如同施展黑魔法。
幸运的是,这些延续多年的难题如今大都已经解决。在现代系统上,可以用统一的方式编写使用“扩展”字符的可移植代码。下面的代码片段展示了理想的做法:
mbstrings-main.c
setlocale(LC_ALL, "");
/* Multibyte character printing only works after the locale
has been switched. */
draw_sep(TOPLEFT " © 2014 jɛnz 'gʊz,tɛt ", TOPRIGHT);88
89
90
也就是说,在程序开头附近切换到“本地”区域设置,之后就可以使用并输出包含扩展字符的文本——这里用的是语音字符(所谓 IPA)。一开始就调用 setlocale 至关重要;否则,把扩展字符集中的字符输出到终端时,很可能只会看到乱码。只要调用了 setlocale,而且系统安装妥当,在多字节字符串 "fär ínóff" 中使用这类字符,效果应该就不会太差(详见下文)。
输出大致如下:
┌ © 2014 jɛnz 'gʊz,tɛt ────────────────────────────────────────────┐实现手段相当简单。先用几个宏,以“魔法”字符串字面量分别定义竖线、横线、左上角和右上角:
mbstrings-main.c
#define VBAR "\u2502" /**< a vertical bar character */
#define HBAR "\u2500" /**< a horizontal bar character */
#define TOPLEFT "\u250c" /**< topleft corner character */
#define TOPRIGHT "\u2510" /**< topright corner character */44
45
46
再临时写一个函数,把一行输出排得美观些:
接口:draw_sep
绘制多字节字符串 start 和 end,中间用一条横线分隔。
void draw_sep(char const start[static 1],
char const end[static 1]) {
fputs(start, stdout);
size_t slen = mbsrlen(start, 0);
size_t elen = 90 - mbsrlen(end, 0);
for (size_t i = slen; i < elen; ++i) fputs(HBAR, stdout);
fputs(end, stdout);
fputc('\n', stdout);
}2
3
4
5
6
7
8
9
这里用函数 mbsrlen 计算多字节字符串中的可打印字符数,还用到了文本输出的老朋友 fputs 和 fputc。
接口:mbsrlen
把 mbs 中的多字节字符串解释为宽字符串,并返回其长度。
返回: 多字节字符串的长度;如果发生编码错误,则返回 mbinvalid。
只要传入的 state 实参与 mbs 开头的多字节字符相一致,本函数就可以用在对同一字符串的一系列搜索之中。函数不会修改状态本身。
备注: state 为空,表示扫描 mbs 时无须考虑任何上下文。
size_t mbsrlen(char const* restrict mbs,
mbstate_t const* restrict state);2
多字节字符是一个字节序列,它被解释为扩展字符集中的单个字符;多字节字符串则是包含这类多字节字符的字符串。所幸,这些奇特的东西与我们迄今处理的普通字符串是兼容的。
要点 14.3 #1
多字节字符不包含空字节。
要点 14.3 #2
多字节字符串以空字符终止。
因此,许多标准字符串函数(例如 strcpy)无需修改就能用于多字节字符串。不过,多字节字符串带来了一项重大困难:可打印字符数再也无法直接从 char 数组的元素数或函数 strlen 推算出来。正因如此,前面的代码才使用了(非标准的)函数 mbsrlen。
从接口说明可以看出,从多字节字符串中解析出各个多字节字符,过程可能稍微复杂一些。尤其是,我们通常需要用 C 标准在头文件 <wchar.h> 中提供的类型 mbstate_t 来保存解析状态。[4] 这个头文件既提供处理多字节字符串与多字节字符的工具,也提供宽字符类型 wchar_t。相关函数通常会返回若干种不同的代码,用来表示当前的解析状态。
mbcode:多字节转换函数的返回代码
这些函数通常返回 size_t 类型的值。因此,各个特殊代码必须位于该类型范围的上端。
下面用枚举类型为这些常量命名。这种做法自 C23 起才可行,因为此前枚举常量只能是 signed int。
枚举项:
mbinvalid:遇到了无效编码;mbincomplete:编码不完整;mbstored:存储了此前检测到的一个输出字符。
enum mbcode {
//! An invalid encoding was encountered.
mbinvalid = (size_t)-1,
//! The encoding was incomplete.
mbincomplete = (size_t)-2,
//! A previously detected output character was stored.
mbstored = (size_t)-3,
};2
3
4
5
6
7
8
不过首先还要介绍另一项国际标准:ISO 10646(Unicode [2017])。它试图为字符代码提供统一框架(参见 Joel Spolsky 的文章 The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets),为人类迄今构想出的几乎所有字符概念建立了一张庞大的表格。[5]
这里,“概念”二字极其重要:必须把字符概念与它在某种字体中的印刷形式(字形)区分开。例如,“拉丁大写字母 A”在本书正文中可能显示成 A、𝐀、𝑨 或 𝒜;而另一些概念字符,例如“希腊大写字母 Alpha”,印出来的字形甚至可能与“A”相同或相似。
Unicode 把每个字符概念——用它自己的行话说,就是每个码点——置于语言或技术上下文中。除定义字符外,Unicode 还会为它分类,例如归类为大写字母;也会建立它与其他码点的关系,例如说明 A 是 a 的大写形式。
如果你的语言需要特殊字符,那么键盘上很可能已经有这些字符,可以把它们原样输入 C 代码的多字节字符串中。也就是说,系统可能已经配置好,会直接把(比如说)ä 的整个字节序列插入文本,替你完成所有必要的魔法。如果键盘上没有这些字符,或者你不想这样做,可以使用前面定义宏 HBAR 时的技巧。那里使用了 C11 新增的一种转义序列(参见 The Interesting State of Unicode in C):反斜杠加 u,再跟四个十六进制数字,用来编码 Unicode 码点。例如,“带分音符的拉丁小写字母 a”的码点是 228,也就是 0xE4。在多字节字符串中,它写作 "\u00E4"。四个十六进制数字只能表示 65,536 个码点,因此还可以用反斜杠加大写 U 引出八个十六进制数字;不过,只有非常专业的场合才会用到后一种写法。
前面的示例就用这种 Unicode 写法编码了四个图形字符,它们很可能不在任何键盘上。有不少在线网站可以帮你查找所需字符的码点。
如果要对多字节字符和字符串做比简单输入输出更多的操作,事情就会复杂一些。仅仅统计字符数就已经不容易:strlen 给不出正确答案,strchr、strspn 和 strstr 等其他字符串函数也不会按预期工作。所幸,C 标准提供了一组替代函数,它们通常用前缀 wcs 取代 str,作用于宽字符串。前面介绍的 mbsrlen 函数可以写成:
mbstrings.c
size_t mbsrlen(char const* s, mbstate_t const* restrict state) {
state = state ? state : MBSTATE;
mbstate_t st = *state;
size_t mblen = mbsrtowcs(nullptr, &s, 0, &st);
if (mblen == mbinvalid) errno = 0;
return mblen;
}31
32
33
34
35
36
函数的核心是使用库函数 mbsrtowcs,其名称表示“多字节字符串(mbs),可重启(r),转为宽字符串(wcs)”。它是 C 标准为处理多字节字符串所提供的基本操作之一:
size_t mbsrtowcs(wchar_t* restrict dst,
char const** restrict src,
size_t len, mbstate_t* restrict ps);2
3
因此,只要拆解这个缩写名称,就知道该函数会把多字节字符串 src 转换为宽字符串 dst。这里的宽字符(wc)是 wchar_t 类型,每个宽字符恰好编码扩展字符集中的一个字符。宽字符组成宽字符串的方式,与 char 组成普通字符串的方式基本相同:宽字符串是由宽字符构成、以空字符终止的数组。
如果 state 为空,mbsrlen 会改用宏 MBSTATE(这里没有给出),该宏提供一个 mbstate_t 类型的辅助缓冲区。此外,如果传入的源字符串无效,该函数可能把 errno 设为某个错误代码。我们对此不感兴趣,所以在返回前把它重置为零。
C 标准对 wchar_t 采用的编码没有太多限制,但如今任何正常的环境都应当以 Unicode 作为内部表示。可以用下面两个宏检查这一点:
mbstrings.h
#ifndef __STDC_ISO_10646__
# warning "wchar_t wide characters have to be Unicode code points"
#endif
#ifdef __STDC_MB_MIGHT_NEQ_WC__
# warning "basic character codes must agree on char and wchar_t"
#endif25
26
27
28
29
现代平台通常用 16 位或 32 位整数类型实现 wchar_t。只要仅使用 \uXXXX 记法(四个十六进制数字)能表示的码点,一般就不必关心平台用的是哪一种。实际采用 16 位的平台无法使用 \UXXXXXXXX 记法中的其余码点,不过这通常不会给你带来太多麻烦。
宽字符和宽字符串字面量遵循与 char 和普通字符串相似的规则。两者都用前缀 L 表示宽字符或宽字符串。例如,L'ä' 与 L'\u00E4' 是同一个字符,类型都是 wchar_t;L"b\u00E4" 则是含有三个 wchar_t 元素的数组,元素依次为宽字符 L'b'、L'ä' 和 0。
宽字符的分类方式与普通 char 相似,所需的函数和宏由头文件 <wctype.h> 提供。
回到 mbsrtowcs。该函数把多字节字符串 src 解析成一个个多字节字符(mbc),并把相应的码点赋给 dst 中的宽字符。形参 len 指定所得宽字符串的最大长度。形参 state 指向一个对象,用来保存多字节字符串可能具有的解析状态;稍后会简要讨论这个概念。
可以看到,函数 mbsrtowcs 有两点特别之处。第一,如果以空指针作为 dst 调用,它不会存储宽字符串,只返回该宽字符串应有的大小。第二,如果多字节字符串编码不正确,它会产生编码错误:函数返回 mbinvalid,并把 errno 设为 EILSEQ(参见 <errno.h>)。正因为这种错误处理策略,mbsrlen 中才有一段代码把 errno 重新设为 0,作为补救。
现在来看另一个帮助我们处理多字节字符串的函数。
接口:mbsrdup
把 s 中的字节序列解释为多字节字符串,并将其转换为宽字符串。
返回: 用 malloc 新分配、长度适当的宽字符串;如果发生编码错误,则返回空指针。
备注: 只要传入的 state 实参与 c 开头的多字节字符相一致,本函数就可以用在对同一字符串的一系列同类搜索之中。函数不会修改状态本身。
state 为空,表示扫描 s 时无须考虑任何上下文。
wchar_t* mbsrdup(char const* s, mbstate_t* restrict state);该函数返回一个新分配的宽字符串,内容与输入的多字节字符串 s 相同。除了状态形参的处理之外,实现相当直接:
mbstrings.c
wchar_t* mbsrdup(char const* s, mbstate_t* restrict state) {
mbstate_t st = state ? *state : *MBSTATE;
size_t mblen = mbsrlen(s, &st);
if (mblen == mbinvalid) return nullptr;
wchar_t* S = malloc(sizeof(wchar_t[mblen+1]));
/* We know that s converts well, so no error check */
if (S) mbsrtowcs(S, &s, mblen+1, state);
return S;
}39
40
41
42
43
44
45
46
确定目标字符串长度后,使用 malloc 分配空间,再用 mbsrtowcs 复制数据。
为了更精细地控制多字节字符串的解析,标准提供了函数 mbrtowc:
size_t mbrtowc(wchar_t* restrict pwc,
char const* restrict s, size_t len,
mbstate_t* restrict ps);2
3
在该接口中,形参 len 表示为了得到一个多字节字符,最多在 s 中扫描到多远。一般来说,我们不知道目标机器上的多字节编码如何工作,只能靠一些猜测来确定 len。为了封装这种启发式做法,我们设计了下面这个接口。它的语义与 mbrtowc 相似,但无须指定 len。
接口:mbrtow
把 c 中的字节序列解释为多字节字符,并通过 C 将其作为宽字符返回。
返回: 多字节字符的长度;如果发生编码错误,则返回 mbinvalid。
只要对本函数或类似函数的所有调用都传入同一个 state 实参,本函数就可以用在对同一字符串的一系列同类搜索之中。
备注: state 为空,表示扫描 c 时无须考虑任何上下文。
size_t mbrtow(wchar_t* restrict C,
char const c[restrict static 1],
mbstate_t* restrict state);2
3
该函数返回在字符串开头识别出的第一个多字节字符所占的字节数,出错时返回 mbinvalid。如果 len 不够大,mbrtowc 还可能返回另一个值 mbincomplete。实现借助这个返回值检测这种情形,并不断增大 len,直至足够为止:
mbstrings.c
size_t mbrtow(wchar_t* restrict C,
char const c[restrict static 1],
mbstate_t* restrict state) {
if (!state) state = MBSTATE;
size_t len = -2;
for (size_t maxlen = MB_LEN_MAX; len >= mbincomplete; maxlen *= 2)
len = mbrtowc(C, c, maxlen, state);
if (len == mbinvalid) errno = 0;
return len;
}15
16
17
18
19
20
21
22
23
这里的 MB_LEN_MAX 是一个标准值,在大多数情况下都是 len 的不错上界。请注意,即使字符串正确地以空字符结束,这个过程也能正常工作。
下面这个函数利用 mbrtow 识别多字节字符的能力,在多字节字符串中进行搜索。
接口:mbsrwc
把 s 中的字节序列解释为多字节字符串,并搜索宽字符 C。
返回: C 在 s 中第 occurrence 次出现时,对应多字节序列的起始位置;如果发生编码错误,则返回空指针。
如果出现次数少于 occurrence,就返回最后一次出现的位置。因此,传入 SIZE_MAX(或 -1)总会返回最后一次出现的位置。
备注: 只要对本函数或类似函数的所有调用都传入同一个状态实参,而且后续搜索从本函数返回的位置开始,本函数就可以用在对同一字符串的一系列同类搜索之中。
state 为空,表示扫描 s 时无须考虑任何上下文。
char const* mbsrwc(char const s[restrict static 1],
mbstate_t* restrict state,
wchar_t C, size_t occurrence);2
3
mbstrings.c
char const* mbsrwc(char const s[restrict static 1],
mbstate_t* restrict state,
wchar_t C, size_t occurrence) {
if (!C || C == WEOF) return nullptr;
state = state ? state : MBSTATE;
char const* ret = nullptr;
mbstate_t st = *state;
for (size_t len = 0; s[0]; s += len) {
mbstate_t backup = st;
wchar_t S = 0;
len = mbrtow(&S, s, &st);
if (!S) break;
if (C == S) {
*state = backup;
ret = s;
if (!occurrence) break;
--occurrence;
}
}
return ret;
}69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
14.4 UTF 字符编码
前面说过,只要环境保持一致(即源代码、其他文本文件和终端都使用同一种多字节编码),多字节字符串的这套编码方式以及简单的输入输出就能顺利工作。遗憾的是,目前并非所有环境都使用同一种编码,因此在不同环境之间传输文本文件(包括源代码文件)或可执行程序时,就可能遇到困难。除了定义那张庞大的字符表,Unicode 还定义了三种如今广泛使用的编码(它们有望最终取代其他所有编码):UTF-8、UTF-16 和 UTF-32,分别表示以 8 位、16 位和 32 位字为单位的 Unicode 转换格式。
自 C11 起,C 语言不必依赖区域设置,就能对这些编码提供初步的直接支持。采用这些编码的字符和字符串字面量可以用前缀来书写。例如,u8"text"、u"text" 和 U"text" 的类型分别是 char8_t[5]、char16_t[5] 和 char32_t[5]。
现代平台上的多字节编码很可能就是 UTF-8。如果是这样,就用不着这些特殊的字面量和类型。它们主要用于必须确保采用某种特定编码的场合,例如网络通信。在遗留平台上,日子可能会难过一些;这篇文章 概览了 Windows 平台上的情况。
在比较简单的情况下,UTF 与基本字符类型之间的对应关系如表 14.4 所示。
表 14.4 UTF 与基本字符类型之间的常见对应关系
| 字面量 | typedef | 类型 | 编码 | 对应关系 |
|---|---|---|---|---|
u8'a' | uchar8_t | unsigned char | UTF-8 | char 中的 ASCII |
u'π' | uchar16_t | uint_least16_t | UTF-16 | |
U'𝒜' | uchar32_t | uint_least32_t | UTF-32 | wchar_t 中的“wc” |
u8"𝒜" | uchar8_t[5] | unsigned char[5] | UTF-8 | char[] 中的“mbs” |
u"𝒜" | uchar16_t[3] | uint_least16_t[3] | UTF-16 | |
U"𝒜" | uchar32_t[2] | uint_least32_t[2] | UTF-32 | wchar_t[] 中的“wcs” |
UTF-16 的情况尤其复杂。u"𝒜" 这样的字符串含有三个元素,因为码点 0x1D49C 至少需要 17 位来编码,按定义装不进 16 位编码。UTF-16 借助所谓的代理对解决这个问题,也就是用两个 16 位字编码这类字符(另加一个字存放末尾的空字符)。有一种已经过时的编码叫 UCS-2,它只包含 UTF-16 能用单个 16 位字编码的那些码点,这些码点组成的集合称为基本多文种平面(BMP)。其他字符(例如示例中的 u'𝒜')不在这个集合中,因而无法用 UCS-2 表示。请尽可能避免使用 UTF-16(以及 UCS-2)。UTF-8 是一种效率很高的多字节编码,UTF-32 则能用一个字编码整个 Unicode 字符集;只要有得选,就应当优先使用它们。
与名称中含有 wcs 或 mbs 的函数类似,还有一些函数用 c8、c16 和 c32 分别表示源或目标采用 UTF-8、UTF-16 和 UTF-32 编码。遗憾的是,这套函数并不完整,其中一部分直到 C23 才姗姗来迟。因此,如果平台较旧或者有特殊需求,可能要费些力气才能用它们拼出实用的功能。
C23 还带来了另一项保证:char16_t 和 char32_t 使用的编码确实是 UTF-16 和 UTF-32,而此前的标准允许实现另作选择。过去可以用功能测试宏 __STDC_UTF_16__ 和 __STDC_UTF_32__ 检验这一点;现在它们固定为 1,所以下面的预处理代码在现代编译器上绝不应该触发:
mbstrings.h
#if (__STDC_UTF_16__ != 1) || (__STDC_UTF_32__ != 1)
# error "wide character strings should use UTF encodings"
#endif6
7
14.5 可重启的文本转换
前面见过的那些名称古怪、形如 XXXrtoYYY 的函数还有一个特点:名称中的 r 表示“可重启”(restartable)。也就是说,这些函数可以接收不完整的多字节字符串作为输入,把目前已读到的输入状态收集到(由 state 形参指向的)状态对象中,等检测到完整的多字节字符后再开始产生输出。C23 支持下面这些编码组合:
mbrtowc mbrtoc8 mbrtoc16 mbrtoc32
wcrtomb c8rtomb c16rtomb c32rtomb2
各编码的缩写和补充信息见表 14.5。请注意,wc 和 c32 都是单字符编码,所以两端都无须缓冲。一般来说,其他编码都需要缓冲,因为一个码点可能需要多个字符。这样看来,唯一出人意料的是 mb 用在输出端的情况。
表 14.5 形如 XXXrtoYYY 的文本转换函数
| 缩写 | 编码 | 用作 XXX | 用作 YYY |
|---|---|---|---|
mb | 区域设置的多字节编码 | 有缓冲 | 无缓冲 |
wc | 区域设置的宽字符编码 | 无缓冲 | 无缓冲 |
c8 | UTF-8 | 有缓冲 | 有缓冲 |
c16 | UTF-16 | 有缓冲 | 有缓冲 |
c32 | UTF-32 | 无缓冲 | 无缓冲 |
要点 14.5 #1
一个码点的多字节 mb 编码会一次性写入输出字符串。
输入端是否在缓冲很容易判断:只要输入尚不完整,函数就返回 mbincomplete。在输出端,判断一次写操作是否完成要稍微微妙一些:
- 较小的数表示输入产生或补全了一个码点,并且输出的第一个字符已经存储。此时还不知道输出是否还需要更多字符。
- 之后再调用同一个函数,只要它又存储了编码中的一个字符,就会返回
mbstored。
为了观察实际效果,假设要扫描从 stdin 读入的多字节输入序列。代码由两个嵌套循环组成,循环之前还有若干缓冲区声明和语句。
analyze-utf8.c
int main (void) {
// Make sure to have the platform's mb encoding on input.
setlocale(LC_CTYPE, "");
// Holds the state of input/output buffering.
mbstate_t st = { };
// collects the input mb sequence
char ib[23];
// collects the current UTF-8 mb sequence
char8_t ob[5] = { };
// the number of input characters for the current code point
size_t in = 0;
while (fgets(ib, sizeof(ib), stdin)) {
// Run through the current line. The last character is
// always reserved for the string terminator.
for (char* p = ib; (p-ib) < sizeof(ib)-1;) {
size_t const n = sizeof(ib)-1-(p-ib);7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这里调用 setlocale,是为了确保我们真的把输入当作多字节字符串来看待。如果不调用它,就只会按 "C" 区域设置逐字节扫描。
在内层循环体中,输入由指针 p 给出;我们把 UTF-8 输出序列写入输出缓冲区 ob。
analyze-utf8.c
size_t const r = mbrtoc8(ob, p, n, &st);
switch (r) { // Handle the special cases.
case mbincomplete: p += n; in += n; continue;
case 0: case mbstored: case mbinvalid: goto INVAL;
}
p += r; in += r;
char8_t* cont = ob+1; // first character is already stored
while (mbrtoc8(cont, "", 1, &st) == mbstored) {
cont++;
}23
24
25
26
27
28
29
30
31
目标是转换这个序列,让我们能看清每个完整码点的边界。为此,代码多次调用 C 库函数 mbrtoc8,每次都使用同一个 mbstate_t 对象 st。第一次调用应当返回从输入中读取的字节数,以此表明码点是否已经补全;这个值累加到名为 in 的对象中。随后,循环不断尝试把空字符存入目标缓冲区,直到成功为止,也就是直到构成该 UTF-8 序列的所有字节都已写出。
switch 语句处理各种例外情形:特别是会跳到标签 INVAL 处的错误处理代码(见下文);如果多字节输入序列还不完整,就继续读取输入。例如,当输入缓冲区恰好在某个多字节字符中间结束时,就会出现这种情况。
要点 14.5 #2
一个码点的多字节 mb 编码可以从输入中分段收集。
现在,输出缓冲区 ob 中已经有了码点的 UTF-8 编码,可以分析结果,并把只含一个 ASCII 字符的情形区分出来。输入行末需要特殊处理,因为我们知道 fgets 会在那里停下。比较时还必须使用 UTF-8 中正确的行末字符,因为原则上它可能与平台的本机编码不同:
analyze-utf8.c
// Now we have the whole UTF-8. Analyze the result.
printf("%s", ((cont-ob) == 1) ? "ASCII\t" : "UTF-8\t");
for (char8_t* o = ob; o < cont; ++o) {
printf("|%02hhx", *o);
}
// fgets stopped at an end of a line
if (*ob == u8'\n') {
puts("|\t~ eol");
in = 0;
break;
} else if (in == (cont-ob)) {
printf("|\t~ '%s'\n", ob);
} else {
printf("|\t%zu→→→%tu\n", in, (cont-ob));
}
in = 0;33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
请注意,我们无须维护一个随长输入行不断调整大小的缓冲区。输入缓冲区 ib 的长度是固定的,而 mbrtoc8 能够保存解析状态,读到不完整输入行的问题由重启机制解决。这个示例中 ib 的长度 23 小得近乎荒谬,这是为了确保测试时很容易覆盖多字节序列被从中间截断的情形。在生产代码中,这个值通常会大得多。
随后,函数代码以两个嵌套循环的结尾收束;错误处理通过标签 INVAL 进入:
analyze-utf8.c
}
if (*ob != u8'\n') {
fputs("incomplete line\n", stderr);
}
}
return EXIT_SUCCESS;
INVAL:
fputs("input error, exiting\n", stderr);
return EXIT_FAILURE;
}49
50
51
52
53
54
55
56
57
14.6 二进制流
第 8.4 节曾简要提到,除了迄今一直使用的普通文本模式,流的输入输出还可以在二进制模式下进行。要理解两者的区别,请记住:在文本模式下,传给 printf 或 fputs 的字节并不会一一原样写到目标文件或设备。
- 视目标平台而定,一个
'\n'字符可能编码成一个或多个字符。 - 换行符之前的空格可能被省略。
- 多字节字符可能从执行字符集(程序的内部表示)转写成文件所在文件系统的字符集。
从文本文件读取数据时也有类似情况。
如果处理的确实是供人阅读的文本,这一切都没有问题:输入输出函数与 setlocale 会配合起来,让这套机制尽可能透明,我们乐得坐享其成。但如果想按照数据在某些 C 对象中的原貌读写二进制数据,这套机制就可能成为沉重的负担,带来严重的麻烦。尤其是,二进制数据可能恰好与文件的行末约定相吻合,写入这样的数据就可能改变文件的内部结构。
如前所述,流可以用二进制模式打开。对于这样的流,文件外部表示与内部表示之间的一切转换都会跳过,每个字节都原样写入或读出。在迄今见过的接口中,只有 fgetc 和 fputc 能以可移植的方式处理二进制文件,其他接口都可能依赖某种行末转换。
为了更方便地读写二进制流,C 库提供了几个更合适的接口:
size_t fread(void* restrict ptr, size_t size, size_t nmemb,
FILE* restrict stream);
size_t fwrite(void const* restrict ptr, size_t size, size_t nmemb,
FILE* restrict stream);
int fseek(FILE* stream, long int offset, int whence);
long int ftell(FILE* stream);2
3
4
5
6
fread 和 fwrite 用起来相对直接。每个流都有一个当前文件位置,读写都从这里进行。调用成功时,这两个函数从该位置开始读取或写入 size*nmemb 个字节,然后相应地更新文件位置。两个函数都返回实际读取或写入的字节数,通常就是 size*nmemb;如果返回值比它小,就说明发生了错误。
函数 ftell 和 fseek 用来操纵文件位置:ftell 返回从文件起点算起的字节位置;fseek 则根据实参 offset 和 whence 在文件中定位。这里,whence 可以取以下值之一:SEEK_SET 表示文件起点,SEEK_CUR 表示调用前的当前文件位置。[6]
借助这四个函数,我们可以在表示文件的流中自如地前后移动,读写其中任意字节。例如,可以用这种方式把大型对象的内部表示写入文件,之后由另一个程序原封不动地读回。
不过,这套接口也有一些限制。为了可移植,必须以二进制模式打开流。在某些平台上,由于根本没有什么转换要做,输入输出始终就是二进制的。遗憾的是,不使用二进制模式的程序在这类平台上可能一直运行可靠,移植到其他平台后却会出错。
要点 14.6 #1
凡是使用 fread 或 fwrite 的流,都应当以二进制模式打开。
由于这里处理的是对象的内部表示,所以只有在表示和字节序都相同的平台和程序执行之间才是可移植的。不同的平台、操作系统,甚至同一程序的不同次执行,都可能采用不同的表示。
要点 14.6 #2
以二进制模式写出的文件不能跨平台移植。
文件位置使用 long 类型,因此 ftell 和 fseek 能够轻松处理的文件大小被限制在 LONG_MAX 字节以内。在大多数现代平台上,这相当于 2 GiB。[7]
要点 14.6 #3
fseek 和 ftell 不适合非常大的文件偏移量。
在某些情况下,应用需要访问一块每次执行都相同的二进制数据,例如弹出窗口中显示的图形标志,或者需要包含在每个目标文件中的二进制签名。自 C23 起,可以用一个简单的工具——#embed 指令——可靠地把这类二进制数据纳入源代码。
unsigned char logo[] = {
#if defined (__has_embed) && __has_embed("picture.dat")
# embed "picture.dat"
#else
"embed directive is missing"
#endif
};2
3
4
5
6
7
从表面上看,它的工作方式与 #include 指令十分相似:指令后面的文件名指明要纳入哪个文件。与 __has_include 类似,还有一个可用于测试的 __has_embed 特性。首先,如果 __has_embed 本身有定义,就说明实现支持 #embed 指令。其次,如果 __has_embed("picture.dat") 在预处理条件中得到 1,就说明已知该文件存在,可以嵌入。
文件按二进制数据读取,效果如同在该处插入一长串以逗号分隔的数,每个字节都用表示其内容的十进制数表示。可以想见,对于较大的文件,这会形成一张又长又难懂的数字列表。在前面的示例中,这张列表直接用作数组对象 logo 的初始化器。尽管如此,成熟的编译器在初始化器中遇到 #embed 指令时,应当能极其高效地实现它:直接把二进制数据放进可执行文件,而不是先在二进制数据与十进制字面量之间来回编码、解码。下面来看一个更详尽的示例。
embed.c
// define a character array that will contain the entire
// source file
static char const here[] = {
// Cedro does not work with blanks between the # and any directive
#pragma Cedro 1.0 embed
#embed "embed.c"
};
// define another character array that has the same size
static char there[sizeof here];26
27
28
29
30
31
32
33
34
35
36
这段代码定义了两个字符数组 here 和 there。与前面一样,如果编译器实现了 #embed 指令,就会把一个文件作为二进制数据提供出来,用于初始化数组 here。此外,这里还展示了一种可能的后备方案:使用名为 Cedro 的工具;在编译器还没跟上时,它可以帮你填补空白。更多信息参见附录 B.2。
预期的语义可以从相应的 main 中看出:
embed.c
int main (int argc, char* argv[static argc+1]) {
size_t ibytes = 0;
int cmp = 1000;
// Open the file in binary mode.
FILE* inp = fopen(__FILE__, "rb");
if (inp) {
// read the file as binary
ibytes = fread(there, 1, sizeof there, inp);
cmp = memcmp(here, there, sizeof here);
fclose(inp);
} else {
printf("could not open %s\n", __FILE__);
printf("+++++++++++++++++++++++++++++++++++++++++++++++\n");
}
size_t obytes = fwrite(here, 1, sizeof here, stdout);
printf("+++++++++++++++++++++++++++++++++++++++++++++++\n");
printf("in %zu, out %zu, bytes are %s\n",
ibytes, obytes,
cmp < 0 ? "smaller" : (cmp > 0 ? "greater" : "equal"));
}39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
这里以二进制模式把(但愿是)同一个文件读入第二个数组 there。here 与 there 的内容应当完全相同,所以 memcmp 调用应当返回 0。
挑战 16:流中的文本处理
对于流中的文本处理,你能否从 stdin 读取文本,把修改后的文本输出到 stdout,并在 stderr 上报告诊断?能否统计一组单词各自出现的次数?统计某个正则表达式出现的次数?把某个单词的所有出现都替换成另一个单词?
挑战 17:更精密的文本处理器
你能否扩展自己的文本处理器(挑战 12),使其使用多字节字符?
能否进一步扩展它,使其能处理正则表达式,例如搜索单词、对单词做简单的查询替换、用正则表达式对特定单词做查询替换,以及应用正则表达式分组?
小结
- C 库提供了多个文本处理接口,但必须留意 const 限定与缓冲区溢出。
- 用
scanf(及类似函数)做格式化输入时,指针类型、字符串的空字符终止、空白和换行分隔等方面都有微妙的问题。如果可能,应当把fgets与strtod或类似的更专门的函数结合使用。 - 处理扩展字符集的最佳方式是使用多字节字符串。只要稍加小心,就可以像普通字符串一样用它们做输入输出。
- 二进制数据应当用
fwrite和fread写入、读出二进制文件。这类文件依赖于平台。 - 编译期的二进制数据可以用
#embed指令高效地初始化。
练习 51 改进示例的
main,使它能够处理任意长度的输入行。 ↩︎练习 52 修改示例中的格式字符串,使其只接受同一行中以空格分隔的三个数,并跳过末尾的换行字符以及换行前可能有的空格。 ↩︎
基本字符集主要采用的字符编码称为 ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)。 ↩︎
头文件
<uchar.h>也提供该类型。 ↩︎如今 Unicode 大约有 110,000 个码点。 ↩︎
还有表示文件结束位置的
SEEK_END,但它可能存在由平台定义的小毛病。 ↩︎练习 57 编写函数
fseekmax,用intmax_t代替long,并通过组合多次fseek调用来实现很大的定位值。 ↩︎