跳到主要内容
🔍
1.2.6已发布beginner · 约 3 课时 · P0

字符串与安全函数

理解 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。

① 学习目标

  1. 说清 C 字符串的 '\0' 结尾本质,以及"字符数组"与"字符串"的区别;
  2. 正确使用 strlen/strcmp/strcpy/strcat/strstr,并独立手写 my_strlen 与 my_strcpy;
  3. 说出 gets 的缓冲区溢出机理与 C11 移除事实,并用 fgets(buf, sizeof(buf), stdin) 及其换行处理替代;
  4. 说出 strncpy 不保证 '\0' 结尾的坑,与 snprintf 的截断判断方法;
  5. 知道 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 / sizeofstrlen 数到 '\0' 为止(不含它);sizeof 是数组声明大小(含 '\0' 与未用空间)——两者不能混用
strcmp按字典序比较,返回负数/0/正数;实现只保证符号不保证具体值(本机 gcc 返回 -1,别依赖它)
strcpy / strcat复制/追加字符串,都必须连同 '\0' 一起处理;且不检查目标容量——目标必须足够大
strncpy 的坑src 长度达到 n 时不写结尾 '\0',目标可能不是合法字符串;用前手动补 dst[n-1] = '\0',或改用 snprintf
snprintfsnprintf(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),潜在问题是什么?(单选)

    ◌ 未作答

⑨ 章节测验

章节测验

5 题题库 · 随机抽 5 题 · 及格线 60 分 · 前端判分(学习自测)
开始测验 →

⑩ 实战任务

实践任务

手写 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)

把本章技能迁移到 LinuxWindows

环节Windows(MinGW)Linux说明
字符串函数语义一致一致strlen/strcpy 等按 C 标准实现
文本行尾\r\n\nfgets 保留行尾:去除时用 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)