字符串与安全函数
理解 C 字符串的 \0 结尾本质,掌握常用字符串函数与安全替代方案(fgets/snprintf),并知晓 gets 的溢出机理
- 来源
- 补充
- 纠错
- 更新
标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原
完成标准(本章)
- 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 180 秒
- ✏️ 已练习:小练习正确率 ≥ 60%
- 📝 已通过测验:分数 ≥ 60 分
- 🛠️ 已掌握还需完成实践任务
1.2.6 字符串与安全函数
本章来源:\0 结尾、字符数组与字符串、strlen/strcmp/strcpy/strcat/strstr/strtok 基本用法来自《第一阶段讲义》18.1-18.3 节【来源】,经重新组织表述;gets 用法已【纠错】(err-7.2-2:C11 已移除,一律 fgets)、atoi 头文件归属已【纠错】(err-7.3-15:属 stdlib.h);安全替代方案(fgets 换行处理、snprintf 截断判断、strncpy 不保证 \0 的坑)与 gets 溢出机理讲解为新编【补充】(约 15%)。平台适用性 universal。
① 学习目标
- 说清 C 字符串的
'\0'结尾本质,以及"字符数组"与"字符串"的区别; - 正确使用 strlen/strcmp/strcpy/strcat/strstr,并独立手写 my_strlen 与 my_strcpy;
- 说出 gets 的缓冲区溢出机理与 C11 移除事实,并用
fgets(buf, sizeof(buf), stdin)及其换行处理替代; - 说出 strncpy 不保证
'\0'结尾的坑,与 snprintf 的截断判断方法; - 知道 atoi/atol/atof 属于 stdlib.h,并完成力扣字符串题(验证回文串)。
② 前置知识
- 必选:1.2.5 数组(字符数组是本章一切操作的基础;越界写的后果在数组章讲过下标边界,本章讲字符串函数的边界);
- 建议:1.1.1 GCC 四阶段(本章示例编译命令沿用
-std=c11 -Wall -Wextra -Wpedantic)。
③ 核心概念【来源】
| 概念 | 说明 |
|---|---|
'\0' 结尾 | C 字符串是"以 '\0'(数值 0)结尾的字符数组";strlen/strcpy/printf("%s") 都靠它判断终点——丢了它,任何字符串操作都会越界 |
| 字符数组 vs 字符串 | char s[5] = "hi" 是含字符串的字符数组;没有 '\0' 的字符数组不是字符串,不能当字符串使用 |
| strlen / sizeof | strlen 数到 '\0' 为止(不含它);sizeof 是数组声明大小(含 '\0' 与未用空间)——两者不能混用 |
| strcmp | 按字典序比较,返回负数/0/正数;实现只保证符号不保证具体值(本机 gcc 返回 -1,别依赖它) |
| strcpy / strcat | 复制/追加字符串,都必须连同 '\0' 一起处理;且不检查目标容量——目标必须足够大 |
| strncpy 的坑 | src 长度达到 n 时不写结尾 '\0',目标可能不是合法字符串;用前手动补 dst[n-1] = '\0',或改用 snprintf |
| snprintf | snprintf(dst, n, ...) 写入必受 n 限制且保证 '\0' 结尾;返回"本应写入"的字符数,返回值 >= n 即发生截断 |
| 【纠错】atoi 族头文件 | atoi/atol/atof 声明在 stdlib.h(不是 string.h);string.h 是 strlen/strcpy 等字符串函数(err-7.3-15) |
| 【更新】gets 已移除 | gets 无法限长、必然存在溢出风险,C11 已将其从标准删除;现代编译器仍可能提供兼容实现并告警——一律不用(err-7.2-2) |
④ 通俗解释【补充】
'\0'是"到此为止"路牌:字符串操作员(strlen/strcpy)走路只认路牌,不认围墙(数组边界)。路牌被冲走(覆盖),它们就会一直走到围墙外面去——这就是字符串越界;- gets 是没有长度限制的"无限装货":顾客说多长就装多长,货物溢出车厢直接压坏邻居的摊位(栈上相邻变量)——所以被 C11 永久停业;
- fgets 是"限重装货":装到限重(sizeof(buf)-1)就停,但会顺手把"回车小票"(
'\n')也装进去,结账时记得撕掉; - strncpy 是"偷懒的抄写员":抄满 n 个字就下班,忘了写"到此为止"路牌——抄完要么自己补路牌,要么换 snprintf 这位负责任的抄写员(必写路牌还报"本应写多少")。
⑤ 示例代码
代码示例与验证记录
- examples/ex1-string-ops.c常用字符串函数(strlen/sizeof/strcmp/strcpy/strcat/strstr)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16)编译:
gcc ex1-string-ops.c -o ex1 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
strlen(s1) = 5(不含结尾 \0) sizeof(s1) = 6(数组总字节数,含 \0) strcmp(s1, s2) = -1(负值:s1 按字典序小于 s2) 拼接后: hello, world(长度 12) strstr("hello, world", "wor") -> world差异说明:编译 0 警告;strcmp 的返回值实现只保证正/负/零(本机为 -1,其他实现可能是 -15 等),判断时只依赖符号
完整源码见 /code 代码示例页
- examples/ex2-my-strlen-strcpy.c手写 my_strlen / my_strcpy(\0 处理与断言验证)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16)编译:
gcc ex2-my-strlen-strcpy.c -o ex2 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
my_strlen("embedded linux") = 14(与标准库一致,断言通过) my_strcpy 后 buf = "embedded linux"(含结尾 \0,strcmp 断言通过)差异说明:编译 0 警告;断言(assert)默认开启——若定义 NDEBUG 则断言被移除,本示例未定义 NDEBUG
完整源码见 /code 代码示例页
- examples/ex3-fgets-safe.cfgets 读行 + 去除换行符 + snprintf 拼接(安全替代)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16)编译:
gcc ex3-fgets-safe.c -o ex3 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
请输入名字(最多 15 字符): 请输入一句话: Tom 说:hello world(本行内容长 11) snprintf 返回值 = 45(本应写入的字符数;>= 96 即发生截断)
差异说明:输入为 'Tom' 与 'hello world' 两行(管道重定向实测,故提示语与输出同行);snprintf 返回值 45 = 本应写入字符数(未截断)。fgets 受 sizeof 限制,不存在 gets 式的越界写
完整源码见 /code 代码示例页
- examples/ex4-gets-danger.txtgets() 危险机理静态讲解(conceptual:不执行危险输入)概念讲解示例✓ 文档核对(非执行)· C11 标准与缓冲区溢出原理核对, 2026-08-16编译:
无(conceptual 讲解文本,不编译不运行)适用环境:LinuxWindows(MinGW)展开文档核对记录
(无运行输出;讲解内容见文件)
差异说明:依据 C11 标准 7.21.7.7(gets 已移除)与缓冲区溢出原理;纪律:不通过执行危险代码演示未定义行为
完整源码见 /code 代码示例页
⑥ 编译与运行方法
三个可执行示例编译命令一致:gcc <文件>.c -o <输出> -std=c11 -Wall -Wextra -Wpedantic,本机实测 0 警告;ex3 需要两行输入(键盘或重定向均可)。ex4 为概念讲解文本(不编译不运行):gets 的溢出机理只做静态说明,不通过执行危险代码演示。
⑦ 常见错误
| 症状 | 原因 | 解决 |
|---|---|---|
| 【纠错】代码里出现 gets(buf) | C11 已移除该函数(无长度检查 → 缓冲区溢出) | 一律 fgets(buf, sizeof(buf), stdin) 并处理保留的 '\n'(err-7.2-2) |
| 【纠错】atoi 报未声明 | 错 #include <string.h> | #include <stdlib.h>(atoi/atol/atof 的归属,err-7.3-15) |
| strncpy 后 printf("%s") 输出"长串尾巴" | src 达 n 时没写结尾 '\0' | 手动 dst[n-1] = '\0',或改用 snprintf |
| 用 sizeof(指针) 当字符串长度 | 数组退化为指针后 sizeof 是指针大小(8 字节) | 用 strlen;缓冲容量用 sizeof(数组) |
| fgets 后输出莫名多空行 | fgets 保留行尾 '\n' | s[strcspn(s, "\r\n")] = '\0' 去除 |
| 中文字符串长度"不对" | strlen 数的是字节数(UTF-8 下 1 个汉字 3 字节) | 理解字节与字符的区别(跨平台迁移训练) |
⑧ 小练习
小练习
学习自测:提交后才显示答案与解析(前端判分,不作为正式考试)ex-1-2-6-1.char s[10] = "hello"; strlen(s) 与 sizeof(s) 分别是?(单选)
◌ 未作答ex-1-2-6-2.用 fgets(buf, 8, stdin) 读入一行 "hello\n" 后,buf 的内容是?(单选)
◌ 未作答ex-1-2-6-3.代码用 strncpy(dst, src, sizeof(dst)) 拷贝,且 strlen(src) >= sizeof(dst),潜在问题是什么?(单选)
◌ 未作答
⑨ 章节测验
章节测验
⑩ 实战任务
实践任务
手写 strcpy 并用 fgets 重写一个带溢出风险的输入程序
1) 独立实现 my_strcpy(连同结尾 \0 一起复制)并用断言与标准库 strcpy 对照验证; 2) 把一段使用 gets 风格的"读字符串"程序改写为 fgets 版本(含去除换行符处理), 验证长输入不会越界写、换行处理正确。
输入与输出
交付物:① my_strcpy 源码(可与本章 ex2 对照,但不能照抄);② 改写后的 fgets 版程序; ③ 一份 3 句话以内的说明:为什么 fgets 版本不存在缓冲区溢出。
功能要求
- 实现 my_strcpy,含结尾 \0 的复制
- 用 assert 与标准库 strcpy 对照验证(至少两个不同长度的字符串)
- 把 gets 风格读取改写为 fgets(buf, sizeof(buf), stdin),并正确处理保留的换行符
- 用超过缓冲区长度的输入实测:程序不崩溃、不越界写
限制条件
- 不使用 gets(C11 已移除)
- 长输入实测用重定向/文件输入,不依赖手工输入
验收步骤(自检清单 0/4)
验收标准
- my_strcpy 与标准库 strcpy 对至少 2 个字符串输出一致(验收步骤 1)
- fgets 版在超长输入下不崩溃、输出被正确截断或完整处理(验收步骤 3)
- 安全性说明能说清"写入受 sizeof(buf) 限制"(验收步骤 4)
常见失败原因
- my_strcpy 漏复制结尾 \0(目标不是合法字符串)
- fgets 后忘记去换行,输出多出空行
- 缓冲区大小手写常量(如 fgets(buf, 16, ...))而不是 sizeof(buf)
可选扩展
- 再实现 my_strcat,并说明它与 strcpy 的差异
- 用 snprintf 重写拼接逻辑,观察返回值与截断判断
完成必要清单后才能计入"已完成实践"(学习状态自动推导,不提供一键完成)
⑪ 面试问题
面试问题
为什么 gets() 危险?它的正确替代是什么?高频C/C++ · medium
要点:gets 无法指定长度,超长输入必然缓冲区溢出;C11 已移除。替代:fgets(buf, sizeof(buf), stdin) 并处理其保留的换行符。
gets 只有一个参数(缓冲区指针),写入长度完全由输入决定:输入超过缓冲区容量时继续越过边界 写入相邻内存(其他局部变量、返回地址),即缓冲区溢出——历史上最著名的安全漏洞类型之一 (1988 年 Morris 蠕虫利用的正是 gets)。C11 已将其从标准移除。替代方案 fgets(buf, n, stdin) 最多读 n-1 个字符并保留行尾换行符,写入受 sizeof(buf) 限制;使用后按需去掉换行符。 加分项:提到 snprintf 的截断判断、以及 strncpy 不保证 \0 的坑。
追问:- 追问:fgets 和 gets 在换行符处理上有什么区别?(fgets 保留 \n,gets 丢弃)
- 追问:如果必须用可变长度读行怎么办?(POSIX 的 getline,或循环 fgets 拼接)
评分要点:- 无长度检查 → 溢出机理
- fgets 替代与换行处理
- C11 移除的事实
strncpy 安全吗?它有什么坑?高频C/C++ · medium
要点:不完全安全:src 长度达到 n 时不写结尾 \0,目标可能不是合法字符串,后续按字符串使用会越界读。
strncpy(dst, src, n) 复制最多 n 个字符:若 src 长度 >= n,则不会写入结尾 \0, dst 就不再是合法 C 字符串——后续 strlen/printf("%s") 会一路读到越界为止。 此外若 src 比 n 短,strncpy 会用 \0 填满剩余部分(性能问题)。安全的替代: ① 先保证 dst 有足够空间再 strcpy;② snprintf(dst, n, "%s", src)(必写 \0,还能判断截断); ③ 用 strlcpy(BSD 系,非标准)。如果用了 strncpy,务必手动补 dst[n-1] = '\0'。
追问:- 追问:snprintf 和 strncpy 在截断判断上有什么不同?(snprintf 返回值即'本应写入'长度)
评分要点:- \0 不保证的机理
- 手动补 \0 或换 snprintf
- 填零的性能副作用
strlen 和 sizeof 在字符串上下文里有什么区别?高频C/C++ · easy
要点:strlen 是运行期函数,返回到结尾 \0 为止的字符数(不含 \0);sizeof 是编译期运算符,返回对象占用的字节数(含 \0 与未用空间)。
char s[10] = "hello":strlen(s) = 5(运行时数到 \0),sizeof(s) = 10(数组声明大小)。 典型错误:用 sizeof(s) 判断字符串长度(把未用空间算进去);对指针 char *p,sizeof(p) 是指针大小(64 位系统 8 字节)而不是字符串长度——数组退化为指针后 strlen 才能得到长度。 另外中文按 UTF-8 编码时每个汉字 3 字节,strlen 数的是字节数不是"字符数"。
追问:- 追问:sizeof("abc") 是多少?(4:字符串字面量含结尾 \0)
评分要点:- 运行期 vs 编译期
- 指针与数组的 sizeof 区别
- 字节数与字符数(UTF-8)
⑫ 延伸阅读
- 《C 程序设计语言(K&R)》第 5 章(指针与数组、字符串库函数实现思路);
- C 标准库参考:
man 3 string/ cppreference.com 的 string 页面(只引名称); - 力扣 344 反转字符串已在 1.3.1 作为指针章主选题——学完指针后回头做,交叉引用不重复设必做;
- 下一章预告:1.2.7 输入输出深入 scanf/printf——格式化输入输出的坑与 snprintf 工程实践。
力扣推荐题
力扣推荐题
外部题目为「引用与推荐」模式(reference_only):仅保留题号、题名、难度与推荐理由,完整题面与提交请前往力扣;完成状态为自报记录,未与力扣账户验证,不影响章节掌握度判定。
- #125 验证回文串Valid Palindrome推荐练习(适配度 80)力扣难度:easy课程难度:L1预计 30 分钟章节必做前置:无推荐理由:
- 只需字符数组下标遍历与大小写/字母数字判断,无指针前置
- 字符串遍历是本章核心技能
- c语言解法不越级
匹配知识点:string - #14 最长公共前缀Longest Common Prefix可选拓展(适配度 65)力扣难度:easy课程难度:L2预计 30 分钟前置:无推荐理由:
- 二维字符数组/字符串数组基本操作
- 无指针前置
匹配知识点:string
勘误与纠错
本章勘误与更新记录(【纠错】/【更新】)
err-7.2-2 · 过时更新 · 出处 18.4 节(第一阶段讲义)
原文:资料使用 gets() 读字符串
正确:gets() 在 C11 已移除(无法指定长度、必然存在缓冲区溢出风险);一律用 fgets(buf, sizeof(buf), stdin) 并处理其保留的换行符
原因:第一步报告 7.2 第 2 条:gets 无法限长是历史安全漏洞根源,讲解必须加'禁止使用'警示并替换为 fgets
err-7.3-15 · 表述修正 · 出处 18.2 节
原文:资料把 atoi 系列列在 string.h 标题下
正确:atoi/atol/atof 属于 stdlib.h(string.h 是 strlen/strcpy 等字符串函数);使用时 #include <stdlib.h>
原因:第一步报告 7.3 第 15 条:头文件归属错误会让初学者编译报错或查错手册
迁移训练(migration training)
把本章技能迁移到 Linux 与 Windows:
| 环节 | Windows(MinGW) | Linux | 说明 |
|---|---|---|---|
| 字符串函数语义 | 一致 | 一致 | strlen/strcpy 等按 C 标准实现 |
| 文本行尾 | \r\n | \n | fgets 保留行尾:去除时用 strcspn(s, "\r\n") 一次兼容两边 |
| 中文长度 | 字节数(UTF-8) | 字节数(UTF-8) | strlen 数字节;字符数需按编码计算 |
| gets | 已移除(部分库残留) | 已移除 | 一律 fgets |
不变的:'\0' 结尾、全部字符串函数语义、溢出机理;要改的:换行符细节与编码认知。
内容来源映射
| 内容部分 | 资料 | 位置 | 标记 | 说明 |
|---|---|---|---|---|
| 字符串以 \0 结尾、字符数组与字符串、strlen/strcmp/strcpy/strcat/strstr/strtok 基本用法 | 第一阶段讲义 | 18.1/18.2/18.3 节 | 【来源】 | 正文在原资料基础上重新组织表述,未大段复制原文 |
| gets() 用法(错误示范) | 第一阶段讲义 | 18.4 节 | 【纠错】 | 见勘误 err-7.2-2:gets 已在 C11 移除,正文一律用 fgets 并加"禁止使用"警示 |
| atoi 系列头文件归属(错列在 string.h 下) | 第一阶段讲义 | 18.2 节 | 【纠错】 | 见勘误 err-7.3-15:atoi/atol/atof 属 stdlib.h |
| 安全替代方案(fgets 换行处理、snprintf 截断判断、strncpy 不保证 \0 的坑)、gets 溢出机理静态讲解、实验与练习 | 无 | 【补充】 | 原资料未覆盖安全函数细节;本章新编约 15%(实验为本机 gcc 15.2.0 实测) | |
| 小练习 / 章节测验 / 实践任务 / 面试问题 / 延伸阅读 / 力扣题映射 | 无 | 【补充】 | 原资料该章无成体系练习,全部新编;力扣 2 题为原创适配分析(reference_only) |