C语言之玩转指针(进阶)

简介: 在初始指针阶段,相信大家对指针已经有了初步了了解,接下来,我们进入C语言的指针进阶部分。在这之前,我们知道:1.指针是一个变量,是用来存放地址的变量,这个地址唯一标识一块内存空间。2.指针的大小是固定的4/8个字节,(32位平台/64位平台)。3.指针也是分为很多类型的,指针的类型决定了指针±整数的步长,也决定了指针解引用操作时有多大的访问权限(能访问几个字节)。4.指针的运算。

1.字符指针

在指针的类型中,有一种指针类型位字符指针char*。

如char ch = 'w'; char* ch = &ch;,这就是字符指针。再来看一段代码:

1.png

现在来解释一下这里发生了什么。首先先将字符串"abcdefg"存放到数组arr中去,数组名代表着首元素的地址,然后将数组名赋给pf,此时,pf相当于指向了那个字符数组arr。所以打印arr和打印pf结果是相同的。其实arr和pf指向的都是都是数组首元素的地址。

我们也可以换一种写法:

2.png

%c是打印一个字符,p是首地址,所以要解引用。%s是打印字符串,字符串打印的时候从首地址开始一直到‘\0’结束


我们来看一下其内存分布情况:

3.png

前面提到过这里的char* p = "abcdef";中的"abcdef"是一个常量字符串,那么其就不能被我们修改。例如*p = 'w';,我们是不能这样做的,这是一个错误的写法,一定要注意。

所以我们在char* p = "abcdef";之前加上const,即const char* p = "abcdef";,const修饰的是*p,意思是指针变量p所指向的字符串 "abcdef";不可被修改。

所以const char* p = "abcdef";是最正确的写法。


接下来以一道非常典型的题目来作为字符指针的结束:

4.png

这里就不进行细说,大家参照这句话可以自行尝试:数组存放在栈区,不管保存的数组内容是否相同,声明arr1和arr2时开辟的是两个独立的空间,所以两个数组地址是不同的;字符串存放在常量区,地址只有一份,两个指针变量指向的是同一个地址。

T1答案为456,T2答案为321。


2.指针数组

既然是指针数组,那它当然是一个数组了。

就比如int arr[10] = {0};是一个整型数组,里面存放着10个整型。char arr[5] = {0};是一个字符数组,里面存放着5个字符。

顾名思义,指针数组里存放的当然是指针了。

举一个指针数组的例子int* parr[4];是存放整型指针的数组,故称之为指针数组,再比如char* pch[5];是存放字符指针的数组,我们也将之称为指针数组。

我们结合代码来理解指针数组:

5.png

那如果我们想把10 20 30 40打印出来,我们可以这样:

6.png

然而指针数组其实不是这么用的,因为太低级了。

下面我们打开指针数组的一种用法:

7.png

下面来看其指针数组parr的内存分布:

8.png

其中parr[i]指向的是数组arr1、arr2、arr3中首元素的地址,加上j即arr[i]+j能够指向下一个地址,解引用之后即*(parr[i]+j)就能找到每个元素并打印了。

这只是指针数组的一种用法。


3.数组指针

数组指针,我们单纯的看名字也会认为它是一个指针,没错,数组指针的确是一个指针。

我们先来回顾一下之前学过的其他类型的指针。比如:int* p = NULL;是一个整型指针,是指向整型的指针,该指针中可以存放整型的地址;再比如:char* p = NULL;是一个字符指针,是指向字符的指针,该指针中可以存放字符的地址;那数组指针当然是指向数组的指针了,该指向数组的指针可以存放数组的地址。

那什么是数组的地址呢?数组arr是数组首元素的地址;&arr[0]也是数组首元素的地址;而&arr才是数组的地址。

现在,有一个数组arr[10]={1,2,3,4,5,6,7,8,9,10};我们如何通过数组指针将该数组存放起来呢?我们可以这样:int (*p)[10] = &arr。数组的地址要存起来,现在p就能指向数组arr了。即p是一个数组指针,它指向一个数组,数组里包含10个元素,每个元素的类型是int。

9.png

如上:我们现在要写出p的类型,应该怎么写呢?请跟着我的思路来:我们通过&arr已经拿到了数组arr的地址,数组的地址当然要存放到指向数组的指针里去。第一步:*p说明p是一个指针;第二步:(*p)[5]说明了指针p指向的是一个数组,数组里有五个元素;第三步:char*(*p)[5]说明了指针p指向的那个数组中每个元素的类型是char*。知道这里,char* (*p)[5]就是一个指向数组的数组指针。p是指针变量的名字。

再来看一个例子:

int arr[10];
int (*p)[10]=arr;


/

上面也是一个数组指针。


3.1arr和&arr的比较

数组名arr是首元素的地址。

而&数组名即&arr取出的是整个数组的地址。

来看下面一段代码:

10.png

但是我们会发现打印出来的地址是相同的,但事实真的如此吗?下面再来看一段代码:

11.png

现在,我们很明显的可以看出arr+1和&arr+1有着截然不同的差别。arr+1跳过的是一个元素

而&arr+1跳过的是一个数组。

总结:&arr和arr虽然值是一样的,但其带来的意义确实不相同的。&arr是数组的地址,而不是首元素的地址。数组的地址+1跳过的是整个数组的大小,所以在上面的代码中,&arr+1相对于&arr的差值是40。在数组指针中,我们是把数组的地址拿出来交给指针,而不是把数组首元素的地址拿出来交给指针。


3.2数组指针的使用

现在我们通过数组指针来吧数组int arr[10] = {1,2,3,4,5,6,7,8,9,10};中的每个元素打印出来,请看:


在这段代码中,数组指针pa中存放的既然是数组arr的地址,那我们通过*pa对其进行解引用操作就找到了数组arr,事实上这里*pa就相当于arr即arr==*pa,那么我们就可以把(*pa)[i]换一种写法:*(*pa+i)。当然无论是哪一种写法,其实都相对比较复杂。那我们为什么不这么写呢:

12.png

注意这里的*(p+i)完全可以替换成*(arr+i)读者可自行去尝试。

我们发现上述中的*(p+i)依然可以完成任务,既然是这样的话那我们为什么要把如此简单的代码写成数组指针的形式呢?这难道不是自讨苦吃吗,这实在是太罗嗦了。

事实上,数组指针不是这么用的,倘若非要这么用的话,会给我们添加不必要的麻烦。一般数组指针至少用到二维数组以上才会方便一些。有一个二维数组int arr[3][5] = {{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};我们想把这个二维数组打印一下。方式一(参数是数组的方式):

14.png

方式一理解起来也简单一些。

在进行第二种方式之前,我们先简单分析一下arr这个数组,即


int arr[3][5] = {{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};

数组名arr代表着首元素的地址,一定要注意,这里的首元素地址是第一行一维数组的地址,既然首元素地址是一个一维数组的地址,数组的地址,就应该放到数组指针中去,那我们进行传参时就可以写成这种形式,如下方式二(参数是指针的形式):

15.png

p+i就是指针在二维数组中行的移动,p+i找到的是某一行这个一维数组的地址,不要忘了,数组的地址和数组首元素地址大小是一样的,*(p+i)+j就是在这个一维数组里面移动,最后就是再通过*(*(p+i)+j)即可得到对应元素的值。

注意当我们要打印一维数组int arr[10] = {1,2,3,4,5,6,7,8,9,10};中的元素时,这四种写法是等价的(arr[i]==*(arr+i)==p[i]==*(p+i)),请看:

16.png

当我们要打印二维数组,((*(*(p + i)) + j)==*(p[i] + j)==(*(p + i))[j]==p[i][j])是等价的(参数是指针的形式),请看:

17.png

上述不同的写法一定要掌握。

关于数组指针的讲解到此结束。


4.数组传参和指针传参

在写代码时,我们有时要把数组或者指针传给函数,那我们应该如何设计函数的参数呢?


4.1一维数组传参

1.png

一维数组传参时,参数部分可以写成数组形式,也可以写成指针形式。倘若写成数组形式,数组的大小可以省略。


4.2二维数组传参

请看二维数组传(参数部分为数组形式)参示例:

2.png

下面再来看二维数组传参的另一种方式(参数为指针形式):

3.png

总结:二维数组传参时,函数形参的设计只能省略第一个[]内的数字,即行可以省略,但是列不可以省略。因为对于一个二维数组,可以不知道有多少行,但必须知道一行中有多少个元素,这样会方便计算。


4.3指针传参

4.3.1一级指针传参

我们直接来看代码:

4.png

思考:现在,请大家思考,当一个函数的参数部分为一级指针时,函数能接收什么参数呢?

比如:

void test1(int* p)
{}
//请问test1函数能接受什么参数呢?
void test2(char* p)
{}
//请问test2函数能接收什么参数呢?

请看:


#include<stdio.h>
void test1(int* p1)
{}
void test2(char* p2)
{}
int main()
{
  int a = 10;
  int* p1 = &a;
  test1(p1);
  test1(&a);
  char ch = 'w';
  char* pc = &ch;
  test2(&ch);
  test2(pc);
  return 0;
}


4.3.2二级指针传参

#include<stdio.h>
void test(int** ptr)
{
  printf("%d\n", **ptr);
}
int main()
{
  int n = 10;
  int* p = &n;
  int** pp = &p;
  test(pp);
  test(&p);
  return 0;
}


二级指针传参时,参数部分直接设计成二级指针没有任何问题

请大家再来思考一个问题,当这里的参数部分是一个二级指针,我们可以传什么样的参数呢?,我们可以传二级指针变量本身,一级指针变量的地址我们也可以传过去!即参数部分如果是二级指针的话,其无非就是接收二级指针或者一级指针的地址 当然还有其它的传参方式(数组),比如:


#include<stdio.h>
void test(int **p)
{}
int main()
{
  int* arr[10];
  test(arr);//此时的数组名是首元素的地址,即int*的地址,所以我们拿二级指针来接收当然没有问题
  return 0;
  //所以,我们传的可以是一个指针数组
}


二级指针传参总结:

  1. 当函数参数为二级指针时,可以传一个一级指针变量的地址
  2. 也可以传二级指针变量本身
  3. 也可以传一个存放一级指针数组的这样一个数组名(数组名代表着首元素地址,即一级指针的地址)


5.函数指针

//函数指针

//数组指针-指向数组的指针

//函数指针-指向函数的指针-存放函数地址的指针


那函数也有地址吗?当然了,函数当然也有自己的地址,并且我们还要知道:&函数名和函数名都是函数的地址,下面请看代码验证:

5.png

当我们要把函数的地址存放起来时,就需要用到函数指针了。比如:

daad36a73cd54394ad08b7707dd0f344.png

再比如:

7.png

void(*p)(char*) = Print;//这句话是什么意思呢:

我们来说说这段代码中的void(*p)(char*) = Print;是什么意思:首先*p说明p是一个指针,后面跟着一个圆括号即()说明指针p指向的是一个函数,char*说明函数的参数类型为char*,void说明函数的返回类型为void。

下面我们来看一个有趣的代码:

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

下面对代码1进行分析,注意代码1中的void (*)(int)是一个函数指针类型,用()把函数指针类型void (*) (int);括起来并在后面加上0即(void (*)(int))0意思是把0进行强制类型转换此时,数字0就会被当成是一个地址,即一个函数的地址。而此时(void (*)(int))0是一个地址,然受对这个地址进行解引用操作即*(void (*)(int))0就找到了这个函数,现在就到了最后一步,即(*(void (*)())0)();意思是调用函数,只不过函数是无参数的。总之,这一段代码(代码1)的意思是函数调用,即调用0地址处内的那个函数(参数为无参,返回类型为void类型)


下面对代码2进行分析:

代码2是这样的:

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

首先signal后面跟着圆括号说明signal是一个函数,那么这个函数的参数是什么呢?它的返回类型是什么呢?我们在来看signal后面的圆括号内部,即(int, void(*)(int)),int是其中一个参数、 void(*)(int)是其中的第二个参数;那signal既然是一个函数,那它的返回类型是什么呢?其实我们把函数名和函数的两个参数去掉之后,剩下的就是函数的返回类型,所以我们把signal(int, void(*)(int))去掉之后剩下的就是函数signal的返回类型,则函数signal的返回类型是void (*)(int);,所以说函数signal 的返回类型依然是一个函数指针 。 尤其要注意,这里即void (*signal(int, void(*)(int)))(int);并不是函数的调用,而是函数的声明(参数、返回类型)。

相信一些小伙伴们对代码2的函数signal的返回类型有一点点生疏,没有关系,下面我给大家举一个简单的函数返回类型的例子:

比如函数int Add(int,int);圆阔号内的两个int是该函数的两个参数,Add是该函数的一个函数名,当我们把函数名和函数的参数去点之后,剩下的就是函数的返回类型,所以函数Add的返回类型是int。

所以要想得到函数的返回类型,我们只需要把函数名和该函数的两个参数去掉之后,剩下的就是函数的返回类型。这个点大家一定要牢记。


通过对代码2的分析,我们发现函数指针这个类型稍微复杂一些,那我们能不能把它进行一些简化呢?答案是肯定的。我们可以通过关键字typedef将其进行简化,即我们可以让函数指针这个类型进行一些简化。请看:


typedef void(* pfun_t)(int);

该代码意思是把函数指针,即void(* pfun_t)(int);这个函数指针类型进行了简化,即我们把void(* pfun_t)(int)这个函数指针简化成了pfun_t那么我们就可以把void(* pfun_t)(int)替换成pfun_t。所以代码2void (*signal(int, void(*)(int)))(int);我们就可以写成这样:


pfun_t signal(int,pfun_t);

简化之后的代码应该是这样的:


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

这两句代码合起来表达的意思就是代码2的意思,即这两句代码合起来就是void (*signal(int, void(*)(int)))(int);的意思。

这个时候,我们写成pfun_t signal(int,pfun_t);这种形式就是写成void (*signal(int, void(*)(int)))(int);这种形式简单多了,这也使代码看起来更加简洁,可读性更好。

此时此刻,我们对代码2进行最终的解释:

1.signal是一个函数声明。2.signal函数的参数有两个,第一个是int类型,第二个是函数指针类型,该函数指针指向的函数的参数是int,返回类型是void。3.函数的返回类型也是一个函数指针:该指针指向的函数的参数是int,返回类型是void。

请大家务必对代码1,代码2进行反复的揣摩,这两段代码跟函数指针也是有着非常大的关联,理解之后对我们学习函数指针有着非常大的帮助。(偷偷告诉大家:代码1、2其实是笔试题哦)

在学习函数指针数组之前,我们还是要提函数指针的一个点:

9.png

我们可以观察到,无论加多少*结果都是一样的,但是我们平常在写代码的时候一般写成(pa)(2, 3)或者 (*pa)(2, 3)这两种形式,第一种形式我们可以通过指针的角度进行理解,第二种形式我们可以通过数组地址的角度进行理解。

在这里我们写几个*都可以,但是如果说这里的*真的有意义的话,我们对pa解引用一次得到一个数字,再解引用一次把上一次解引用产生的结果当成一个地址进行解引用,那这样的话反复解引用肯定会出问题,但是呢根据上述代码我们发现代码运行之后并没有产生什么问题(即产生的结果都一样),这说明了*放在这里其实就是一个摆设,并没有什么实际的价值,既然是摆设的话我们就可以删掉,即(pa(2,3));然而我们加上这个*即*p(2,3)当然也没有什么问题,这可能会让你更加理解一些,但这仅仅只是理解层面的意思,这个地方其实并没有进行解引用操作,加上*只是让我们从语法上更理解一些。

到这里,函数指针就先暂时告一段落,接下来我们正式进入函数指针数组。


6.函数指针数组

我们之前学过指针数组,如:int* arr[5];就是一个指针数组,接下来我们需要一个数组,这个数组可以存放函数的地址,这个数组就是函数指针数组。

那函数指针的数组如何定义呢?下面举一个函数指针数组的例子,请看:


int (*parr[5])(int,int)这就是一个函数指针数组。

//parr先和[]结合,说明parr是一个数组,那么数组的内容是什么呢?数组的

是一个int(*)( )类型的数组指针。

上述就是定义一个函数指针类型的数组。


下面我们来进行一个小小的练习,请看:

现在有一个函数char* my_strcpy(char* dest,const char* src);

1.写一个函数指针pf,能够指向函数my_strcpy;

2.写一个函数指针数组pfArr,能够存放4个my_strcpy函数的地址。


下面来看练习1、2的答案:


1.char* (*pf)(char*,const char*); 首先,*pf中的*说明pf是一个指针,(*pf)(char*,const char*)说明指针指向的是一个函数,函数的两个参数类型是char*和const char*,函数的返回类型是char*。

2.char* (*pfArr[4])(char*,const char*);。


那函数指针数组应该如何来用呢?下面给大家举一个函数指针数组的一个小用途:转移表

例子:(计算器)


#define _CRT_SECURE_NO_WARNINGS 1
//函数指针数组实现转移表(计算器)
#include<stdio.h>
//菜单
void menu()
{
  printf("**********************\n");
  printf("*****1,add  2.sub*****\n");
  printf("*****3.mul  4.div*****\n");
  printf("*****   0.exit   *****\n");
  printf("**********************\n");
}
//算法函数实现
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 input = 0;
  int x = 0;
  int y = 0;
  do
  {
  menu();
  printf("请选择:>");
  scanf("%d", &input);
  switch (input)
  {
  case 1:
    printf("请输入两个操作数:>");
    scanf("%d %d", &x, &y);
    printf("%d\n", Add(x, y));
    break;
  case 2:
    printf("请输入两个操作数:>");
    scanf("%d %d", &x, &y);
    printf("%d\n", Sub(x, y));
    break;
  case 3:
    printf("请输入两个操作数:>");
    scanf("%d %d", &x, &y);
    printf("%d\n", Mul(x, y));
    break;
  case 4:
    printf("请输入两个操作数:>");
    scanf("%d %d", &x, &y);
    printf("%d\n", Div(x, y));
    break;
  case 0:
    printf("退出\n");
    break;
  default:
    printf("选择错误\n");
    break;
  }
  } while (input);
  return 0;
}


上面的代码我们并没有用函数指针数组来实现,我们发现像printf("请输入两个操作数:>"); scanf("%d %d", &x, &y);这样的代码重复出现了4次,显得有些繁琐;现在我们假设还要实现一些功能,比如我们想计算一下x按位与y、x按位或y、x按位异或y、x右移几位算一下、x右移b位算一下、x左移b位算一下,这个时候我们的函数实现功能就要写各种各样功能的代码,一旦我们写好多好多代码的时候,那case语句就会越来越长,就会变的越来越繁杂,现在,我们用函数指针数组来改造优化一下代码。


#include<stdio.h>
//菜单
void menu()
{
  printf("**********************\n");
  printf("*****1,add  2.sub*****\n");
  printf("*****3.mul  4.div*****\n");
  printf("*****   0.exit   *****\n");
  printf("**********************\n");
}
//函数实现
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 input = 0;
  int x = 0;
  int y = 0;
  //pfArr是一个函数指针数组---转移表
  int(*(pfArr[5]))(int, int) = { 0,Add,Sub,Mul,Div };
  do
  {
  menu();
  printf("请选择:>");
  scanf("%d", &input);
  if (input >= 1 && input <= 4)
  {
    printf("请输入两个操作数:>");
    scanf("%d %d", &x, &y);
    int ret = pfArr[input](x, y);
    printf("ret=%d\n", ret);
  }
  else if (input == 0)
  {
    printf("退出\n");
  }
  else
  {
    printf("选择错误\n");
  }
  } while (input);
  return 0;
}


我们可以看到,虽然我们是用函数指针数组的形式来实现这个计算器,但整体的逻辑是没有变的,依然是我们之前的逻辑。如果我们用case语句的话,随着功能的增加,case语句会变的越来越长。而我们如果用函数指针数组的形式,就会变得简单一些了,我们只是把这些函数实现的地址很好的放到数组pfArr中去,这个函数指针数组通过下标找到元素,然后在再通过元素所指向的那个函数,所以我们通常把这样的函数指针数组叫做转移表。


倘若在未来我们还要增加其他的运算,比如说异或(Xor),我们直接在函数实现部分添加一个实现异或的函数,并且改一些相关的参数就可以了。比如说,我现在想增加几个计算异或的功能,下面是计算异或功能添加之后的代码:

#include<stdio.h>
//菜单
void menu()
{
  printf("**********************\n");
  printf("*****1,add  2.sub*****\n");
  printf("*****3.mul  4.div*****\n");
  printf("*****5.Xor  0.exit****\n");
  printf("**********************\n");
}
//函数实现
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 Xor(int x, int y)
{
  return x ^ y;
}
int main()
{
  int input = 0;
  int x = 0;
  int y = 0;
  int(*(pfArr[6]))(int, int) = { 0,Add,Sub,Mul,Div,Xor };
  do
  {
    menu();
    printf("请选择:>");
    scanf("%d", &input);
    if (input >= 1 && input <= 5)
    {
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      int ret = pfArr[input](x, y);
      printf("ret=%d\n", ret);
    }
    else if (input == 0)
    {
      printf("退出\n");
    }
    else
    {
      printf("选择错误\n");
    }
  } while (input);
  return 0;
}


注意,我们不只是简简单单的只增加异或函数的实现,我们还需要对一些数据(比如函数指针数组的大小等等)做出修改。

现在我们再来回到最初实现计算器用的case语句代码,即:

#define _CRT_SECURE_NO_WARNINGS 1
//函数指针数组实现转移表(计算器)
#include<stdio.h>
//菜单
void menu()
{
  printf("**********************\n");
  printf("*****1,add  2.sub*****\n");
  printf("*****3.mul  4.div*****\n");
  printf("*****   0.exit   *****\n");
  printf("**********************\n");
}
//算法函数实现
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 input = 0;
  int x = 0;
  int y = 0;
  do
  {
    menu();
    printf("请选择:>");
    scanf("%d", &input);
    switch (input)
    {
    case 1:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      printf("%d\n", Add(x, y));
      break;
    case 2:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      printf("%d\n", Sub(x, y));
      break;
    case 3:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      printf("%d\n", Mul(x, y));
      break;
    case 4:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      printf("%d\n", Div(x, y));
      break;
    case 0:
      printf("退出\n");
      break;
    default:
      printf("选择错误\n");
      break;
    }
  } while (input);
  return 0;
}


我们可以看到:

printf(“请输入两个操作数:>”);
scanf(“%d %d”, &x, &y);
printf(“%d\n”, Add(x, y));

这段代码块重复出现了多次,唯一的区别就是调用的函数不同(Add,Sub,Mul,Div,Xor),我们如何让它只出现一份呢?那我们只需要把上述重复的代码块分装成一个函数,那我们应该怎么做呢?


请注意请注意请注意!!!

在此之前,我们先讲解一个知识点,即回调函数的概念


回调函数

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


还是以刚刚计算器(利用case语句实现的)为例,即

#define _CRT_SECURE_NO_WARNINGS 1
//函数指针数组实现转移表(计算器)
#include<stdio.h>
//菜单
void menu()
{
  printf("**********************\n");
  printf("*****1,add  2.sub*****\n");
  printf("*****3.mul  4.div*****\n");
  printf("*****5.exit 0.exit****\n");
  printf("**********************\n");
}
//算法函数实现
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 Xor(int x,int y)
{
  return x ^ y;
}
int main()
{
  int input = 0;
  int x = 0;
  int y = 0;
  do
  {
    menu();
    printf("请选择:>");
    scanf("%d", &input);
    switch (input)
    {
    case 1:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      printf("%d\n", Add(x, y));
      break;
    case 2:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      printf("%d\n", Sub(x, y));
      break;
    case 3:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      printf("%d\n", Mul(x, y));
      break;
    case 4:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      printf("%d\n", Div(x, y));
      break;
    case 5:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      printf("%d\n", Xor(x, y));
      break;
    case 0:
      printf("退出\n");
      break;
    default:
      printf("选择错误\n");
      break;
    }
  } while (input);
  return 0;
}


我们之前说过:


printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", Add(x, y));

printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", Sub(x, y));

printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", Mul(x, y));


printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", Div(x, y));

printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", Xor(x, y));


上面这5段代码唯一的区别就是调用的函数不同,既然调用的函数不同那我们上述代码分装成这样:


Calc(Add);//当我们的函数内部要调用Add函数时,我们只需要把Add传过来就行
Calc(Sub);//当我们的函数内部要调用Sub函数时,我们只需要把Sub传过来就行
Calc(Mul);//当我们的函数内部要调用Mul函数时,我们只需要把Mul传过来就行
Calc(Div);//当我们的函数内部要调用Div函数时,我们只需要把Div传过来就行
Calc(Xor);//当我们的函数内部要调用Xor函数时,我们只需要把Xor传过来就行

那我们利用分装的函数Calc之后写出来的代码是什么样的呢?请看:

#include<stdio.h>
//菜单
void menu()
{
  printf("**********************\n");
  printf("*****1,add  2.sub*****\n");
  printf("*****3.mul  4.div*****\n");
  printf("*****5.Xor  0.exit****\n");
  printf("**********************\n");
}
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 Xor(int x, int y)
{
  return x ^ y;
}
void Calc(int (*pf)(int, int))
{
  int x = 0;
  int y = 0;
  printf("请输入两个操作数:>");
  scanf("%d %d", &x, &y);
  printf("%d\n", pf(x, y));
}
int main()
{
  int input = 0;
  do
  {
    menu();
    printf("请选择:>");
    scanf("%d", &input);
    switch (input)
    {
    case 1:
      Calc(Add);
      break;
    case 2:
      Calc(Sub);
      break;
    case 3:
      Calc(Mul);
      break;
    case 4:
      Calc(Div);
      break;
    case 5:
      Calc(Xor);
      break;
    case 0:
      printf("退出\n");
      break;
    default:
      printf("选择错误\n");
      break;
    }
  } while (input);
  return 0;
}


我们可以看到Calc这个函数接收了一个函数的地址,int (*pf)(int, int)这个函数指针通过接收到的地址去调用这个地址所指向的那个函数,这个时候我们就是实现了不同计算器算法的功能。此时我们发现,Calc这个函数好像具有了多种功能(加减乘除异或等等等等都可以实现)。而最基本的一点是,不管要实现怎样的运算功能,我们要把我们要运算的那个函数写好之后传给Calc。

我们把这叫做什么呢?我们把一个函数的地址传递给void Calc(int (*pf)(int, int))中的指针,然后再在函数Calc内部通过这个指针去调用那个函数的时候,那个被调用的函数就被称为回调函数。

现在,我们通过回调函数解决了刚刚计算器中代码冗余(即编程时不必要的代码段)的问题。


接下来再来举一个回调函数的例子:

10.png



在上述代码中,我们其实是在test函数内部去调用p指向的那个print函数,并没有去主动调用print函数,我们是把print函数的地址传递给test函数,然后再test函数内部的某种场景底下去调用print这个函数,当我们用p去调用print这个函数时,此时这个print函数就被称为回调函数。这种机制也被称之为回调函数机制。


然而上述的案例并不是很贴切回调函数真正的使用场景,待会我们举一个回调函数使用的一个真正场景。


回调函数的一个使用场景

再进行回调函数的使用场景之前,我们先来回顾一下冒泡排序:


#include<stdio.h>
void BubbleSort(int arr[], int sz)
{
  //...具体就不展开写了
}
int main()
{
  //冒泡排序
  //冒泡排序只能排序整型数组
  int arr[] = { 1,3,5,7,9,2,4,6,8,0 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  BubbleSort(arr, sz);
  return 0;
}


我们也会发现冒泡排序只能排序整型数组,倘若现在有一组浮点型我们用冒泡排序是不能排的,倘若现在有一组学生数据、学生成绩、按照年龄来排序我们也是不能用冒泡排序来对其进行排序的,所以说冒泡排序是非常局限的,其只能排序指定的类型。


我们先通过一段代码回顾之前学过的知识:

#include<stdio.h>
int Add(int x, int y)
{
  return 0;
}
int main()
{
  //指针数组
  int* arr[10];
  //数组指针
  int* (*pa)[10] = &arr;
  //函数指针
  int (*pAdd)(int, int) = Add;//Add也可以换成&Add
  //int sum = (*pAdd)(2, 3);//通过函数指针来调用Add这个函数的方式1
  int sum = (pAdd)(2, 3);//通过函数指针来调用Add这个函数的方式2
  printf("%d\n", sum);
  //函数指针的数组
  int (*pArr[5])(int, int);
  //指向函数指针数组的指针
  int (*(*ppArr)[5])(int, int);
  return 0;
}


下面我们完整的把冒泡排序写一下:


#include<stdio.h>
void bubble_sort(int arr[], int sz)//整型数组接收
{
  int i = 0;
  for (i = 0; i < sz - 1; i++)
  {
  int j = 0;
  for (int j = 0; j < sz - 1 - i; j++)
  {
    if (arr[j] > arr[j + 1])
    {
    int tmp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = tmp;
    }
  }
  }
}
int main()
{
  int arr[10] = { 9,7,8,6,5,4,3,2,1,0 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  bubble_sort(arr, sz);
  //打印结果
  int i = 0;
  for (i = 0; i < sz; i++)
  {
  printf("%d ", arr[i]);
  }
  return 0;
}


我们刚刚提到过,上述的冒泡排序只能排序整型,那未来如果我们想排序结构体类型、浮点型等等,很显然上述的冒泡排序就局限性就体现出来了,即其他类型的数据无法进行排序,那我们就需要一个更好的一个实现方法来实现不同类型的排序。接下来给大家正式的介绍qsort函数。


qsort函数是C语言的库函数提供的一个函数:qsort函数,可以排序任意类型的数据。其所用的算法思想是快速排序。

11.png

qsort函数原型:


下面是参数类型描述:


  • base—Start of target array—目标数组的起始位置
  • num—Array size in elements—数组元素的个数
  • width—Elements size in bytes—一个元素占几个字节
  • compare—Comparison funtcion—比较函数
  • elem1—要比较的两个元素中第一个元素的地址
  • elem2—要比较的两个元素中第二个元素的地址


我们把它进行一个小小的简化(不影响理解):

12.png

即:

13.png

这里有一个结构体:

14.png



我们先简单说一下这个比较函数cmp,倘若我们想利用冒泡排序来排各种各样的数据,根据冒泡排序的一个思想,因为我们想要排的是各种各样不同的数据而已,但是其算法思想没有变,还是首先确定趟数是多少,然后确定一趟中要进行多少对比较,而不一样的是我们比较方法:比如说我们排序整型数组,我们可以直接利用><或者=来进行比较两个元素就可以了;但是我们如果用冒泡排序来排序一个结构体数组,两个元素一个"张三",一个"李四",我们当然不能用><=来进行两个元素的比较。所以如果我们想把冒泡排序写成一个通用的函数,对谁又可以进行冒泡排序,不变的还是趟数和一趟中要比较多少对,而变的是是什么呢?变是两个元素的比较方法,即对于两个整型元素的比较我们可以直接用><甚至=来进行比较,而对于结构体的比较我们还需要另外找到一个比较的方法,我们是根据名字(比如张三李四)来比较呢?还是根据年龄来比较呢?,所以我们还需要写出不同的比较方法。


倘若我们现在排序一个数组int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };我们可以直接利用><来进行比较(这里使用qsort函数),现在我们需要写一个函数传给cmp这个指针,int( * cmp) (const void *e1, const void *e2),那这里的void*是个什么东西呢?***我们先同通过几段代码来进行对void*的讲解。***即:

15.png

16.png

17.png



现在,我们回过头来进来进行刚才的代码(qsort)的讲解。我们再来看int cmp_int(const void* e1, const void* e2)//比较两个整型值的函数,由于我们想要比较的是两个整型的元素,而这里是void*类型,所以这里需要一个强制类型转换。即:


int cmp_int(const void* e1, const void* e2)//比较两个整型值的函数
{
  //比较两个整型
  return *(int*)e1 - *(int*)e2;//强制类型转换
}


此时,我们就完成了qsort函数的使用,最终的代码如下:

#include<stdio.h>
#include<stdlib.h>
int cmp_int(const void* e1, const void* e2)//比较两个整型值的函数
{
  //比较两个整型
  return *(int*)e1 - *(int*)e2;
}
int main()
{
  int arr[10] = { 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);
  for (int i = 0; i < sz; i++)
  {
  printf("%d ", arr[i]);
  }
  return 0;
}

18.png

为了让代码看起来清晰一些,我们重新分装一个函数,如下:

19.png


现在,如果我们想排序浮点数类型的数组呢?代码如下:

20.png

我们会看到这里有一个警告⚠,这个警告是因为*(float*)e1 - *(float*)e2是一个浮点数而这个函数int cmp_float(const void* e1, const void* e2)中要求是一个返回int型。故我们对函数cmp_float可以做出如下修改:

#include<stdio.h>
#include<stdlib.h>
int cmp_float(const void* e1, const void* e2)
{
  if (*(float*)e1 == *(float*)e2)
    return 0;
  else if (*(float*)e1 > *(float*)e2)
    return 1;
  else
    return -1;
}
void test2()
{
  float f[5] = { 9.0,8.0,7.0,6.0,5.0 };
  int sz = sizeof(f) / sizeof(f[0]);
  qsort(f, sz, sizeof(f[0]), cmp_float);
  for (int j = 0; j < sz; j++)
  {
    printf("%f ", f[j]);
  }
}
int main()
{
  test2();
  return 0;
}


这样就不会报错了。

现在,我们来比较之前的结构体,我们先通过结构体中的年龄来进行比较,请看:

#include<stdio.h>
#include<stdlib.h>
struct Stu
{
  char name[20];
  int age;
};
int cmp_stu_by_age(const void* e1, const void* e2)
{
  //比较名字就是比较字符串
  //字符串比较不能直接用><=来比较,应该用strcmp函数
  return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
void test3()
{
  struct Stu s[3] = { {"zhangsan",40},{"lisi",30},{"wangwu",20} };
  int sz = sizeof(s) / sizeof(s[0]);
  qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
int main()
{
  test3();
  return 0;
}


21.png

我们再来通过结构体中的名字来进行比较,请看:


#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{
  char name[20];
  int age;
};
int cmp_stu_by_name(const void* e1, const void* e2)
{
  //比较名字就是比较字符串
  //字符串比较不能直接用><=来比较,应该用strcmp函数
  return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test4()
{
  struct Stu s[3] = { {"zhangsan",40},{"lisi",30},{"wangwu",20} };
  int sz = sizeof(s) / sizeof(s[0]);
  qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{
  test4();
  return 0;
}


22.png

冒泡排序各类数据

前面就是qsort函数的一个用法:我们再来简单的回顾一下它的四个参数:


  • 第一个参数:待排序地址的首元素地址
  • 第二个参数:待排序数组的元素个数
  • 第三个参数:待排序数组的每个元素的大小—单位为字节
  • 第四个参数:是函数指针,比较两个元素所用函数的地址-这个函数需要使用者自己实现,函数指针的两个参数是:带比较的两个元素的地址

我们前面借助qsort函数已经可以实现各种数据的一个排序,但是我们的冒泡排序依然是只能排序整型的数据,那我们怎么改造这个冒泡排序来实现各种类型数据的排序呢?接下来我们就来解决这个问题。


#include<stdio.h>
struct Stu
{
  char name[20];
  int age;
};
//比较函数cmp
int cmp_int(const void* e1, const void* e2)//比较两个整型值的函数
{
  //比较两个整型
  return *(int*)e1 - *(int*)e2;
}
int cmp_stu_by_age(const void* e1, const void* e2)
{
  return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
//实现bubble_sort函数的程序员,他是不知道未来排序的数据类型的
//那程序员也不知道待比较的两个元素的类型
void 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 bubble_sort(void* base, int sz, int width,int (*cmp)(void*,void*))
{
  int i = 0;
  //趟数
  for (i = 0; i < sz-1; i++)
  {
    //每一趟的对数
    int j = 0;
    for (j = 0; j < sz - 1 - i; j++)
    {
      //两个元素的比较
      if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
      {
        //交换
        Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
      }
    }
  }
}
void test5()
{
  int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  //使用bubble_sort的程序员一定知道自己排序的是什么数据
  //就应该知道排序数组中的元素
  printf("%d\n", sz);
  bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
  for (int i = 0; i < sz; i++)
  {
    printf("%d ", arr[i]);
  }
}
void test6()
{
  struct Stu s[3] = { {"zhangsan",40},{"lisi",30},{"wangwu",20} };
  int sz = sizeof(s) / sizeof(s[0]);
  bubble_sort(s, sz, sizeof(s[0]),cmp_stu_by_age );
}
int main()
{
  test5();
  test6();
  return 0;
}


下面是调试的结果:

23.png

仔细观察的话不难发现其实上述代码就是仿照qsort函数的思想来的,这里只给出代码和调试结果,就不展开说明了。当然不要忘了,这里非常重要的一个点就是使用回调函数,即利用回调函数去模拟实现我们的冒泡排序,让这个冒泡排序变得更加通用来能够排序各种类型的数据。


7.指向函数指针数组的指针

指向函数指针数组的指针是一个指针,指针指向一个数组,数组的元素都是函数指针;


24.png


我们简单的对int (*(*ppfArr)[4])(int, int) 这个指向函数指针数组的指针进行一个分析:*ppfArr说明ppfArr是一个指针,(*ppfArr)[4]说明指针ppfArr指向的是一个数组,然后我们把int (*(*ppfArr)[4])(int, int)中的(*ppfArr)[4]去掉之后剩下的int (*)(int, int) 就是我们数组的元素类型,可以看出,数组元素类型为函数指针。

现在我们重新描述一下ppfArr这个指向函数指针数组的指针:


  1. ppfArr是一个数组指针,指针指向的数组中有4个元素。
  2. 指向的数组的每个元素的类型是函数指针即int (*)(int, int)

下面请看这样一段代码(层层递进,望帮助大家理解):


int(*pf)(int, int);//函数指针
int(pfArr[4])(int, int);//函数指针数组
int((*pfArr)[4])(int, int);//指向函数指针数组的指针

最后,本文直到知道这里就结束了,文章确实有点长,但基本上讲解了指针进阶部分的全部内容,望本文对大家的对指针的理解、学习有所帮助。再次感谢!!!

目录
相关文章
|
3月前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
69 0
|
1月前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
107 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
1月前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
93 9
|
1月前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
53 7
|
2月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
223 13
|
2月前
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
79 11
|
2月前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
2月前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
180 3
|
2月前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
2月前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
57 1

热门文章

最新文章