在C++中,函数的返回类型可以是任意合法类型,指针也不例外。所谓返回指针,就是函数把一个内存地址交还给调用方,调用方再通过解引用操作访问该地址上的数据。指定方法本身并不难:在函数声明里把星号写进返回类型即可,例如int* createValue()。真正值得花心思的,是星号的书写位置、指针所指向对象的生命周期,以及返回函数指针这类复杂声明时的解析规则。下面按照由浅入深的顺序,把这些内容逐一讲清楚。

基本语法:星号的位置与声明解读
返回指针的函数声明,最直观的写法是让星号紧贴返回类型,比如int* findMax(int* arr, int size)。星号也可以贴着函数名写成int *findMax(...),甚至两侧都留空格,三种形式在编译器眼里完全等价。团队协作时建议统一风格,常见约定是让星号靠近类型,一眼就能看出返回的是指针类型。
int* findMax(int* arr, int size); // 风格一:星号贴近返回类型 int *findMax(int *arr, int size); // 风格二:星号贴近名字 int * findMax(int * arr, int size); // 风格三:两侧留空格,同样合法 int* p1, p2; // 注意:p1是指针,p2只是普通int
代码最后一行是个经典陷阱:一条语句声明多个变量时,星号只作用于紧跟它的那个名字,p1是指向int的指针,p2却是普通int。这也是许多编码规范要求一行只声明一个变量的原因。读复杂声明时有个实用技巧叫从右向左法:以int* func()为例,从函数名出发,先遇到括号说明func是函数,向左看到星号说明函数返回指针,再向左看到int,说明指针指向整数。层层剥开,再长的声明也能读懂。
还要区分两个容易混淆的形式:返回指针的函数与返回数组指针的函数。前者写作int* func();后者因为数组维度要参与声明,需要额外的括号改变结合顺序,写作int (*func())[5],含义是调用func后得到一个指向含5个int元素的数组的指针。括号一旦省略,语法含义完全改变,编译器会直接报错。
返回堆内存指针与悬空指针陷阱
函数返回指针最危险的用法,是返回局部变量的地址。局部变量存放在栈上,函数调用结束栈帧即被回收,返回的地址随即失效,调用方一旦解引用就是未定义行为,这类指针被称为悬空指针。麻烦在于它往往不会立刻崩溃,可能读到残留旧数据,也可能等别的代码改写了那块栈内存后才出错,定位起来非常费劲。
int* badExample() {
int local = 42;
return &local; // 危险:返回局部变量的地址
}
int* heapExample() {
int* data = new int(42);
return data; // 堆内存不随函数结束而销毁
}
int* staticExample() {
static int cached = 100;
return &cached; // 静态局部变量活到程序结束
}
第二种写法返回new分配的堆地址,函数结束后依然有效,但所有权问题随之而来:调用方必须记得delete,否则内存泄漏;两个调用方都以为该自己释放,又会重复释放。接口设计上有个约定,返回裸指针时应在注释中写明所有权是否转移。第三种写法借助静态局部变量,其生命周期从首次执行延伸到程序结束,返回地址是安全的,代价是所有调用共享同一份数据,多线程环境需要额外同步。
如果确实需要向调用方交付一块新分配的内存,更稳妥的思路是把分配与释放封装进类的构造和析构,靠RAII机制自动管理,而不是让裸指针在模块之间来回流转。这一点在后面的智能指针部分展开。
类成员函数返回指针的典型用法
类的成员函数同样可以返回指针,指向的对象可以是成员变量、堆上的新对象,或者this所代表的当前对象。返回this常见于链式调用风格,成员函数处理完后把this返回,调用方就能连续用点号串起多个操作。
class Buffer {
public:
Buffer(size_t size) : data_(new char[size]), size_(size) {}
~Buffer() { delete[] data_; }
// 只读访问,const保证外部无法通过返回值修改内部数据
const char* data() const { return data_; }
// 返回可写指针,允许调用方直接改动缓冲区内容
char* at(size_t pos) { return data_ + pos; }
private:
char* data_;
size_t size_;
};
这里存在设计层面的取舍:返回成员变量的指针等于把内部实现暴露给外部,调用方可以绕过类的接口直接改写对象状态,封装性被破坏。缓解办法是像data函数那样返回const指针,只允许读取。另一个要点是生命周期绑定:成员指针的有效期依附于对象本身,Buffer一旦析构,从它那里拿到的所有指针立即失效,这种悬空比局部变量更隐蔽,因为对象何时销毁通常由上层逻辑决定,调用方很难察觉。
成员函数返回堆对象指针时,同样要回答所有权归谁的问题。若返回的是类内部管理的资源指针,应明确标注仅供观察、不可释放;若希望调用方接管资源,则应返回智能指针而不是裸指针,避免靠口头约定带来的隐患。
返回函数指针:复杂声明的简化技巧
函数指针作为返回类型时,声明会变得相当拗口。规则是:把返回的函数指针类型整体包进括号,放在函数名的位置。假设要返回一个接收两个int、返回int的函数指针,而函数本身接收一个char参数,传统C风格写法是int (*getHandler(char op))(int, int)。按从内向外、从右向左的顺序拆解:getHandler是函数名,参数为char;内层括号外的星号说明返回值是指针;最外层的int和参数表说明该指针指向接收两个int并返回int的函数。
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
// 写法一:传统声明,可读性差
int (*getHandler(char op))(int, int) {
return op == '+' ? add : sub;
}
// 写法二:类型别名简化,推荐
using Handler = int(*)(int, int);
Handler getHandler2(char op) {
return op == '+' ? add : sub;
}
// 写法三:尾置返回类型,C++11起可用
auto getHandler3(char op) -> int(*)(int, int) {
return op == '+' ? add : sub;
}
后两种写法把返回类型从函数名前挪走,可读性明显改善。using别名给复杂类型起了个短名字,声明处一目了然;尾置返回类型用auto占位,把真正的返回类型放到参数列表之后,特别适合返回类型依赖参数的模板函数。实际编码中只要碰到返回函数指针的需求,基本都应条件反射地想到这两种简化手段。
现代C++实践:用智能指针表达所有权
裸指针不携带任何所有权信息,光看函数签名猜不出谁负责释放,这是大量内存泄漏和重复释放的根源。C++11引入智能指针后,业界共识是用unique_ptr表达独占所有权、shared_ptr表达共享所有权,让返回类型自己说明资源的归属。
#include <memory>
#include <string>
std::unique_ptr<int[]> createArray(int n) {
auto data = std::make_unique<int[]>(n);
for (int i = 0; i < n; ++i) {
data[i] = i * 2;
}
return data; // 所有权随返回值自动转移给调用方
}
class File {
public:
explicit File(const std::string& path) { /* 打开文件 */ }
};
std::shared_ptr<File> openConfig() {
return std::make_shared<File>("config.ini");
}
unique_ptr在返回时由编译器自动完成移动语义,调用方拿到对象后无需手动delete,超出作用域自动释放;shared_ptr借助引用计数允许多个持有者共存,最后一个持有者负责销毁。当函数只是提供访问而不转移所有权时,返回裸指针或引用仍然合理,但注释里要写明不拥有该资源。另外,表示空指针请统一使用nullptr,它具有明确的指针类型,不会像NULL那样在重载解析中产生歧义。
总结一下:指定返回指针类型在语法上只需把星号写进返回类型,真正的难点在于让指针指向的对象活得比函数调用更久,并让所有权边界清晰可读。能直接返回对象就返回对象,必须返回指针时优先智能指针,返回裸指针时务必确认生命周期与所有权约定,守住这三条原则,绝大多数相关缺陷都能避开。