【C语言学习】指针进阶【进阶详解篇14】

【声明】:本章主要是对C语言指针部分的讲解,因为觉得指针理解起来有些烧脑,可能在读者认为很简单的代码我依然作了大量注释,主要是为了防止以后忘记,方便更快速的复习回忆,毕竟本人智商确实有限,有时候甚至不在线,哈哈。
代码注释较多,可能会给读者造成些许阅读障碍,建议电脑阅读哈。
由于水平有限,如发现有错误之处,恳切希望读者们批评指正,以免自己或他人入坑!

文章目录

    • 指针进阶
      • 字符指针
      • 指针数组
      • 数组指针
      • 区分&数组名和数组名
      • 数组指针的使用
      • 数组参数、指针参数
        • 一维数组传参
        • 二维数组传参
      • 一级指针传参
      • 二级指针传参
      • 函数指针
      • 函数指针数组
      • 指向函数指针数组的指针
      • 回调函数
        • 使用自定义函数实现冒泡排序
        • qsort函数的学习
        • 使用qsort这个库函数实现冒泡排序
        • 使用qsort函数排序整型数据
        • 使用qsort函数排序结构体数据
        • 模仿qsort函数实现一个冒泡排序的通用算法
        • 模仿qsort函数实现排序结构体数据

指针进阶

在初级阶段我们已经接触过了《指针》,知道了指针的概念:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 指针的大小是固定的4 / 8个字节(32位平台 / 64位平台)。
  3. 指针是有类型,指针的类型决定了指针的 + -整数的步长,指针解引用操作的时候的权限。
  4. 指针的运算。

接下来让我们继续更深入的学习和探讨指针的相关内容吧!!

字符指针

结合代码学习

int main()
{
 char ch = 'a';
 char *p = &ch;//*表示p是一个指针,*p是一个指针变量;char表示指针p指向的变量ch的类型为char类型
 //指针不仅可以指向一个字符,还可以指向一个字符串。如下:
 char *pc = "hello china";//这并不是把整个字符串都存到指针变量pc中,本质上其实是把
 //"hello china"这个字符串的首字符的地址存储在了pc中,它跟数组是不相同的
 *pc = 'w';
 printf("%c\n", *pc);//打印首字符地址,结果为:h
 printf("%s\n", pc);//打印字符串,结果为hello china
}
#include 
int main()
{
 char str1[] = "hello world.";
 char str2[] = "hello world.";
 char *str3 = "hello world.";
 char *str4 = "hello world.";
 if (str1 == str2)
  printf("str1 and str2 are same\n");
 else
  printf("str1 and str2 are not same\n");
 //str1和str2是数组,他们来自不同的内存空间,指向首元素的地址
 if (str3 == str4)
  printf("str3 and str4 are same\n");
 else
  printf("str3 and str4 are not same\n");
 //str3和str4是指针变量,它指向的hello world是常量字符串,他们来自同一空间
 return 0;
}

打印结果如下:
【C语言学习】指针进阶【进阶详解篇14】_第1张图片
结果为什么是这样呢?
答:这里str3和str4指向的是一个同一个常量字符串。C / C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。

指针数组

指针数组是一个存放指针的数组。
指针数组本身是一个数组,数组中存放的是指针(地址)

#include
int main()
{
如:
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
 //int* arr[3];//这个arr数组有三个元素每个元素的类型是int*,
 //arr[3]是一个存放整型指针的数组,它能存放3个整型指针
 int a = 10;
 int b = 20;
 int c = 30;
 int* arr[3] = { &a, &b, &c };
 int i = 0;
 for (i = 0; i < 3; i++)
 {
  printf("%d ", *(arr[i]));//arr[i]是一个地址,通过解引用得到一个值
 }
 return 0;
}
#include
int main()
{
 int a[5] = { 1, 2, 3, 4, 5 };
 int b[] = { 2, 3, 4, 5, 6 };
 int c[] = { 3, 4, 5, 6, 7 };
 int * arr[3] = { a, b, c };
 int i = 0;
 for (i = 0; i < 3; i++)//a是第一行,b是第二行,c是第三行,i++从第一行到第三行
 {
  int j = 0;
  for (j = 0; j < 5; j++)
  {
   //arr[i];
   //遍历arr[3],拿到每个数组的起始位置的地址(首元素)
   printf("%d ", *(arr[i] + j));//首元素地址+j并解引用:打印一行的数据也可以这样写
   //printf("%d ", arr[i][j]);因为[j]就相当于+j并解引用
  }
  printf("\n");
 }
 return 0;
}

数组指针

数组指针的定义
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:

整型指针: int * pint; 能够指向整型数据的指针。
字符指针:char * pch 能够指向字符的指针
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
什么叫做数组指针呢?

int main()
{
 int a = 10;
 int*p = &a;//&a就是要拿到a的地址
 char ch = 'w';
 char*pc = &ch;
 int arr[10] = { 1, 2, 3, 4, 5 };
 //arr:是数组名,它取出是数组首元素的地址,即是arr[0]的地址
 //&arr:它取出的是数组的地址
 int(*parr)[10] = &arr;//此时parr是一个数组指针:指向一个数组的指针,parr里面存放的就arr这个数组的地址
 //数组指针的语法格式
 //parr:它是指针名字
 //int (*)[10]:表示这是一个指针,他指向一个数组,素组中有十个元素,每个元素的类型是int
 /*(*parr):表示parr是一个指针,想要表示parr是一个指针就需要让*和parr结合,如果不用括号括起来,表示的是parr和
  [10]结合,因此必须用括号括起来才对,*/
 //int (*parr)[10]:(*parr):表示parr是一个指针;[10]:表示parr指向的是一个数组,这个有十个元素;int:表示parr指向的数组的每个元素是int 类型
 //仿写:
 double* d[5];
 double* (*pd)[5] = &d;//pd这个数组指针指向的这个数组的每个元素的类型是double*的
 return 0;
}

区分&数组名和数组名

#include
int main()
{
 int arr[10] = { 0 };
 int*p1 = arr;//p1指向的是一个数组的首元素
 int(*p2)[10] = &arr;//p2指向的是一个数组
 printf("%p\n", p1);
 printf("%p\n", p1 + 1);//p1+1跳过的是一个整型4个字节,也就是一个元素
 printf("%p\n", p2);
 printf("%p\n", p2 + 1);//p2+1跳过的是一个数组,每个元素是整型,共10个元素,所以跳过的就是40个字节
 //指针类型决定了指针+1到底加几
 return 0;
}

打印结果如下:
【C语言学习】指针进阶【进阶详解篇14】_第2张图片

  • 总结:根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。
  • 实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。
  • 数组的地址 + 1,跳过整个数组的大小,所以 &arr + 1 相对于 &arr 的差值是40.

注意:数组名是数组首元素的地址,但是有两个例外:

1、sizeof(数组名):这里的数组名表示整个数组计算的是整个数组的大小,单位是字节
2、&数组名:这里的数组名表示整个数组,取出的是整个数组的地址
除这两个例外的数组名表示的不是数组首元素的地址外,其他数组名表示的都是数组首元素的地址

数组指针的使用

下面这个代码是用数组指针的方法拿到整个数组中的每个元素,但其实这种方法并太不可取

#include
int main()
{
 int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
 int(*pa)[10] = &arr;
 //*pa:就是对pa进行解引用拿到了整个数组,拿到整个数组访问数组元素的话,这个时候整个数组就是数组名,此时*pa就相当于arr,而数组名是数组首元素的地址
     //要想通过首元素的地址把其他元素打印出来该怎么打印呢?应该将*((*pa) + i)
  //数组首元素地址 + i之后再解引用,此时拿到的就是arr数组的每个元素
  //代码如下:
 int i = 0;//下标为i
 for (i = 0; i < 10; i++)
 {
  printf("%d ", *((*pa) + i));//arr首元素的地址+下标为i元素的地址,再整个括起来并解引用,此时就拿到了整个数组中的每个元素。
 }
 return 0;
}

对数组指针的使用通常都用在二维数组以上上面,下面这种代码就是,这种方法是可行的

#include
void print1(int arr[3][5], int r, int c)//用传统二维数组方法传参
{
 int i = 0;//行
 int j = 0;//列
 for (i = 0; i < r; i++)
 {
  for (j = 0; j < c; j++)
  {
   printf("%d ", arr[i][j]);
  }
  printf("\n");
 }
}
void print2(int(*p)[5], int r, int c)//用指向一维数组的数组指针传参
{
 int i = 0;
 int j = 0;
 for (i = 0; i < r; i++)
 {
  for (j = 0; j < c; j++)
  {
   printf("%d ", *(*(p + i) + j));//p+i指向的是某一行,再解引用,*(p + i)找到的是某一行的数组名
   //而某一行的数组名又相当于是数组首元素的地址,也就是某一行中的第一个元素的地址;
   //再+j:*(p + i) + j表示某一行里下标为j的元素的地址,再括号解引用*(*(p+i)+j)
   //表示找到了二维数组中第i行下标为j的元素。
  }
  printf("\n");
 }
}
int main()
{
 int arr[3][5] = { { 1, 2, 3, 4, 5 }, { 2, 3, 4, 5, 6 }, { 3, 4, 5, 6, 7 } };
 print1(arr,3,5);//用数组传参需要用数组接收
 print2(arr, 3, 5);//arr:数组名表示数组首元素的地址
 //二维数组的数组名,表示首元素的地址,而二维数组的首元素地址是它的第一行的地址
 //也就是一个一维数组的地址,因此传参的时候也可以使用数组指针int(*p)[5],来作为二维数组首元素的地址
 return 0;
}

学了指针数组和数组指针我们来一起回顾并看看下面代码的意思吧:

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

①.int arr[5];——arr是整型数组,它里面有5个元素,每个元素的类型为int
②.int *parr1[10];——parr1是整型指针数组,即存放整型指针的数组,它里面有10个元素,每个元素的类型为int
③.int(*parr2)[10];——parr2是数组指针,该指针能够指向一个数组,它里面有10个元素,每个元素的类型为int
④.int(*parr3[10])[5];——parr3是一个存放数组指针的数组,该数组能够存放10个数组指针,每个数组指针能够指向一个数组,数组5个元素,每个元素是int类型(挖掉parr3[10],就是这个数组里面的东西)。

数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?让我们一起来康康吧!!

一维数组传参

一维数组传参可以写成以下形式:

#include 
//形参可以写成数组的形式
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
//形参也可以写成指针的形式
void test(int *arr)//ok
{}
void test2(int *arr[20])//ok
{}
void test2(int **arr)//ok
{}
int main()
{
 int arr[10] = { 0 };
 int *arr2[20] = { 0 };
 test(arr);
 test2(arr2);
}

二维数组传参

二维数组传参可以写成以下形式:

//形参可以写成数组的形式
void test(int arr[3][5])//ok
{}
void test(int arr[][])//error
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素,这样才方便运算。

//形参也可以写成指针的形式
void test(int *arr)//error
{}
void test(int* arr[5])//error
{}
void test(int(*arr)[5])//ok
{}
void test(int **arr)//error
{}
int main()
{
 int arr[3][5] = { 0 };
 test(arr);
}

一级指针传参

可简单理解为:用一级指针变量传参,用一级指针变量来接收
先结合代码理解一下吧

#include
void print(int* ptr,int sz)//一级指针传参用一级指针变量来接收
{
 int i = 0;
 for (i = 0; i < sz; i++)
 {
  printf("%d ", *(ptr + i));
 }
}
int main()
{
 int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
 int* p = arr;
 int sz = sizeof(arr) / sizeof(arr[0]);//元素个数
 //p是一级指针
 print(p, sz);//想把什么打印出来,就把什么传过去
 return 0;
}

思考:当一个函数的参数部分设置为一级指针的时候,函数能接收什么参数?
(1)可以直接传地址
(2)也可以传指针变量
不多废话,看代码吧。

#include
void test(char* p)//test能接收什么样的参数呢?
{

}
int main()
{
 char ch= 'a';
 test(&ch);//(1)可以直接传地址
 char* p1 = &ch;
 test(p1);//(2)也可以传指针变量
 return 0;
}

二级指针传参

二级指针进行传参:也可简单理解为把一个二级指针传过去,再用一个二级指针接收。
代码举例如下:

void test(int ** p2)//用一个二级指针变量来接收
{
 **p2 = 20;//*p2:对p2解引用找到pa,**p2对*p2再解引用找到a;此时就改变了a的值
}
int main()
{
 int a = 10;
 int* pa = &a;//pa是一级指针
 int ** ppa = &pa;//ppa是二级指针
 test(ppa);//传一个二级指针变量
 printf("%d\n", a);
 return 0;
}

思考:当函数的参数为二级指针的时候,可以接收什么参数?
(1)可以传一个二级指针的变量
(2)可以传一级指针变量的地址
(3)也可以传存放一级指针的数组
代码举例如下:

void test(int ** p2)
{
 **p2 = 20;
}
int main()
{
 int a = 10;
 int* pa = &a;
 int **  ppa = &pa;
 //test(ppa);//(1)可以传一个二级指针的变量
 test(&pa);//(2)可以传一级指针变量的地址
 //int* arr[10] = { 0 };
 //test(arr);//(3)也可以传存放一级指针的数组
 printf("%d\n", a);
 return 0;
}

总结归类:

  • 指针我们学习了
    • 一级指针:
      int*p; ——整型指针:指向整型的指针
      char pc; ——字符指针:指向字符的指针
      void
      pv——无类型的指针
    • 二级指针:
      char**p;
      int **p;
    • 数组指针:指向数组的指针
      int(*p)[4];
  • 数组我们学习了
    • 一维数组
    • 二维数组
    • 指针数组-存放指针的数组

函数指针

首先我们先复习一下之前学的指针,由此通过对比或类比来进入函数指针的学习
代码如下:

int main()
{
 //一级指针里面的整型指针
 int a = 10;
 int *pa = &a;
 //一级指针里面的字符指针
 char ch = 'w';
 char* pc = &ch;
 //数组指针
 int arr[10] = { 0 };
 int(*parr)[10] = &arr;//&arr取出数组的地址
 //parr:是指向数组的指针,存放的是数组的地址
 return 0;
}

函数指针:指向函数的指针,它是存放函数地址的指针

#include
int Add(int x, int y)
{
 return x + y;
}
int main()
{
 int(*pf)(int, int) = &Add;
 //pf接收Add这个函数的地址,因此他是指针,需要加*
 //*pf要指向一个函数,因此后面需要加()
 //*pf()如果不在*pf加括号,pf会跟()结合,就成为了一个函数,而我们希望pf是一个指针,所以要给*pf加括号
 //(*pf)():pf是一个指针,他指向的是一个函数
 //pf这个指针指向的Add函数的参数类型是什么类型,()里面就写什么类型,返回类型是什么,就在前面写上什么类型,以此来交代清楚函数的参数类型和返回类型
 //由以上就得来了这行代码: int(*pf)(int, int) = &Add;
 //pf就是一个函数指针变量
 printf("%p\n", &Add);//&Add:拿到函数的地址,并打印
 //&函数名:取到的就是函数的地址
 printf("%p\n", Add);
 return 0;
}

在这里插入图片描述
注意:我们学过

  • 数组名!=&数组名,他们两个虽然打印结果相同,但意义不同
  • 但是,函数名==&函数名,他们两个打印结果相同,意义也相同,只是写法形式有区别!!因为函数又没有首元素地址。
    通过以上,我们已经了解了函数指针的规则和写法,下面做一个小练习体验一下吧!
void test(char* str)
{
}
int main()
{
 void (*pt)(char*) = &test;
 return 0;
}

接下来通过下面这个代码来进一步理解并练习函数指针

#include
int Add(int x, int y)
{
 return x + y;
}
int main()
{
 //pf是一个函数指针变量
 //int(*pf)(int, int) = &Add;//因为函数名和&函数名是一个道理,所以Add也可以不加&
 int(*pf)(int, int) = Add;//可以把函数名这个函数的地址放到pf这个指针变量里面去,说明Add==pf
 //通过函数指针进行调用可以写成下面两种方式
 int ret1=(*pf)(3, 5);//用函数指针去调用他所指向的那个函数
 //通过pf解引用先找到它指向的那个函数,找到那个函数之后通过传参去调用它,(将(3,5)这样的参数传进去),然后再用一个变量接收
 int ret2 = pf(3, 5);//因为Add=pf,所以不加*,也可以,同时表明了在函数指针这里,*其实就是一个摆设,并没有实际的意义,只是便于我们理解罢了
 int ret3 = Add(3, 5); //因为Add = pf,所以也可以这样写,但这种不是通过函数指针的方式调用的,而是通过函数名的方式调用的
 printf("ret1=%d\n", ret1);
 printf("ret2=%d\n", ret2);
 printf("ret3=%d\n", ret3);
 //截图打印结果
 //通过打印结果可以看出,这三调用方式是等价的
 return 0;
}

在这里插入图片描述
如果理解了,那么试这读分析一下下面两段代码吧

代码1

(*void(*)()0)();

解释:

void(* )() : 表示它是一个函数指针类型,(void(*)())0 : 表示把0强制类型转换为函数指针类型,此时0就当做(void(*)())这个函数指针类型的地址了,0地址中放的那个函数参数是无参的,返回类型是void
 *void(*)()0 : 想要调用这个函数需要在前面加一颗*进行解引用操作,然后括起来(*void(*)()0),此时就相当于找到0地址中的函数
(*void(*)()0)() : 找到0地址中的函数之后在后面加()进行调用,因为0地址中放的那个函数的参数是无参的,所以解引用之后调用的时候()里面也不传参
简单来说就是:调用0地址处的函数,该函数无参返回类型是void
1.void(*)():函数指针类型
2.(void(*)())0:0进行强制类型转换
3.*(void(*)())0:0地址进行解引用操作
4.(*(void(*)())0():调用0地址处的函数

代码2

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

1.signal和()先结合,说明signal是函数名
2.signal函数的第一个参数的类型是int,第二个参数的类型是函数指针,该函数指针,指向一个参数为int,返回类型为void的函数
3.signal这个函数的返回类型也是一个函数指针,即void(*)(int),该函数指针,指向一个参数为int,返回类型为void的函数

signal是一个函数的声明
可以换一种方式将这段代码简化一下
typedef:对类型进行重定义
如果想对void(*)(int)的函数指针类型重命名为pfun_t可以写成:typedef void(*pfun_t)(int)
 此时pfun_t就等于void(*)(int)
因此void(*signal(int, void(*)(int)))(int); 就可以写成pfun_t signal(int pfun_t);他们是完全等价的。这样写是不是简便了许多呢

函数指针数组

函数指针数组:存放函数指针的数组
先来个代码熟悉一下

int Add(int x, int y)
{
 return x + y;
}
int Sub(int x, int y)
{
 return x - y;
}
int main()
{
 int (*pf1)(int,int)=Add;//pf1是指向函数的指针
 int(*pf2)(int, int) = Sub;
 int(*pfArr[2])(int, int) = {Add,Sub};
 //pfArr[2]:pfArr首先和[]结合,说明pfArr是一个数组,数组有两个元素,而int(*)(int, int);是一个函数指针
 //此时就知道了pfArr是函数指针数组,= {Add,Sub}或者={pf1,pf2}就是给pfArr这个函数指针数组初始化,有几个元素就初始化几个元素
 //可简单理解为在函数指针变量后面加一个[],就是函数指针数组,这个[]里面写的有几个元素,就初始化几个元素
 return 0;
}

使用以前学习的方式写一个计算器,计算整型变量的加、减、乘、除
代码一点都比简便

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 input = 0;
 do{
  menu();
  int x = 0;
  int y = 0;
  int ret = 0;
  printf("请选择:>");
  scanf("%d", &input);
  switch (input)
  {
  case 1:
   printf("请输入两个操作数>:");
   ret = Add(x, y);
   printf("ret=%d\n", ret);
   break;
  case 2:
   printf("请输入两个操作数>:");
   scanf("%d %d", &x, &y);
   ret = Sub(x, y);
   printf("ret=%d\n", ret);
   break;
  case 3:
   printf("请输入两个操作数>:");
   scanf("%d %d", &x, &y);
   ret = Mul(x, y);
   printf("ret=%d\n", ret);
   break;
  case 4:
   printf("请输入两个操作数>:");
   scanf("%d %d", &x, &y);
   ret = Div(x, y);
   printf("ret=%d\n", ret);
   break;
  case 0:
   printf("退出程序\n");
   break;
  default:
   printf("选择错误,请重新选择.\n");
   break;
  }
 } while (input);
 return 0;
}

函数指针数组的用途和应用
使用现在学习的函数指针数组的方式写一个计算器,计算整型变量的加、减、乘、除

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 input = 0;
 do{
  menu();
  int NULL = 0;
  //pfArr就是函数指针数组
  int(*pfArr[5])(int, int) = { NULL, Add, Sub, Mul, Div };//将函数指针数组的第一个元素置为NULl,是为了让加减乘除的从下标刚好为1,2,3,4.
  int x = 0;
  int y = 0;
  int ret = 0;
  printf("请选择:>");
  scanf("%d", &input);
  if (input >= 1 && input <= 4)
  {
   printf("请输入两个操作数>:");
   scanf("%d %d", &x, &y);
   ret = (pfArr[input])(x, y);
   printf("ret=%d\n", ret);
  }
  else if (input == 0)
  {
   printf("退出程序\n");
   break;
  }
  else
  {
   printf("选择错误\n");
  }
   } while (input);
 return 0;
}

指向函数指针数组的指针

1.整形数组

int arr[5];
int(*p1)[5] = &arr;

2.整型指针数组

int* arr[5];
int* (*p2)[5] = &arr;
//p2是指向【整型指针数组】的指针

3.函数指针数组
&函数指针数组:取出函数指针数组的地址

int*p)(int, int);//函数指针
int(*p2[4])(int, int);//函数指针的数组;数组的每个元素是函数指针: int(*)(int, int);
int(*(*p3)[4])(int,int) = &p2;//&p2取出的是函数指针数组的地址
//p3就是一个指向【函数指针的数组】的指针,因为p2是一个函数指针的数组,把它的地址取出来放到p3里面去

补充:数组元素的类型和数组类型如何表示或判断?

int main()
{
 int arr[10];
 //arr[10]:组名和数组元素个数
// int:数组元素类型
//去掉数组名和数组元素个数剩下的就是数组元素的类型
 int[10];//数组类型
 //去掉数组名,剩下的就是数组的类型
 return 0;
}

回调函数

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

用回调函数的方法还写这个计算器!!

把Add函数的函数名作为参数传给Calc函数,通过指针去调用它所指向的那个函数的这种回调函数机制。

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 Calc(int(*pf)(int, int))
{
 int x = 0;
 int y = 0;
 printf("请输入两个操作数>:");
 scanf("%d %d", &x, &y);
 return pf(x, y);//通过指针去调用它所指向的那个函数 
}
int main()
{
 int input = 0;
 do{
  menu();
  int ret = 0;
  printf("请选择:>");
  scanf("%d", &input);
  switch (input)
  {
  case 1:
   ret=Calc(Add);
   printf("ret=%d\n", ret);
   break;
  case 2:
   ret = Calc(Sub);
   printf("ret=%d\n", ret);
   break;
  case 3:
   ret = Calc(Mul);
   printf("ret=%d\n", ret);
   break;
  case 4:
   ret = Calc(Div);
   printf("ret=%d\n", ret);
   break;
  case 0:
   printf("退出程序\n");
   break;
  default:
   printf("选择错误,请重新选择.\n");
   break;
  }
 } while (input);
 return 0;
}

使用自定义函数实现冒泡排序

#include
void bubble_sort(int arr[], int sz)
{
 int i = 0;
 //冒泡排序的趟数,所有数字排好需要走多少趟
 for (i = 0; i < sz - 1; i++)
 {
  //一趟冒泡排序需要比较的次数
  int j = 0;
  for (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;
   }
  }
 }
}
void print_arr(int arr[], int sz)
{
 int i = 0;
 for (i = 0; i < sz; i++)
 {
  printf("%d ", arr[i]);//将每个元素打印出来
 }
 printf("\n");
}
int main()
{
 //升序
 int arr[10] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };//数组
 int sz = sizeof(arr) / sizeof(arr[0]);//元素个数
 print_arr(arr, sz);//排序前打印
 bubble_sort(arr, sz);
 print_arr(arr, sz);//排序后打印
 return 0;
}

qsort函数的学习

【C语言学习】指针进阶【进阶详解篇14】_第3张图片

【C语言学习】指针进阶【进阶详解篇14】_第4张图片

  • bubble_arr和print_arr是我们自定义的函数,那么库里面有没有一种库函数也可以帮我们实现冒泡排序呢?
  • 有,qsort:quick sort-快速排序
  • qsort这个函数不挑类型什么类型,如整型、浮点型、结构体类型等都可以使用它进行排序

先认识一下qsort中各个参数的设置意义

void qsort(void* base, //base中存放的是待排序数据中第一个对象的地址,void*:表示无具体类型的
 size_t num, //待排序的数组元素的个数
 size_t size,//数组中一个元素的大小,单位是字节,因为不知道要排序的是什么类型,所以设置一个size来表示一个元素有多大
 int( * com)(const void * , const void*))//参数为函数指针,cmp这个函数指针所指向的这个函数是用来比较待排序数据中的2个元素的函数地址
 //比较完之后返回一个整型,返回大于0的数字,表示第一个元素大于第二个元素;返回0,表示第一个元素等于第二个元素;
 //返回小于0的数字,表示第一个元素小于第二个元素;
int( * com)(const void * e1, const void* e2))
{
}

使用qsort这个库函数实现冒泡排序

#include
#include
void print_arr(int arr[], int sz)
{
 int i = 0;
 for (i = 0; i < sz; i++)
 {
  printf("%d ", arr[i]);//将每个元素打印出来
 }
 printf("\n");
}
//将两个函数比较的函数交给函数的使用者来实现,所以在qsort参数中两个函数的比较处给了一个函数指针,这个指针能够指向一个函数,这个指针能够指向一个函数,那个函数可以帮我们把e1和e1两个元素指向的大小表示出来,并且返回一个合适的值,这就是qsort函数
int cmp_int(const void* e1, const void*e2)//
{
 //提供一个比较两个整型的方法
 return *(int*)e1 - *(int*)e2;
 //*(int*)e1:因为不知道e1是什么类型,所以先强制类型转换成int*,使其变成整型指针,然后再解引用,这时就从e1的位置向后访问了4个字节
 //*(int*)e2:e2同e1
 // 然后将e1和e2进行比较完之后返回一个整型,返回大于0的数字,表示第一个元素大于第二个元素;返回0,表示第一个元素等于第二个元素;返回小于0的数字,表示第一个元素小于第二个元素;
}
int main()
{
 int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
 int sz = sizeof(arr) / sizeof(arr[0]);
 print_arr(arr, sz);
 //排序
 qsort(arr,sz,sizeof(arr[0]),cmp_int);
 //打印
 print_arr(arr, sz);
 return 0;
}

使用qsort函数排序整型数据

#include
#include
//使用qsort函数排序整型数据
void print_arr(int arr[], int sz)
{
 int i = 0;
 for (i = 0; i < sz; i++)
 {
  printf("%d ", arr[i]);//将每个元素打印出来
 }
 printf("\n");
}
int cmp_int(const void* e1, const void*e2)//
{
 //提供一个比较两个整型的方法
 return *(int*)e1 - *(int*)e2;//升序排
 //如果想要降序排的话,只需要将e1和e2换一下位置即可
 return *(int*)e2 - *(int*)e1;//降序排
}
void test1()
{
 int arr[] = { 1,3,5,7,9,2,4,6,8,0 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  print_arr(arr, sz);
  //排序
  qsort(arr,sz,sizeof(arr[0]),cmp_int);
  //打印
  print_arr(arr, sz);
}

使用qsort函数排序结构体数据

struct Stu
{
 char name[20];
 int age;
};
//1.按照年龄来拍序
int sort_by_age(const void* e1, const void* e2)
{
 return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
 //(struct Stu*)e1:将e1强制类型转换为结构体指针类型
}
//2.按照姓名来排序
int sort_by_mame(const void* e1, const void* e2)
{
 //因为姓名是字符串类型,两个字符串进行比较,需要使用strcpm这个库函数,strcmp需要引用头文件
 //比较的不是字符串长度的大小而是对应位置的Ascii码值
 return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test2()
{
 struct Stu s[4] = { { "zhangsan", 30 }, { "lisi", 31 }, { "wanger", 20 }, {"mazi",25} };
 int sz = sizeof(s) / sizeof(s[0]);
 //1.按照年龄来拍序
 //qsort(s, sz, sizeof(s[0]), sort_by_age);
   //2.按照姓名来排序
 qsort(s, sz, sizeof(s[0]), sort_by_mame);
}
int main()
{
 test1();//cmp_int、 print_arr
 test2();//sort_by_age、sort_by_name、struct Stu
 return 0;
}

模仿qsort函数实现一个冒泡排序的通用算法

#include
//#include
void print_arr(int arr[], int sz)
{
 int i = 0;
 for (i = 0; i < sz; i++)
 {
  printf("%d ", arr[i]);//将每个元素打印出来
 }
 printf("\n");
}
void Swap(char*buf1, char*buf2, int width)
//buf1:指向第一个元素的起始位置,也就是9,从当前元素向后+四个字;
//buf2:指向第二个元素的起始位置,也就是8,从当前元素向后+四个字;
{
 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)(const void*e1,const void* e2))
{
 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)
    //将base强制类型转换为char*,
    //cmp((char*)base + j*width, (char*)base) + (j + 1)*width:比较前一个元素的地址和下一个元素的地址,
    //从当前元素向后+四个字节,并传给cmp这个函数
   {
    //交换
    Swap((char*)base + j*width, (char*)base + (j + 1)*width, width);
   //把两个元素比较出来之后,传给Swap函数进行交换,因为不知道要交换的元素是什么类型,所以只给起始地址还不够,需要再给出宽度,给了宽度之后就可以将这么多宽度字节的内容一对字节一对字节地进行交换
    //width :要交换的两个元素的宽度是什么
   }
  }
 }
}
int cmp_int(const void* e1, const void*e2)//
{
 //提供一个比较两个整型的方法
 return *(int*)e1 - *(int*)e2;//升序排
 //如果想要降序排的话,只需要将e1和e2换一下位置即可
 //return *(int*)e2 - *(int*)e1;//降序排
}
void test3()
{
 int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
 int sz = sizeof(arr) / sizeof(arr[0]);
 print_arr(arr, sz);
 //排序
 bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
 //打印
 print_arr(arr, sz);
}
int main()
{
 test3();
 return 0;
}

模仿qsort函数实现排序结构体数据

#include
#include
void Swap(char*buf1, char*buf2, int width)
//buf1:指向第一个元素的起始位置,也就是9,从当前元素向后+四个字;
//buf2:指向第二个元素的起始位置,也就是8,从当前元素向后+四个字;
{
 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)(const void*e1, const void* e2))
{
 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)
   // 将base强制类型转换为char*,因为char*的指针加几就跳过几个字节,+宽度就可以求到任意一个字节
    cmp((char*)base + j*width, (char*)base) + (j + 1)*width:比较前一个元素的地址和下一个元素的地址,
   // 从当前元素向后+四个字节,并传给cmp这个函数
   {
   // 交换
    Swap((char*)base + j*width, (char*)base + (j + 1)*width, width);
  //  把两个元素比较出来之后,传给Swap函数进行交换,因为不知道要交换的元素是什么类型,
  //所以只给起始地址还不够,需要再给出宽度,给了宽度之后就可以将这么多宽度字节的内容一对字节一对字节地进行交换
  //  width :要交换的两个元素的宽度是什么
   }
  }
 }
}
struct Stu
{
 char name[20];//一个数组里面放了20个char类型的元素,也就是20个字节
 int age;//年龄是整型,4个字节
// 所以这个结构体的大小是24个字节
};
//1.按照年龄来拍序
int sort_by_age(const void* e1, const void* e2)
{
 return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
 (struct Stu*)e1:将e1强制类型转换为结构体指针类型
}
//2.按照姓名来排序
int sort_by_mame(const void* e1, const void* e2)
{
// 因为姓名是字符串类型,两个字符串进行比较,需要使用strcpm这个库函数,strcmp需要引用头文件
 //比较的不是字符串长度的大小而是对应位置的Ascii码值
 return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test4()
{
 struct Stu s[4] = { { "zhangsan", 30 }, { "lisi", 31 }, { "wanger", 20 }, { "mazi", 25 } };
 int sz = sizeof(s) / sizeof(s[0]);
// 1.按照年龄来拍序
 bubble_sort(s, sz, sizeof(s[0]), sort_by_age);
// 2.按照姓名来排序
 bubble_sort(s, sz, sizeof(s[0]), sort_by_mame);
}
int main()
{
 test4();//sort_by_age、sort_by_name、struct Stu
 return 0;
}

你可能感兴趣的:(C语言篇,编程语言基础,c语言)