【C指针进阶】(C精髓)——对指针的更进一步深入剖析(图文近2w详解)

简介: 【C指针进阶】(C精髓)——对指针的更进一步深入剖析(图文近2w详解)

本文承接了上文:初阶指针——全面了解指针,在此基础上对指针进行更深一步的剖析,相信我,也请相信你自己!



指针全解


前言

1、初阶指针大致回顾

2、 字符指针

3、指针数组

4、 数组指针

5、数组传参和指针传参

6、函数指针

7、 函数指针数组

8、指向函数指针数组的指针

9、回调函数

qsort 函数实现

qsort函数模拟(冒泡思想)

前言


我们都知道,指针是C语言中必不可少的一部分,是C语言的精髓所在,一个学习C语言的人如果不对指针有着深刻的理解,那还不算得上是真正入门,本篇文章整理了对于指针更深一步的了解所学到的知识,以及一些个人的见解,期待能帮到您对此有更深刻的认识。话不多说,直接开搞!


1、初阶指针大致回顾


在之前的指针初阶文章里,主要讲了以下内容:


指针就是个变量,用来存放地址,地址唯一标识一块内存空间。

不管是什么类型的指针,它的大小是固定的4/8个字节(32位平台/64位平台)。

指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。

指针的运算——两个指针相减后的结果的绝对值,就是两个指针之间的元素个数。

避免野指针的出现,写出好的代码

二级指针是存放一级指针地址的指针变量

大概就是这些,接下来,正式进入指针进阶。


2、 字符指针


字符指针是用来存放字符的一种指针类型,我们平常都是这么使用:


#include<stdio.h>
int main()
{
  char ch = 's';
  char* p = &ch;
  //这里的p就是指针变量,类型为char*,存放字符's'的地址
  return 0;
}


或者这样使用:


#include<stdio.h>
int main()
{
  const char* p = "abcdef";
  //这里要注意一点,就是这里的p存放的实际上是字符串首字符'a'的地址,并不是整个字符串的地址
  return 0;
}


2.1 字符数组与字符指针

大家这里还要注意一点,就是字符数组与字符指针之间的区别,它们两个看上去很像,但是所表达的意思却完全不同,如下代码:


#include<stdio.h>
int main()
{
  char str[] = "abcdef";
  char* str1 = "abcdef";
  return 0;
}


在这里,str表示的是一个数组,既然是数组,那么这里面的元素是可以进行修改的,就比如把a改成z,只需要把z赋值给数组首元素就可以了,但是,下面的str1所指向的是一个字符串常量,而字符串常量是不可以进行修改的。如下:


#include<stdio.h>
int main()
{
  char str[] = "abcdef";
  char* str1 = "abcdef";
  str[0] = 'z';
  *str1 = 'z';
  return 0;
}


1.png


所以我们一般在用字符指针指向字符串常量时,前面会加一个const,从而增加代码的健壮性,避免我们后面万一修改字符串常量的操作,因为修改的话,编译器是不会报错的,只有通过调试才能发现错误,加上const后,如果进行修改的话,编译器会报错提醒。

另外,我们还可以通过字符串首字符的地址找到整个字符串:


#include<stdio.h>
int main()
{
  const char* str1 = "abcdef";
  printf("%s\n", str1);//abcdef
  printf("%c\n", *str1);//a
  return 0;
}


这里注意一点,str1存放的是首字符a的地址,所以再对其解引用的时候,打印出来的其实是首字符a。


2.3 练习题


#include <stdio.h>
int main()
{
    char str1[] = "hello";
    char str2[] = "hello";
    const char *str3 = "hello";
    const char *str4 = "hello";
    if(str1 ==str2)
 printf("str1 and str2 are same\n");
    else
 printf("str1 and str2 are not same\n");
    if(str3 ==str4)
 printf("str3 and str4 are same\n");
    else
 printf("str3 and str4 are not same\n");
    return 0;
}


对于这个题,我们要了解一点,就是。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是数组就不同了,就算数组的内容相同,但是str1和str2都会开辟属于自己的内存空间。如下所示:


2.png


所以,这里str3与str4是等价的,但str1与str2不等价,所以最终实际结果为:

str1 and str2 are not same

str3 and str4 are same


3.png


3、指针数组


在前面的文章里已经讲过了关于指针数组的知识,指针数组是数组,数组里的元素为指针类型,也就是说,指针数组是一个用来存放指针(地址)的数组。


int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组


举个例子:


我们可以通过指针数组来实现用一维数组模拟二维数组:


#include<stdio.h>
int main()
{
  int arr1[] = { 1,2,3,4 };
  int arr2[] = { 2,3,4,5 };
  int arr3[] = { 3,4,5,6 };
  //数组名表示首元素地址,这里把arr1、arr2、arr3的首元素存放在指针数组arr里
  int* arr[] = { arr1,arr2,arr3 };
  int i = 0;
  for (i = 0; i < 3; i++)
  {
  int j = 0;
  for (j = 0; j < 4; j++)
  {
    //先通过arr[i]找到每个数组的首元素,[j]表示找到的那个数组的下标为j的元素arr1[0]、arr1[1]......
    printf("%d ", arr[i][j]);
  }
  printf("\n");
  }
  return 0;
}


4.png


4、 数组指针


4.1定义

数组指针,看起来与指针数组很像,但是表达的意思却完全不同,数组指针,顾名思义它是个指针,我们知道整形指针int*,它指向的对象为整形,浮点型指针float*指向的对象是浮点型,那么,类比一下,我们就可以知道,数组指针指向的对象为整个数组。

我们下面来看以下两个代码:


int *p1[10];
int (*p2)[10];
//我们把变量名称去掉,然后剩下的就表示变量的类型,在这里,[]的优先级要高于*号,所以p1表示的是一个数组,数组具有十个元素,每个元素的类型为int*,而()的优先级要高于[],所以这里p2首先表示的是一个指针,然后指向了一个具有10个元素的数组,数组的每个元素为int


4.2&数组名、数组名

再次复习一下&数组名与数组名

数组名表示首元素地址,除去两个例外。

1、sizeof(数组名),在这里,数组名代表的就是整个数组,所求出来的就是整个数组的大小。

2、&数组名,数组名前面加上&,表示取出的是整个数组的地址。


#include<stdio.h>
int main()
{
  int arr[5] = { 1,2,3,4,5 };
  printf("%p\n",arr);
  printf("%p\n",&arr[0]);
  printf("%p\n",&arr);
  printf("\n");
  printf("%p\n", arr+1);
  printf("%p\n", &arr[0]+1);
  printf("%p\n", &arr+1);
  return 0;
}


这里我们来验证一下:


5.png


4.3数组指针的使用

4.3.1一维数组里的应用


#include<stdio.h>
//用数组指针来接收整个数组的地址,这里注意[]要与数组arr的[]相对应
void test(int(*p)[8], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
  printf("%d ", (*p)[i]);
  //p=&arr,*p==*&arr==arr
  //*和&放一起,就相当于抵消
  //所以(*p)[i]就相当于arr[i]
  //还可以写为*((*p)+i)
  //*((*p)+i) == *(arr+i) ==arr[i],而arr表示首元素地址,+i后就可以指向数组的下标为i的元素,再解引用就会访问arr里下标为i的元素
  }
}
int main()
{
  int arr[8] = { 1,2,3,4,5,6,7,8 };
  //这里的arr单独放在sizeof内部,求出来的是整个数组的大小
  int sz = sizeof(arr) / sizeof(arr[0]);
  //这里&arr,取出的是整个数组的地址
  test(&arr, sz);
  return 0;
}


6.png


大家可以看一下这段代码,注释中解释了每一个原因,相信会对大家的理解有所帮助。

同样,数组指针的应用还经常应用在二维数组里。

4.3.2二维数组里的应用


#include<stdio.h>
//由于传过来的是二维数组里的第一行的地址,第一行有四个元素,所以可以把它看作传来的是一个有四个元素的一维数组的地址
//所以用数组指针来接收,指针指向数组的四个元素
//row,col分别接收行和列
void test(int(*p)[4], int row,int col)
{
  int i = 0; 
  for (i = 0; i < row; i++)
  {
  int j = 0;
  for (j = 0; j < col; j++)
  {
    printf("%d ", *(*(p + i) + j));
    //*(p+i)==p[i],也就是每一行的数组名,而每一行把它看成一维数组,数组名又表示数组首元素的地址,所以*(p+i)+j就指向了每一行的下标为j的元素的地址,再进行解引用,就访问到了每一行每一列的元素
  }
  printf("\n");
  }
}
int main()
{
  int arr[3][4] = { 1,2,3,4,5 };
  //这里arr表示数组首元素的地址,二维数组的首元素是整个第一行的地址,所以这里本质上传的参数其实是整个第一行,是一维数组
  test(arr,3,4);
  return 0;
}


7.png


在二维数组里,数组名表示数组首元素地址,二维数组的首元素地址就是整个第一行的地址。而每一行,又是一个一维数组。


8.png


4.4小练习

以下所代表的意思


int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];


int arr [5]就是一个数组,有五个元素,每个元素类型为int


int parr1[10],首先parr1是一个数组,数组10个元素,每个元素类型为int*,所以这是一个指针数组


int(*parr2)[10],parr2是一个指针,指针指向一个数组,所指向的数组有10个元素,每个元素类型为int


int (*parr3[10])[5],parr3是一个数组,有10个元素,每个元素类型为数组指针(地址),该指针指向一个数组,有五个元素,每个元素为int

如果还是不理解这个,就参考以下下面的图解,类比一下就明白了


9.png


以上就是关于数组指针的内容,下面继续


5、数组传参和指针传参


在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计?

5.1一维数组传参

对于一维数组传参,我们通常用以下几种方式来接收


#include <stdio.h>
void test(int arr[3])//第一种,我们用数组传参,那我们可以用数组的形式来接收参数,写成arr[]的形式,方括号里的数组可以省略,但必须有方括号
{}
void test(int* arr)//第二种,数组名表示首元素地址,我们可以用指针来接收地址
{}
void test2(int* arr[10])//指针数组传参,指针数组接收,没毛病,方括号的10可省略
{}
void test2(int** arr)//二级指针来接收一级指针的地址,没毛病
{}
int main()
{
  //arr是整形数组,具有10个元素,每个元素类型为int
  int arr[10] = { 0 };
  //arr2为指针数组,具有10个元素,每个元素类型为int*
  int* arr2[10] = { 0 };
  test(arr);//数组名表示首元素地址,这里传送的是arr[0](int类型)的地址
  test2(arr2);//数组名表示首元素地址,这里传的是arr2[0](int*类型)的地址
}


5.2二维数组传参

对于二维数组传参,我们通常用以下几种方式来接收


#include<stdio.h>
void test(int arr[3][5])//二维数组传参,二维数组接收,没毛病
{}
void test(int arr[][5])//二维数组来接收,可以省略行,但绝不能省略列
{}
void test(int(*arr)[5])//arr表示首元素地址,二维数组首元素是整个第一行的地址,本质上是一个一维数组,用数组指针来接收整个一维数组的地址,没毛病
{}
int main()
{
  int arr[3][5] = { 0 };
  test(arr);
}


5.3一级指针传参

对于一级指针的传参与接收,有以下几种:


#include<stdio.h>
void test1(int* p)
{}
int main()
{
  int n = 10;
  int arr[10];
  int* ps = &n;
  int* ps1 = arr;
  //数组名表示首元素地址
  //可以被形参为一级指针接收的实参部分
  test1(&n);//&n取出n的地址传过去,指针变量接收地址
  test1(arr);//arr数组名表示首元素地址,p接受,没毛病
  test1(ps);//ps类型为int*,存放n的地址,p来接收,没毛病
  test1(ps1);//ps1存放arr首元素地址,p来接收,ok的
  return 0;
}


5.4二级指针传参

对于二级指针的传参与接收,有以下几种:


#include<stdio.h>
void test1(int** p)
{}
int main()
{
  int n = 10;
  int* arr[10];
  int* ps = &n;
  int** ps1 = &ps;
  //数组名表示首元素地址
  //可以被形参为二级指针接收的实参部分
  test1(ps1);//二级指针传参,二级指针接收,没毛病
  test1(&ps);//ps类型为int*,&ps表示取出一级指针的地址,用二级指针接收,没得问题
  test1(arr);//arr表示指针数组首元素的地址,指针数组首元素类型为int*,取出int*的地址,用二级指针接收,没毛病
  return 0;
}


以上为数组以及指针传参的内容,望诸君加油。


6、函数指针


6.1函数指针定义

在上面的内容中,我们讲到了&数组名表示取出整个数组的地址,数组名表示数组首元素的地址,那么,我们不妨大胆猜测,&函数名,以及函数名是不是表示函数的地址呢?带着这个疑问,我们来看以下代码:


#include<stdio.h>
int add(int x,int y)
{
  return 0;
}
int main()
{
  printf("add  = %p\n", add);
  printf("&add = %p\n", &add);
  return 0;
}


10.png


在这里,我们可以发现,原来&函数名以及函数名所表示出来的地址是相同的,这里由于函数并不像数组那样,具有多个元素,每个元素都有各自所对应的地址,所以,&函数名以及函数名都表示函数的地址,两者之间没有任何区别。


因此,我们可以这样来定义函数指针:


#include<stdio.h>
int add(int x,int y)
{
  return 0;
}
int main()
{
  int(*p1)(int x, int y) = add;
  //这里p1先与*结合,说明p1是一个指针,再与后面的()结合,说明指针指向的是一个函数,int x,int y表示所指向的函数的参数为int类型
  //最外面的int 表示p1指向的函数的返回类型为int
  //这里可以把x、y省略,只要表示所指向的函数的参数类型即可
  int(*p2)(int, int)=add;
  //或者可以写为
  int(*p3)(int ,int) = &add;
  int(*p4)(int x, int y) = &add;
  return 0;
}


11.png


6.2函数指针使用


#include<stdio.h>
int test(int x, int y)
{
  return x + y;
}
int main()
{
  int sum = 0;
  int sum1 = 0;
  int sum2 = 0;
  //这是我们一般情况下的写法,调用函数test,传送参数2,3,返回2+3的值
  sum = test(2, 3);
  //我们还可以用函数指针来实现
  int (*p)(int, int) = test;
  //p为函数指针变量,指向函数test,指向函数的参数为int,int,返回类型为int
  sum1 = (*p)(2, 3);
  //p存放的是test的地址,解引用指向函数test
  //其实,由于p存放的是test的地址,而test本身就表示函数test的地址,所以p == test
  //所以test(2,3) == p(2,3)
  //所以,实际上真正的写法是下面这种,写成(*p)(2,3)只是方便理解罢了
  sum2 = p(2,3);
  printf("%d\n", sum); //5
  printf("%d\n", sum1);//5
  printf("%d\n", sum2);//5
  return 0;
}


6.3小练习


看如下代码


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


解释如下:


#include<stdio.h>
int main()
{
  //代码1
  (*(void (*)())0)();
  //这里可以先把(void(*)())先给去掉,此时就剩下(*0)();这表明调用0所指向的函数,但是0只是一个整形,怎么能调用函数呢,只有函数指针才可以调用函数
  //所以(void(*)()),最外面的()是强制类型转换,void(*)()是一个函数指针的格式
  //所以这个代码的意思就是把0,强制类型转换为一个函数指针类型,此时的0不再是一个整形,而是一个函数指针,然后调用0所指向的函数
  //代码2
  void (*signal(int, void(*)(int)))(int);
  //()的优先级  >  *,所以,signal是一个函数
  //(int,void(*)(int)) 表示该函数的参数,一个为int,一个为函数指针类型,该函数指针类型所指向的函数有一个参数,类型为int,返回类型为void
  //去掉以上这些,还剩下 void(*)(int),而signal是一个函数,这个就表示该函数的返回类型为void(*)(int),即函数指针类型
  //而该函数指针指向的函数有一个参数,为int类型,返回类型为void
}


7、 函数指针数组


数组去掉数组名[ ]剩下的就表示数组的元素类型,就比如int arr[5],去掉arr[5],剩下的int就表示数组的元素类型。


7.1定义

函数指针数组,顾名思义就是一个数组,用来存放函数指针的数组,即该数组可以存放多个参数相同、返回类型相同的函数地址

如下:



#include<stdio.h>
int add(int x, int y)
{
  return x + y;
}
int sub(int x, int y)
{
  return x - y;
}
int main()
{
  int (*p1)(int ,int) = add;
  int (*p2)(int ,int) = sub;
  //p1、p2为函数指针,指向的函数的返回类型都是int,参数都为int  int
  //add与sub的函数返回类型以及参数类型都相同
  //所以就可以这么来写
  int(*p3[2])(int, int) = { add,sub };
  int(*p4[2])(int, int) = { p1,p2 };
  //[]的优先级 > *,所以p3,p4都是一个数组,有两个元素,去掉数组名[]后剩下int(*)(int ,int),这表示的是函数指针类型(函数的地址)
  //所以,p3、p4数组的元素类型都是函数指针类型,或者可以说p3、p4数组的元素都表示函数的地址
  //而p3中,add、sub都是函数名,函数名表示函数地址,并且他们的返回类型以及函数参数都相同,所以都可以存放在数组p3中
  //p4中,p1、p2都是函数指针,并且所指向函数的返回类型及参数类型都相同,所以p1、p2都可以存放在数组p4中
  //上面的p3、p4都是函数指针数组,
  return 0;
}


7.2函数指针数组的使用

有了函数指针数组,我们就可以通过数组的元素,来访问所对应的函数指针即函数地址了。举个例子:


#include<stdio.h>
int add(int x, int y)
{
  return x + y;
}
int sub(int x, int y)
{
  return x - y;
}
int main()
{
  int sum1 = 0;
  int sub1 = 0;
  int(*p[2])(int, int) = {add,sub};
  //p是一个函数指针数组,通过该数组的元素,实现访问函数地址目的
  sum1 = p[0](1, 2);//p[0]即add,本质即sum=add(1,2),返回1+2,即3
  sub1 = p[1](1, 2);//p[1]即sub,本质即sub=sub(1,2),返回1-2,即-1
  printf("%d\n", sum1);//3
  printf("%d\n", sub1);//-1
  return 0;
}


了解这个后,我们可以通过函数指针数组实现一个具有加减乘除的简易计算器,如下:


#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;
  //函数指针数组,下标为1、2、3、4的元素分别对应+-*/
  int (*psArr[5])(int, int) = { 0,add,sub,mul,div };
  int ret = 0;
  do
  {
  menu();
  printf("请选择:->");
  //选择实现的功能
  scanf("%d", &input);
  if (input == 0)
  {
    printf("退出\n");
    break;
  }
  //控制范围在有效范围内
  if (input >= 1 && input <= 4)
  {
    printf("请输入操作数:->");
    scanf("%d %d", &x, &y);
    //通过psArr数组的元素,来调用对应的函数,把返回结果存放到ret中
    ret = psArr[input](x, y);
    //打印结果
    printf("%d\n", ret);
  }
  else
  {
    printf("输入错误\n");
  }
  } while (input);
  return 0;
}



8、指向函数指针数组的指针


8.1定义

上面我们讲完了函数指针数组,而我们一开始所了解的整型数组,字符数组等,它们都可以将自己的地址存放到指针变量中,然后通过这个指针就可以访问它们,而作为函数指针数组,它也有自己的地址,我们也可以通过一个指针变量来存放它的地址,然后再通过指针来灵活使用它们。


那么它的形式是怎样的呢?且看下面:


#include<stdio.h>
int main()
{
  //函数指针
  int(*p1)(int, int);//p1先与*结合,说明p1是个指针,然后再与后面的()结合,说明p1指向的是一个函数,函数参数类型为int,int,返回类型int
  //函数指针数组
  int(*p2[3])(int, int);//p2先与[]结合,说明p2是一个数组,去掉数组名[],剩下int(*)(int,int)为数组的元素类型,为函数指针类型,所以p2是一个函数指针数组
  //指向函数指针数组的指针(用来存放函数指针数组地址的指针变量)
  int(*(*p3)[3])(int, int);//p3先与*结合,说明p3是一个指针,然后再与[]结合,说明p3指向一个数组,该数组的元素类型为int(*)(int,int),即函数指针类型
  //所以p3是一个指向函数指针数组的指针
  return 0;
}


8.2指向函数指针数组的指针的使用

了解它的含义后,那么该如何使用呢?举个例子:



#include<stdio.h>
int add(int x, int y)
{
  return x + y;
}
int sub(int x, int y)
{
  return x - y;
}
int main()
{
  //p是一个函数指针数组,存放add、sub的地址
  int(*p[2])(int, int) = { add,sub };//也可以写成{&add,&sub}
  int(*(*p1)[2])(int, int) = &p;//p是数组名,&p表示取出整个数组的地址,即函数指针数组p的地址,存放在指针变量p1中
  int sum1 = 0;
  int sub1 = 0;
  sum1 = (*p1)[0](1, 2);//这里p1 == &p,所以*p1 == *&p,*与&相抵消,所以这段代码的本质即p[0](1,2),即add(1,2)
  sub1 = (*p1)[1](1, 2);//这里p1 == &p,所以*p1 == *&p,*与&相抵消,所以这段代码的本质即p[1](1,2),即sub(1,2)
  printf("%d\n", sum1);//3
  printf("%d\n", sub1);//-1
  return 0;
}


8.3练习题

声明一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,该函数的返回值是int,参数是int*


int(*(*p)[10])(int*)
//p先与*结合,说明p是一个指针,再与[10]结合,说明指向一个含有10个元素的数组。
//去掉数组名和[]后剩下数组的元素类型int(*)(int*),即函数指针类型,所指向的函数返回类型int,参数为int*类型


9、回调函数


9.1定义

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


简单来说,就是把函数a的地址作为函数b的参数,然后在函数b的内部调用函数a时,这时的函数a就称为回调函数。


9.2回调函数的使用*

这里我们再举之前计算器的例子,对那段代码进行修改,如下:


#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;
}
void calc(int(*p)(int, int))
{
  int x = 0;
  int y = 0;
  printf("请输入操作数:->");
  scanf("%d %d", &x, &y);
  //这里,被调用的add、sub、mul、div都是回调函数
  //通过函数指针调用的函数
  int ret = p(x, y);
  printf("%d\n", ret);
}
int main()
{
  int input = 0;
  do
  {
  menu();
  printf("请选择:->");
  scanf("%d", &input);
  switch (input)
  {
  case 1:
    //函数add的地址作为函数calc的参数
    calc(add);
    break;
  case 2:
    //函数sub的地址作为函数calc的参数
    calc(sub);
    break;
  case 3:
    //函数mul的地址作为函数calc的参数
    calc(mul);
    break;
  case 4:
    //函数div的地址作为函数calc的参数
    calc(div);
    break;
  case 0:
    printf("退出\n");
    break;
  default:
    printf("输入错误\n");
    break;
  }
  } while (input);
  return 0;
}


详细图解


12.png


同理,被通过函数指针p调用sub、mul、div函数也都是回调函数,原理同上。


回调函数是函数指针内容里最为重要的部分,通过回调函数可以为我们节省很多麻烦事,就比如,程序员写了一个复杂的函数a,后面假如再想实现这个代码的作用时,只需要通过函数指针来调用a即可了,就不需要再次写a。


了解什么是回调函数后,接下来我们来讲一下qsort函数,以及它的模拟实现。


qsort 函数实现

到目前为止,我们学过了一种排序方法,即冒泡排序,如下:


#include<stdio.h>
void bubble_sort(int* arr, int sz)
{
  //趟数
  int t = 0;
  for (t = 0; t < sz - 1; t++)
  {
  int i = 0;//每一趟的元素排序次数
  for (i = 0; i < sz - 1 - t; i++)
  {
    //降序排序
    if (arr[i] < arr[i + 1])
    {
    int tmp = arr[i];
    arr[i] = arr[i + 1];
    arr[i + 1] = tmp;
    }
  }
  }
}
void print(int arr[], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
  printf("%d ", arr[i]);
  }
}
int main()
{
  int arr[] = { 1,8,1,6,8,6,4,8,9,8,2,5,7,3 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  bubble_sort(arr, sz);//核心思想:两两相邻元素进行比较
  //打印排序后的元素
  print(arr,sz);//9 8 8 8 8 7 6 6 5 4 3 2 1 1
  return 0;
}


但是,冒泡排序有一个缺陷,就是冒泡排序只能排序整形,对于char类型,或者结构体类型是无法进行排序的,那么有没有一种函数是可以实现对任意类型元素的排序呢?答案是有的,就是qsort函数

13.png


在这里,我们发现,qsort函数有四个函数参数,这些函数参数都是什么意思呢?大家看以下讲解就明白了:


14.png


而对于用来比较两个元素大小的函数指针,又有以下定义:


15.png


这里我们知道,对于两个元素e1,e2,假如e1>e2,就返回一个大于0的数,如果e1<e2,就返回一个小于0的数,如果e1 == e2 ,就返回0


qsort是一个库函数,它的使用需要包含头文件#include<stdlib.h>

下面我们通过qsort函数再来排序上面的数:

1.qsort排序整形


#include<stdlib.h>
#include<stdio.h>
//compar的两个参数都为const void*,返回类型为int
int compar(const void* e1, const void* e2)
{
  return *(int*)e1 - *(int*)e2;
  //这里,假如e1>e2即(e1-e2)>0, 返回>0的数,如果 e1<e2即(e1-e2)<0 ,返回<0的数,如果e1 == e2即(e1-e2 == 0) ,返回0
  //这里我们直接返回(e1-e2)的值就可以了
  //由于e1,e2是void*类型,解引用后才能访问用来比较的两个元素,而我们知道void*类型的数不能直接解引用,所以要进行强制类型转换
  //强制类型转换成int*后,再解引用,就能进行运算了
}
int main()
{
  int arr[] = { 1,8,1,6,8,6,4,8,9,8,2,5,7,3 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  qsort(arr, sz, sizeof(arr[0]), compar);
  //arr表示首元素地址      sz是要排序的元素个数     sizeof(arr[0])是要一个排序的元素大小
  //compar是函数名,函数名表示函数地址,即用来比较两个元素大小的函数地址
  return 0;
}


16.png


这里我们发现,qsort实现了升序排序,如果要实现降序排序,只需要把e1与e2的位置调换一下就可以了。


17.png


2.qsort排序字符

与上面排序整形一个道理,例:


#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int compar(const void* e1, const void* e2)
{
  return *(char*)e1 - *(char*)e2;
}
int main()
{
  char arr[] = "jsdoqwidj";
  int len = strlen(arr);
  qsort(arr, len, sizeof(arr[0]), compar);
  return 0;
}


18.png


3.qsort排序结构体

原理本质上都是一样的,例:


#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//qsort排序结构体以及用冒泡思想实现qsort的功能
struct stu
{
  char name[10];
  int age;
};
//结构体成员排序
int cmp_struct_name(const void* e1, const void* e2)
{
  //strcmp比较字符串大小的库函数,后面会讲到
  //强制类型转换,再指向结构体成员name
  return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
//结构体成员排序
int cmp_struct_age(const void* e1, const void* e2)
{
  //强制类型转换,再指向结构体成员age
  return ((struct stu*)e1)->age - ((struct stu*)e2)->age;
}
int main()
{
  struct stu s[] = { {"zhangsan",20},{"lisi",50},{"wangwu",8} };
  int sz = sizeof(s) / sizeof(s[0]);
  //qsort函数实现结构体排序
  //按姓名排序
  qsort(s, sz, sizeof(s[0]), cmp_struct_name);
  //按年龄排序
  qsort(s, sz, sizeof(s[0]), cmp_struct_age);
  return 0;
}


19.png


在这里我们要知道strcmp函数:


20.png


strcmp也是一个库函数,它的使用要包含头文件#include<string.h>,返回类型为int,两个参数为const char*


21.png


对于两个字符串如果str1<str2即(str1-str2)<0,返回一个<0的数,如果(str1==str2),返回0,如果str1>str2,即(str1-str2)>0,返回>0的数。刚好与qsort的返回值相同

strcmp是专门用来比较字符串大小的库函数,在以上例子中就用到了此函数。


qsort函数模拟(冒泡思想)

清楚qsort函数的使用后,我们用我们学过的冒泡思想来模拟实现qsort的功能


#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//qsort排序结构体以及用冒泡思想实现qsort的功能
struct stu
{
  char name[10];
  int age;
};
//结构体成员排序(姓名)
int cmp_struct_name(const void* e1, const void* e2)
{
  //strcmp比较字符串大小
  //强制类型转换,再指向结构体成员
  return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
//结构体成员排序(年龄)
int cmp_struct_age(const void* e1, const void* e2)
{
  //强制类型转换,再指向结构体成员
  return ((struct stu*)e1)->age - ((struct stu*)e2)->age;
}
//交换
void Swap(char* buf1, char* buf2,int width)
{
  //如何实现对任意类型的元素实现交换,答案很简单
  //交换每一个字节从而实现任意类型数据的交换
  //就比如int类型,占用四个字节,把每一个字节都进行交换,就实现了整个元素的交换,而width其实是元素的大小,也就是该元素占几个字节,即交换的次数
  //举个例子,假如传过来的是int类型的4和5的地址,但是这里使用char*来接收的,也就是说只能接收4和5的第一个字节的地址,解引用只能访问到4和5的第一个字节的内容
  // 即进行交换的话也只能交换4和5的第一个字节的内容,但是4和5占用4个字节,不过有了width后,然后进行解引用,交换width次,就可以实现对4和5的四个字节的每一个字节所对应的内容进行交换,从而实现4和5的整体交换
  //换成short类型等也是一样
  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*, const void*))
{
  //趟数
  int t = 0;
  for (t = 0; t < sz - 1; t++)
  {
  //排序元素对数
  int j = 0;
  for (j = 0; j < sz - 1 - t; j++)
  {
    //上面还是保持冒泡排序的思想,只不过下面进行了改变
    // 
    //(char*)base+j*width:找到比较的元素,char*每次跳过一个字节,而j*width,就保证了不管是什么类型的元素,都能j+1后都能访问到后面想要访问的元素
    if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
    {
    //调用函数实现元素的交换(冒泡思想,两两相邻进行比较)
    Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
    }
  }
  }
}
int main()
{
  struct stu s[] = { {"zhangsan",20},{"lisi",50},{"wangwu",8} };
  int sz = sizeof(s) / sizeof(s[0]);
  //冒泡思想实现qsort函数功能,从而实现排序结构体类型
    //这里我们的冒泡排序模拟实现qsort,保证与qsort的参数一致
  //首元素地址、待排序元素个数、元素大小、用来比较元素的函数地址(按姓名排序)
  bubble_sort(s, sz, sizeof(s[0]), cmp_struct_name);
  //首元素地址、待排序元素个数、元素大小、用来比较元素的函数地址(按年龄排序)
  bubble_sort(s, sz, sizeof(s[0]), cmp_struct_age);
  return 0;
}


22.png23.png


在这里,最难的地方主要在于如何实现对任意类型的元素进行两两排序。这里再来画图解释一下为何要这么书写

(char*)base + j * width, (char*)base + (j + 1) * width


24.png


最后,指针是C语言的精髓所在,望诸君不负努力,加油!

end


相关文章
|
4月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
4月前
|
机器学习/深度学习 搜索推荐 算法
【再识C进阶2(下)】详细介绍指针的进阶——利用冒泡排序算法模拟实现qsort函数,以及一下习题和指针笔试题
【再识C进阶2(下)】详细介绍指针的进阶——利用冒泡排序算法模拟实现qsort函数,以及一下习题和指针笔试题
|
4月前
|
C语言
指针进阶(回调函数)(C语言)
指针进阶(回调函数)(C语言)
|
4月前
|
存储 C语言 C++
指针进阶(函数指针)(C语言)
指针进阶(函数指针)(C语言)
|
4月前
|
编译器 C语言
指针进阶(数组指针 )(C语言)
指针进阶(数组指针 )(C语言)
|
4月前
|
搜索推荐
指针进阶(2)
指针进阶(2)
42 4
|
4月前
指针进阶(3)
指针进阶(3)
34 1
|
4月前
|
C++
指针进阶(1)
指针进阶(1)
38 1
|
4月前
|
存储 安全 编译器
C++进阶之路:何为引用、内联函数、auto与指针空值nullptr关键字
C++进阶之路:何为引用、内联函数、auto与指针空值nullptr关键字
37 2
|
4月前
|
Java 程序员 Linux
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
41 0