揭开指针的面纱(下)

目录

前言

[1 · 函数指针变量](#1 · 函数指针变量)

[1 - 1 · 函数指针变量的创建](#1 - 1 · 函数指针变量的创建)

[1 - 2 · 函数指针变量的使用](#1 - 2 · 函数指针变量的使用)

[2 · 两段有趣的代码](#2 · 两段有趣的代码)

[3 · typedef 关键字](#3 · typedef 关键字)

[4 · 函数指针数组](#4 · 函数指针数组)

[5 · 转移表](#5 · 转移表)

[6 · 回调函数](#6 · 回调函数)

[7 · qsort](#7 · qsort)

[7 - 1 · qsort 排序整型数据](#7 - 1 · qsort 排序整型数据)

[7 - 2 · qsort 排序结构数据](#7 - 2 · qsort 排序结构数据)

[7 - 3 · 模仿 qsort 函数](#7 - 3 · 模仿 qsort 函数)

[8 · sizeof 和 strlen 的对比](#8 · sizeof 和 strlen 的对比)

总结


前言

上一篇中简单的介绍了指针与数组有关内容,本篇是对指针介绍的最后一篇,将着重介绍指针与函数结合的内容。


1 · 函数指针变量

1 - 1 · 函数指针变量的创建

根据前面我们对数组指针,整型指针的类比,不难得出:

函数指针变量是指针变量,是指向函数的指针变量。

那既然我们要让指针指向函数,就必须要得到函数的地址,那么函数有没有地址呢?我们可以做个测试:

cpp 复制代码
#include <stdio.h>

void Test()
{
	;
}

int main()
{
	printf("%p\n", Test);
	printf("%p\n", &Test);
	return 0;
}

运行一下试试:

可以看到,函数的确是有地址的,并且函数名就是函数的地址。函数名和&函数名都是函数的地址 这两者没有区别。

那么既然有了地址,就可以放进指针变量中,考虑到类型匹配,函数的地址自然要放到函数指针变量中。

函数指针变量的写法其实与数组指针变量类似,如下:

cpp 复制代码
int Add(int x, int y)
{
	return x + y;
}

void Test()
{
	;
}

int main()
{
	void (*p1)() = Test;
	void (*p2)() = &Test;
	int (*p3)(int,int) = Add;
	int (*p4)(int x,int y) = &Add;//x和y 可以省略

	return 0;
}

考虑到优先级,(*p) 的圆括号不能丢。

(*p) 前面的类型是指向函数的返回类型。

(*p) 后面的圆括号,需要与指向函数的形参 类型,个数一一对应。


1 - 2 · 函数指针变量的使用

通过函数指针调用指向的函数:

cpp 复制代码
#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int (*p)(int, int) = Add;

	int a = Add(3, 4);
	printf("%d\n", a);

	int b = (*p)(5, 6);
	printf("%d\n", b);

	int c = p(7, 8);
	printf("%d\n", c);

	int d = (*******p)(9, 10);
	printf("%d\n", d);
	return 0;
}

我们看看运行结果:

可以看到,都成功完成了任务。

那么这个时候你可能会对上面例子中的 下面两个函数指针变量的使用感到疑惑。

我们先来回想一下我们以前是怎么调用函数的:

函数名(实参)

在上面,我们测试了,函数名就是函数的地址,那么我们此时的指针变量p 里面放的就是函数的地址,所以之间用 p 也是可以调用的。

所以在我们使用函数指针变量调用函数时 这里的 * 其实是个摆设,可以不写,也可以写很多个。

那么函数指针变量的使用场景是什么呢,用在回调函数中,本文后面会介绍。


2 · 两段有趣的代码

cpp 复制代码
(*(void (*)())0)();

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

两段代码均出⾃:《C陷阱和缺陷》这本书
我们来逐个分析一下:

cpp 复制代码
( * ( void (*)() ) 0 )();

我们之前提到过,去掉变量名就是类型。

所以这里的 void (*)() 是一个函数指针类型。

那么给一个类型加上圆括号,那就是强制类型转换了,这里将 0 强制类型转换成 void (*)() 这个函数指针类型。

然后进行调用,这就意味着,我们假设 0 地址处放着一个无参数 返回类型为 void 的函数,最终效果是调用了 0 地址处放着的这个函数。

cpp 复制代码
void (*signal(int , void(*)(int)))(int);

这里我们看到,由于优先级的关系,signal 会优先与后面的圆括号结合,所以这里的 signal 是一个函数名。

那么函数名后面的圆括号自然放的就是参数,有两个参数,参数1的类型是 int 整型类型 ,参数2的类型是 void(*)(int) 函数指针类型。

那么一个函数,有了函数名,有了参数,是不是还差一个返回类型。

我们将我们分析过的部分暂时移开,那么就剩下了 void (*)(int) 这是一个函数指针类型,也是signal 的返回类型。

所以这段代码其实是在进行函数声明。

不难发现,这两段代码看起来很复杂,那么可不可以简化呢?我们可以用一个C语言中的关键字:typedef。


3 · typedef 关键字

typedef 如其名字 , 是用来对类型进行重命名的。

比如你觉得 unsigned int 太长了,要是能写成 uint 就好了,那么你就可以:

cpp 复制代码
typedef unsigned int uint;

如果是指针类型,能否重命名呢?其实也是可以的。比如将 int* 重命名为pint 可以这样写:

cpp 复制代码
typedef int* pint;

但是对于数组指针和函数指针稍微有点区别

比如我们有数组指针类型 int(*)[5] ,需要重命名为 parr

cpp 复制代码
typedef int(*parr)[5]; //新的类型名必须在*的右边

函数指针类型的重命名也是⼀样的,比如,将 void(*)(int) 类型重命名为 pfun

cpp 复制代码
typedef void (*pfun)(int)

那么我们想要简化上面的第二个代码,就可以这样写:

cpp 复制代码
typedef void (*pfun)(int);
pfun signal(int,pfun);

我们之前在扫雷游戏模拟实现那篇博客中 提到了 #define ,那么可不可以用 #define 来重命名呢?

可以是可以,不过 #define 和 typedef 是有差别的:

cpp 复制代码
typedef int* pint;

#define PINT int*

pint p1,p2;

PINT p3,p4;

在上面的例子中,p1,p2都是指针变量。p3是指针变量,p4是整型变量。

#define 实则是一个东西替换另一个东西 , 在上面的例子中 PINT p3,p4 ,实际上是 int* p3,p4。


4 · 函数指针数组

数组是⼀个存放相同类型数据的存储空间,我们之前介绍过指针数组,那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
如下:

cpp 复制代码
int (*parr[10])()

parr 先与 [ ] 结合,说明 parr 是一个数组,数组的内容是 int (*)() 类型的函数指针。

注意:存放在同一个函数指针数组里的函数指针类型要相同。


5 · 转移表

函数指针数组的用途:转移表

比如我们现在想要模拟实现一个简易计算机,支持加减乘除运算,我们一般会这么写:

cpp 复制代码
#include <stdio.h>

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 x = 0;
	int y = 0;
	int ret = 0;
	int input = 0;
	do
	{
		Menu();
		printf("请输入要进行的操作:>");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			printf("请输入两个数:>");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("结果是%d\n", ret);
			break;
		case 2:
			printf("请输入两个数:>");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("结果是%d\n", ret);
			break;
		case 3:
			printf("请输入两个数:>");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("结果是%d\n", ret);
			break;
		case 4:
			printf("请输入两个数:>");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("结果是%d\n", ret);
			break;
		case 0:
			printf("成功退出\n");
			break;
		default:
			printf("输入有误,请重新输入\n");
			break;
		}
	} while (input);
	return 0;
}

可以看到:我们的Add Sub Mul Div 这四个函数,它们的返回类型,参数个数,参数类型,都是一致的,并且在我们的 case1 2 3 4 中,除了调用的函数不同,其余代码都是相同的,那么我们写的这段代码就很冗余。

那么我们就可以使用函数指针数组来优化,如下:

cpp 复制代码
int main()
{
	int x = 0;
	int y = 0;
	int ret = 0;
	int input = 0;
	int (*cul[5])(int, int) = { 0,Add,Sub,Mul,Div };
	//第一个元素置0是为了让输入的操作与下标对应
	do
	{
		Menu();
		printf("请输入要进行的操作:>");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个数:>");
			scanf("%d %d", &x, &y);
			ret = cul[input](x, y);
			printf("结果为%d\n", ret);
		}
		else if (input == 0)
		{
			printf("成功退出\n");
		}
		else
		{
			printf("输入有误,请重新输入\n");
		}
		
	} while (input);
	return 0;
}

6 · 回调函数

回调函数就是一个通过函数指针调用的函数
把一个函数的地址给另一个函数,并在其中通过指针调用 那么被给出地址并调用的这个函数就被称为回调函数。
那么对于上面的简易计算器,我们也可以这么优化:

cpp 复制代码
int cul(int (*pfun)(int, int))
{
	int x = 0;
	int y = 0;
	int ret = 0;
	printf("请输入两个数:>");
	scanf("%d %d", &x, &y);
	ret = pfun(x, y);
	printf("结果为%d\n", ret);
	return 0;
}

int main()
{
	int input = 0;
	do
	{
		Menu();
		printf("请输入要进行的操作:>");
		scanf("%d", &input);
		
		switch (input)
		{
		case 1:
			cul(Add);
			break;
		case 2:
			cul(Sub);
			break;
		case 3:
			cul(Mul);
			break;
		case 4:
			cul(Div);
			break;
		case 0:
			printf("成功退出\n");
			break;
		default:
			printf("输入有误,请重新输入\n");
			break;
		}
	} while (input);
	return 0;
}

这也是函数指针的一种用途 作为一个中介。
主调函数中没有直接调用这些回调函数,而是把信息传递给一个函数 再在这个函数中通过函数指针调用这些回调函数。


7 · qsort

qsort 是一个库函数,是用来排序的。使用需要包含头文件 stdlib.h 直接就可以用来排序数据。

qsort 底层使用的是快速排序的方式,并且qsort 可以排序任意类型的数据。

我们先来看看qsort的原型:

复制代码
void qsort (void* base, 
            size_t num, 
            size_t size,
            int (*compar)(const void*,const void*)
            );

可以看到 qsort 有四个参数:

void* base 指针,指向待排序数组的第一个元素。

size_t num 是base 指向的待排序数组的元素个数。

size_t size 是base 指向的待排序数组的元素大小。

int (*compar)(const void*,const void*) 是一个函数指针,指向一个两个元素的比较函数。

比较函数是有要求的:

如果前一个指针指向的数据 大于 后一个指针指向的数据,返回一个 大于 0 的值。

如果两指针指向的数据相等,返回0

如果前一个指针指向的数据 小于 后一个指针指向的数据,返回一个 小于 0 的值。

注意:void* 类型的指针,是无具体类型的指针 这种类型的指针是不能直接解引用,也不能+-整数的,但是我们作为qsort 的使用者,是知道我们要排序什么类型的数据的,所以这里我们需要用到强制类型转换。


7 - 1 · qsort 排序整型数据

我们可以这样写:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

void Print(int* p,int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
}

int CmpByInt(const void* p1, const void* p2)
{
	if (*(int*)p1 > *(int*)p2)
	{
		return 1;
	}
	else if (*(int*)p1 < *(int*)p2)
	{
		return -1;
	}
	else
	{
		return 0;
	}
}

int main()
{
	int arr[] = { 4,9,7,6,5,2,8,3,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("排序前:");
	Print(arr, sz);
	printf("\n");
	qsort(arr, sz, sizeof(arr[0]), CmpByInt);
	printf("排序后:");
	Print(arr, sz);
	return 0;
}

运行一下试试:

这里我们的 CmpByInt 还有种简便的写法:

cpp 复制代码
int CmpByInt(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}

这里的排序是从小到大升序 如果想要改成降序,只需对调return 后面的p1 和 p2

cpp 复制代码
int CmpByInt(const void* p1, const void* p2)
{
	return *(int*)p2 - *(int*)p1;
}

因为本来的逻辑是前者大于后者就交换。


7 - 2 · qsort 排序结构数据

假设我们定义了一个结构体:

cpp 复制代码
struct stu
{
	char name[20];
	int age;
};

那么结构体怎么比较大小呢?

对我们定义的这个结构体来说,可以通过名字来比较,也可以通过年龄来比较

通过名字比较,那就是字符串比较,可以用 strcmp。

通过年龄比较,那就是整型数据比较。

如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct stu
{
	char name[20];
	int age;
}stu;

int CmpStuByName(const void* p1, const void* p2)
{
	return strcmp(((stu*)p1)->name, ((stu*)p2)->name);
}

int CmpStuByAge(const void* p1, const void* p2)
{
	return ((stu*)p1)->age - ((stu*)p2)->age;
}

void Test1()
{
	stu a[] = { {"zhangsan",18},{"lisi",20},{"wangwu",19} };
	int sz = sizeof(a) / sizeof(a[0]);
	qsort(a, sz, sizeof(a[0]), CmpStuByName);
}

void Test2()
{
	stu a[] = { {"zhangsan",18},{"lisi",20},{"wangwu",19} };
	int sz = sizeof(a) / sizeof(a[0]);
	qsort(a, sz, sizeof(a[0]), CmpStuByAge);
}

int main()
{
	Test1();
	Test2();
	return 0;
}

我们通过调试看看效果:

Test1:

从监视窗口可以看到排序后的数组。

Test1 是通过名字比较,是字符串比较,用到了 strcmp。

strcmp 使用需包含头文件 string.h,是按照字符串中字符的 ASCII码值进行比较的,从比较的两字符串的 首字符开始比较,如果相同就比较下一个,直到不相同。如果全部字符都相同,那就是等于。\0的ASCII 码值是0。

第一个字符串大于第二个字符串,返回大于0的数字。

第一个字符串等于第二个字符串,返回0。

第一个字符串小于第二个字符串,返回小于0的数字。

Test2 :

从监视窗口可以看到排序后的数组。


7 - 3 · 模仿 qsort 函数

我们之前写了一个冒泡排序,但是那个冒泡排序只能用于对整型数据的排序,那么我们可以模仿 qsort 来优化我们的冒泡排序,使其可以排序任意类型的数据。

如下:

cpp 复制代码
void Swap(char* p1, char* p2, size_t size)
{
	while (size--)
	{
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		p1++;
		p2++;
	}
}

void BubbleSort(void* base, size_t num, size_t size, int (*compare)(const void* p1, const void* p2))
{
	int i = 1;
	int j = 0;
	int t = 0;
	int flag = 1;
	//趟数
	for (i = 1; i <= num - 1; i++)
	{
		int flag = 1;//判断是否提前排序完成
		//一次确定一个
		for (j = 0; j <= num - i - 1; j++)
		{
			if (compare((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
			{
				//从小到大排,前者大就交换
				Swap((char*)base + j * size, (char*)base + (j + 1) * size,size);
				//如果发生交换,说明还在进行排序
				flag = 0;
			}
		}
		//如果一趟下来没发生交换,说明已排序完成
		if (flag)
		{
			break;
		}
	}
}

这里需要注意的是,我们的参数没有数组,所以不能用下标访问操作符直接找到元素,及位置。

但是我们有起始位置 base ,所以我们可以从 base 开始,走 j*size 个,就能相当于找到了&arr[j]

一步走一个,所以把 base 强转成了 char* ,因为无法确定我们排序元素的大小,其他类型可能过大或过小,所以用 char*,一步走的距离为 1 字节。

Swap 也是同理,不清楚具体的元素类型,和具体大小所以一个一个字节交换。

下面我们测试一下:

cpp 复制代码
typedef struct stu
{
	char name[20];
	int age;
}stu;

int CmpByInt(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}

void Print(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
}

int CmpStuByAge(const void* p1, const void* p2)
{
	return ((stu*)p1)->age - ((stu*)p2)->age;
}

void Test3()
{
	int arr[] = { 4,9,7,6,5,2,8,3,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	BubbleSort(arr, sz, sizeof(arr[0]), CmpByInt);
	Print(arr, sz);
}

void Test4()
{
	stu a[] = { {"zhangsan",18},{"lisi",20},{"wangwu",19} };
	int sz = sizeof(a) / sizeof(a[0]);
	BubbleSort(a, sz, sizeof(a[0]), CmpStuByAge);
}

int main()
{
	Test3();
	Test4();
	return 0;
}

Test3 :

Test4 :

通过监视窗口可以看到排序后的数组。


8 · sizeof 和 strlen 的对比

sizeof是单目操作符
sizeof 计算变量所占内存内存空间大小的,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。
sizeof 只关注占用内存空间的大小,不在乎内存中存放什么数据。
如果sizeof 后接变量 那么括号可以省略,这也证明了sizeof 不是函数。
strlen 是C语言库函数
功能是求字符串长度。函数原型如下:

cpp 复制代码
size_t strlen ( const char * str );

统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。
strlen 函数会⼀直向后找 \0 字符,直到找到为止,所以可能存在越界查找。
对比:
sizeof
sizeof是操作符
sizeof计算操作数所占内存的大小,单位是字节
不关注内存中存放什么数据
strlen
strlen是库函数,使用需要包含头文件 string.h
srtlen是求字符串长度的,统计的是 \0 之前字符的个数
关注内存中是否有 \0 ,如果没有 \0 ,就会持续往后找,可能会越界
sizeof后接的表达式是不会计算的。

cpp 复制代码
#include <stdio.h>
int main()
{
	int a = 2;
	int b = 0;
	printf("%zd \n", sizeof(b = a + 4));
	printf("%d \n", b);
	return 0;
}

运行一下:

为什么呢?

C语言是编译型语言

sizeof 后接的表达式其实是在生成可执行程序后,双击运行才计算的。

而sizeof 在编译时就计算了。


总结

以上简单介绍了指针相关的一部分内容,关于C语言的其余内容,请期待后续更新


以上内容如有错误或不准确之处,欢迎指出,或者你有更好的想法,也欢迎交流。

相关推荐
计算机安禾2 小时前
【数据结构与算法】第43篇:Trie树(前缀树/字典树)
c语言·开发语言·矩阵·排序算法·深度优先·图论·宽度优先
yashuk2 小时前
C语言入门教程:程序结构与算法举例
c语言·算法·教程·程序设计·开发过程
代码地平线2 小时前
C语言实现堆与堆排序详解:从零手写到TopK算法及时间复杂度证明
c语言·开发语言·算法
学习噢学个屁3 小时前
基于51单片机心率仪—体温心率血氧蓝牙
c语言·单片机·嵌入式硬件·51单片机
千谦阙听3 小时前
数据结构最终章:万字详解排序算法!(内部排序)
c语言·数据结构·学习·算法·排序算法
念恒123063 小时前
Linux基础开发工具(Vim篇)
linux·c语言
念恒123063 小时前
Linux基础开发工具(yum篇)
linux·c语言
老花眼猫4 小时前
数学艺术图案画-曼陀罗(二)
c语言·经验分享·青少年编程·课程设计
网域小星球4 小时前
C 语言从 0 入门(十九)|共用体与枚举:自定义类型进阶
c语言·开发语言·算法·枚举·自定义类型·共用体