函数指针和指针函数

一、核心区别先明确

函数指针和指针函数是 C 语言中极易混淆的概念,核心差异在于本质属性

特性 指针函数(Function Returning Pointer) 函数指针(Pointer to Function)
本质 函数,返回值为指针类型 指针,指向函数的内存入口地址
语法核心 返回类型 *函数名(参数列表)(无外层括号) 返回类型 (*指针名)(参数列表)(必须有括号)
关键优先级 () 优先级 > *,因此先解析为函数 括号提升 * 优先级,先解析为指针
用途 函数返回地址(如字符串、数组元素、动态内存) 调用函数、回调函数、动态选择函数逻辑

二、指针函数(返回指针的函数)

1. 定义

指针函数是普通函数 ,唯一特殊点是:返回值不是基础类型(int/char),而是指针(内存地址)。语法格式:

c

运行

复制代码
// 基础格式
数据类型 *函数名(参数列表);

// 示例:返回int类型指针的函数
int *get_ptr(int a);
// 示例:返回char类型指针的函数(最常用)
char *get_str(int flag);

2. 核心示例(附详细注释)

示例 1:返回字符串指针(常用场景)

需求:根据传入的 flag,返回不同的常量字符串。

c

运行

复制代码
#include <stdio.h>

// 指针函数:返回char类型指针(字符串地址)
char *get_str(int flag) {
    // 常量字符串存储在只读数据区,生命周期全局,可安全返回
    if (flag == 1) {
        return "Success"; // 返回字符串首地址
    } else if (flag == 0) {
        return "Failed";
    } else {
        return "Unknown";
    }
}

int main() {
    // 调用指针函数,接收返回的字符串指针
    char *result1 = get_str(1);
    char *result2 = get_str(0);
    char *result3 = get_str(2);

    // 通过指针访问字符串
    printf("Result1: %s\n", result1); // 输出:Result1: Success
    printf("Result2: %s\n", result2); // 输出:Result2: Failed
    printf("Result3: %s\n", result3); // 输出:Result3: Unknown

    return 0;
}
示例 2:返回数组元素指针

需求:返回数组中指定下标的元素地址。

c

运行

复制代码
#include <stdio.h>

// 全局数组(生命周期全局)
int arr[5] = {10, 20, 30, 40, 50};

// 指针函数:返回int类型指针(数组元素地址)
int *get_arr_ptr(int index) {
    // 校验下标合法性
    if (index < 0 || index >= 5) {
        return NULL; // 非法下标返回空指针
    }
    // 返回数组第index个元素的地址(arr[index] 等价于 *(arr+index),arr+index是地址)
    return &arr[index]; 
}

int main() {
    // 调用指针函数,获取下标2的元素地址
    int *p = get_arr_ptr(2);
    if (p != NULL) {
        printf("arr[2] = %d\n", *p); // 输出:arr[2] = 30
    }

    // 尝试非法下标
    int *p_err = get_arr_ptr(10);
    if (p_err == NULL) {
        printf("Invalid index\n"); // 输出:Invalid index
    }

    return 0;
}

3. 指针函数的核心坑点(必看)

严禁返回局部变量的指针!局部变量存储在栈区,函数执行完毕后栈帧销毁,地址变为无效内存,访问会导致未定义行为。

❌ 错误示例(返回局部变量指针):

c

运行

复制代码
#include <stdio.h>

char *bad_func() {
    char str[10] = "Hello"; // 局部数组,栈上分配
    return str; // 错误:返回栈区地址,函数结束后str销毁
}

int main() {
    char *p = bad_func();
    printf("%s\n", p); // 未定义行为:可能输出乱码/程序崩溃
    return 0;
}

✅ 解决方法(3 种):

  1. 返回常量字符串(如示例 1,存放在只读数据区,生命周期全局);
  2. 使用static修饰局部变量(将变量移到静态数据区,生命周期全局);
  3. 动态分配内存(malloc/calloc,堆区内存,需手动free)。

示例(static 修饰局部变量):

c

运行

复制代码
#include <stdio.h>

char *good_func() {
    static char str[10] = "Hello"; // static修饰,存放在静态区
    return str; // 安全:静态区变量生命周期全局
}

int main() {
    char *p = good_func();
    printf("%s\n", p); // 输出:Hello
    return 0;
}

三、函数指针(指向函数的指针)

1. 定义

函数在内存中有唯一的入口地址,函数指针是存储这个地址的指针变量 ,本质是指针,而非函数。通过函数指针可以调用指向的函数,核心用途是回调函数、动态函数调用

语法格式

c

运行

复制代码
// 基础格式:括号必须加,否则变成指针函数
返回类型 (*函数指针名)(参数列表);

// 示例:指向"返回int、参数为两个int"的函数的指针
int (*fp)(int, int);
语法关键点

函数指针的类型必须与指向的函数完全匹配

  • 返回值类型一致;
  • 参数个数、类型、顺序一致;
  • 参数名可省略(如int (*fp)(int, int) 等价于 int (*fp)(int a, int b))。

2. 核心示例(附详细注释)

示例 1:基础使用(定义 + 赋值 + 调用)

需求:定义函数指针,指向加法 / 减法函数,通过指针调用函数。

c

运行

复制代码
#include <stdio.h>

// 定义两个普通函数(返回int,参数两个int)
int add(int a, int b) {
    return a + b;
}

int sub(int a, int b) {
    return a - b;
}

int main() {
    // 1. 定义函数指针(类型匹配add/sub)
    int (*fp)(int, int);

    // 2. 赋值:函数名就是函数的入口地址,无需&(加&也可以)
    fp = add; 
    // fp = &add; // 等价写法,效果相同

    // 3. 调用:两种方式(推荐直接fp(),更简洁)
    int res1 = fp(5, 3);        // 等价于 add(5,3)
    int res2 = (*fp)(10, 4);    // 传统写法,效果相同
    printf("add(5,3)=%d, add(10,4)=%d\n", res1, res2); // 输出:8,6

    // 4. 切换指向的函数
    fp = sub;
    int res3 = fp(8, 2);
    printf("sub(8,2)=%d\n", res3); // 输出:6

    return 0;
}
示例 2:函数指针作为参数(回调函数,核心用途)

回调函数是函数指针最常用的场景:将函数指针作为参数传入另一个函数,在被调用函数中执行回调逻辑,实现 "通用化逻辑 + 自定义行为"。

需求:实现一个通用的数组遍历函数,通过回调函数自定义对每个元素的处理(如打印、累加)。

c

运行

复制代码
#include <stdio.h>

// 定义回调函数类型:返回void,参数int(数组元素)
typedef void (*Callback)(int); // 用typedef简化函数指针语法,可选但推荐

// 通用数组遍历函数:接收数组、长度、回调函数
void traverse_arr(int arr[], int len, Callback cb) {
    for (int i = 0; i < len; i++) {
        cb(arr[i]); // 调用回调函数,处理每个元素
    }
}

// 回调函数1:打印数组元素
void print_elem(int elem) {
    printf("%d ", elem);
}

// 回调函数2:累加数组元素(用全局变量暂存结果,简化示例)
int sum = 0;
void sum_elem(int elem) {
    sum += elem;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int len = sizeof(arr) / sizeof(arr[0]);

    // 1. 遍历数组,打印每个元素(传入print_elem作为回调)
    printf("数组元素:");
    traverse_arr(arr, len, print_elem); // 输出:1 2 3 4 5
    printf("\n");

    // 2. 遍历数组,累加元素(传入sum_elem作为回调)
    traverse_arr(arr, len, sum_elem);
    printf("数组累加和:%d\n", sum); // 输出:15

    return 0;
}
示例 3:函数指针数组(批量管理函数,如菜单)

需求:实现控制台菜单,1 - 加法,2 - 减法,3 - 退出,用函数指针数组映射选项和函数,简化分支逻辑。

c

运行

复制代码
#include <stdio.h>

// 定义函数指针类型:返回int,参数两个int
typedef int (*CalcFunc)(int, int);

// 业务函数:加法、减法
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main() {
    // 函数指针数组:下标0/1对应加法/减法,与菜单选项对应
    CalcFunc func_arr[] = {add, sub};
    int choice, a, b, res;

    while (1) {
        printf("\n===== 菜单 =====\n");
        printf("1. 加法\n2. 减法\n3. 退出\n");
        printf("请选择:");
        scanf("%d", &choice);

        if (choice == 3) {
            printf("退出程序\n");
            break;
        } else if (choice < 1 || choice > 2) {
            printf("无效选项\n");
            continue;
        }

        printf("请输入两个数(空格分隔):");
        scanf("%d %d", &a, &b);

        // 通过函数指针数组调用对应函数(choice-1对应数组下标)
        res = func_arr[choice-1](a, b);
        printf("结果:%d\n", res);
    }

    return 0;
}

3. 函数指针的常见用途

  1. 回调函数:如 Linux 内核驱动、库函数(qsort 的比较函数)、异步逻辑处理;
  2. 动态函数调用:运行时根据条件选择不同函数(如示例 3 的菜单);
  3. 模拟多态:C 语言无类和虚函数,可通过函数指针实现类似 C++ 的多态行为;
  4. 函数注册:如框架中注册插件 / 驱动的回调接口(如 Linux 的中断处理函数)。

四、核心对比总结

维度 指针函数 函数指针
本质 函数(返回值是指针) 指针(指向函数的地址)
语法 int *func(int)(无外层括号) int (*fp)(int)(必须有括号)
赋值 / 调用 像普通函数一样调用,返回值是地址 赋值为函数名,通过fp()(*fp)()调用函数
核心用途 返回字符串、数组元素、动态内存等地址 回调函数、动态函数调用、框架设计
记忆技巧 "函数返回指针"→ 先想函数,再看返回值 "指针指向函数"→ 先想指针,再看指向的函数

五、拓展:typedef 简化函数指针

函数指针语法繁琐,可通过typedef定义别名,简化代码(如示例 2/3 中的typedef):

c

运行

复制代码
// 定义别名:CalcFunc 等价于 "返回int、参数两个int"的函数指针类型
typedef int (*CalcFunc)(int, int);

// 原写法:int (*fp)(int, int);
// 简化写法:
CalcFunc fp; // 等价于 int (*fp)(int, int);

// 函数指针数组原写法:int (*func_arr[])(int, int) = {add, sub};
// 简化写法:
CalcFunc func_arr[] = {add, sub};

通过typedef可大幅提升函数指针代码的可读性,尤其在复杂场景(如回调函数嵌套)中。

函数指针采用 (*fp) 的格式,核心是C 语言运算符优先级规则语义表达的准确性共同决定的,是 C 语言语法设计的必然结果,而非随意规定。下面从「优先级根源」「语法解析逻辑」「语义合理性」三个维度拆解这个格式的讲究:

一、核心根源:运算符优先级(最关键)

C 语言中运算符有明确的优先级排序,其中:()(函数调用)> *(解引用)> 普通变量 / 标识符

如果没有括号包裹 *fp,编译器会优先解析函数调用,而非指针,最终变成「指针函数」而非「函数指针」。

对比两个写法的解析过程:
写法 解析步骤(按优先级) 最终语义
int *fp(int); 1. 先解析 fp(int) → fp 是一个带 int 参数的函数;2. 再解析 int * → 该函数返回 int 类型指针 指针函数(函数返回指针)
int (*fp)(int); 1. 括号提升优先级,先解析 (*fp) → fp 是一个指针;2. 再解析后面的 (int) → 该指针指向「带 int 参数、返回 int 的函数」 函数指针(指针指向函数)

一句话总结 :括号的作用是「打断默认优先级」,强制编译器先把 fp 识别为「指针」,再关联函数的参数 / 返回值特征,避免被误解析为「返回指针的函数」。

二、语义层面:贴合指针的本质逻辑

函数指针的核心是「指针变量存储函数的入口地址」,(*fp) 的格式是对「解引用指针得到函数」这一语义的直观表达:

  1. 普通指针的逻辑:int a = 10; int *p = &a; *p → 解引用指针 p 得到变量 a
  2. 函数指针的逻辑:int add(int a, int b); int (*fp)(int,int) = &add; (*fp)(1,2) → 解引用指针 fp 得到函数 add,再通过 () 调用函数。
示例:语义对齐普通指针

c

运行

复制代码
#include <stdio.h>

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

int main() {
    // 普通指针:&取地址,*解引用
    int num = 10;
    int *p_num = &num;
    printf("普通指针解引用:%d\n", *p_num); // 输出10

    // 函数指针:&取函数地址(可省略),*解引用(可省略)
    int (*fp_add)(int, int) = &add; // &add等价于add
    printf("函数指针解引用调用:%d\n", (*fp_add)(2,3)); // 输出5(语义直观)
    printf("省略*调用:%d\n", fp_add(2,3)); // 输出5(C语言语法糖)

    return 0;
}

可以看到:(*fp_add) 完全贴合「解引用指针获取目标(函数)」的语义,而 fp_add() 是 C 语言为了简化书写提供的语法糖 (编译器会自动补全解引用),但原始的 (*fp) 格式才是对函数指针本质的准确表达。

三、历史设计:C 语言的语法一致性

C 语言由丹尼斯・里奇设计时,始终追求「语法和语义的一致性」:

  • 所有指针的核心操作都是「* 解引用获取目标」,函数指针也不例外;
  • 函数名本身是「函数入口地址的常量」(类似数组名是数组首地址的常量),因此 fp = add 等价于 fp = &add& 可省略),而 (*fp) 则是对这个地址的解引用,符合 C 语言指针的统一设计逻辑。

四、常见误区澄清

  1. 误区 1 :"必须写 (*fp) 才能调用函数"❌ 错误:C 语言允许省略 *,直接用 fp() 调用,因为编译器会自动将函数指针的调用解析为「解引用指针 + 调用函数」,但 (*fp) 是更贴合语义的原始写法。

  2. 误区 2:"括号只是为了区分指针函数,没有实际意义"❌ 错误:括号不仅是 "区分",更是「改变优先级」的核心手段 ------ 没有括号,编译器会完全误解语义(把指针解析成函数),而非单纯 "区分两个概念"。

相关推荐
dddaidai1239 小时前
深入JVM(四):垃圾收集器
java·开发语言·jvm
没有bug.的程序员12 小时前
微服务基础设施清单:必须、应该、可以、无需的四级分类指南
java·jvm·微服务·云原生·容器·架构
没有bug.的程序员13 小时前
微服务中的数据一致性困局
java·jvm·微服务·架构·wpf·电商
重生之我是Java开发战士14 小时前
【数据结构】Java对象的比较
java·jvm·数据结构
invicinble1 天前
jar包在执行的时候需要关注的细节(提供一个解构jvm问题的视角)
java·jvm·jar
Evan芙1 天前
JVM原理总结
jvm
fei_sun1 天前
【总结】【OS】成组链接法
jvm·数据结构
7ioik1 天前
JVM 核心参数调优清单
jvm
CodeAmaz2 天前
JVM一次完整GC流程详解
java·jvm·gc流程