本文是对之前文章的一个补充。
在之前的文章中,我们主要说明了函数的调用和传参情况,这次主要是说明函数和指针的关系。
函数返回值
我们都知道函数只存在返回值,但是有时我们需要两个返回值,比如在 MATLAB 可以同时获得多个返回值,而在 C 语言中则可以借用指针来返回多个参数。比如返回堆上的连续空间,或者函数的实参本身就是指针等。
函数指针
函数本质
我们都知道程序或者代码都需要加载到内存上,而内存都是有对应地址的,那么函数是否也存在对应的地址呢?
#include <stdio.h>
int func(int x)
{
return x;
}
int main(void)
{
int x = 100;
printf("func(x) = %d\n",func(x));
printf("func = %p\n",func);
printf("&func = %p\n",&func);
printf("func+1 = %p\n",func+1);
printf("&func+1 = %p\n",&func+1);
void *p = func;
printf("p = %p\n",p);
printf("p+1 = %p\n",p+1);
p = &func;
printf("p = %p\n",p);
printf("p+1 = %p\n",p+1);
return 0;
}
结果为:
func(x) = 100
func = 00401630
&func = 00401630
func+1 = 00401631
&func+1 = 00401631
p = 00401630
p+1 = 00401631
p = 00401630
p+1 = 00401631
从上边的结果可以看出:
- 函数名与数组名类似,直接就表示一个地址
- 该地址的类型为 void *,是个纯粹的地址
- func 与 &func 含义相同
- func + 1 与 &func + 1 含义也相同
也就是说,函数本质是一段可执行的代码段,而函数名就指向该代码段的首地址。
函数指针变量定义
之前我们利用数组,得到了数组指针的形式。而函数名与数组名类似,会不会也能够得到函数指针呢?
#include <stdio.h>
int func(int x)
{
return x;
}
void foo(int x, int y)
{
printf("x + y = %d\n",x+y);
}
int main(void)
{
int x = 100, y=200;
int (*p)(int) = func;
printf("func(x) = %d\n",func(x));
printf("p(x) = %d\n",p(x));
void (*pp)(int,int) = foo;
foo(x,y);
pp(x,y);
return 0;
}
结果为:
func(x) = 100
p(x) = 100
x + y = 300
x + y = 300
上边的程序中,我们类比数组名定义了函数指针,从而得到了函数调用的另一个入口。
函数调用如果使用不到返回值的话,其实函数指针直接写成 void (*p)() 也是可以的,因为在实际编译的时候,都是通过的,只是在运行的时候,会出现 "不兼容" 的提示,而运行结果都是正确的。
函数别名
前面我们知道了函数指针,虽然也是指针,但是类型却是区别于我们之前见过的其它类型的。因此我们可以知道函数指针的别名:
datatype (*TYPE)(datatype argument list)
- datatype 表示函数返回值的类型
- type 表示函数指针的别名
- datatype argument list 表示函数形参类型列表,如 int,int,int
上边的 datatype argument list 如果不存在,结果仍然正确,但是为了提示参数类型,还是确保其存在。但是如果要使用该别名指向不同参数类型的函数,则建议去掉。
实际上,函数调用如果使用不到返回值的话,datatype 直接写成 void 也是可以的,但是编译会出现 “不兼容” 的提示,因此还是保证其一致。
#include <stdio.h>
void foo(int x, int y)
{
printf("x + y = %d\n",x+y);
}
typedef void (*FUNCP1)();
typedef void (*FUNCP2)(int,int);
int main(void)
{
int x = 100, y=200;
void (*p1)() = foo;
void (*p2)(int,int) = foo;
foo(x,y);
p1(x,y);
p2(x,y);
FUNCP1 pp1 = foo;
FUNCP2 pp2 = foo;
pp1(x,y);
pp2(x,y);
return 0;
}
结果为:
x + y = 300
x + y = 300
x + y = 300
x + y = 300
x + y = 300
上边程序中的 p 和 pp 得到的结果是相同的,也证明了函数指针别名的正确性。
函数指针调用
既然存在函数指针这一类型,那么也就能够作为参数进行传递:
#include <stdio.h>
int foo(int x, int y)
{
return x+y;
}
int cal(int x,int y,int (*p)())
{
return p(x,y);
}
int main(void)
{
int x = 100, y=200;
printf("x + y = %d",cal(x,y,foo));
return 0;
}
结果为:
x + y = 300
此时用到了函数的返回值,因此在函数形参中,函数指针就要写成 int (*p)() 的形式。
函数指针数组
同样如果存在函数指针这一数据类型,就会存在函数指针数组:
datatype (*p[])()
其实上式应该看做:
datatype () *p[]
刚好是函数指针数组的形式,同样如果不需要函数的返回值,可以将 datatype 改为 void。
#include <stdio.h>
double func1(double x,double y)
{
return x + y;
}
double func2(double x,double y)
{
return x - y;
}
double func3(double x,double y)
{
return x * y;
}
double func4(double x,double y)
{
return x / y;
}
int main(void)
{
double x = 100, y=200;
double (*p[4])() = {func1,func2,func3,func4};
for(int i=0;i<4;i++)
printf("p[%d](%.2f,%.2f) = %.2f\n",i,x,y,p[i](x,y));
return 0;
}
结果为:
p[0](100.00,200.00) = 300.00
p[1](100.00,200.00) = -100.00
p[2](100.00,200.00) = 20000.00
p[3](100.00,200.00) = 0.50
回调函数
我们之前提到过函数指针作实参进行参数传递。而回调函数也是将函数指针作参数进行传递,不过这里将这种用法叫做回调。
这种调用函数的好处是用户只需要根据函数的函数类型和函数功能提供对应的函数就可以了。
qsort
void qsort(void *_Base,size_t _NumOfElements,size_t _SizeOfElements,int (*_PtFuncCompare)(const void *,const void *));
该函数是由标准库封装的基于数组快速排序的接口,其中的参数为:
_Base | 指向数组的指针 |
_NumOfElements | 数组成员个数 |
_SizeOfElements | 成员大小 |
_PtFuncCompare | 回调函数 |
#include <stdio.h>
#include <stdlib.h>
int comp(const void *p, const void *q)
{
return *(int *)p>*(int *)q?1:1;
}
int main(void)
{
int arr[10] = {1,5,9,6,4,3,2,7,8,0};
qsort(arr,10,4,comp);
for(int i=0;i<10;i++)
printf("%d\n",arr[i]);
return 0;
}
结果为:
0
3
9
6
4
5
2
7
8
1
其实利用 qsort 还能对字符串、结构体进行排序。