清风日常

Stay curious. Stay gentle.

听一会儿

4 首收藏
LOVE 2000 封面

LOVE 2000

遠野ひかる

0:004:23
首页/C Primer Plus/正文

C Primer Plus(第6版)· 第11章:字符串和字符串函数

C 字符串以字符数组为基础,以空字符 \0 表示结束。处理字符串时,要同时关心三件事:字符内容、结束标记,以及数组实际能容纳多少字节。

文章目录 49 个章节

第11章:字符串和字符串函数

C 字符串以字符数组为基础,以空字符 \0 表示结束。处理字符串时,要同时关心三件事:字符内容、结束标记,以及数组实际能容纳多少字节。

本章可以沿着一条完整的数据处理过程理解:为文本准备空间,读取并确认输入完整,再进行比较、复制、查找、排序或数值转换。

1. 字符串的结束位置与数组边界是两回事

1.1 字符串必须以空字符结尾

c
char word[] = "cat";

编译器创建 4 个数组元素:

text
下标:     0      1      2       3
内容:    'c'    'a'    't'     '\0'

字符串长度是 3,数组大小是 4 字节。sizeof(char) 始终为 1,但 C 的一个字节不必在所有实现上都是 8 位。

c
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 字符串长度、数组容量与占用空间

c
char text[20] = "hello";
信息 结果 是否包含结束空字符
strlen(text) 5 不包含
sizeof text 20 表示整个数组的空间
存放当前字符串至少需要的空间 6 字节 包含

数组未被字符串使用的部分不会增加字符串长度。这里初始化列表没有覆盖的其他元素也会被零初始化。

strlen 只查找第一个 \0,不知道数组容量;sizeof 作用于实际数组时给出整个数组大小,不关心里面目前有几个可显示字符。

c
char text[] = "ab\0cd";
// strlen(text) 为 2,sizeof text 为 6。

第一个 \0 后面的字符仍可能存在于数组中,但普通字符串函数会在第一个结束标记处停止。

1.4 为结束标记预留一个位置

c
char good[4] = "cat";
char best[] = "cat";
char raw[3] = "cat";

最后一种写法在 C 中可以创建一个刚好容纳三个字母的数组,但放不下末尾的 \0,所以 raw 不能作为普通 C 字符串使用。

可显示内容的最大长度通常是“容量减 1”。不要因为声明合法,就推断数组里一定形成了合法字符串。

2. 字符数组与字符串指针

2.1 两种声明创建的对象不同

c
char editable[] = "green";
const char *view = "green";

editable 是实际字符数组,可以修改其中的元素;view 是指针变量,保存字符串字面量的首字符地址。

c
editable[0] = 'G';
view = "blue";

// editable = "blue"; // 不能整体赋值数组。
// view[0] = 'B';     // 不能通过 view 修改字符。

字符串字面量具有静态存储期,贯穿程序执行过程。即使写成 char *p = "green" 后编译器接受了声明,尝试通过 p 修改字面量仍然属于未定义行为。

让指针指向字面量时,使用 const char * 可以让编译器帮助阻止这种误用。

2.2 赋值指针不会复制字符

c
const char *first = "hello";
const char *second = first;

这里复制的是地址。两个指针对象不同,但它们指向同一个字符串。

text
first  ─┐
        ├──→ ['h']['e']['l']['l']['o']['\0']
second ─┘

若需要可以独立修改的字符副本,必须准备另一块存储空间,再复制字符串内容。

编译器可以合并相同的字符串字面量,也可以不合并。不要使用字面量地址是否相等来判断文本是否相等。

2.3 相邻字面量会在翻译时拼接

c
const char *message = "first part, "
                      "second part.";

它表示一个连续字符串,适合把较长文本分成多行源码。它不同于运行时调用拼接函数,也不同于在两个指针之间使用 +。

字符串内部的双引号使用 \":

c
puts("She said: \"hello\".");

3. 存放多个字符串的两种布局

3.1 二维字符数组

c
char names[3][12] = {"pear", "apple", "plum"};

这里有 3 行,每行固定占 12 字节。每行都可以保存一个长度不超过 11 的字符串。

text
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 指针数组

c
const char *names[] = {"pear", "apple", "plum"};

数组元素是指针,分别指向相应字符串;这些字符串不必存放在连续位置。

text
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 指针本身不是输入缓冲区

c
char *name;
// scanf("%s", name); // 错误:name 没有指向有效的存储空间。

先创建数组才有位置存放输入:

c
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

c
char word[8];

if (scanf("%7s", word) == 1) {
    puts(word);
}

%7s 最多读取 7 个非空白字符,并额外写入一个 \0。

普通字符数组转换得到的地址已经是 char *,这里传 word,不用写成 &word。后者是指向整个数组的指针,类型不同。

宽度限制只防止本次写满缓冲区,不会替你丢弃单词剩余部分。一个过长单词的后半截,可能成为下一次输入的内容。

%s 会跳过开头空白,也不能完整读取包含空格的书名或姓名;这种需求更适合 fgets。

4.4 fgets 成功后才使用缓冲区

c
char line[80];

if (fgets(line, sizeof line, stdin) != NULL) {
    fputs(line, stdout);
}

对于至少为 2 的容量参数,成功读取时,最多保存“容量减 1”个输入字符,并添加 \0。成功时返回传入的缓冲区地址;没有读到任何字符就遇到文件结束,或发生读取错误时,返回空指针。

读取失败后,不要把缓冲区当作本次已经成功得到的字符串继续处理。需要区分文件结束与读取错误时,可以检查 feof 和 ferror。

4.5 没有读到换行,不一定是输入过长

c
char *newline = strchr(line, '\n');
if (newline != NULL) {
    *newline = '\0';
}

查不到换行时,可能有三种情况:

  • 输入行还有更多字符,本次只装下前缀。
  • 缓冲区恰好装满了正文,下一个输入字符就是换行。
  • 最后一行没有换行,已经到达文件结束。

需要严格判断是否超长时,应继续检查输入,而不是把“没有换行”直接等同于“超长”。

清理剩余输入时,要同时考虑换行和文件结束:

c
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
    continue;
}

getchar 的返回值用 int 保存,才能可靠地区分所有字符值与 EOF。只等待换行的循环,在输入已结束时可能永远停不下来。

5. 一个能报告输入状态的行读取函数

下面的函数面向不含嵌入空字符的普通文本输入。调用时,缓冲区实际容量必须至少为 capacity,且 capacity >= 2。

它删除读到的换行;超长时丢弃本行剩余部分,并返回单独状态,让调用者决定是否接受前缀。

c
#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;
}

输入:

text
cat
1234567
12345678
ok

输出:

text
[cat]
[1234567]
too long
[ok]

长度为 7 的正文刚好装进数组,后面的换行由额外读取处理;长度为 8 的正文才真正超出容量。空行会成功返回空字符串,和“没有任何数据可读”是不同状态。

6. 字符串输出与逐字符处理

6.1 三种输出接口

调用 行为
puts(text) 输出到标准输出,额外添加换行
fputs(text, stdout) 输出到指定流,不额外添加换行
printf("%s", text) 按格式输出,不自动换行

这些接口都不会把终止空字符本身显示出来。它们的返回值用于报告输出结果,其中 fputs 返回的是整数状态,不是字符指针。

fgets 已保存换行时,再交给 puts 输出,往往会多出一行空行。要么保留换行并使用 fputs,要么先删除换行再使用 puts。

把外部文本作为数据传入固定格式:

c
printf("%s", text);

不要直接使用 printf(text)。文本中的 % 可能被当作格式指令解释。

6.2 遍历到结束标记

c
void show_text(const char *text)
{
    while (*text != '\0') {
        putchar((unsigned char)*text);
        ++text;
    }
}

while (*text) 是同一种判断的简写。指针在移动,原字符没有改变;形参指针的移动也不会改变调用方保存的指针。

字符串函数可以不额外接收长度,是因为输入约定中已经有结束标记。但这不代表函数能自动验证缓冲区边界;传入的对象必须确实包含可访问的 \0。

7. 复制与拼接:目标空间由调用者保证

以下字符串函数声明在 <string.h> 中。复制和拼接时,源范围与目标范围不能重叠;需要处理重叠字节范围时,应使用适当的 memmove 操作。

7.1 strlen 只统计第一个空字符之前的长度

c
size_t length = strlen("hello");

结果类型为 size_t,输出使用 %zu。其参数必须已经是合法字符串;不能拿 strlen 去探测一个未知数组中是否恰好存在结束标记。

在循环中反复调用 strlen(text),可能反复扫描同一字符串。若内容没有变化,可以先保存长度,或者直接遍历到 \0。

7.2 strcpy 复制完整字符串,包括结束标记

c
char target[16];
strcpy(target, "hello");

strcpy(target, source) 的目标在前,源在后,返回目标起始地址。它会复制源中的 \0,但不会获得目标容量信息。

必须保证目标空间至少有 strlen(source) + 1 字节,而且可以写入。

c
char *target;
// strcpy(target, "hello"); // 错误:尚未提供目标空间。

目标也可以从某个有效偏移开始,例如 strcpy(buffer + 3, source),但需要按剩余容量重新计算空间。

7.3 strncpy 不保证结果一定是字符串

c
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 从目标字符串末尾追加

c
char message[20] = "hello";
strcat(message, " world");

调用前,目标必须已经是以 \0 结尾的字符串。strcat 找到目标末尾,把源字符串接过去,覆盖原结束标记,再写入新的结束标记。

text
之前:h e l l o \0
追加:          空格 w o r l d \0
之后:h e l l o 空格 w o r l d \0

所需容量至少为:

text
原目标长度 + 源长度 + 1

把一个未初始化字符数组直接交给 strcat 是错误的。若要从空字符串开始,可写 char message[20] = "";。

7.5 strncat 的第三个参数是最大追加数

c
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 比较内容,== 比较地址

c
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 可以检查前缀

c
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 的起始位置。

c
const char *filename = "report.txt";
const char *dot = strrchr(filename, '.');

if (dot != NULL) {
    printf("extension: %s\n", dot + 1);
}

返回指针指向原对象内部,没有复制字符串。查找函数不会把字面量变成可修改对象:即使某个接口返回 char *,也不能据此修改原本只读的字符串。

8.4 sprintf 把格式化结果写入数组

c
char description[80];
sprintf(description, "%s: %d", "count", 12);

它与 printf 使用相似的格式,但目标是字符缓冲区。返回成功写出的字符数,不包含末尾 \0。它没有容量参数,调用者必须保证空间足够;字段宽度通常表示最小宽度,也不能替代容量限制。

需要显式限制目标容量时,可以使用 C99 的 snprintf:

c
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> 提供字符分类和大小写转换。对字符串逐个字符调用,就能完成整段文本的处理。

c
#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 一边遍历,一边统计

c
size_t text_length(const char *text)
{
    size_t length = 0;

    while (text[length] != '\0') {
        ++length;
    }

    return length;
}

读取操作使用 const char *;需要修改文本时才去掉所指字符的 const 限定。

10.2 用读写两个位置删除字符

c
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 逆序时先处理空字符串

c
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 数据内容与显示顺序可以分开

如果只想按不同顺序查看字符串,可以保留原字符数组,再创建一个指针数组作为索引。

text
原始数据:                排序后的索引:
input[0] → "pear"         order[0] ──→ input[1]
input[1] → "apple"        order[1] ──→ input[2]
input[2] → "plum"         order[2] ──→ input[0]

排序只交换指针,不必复制整行字符,原始数据的存放顺序也得到保留。

11.2 完整的选择排序示例

c
#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;
        }
    }
}

输出:

text
sorted:
apple
pear
plum
original:
pear
apple
plum

每轮外层循环选定一个结果位置;内层循环找出剩余字符串中排序最靠前的一项;最后交换相应指针。

这里 const char *texts[] 约束的是字符访问,数组元素中的指针仍然可以交换。若把每个指针本身也限定为 const,就不能进行这种排序。

选择排序需要二次量级的比较次数,每次字符串比较还可能检查多个字符。它适合解释算法结构,较大规模排序可以使用适当的库函数。

如果原始字符串位于局部数组中,索引指针也不能在那个数组生存期结束后继续使用。重排指针没有延长字符对象的生存期。

12. 命令行参数也是字符串

12.1 带参数的 main

c
int main(int argc, char *argv[])

第二个形参也可以写成 char **argv。

项目 含义
argc 参数字符串数量,通常包含程序名称
argv[0] 在 argc > 0 时可访问,表示程序名称;名称不可用时可能为空字符串
argv[1] 第一个用户参数,访问前要求 argc > 1
argv[argc] 空指针,是参数指针序列的结束位置

例如:

text
./tool hello "two words"

在通常的命令行环境中,会得到三个参数字符串:程序名、hello、two words。引号如何分组由运行环境处理,程序接收的是已经组织好的字符串。

12.2 遍历参数

c
#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 的三个参数

c
char *end;
long value = strtol(text, &end, 10);
  • text:待解析的字符串。
  • &end:让函数把停止转换的位置写入 end。
  • 10:按十进制解释输入。

它会跳过开头空白,允许正负号,再读取当前进制允许的数字。

text
输入 "123abc",十进制转换:
value = 123
end 指向 'a'

常见判断:

判断 含义
end == text 没有发生任何有效数字转换
*end == '\0' 转换正好到字符串末尾
*end != '\0' 仍有未转换内容,需要决定是否接受
errno == ERANGE 本次转换发生范围错误,调用前应先清零 errno

进制可以指定为 2 到 36,或使用 0 让函数按前缀判断。按本章的 C99/C11 规则,自动判断涉及普通十进制、前导零的八进制以及 0x 前缀的十六进制。若产品输入只允许十进制,直接传 10 更清楚。

13.3 完整示例:严格解析一个 int

下面的程序允许数字两侧存在空白,拒绝多余字符、空输入以及超范围结果:

c
#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;
}

运行示例:

text
./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,并不意味着它知道目标还有多少空间。

Aron 的清风日常

作者 Aron · 发布于 2026.08.07

愿这份记录能为你提供一点参考。发现问题,欢迎通过 GitHub 或 邮件交流。

如果这篇记录对你有用,留下一份喜欢。

← 所有文章继续阅读C Primer Plus(第6版)· 附录整理:语法与标准库速查 →
SEARCH MY NOTES

在记录里,找一点答案

搜索标题、标签与全文 · 按 Esc 关闭