C 库函数 – gets()(建议收藏)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观
前言:为什么学习 gets() 函数?
在 C 语言编程中,输入与输出(I/O)操作是开发者必须掌握的基础技能之一。gets()
函数作为标准库中的输入函数,曾被广泛用于从标准输入(通常是键盘)读取字符串。然而,由于其设计缺陷,该函数在现代编程实践中已被弃用,并被更安全的替代方案取代。尽管如此,学习 gets()
仍然具有重要意义:它帮助开发者理解缓冲区管理的核心概念,并为深入学习安全编码原则奠定基础。
本文将从函数的基本用法、潜在风险、替代方案等角度展开,结合代码示例和实际案例,帮助读者全面理解 gets()
的特性及其实用场景。对于编程初学者,这将是一次系统化学习输入函数的机会;对于中级开发者,则能通过对比分析提升代码安全性意识。
函数原型与基本用法:像水杯一样理解缓冲区
函数原型解析
gets()
的函数原型如下:
char *gets(char *str);
该函数接受一个字符指针作为参数,其功能是从标准输入设备(如键盘)读取一行字符,直到遇到换行符(\n
)或文件结束符(EOF)。读取完成后,换行符会被替换为字符串终止符 '\0'
,并将输入的字符串存储到 str
指向的内存地址中。
简单示例:基础用法
以下是一个典型的 gets()
使用场景:
#include <stdio.h>
int main() {
char user_input[50];
printf("请输入你的名字:");
gets(user_input);
printf("你好,%s!\n", user_input);
return 0;
}
当运行此程序时,用户输入的内容(如“张三”)会被存储到 user_input
数组中。但需要注意:gets()
不会检查输入长度,这正是其最大的安全隐患。
常见问题与风险:当水杯被灌满时会发生什么?
缓冲区溢出的比喻
想象一个容量为 500 毫升的水杯(对应 char user_input[50]
)。如果倒入 1000 毫升的水(用户输入超过 49 个字符),溢出的水会污染周围环境。在编程中,这表现为缓冲区溢出——当输入数据超过目标数组的容量时,多余的数据会覆盖相邻内存区域,导致程序崩溃或被恶意利用。
案例分析:缓冲区溢出攻击演示
以下代码展示了 gets()
的危险性:
#include <stdio.h>
void read_name() {
char name[10]; // 只能存储 9 个字符 + 终止符
printf("输入名字:");
gets(name); // 没有长度限制
}
int main() {
read_name();
printf("输入结束!\n"); // 可能永远不会执行
return 0;
}
若用户输入超过 9 个字符(例如“abcdefghijklmnop”),程序可能因覆盖栈中的返回地址而崩溃。在更严重的场景中,攻击者可能通过精心构造的输入控制程序执行流程,例如:
// 假设 name 数组位于内存地址 0x1000
// 攻击者输入:'A' × 14 + '\x00\x00\x10\x08'(伪造的返回地址)
// 这会导致程序跳转到攻击者指定的内存位置执行代码
为什么 gets() 被弃用?
根据 C11 标准,gets()
已被官方标记为“已弃用”,原因包括:
- 缺乏长度检查:无法限制输入长度,直接导致缓冲区溢出风险。
- 不可控的输入来源:无法指定输入源(如文件),仅支持标准输入。
- 安全隐患:历史上众多安全漏洞(如 Shellshock)与
gets()
直接相关。
安全替代方案:如何优雅地实现输入功能?
替代函数:fgets() 的正确使用
fgets()
是 gets()
的安全替代函数,其原型如下:
char *fgets(char *str, int size, FILE *stream);
参数说明:
str
:目标字符串的首地址。size
:最大读取字节数(包括终止符)。stream
:输入源(如stdin
)。
关键区别:
| 特性 | gets() | fgets() |
|---------------------|----------------|----------------|
| 输入长度限制 | 无 | 由 size
参数控制 |
| 输入源 | 仅标准输入 | 可指定任意流 |
| 安全性 | 高风险 | 安全可控 |
| 返回值 | 成功返回 str | 失败或 EOF 返回 NULL |
代码示例:用 fgets() 替代 gets()
#include <stdio.h>
int main() {
char user_input[50];
printf("请输入你的名字:");
fgets(user_input, sizeof(user_input), stdin); // sizeof 自动计算数组大小
// 处理可能存在的换行符
user_input[strcspn(user_input, "\n")] = '\0';
printf("你好,%s!\n", user_input);
return 0;
}
改进点说明:
- 输入长度控制:通过
sizeof(user_input)
确保最多读取 50 字节。 - 换行符处理:使用
strcspn()
移除输入中的换行符,避免后续处理问题。 - 输入源明确:显式指定
stdin
,代码意图更清晰。
进阶实践:如何防御缓冲区溢出?
策略一:始终使用安全函数
在 C 语言中,除了 fgets()
,还有以下安全函数可供选择:
snprintf()
:安全的字符串格式化输出。strncpy()
:安全的字符串复制(需手动添加终止符)。strncat()
:安全的字符串拼接。
策略二:静态代码分析工具
使用工具(如 Valgrind、Clang Static Analyzer)进行代码扫描,自动检测潜在的缓冲区溢出风险。
策略三:防御性编程习惯
- 固定缓冲区大小:避免使用动态内存分配时的不确定性。
- 输入验证:对用户输入进行格式和长度检查。
- 错误处理:检查函数返回值,及时响应异常情况。
结论:从 gets() 看安全编程的核心原则
通过学习 gets()
函数,我们深刻认识到:安全性与便利性往往不可兼得。虽然 gets()
的简洁语法曾让开发者青睐,但其设计缺陷使其成为安全漏洞的温床。现代编程中,开发者必须遵循以下原则:
- 预分配资源:明确输入/输出的最大容量。
- 边界检查:对所有外部输入进行长度验证。
- 使用安全函数:优先选择提供参数控制的库函数。
尽管 gets()
已被弃用,但它依然是一个极佳的教学案例——通过理解其缺陷与改进方案,开发者能更深入地掌握 C 语言的内存管理机制,并培养出严谨的安全编码习惯。在实际开发中,我们应始终以 fgets()
等安全函数为基础,构建可靠且健壮的程序。
扩展思考:为什么某些代码仍使用 gets()?
在特定场景下(如旧代码维护或受限环境),开发者可能仍需使用 gets()
。此时应采取以下措施:
- 严格控制输入环境:确保输入来源可信且可控。
- 手动添加长度检查:结合
strlen()
验证输入长度。 - 设置沙箱环境:通过隔离机制限制潜在危害。
但需强调:在任何新项目中,使用 gets()
都是不可接受的。安全编码不仅是技术要求,更是对用户和社会的责任。