【C语言-指针-返回指针的函数】5. 返回指针值的函数——函数不但能给你个结果,还能给你个“地址”

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 5
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

前面咱们把函数指针讲完了,那节课说的是“指针指向函数”。今天咱们反过来——函数返回一个指针。

这俩听着好像差不多,但其实完全是两码事。函数指针是“你手里拿个指针,指着某个函数”。返回指针的函数是“你调用一个函数,它甩给你一个地址”。方向正好反过来。

那问题来了:函数返回指针有啥用?啥时候需要这么干?

咱们慢慢说。

先回忆一下普通函数返回啥

你写过的函数,返回值大部分是整数、浮点数、字符。比如:

int add(int a, int b) {
    return a + b;
}

调用add(3,5),它返回8。这个8是个值,你拿个变量接着就行。

那如果我想让函数返回一个地址呢?比如我写个函数,从数组里找到最大值,然后把最大值所在的位置(地址)返回给你,而不是把最大值本身返回给你。

这时候就需要返回指针的函数了。

返回指针的函数长啥样?

格式特别简单,就是在函数名前面加个*:

返回类型 *函数名(参数列表) {
    // ...
    return 指针;
}

比如:

int* find_max(int arr[], int n) {
    int *maxPos = &arr[0];   // 假设第一个是最大的
    for (int i = 1; i < n; i++) {
        if (arr[i] > *maxPos) {
            maxPos = &arr[i];   // 更新最大值的位置
        }
    }
    return maxPos;   // 返回最大值的地址
}

这个函数的返回值类型是int*——一个指向int的指针。它不返回最大值本身,返回的是最大值在数组里的地址。

调用它的时候:

int a[5] = {3, 7, 1, 9, 4};
int *p = find_max(a, 5);
printf("最大值:%d\n", *p);   // 输出9
printf("位置:%d\n", p - a);  // 输出3(下标)

你看,find_max返回了一个地址,p接住了它。通过*p拿到最大值,通过p - a算出它在数组里的下标。

这就是返回指针的函数最典型的用法——返回一个位置,而不是一个值。

啥时候需要返回指针?

我总结了几种常见场景:

场景一:从数组里找东西,返回找到的位置

就像上面的find_max。你不仅想知道最大值是多少,还想知道它在哪儿。返回指针就能同时满足这两个需求——指针本身就包含了位置信息,解引用又能拿到值。

场景二:在字符串里找字符,返回找到的位置

C标准库里的strchr函数就是这么干的:

char *strchr(const char *str, int ch);

它在字符串str里找字符ch,找到了就返回指向那个字符的指针,找不到返回NULL。你拿到这个指针之后,不仅能知道字符在哪儿,还能从这个位置开始继续往后操作。

咱们自己写一个简化版:

#include <stdio.h>

char* my_strchr(char *str, char target) {
    while (*str != '\0') {
        if (*str == target) {
            return str;   // 找到了,返回当前位置
        }
        str++;
    }
    return NULL;   // 没找到
}

int main() {
    char s[] = "Hello World";
    char *p = my_strchr(s, 'W');
    
    if (p != NULL) {
        printf("找到了:%s\n", p);   // 输出 World
    } else {
        printf("没找到\n");
    }
    
    return 0;
}

注意这个printf("%s", p)——因为p指向了'W'的位置,%s会从这个位置开始一直打印到字符串末尾。这就是返回指针的好处——你拿到的不是一个孤立的字符,而是字符串中的一个位置,你可以从这个位置继续做很多事情。

场景三:动态内存分配,返回申请到的内存地址

这个咱们下节课会详细讲。简单说就是malloc函数——你让它给你分配一块内存,它返回这块内存的地址。这个地址就是个指针。

场景四:返回结构体指针

当你有一个结构体,想让函数返回这个结构体的地址时,也用到返回指针的函数。这个在链表里特别常见。

一个完整的例子:查找学生信息

咱们用一个学生信息的例子,把返回指针的函数彻底讲透。

#include <stdio.h>
#include <string.h>

typedef struct {
    int id;
    char name[20];
    float score;
} Student;

// 按学号查找学生,返回学生结构体的指针
Student* find_student(Student students[], int n, int target_id) {
    for (int i = 0; i < n; i++) {
        if (students[i].id == target_id) {
            return &students[i];   // 找到了,返回这个学生的地址
        }
    }
    return NULL;   // 没找到
}

int main() {
    Student list[3] = {
        {101, "张三", 85.5},
        {102, "李四", 92.0},
        {103, "王五", 78.5}
    };
    
    Student *p = find_student(list, 3, 102);
    
    if (p != NULL) {
        printf("学号:%d\n", p->id);
        printf("姓名:%s\n", p->name);
        printf("成绩:%.1f\n", p->score);
    } else {
        printf("没找到这个学生\n");
    }
    
    return 0;
}

这个例子比数组那个更实用。find_student返回了一个Student*,通过它你能访问学生的所有信息。而且因为返回的是指针,你甚至可以通过它修改原数组里的数据:

Student *p = find_student(list, 3, 102);
if (p != NULL) {
    p->score = 95.0;   // 直接修改原数组里李四的成绩
}

你看,返回指针的函数威力就在这里——你拿到的不是一份拷贝,而是原数据的地址。想读就读,想改就改。

最大的坑:千万别返回局部变量的地址

这是返回指针的函数里最危险、最常见、最要命的错误。你一不小心就会踩进去。

看这个例子:

int* func() {
    int a = 10;
    return &a;   // 危险!返回局部变量的地址
}

int main() {
    int *p = func();
    printf("%d\n", *p);   // 输出什么?不确定!
    return 0;
}

你可能会想:func返回了a的地址,p接住了它,那*p不就是10吗?

不是。 因为a是局部变量。函数func执行完的时候,a的内存就被释放了。你返回的是一个“已经不属于你的地址”。这块内存可能马上被别的函数用了,里面原来的10可能已经被覆盖成别的值了。

你再去访问*p,拿到的可能是10,可能是垃圾数据,也可能是程序直接崩溃。这叫“返回了悬空指针”——指向已释放内存的指针。

那怎么办?如果你真需要一个函数返回一个指针,而且这个指针指向的数据在函数结束后还要继续用,你有两种办法:

办法一:用static变量

int* func() {
    static int a = 10;   // static变量,函数结束也不释放
    return &a;
}

static变量生命周期是整个程序,函数结束它还在。所以返回它的地址是安全的。但这个方法有个问题——每次调用func返回的都是同一个地址,如果你调用两次,第二次会把第一次的结果覆盖掉。适合简单场景,复杂的就不行了。

办法二:动态分配内存

int* func() {
    int *p = (int*)malloc(sizeof(int));
    *p = 10;
    return p;   // 返回堆上的地址,安全
}

用malloc从堆上申请内存,这块内存在你手动free之前一直有效。所以你可以安全地返回它的地址。但记住:谁申请,谁释放,调用者用完得负责free。

办法三:返回传入的地址

如果函数接收了一个指针参数,你可以把那个指针返回回去,或者返回基于它计算出来的地址。这个地址本来就不属于函数内部,所以安全。

咱们前面的find_max和find_student就是这种——返回的是传入数组里某个元素的地址,不是局部变量的地址,所以没问题。

另一个容易犯的错:返回值类型忘了写*

int func() {     // 返回int,不是int*
    int a = 10;
    return a;    // 返回的是值,不是地址
}

int main() {
    int *p = func();   // 类型不匹配,编译器会报错
    return 0;
}

函数返回指针,返回值类型必须是指针类型。int func()返回的是int,int* func()返回的才是int*。少写一个*,意思完全变了。

返回指针的函数 vs 函数指针,别再搞混了

这俩名字长得像,我再说一遍区别:

返回指针的函数:int* func(int a);

  • 意思是:func是一个函数,它返回一个int*。

  • 重点在“函数”两个字上,它是个函数。

函数指针:int (*p)(int);

  • 意思是:p是一个指针,它指向一个返回int的函数。

  • 重点在“指针”两个字上,它是个指针。

怎么区分?看*跟谁挨着。int* func——*跟int挨着,说明返回类型是int*。int (*p)——*跟p挨着,说明p是指针。

总结一下

返回指针的函数,核心就几点:

格式:返回类型 *函数名(参数列表),比如int* find(int arr[], int n)。

作用:返回一个地址,而不是一个值。适合需要知道“在哪儿”的场景。

最典型的用法:从数组里找元素返回地址、从字符串里找字符返回地址、动态内存分配返回地址。

最大的坑:绝对不要返回局部变量的地址。局部变量函数结束就没了,你返回的地址指向一块已经不属于你的内存。用static变量、动态分配的内存、或者传入的地址,才是安全的。

回去自己动手写几个例子:

  • 写一个函数,从整型数组里找第一个负数,返回它的地址。

  • 写一个函数,从字符串里找第一个空格,返回它的地址。

  • 写一个函数,用static变量实现一个计数器,返回计数器的地址。

写完跑一跑,看看结果对不对。再故意写一个返回局部变量地址的函数,看看编译器给什么警告,运行结果是不是不可预测。亲手踩一次坑,比看十遍都管用。

下节课咱们讲动态内存分配,那是指针真正的用武之地。