目录
[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语言的其余内容,请期待后续更新
以上内容如有错误或不准确之处,欢迎指出,或者你有更好的想法,也欢迎交流。