笔记19-2(C语言进阶 指针进阶)

目录

注:

函数指针

调用函数指针

有趣的代码

函数指针数组

函数指针数组的应用

指向函数指针数组的指针

回调函数

例子:改进计算器

qsort函数(头文件:)

分析

使用例

qsort函数实现降序

模拟实现

小知识(空指针void)


注:

 本笔记参考B站up鹏哥C语言的视频



函数指针

||| 顾名思义,函数指针就是指向函数的指针,即存放函数地址的指针。

首先观察函数的地址

int Add(int x, int y)
{
	return x + y;
}
int main()
{
        //&函数名 - 取到的就是函数的地址
	printf("%p\n", &Add);
	printf("%p\n", Add);

	return 0;
}

通过%p打印地址,结果发现函数确实存在地址。

注意:此处对Add函数是否取地址(&),并没有影响打印结果。即存在 函数名 == &函数名 (函数没有首元素的地址这种说法)

想要把函数存入指针内,指针的类型必须和函数匹配。

int(*pf)(int, int) = &Add;

这里的 pf 就是一个函数指针变量。

举一反三

void test(char* str)
{}
int main()
{
	void (*pt)(char*) = &test;
	return 0;
}

调用函数指针

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int(*pf)(int, int) = &Add;
	int ret = (*pf)(3, 5);
	printf("%d\n", ret);

	return 0;
}

这就是函数指针调用的一种形式(即对pf进行解引用),输出结果:8。同时,由于取地址(&)可以去掉,所以指针调用也可以写成 int(*pf)(int, int) = Add; 的形式。

从上面可以得出一个结论:Add == pf

这时候再倒回来看最开始调用函数的方式:

int ret = Add(3, 5);

由结论猜想在该函数调用中,是否可以把 Add 换成 pf ?

int ret = pf(3, 5);

结果程序顺利运行。所以由结论引申出:pf == (*pf)

实际上,无论这里的 * 是否存在,乃至数量多少,存在 pf == (**pf)(** = n个*)。即 * 在这里没有实际运算上的意义

注意:

*pf(3, 5);这种写法是不行的,因为pf(3, 5)已经完成了对函数的调用,* 在这里是对8进行解引用操作,显然不行。

有趣的代码

推荐书籍:《C陷阱和缺陷》 |||   这本书中提及下面两个代码。

//代码1
(*(void (*) () )0)();

代码意思:调用0地址(首地址是0)处的函数,被调用的函数无参,返回类型是void。

分析:

||| 0是一个数字,本身不能是一种地址。通过把0强制转换成函数指针类型,就可以把0看作一个地址。比如:(char) 0;  把0强制转换成了char类型。

再回来看代码1本身,( (void (*) () )0 - 就是把0强制转换成(被解释成)函数指针类型。此时0这个地址内就 存放着函数 (void (*) ()

而如果现在要调用放在0这个地址内部的函数,就可以解引用:( * ( (void (*) () )0 ) ( )(最后面的( )要和函数的参数部分匹配,原函数无参,所以此处不写参数)

ps:由于Windows把0地址都分配给了系统文件,所以平时是不可以调用的。

---

//代码2
void (*signal(int, void(*)(int)))(int);

代码意思:声明了一次signal函数,这个函数返回类型是void (*)(int),有两个参数 - int 和 一个函数指针。

分析:

对signal(int, void(*)(int)

  1. 首先,signal先和( )结合,说明signal是个函数名。
  2. 其次,对这个函数而言,第一个参数的类型是int,第二个参数的类型是函数指针。(注意:void(*)(int) 内部的(*)内并没有出现变量名,由此可知这是一个函数指针类型的参数。而这个函数指针,指向的是一个参数是int,返回类型是void的函数。)

现在,函数名和函数的参数已经得到了讨论,还缺少函数的返回类型

回来看去掉已经讨论过部分的原代码:void (*)(int)  这是一个函数指针类型。结合上文,说明这就是signal函数的返回类型。

综上所述,signal是一次函数的声明。

如果进行简化:void (*)(int) signal(int, void(*)(int)) 这种写法语法是不支持的。函数类型是指针的话,* 必须和函数名和函数参数的部分连在一起。

语法允许的简化:

typedef - 对类型进行重定义/重命名如:

typedef void (*pfun_t) (int);//对void(*)(int)的函数指针类型重命名为pfun_t
typedef unsigned int uint;//对unsigned int 的整型类型重命名为uint

所以,函数可以简化为

typedef void (*pfun_t) (int);
pfun_t signal(int, pfun_t);

函数指针数组

根据学过的知识:

//整型指针 int*
//整型指针数组 int* arr[5]

由以上可知:
函数指针 - 存放函数指针的数组。

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int main()
{
	int (*pf_1)(int, int) = Add;
	int (*pf_2)(int, int) = Sub;
	int (*pf_Arr[2])(int, int) = { Add, Sub };

	return 0;
}

上述代码中的 pf_Arr 就是一个函数指针数组。

即:在函数指针数组内,可以存放多个同类型的函数指针

函数指针数组的应用

想看这串代码,写一个计算器,要实现整型的加减乘除:

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int Mul(int x, int y)
{
	return x * y;
}

int Div(int x, int y)
{
	return x / y;
}

void menu()
{
	printf("************************\n");
	printf("**** 1.add    2.sub ****\n");
	printf("**** 3.mul    4.div ****\n");
	printf("****     0.exit     ****\n");
	printf("************************\n");
}

int main()
{
	//计算器 - 计算整型变量的加、减、乘、除
	int input = 0;
	do
	{
		menu();
		int x = 0;
		int y = 0;
		int ret = 0;
		printf("请选择:");
		scanf("%d", &input);
		printf("请输入2个操作数:");
		scanf("%d %d", &x, &y);
		switch (input)
		{
		case 1:
			ret = Add(x, y);
			break;
		case 2:
			ret = Sub(x, y);
			break;
		case 3:
			ret = Mul(x, y);
			break;
		case 4:
			ret = Div(x, y);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
		printf("ret = %d\n", ret);
	} while (input);
	return 0;
}

这串代码存在问题,会发生:

笔记19-2(C语言进阶 指针进阶)_第1张图片

这就存在问题了:用户明明是输入错误和要求退出,程序却还是要求用户输入数据。

既然如此,要怎么更改代码呢?

比如这样:

case 1:		
	printf("请输入2个操作数:");
	scanf("%d %d", &x, &y);
	ret = Add(x, y);
	break;

这样写,确实是一种方法,但是这样的代码存在缺陷:

  1. 如果观察整块代码,会发现同样的代码重复了4次,出现代码冗余。
  2. 对于后续增加计算器功能不方便。
int main()
{

	int input = 0;
	do
	{
		menu();

		int x = 0;
		int y = 0;
		int ret = 0;
		printf("请选择:");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入2个操作数:");
			scanf("%d %d", &x, &y);

			int (*pf_Arr[5])(int, int) = { NULL, Add,Sub,Mul,Div };
			//pf_Arr就是函数指针数组

			ret = (pf_Arr[input])(x, y);
			printf("ret = %d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出程序\n");
		}
		else
		{
			printf("输入错误,请重新输入\n");
		}
	} while (input);
	return 0;
}

在上述程序中,int (*pf_Arr[5])(int, int) = { NULL, Add,Sub,Mul,Div } 的应用被称为转移表 (来自《C和指针》)

指向函数指针数组的指针

整型数组
int arr_1[5];
int(*p1)[5] = &arr_1;

整型指针的数组
int* arr_2[5];
int* (*p2)[5] = &arr_2;
p2是指向【整型指针数组】的指针

函数指针的数组是一个数组,取出函数指针数组的地址:

int(*p)(int, int); 函数指针
int(*p3[4])(int, int); 函数指针的数组

&p3 - 取出的是函数指针数组的地址。
int(* (*p3)[4])(int, int) = &p3;
p3就是一个指向【函数指针数组】的指针

void test(const char* str)
{
	printf("%s\n", str);
}
int main()
{
	//函数指针 p_fun
	void (*p_fun)(const char*) = test;

	//函数指针的数组 p_fun_Arr
	void (*p_fun_Arr[5])(const char*);
	*p_fun_Arr = test;

	//指向函数指针数组 p_fun_Arr 的指针 pp_fun_Arr
	void (*(*pp_fun_Arr)[10])(const char*) = &p_fun_Arr;

	return 0;
}

回调函数

||| 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个函数被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或者条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

笔记19-2(C语言进阶 指针进阶)_第2张图片

例子:改进计算器

新建并使用Calc函数

int Calc(int(*pf)(int, int))//参数是一个函数指针
{
	int x = 0;
	int y = 0;
	printf("请输入2个操作数:");
	scanf("%d %d", &x, &y);
	return pf(x, y);
}

具体使用例:

int Add(int x, int y)
{}

int Sub(int x, int y)
{}

int Mul(int x, int y)
{}

int Div(int x, int y)
{}

void menu()
{}

int Calc(int(*pf)(int, int))
{
	int x = 0;
	int y = 0;
	printf("请输入2个操作数:");
	scanf("%d %d", &x, &y);
	return pf(x, y);
}
int main()
{
	int input = 0;
	do
	{
		menu();

		int ret = 0;
		printf("请选择:");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			ret = Calc(Add);
			printf("ret = %d\n", ret);
			break;
		case 2:
			ret = Calc(Sub);
			printf("ret = %d\n", ret);
			break;
		case 3:
			ret = Calc(Mul);
			printf("ret = %d\n", ret);
			break;
		case 4:
			ret = Calc(Div);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
		printf("ret = %d\n", ret);
	} while (input);
	return 0;
}

通过回调函数,可以减少冗余

qsort函数(头文件:

分析

库函数 - qsort   ||| 快速排序

qsort函数的原型(包含了4个参数):

笔记19-2(C语言进阶 指针进阶)_第3张图片

值得注意的是:qsort函数排序时可以接收所有类型的函数

||| 对于 void* base :base中存放的是待排序的数据中的第一个对象的地址。使用 void* 可以承接不同数据类型。

 ------

||| 对于 size_t num待排序的数据元素的个数

 ------

||| 对于 size_t size一个元素字节所占空间大小,单位是字节qsort函数在进行排序时,并没有限制排序元素所占空间的大小,而不同的数据类型之间存在差异。

------

||| 对于 int (*compar)(const void*, const void*)比较待排序数据的不同元素的函数(返回类型是整型),函数的第一个参数传入要比较的 第一个元素 的地址,第二个函数传入的是要比较的 第二个元素 的地址,同样,函数内的 void 类型是为了接收各种类型的元素。

笔记19-2(C语言进阶 指针进阶)_第4张图片

此处,对于被调用的比较函数要求返回三种情况,分别是:>0   =0   <0

参考冒泡排序

void bubble_sort(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < sz - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

 比如:如果我们要求是比较字符串类型的数据,可以通过 strcmp函数 比较字符串的大小,这时候最方便的方法就是更改两个不同元素的比较方法:

即原本 if (arr[j] > arr[j + 1]) 内部比较数组的方式,改为使用 strcmp函数


使用例

排序整型

#include //printf
#include//qsort
void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//排序
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	//打印
	print_arr(arr, sz);
	return 0;
}

排序结构体(按照年龄排序)

#include//qsort
struct Stu
{
	char name[20];
	int age;
};

int sort_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

int main()
{
	//使用qsort函数排序结构体数据
	struct Stu s[] = { {"zhangsan", 30}, {"lisi", 34}, {"wangwu", 20}};
	int sz = sizeof(s) / sizeof(s[0]);

	//按照年龄排序
	qsort(s, sz, sizeof(s[0]), sort_by_age);

}

启动调试,完成排序,监视

笔记19-2(C语言进阶 指针进阶)_第5张图片

---

排序结构体(按照名字排序)

#include//qsort函数
#include//strcmp函数

struct Stu
{
	char name[20];
	int age;
};

int sort_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

int main()
{
	//使用qsort函数排序结构体数据
	struct Stu s[] = { {"zhangsan", 30}, {"lisi", 34}, {"wangwu", 20}};
	int sz = sizeof(s) / sizeof(s[0]);

	//按照名字排序
	qsort(s, sz, sizeof(s[0]), sort_by_name);
}

 启动调试,完成排序,监视(顺序应该是 l > w > z)

笔记19-2(C语言进阶 指针进阶)_第6张图片

ps:

        字符串比较是对于位置的ASCII值进行比较,这和长度无关。


qsort函数实现降序

只需要对比较函数的两个参数进行位置交换即可。如:

int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

这段代码原本是*(int*)e1 - *(int*)e2 ,对应的qsort函数实现的是 升序

只要改成 return *(int*)e2 - *(int*)e1  ,即可使qsort函数实现 降序 。(原本应该传回>0的情况传回了<0,<0的情况传回了>0)


模拟实现

Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for ( i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

//模仿qsort实现一个冒泡排序的通用算法
void bubble_sort(
	void* base,
	int sz,
	int width,
	int (*cmp)(const void* e1, const void* e2)
)
{
	int i = 0;
	//趟数
	for (i = 0; i < sz - 1; i++)
	{
		//一趟排序
		int j = 0;
		for (j = 0; j < sz - i - 1; j++)
		{
			//两个元素进行比较
			if (cmp((char*)base + j * width, 
				(char*)base + (j + 1) * width) > 0)
			{
				//交换
				Swap((char*)base + j * width, 
					(char*)base + (j + 1) * width, 
					width);
			}
		}
	}
}

bubble_sort函数模拟实现。

比较部分(if语句)

对于传来的数据,需要寻找 e1e2 的地址。注意:此时base是无类型指针

这种方式是对 base 强制类型转换成 char*类型,在加上单个元素所占空间大小 width 得到 (char*)base + width。所以比较部分代码为:

if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)

通过 jj + 1 寻找任意两个相邻的元素。这就是函数指针实现回调函数

交换部分(Swap函数)

还是通过寻找函数位置找到任意相邻的两个元素。要注意的是:交换部分是通过交换 每一个字节的内容(char*类型 +1 就是操作1个字节的内容)来实现两个元素的交换。

Swap((char*)base + j * width, 
	(char*)base + (j + 1) * width, 
	width);
Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for ( i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

小知识(空指针void)

对于空指针void,存在:

int main()
{
	int a = 10;
	char ch = 'w';
	void* p = &a;
	p = &ch;
	return 0;
}

以上操作均可实现。但是,对p进行解引用操作*p,或者p++,这些都是是无法实现的。会出现错误:

*p时报错

p++时报错

 毕竟void*类型的指针到底访问几个字节是不知道的。

你可能感兴趣的