第11章:字符串和字符串函数
C 字符串以字符数组为基础,以空字符 \0 表示结束。处理字符串时,要同时关心三件事:字符内容、结束标记,以及数组实际能容纳多少字节。
本章可以沿着一条完整的数据处理过程理解:为文本准备空间,读取并确认输入完整,再进行比较、复制、查找、排序或数值转换。
1. 字符串的结束位置与数组边界是两回事
1.1 字符串必须以空字符结尾
char word[] = "cat";
编译器创建 4 个数组元素:
下标: 0 1 2 3
内容: 'c' 'a' 't' '\0'
字符串长度是 3,数组大小是 4 字节。sizeof(char) 始终为 1,但 C 的一个字节不必在所有实现上都是 8 位。
char first[] = {'c', 'a', 't', '\0'}; // 字符串。
char second[] = {'c', 'a', 't'}; // 只有字符,没有字符串结束标记。
第二个数组不能直接交给要求以 \0 结尾的字符串函数。否则函数可能越过数组边界继续查找结束标记,造成未定义行为。
1.2 空字符、空指针和数字字符不同
| 写法 | 含义 |
|---|---|
'\0' |
值为零的字符,用于终止字符串 |
'0' |
可显示的数字字符,不是结束标记 |
NULL |
空指针常量宏,用于表示没有有效目标地址 |
"" |
空字符串,数组表示中仍有一个 \0 |
'A' |
普通字符常量,在 C 中具有 int 类型 |
"A" |
包含 'A' 和 \0 的字符数组字面量 |
因此,strlen("") 是 0,而 sizeof "" 是 1。sizeof 'A' 是 sizeof(int),不能和 sizeof "A" 混为一谈。
1.3 字符串长度、数组容量与占用空间
char text[20] = "hello";
| 信息 | 结果 | 是否包含结束空字符 |
|---|---|---|
strlen(text) |
5 | 不包含 |
sizeof text |
20 | 表示整个数组的空间 |
| 存放当前字符串至少需要的空间 | 6 字节 | 包含 |
数组未被字符串使用的部分不会增加字符串长度。这里初始化列表没有覆盖的其他元素也会被零初始化。
strlen 只查找第一个 \0,不知道数组容量;sizeof 作用于实际数组时给出整个数组大小,不关心里面目前有几个可显示字符。
char text[] = "ab\0cd";
// strlen(text) 为 2,sizeof text 为 6。
第一个 \0 后面的字符仍可能存在于数组中,但普通字符串函数会在第一个结束标记处停止。
1.4 为结束标记预留一个位置
char good[4] = "cat";
char best[] = "cat";
char raw[3] = "cat";
最后一种写法在 C 中可以创建一个刚好容纳三个字母的数组,但放不下末尾的 \0,所以 raw 不能作为普通 C 字符串使用。
可显示内容的最大长度通常是“容量减 1”。不要因为声明合法,就推断数组里一定形成了合法字符串。
2. 字符数组与字符串指针
2.1 两种声明创建的对象不同
char editable[] = "green";
const char *view = "green";
editable 是实际字符数组,可以修改其中的元素;view 是指针变量,保存字符串字面量的首字符地址。
editable[0] = 'G';
view = "blue";
// editable = "blue"; // 不能整体赋值数组。
// view[0] = 'B'; // 不能通过 view 修改字符。
字符串字面量具有静态存储期,贯穿程序执行过程。即使写成 char *p = "green" 后编译器接受了声明,尝试通过 p 修改字面量仍然属于未定义行为。
让指针指向字面量时,使用 const char * 可以让编译器帮助阻止这种误用。
2.2 赋值指针不会复制字符
const char *first = "hello";
const char *second = first;
这里复制的是地址。两个指针对象不同,但它们指向同一个字符串。
first ─┐
├──→ ['h']['e']['l']['l']['o']['\0']
second ─┘
若需要可以独立修改的字符副本,必须准备另一块存储空间,再复制字符串内容。
编译器可以合并相同的字符串字面量,也可以不合并。不要使用字面量地址是否相等来判断文本是否相等。
2.3 相邻字面量会在翻译时拼接
const char *message = "first part, "
"second part.";
它表示一个连续字符串,适合把较长文本分成多行源码。它不同于运行时调用拼接函数,也不同于在两个指针之间使用 +。
字符串内部的双引号使用 \":
puts("She said: \"hello\".");
3. 存放多个字符串的两种布局
3.1 二维字符数组
char names[3][12] = {"pear", "apple", "plum"};
这里有 3 行,每行固定占 12 字节。每行都可以保存一个长度不超过 11 的字符串。
names[0] → [p e a r \0 ...] 共 12 字节
names[1] → [a p p l e \0 ...] 共 12 字节
names[2] → [p l u m \0 ...] 共 12 字节
它适合为多组可修改文本预留空间。各行连续排列,但每行必须使用同一个容量。
3.2 指针数组
const char *names[] = {"pear", "apple", "plum"};
数组元素是指针,分别指向相应字符串;这些字符串不必存放在连续位置。
names[0] ──→ "pear"
names[1] ──→ "apple"
names[2] ──→ "plum"
| 比较项 | char names[3][12] |
const char *names[3] |
|---|---|---|
| 数组中直接存放什么 | 字符 | 指针 |
| 字符串存储布局 | 固定行宽、连续排列 | 指针可以指向不同位置 |
| 能否修改这里的字符 | 能 | 不能修改所指字面量 |
能否对 names[i] 直接赋地址 |
不能,行本身是数组 | 能,元素是指针变量 |
sizeof names 是否计入所指字符串 |
字符都在数组内部 | 只计算指针数组本身 |
names[i][j] 在两种布局下都可能访问到一个字符,但相似的下标语法不能证明对象类型和内存布局相同。
4. 输入字符串:先准备可写空间
4.1 指针本身不是输入缓冲区
char *name;
// scanf("%s", name); // 错误:name 没有指向有效的存储空间。
先创建数组才有位置存放输入:
char name[32];
输入函数不会因为拿到了一个指针,就自动推算长度并为字符串分配空间。
4.2 单词输入与整行输入
| 函数 | 如何停止 | 换行处理 | 容量约束 |
|---|---|---|---|
scanf("%31s", name) |
读到空白或字段宽度上限 | 不把终止空白收入字符串 | 宽度最多为容量减 1 |
fgets(name, 32, stdin) |
读到换行、读满 31 个字符或输入结束 | 能读到换行时保留它 | 参数指定缓冲区大小 |
gets(name) |
历史函数,按行读取 | 丢弃换行 | 无容量参数,C11 已移除 |
gets_s(name, 32) |
受容量限制的行输入 | 丢弃换行 | C11 可选扩展,超长输入涉及运行时约束处理 |
gets 没有表达缓冲区边界的办法,不应继续使用。gets_s 也不是每个支持 C11 的实现都提供,不能简单假定它普遍可用。
4.3 scanf 的字段宽度不包含末尾 \0
char word[8];
if (scanf("%7s", word) == 1) {
puts(word);
}
%7s 最多读取 7 个非空白字符,并额外写入一个 \0。
普通字符数组转换得到的地址已经是 char *,这里传 word,不用写成 &word。后者是指向整个数组的指针,类型不同。
宽度限制只防止本次写满缓冲区,不会替你丢弃单词剩余部分。一个过长单词的后半截,可能成为下一次输入的内容。
%s 会跳过开头空白,也不能完整读取包含空格的书名或姓名;这种需求更适合 fgets。
4.4 fgets 成功后才使用缓冲区
char line[80];
if (fgets(line, sizeof line, stdin) != NULL) {
fputs(line, stdout);
}
对于至少为 2 的容量参数,成功读取时,最多保存“容量减 1”个输入字符,并添加 \0。成功时返回传入的缓冲区地址;没有读到任何字符就遇到文件结束,或发生读取错误时,返回空指针。
读取失败后,不要把缓冲区当作本次已经成功得到的字符串继续处理。需要区分文件结束与读取错误时,可以检查 feof 和 ferror。
4.5 没有读到换行,不一定是输入过长
char *newline = strchr(line, '\n');
if (newline != NULL) {
*newline = '\0';
}
查不到换行时,可能有三种情况:
- 输入行还有更多字符,本次只装下前缀。
- 缓冲区恰好装满了正文,下一个输入字符就是换行。
- 最后一行没有换行,已经到达文件结束。
需要严格判断是否超长时,应继续检查输入,而不是把“没有换行”直接等同于“超长”。
清理剩余输入时,要同时考虑换行和文件结束:
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
continue;
}
getchar 的返回值用 int 保存,才能可靠地区分所有字符值与 EOF。只等待换行的循环,在输入已结束时可能永远停不下来。
5. 一个能报告输入状态的行读取函数
下面的函数面向不含嵌入空字符的普通文本输入。调用时,缓冲区实际容量必须至少为 capacity,且 capacity >= 2。
它删除读到的换行;超长时丢弃本行剩余部分,并返回单独状态,让调用者决定是否接受前缀。
#include <stdio.h>
#include <string.h>
enum line_status {
LINE_OK,
LINE_END,
LINE_TOO_LONG,
LINE_ERROR
};
enum line_status read_line(char *text, int capacity)
{
if (capacity < 2) {
return LINE_ERROR;
}
if (fgets(text, capacity, stdin) == NULL) {
return ferror(stdin) ? LINE_ERROR : LINE_END;
}
char *newline = strchr(text, '\n');
if (newline != NULL) {
*newline = '\0';
return LINE_OK;
}
int ch = getchar();
if (ch == '\n') {
return LINE_OK;
}
if (ch == EOF) {
return ferror(stdin) ? LINE_ERROR : LINE_OK;
}
do {
ch = getchar();
} while (ch != '\n' && ch != EOF);
return ferror(stdin) ? LINE_ERROR : LINE_TOO_LONG;
}
int main(void)
{
char line[8];
for (;;) {
enum line_status status = read_line(line, sizeof line);
if (status == LINE_END) {
break;
}
if (status == LINE_ERROR) {
fputs("input error\n", stderr);
return 1;
}
if (status == LINE_TOO_LONG) {
puts("too long");
continue;
}
printf("[%s]\n", line);
}
return 0;
}
输入:
cat
1234567
12345678
ok
输出:
[cat]
[1234567]
too long
[ok]
长度为 7 的正文刚好装进数组,后面的换行由额外读取处理;长度为 8 的正文才真正超出容量。空行会成功返回空字符串,和“没有任何数据可读”是不同状态。
6. 字符串输出与逐字符处理
6.1 三种输出接口
| 调用 | 行为 |
|---|---|
puts(text) |
输出到标准输出,额外添加换行 |
fputs(text, stdout) |
输出到指定流,不额外添加换行 |
printf("%s", text) |
按格式输出,不自动换行 |
这些接口都不会把终止空字符本身显示出来。它们的返回值用于报告输出结果,其中 fputs 返回的是整数状态,不是字符指针。
fgets 已保存换行时,再交给 puts 输出,往往会多出一行空行。要么保留换行并使用 fputs,要么先删除换行再使用 puts。
把外部文本作为数据传入固定格式:
printf("%s", text);
不要直接使用 printf(text)。文本中的 % 可能被当作格式指令解释。
6.2 遍历到结束标记
void show_text(const char *text)
{
while (*text != '\0') {
putchar((unsigned char)*text);
++text;
}
}
while (*text) 是同一种判断的简写。指针在移动,原字符没有改变;形参指针的移动也不会改变调用方保存的指针。
字符串函数可以不额外接收长度,是因为输入约定中已经有结束标记。但这不代表函数能自动验证缓冲区边界;传入的对象必须确实包含可访问的 \0。
7. 复制与拼接:目标空间由调用者保证
以下字符串函数声明在 <string.h> 中。复制和拼接时,源范围与目标范围不能重叠;需要处理重叠字节范围时,应使用适当的 memmove 操作。
7.1 strlen 只统计第一个空字符之前的长度
size_t length = strlen("hello");
结果类型为 size_t,输出使用 %zu。其参数必须已经是合法字符串;不能拿 strlen 去探测一个未知数组中是否恰好存在结束标记。
在循环中反复调用 strlen(text),可能反复扫描同一字符串。若内容没有变化,可以先保存长度,或者直接遍历到 \0。
7.2 strcpy 复制完整字符串,包括结束标记
char target[16];
strcpy(target, "hello");
strcpy(target, source) 的目标在前,源在后,返回目标起始地址。它会复制源中的 \0,但不会获得目标容量信息。
必须保证目标空间至少有 strlen(source) + 1 字节,而且可以写入。
char *target;
// strcpy(target, "hello"); // 错误:尚未提供目标空间。
目标也可以从某个有效偏移开始,例如 strcpy(buffer + 3, source),但需要按剩余容量重新计算空间。
7.3 strncpy 不保证结果一定是字符串
char target[8];
strncpy(target, source, sizeof target - 1);
target[sizeof target - 1] = '\0';
strncpy(target, source, n) 的规则是:
- 源长度至少为
n时,复制前n个字符,不会额外写入\0。 - 源长度小于
n时,复制内容,并把剩余位置补成空字符,总共写入n个字符位置。
所以它不是“只限制长度的 strcpy”。目标必须能写入 n 个字符;需要字符串结果时,还要明确保证终止空字符。
上例保证终止,但允许内容被截断。是否接受截断,是接口设计需要说明的选择,不能只看程序是否越界。
7.4 strcat 从目标字符串末尾追加
char message[20] = "hello";
strcat(message, " world");
调用前,目标必须已经是以 \0 结尾的字符串。strcat 找到目标末尾,把源字符串接过去,覆盖原结束标记,再写入新的结束标记。
之前:h e l l o \0
追加: 空格 w o r l d \0
之后:h e l l o 空格 w o r l d \0
所需容量至少为:
原目标长度 + 源长度 + 1
把一个未初始化字符数组直接交给 strcat 是错误的。若要从空字符串开始,可写 char message[20] = "";。
7.5 strncat 的第三个参数是最大追加数
char message[12] = "hello";
size_t used = strlen(message);
size_t available = sizeof message - used - 1;
strncat(message, " world!", available);
这里最多追加剩余空间所允许的字符,并写入新的 \0。
第三个参数不是整个目标数组容量,也不是最终字符串的最大长度。strncat 与 strncpy 中的 n 含义不同,不能机械互换。
计算 available 的前提是目标已经是容量范围内的合法字符串。否则 strlen 本身就可能越界,无符号减法也可能得到很大的错误结果。
8. 比较、查找与格式化
8.1 strcmp 比较内容,== 比较地址
if (strcmp(command, "quit") == 0) {
puts("bye");
}
strcmp(left, right) 的结果 |
含义 |
|---|---|
| 小于 0 | 左边排在前面 |
| 等于 0 | 两个字符串内容相同 |
| 大于 0 | 左边排在后面 |
标准只保证结果的正负与零,不保证非零结果一定是 1 或 -1。
比较按首对不同字符的值决定,字符按 unsigned char 解释。它区分大小写,也不是按中文拼音或自然语言规则排序。
if (strcmp(a, b)) 表示二者不同。初学时写成 strcmp(a, b) != 0,更不容易把真假含义记反。
8.2 strncmp 可以检查前缀
if (strncmp(filename, "log_", 4) == 0) {
puts("has prefix");
}
它最多比较 n 个字符,也会在遇到字符串结束时停止。前 n 个字符相同,不代表整个字符串相同;n 为零时比较结果为零。
8.3 查找函数返回位置或空指针
| 函数 | 用途 |
|---|---|
strchr(text, ch) |
查找字符第一次出现的位置 |
strrchr(text, ch) |
查找字符最后一次出现的位置 |
strpbrk(text, set) |
查找 set 中任意字符第一次出现的位置 |
strstr(text, part) |
查找子字符串第一次出现的位置 |
没有找到时返回空指针。strchr 和 strrchr 也可以查找结束空字符;strstr(text, "") 返回 text 的起始位置。
const char *filename = "report.txt";
const char *dot = strrchr(filename, '.');
if (dot != NULL) {
printf("extension: %s\n", dot + 1);
}
返回指针指向原对象内部,没有复制字符串。查找函数不会把字面量变成可修改对象:即使某个接口返回 char *,也不能据此修改原本只读的字符串。
8.4 sprintf 把格式化结果写入数组
char description[80];
sprintf(description, "%s: %d", "count", 12);
它与 printf 使用相似的格式,但目标是字符缓冲区。返回成功写出的字符数,不包含末尾 \0。它没有容量参数,调用者必须保证空间足够;字段宽度通常表示最小宽度,也不能替代容量限制。
需要显式限制目标容量时,可以使用 C99 的 snprintf:
char description[16];
int result = snprintf(description, sizeof description,
"%s: %d", "count", 12);
if (result < 0) {
/* 格式化失败。 */
} else if ((size_t)result >= sizeof description) {
/* 空间不足,结果被截断。 */
}
在容量大于零且调用有效时,snprintf 会为实际写入内容保留结束位置。其非负返回值表示完整结果原本需要的字符数,不包含 \0,所以要检查是否大于等于容量。
格式化源和目标也不应重叠,不要写 sprintf(buffer, "%s...", buffer) 来尝试原地追加。
9. 用字符函数处理整个字符串
<ctype.h> 提供字符分类和大小写转换。对字符串逐个字符调用,就能完成整段文本的处理。
#include <ctype.h>
#include <stddef.h>
void uppercase(char *text)
{
while (*text != '\0') {
*text = (char)toupper((unsigned char)*text);
++text;
}
}
size_t count_punctuation(const char *text)
{
size_t count = 0;
while (*text != '\0') {
if (ispunct((unsigned char)*text)) {
++count;
}
++text;
}
return count;
}
这些函数的实参必须是 EOF 或可表示为 unsigned char 的值。如果普通 char 是有符号类型,直接传入负值会违反要求,所以遍历字符串时先转换成 unsigned char。
同样的模式可以统计数字、空白、大写字母、小写字母,或把文本统一成小写再比较。
普通 char 字符串处理的是字节。UTF-8 中的一个汉字通常包含多个字节,因此 strlen 不是汉字数量,按字节逆序也会破坏这样的多字节编码。这里的逐字符大小写示例适合基本单字节文本,不是完整的 Unicode 文本处理方案。
10. 自定义字符串函数的常见结构
10.1 一边遍历,一边统计
size_t text_length(const char *text)
{
size_t length = 0;
while (text[length] != '\0') {
++length;
}
return length;
}
读取操作使用 const char *;需要修改文本时才去掉所指字符的 const 限定。
10.2 用读写两个位置删除字符
void remove_spaces(char *text)
{
char *write = text;
for (char *read = text; *read != '\0'; ++read) {
if (*read != ' ') {
*write = *read;
++write;
}
}
*write = '\0';
}
读指针负责扫描,写指针负责保存需要保留的字符。写位置不会超过读位置,适合原地压缩字符串。
最后必须写入新的 \0,否则旧尾部仍会被当成结果的一部分。这个函数只删除普通空格;若要删除所有空白,需要使用 isspace((unsigned char)*read)。
10.3 逆序时先处理空字符串
void reverse_text(char *text)
{
size_t length = strlen(text);
for (size_t i = 0; i < length / 2; ++i) {
size_t opposite = length - 1 - i;
char temp = text[i];
text[i] = text[opposite];
text[opposite] = temp;
}
}
空字符串不会进入循环,也就不会计算循环体中的 length - 1。结束标记保持原位。
不要先形成一个指向首元素之前的指针,再指望比较条件阻止解引用。指针越过合法范围这件事本身就可能违反规则。
11. 字符串排序:重新排列指针
11.1 数据内容与显示顺序可以分开
如果只想按不同顺序查看字符串,可以保留原字符数组,再创建一个指针数组作为索引。
原始数据: 排序后的索引:
input[0] → "pear" order[0] ──→ input[1]
input[1] → "apple" order[1] ──→ input[2]
input[2] → "plum" order[2] ──→ input[0]
排序只交换指针,不必复制整行字符,原始数据的存放顺序也得到保留。
11.2 完整的选择排序示例
#include <stddef.h>
#include <stdio.h>
#include <string.h>
void sort_texts(const char *texts[], size_t count);
int main(void)
{
char input[3][12] = {"pear", "apple", "plum"};
const char *order[3];
for (size_t i = 0; i < 3; ++i) {
order[i] = input[i];
}
sort_texts(order, 3);
puts("sorted:");
for (size_t i = 0; i < 3; ++i) {
puts(order[i]);
}
puts("original:");
for (size_t i = 0; i < 3; ++i) {
puts(input[i]);
}
return 0;
}
void sort_texts(const char *texts[], size_t count)
{
for (size_t top = 0; top < count; ++top) {
size_t best = top;
for (size_t seek = top + 1; seek < count; ++seek) {
if (strcmp(texts[seek], texts[best]) < 0) {
best = seek;
}
}
if (best != top) {
const char *temp = texts[top];
texts[top] = texts[best];
texts[best] = temp;
}
}
}
输出:
sorted:
apple
pear
plum
original:
pear
apple
plum
每轮外层循环选定一个结果位置;内层循环找出剩余字符串中排序最靠前的一项;最后交换相应指针。
这里 const char *texts[] 约束的是字符访问,数组元素中的指针仍然可以交换。若把每个指针本身也限定为 const,就不能进行这种排序。
选择排序需要二次量级的比较次数,每次字符串比较还可能检查多个字符。它适合解释算法结构,较大规模排序可以使用适当的库函数。
如果原始字符串位于局部数组中,索引指针也不能在那个数组生存期结束后继续使用。重排指针没有延长字符对象的生存期。
12. 命令行参数也是字符串
12.1 带参数的 main
int main(int argc, char *argv[])
第二个形参也可以写成 char **argv。
| 项目 | 含义 |
|---|---|
argc |
参数字符串数量,通常包含程序名称 |
argv[0] |
在 argc > 0 时可访问,表示程序名称;名称不可用时可能为空字符串 |
argv[1] |
第一个用户参数,访问前要求 argc > 1 |
argv[argc] |
空指针,是参数指针序列的结束位置 |
例如:
./tool hello "two words"
在通常的命令行环境中,会得到三个参数字符串:程序名、hello、two words。引号如何分组由运行环境处理,程序接收的是已经组织好的字符串。
12.2 遍历参数
#include <stdio.h>
int main(int argc, char *argv[])
{
for (int i = 1; i < argc; ++i) {
printf("%d: %s\n", i, argv[i]);
}
return 0;
}
访问 argv[1] 之前先检查数量。短路求值可以让数量检查保护后面的访问,但条件必须按正确顺序书写。
参数里的 -u、-l 或 --help 本身没有特殊的 C 语法意义,程序需要使用 strcmp 等操作解析,再选择对应处理逻辑。
13. 把数字文本转换为数值
13.1 文本数字与整数不是同一种表示
"123" 是四个字符位置组成的字符串;整数 123 是一个数值。不能把字符串地址直接当作整数参与数值计算。
| 函数 | 转换结果 | 特点 |
|---|---|---|
atoi |
int |
接口简单,没有可靠的错误报告通道 |
atol |
long |
同类简化接口 |
atof |
double |
同类简化接口 |
strtol |
long |
可指定进制,提供停止位置和范围错误信息 |
strtoul |
unsigned long |
转换为无符号长整数 |
strtod |
double |
转换为浮点数,提供停止位置和范围错误信息 |
这些函数声明在 <stdlib.h> 中。
atoi("12items") 可以只转换前面的数字,得到 12;无法转换的文本与合法的零不容易通过返回值区分。结果超出返回类型范围时还可能导致未定义行为,所以需要验证输入时,更适合使用 strtol 一类接口。
13.2 strtol 的三个参数
char *end;
long value = strtol(text, &end, 10);
text:待解析的字符串。&end:让函数把停止转换的位置写入end。10:按十进制解释输入。
它会跳过开头空白,允许正负号,再读取当前进制允许的数字。
输入 "123abc",十进制转换:
value = 123
end 指向 'a'
常见判断:
| 判断 | 含义 |
|---|---|
end == text |
没有发生任何有效数字转换 |
*end == '\0' |
转换正好到字符串末尾 |
*end != '\0' |
仍有未转换内容,需要决定是否接受 |
errno == ERANGE |
本次转换发生范围错误,调用前应先清零 errno |
进制可以指定为 2 到 36,或使用 0 让函数按前缀判断。按本章的 C99/C11 规则,自动判断涉及普通十进制、前导零的八进制以及 0x 前缀的十六进制。若产品输入只允许十进制,直接传 10 更清楚。
13.3 完整示例:严格解析一个 int
下面的程序允许数字两侧存在空白,拒绝多余字符、空输入以及超范围结果:
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int parse_int(const char *text, int *result);
int main(int argc, char *argv[])
{
if (argc != 2) {
fputs("usage: parse integer\n", stderr);
return EXIT_FAILURE;
}
int value;
if (!parse_int(argv[1], &value)) {
fputs("invalid integer\n", stderr);
return EXIT_FAILURE;
}
printf("value: %d\n", value);
return EXIT_SUCCESS;
}
int parse_int(const char *text, int *result)
{
char *end;
errno = 0;
long value = strtol(text, &end, 10);
if (end == text || errno == ERANGE) {
return 0;
}
while (isspace((unsigned char)*end)) {
++end;
}
if (*end != '\0' || value < INT_MIN || value > INT_MAX) {
return 0;
}
*result = (int)value;
return 1;
}
运行示例:
./parse " 42 "
value: 42
./parse 42items
invalid integer
strtol 的返回类型是 long,不代表结果一定适合 int。转换到更窄的目标类型之前,还需要检查目标类型的范围。
result 必须指向一个可写的 int 对象;失败时,这个函数不修改它。这样的约定让调用者能够清楚地区分“成功得到结果”和“输入无效”。
14. 按任务选择接口
| 任务 | 优先考虑 |
|---|---|
| 为固定短文本提供可写空间 | 有明确容量的字符数组 |
| 引用不修改的固定文本 | const char * 指向字面量 |
| 读取包含空格的一行 | fgets,并处理换行、超长和输入结束 |
| 读取单个短单词 | 检查返回值并限制字段宽度的 scanf |
| 输出一行普通文本 | puts |
| 原样输出已有换行的文本 | fputs |
| 比较完整内容 | strcmp |
| 检查固定前缀 | strncmp |
| 复制与拼接 | 先验证目标容量,再选对应函数 |
| 产生格式化文本 | 指定容量并检查返回值的 snprintf |
| 调整字符串显示顺序 | 为原数据建立指针索引后排序 |
| 解析需要验证的数字 | strtol、strtoul 或 strtod,检查停止位置与范围 |
字符串的结束标记提供了内容边界,数组容量提供了存储边界。读取、复制、拼接和修改时,两种边界都必须成立;一个函数会寻找 \0,并不意味着它知道目标还有多少空间。

