前言:C语言最难的地方莫过于各种类型的指针,光听名字就把人给绕晕了,本文是在一些重要的基础概念之上进行说明的,需要一些知识储备,即:什么是数组指针、指针数组、函数指针、指针函数等,然后才能看得懂本文。
一、简单概述
1.1 函数指针
所谓函数指针即定义一个指向函数的指针变量,定义的格式如下:
int (*p)(int x, int y); //注意:这里的括号不能掉,因为括号()的运算优先级比解引用运算符*高
这个函数的类型是有两个整型参数,返回值是个整型。对应的函数指针类型:
int (*) (int a, int b);
对应的函数指针定义:
int (*p)(int x, int y); //参数名可以去掉,并且通常都是去掉的。这样指针p就可以保存函数类型为两个整型参数,返回值是整型的函数地址了。
int (*p)(int, int);
我们一般可以这么使用,通过函数指针调用函数:
int maxValue (int a, int b) {
return a > b ? a : b;
}
int (*p)(int, int) = NULL; //定义一个与maxValue兼容的指针
p = maxValue;
p(20, 45); //通过指针调用
1.2 指针函数
指针函数:指的是函数的返回值是一个指针,比如我的函数返回的是一个指向整数int的指针,定义格式如下:
int *p(int a,int b); //注意这里的*与P之间是没有括号的,所以含义是函数p(int,int)会返回一个(int *)指针
当然,函数本身也可能返回一个函数指针,后面会说到。
最重要的点:如何确定指针变量的类别是非常重要的,我们可以通过c++的
typeid(variable).name(); //查看变量的类别
总结如下:
名称(xx指针) | 含义(指向...的指针) | 定义形式 | 指针p的类型typeid(p).name |
变量指针 | 指向变量的指针 | int *p=&a int (*p)=&a | int * |
常量指针 | 指向常量的指针 | int const *p=&a(括号省略了) const int *p=&a(括号省略了) | int const * |
一维数组指针 | 指向一维数组(首元素)的指针 | int *p=a(括号省略了) | int * |
二维数组指针 | 指向二维数组(第一行整体是首元素的指针) | int (*p)[4]=a | int (*)[4] |
函数指针 | 指向函数的指针 | int (*p)(int a,int b)=add | int (*)(int,int) |
二、函数也可以作为参数
2.1 回调函数
上述内容是函数指针的基础用法,很多语言都支持函数作为参数和返回值,典型的像python动态语言,C语言当然也可以了,没错,其实函数指针更重要的意义在于函数回调。
举个例子:
现在我们有这样一个需求:实现一个函数,将一个整形数组中比50大的打印在控制台,我们可能这样实现:
void compareNumberFunction(int *numberArray, int count, int compareNumber)
{
for (int i = 0; i < count; i++)
{
if (*(numberArray + i) > compareNumber)
{
printf("%d\n", *(numberArray + i));
}
}
}
int main()
{
int numberArray[5] = {15, 34, 44, 56, 64};
int compareNumber = 50;
compareNumberFunction(numberArray, 5, compareNumber);
return 0;
}
这样实现是没有问题的,然而现在我们又有这样一个需求:实现一个函数,将一个整形数组中比50小的打印在控制台。当然我么可以完全又把上面的代码copy一遍,将大于改写成小于。这样做当然可以,然而作为开发者,我们要未雨绸缪,要考虑到将来可能添加更多类似的需求,那么你将会有大量的重复代码,使你的项目变得臃肿,所以这个时候我们需要冷静下来思考,其实这两个需求很多代码都是相同的,只要更改一下判断条件即可,而判断条件我们如何变得更加灵活呢?
这时候我们就用到回调函数的知识了,我们可以定义一个函数,这个函数需要两个int型参数,函数内部实现代码是将两个整形数字做比较,将比较结果的bool值作为函数的返回值返回出来,以大于被比较数字的情况为例:
BOOL compareGreater(int number, int compareNumber) {
return number > compareNumber;
}
同理,小于被比较的数字函数定义如下:
BOOL compareLess(int number, int compareNumber) {
return number < compareNumber;
}
接下来,我们可以将这个函数作为compareNumberFunction的一个参数进行传递(没错,函数可以作为参数),那么我们就需要一个函数指针获取函数的地址,从而在compareNumberFunction内部进行对函数的调用,于是,compareNumberFunction函数的定义变成了这样:
void compareNumberFunction(int *numberArray, int count, int compareNumber, BOOL (*p)(int, int))
{
for (int i = 0; i < count; i++)
{
if (p(*(numberArray + i), compareNumber)) //通过函数指针调用比较函数
{
printf("%d\n", *(numberArray + i));
}
}
}
具体使用时代吗如下:
int main() {
int numberArray[5] = {15, 34, 44, 56, 64};
int compareNumber = 50;
// 大于被比较数字情况:
compareNumberFunction(numberArray, 5, compareNumber, compareGreater);
// 小于被比较数字情况:
compareNumberFunction(numberArray, 5, compareNumber, compareLess);
return 0;
}
所以,函数回调本质为函数指针作为函数参数,函数调用时传入函数地址,这使我们的代码变得更加灵活,可复用性更强。
说了这么多,其实函数指针作为函数参数很简单,我们只要能知道函数指针的类型即可,一般格式如下:
void MyFunction(..., int (*p)(int,int),....)
下面是一些常见的函数指针,注意函数的返回值和参数类型要匹配哦!!!另外注意解引用运算符*上面的括号不能掉啊!!!
int (*p)(int,int) //有参数,有返回值的函数
void (*p)(int,int) //有参数,无返回值的函数
void (*p)() //无参数,无返回值的函数
void (*p)(void)
2.2 借助于函数指针作为参数实现“动态排序”
首先我们应该理解动态这个词,我的理解就是不同时刻,不同场景,发生不同的事,这就是动态。动态排序就是根据不同的排序指标进行排序,不用书写很多重复性的代码,话不多说,直接上案例。
需求: 有30个学生需要排序
按成绩排
按年龄排
…
这种无法预测的需求变更,就是我们上文说的动态场景,那么解决方案就是函数回调:
//定义一个结构体
typedef struct student
{
char name[20];
int age;
float score;
}Student;
//比较两个学生的年龄
BOOL compareByAge(Student stu1, Student stu2)
{
return stu1.age > stu2.age ? YES : NO;
}
//比较两个学生的成绩
BOOL compareByScore(Student stu1, Student stu2)
{
return stu1.score > stu2.score ? YES : NO;
}
void sortStudents(Student *array, int n, BOOL(*p)(Student, Student))
{
Student temp;
int flag = 0;
for (int i = 0; i < n - 1 && flag == 0; i++)
{
flag = 1;
for (int j = 0; j < n - i - 1; j++)
{
if (p(array[j], array[j + 1]))
{
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
flag = 0;
}
}
}
}
int main() {
Student stu1 = {"小明", 19, 98};
Student stu2 = {"小红", 20, 78};
Student stu3 = {"小白", 21, 88};
Student stuArray[3] = {stu1, stu2, stu3};
sortStudents(stuArray, 3, compareByScore);
return 0;
}
没错,动态排序就是这么简单!
三、函数指针作为函数的返回值
函数既然可以作为参数,自然也可以作为返回值。
比如我们有一个函数AFunction,这个函数的参数为一个字符串,即char类型的指针,还有一个函数指针参数接受一个函数作为参数;
要返回这样一个函数BFunction,这个函数有一个int类型的返回值,有两个int类型的参数,那指向这个函数的指针定义为如下:
int (*p)(int a,int b)=BFunction;
按照第一节的内容,这个指针的类型应改为:
int (*)(int a,int b) //这就是BFunction的类型
那我们怎么去定义AFunction呢?
按照我们面向对象的思想,知道了函数的返回类型,我们这样定义是不是就可以了:
int (*)(int, int) AFunction(char *ch,int (*p)(int,int))
{
}
//前面的 int (*)(int, int) 就是我要返回的函数的指针
然而:这看起来很符合我们的理解,然而,这并不正确,编译器无法识别两个完全并行的包含形参的括号(int, int)和char *ch,int (*p)(int,int),
那到底该怎么做呢?真正的形式其实是这样:
int (*AFunction(char *ch,int (*p)(int,int)))(int, int);
这种声明从外观上看更像是脸滚键盘出来的结果一团乱糟糟的,现在让我们来逐步的分析一下这个声明的组成步骤:AFunction() 是一个函数
- (1)AFunction(char *ch,int (*p)(int,int)) 函数接受一个类型为char *的参数和一个函数指针int (*p)(int,int)
- (2)*AFunction(char *ch,int (*p)(int,int)) 函数返回一个指针,这不就是“指针函数(返回一个指针的函数)”的通用形式吗?这不过这里返回的指针本神又指向一个函数而已,所以类比于通用形式:
int *p(int,int) //指针函数的通用形式
- 我们将这里的 *AFunction(char *ch,int (*p)(int,int)) 这个整体看成是通用形式里面的 p
- (3)(*findFunction(char *ch,int (*p)(int,int)))() 这个指针指向一个函数
- (4)(*findFunction(char *ch,int (*p)(int,int)))(int, int) 指针指向的函数接受两个整形参数
- (5)int (*findFunction(char *ch,int (*p)(int,int)))(int, int)指针指向的函数返回一个整形
现在我们的分析已经完成了,编译器可以通过了,我们来看一个简单的例子,要实现的功能如下:
函数AFunction同上面不变,他接受的参数是一个BFunction函数,然后根据给AFunction传递的参数信息,选择性的返回这个BFunction函数,如下:
#include <iostream>
#include <stdlib.h>
using namespace std;
//即上面需要返回的BFunction函数,执行加法操作
int add(int a, int b)
{
return a + b;
}
int (*AFunction(const char * ch, int(*p)(int, int)))(int a,int b) //实际上就是 int (*p)(int,int)
{
if (ch == "add") //只有传入“add”的时候才返回加法函数,否则返回null
{
return p;
}
else
{
return NULL;
}
}
int main()
{
//返回的类型要与定义的BFunction兼容
int(*p)(int, int) = AFunction("add", add);
int result = p(1000, 2000);
printf("the result is : %d\n", result);
getchar();
return 0;
}
我们可能会疑惑,这用一个简单的条件判断,然后直接调用BFunction还不是一样的,何必多此一举,为什么我要以函数去获取函数呢,直接使用BFunction不就好了么,其实在以后的编程过程中,很有可能maxValue和minValue被封装了起来,类的外部是不能直接使用的,那么我们就需要这种方式,如果你学习了Objective-C你会发现,所有的方法调用的实现原理都是如此。
但是,上面的这个定义是在是太过于难看,括号那么多,看的不清楚,有没有简单一些的方法,当然是有的,借助于typedef即可完成。
我们说有下面的关系:
int (*p)(int,int) 实际上等价于 int (*)(int,int) p
前者是正确的书写,后者是面向对象更直观的展现,方便人看,最然并不能通过编译,我们借助于typedef可以完成这一转变。
typedef int(*FUNC)(int, int);
//这就相当于自定定义一个 “类型 对象” 的转换
//等价于 int(*)(int, int) 这个类型用FUNC来简短表示
等价于 int(*)(int, int) 这个类型用FUNC来简短表示 ,FUNC在这里就有了类型的含义了,注意理解,这个很重要;
现在我们来重新实现上面的代码,如下:
#include <iostream>
#include <stdlib.h>
using namespace std;
typedef int(*FUNC)(int, int); //定义一个FUNC代表 int(*)(int, int) 类型
//即上面需要返回的BFunction函数,执行加法操作
int add(int a, int b)
{
return a + b;
}
FUNC AFunction(const char * ch, int(*p)(int, int)) //这实际上就是 “类型名称 对象名称”,看起来就比较自然了
{
if (ch == "add") //只有传入“add”的时候才返回加法函数,否则返回null
{
return p;
}
else
{
return NULL;
}
}
int main()
{
//返回的类型要与定义的BFunction兼容,也是“类型名称 对象名称”的表示
FUNC p = AFunction("add", add);
int result = p(1000, 2000);
printf("the result is : %d\n", result);
getchar();
return 0;
}
是不是觉得原来C语言也可以这么灵活,也可以这么玩啊。
四、C语言使用typedef 简化指针定义
typedef的作用就是专门给类型名起一个别名的,如下:
typedef int HaHa;
typedef double Hello;
所以我们同样可以给指针类型起一个别名。
(1)变量指针
int a = 100;
typedef int * Pointer; //Pointer就是类型 int *,int *是类型名,Pointer是别名
Pointer p = &a;
(2)常量指针
int const a = 100;
typedef int const * Pointer; //Pointer的类型就是 int const *,int const *是类型名,Pointer是别名
Pointer p = &a;
(3)数组指针(二维的)
int a[][4] = { {1,2,3,4},{5,6,7,8} };
typedef int(*Pointer)[4]; // Pointer等价于类型 int (*)[4],int (*)[4]是类型名,Pointer是别名
Pointer p = a;
(4)函数指针
typedef int (*Pointer)(int,int); //Pointer等价于类型 int (*)(int,int),int (*)(int,int)是类型名,Pointer是别名
Pointer p = add; //但是这里由于C语言语法的关系,我们不能写成 int (*)(int,int) Pointer 这样的形式
//函数本身又返回一个指向int的指针
typedef int *(*Pointer)(int,int); //Pointer等价于类型 int *(*)(int,int),int *(*)(int,int)是类型名,Pointer是别名
Pointer p = add;
总结:通过typedef我们可以将C语言晦涩难懂的各种指针统一成一样的格式,即
类型 变量
这样的规范格式,方便查看。