字符串和字符串函数
掌握字符串表示与I/O、string.h函数、字符串指针排序、命令行参数和可诊断的字符串转数值
学习目标
- 能解释 C 字符串为何必须以
\0结尾,并说明strlen与sizeof对同一数组为何常差 1 - 能正确使用
strlen、strcpy、strcat、strcmp,并判断fgets是否读到完整记录 - 能用字符串指针数组和 qsort 排序词表,并解释比较器为何接收二级指针
- 能验证 argc/argv 命令行参数,并用 strtol 区分无数字、尾随垃圾和范围错误
机制总览
字符串和字符串函数:机制路径
- 1
为什么需要专门的「字符串函数」?
上一章你已经知道:C 里没有 string 类型,文本就是 字符数组(character array) 。但「知道地址从哪开始」还不够——你还得知道「到哪结束」。
- 2
字符串表示:可见字符 + 看不见的 \0
编译器放入 4 个字符: C 、 a 、 t 、 \0 。数组总长 10,剩余 6 个字节自动填 \0 (若未初始化则内容不确定,但字符串有效部分仍到第一个 \0 为止)。
- 3
string.h 四大基础函数
参数必须是 以 \0 结尾 的有效字符串。若数组中间没有 \0 , strlen 会一直往后读——危险。
章级决策实验
字符串和字符串函数:机制与证据
切换《字符串和字符串函数》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么需要专门的「字符串函数」?
上一章你已经知道:C 里没有 string 类型,文本就是 字符数组(character array) 。但「知道地址从哪开始」还不够——你还得知道「到哪结束」。
可核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么需要专门的「字符串函数」?」的实际行为。
学完《字符串和字符串函数》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
字符串和字符串函数:失效与核验
为什么需要专门的「字符串函数」?
典型失效
若只记语法而忽略「为什么需要专门的「字符串函数」?」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么需要专门的「字符串函数」?」的实际行为。
字符串表示:可见字符 + 看不见的 \0
典型失效
若只记语法而忽略「字符串表示:可见字符 + 看不见的 \0」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「字符串表示:可见字符 + 看不见的 \0」的实际行为。
string.h 四大基础函数
典型失效
若只记语法而忽略「string.h 四大基础函数」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「string.h 四大基础函数」的实际行为。
为什么需要专门的「字符串函数」?
上一章你已经知道:C 里没有 string 类型,文本就是 ↡元素类型为 char 的数组,通常以空字符 \0 标记结尾,表示一段可打印文本。。但「知道地址从哪开始」还不够——你还得知道「到哪结束」。
想象一排信箱:每个格子里一个字母,最后一格贴着「到此为止」的标签。如果没有这个标签,邮递员(strlen、strcpy)会一直往后走,直到误闯邻居家的信箱——那就是 未定义行为,轻则乱码,重则程序崩溃。
这一章把 \0 铁律、string.h 四大函数、安全读写的 fgets,以及自己写字符串处理函数的方法讲清楚。搞懂这些,后面结构体、文件 I/O、动态内存都会轻松很多。
字符串表示:可见字符 + 看不见的 \0
↡ASCII 值为 0 的特殊字符,写作 \0。C 用它标记字符串结束,本身不计入 strlen 返回的长度。 是 C 字符串的「终点站」。声明:
char animal[10] = "Cat";编译器放入 4 个字符:C、a、t、\0。数组总长 10,剩余 6 个字节自动填 \0(若未初始化则内容不确定,但字符串有效部分仍到第一个 \0 为止)。
↡定义在 string.h,从 s[0] 开始计数,遇到 \0 停止,返回字符个数(不含 \0)。 从首字符数到 \0 之前;sizeof animal 则是整个数组 10 字节。二者差的那 1(或更多)字节,往往就是结尾的 \0 和未用空间。
#include <stdio.h>
#include <string.h>
int main(void) {
char animal[10] = "Cat";
printf("strlen = %zu, sizeof = %zu\n",
strlen(animal), sizeof(animal));
/* 输出:strlen = 3, sizeof = 10 */
return 0;
}手动逐字符赋值时,你必须自己写上 \0,否则不是合法字符串:
char word[4];
word[0] = 'B';
word[1] = 'i';
word[2] = 'g';
word[3] = '\0'; /* 缺一不可 */string.h 四大基础函数
#include <string.h> 后,最常用的四个函数如下。
strlen:测长度
size_t len = strlen("Hello"); /* 5 */参数必须是以 \0 结尾的有效字符串。若数组中间没有 \0,strlen 会一直往后读——危险。
strcpy 与 strcat:复制与拼接
↡把源字符串(含结尾 \0)复制到 dest,返回 dest。不检查 dest 是否够大。 把 src 逐字节拷到 dest,包括结尾的 \0:
char dest[20];
strcpy(dest, "Hello");
/* dest 现在是 "Hello" */↡找到 dest 中第一个 \0,从该位置起把 src 接上(含 \0)。dest 必须有足够剩余空间。 把 src 接到 dest 已有内容的末尾:
char buf[20] = "Hello";
strcat(buf, ", world!");
/* buf 现在是 "Hello, world!" */猜一猜:若
dest只有 8 字节,却strcpy(dest, "Hello, world!"),会发生什么?用下面 Stepper 看溢出过程。
第一步:目标缓冲区太小
char dest[8] 只能安全存 7 个可见字符 + \0。源串更长时,空间从设计上就不够。
strcmp:字典序比较
↡按 unsigned char 字典序比较两个字符串。相等返回 0;s1 较小返回负值;s1 较大返回正值。 比较的是内容,不是指针地址:
if (strcmp(s1, s2) == 0)
printf("相同\n");
else if (strcmp(s1, s2) < 0)
printf("s1 在字典序中更靠前\n");不要写 if (s1 == s2)——那比较的是两个指针是否指向同一地址,不是字符串内容是否相同。
char 数组 vs char 指针:能不能改?
这是 C 字符串里最容易混淆的一点:
char ar[] = "Hello"; /* 数组:栈上副本,可修改 */
const char *ptr = "Hello"; /* 指针:只读访问字符串字面量 */ar[0] = 'h' 合法;字符串字面量在 C 中具有 char 数组类型,但任何修改尝试都是未定义行为。↡双引号中的文本,在程序中形成带空字符结尾的静态存储期字符数组;是否与相同字面量合并、放在哪个物理段由实现决定。 不保证位于某个特定“只读数据段”,所以规则应写成语言契约而不是内存段猜测。用 const char * 接收能让编译器阻止误写;要可改则用 char ar[] 或拥有足够容量的动态对象。
sizeof 也不同:sizeof(ar) 是数组总字节(含 \0);sizeof(ptr) 是指针变量本身的大小(通常 8 或 4),与字符串长度无关。
函数形参位置的 char s[] 会调整为 char *s,两种声明表示相同参数类型。只读接口应声明 const char *s;否则类型会错误暗示函数可能修改内容。
字符串输入输出:按流读写完整记录
输出:puts 与 fputs
puts(str) 输出字符串并自动换行;fputs(str, stdout) 不自动换行,适合拼到已有输出流:
puts("Hi"); /* Hi + 换行 */
fputs("Hi", stdout); /* 仅 Hi,无换行 */输入:用 fgets,别用 gets
↡从流中读取至多 size-1 个字符,遇到换行时把换行也存入;成功时写入空字符并返回目标指针,输入结束或错误且未读到字符时返回空指针。 接受缓冲区大小,但“调用成功”不等于“整行已读完”:
char line[80];
if (fgets(line, sizeof line, stdin) != NULL) {
char *newline = strchr(line, '\n');
if (newline != NULL) {
*newline = '\0';
printf("完整一行:%s\n", line);
} else {
int ch = getchar();
if (ch == '\n') {
printf("完整一行:%s\n", line);
} else if (ch == EOF) {
if (ferror(stdin))
fputs("读取失败\n", stderr);
else
printf("文件末尾的最后一行:%s\n", line);
} else {
while ((ch = getchar()) != '\n' && ch != EOF)
;
fputs("输入行过长\n", stderr);
}
}
}fgets 保留读到的换行;缓冲区填满前没有换行时,要区分“最后一行没有换行”和“当前记录被截断”。gets() 已被 C11 移除,因为接口根本没有容量参数。
自定义字符串函数
理解库函数原理后,可以自己实现简化版——逻辑都是「找 \0」或「逐字节复制直到 \0」。
手写 my_strlen
size_t my_strlen(const char *s) {
size_t count = 0;
while (s[count] != '\0')
count++;
return count;
}const char * 表示函数只读字符串,不修改内容——好习惯。
手写 my_strcpy
char *my_strcpy(char *dest, const char *src) {
char *save = dest;
while ((*dest++ = *src++) != '\0')
; /* 空循环体:拷贝含 \0 */
return save;
}while ((*dest++ = *src++) != '\0') 是经典 C 惯用法:赋值、比较、指针后移一步完成。
它与标准 strcpy 一样要求 dest 容量足够,并要求源与目标对象不重叠;接口没有容量参数,函数自身无法验证这两个前置条件。
场景:读一行并统计单词数(简化)
#include <stdio.h>
#include <ctype.h>
int count_words(const char *s) {
int in_word = 0, count = 0;
while (*s) {
if (isspace((unsigned char)*s))
in_word = 0;
else if (!in_word) {
in_word = 1;
count++;
}
s++;
}
return count;
}用 isspace 判断空白,用状态机 in_word 检测「从非空白进入空白」的边界——这就是自定义字符串处理的典型模式。
排序字符串:排序指针而不是复制字符
字符串列表常表示为指针数组。↡让字符串指针数组按 strcmp 顺序重排;排序交换的是指针元素,字符串字节本身不移动。 可以交给 qsort:
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
static int compare_words(const void *lhs, const void *rhs) {
const char *left = *(const char *const *)lhs;
const char *right = *(const char *const *)rhs;
return strcmp(left, right);
}
int main(void) {
const char *words[] = {"pear", "apple", "plum"};
size_t count = sizeof words / sizeof words[0];
qsort(words, count, sizeof words[0], compare_words);
return strcmp(words[0], "apple") != 0;
}qsort 把“数组元素的地址”传给比较器。这里元素本身是 const char *,所以参数要解释为指向该指针元素的指针,再解引用一次取得字符串。排序只改变 words 中各指针的顺序,不会尝试修改字符串字面量。strcmp 是逐字节字典序,不是面向 locale 的自然语言排序。
命令行参数:argc 与 argv 的边界
宿主环境可以用 main(int argc, char *argv[]) 提供 ↡程序启动时由宿主环境传入的字符串数组;argc 给出参数数量,argv 保存各参数指针,并以空指针哨兵结束。。在宿主提供这些信息时,argc 非负,argv[argc] 是空指针;argv[0] 通常表示程序调用名,但它也可能是空字符串,不能拿来做安全判断。
#include <stdio.h>
int main(int argc, char *argv[]) {
int i;
if (argc < 2) {
fprintf(stderr, "usage: %s WORD...\n",
argc > 0 ? argv[0] : "program");
return 2;
}
for (i = 1; i < argc; ++i)
printf("argv[%d] = %s\n", i, argv[i]);
return 0;
}argc 是元素计数,不含末尾空指针;业务参数从 argv[1] 开始。访问前先验证 argc,不能假定用户一定传了某个位置。虽然 C 允许修改 argv 指向的字符串内容,接口设计通常把它们当输入,并在需要长期保存或重排内容时复制到自有存储。
字符串转数字:用 strtol 保留失败原因
atoi 无法把合法的 0 与转换失败区分开,也没有可用的结束位置和可靠范围诊断。↡把字符串前缀按指定进制转换为 long,并通过 endptr 返回首个未转换字符;范围错误通过 errno 的 ERANGE 报告。 允许调用方逐层验证:
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
static int parse_int(const char *text, int *result) {
char *end;
long value;
errno = 0;
value = strtol(text, &end, 10);
if (end == text) /* 一个数字也没读到 */
return 0;
if (*end != '\0') /* 有未接受的尾随字符 */
return 0;
if (errno == ERANGE || value < INT_MIN || value > INT_MAX)
return 0;
*result = (int)value;
return 1;
}调用前把 errno 清零,只在返回后结合结果检查 ERANGE;不能仅看 errno 的旧值。end == text 表示没有转换,*end 检查是否完整消费字符串,再检查 long 到 int 的目标范围。若产品允许尾随空白,应显式用 isspace((unsigned char)*end) 跳过后再要求结束;进制参数用 10 表示十进制,用 0 才会按前缀识别八进制或十六进制。
容易踩的坑
小结
- C 字符串 = 连续
char+ 结尾\0;strlen数到\0前,sizeof数整个数组 strcpy/strcat不检查目标大小;strcmp返回 0 表示相等char ar[]创建可修改副本;字符串字面量应经 const char 指针只读访问- fgets 成功后仍要判断换行、文件末尾与截断;输出可用 puts / fputs
- qsort 排字符串指针时交换指针元素,比较器需先解引用到字符串再调用 strcmp
- 命令行参数先检查 argc;字符串转整数用 strtol 检查 endptr、errno 和目标范围
- 自定义函数核心:找
\0、逐字节复制、用const char *保护只读参数
练习
问题 1(改代码型) 下面程序想比较用户输入与 "yes" 是否相同,但有 bug。改正后输入 yes 应打印 匹配。
char answer[10];
fgets(answer, sizeof answer, stdin);
if (answer == "yes")
printf("匹配\n");问题 2(问答型) char a[] = "Hi"; 与 const char *b = "Hi"; 执行 sizeof(a) 和 sizeof(b) 通常各得多少(64 位系统)?为什么?
问题 3(综合实现型) 不用 strlen,写函数 size_t str_len(const char *s),并在 main 里测试 "Cat" 与 ""。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 字符数组(character array)
元素类型为 char 的数组,用来存文本。C 字符串要求以 \0 结尾。声明如 char s[20] = "Hi";。详见 CStringNullTerminatorDiagram。
- 空字符(null character)
ASCII 0,写作 \0。标记 C 字符串结束。strlen 数到它之前停止;string.h 函数都依赖它判断边界。
- strlen()
string.h 函数,返回字符串中可见字符个数(不含 \0)。参数必须指向以 \0 结尾的有效字符串。
- strcpy()
把源串(含 \0)复制到 dest。不检查 dest 大小,目标必须足够大。详见 StrcpyBufferDiagram。
- strcat()
把 src 接到 dest 已有字符串末尾。需保证 dest 剩余空间 ≥ strlen(src)+1。
- strcmp()
字典序比较两串。相等返回 0;不要用 == 比较字符串内容。详见 StringFunctionTableDiagram。
- fgets()
从流读取至多 size-1 个字符;成功时写入空字符并返回目标指针。调用方仍须判断是否读到换行、无换行的文件末尾或被缓冲区截断的记录。
- 字符串字面量
双引号中的文本,在程序中形成带空字符结尾的静态存储期字符数组。C 不给它 const 类型,但任何修改行为未定义;物理存放位置由实现决定。
- 字符串指针排序
对字符串指针数组重排指针元素,而不复制各字符串字节。qsort 比较器收到元素地址,需先解引用为字符串指针再调用 strcmp。
- 命令行参数
程序启动时由宿主环境提供的字符串数组。argc 给出参数数量,argv 保存指针,argv[argc] 是空指针哨兵;访问业务参数前应检查 argc。
- strtol()
把字符串前缀转换为 long,并通过 endptr 返回首个未转换字符。可靠解析需检查是否读到数字、尾部策略、errno 的 ERANGE 和目标类型范围。
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
字符串转数字:机制、边界与证据
在《字符串和字符串函数》的官方单元 cpr-11 中,字符串转数字连接本章第 6 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。