如何正确实现一个返回指针的函数?
StudyingFather · · 个人记录
有的时候,我们会遇到形如这样的函数 API(来源 WC 2022 T3):
const char *guess(int num_testcase, int remaining_guesses, char initial_letter, bool *gold, bool *silver);
这样的 API 的显著特点是,它的返回值是一个指针类型变量。实现一个返回指针的函数事实上暗藏着许多坑,缺乏相关编程经验的编程者上手时很容易遇到各种问题。
本文将给出此类函数的正确实现方法,也希望大家在阅读本文后,能够养成一种更现代,更健康的编程习惯。
错误的实现
如果你没有使用堆空间(不少算法竞赛选手可能缺乏相关知识的了解)的习惯,很容易会写出这样的代码:
const char *fun() {
char s[5];
// do something
return s;
}
看起来没问题啊?这个函数里声明了一个长度为 5 的字符数组,然后返回了这个数组的首地址。没错,语义上确实是没毛病的。
让我们来分析一下上面这个程序的运行过程:
- 调用者调用了
fun
函数。 fun
函数创建了局部变量,占用 5 字节,分配在 栈空间 上(简单来说,栈指针sp -= 5
,相应内存位置入栈,分配给该局部变量)。- 函数体执行完毕后,栈指针恢复到调用
fun
函数前的位置(栈指针sp += 5
,相应内存位置被弹出栈),程序继续执行,相应的内存空间已经不在栈中。
(更多的技术细节这里不再叙述,感兴趣的同学可以阅读 CSAPP 等系统方面的书籍。)
我们发现,被调用者拿到的返回指针,在函数返回后就无效了!这样的指针一般被称为 悬垂指针。由于指针对应的空间已经被弹出栈,在后续的出入栈操作下,悬垂指针指向的内存空间可能会被分配给其他函数 / 变量,对应空间的内容往往会被意外改变,被调用者再访问该指针指向的内存空间,可能会拿到错误的结果(甚至可能出现运行时错误)!
正确的实现
栈空间在函数调用返回时会失效,但 堆空间 不会。所以正确的方法是声明一个指针,将该指针指向堆空间。
const char *fun() {
char *s = new char[5];
// do something
return s;
}
C++ 中,new
和 delete
关键字用于管理堆上空间。这里在堆上分配了一个 5 字节的空间用于存放该数组,并返回该空间的首地址存放于指针变量 s
中。
接下来,你可以继续使用下标访问运算符读写这个空间中某个位置的变量,操作完毕后,放心将 s
返回即可。
当然,这里还需要注意一点:分配空间并不总是成功的,如果因为剩余内存不足等原因导致分配空间失败,new
操作将返回一个空指针 nullptr
。一个实现严谨的程序应该对这种情况作出处理,不过算法竞赛中不太可能会出现这种情况。
另外,C++ 也保留了 C 中的 malloc
和 free
函数用于管理堆空间。不过 new
和 delete
的语法相对来说更友好,一般更推荐使用后者。注意不要混合使用两种堆内存管理方法!例如将 new
申请的空间用 free
释放,后果不可预料!
总结
前面的内容已经足够让大家在算法竞赛中避坑了,如果再遇到像去年 WC 的 T3 这样实现一个返回指针的函数,相信大家已经知道了正确的做法。
虽然我们成功解决了避开了一个大坑,但目前的做法还存在其他方面的问题:
- C++ 中并没有垃圾回收等机制,我们在堆上分配的空间,如果不手动释放,在程序结束前都会被一直占用,如果我们意外丢失了指向堆空间上的指针,这部分空间就被浪费了,这一问题称为 内存泄漏。
char*
这样类型的变量并不好用,我们还需要自己处理很多细节问题(例如记得在字符串的末尾手动添加\0
结束符)。
事实上,C++ 标准库相比 C,提供了很多高层次的抽象,帮助我们有效解决了这些问题。包括 std::string
在内的类型相比原始数组,提供了更多有用的功能,安全性也更好,使用起来也更加方便。
因此,请尽量避免再写下面的函数 API 了:
int *fun1(int x);
char *fun2();
请替换为下面这些更 C++ 的 API:
vector<int> fun1(int x);
string fun2();