C语言教程-13_4-函数指针

前置知识:

  1. 函数
  2. 指针

函数指针

C语言中,函数也是一个可寻址对象,我们也可以获取其地址——换句话说,函数代码被存储于内存中的某一个地方,并且可以根据其指针来访问.

调用该函数时,程序就会跳转到该函数的地址,运行此处的代码,也即调用了该函数.

显然,我们可以使用一种特殊的指针来存储一个函数的地址(指针),并且完全可以使用该指针来进行访问.

这就是函数指针,或者说"指向函数的指针".

声明一个函数指针

我们可以如此声明一个函数指针:

#include <stdio.h>
/* func函数用于返回int参数2倍 */
int func(int i){
    return i*2;
}
int main() {
    int (*p)(int) = func; // p是一个函数指针,初始化为指向func函数
    printf("p: 0x%p\n", p); // 输出p的值
    printf("&func: 0x%p\n", &func); // 输出func函数的地址
    printf("(*p)(3): %d\n",(*p)(3)); // 使用p指针调用func函数
    printf("func(4): %d\n",(*p)(4)); // 调用func函数
    return 0;
}

考虑声明 int (*p)(int):

首先从标识符p开始,有一对()约束*p,指出p是一个指针;

然后在(*p)外的int (int)代表一个函数类型,此函数接受一个int参数,并返回一个int值;

因此推导出,p是一个指针,可以指向int (int)类型的函数,亦即p是一个函数指针.

上面代码的运行结果如下:

image-20240123145057712

可以看出,指针p的值(p指向的内存)就是func的地址.

函数指针的类型

前一个例子中,函数指针p指向的函数的类型是int (int),它代表了"一类"函数,而不是具体的一个函数,考虑下面代码:

#include <stdio.h>
/* func1函数用于返回int参数2倍 */
int func1(int i) {
    return i * 2;
}
/* func函数用于返回int参数3倍 */
int func2(int i) {
    return i * 3;
}
int main() {
    int (*p)(int) = func1; // p是一个函数指针,初始化为NULL
    p = func1; // p指向func1函数
    printf("/*当前p指向func1函数*/\n");
    printf("p: %p\n", p);
    printf("&func1: %p\n", &func1);
    printf("(*p)(2): %d\n", (*p)(2));
    printf("func1(3): %d\n", func1(3));

    p=func2; // p指向func2函数
    printf("\n/*当前p指向func2函数*/\n");
    printf("p: %p\n", p);
    printf("&func2: %p\n", &func2);
    printf("(*p)(2): %d\n", (*p)(2));
    printf("func2(3): %d\n", func2(3));

    return 0;
}

运行结果如下:

image-20240123145635901

显然,func1func2的类型完全相同,均为int (int),但是他们却是完全不同的2个函数,p指针可以分别指向他们,并进行调用.


另一方面,复习指针的内容:指针变量只能被赋值(指向)为指针变量所能指向的类型的地址,或者是能够被隐式转换为这种类型的地址.

和其他所有指针一样,函数指针只能指向自身指向的类型(即特定的一种函数类型)的地址.

但是需要注意,不同类型的函数指针是不能够互相转换的,也就是说,int (*p)(int)只能指向int (int)类型的函数,而不能指向例如int (double)类型的函数:

#include <stdio.h>
/* 2个不同类型的函数 */
int func1(int i) {
    return i * 2;
}
int func2(double i) {
    return (int)i;
}
int main() {
    int (*p)(int) = func1;
    // 下面这行代码会报错: 
    // invalid conversion from 'int (*)(double)' to 'int (*)(int)'
    // p = func2;
    printf("p: %p\n", p);
    return 0;
}

C语言中,不仅仅是参数类型,包括返回值类型,参数个数不同,都意味着他们不是相同类型的函数!

注: C语言没有函数重载,而且即使是C++的函数重载也不允许有这种指针类型转换

函数名的本质

C语言在处理函数指针和函数调用这方面有一些很有意思的特性,先看下面的代码,有可能让你懵逼:

#include <stdio.h>
int func(int i){
    return i*2;
}
int main() {
    int (*p)(int) = func;
    printf("    p: %p\n", p);
    printf("   *p: %p\n", *p);
    printf(" func: %p\n", func);
    printf("*func: %p\n", *func);
    printf("&func: %p\n", &func);
    return 0;
}

令人迷惑的是,每一行的输出值都是一样的:

image-20240123153519714

我们可以将其反汇编查看汇编代码,结果发现每一步的参数都是完全一样的:

image-20240123154307027

也就是说,无论是对func做*运算,还是&运算,还是直接对函数名func求值,得到的结果甚至汇编代码都是完全一致的,函数指针p也是如此.

事实上,函数名被使用时总是被编译器转换为函数指针,因此,诸如&func这样手动加上&运算符只不过是显式地说明了编译器将隐式执行的任务.

因此,我们通常会这样简化对函数指针的赋值:

#include <stdio.h>

int func(int i) {
    return i * 2;
}

int main() {
    int (*p)(int) = func; // 直接把函数名赋值给指针,由编译器自动转换
    printf("p: %p\n", p);
    return 0;
}

而同时,使用函数指针进行函数调用的代码也可以如此简化:

// 简化使用指针进行函数调用
#include <stdio.h>

int func(int i) {
    return i * 2;
}

int main() {
    int (*p)(int) = func; // 直接把函数名赋值给指针,由编译器自动转换
    printf("p(2): %d\n", p(2)); // 不需要(*p)(2),因为*p是函数,编译器仍然会将其重新转换为函数指针
    // 下面的代码没有问题,但是一步解引用看起来多此一举
    printf("(*p)(2): %d\n", (*p)(2));
    return 0;
}

另外,尽管从上面的角度分析来看,这种编译器负责的"转换"是必然发生的,但是事实并非如此:

就拿上面这段代码("简化使用指针进行函数调用")来看,编译器甚至可能直接把这个指针变量p优化掉:

image-20240123155637813

指针变量p呢?我不知道……你知道吗(手动滑稽)

所以从实际优化的角度来看,简单的函数指针使用甚至会直接被优化掉,更别提什么"函数名的转换"了,统统直接用lea func指令获取地址就完事了……

总结:实际使用过程中一旦用一个函数指针p来指向某个函数f,那么干脆直接把p当成函数f的一个别名也无伤大雅.唯一的区别是,p是一个指针,可以改变它的指向.

函数指针数组

这个很简单,前面讲过指针数组,函数指针数组就是一种指针数组,只不过这个数组中的每个元素都是一个函数指针而已.

简单举个例子,注意声明的写法:

#include <stdio.h>

int add(int a, int b) {
    printf("add: %d + %d", a, b);
    return a + b;
}
int sub(int a, int b) {
    printf("sub: %d - %d", a, b);
    return a - b;
}
int mul(int a, int b) {
    printf("mul: %d * %d", a, b);
    return a * b;
}
int div(int a, int b) {
    printf("div: %d / %d", a, b);
    return a / b;
}

int main() {
    /* p是一个数组,每个元素是一个指针,指针指向的类型是int (int,int)函数 */
    int (*p[4])(int, int) = { add, sub, mul, div }; // 使用4个函数指针来初始化数组
    int x, y;
    scanf("%d %d", &x, &y);
    for (int i = 0; i < 4; i++) {
        printf(" = %d\n", p[i](x, y));
    }
    return 0;
}

8 2作为x,y的值运行,结果如下:

image-20240123160756038

函数指针数组可以存储一系列类似功能(接口)的函数的指针,适当地使用可以大大简化程序,例如实现一个功能菜单的选择.


——WAHAHA 2024.1.23



上一篇:C语言教程-13_3-初探指针和数组的关系

posted @ 2024-01-24 17:23  WA-HAHA  阅读(58)  评论(0)    收藏  举报