🔷 第七章 数组、指针和引用
📄 一句话概括

函数的返回类型可以写成指针:int *func(...)——把"地址"当结果交回来。但有一条铁的纪律:绝不能返回局部变量的地址。因为局部变量在函数结束时就销毁了(6.7.1),交回来的地址指向一片已经不存在的地方,这叫"悬空指针",用它就是灾难。能安全返回的有三样:静态变量、全局变量、调用方传进来的指针。

📝 本节和前面几节的关系

7.6.1(地址进函数)、7.6.2(函数地址当数据)之后,第三件事是"地址从函数里出来"。它踩的雷全在前面讲过:6.7.1 的"auto 变量随函数生灭"是悬空指针的根源,6.7.2 的 static 正是解药之一。所以这一节也是第六章"变量存储类别"那一段的实用落地。

一、格式:把 * 写在函数名前

int *findMax(int a[], int n) {       // 返回类型是 int*(一个地址)
    int *p = &a[0];
    for (int i = 1; i < n; i++)
        if (a[i] > *p) p = &a[i];     // 记住"最大值的地址"
    return p;                         // 把地址交回去
}

和普通函数只差两处:返回类型多一个 *;return 语句交回的是地址而不是值。

读法:int *findMax(...) = "findMax 交回一个地址,那个地址上住着一个 int"。

注意区分两种 *(7.4.2 的三重身份又来了一次):

int  *fp(int);      // 这是"函数声明":fp 是函数,返回 int*
int (*fp)(int);     // 这是"函数指针":fp 是指针,指向函数(7.6.2)

看 * 和名字的先后关系——* 紧贴名字的是"返回指针的函数"。

二、铁的纪律:不能返回局部变量的地址

int *bad() {
    int local = 5;
    return &local;        // ❌ 灾难!
}

为什么是灾难?回忆 6.7.1:局部变量(auto 生存类别)住在栈上,函数一返回,它那块空间就被回收了。此时交回去的地址指向一片已经不属于你的内存——这就是"悬空指针"。

后果不可预测:可能读到"看起来还对"的旧值(最坑),可能读到垃圾,也可能被下一次函数调用覆盖。编译器通常只给警告(function returns address of local variable),不报错。

   函数运行中            函数返回后
  ┌──────────┐         ┌──────────┐
  │ local = 5 │   →     │  已回收   │  ← 地址还在,但"房子"没了
  └──────────┘         └──────────┘
      ↑ 返回这个地址没意义:主人已经搬走了

三、能安全返回的三样东西

能返回什么 为什么安全 说明
static 局部变量 生存期到程序结束(6.7.2) 不会被回收,但要小心"每次调用是同一块"
全局变量 全程存活 同上
调用方传进来的指针/数组 内存属于调用方,函数管不着它什么时候回收 最常用、最推荐
动态申请的内存 由程序员自己管生死 后续章节的主角

最推荐的写法是第三种:函数不自己造内存,只在"调用方给的那块内存里"挑一个地址返回。本节开头的 findMax(int a[], int n) 正是这个套路——它返回的是 a 数组内部的地址,数组是调用方的,函数只是指路。

四、实例13:指针做返回值

#include <iostream>
using namespace std;

// ① 安全:返回"调用方数组"里最大元素的地址
int *findMax(int a[], int n) {
    int *p = &a[0];
    for (int i = 1; i < n; i++)
        if (a[i] > *p) p = &a[i];
    return p;
}

// ② 危险:返回局部变量的地址(演示用,别真写)
int *badDemo() {
    int local = 999;
    return &local;              // 悬空指针
}

// ③ 安全但要注意:返回 static 变量的地址
int *counterPtr() {
    static int count = 0;       // 6.7.2:初始化只一次,活到程序结束
    count++;
    return &count;
}

int main() {
    int arr[6] = {12, 45, 7, 33, 88, 20};

    int *pmax = findMax(arr, 6);
    cout << "最大值 = " << *pmax << endl;                 // 88
    cout << "它的下标 = " << (pmax - arr) << endl;         // 4(指针相减,7.4.3)

    *pmax = 100;                                           // 能改!因为指向 arr 内部
    cout << "改后 arr[4] = " << arr[4] << endl;            // 100

    cout << "计数 = " << *counterPtr() << endl;            // 1
    cout << "计数 = " << *counterPtr() << endl;            // 2
    cout << "计数 = " << *counterPtr() << endl;            // 3
    return 0;
}

看到什么算成功:最大值 = 88、它的下标 = 4、改后 arr[4] = 100、计数 = 1、计数 = 2、计数 = 3。

三个收获:

输出 说明
最大值 = 88 返回的是地址,*pmax 才拿到值
它的下标 = 4 用"指针相减"就能算出下标(7.4.3)——地址含着位置信息
改后 arr[4] = 100 通过返回的指针能改调用方的数组——因为它指向的是 arr 内部
计数 = 1/2/3 static 变量活到程序结束,返回它的地址是安全的

注意 badDemo() 我故意没在 main 里用——它可以编译(多半只有警告),但结果不可靠。"能编译"不等于"能运行",这是本章反复出现的主旋律。

30 秒实验:加一句 cout << *badDemo() << endl; 调用那个危险函数,重跑几次——有时打印 999(假象),有时打印垃圾。这个"时对时错"正是悬空指针最难查的地方。看到"有时对",千万别以为它是对的。

五、常见误区

  1. "返回指针就是把值复制一份出来"——不是。交回的是地址;地址指向的内容是不是还有效,得靠你自己保证。
  2. "局部变量返回地址只是警告,应该没事"——有事,而且是最难查的那种(时对时错)。看到这个警告必须改。
  3. "返回 static 变量地址最省事,那就都用它"——省事但有陷阱:所有调用共用同一块内存(counterPtr() 的三次调用打印同一个地址)。如果调用方把两次返回的指针都留着,会互相覆盖。
  4. "int *f(int) 和 int (*f)(int) 一样"——完全不同。前者是"返回指针的函数"(本节);后者是"函数指针"(7.6.2)。
  5. "返回数组名就等于返回数组"——只是返回首地址,数组本体没被复制(数组从来不能"整体复制",7.1.3)。这也意味着:返回局部数组的地址和返回局部变量的地址一样危险。
自测三问(点开对答案) 1. 为什么不能返回局部变量的地址?——局部变量(`auto`)在函数返回时就被回收(6.7.1),交回的地址指向已失效的内存,成为**悬空指针**。 2. 哪三种地址可以安全返回?——`static` 变量地址、全局变量地址、**调用方传进来的指针/数组**(另外还有动态申请的内存)。 3. `int *f(int)` 和 `int (*f)(int)` 分别是什么?——前者是"返回 `int*` 的函数";后者是"指向函数的指针"。

相关笔记