8.5 指向函数的指针
8.5.1 什么是函数的指针
在程序中定义一个函数时,编译器会将该函数的源代码转换为可执行代码,并分配一段存储空间。这段内存空间有一个起始地址,也称为函数的入口地址。每次调用函数时,都从该地址入口开始执行这段函数代码。函数名代表函数的起始地址,调用函数时,从函数名得到函数的起始地址,并执行函数代码。因此,函数名就是函数的指针,它代表函数的起始地址。
我们可以定义一个指向函数的指针变量,用来存放某一函数的起始地址,这就意味着此指针变量指向该函数。例如:
int (*p)(int, int);
定义 p
是一个指向函数的指针变量,它可以指向函数类型为整型且有两个整型参数的函数。此时,指针变量 p
的类型用 int (*)(int, int)
表示。
8.5.2 用函数指针变量调用函数
如果想调用一个函数,除了可以通过函数名调用以外,还可以通过指向函数的指针变量来调用该函数。
先通过一个简单的例子来回顾一下函数的调用情况。
例 8.22 用函数求整数 a
和 b
中的大者。
解题思路:定义一个函数 max
,实现求两个整数中的大者。在主函数中调用 max
函数,除了可以通过函数名调用外,还可以通过指向函数的指针变量来实现。分别编程并作比较。
(1) 通过函数名调用函数
程序代码:
#include <stdio.h>
int max(int, int); // 函数声明
int main() {
int a, b, c;
printf("Please enter a and b: ");
scanf("%d %d", &a, &b);
c = max(a, b); // 通过函数名调用 max 函数
printf("a = %d\nb = %d\nmax = %d\n", a, b, c);
return 0;
}
int max(int x, int y) {
int z;
if (x > y) z = x;
else z = y;
return z;
}
运行结果:
Please enter a and b: 45 87
a = 45
b = 87
max = 87
这个程序是很容易理解的。
(2) 通过指针变量调用它所指向的函数
将程序改写为:
程序代码:
#include <stdio.h>
int max(int, int); // 函数声明
int main() {
int (*p)(int, int); // 定义指向函数的指针变量 p
int a, b, c;
p = max; // 使 p 指向 max 函数
printf("Please enter a and b: ");
scanf("%d %d", &a, &b);
c = (*p)(a, b); // 通过指针变量调用 max 函数
printf("a = %d\nb = %d\nmax = %d\n", a, b, c);
return 0;
}
int max(int x, int y) {
int z;
if (x > y) z = x;
else z = y;
return z;
}
运行结果与程序 (1) 相同。
程序分析: 可以看到,程序 (1) 和 (2) 的 max
函数是相同的。不同的只是在 main
函数中调用 max
函数的方法。
程序 (2) 的第 4 行 int (*p)(int, int);
用来定义 p
是一个指向函数的指针变量,最前面的 int
表示这个函数的返回值是整型的。最后面的括号中有两个 int
,表示这个函数有两个 int
型参数。注意 *p
两侧的括号不可省略,表示 p
先与 *
结合,是指针变量,然后再与后面的 ()
结合,()
表示是函数,即该指针变量不是指向一般的变量,而是指向函数。如果写成 int *p(int, int);
,由于 ()
优先级高于 *
,相当于 int *(p(int, int));
,就成了声明一个返回值为指向整型变量的指针的 p
函数。
赋值语句 p = max
的作用是将函数 max
的入口地址赋给指针变量 p
。和数组名代表数组首元素地址类似,函数名代表该函数的入口地址。这样,p
就是指向函数 max
的指针变量,此时 p
和 max
都指向函数的开头。调用 *p
就是调用 max
函数。请注意,p
是指向函数的指针变量,它只能指向函数的入口处而不可能指向函数中间的某一条指令处,因此不能用 *(p + 1)
来表示函数的下一条指令。
在 main
函数中有一个赋值语句:
c = (*p)(a, b);
它和
c = max(a, b);
等价。这就是用指针实现函数的调用。
以上用两种方法实现函数的调用,结果是一样的。
8.5.3 怎样定义和使用指向函数的指针变量
从例 8.22 中已经看到定义指向函数的指针变量的例子。定义指向函数的指针变量的一般形式为:
类型名 (*指针变量名)(函数参数表列)
判定指针变量是指向函数的指针变量
- 看变量名的前面是否有
*
号,如*p
。如果有,肯定是指针变量而不是普通变量。 - 看变量名的后面是否有圆括号,内有形参的类型。如果有,就是指向函数的指针变量,这对圆括号是函数的特征。由于优先级的关系,
*指针变量名
要用圆括号括起来。
说明:
- 定义指向函数的指针变量,并不意味着这个指针变量可以指向任何函数,它只能指向在定义时指定类型的函数。如
int (*p)(int, int);
表示指针变量p
可以指向返回值为整型且有两个整型参数的函数。在程序中可以将符合该类型的函数的地址赋给它,它就指向那个函数。在一个程序中,一个指针变量可以先后指向同类型的不同函数。 - 如果要用指针调用函数,必须先使指针变量指向该函数。如:
这将p = max;
max
函数的入口地址赋给指针变量p
。 - 在给函数指针变量赋值时,只需给出函数名而不必给出参数。例如:
因为是将函数入口地址赋给p = max(a, b);
p
,而不涉及实参与形参的结合问题。如果写成:
就错了。c = (*p)(a, b);
p = max(a, b)
的作用是将调用max
函数所得到的函数值赋给p
,而不是将函数入口地址赋给p
。 - 用函数指针变量调用函数时,只需将
(*p)
代替函数名即可。在(*p)
之后的括号中根据需要写上实参。例如:
表示“调用由c = (*p)(a, b);
p
指向的函数,实参为a
和b
。得到的函数值赋给c
”。 - 对指向函数的指针变量不能进行算术运算,如
p + n
、p++
、p--
等运算是无意义的。 - 用函数名调用函数,只能调用指定的一个函数,而通过指针变量调用函数比较灵活,可以根据不同情况先后调用不同的函数。见例 8.23。
例 8.23 输入两个整数,然后让用户选择 1 或 2。选 1 时调用 max
函数,输出二者中的大数;选 2 时调用 min
函数,输出二者中的小数。
解题思路:这是一个示意性的简单例子,说明怎样使用指向函数的指针变量。定义两个函数 max
和 min
,分别用来求大数和小数。在主函数中根据用户输入的数字是 1 或 2,使指针变量指向 max
函数或 min
函数。
程序代码:
#include <stdio.h>
int max(int, int); // 函数声明
int min(int, int); // 函数声明
int main() {
int (*p)(int, int); // 定义指向函数的指针变量
int a, b, c, n;
printf("Please enter a and b: ");
scanf("%d %d", &a, &b);
printf("Please choose 1 or 2: ");
scanf("%d", &n);
if (n == 1) p = max; // 如输入 1,使 p 指向 max 函数
else if (n == 2) p = min; // 如输入 2,使 p 指向 min 函数
c = (*p)(a, b); // 调用 p 指向的函数
printf("a = %d, b = %d\n", a, b);
if (n == 1) printf("max = %d\n", c);
else printf("min = %d\n", c);
return 0;
}
int max(int x, int y) {
int z;
if (x > y) z = x;
else z = y;
return z;
}
int min(int x, int y) {
int z;
if (x < y) z = x;
else z = y;
return z;
}
运行结果:
- 输入
a
、b
的值为 34 和 89,选择模式 1:Please enter a and b: 34 89 Please choose 1 or 2: 1 a = 34, b = 89 max = 89
- 输入
a
、b
的值为 34 和 89,选择模式 2:Please enter a and b: 34 89 Please choose 1 or 2: 2 a = 34, b = 89 min = 34
程序分析: 在程序中,调用函数的语句是 c = (*p)(a, b);
。从这个语句本身看不出是调用哪一个函数,在程序执行过程中由用户选择输入一个数字,程序根据输入的数字决定指针变量 p
指向哪一个函数,然后调用相应的函数。
这个例子虽然简单,但具有很大的实用价值。在许多应用程序中,常用菜单提示用户输入一个数字,然后根据输入的不同值调用不同的函数,实现不同的功能。通过使用指针变量,可以使程序更简洁和专业。
8.5.4 用指向函数的指针作函数参数
指向函数的指针变量的一个重要用途是把函数的入口地址作为参数传递到其他函数。指向函数的指针可以作为函数参数,把函数的入口地址传递给形参,这样就能够在被调用的函数中使用实参函数。其原理如下:
假设有一个函数 fun
,它有两个形参 x1
和 x2
,定义 x1
和 x2
为指向函数的指针变量。在调用函数 fun
时,实参为两个函数名 f1
和 f2
,给形参传递的是函数 f1
和 f2
的入口地址。这样在函数 fun
中就可以调用 f1
和 f2
函数了。例如:
void fun(int (*x1)(int), int (*x2)(int, int)) {
int a, b, i = 3, j = 5;
a = (*x1)(i); // 调用 f1 函数,i 是实参
b = (*x2)(i, j); // 调用 f2 函数,i 和 j 是实参
}
在 fun
函数中声明形参 x1
和 x2
为指向函数的指针变量,x1
指向的函数有一个整型形参,x2
指向的函数有两个整型形参。i
和 j
是调用 f1
和 f2
函数时所要求的实参。函数 fun
的形参 x1
和 x2
(指针变量)在函数 fun
未被调用时并不占内存单元,也不指向任何函数。在主函数调用 fun
函数时,把实参函数 f1
和 f2
的入口地址传给形参指针变量 x1
和 x2
,使 x1
和 x2
指向函数 f1
和 f2
。
为什么使用指向函数的指针变量
有人可能会问,既然在 fun
函数中要调用 f1
和 f2
函数,为什么不直接调用 f1
和 f2
而要用函数指针变量呢?的确,如果只是用到 f1
和 f2
函数,可以在 fun
函数中直接调用 f1
和 f2
。但是,如果在每次调用 fun
函数时,要调用的函数不是固定的,这次调用 f1
和 f2
,下次要调用 f3
和 f4
,再下一次要调用 f5
和 f6
,用指针变量就比较方便了。只要在每次调用 fun
函数时给出不同的函数名作为实参即可,而 fun
函数不必做任何修改。这种方法符合结构化程序设计方法的原则,是程序设计中常使用的。
下面通过一个简单的例子来说明这种方法的应用。
例 8.24 有两个整数 a
和 b
,由用户输入 1、2 或 3。如输入 1,程序就给出 a
和 b
中的大者;输入 2,就给出 a
和 b
中的小者;输入 3,则求 a
与 b
之和。
解题思路:与例 8.23 相似,但现在用一个函数 fun
来实现以上功能。
程序代码:
#include <stdio.h>
int fun(int x, int y, int (*p)(int, int)); // fun 函数声明
int max(int, int); // max 函数声明
int min(int, int); // min 函数声明
int add(int, int); // add 函数声明
int main() {
int a = 34, b = -21, n;
printf("Please choose 1, 2 or 3: ");
scanf("%d", &n); // 输入 1、2 或 3 之一
if (n == 1) fun(a, b, max); // 输入 1 时调用 max 函数
else if (n == 2) fun(a, b, min); // 输入 2 时调用 min 函数
else if (n == 3) fun(a, b, add); // 输入 3 时调用 add 函数
return 0;
}
int fun(int x, int y, int (*p)(int, int)) { // 定义 fun 函数
int result;
result = (*p)(x, y); // 调用 p 指向的函数
printf("%d\n", result); // 输出结果
return result;
}
int max(int x, int y) { // 定义 max 函数
int z;
if (x > y) z = x;
else z = y;
printf("max = ");
return z; // 返回值是两数中的大者
}
int min(int x, int y) { // 定义 min 函数
int z;
if (x < y) z = x;
else z = y;
printf("min = ");
return z; // 返回值是两数中的小者
}
int add(int x, int y) { // 定义 add 函数
printf("sum = ");
return x + y; // 返回值是两数之和
}
运行结果:
- 选择 1,调用 max 函数:
Please choose 1, 2 or 3: 1 max = 34
- 选择 2,调用 min 函数:
Please choose 1, 2 or 3: 2 min = -21
- 选择 3,调用 add 函数:
Please choose 1, 2 or 3: 3 sum = 13
程序分析: 在定义 fun
函数时,在函数首部用 int (*p)(int, int)
声明形参 p
是指向函数的指针,该函数是整型函数,有两个整型形参。max
、min
和 add
是已定义的 3 个函数,分别用来实现求大数、求小数和求和的功能。
当输入 1 时 (n = 1
),调用 fun
函数,除了将 a
和 b
作为实参,将两个整数传给 fun
函数的形参 x
和 y
外,还将函数名 max
作为实参,将其入口地址传送给 fun
函数中的形参 p
(p
是指向函数的指针变量)。这时,fun
函数中的 (*p)(x, y)
相当于 max(x, y)
,调用 max(x, y)
就输出 a
和 b
中的大者。
同理,当输入 2 时 (n = 2
),调用 fun
函数时,以函数名 min
作实参,此时 fun
函数的形参 p
指向函数 min
。调用 min(x, y)
就输出 a
和 b
中的小者。
当输入 3 时 (n = 3
),调用 fun
函数时,以函数名 add
作实参,fun
函数中的 (*p)(x, y)
相当于 add(x, y)
,调用 add(x, y)
就输出 a
和 b
之和。
这个例子的思路与例 8.23 相似,但具体做法不同。在例 8.23 中定义了一个指向函数的指针变量 p
,根据不同情况,使 p
指向不同的函数,然后通过该指针变量调用不同的函数。本例程序没有定义指针变量,而是根据不同情况,将不同的函数名作为调用 fun
函数的实参,把函数入口地址传送给 fun
函数中的形参(该形参是指向函数的指针变量),调用 fun
函数就分别执行不同的函数。
通过本例可以清楚地看到,不论调用 max
、min
或 add
,fun
函数都没有改变,只是改变了实参函数名而已。在 fun
函数中输出 result
,由于在不同情况下调用了不同的函数,因此 result
的值是不同的。这就增加了函数使用的灵活性。
可以编写一个通用的函数来实现各种专用的功能。需要注意的是,对作为实参的函数(如 max
、min
、add
),应在主调函数中用函数原型作函数声明。例如
在 main
函数中的第 3 行到第 6 行的函数声明是不可少的。这样可以确保编译器知道这些函数的存在及其参数和返回值类型。
通过指向函数的指针变量,可以编写出更为复杂和通用的程序。例如,编写一个求定积分的通用函数,用它分别求以下 5 个函数的定积分:
- ∫(1+𝑥) 𝑑𝑥∫(1+x)dx
- ∫(2𝑥+3) 𝑑𝑥∫(2x+3)dx
- ∫(𝑒𝑥+1) 𝑑𝑥∫(ex+1)dx
- ∫(1+𝑥2) 𝑑𝑥∫(1+x2)dx
- ∫(𝑥3) 𝑑𝑥∫(x3)dx
可以看出,每次需要求定积分的函数是不一样的。可以编写一个求定积分的通用函数 integral
,它有 3 个形参:下限 a
、上限 b
以及指向函数的指针变量 fun
。integral
函数的原型可写为:
float integral(float a, float b, float (*fun)(float));
然后分别定义 5 个函数 f1
、f2
、f3
、f4
、f5
,代表上面 5 个函数(1 + x, 2x + 3, e^x + 1, 1 + x^2, x^3)。然后先后调用 integral
函数 5 次,每次调用时把 a
、b
以及一个函数名(f1
、f2
、f3
、f4
、f5
之一)作为实参,即把上限、下限以及有关函数的入口地址传送给形参 fun
。分别执行 integral
函数,可以求出不同函数的定积分。
请读者根据以上思路,编写出完整的程序。这里给出一个简要的示例:
程序代码:
#include <stdio.h>
#include <math.h>
float integral(float a, float b, float (*fun)(float));
float f1(float x) { return 1 + x; }
float f2(float x) { return 2 * x + 3; }
float f3(float x) { return exp(x) + 1; }
float f4(float x) { return 1 + x * x; }
float f5(float x) { return x * x * x; }
int main() {
float a = 0, b = 1;
printf("Integral of f1 from %.2f to %.2f: %.4f\n", a, b, integral(a, b, f1));
printf("Integral of f2 from %.2f to %.2f: %.4f\n", a, b, integral(a, b, f2));
printf("Integral of f3 from %.2f to %.2f: %.4f\n", a, b, integral(a, b, f3));
printf("Integral of f4 from %.2f to %.2f: %.4f\n", a, b, integral(a, b, f4));
printf("Integral of f5 from %.2f to %.2f: %.4f\n", a, b, integral(a, b, f5));
return 0;
}
float integral(float a, float b, float (*fun)(float)) {
int n = 1000; // 分割成 1000 个小区间
float h = (b - a) / n;
float sum = 0.5 * (fun(a) + fun(b));
for (int i = 1; i < n; i++) {
sum += fun(a + i * h);
}
return sum * h;
}
程序说明:
- 定义了
integral
函数,用于求定积分。 - 定义了 5 个函数
f1
、f2
、f3
、f4
和f5
,分别代表不同的函数。 - 在
main
函数中,分别调用integral
函数,求出不同函数的定积分,并打印结果。
运行结果:
Integral of f1 from 0.00 to 1.00: 1.5000
Integral of f2 from 0.00 to 1.00: 4.0000
Integral of f3 from 0.00 to 1.00: 2.7183
Integral of f4 from 0.00 to 1.00: 1.3333
Integral of f5 from 0.00 to 1.00: 0.2500
通过这个示例,可以看出指向函数的指针作为函数参数的灵活性和实用性。它使得程序可以处理不同的函数,而不需要修改 integral
函数的实现。
总结
指向函数的指针作为函数参数,是 C 语言实际应用中的一个比较深入的部分。本节只作初步介绍,使读者对此有一定的了解,为以后进一步学习和应用打下基础。通过灵活使用指向函数的指针变量,可以编写出更加通用和简洁的程序,提高程序的复用性和可维护性。