NULL
NULL 是 C 语言中的一个宏(Macro),它的值通常是整数 0 或者 (void *)0。

核心概念
NULL 的核心作用是表示一个“空指针”或“无效指针”,它不是一个指向有效内存地址的指针,而是一个特殊的值,用来表示“这里没有东西”或“这个指针不指向任何地方”。
定义和使用
NULL 通常在标准头文件 <stdio.h> 或 <stddef.h> 中被定义,你可以像这样使用它:
#include <stdio.h>
int main() {
// 1. 指向整型的指针
int *ptr_to_int = NULL; // 声明一个整型指针,并初始化为NULL,表示它目前不指向任何整数
// 在使用指针前,必须检查它是否为NULL
if (ptr_to_int == NULL) {
printf("指针 ptr_to_int 是 NULL,不能解引用,\n");
// *ptr_to_int = 100; // 如果取消注释,会导致程序崩溃(段错误)
}
// 2. 指向字符的指针(常用于字符串)
char *name = NULL; // 声明一个字符指针,初始化为NULL
if (name == NULL) {
printf("指针 name 是 NULL,\n");
}
// 3. 函数返回值
// 很多函数在失败时会返回NULL,fopen()
FILE *file = fopen("non_existent_file.txt", "r");
if (file == NULL) {
printf("无法打开文件,可能文件不存在或没有权限,\n");
// perror("fopen"); // 会打印更详细的错误信息
return 1; // 返回非零表示错误
}
// ... 使用文件 ...
fclose(file);
return 0;
}
NULL
- 是什么:一个表示“空”或“无效”的宏,值为
0。 - 为什么用:
- 初始化指针:在声明指针时,将其初始化为
NULL是一个好习惯,可以避免使用未初始化的“野指针”。 - 错误检查:函数在无法返回有效指针时(如内存分配失败、文件打开失败),会返回
NULL,调用者必须检查返回值是否为NULL来判断操作是否成功。 - 终止条件:在链表等数据结构中,常用
NULL表示链表的末尾。
- 初始化指针:在声明指针时,将其初始化为
gets
gets 是一个标准 C 库函数,用于从标准输入(通常是键盘)读取一行字符串,直到遇到换行符或文件结束符(EOF),然后将该字符串(不包含换行符)存入指定的缓冲区。
函数原型
char *gets(char *str);
str: 一个指向字符数组的指针,也就是缓冲区的地址。- 返回值:如果成功读取,返回
str(指向缓冲区的指针);如果到达文件末尾或发生读取错误,返回NULL。
gets 的工作方式(极其危险)
gets 的致命缺陷在于:它不知道目标缓冲区有多大,它会盲目地读取输入,直到遇到换行符或 EOF,然后将所有读入的内容(包括可能超出缓冲区容量的部分)写入 str 指向的内存区域。

这会导致缓冲区溢出(Buffer Overflow)。
gets 的危险示例
#include <stdio.h>
int main() {
char name[10]; // 只能容纳 9 个字符 + 1 个字符串结束符 '\0'
printf("请输入你的名字(少于10个字符): ");
gets(name); // 危险!用户输入超过9个字符,就会溢出
printf("你好, %s\n", name);
return 0;
}
如果用户输入 Alexander (9个字符):
程序正常工作,name 数组被正确填充。
如果用户输入 Johann Sebastian Bach (超过9个字符):
gets 会把所有字符都写入 name 数组所在的内存空间,它会覆盖掉 name 数组后面的内存,这些内存可能存储着其他变量、函数返回地址等关键信息,这会导致:
- 程序崩溃(段错误)。
- 数据被破坏。
- 最严重的情况:攻击者可以精心构造一段输入,覆盖函数的返回地址,使其指向恶意代码,从而执行任意代码(这是许多安全漏洞的根源)。
为什么 gets 被废弃?
正是因为 gets 存在如此严重且难以避免的安全漏洞,C 标准委员会已经明确废弃了它。
- 在 C99 标准中,
gets被标记为“删除(removed)”。 - 在 C11 标准中,它被完全移除。
- 在现代的 C 编译器(如 GCC, Clang)中,如果你使用
gets,编译器会直接给出警告或错误。
NULL 和 gets 的关系
NULL 和 gets 本身没有直接的语法关系,但它们通过函数的返回值联系在一起。
gets 函数在两种情况下会返回 NULL:
- 读取到文件结束符:在 Linux/macOS 中按下
Ctrl+D,或者在 Windows 中按下Ctrl+Z再按回车。 - 发生读取错误:输入流出错。
一个包含 NULL 检查的 gets 示例(尽管 gets 仍然不安全)
#include <stdio.h>
int main() {
char buffer[100];
printf("请输入一些内容,然后按回车: ");
// 检查 gets 的返回值是否为 NULL
if (gets(buffer) == NULL) {
printf("读取输入失败或遇到文件结束,\n");
return 1;
}
printf("你输入的内容是: %s\n", buffer);
return 0;
}
这个例子虽然检查了 NULL,但仍然不安全! 因为 NULL 检查只能处理“没有输入”或“输入错误”的情况,完全无法防止缓冲区溢出,用户只要输入超过 99 个字符,程序依然会崩溃或被攻击。
安全的替代方案:fgets
既然 gets 不能用,我们应该用什么?答案是 fgets。
fgets 是一个更安全、更可控的函数。
函数原型
char *fgets(char *str, int n, FILE *stream);
str: 目标缓冲区指针。n: 要读取的最大字符数(包括字符串结束符\0),这是gets所没有的安全机制!stream: 输入流,通常是stdin(标准输入)。
fgets 的工作方式
fgets 会从 stream 中读取最多 n-1 个字符,或者直到遇到换行符或 EOF,它总是会在读取的字符串末尾添加一个 \0。
fgets 的优点
- 防止缓冲区溢出:通过指定
n,fgets保证不会写入超过n-1个字符,从根本上杜绝了缓冲区溢出的风险。 - 保留换行符:如果输入行不超过
n-1个字符,fgets会将换行符\n也读入缓冲区,这有时很有用,有时需要手动去掉。
使用 fgets 的安全示例
#include <stdio.h>
#include <string.h> // 用于 strlen
int main() {
char name[10]; // 缓冲区大小仍然是10
printf("请输入你的名字: ");
// 使用 fgets,并指定缓冲区大小 sizeof(name)
// sizeof(name) 的值是 10
if (fgets(name, sizeof(name), stdin) == NULL) {
printf("读取输入失败,\n");
return 1;
}
// fgets 可能会把换行符也读进来,我们需要去掉它
size_t len = strlen(name);
if (len > 0 && name[len - 1] == '\n') {
name[len - 1] = '\0'; // 将换行符替换为字符串结束符
}
printf("你好, %s\n", name);
return 0;
}
即使用户输入 Johann Sebastian Bach,fgets 也只会读取前 9 个字符(Johann S),然后自动在第 10 个位置放上 \0,程序绝对安全!
总结与对比
| 特性 | gets (已废弃) |
fgets (推荐) |
|---|---|---|
| 安全性 | 极不安全,会导致缓冲区溢出 | 安全,通过指定最大长度防止溢出 |
| 缓冲区大小 | 不关心,完全由用户输入决定 | 必须指定,由程序员控制 |
| 换行符处理 | 自动丢弃换行符 | 会保留换行符(如果缓冲区足够) |
| 返回值 | 成功返回 str,失败返回 NULL |
成功返回 str,失败或到达 EOF 返回 NULL |
| 标准状态 | C99 已删除,C11 已移除 | C 标准库的一部分,安全可靠 |
黄金法则:
在任何现代 C 语言编程中,永远不要使用 gets,当你需要从标准输入读取一行字符串时,总是使用 fgets,并记得传入缓冲区的大小 sizeof(buffer),养成检查函数返回值(包括 NULL)的好习惯。
