前言: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]=aint (*)[4]
函数指针指向函数的指针int (*p)(int a,int b)=addint (*)(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语言晦涩难懂的各种指针统一成一样的格式,即

类型  变量

这样的规范格式,方便查看。

 

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐