前言
该篇博客为本系列最后一期,认真学习后一定可以轻松掌握指针。如果没有看过前两期一定要先学习前两期之后再来学习本篇博客。第一期,冲冲冲!第二期,冲冲冲!
学好C语言是学好编程的基础,在我的博客中,我将会分享一些关于C语言的编程经验和技巧,对此感兴趣的小伙伴千万不要忘记关注博主同时订阅此专栏哦~C语言学习
函数指针是用来存放函数的地址的,这还会有人问:函数也有地址么?
显然回答是肯定的。我们不妨用这样一段代码进行一下测试。
#include
int add(int x, int y)
{
return x + y;
}
int main()
{
printf("%p", add);
return 0;
}
我们成功的打印了加法函数的地址。
我们可以从中得知,函数名就是函数的地址,函数指针是用来储存函数地址的指针。
下面是定义函数指针的格式。(假设有一个与上面一样的add函数)
int ( * p)(int x,int y) = add;(最前头的int代表指针中储存的地址指向的函数的返回值类型为int类型,*p代表p是一个指针,(int x,int y)是指针中储存的地址指向的函数的参数)
使用
#include
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*p)(int x,int y) = add;
printf("%d\n", (*p)(1, 2));//*p可以取出add函数然后后面带上add函数的参数即可
return 0;
}
其实这里的(*p)(1,2)也可以写成p(1,2)因为对p解引用取出来的add函数还是一个add的地址,与p中储存的地址一样。
假如要我们设计这样一个简易的计算器(只计算整数),输入1计算加法,输入2计算减法,输入3计算乘法,输入4计算除法。那么一般情况下我们会使用Switch语句,如下。
#include
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;
}
int main()
{
int n, x, y;
printf("输入1 加法 输入2 减法 输入3 乘法 输入4 除法\n");
while(scanf("%d %d %d", &n, &x, &y)!=-1)
switch (n)
{
case 1:
printf("%d\n", add(x, y));
break;
case 2:
printf("%d\n", sub(x, y));
break;
case 3:
printf("%d\n", mul(x, y));
break;
case 4:
printf("%d\n", div(x, y));
}
return 0;
}
这样我们就实现了简易的计算器,但是我们会发现,我们用switch语句的话比较麻烦,我们编写了四段基本一模一样的代码,那么有没有什么方法可以防止出现重复的代码呢?没错就是使用转移表来解决。
#include
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;
}
int main()
{
int n, x, y;
printf("输入1 加法 输入2 减法 输入3 乘法 输入4 除法\n");
int (*wwr[5])(int x, int y) = { NULL, add,sub,mul,div };//定义一个函数指针数组,下标1代表加法下标2代表减法......
while (scanf("%d %d %d", &n, &x, &y) != -1)
{
int ret = (*(wwr + n))(x, y);//*(wwr + n)可以找到想要的函数,等价于wwr[n]
printf("%d", ret);
}
return 0;
}
既然我们已经学过指针数组,那么函数指针数组的定义我们也肯定可以轻松拿捏,就像上面的定义方式一样。我们使用转移表的方法,只用了两三行就顶替了之前的一大段代码,可见这样可以极大的减少我们的代码量。
观察下面两段代码分别有什么含义(以下两段代码出自于《C陷阱和缺陷》)
1,( * ( void ( * ) ( ) ) 0 )( )
是不是非常的难懂?那么让我们来分析分析吧。
首先我们要把这段代码拆开,0前面的是( void ( * ) ( ) ),其中void ( * ) ( )不正是一个函数指针类型么?,在括号里的一个数据类型不正是强制类型转化么?我们把整数类型的0转化为了函数指针类型。
接下来就可以简化成( * 0 )( )(此时0是一个函数指针)我们对0解引用可以得到函数的地址,然后后面的括号代表调用参数。
我们发现这段代码不就是把数字0强制转化成一个函数指针,然后解引用再调用这个函数么?
2,void ( * signal ( int , void ( * ) ( int ) ) ) ( int )
同样我们先拆解,signal ( int , void ( * ) ( int ) ) 这不是很像一个函数声明么?但是前面没有加返回值类型,我们再看将其拆出来以后,之前的代码变成了void ( * )(int),这不正是一个函数指针类型么?那么我们可以知道signal 这个函数的返回值类型为void ( * )(int)。
连起来就是signal这个函数它有两个参数类一个是int,一个是返回值为void参数为int的函数指针类型,然后signal的返回类型是一个没有返回值且参数为int的函数指针类型。
介绍
void qsort (void * base, size_t num, size_t size,int ( * compar)(const void *,const void * ));
该函数的功能很强大,可以对任何类型的数组根据一项要的方式进行排序(包括整形,浮点型,字符串甚至自定义的结构体类型也可以进行排序)。
那我们来看看这个函数的参数都代表着什么吧。
base
base是首元素的地址,既然要对数组进行排序那么我们应该告诉函数我们要从哪里开始。qsort函数能排序任意数据类型的一组数据,所以我们用void*类型的指针来接受。
num
num是数组的元素的个数,我们需要将排序的元素的个数传给qsort来确定一组数据的终点位置。
size
size是元素的大小,我们知道void * 类型的指针不能进行加减操作,也就无法移动,那么我们该怎样对变量进行操作呢?我们可以把void * 类型的指针强制类型转化为char * 类型因为对char * 类型的指针移动的单位字节长度是1个字节,我们想要找到数组中某下标的位置的时候可以采用这样的加法操作以应对不同数据类型的数组元素大小不同的问题,base + i * size(i是想要寻找的下标)
比如int类型的元素大小为4那么我们只要让base + i * 4就可以找到所需要的下标了。
compar
这是一个自定义的比较函数,我们需要告诉qsort函数我们希望数据按照怎么的方式进行比较,所以我们要写一个函数来制定判断大小的规则。compar的返回值为负数时代表对比的前元素小于后一个元素,两个元素的位置会进行交换,而返回值为0或正数时两个元素不会交换位置。我们简单的用定义一个处理int类型数据的compar函数进行示范。
int compar_int(const void* a, const void* b)
{
return *(int*)a - *(int*)b;//强制性转化后解引用得出a,b的值然后相减,若a
}
使用实例
我们以对结构体数组进行排序作为示例演示。
#include
#include
#include
struct anime_characters//动漫人物
{
char name[20];
int age;
};
int compar(const void* a, const void* b)//定义按照名字的字母顺序进行排列
{
return strcmp(((struct anime_characters*)a)->name, ((struct anime_characters*)b)->name);//强制类型转化后让其指向name然后使用strcmp对name进行比较
}
int main()
{
struct anime_characters arr[4] = { {"mikoto",14},{"emiria",114514},{"rem",17},{"megumin",15} };
qsort(arr, (sizeof(arr) / sizeof(arr[0])), sizeof(arr[0]), compar);
int z = 4;
while (z--)
printf("%s %d\n", arr[z].name, arr[z].age);
return 0;
}
既然我们已经学会使用qsort函数了,那么为了进一步理解函数和指针我们不妨尝试一下自己模拟出来qsort函数。
#include
#include
struct anime_characters
{
char name[20];
int age;
};
int compar(const void* a, const void* b)//排序方式
{
return strcmp(((struct anime_characters*)a)->name, ((struct anime_characters*)b)->name);
}
void my_qsort(void* arr, size_t num, size_t sz, int(*compar)(const void* a, const void* b))
{
for (int i = 0; i < num - 1; i++)//冒泡排序法
{
int flag = 0;
for (int j = 0; j < num - 1 - i; j++)
{
if (compar((char*)arr + j * sz, (char*)arr + (j + 1) * sz) < 0)
{
char* x = ((char*)arr + j * sz);
char* y = (char*)arr + (j + 1) * sz;
for (int k = 0; k < sz; k++)//一个字节一个字节的交换,因为不同类型数组,元素大小不同,一个字节一个字节替换可以应对任何类型
{
char tmp = *x;
*x = *y;
*y = tmp;
x++;
y++;
}
flag = 1;
}
}
if (flag == 0)break;//已排序完成退出循环
}
}
int main()
{
struct anime_characters arr[4] = { {"mikoto",14},{"emiria",114514},{"rem",17},{"megumin",15} };
my_qsort(arr, (sizeof(arr) / sizeof(arr[0])), sizeof(arr[0]), compar);
for (int i = 0; i < 4; i++)
{
printf("name: %-8s age: %-8d\n", arr[i].name, arr[i].age);
}
return 0;
}
指针学习是编程之路上的一次重要的里程碑,它让我们更深入地了解了计算机操作系统和编程语言的底层实现。相信在今后的编程之路上,我们会不断地遇到新的挑战并不断地成长,同时也会更加深刻地体会到指针在编程中的重要性。
如果觉得博主写的不错就请给博主点个赞,收个藏,关个注吧~
我们下期再见!