经典考题形式
这个考题通常以一个函数 getmemory 和一个主函数 main 的形式出现,让你找出其中的错误并修正。

(图片来源网络,侵删)
最经典的错误版本
这个版本旨在考察你是否理解 malloc 分配的内存生命周期。
#include <stdio.h>
#include <stdlib.h> // 包含 malloc 的头文件
// 错误的 getmemory 函数
void getmemory(char *p) {
p = (char *)malloc(100); // 在堆上分配100字节的内存
// 问题出在这里:p 是一个局部指针变量,它只是 main 函数中 str 的一个拷贝。
// 这行代码只是修改了 getmemory 函数内部的 p,让它指向了新分配的内存。
// main 函数中的 str 仍然是一个 NULL 指针。
}
int main() {
char *str = NULL;
getmemory(str);
// str 仍然是 NULL,所以下面这行代码会导致程序崩溃(段错误)
strcpy(str, "hello world"); // 尝向 NULL 指针写入数据
printf("str is: %s\n", str); // 尝从 NULL 指针读取数据
// 更严重的是,getmemory 中分配的内存块丢失了,无法被 free,造成了内存泄漏。
return 0;
}
问题分析:
- 值传递问题:在 C 语言中,函数参数传递是 “值传递”,当调用
getmemory(str)时,str的值(即NULL)被复制给了函数getmemory的形参p。p和str是两个完全不同的变量,它们只是恰好存储了相同的地址值。 - 指针作用域:
p是getmemory函数的 局部变量,它的生命周期仅限于getmemory函数的执行期间,当getmemory函数执行完毕返回时,p所占用的栈内存会被销毁。 - 内存泄漏:虽然
p指向了malloc分配的堆内存,但由于p本身即将被销毁,我们失去了指向这块堆内存的唯一指针,这块内存既无法被程序再次使用,也无法被free释放,成为了无法回收的“孤儿内存”,这就是 内存泄漏。 - 程序崩溃:由于
main函数中的str仍然是NULL,strcpy(str, "hello world")尝试向NULL地址写入数据,这会导致程序立即崩溃(段错误)。
常见错误修正方法
针对上述问题,有几种常见的修正方法,每一种都侧重于考察不同的知识点。
修正方法一:返回指针
这是最直接、最常见的一种修正方式。getmemory 函数返回新分配的内存地址。
#include <stdio.h>
#include <stdlib.h>
// 修正方法1:getmemory 返回分配的内存地址
char *getmemory() {
char *p = (char *)malloc(100);
return p; // 返回堆内存的地址
}
int main() {
char *str = NULL;
str = getmemory(); // 用返回值接收新分配的内存地址
if (str != NULL) { // 好的编程习惯:检查指针是否为空
strcpy(str, "hello world");
printf("str is: %s\n", str);
free(str); // 记得释放内存!
str = NULL; // 释放后将指针置为 NULL,防止成为悬垂指针
}
return 0;
}
优点:
- 逻辑清晰,易于理解。
- 避免了值传递带来的问题。
注意事项:
- 内存管理责任:调用者(
main函数)必须负责释放malloc分配的内存,这是 C 语言中最重要的规则之一。 - 检查返回值:
malloc可能会因内存不足而失败,返回NULL,在使用malloc返回的指针前,必须检查它是否为NULL。
修正方法二:使用二级指针(指向指针的指针)
这个方法旨在考察你是否理解指针的指针,以及如何通过它来修改外部指针的值。
#include <stdio.h>
#include <stdlib.h>
// 修正方法2:使用二级指针
void getmemory(char **p) { // 参数是指向 char* 指针的指针
*p = (char *)malloc(100); // 通过解引用 p,来修改 main 函数中 str 的值
}
int main() {
char *str = NULL;
getmemory(&str); // 传递 str 的地址,即 &str
if (str != NULL) {
strcpy(str, "hello world");
printf("str is: %s\n", str);
free(str);
str = NULL;
}
return 0;
}
原理分析:
main函数调用getmemory(&str),传递的是str这个指针变量的地址。getmemory函数的形参p接收这个地址,p指向了main函数中的str变量。- 在函数内部,
*p就是解引用操作,它直接访问并修改了main函数中str的内容,将其指向新分配的堆内存地址。 - 这样,当函数返回后,
main函数中的str就成功更新了。
优点:
- 是一种在 C 语言中修改函数外部变量的常用技术,尤其是在需要修改指针本身时。
缺点:
- 语法上比返回指针稍微复杂一些,对于初学者来说可能更难理解。
相关变体考题
为了更深入地考察,面试官可能会提出一些变体。
内存泄漏的极致考验
#include <stdio.h>
#include <stdlib.h>
void test1() {
char *p = (char *)malloc(100);
// ... do something ...
// 程序忘记 free(p) 就返回了,造成了 100 字节的内存泄漏
}
void test2() {
char *p = (char *)malloc(100);
if (p == NULL) {
return; // 如果分配失败,直接返回,没有泄漏
}
// ... do something ...
free(p); // 正确释放
}
int main() {
test1();
test2();
return 0;
}
考点:识别出 test1 函数中的内存泄漏,并理解 test2 的正确写法。
悬垂指针
#include <stdio.h>
#include <stdlib.h>
void getmemory(char **p) {
*p = (char *)malloc(100);
strcpy(*p, "hello");
}
int main() {
char *str = NULL;
getmemory(&str);
printf("str is: %s\n", str); // 输出: str is: hello
free(str); // 释放内存
// 错误:str 已经是一个悬垂指针
// 它指向的内存已经被释放,再次使用是未定义行为
printf("str is: %s\n", str); // 危险!可能导致程序崩溃或输出乱码
str = NULL; // 正确的做法:释放后立即置空
// printf("%s\n", str); // 如果这里置空,编译器可能会给出警告,但程序更安全
return 0;
}
考点:
- 悬垂指针:指针指向的内存已经被
free,但指针本身没有被置为NULL,这个指针就变成了“悬垂指针”。 - 安全编程:释放内存后,应立即将指针置为
NULL,这是一个非常好的编程习惯,可以有效防止误用悬垂指针。
总结与核心考点
| 考点 | 描述 | 关键代码/概念 |
|---|---|---|
| 值传递 | 函数参数是值的拷贝,无法直接修改外部变量。 | void getmemory(char *p) |
| 指针与内存管理 | malloc 在堆上分配内存,free 释放内存,必须成对出现。 |
p = malloc(100); free(p); |
| 内存泄漏 | 分配的内存没有 free,导致其永远无法被回收。 |
malloc 后丢失指针引用。 |
| 悬垂指针 | 指向的内存被 free 后,指针未置 NULL,继续使用是危险的。 |
free(p); 之后没有 p = NULL; |
| 返回指针 | 通过函数返回值来传递新分配的内存地址。 | char* getmemory() { ... return p; } |
| 二级指针 | 通过传递指针的地址,来修改外部指针的值。 | void getmemory(char **p) { *p = ...; } |
对于这个考题,一个优秀的回答应该不仅仅是写出正确的代码,还能清晰地解释:
- 原始代码为什么错? (值传递,内存泄漏)
- 你的修正方案为什么对? (解释返回指针或二级指针的原理)
- 还需要注意什么? (检查
malloc返回值,free内存,避免悬垂指针)
掌握这个考题,意味着你对 C 语言的内存管理和指针操作有了非常扎实的理解。
