> 文档中心 > 【C语言详解】一步一步带你手撕指针 ~ You got it?

【C语言详解】一步一步带你手撕指针 ~ You got it?


🌈前言


  • (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
  • 🌍博客主页:张小姐的猫~江湖背景🌍
  • 快上车🚘,握好方向盘跟我有一起打天下嘞!
  • 想送给自己一句话🤔:
  • 🔥想要30万的年薪,就要付出30万的努力🔥
  • 🙏作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
  • 🎉🎉欢迎持续关注!🎉🎉
    在这里插入图片描述

请添加图片描述

在这里插入图片描述

手把手教会你指针

  • 🌈前言
  • 🐾浅浅的复习一下基本知识吧
  • 🐾字符指针
    • 🌅字符指针的定义
    • 🌅字符指针的用途
    • ✅例题
  • 🐾指针数组
    • 🌅指针数组的定义
    • 🌅指针数组的应用
  • 🐾数组指针
    • 🌅数组指针的定义
    • 🌅浅探究指针
      • 🌳指针的类型
      • 🌳指针所指向的类型
      • 🌳指针的值
      • 🌳指针占据的大小
    • 🌅&数组名VS数组名
    • 🌅数组指针的使用
    • 📜 小练习
  • 🐾数组参数和指针参数
    • 🌅 一维数组传参
    • 🌅 二维数组传参
    • 🌅 一级指针传参
    • 🌅 二级指针传参
  • 🐾函数指针
    • 🌅 函数指针的定义
    • 🌅 函数指针的类型
  • 🐾函数指针数组
    • 🌅 函数指针数组的定义
    • 🌅 函数指针数组的实现
  • 🐾指向函数指针数组的指针
  • 🐾回调函数
    • 🌅回调函数的定义
    • 🌅回调函数的应用
  • 🐾sqort函数
    • 🌅sqort函数的定义
    • 🌅sqort函数的使用
    • 🌅sqort函数的模拟实现
  • 📢写在最后

请添加图片描述

🐾浅浅的复习一下基本知识吧

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间
  • 内存会划分为小的内存单元,每个内存单元都有一个编号,这个编号就称为地址,这地址也叫作指针。所以内存编号 = 地址 =指针 ,指针或地址要存储可以放到指针变量中。
  • 我们常说的指针实际为指针变量。
  1. 指针的大小是固定的4/8个字节(32位平台/64位平台)
  2. 指针是有类型的,指针的类型决定了指针±整数的步长 ,指针解引用操作的时候的权限
  3. 指针的运算

【C语言详解】一步一步带你手撕指针 ~ You got it?

接下来我们继续探讨指针的高级主题吧

🐾字符指针

🌅字符指针的定义

字符指针:是指向字符的指针,类型为char*

🌅字符指针的用途

指向单个字符

char ch = 'w';char* pc = &ch;

指向字符串的首地址

char* pa = "abcdef";printf("%c\n",*pa);

用图像来深入理解:
在这里插入图片描述

1️⃣pc存有ch的地址,因此可通过解引用操作访问ch
2️⃣存放在指针里的是字符串首元素的地址,所以可以通过首元素的地址,从而打印出整个字符串
【C语言详解】一步一步带你手撕指针 ~ You got it?
3️⃣字符串在内存空间上是连续存放的,即打印的abcdef是连续的。

为了防止常量字符串被改变,我们可以用const修饰,const的详细用法可以看我这篇博客十分钟深入理解const用法🌋

const char* pa = "abcdef";

✅例题

这是一道经典的面试题:

#include int main(){    char str1[] = "hello bit.";    char str2[] = "hello bit.";    const char *str3 = "hello bit.";    const char *str4 = "hello bit.";    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语言详解】一步一步带你手撕指针 ~ You got it?

我们分析得知:

1️⃣ 这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。
2️⃣但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块开辟出不同的内存块。所以str1和str2不同,str3和str4不同。
在这里插入图片描述

请添加图片描述

🐾指针数组

🌅指针数组的定义

顾名思义→是一个存放指针的数组

int arr[10];//存放整型的数组char arr2[10];//存放字符的数组int* arr3[10];//存放整型指针的数组char* ch[5];//存放字符指针的数组

🌅指针数组的应用

//不常见的方法int a = 10;int b = 20;int c = 30;int* p1 = &a;int* p2 = &b;int* p3 = &c;int* arr[3] = { &a,&b,&c };

图解:在这里插入图片描述

    //常见的方法#define _CRT_SECURE_NO_WARNINGS#includeint main(){int arr1[5] = { 1,2,3,4,5 };int arr2[5] = { 2,3,4,5,6 };int arr3[5] = { 3,4,5,6,7 };int* parr[3] = { arr1,arr2,arr3 };for (int i = 0; i < 3; i++){for (int j = 0; j < 5; j++){printf("%d ",parr[i][j]);//printf("%d ", *(parr[i]+j));//printf("%d ", *(*(parr+i)+j));}printf("\n");}return 0;}

图解:
在这里插入图片描述

  • 通过访问数组指针的时候,把[ ]当成运算符:

  • parr[i][j]等价于*(*(parr + i) + j) 【C语言详解】一步一步带你手撕指针 ~ You got it?

请添加图片描述

🐾数组指针

🌅数组指针的定义

数组指针是指针?还是数组?
答案是:指针。

我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。

浮点型指针: float * pf;能够指向浮点型数据的指针。
打个比方:好孩子,主语应该是孩子,所以通过类比我们知道
数组指针:能够指向数组的指针

为了方便我们理解一个复杂的指针类型,我总结出一个小法宝,屡试不爽。

一个复杂的指针类型通常是由多个运算符组合 而成,我们首要目的就是要捋清楚运算符的优先级,哪个先哪个后,那指针的类型不就手到擒来吗?

【C语言详解】一步一步带你手撕指针 ~ You got it?

  • 指针里的运算符主要有三种,它们的优先级是:()> [ ] > *
  • 变量首次与*结合,就会变成指针,与[ ]结合就会变成数组。
  • 可以结合()变成函数,也可以通过()来改变优先级。

接下来,我们实战一下吧

    int* p[10];int(*p)[10];int p(int);Int (*p)(int); int *(*p(int))[3];//上述的p分别是什么?

解释

  • int* p1[10]
    解释:p1和[ ]先结合,说明p1是个数组名,数组有十个元素,每个元素是int*。所以p1是一个数组,里面存放的是指针,叫整型指针数组
  • int (*p)[10]
    解释:p先和 *结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫整型数组指针
  • 这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合
  • int p(int)
    解释:从P 处起,先与( )结合,说明P 是一个函数,然后进入( )里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明则是参数为整型,返回值是整型的函数
    Int (*p)(int)
    解释:P 先与*结合,说明P 是一个指针,然后与( )结合,说明指针指向的是一个函数,函数的参数是int整型,返回值也是int,所以P 是一个指向有一个整型参数且返回类型为整型的函数指针
  • int *(*p(int))[3]
    解释:P先与( )结合,说明P 是一个参数为int的函数,然后与*结合,说明函数返回的是一个指针,再与[ ]结合,说明返回的指针指向的是一个数组,再与*结合,说明数组里的元素是指针,最后与int 结合,说明指针指向的内容是整型数据。
    所以P 是一个参数为int型且返回一个指向由整型指针组成的数组指针函数.

🌅浅探究指针

指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四个方面,下面我们继续探究🛫
请添加图片描述

🌳指针的类型

从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:

int*ptr;//指针的类型是int*char*ptr;//指针的类型是char*int**ptr;//指针的类型是int**int(*ptr)[3];//指针的类型是int(*)[3]int*(*ptr)[4];//指针的类型是int*(*)[4] 

怎么样?找出指针的类型的方法是不是很简单🥸?

🌳指针所指向的类型

当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的,指针名字和名字左边的指针声明符*去掉剩下的就是指针所指向的类型。例如:

int*ptr; //指针所指向的类型是intchar*ptr; //指针所指向的的类型是charint**ptr; //指针所指向的的类型是int*int(*ptr)[3]; //指针所指向的的类型是int()[3]int*(*ptr)[4]; //指针所指向的的类型是int*()[4] 

我们找到规律:

二者通过加减*可以互推在这里插入图片描述

🌳指针的值

指针的值:指针里存放的地址

    int a = 8;    int *p = &a;    *p = 0;    printf("%d\n",a);

指针的值:p本身的值,p里存放这变量a的内存的起始地址。
指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。
以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?(重点注意)

🌳指针占据的大小

指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。
32位平台下占4个字节,64位平台占8个字节

请添加图片描述

🌅&数组名VS数组名

    int arr[10];

arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥? 我们看一段代码:

#include int main(){    int arr[10] = {0};    printf("%p\n", arr);    printf("%p\n", &arr);    return 0; }

结果如下:
在这里插入图片描述

可见数组名和&数组名打印的地址是一样的。
难道两个是一样的吗?
我们再看一段代码:

#include int main(){ int arr[10] = { 0 }; printf("arr = %p\n", arr); printf("&arr= %p\n", &arr); printf("arr+1 = %p\n", arr+1); printf("&arr+1= %p\n", &arr+1); return 0; }

【C语言详解】一步一步带你手撕指针 ~ You got it?根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。
实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
【C语言详解】一步一步带你手撕指针 ~ You got it?
本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型
数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.

通常情况下,数组名都是数组首元素的地址
🌍敲黑板~重点:有两个例外
【C语言详解】一步一步带你手撕指针 ~ You got it?
sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
&数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
③除这两种外,数组名都是数组首元素的地址。

请添加图片描述

🌅数组指针的使用

那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

void print1(int arr[], int sz){int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");}//形参写成指针的形式void print2(int *arr, int sz){int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(arr+i));}printf("\n");}int main(){int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//写一个函数打印arr数组的内容int sz = sizeof(arr) / sizeof(arr[0]);print1(arr, sz);print2(arr, sz);return 0;}

结果如下👇🏻:这里是引用

当我们把数组传给函数时,有两种方法:👇🏻

  1. int arr[ ]数组接收,其本质也是指针,因为编译器会把数组转化成指针!
  2. 指针接收,接收的数组名是首元素的地址。

当我们用数组指针来接收:👇🏻

void print1(int(*p)[10], int sz){int i = 0;for (i = 0; i < 10; i++){    //*p 相当于数组名,数组名又是首元素的地址,所以*p就是&arr[0]printf("%d ", *((*p) + i));}printf("\n");}int main(){int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//写一个函数打印arr数组的内容int sz = sizeof(arr) / sizeof(arr[0]);print1(&arr, sz);return 0;}

但是我很不推荐用这种方法,这种写法比较别扭, 有一种脱裤子放屁、画蛇添足的感觉。
所以数组指针很少应用在一维数组上

我们通常会用数组指针来接收二维数组👇🏻

void print_arr1(int arr[3][5], int row, int col) {int i = 0;for (i = 0; i < row; i++){for (int j = 0; j < col; j++){printf("%d ", arr[i][j]);}printf("\n");}}void print_arr2(int(*arr)[5], int row, int col) {int i = 0;for (i = 0; i < row; i++){for (int j = 0; j < col; j++){    //arr+i是指向第i行的    //*(arr+i)相当于拿到了第i行,也相当于第i行的数组名    //数组名表示首元素的地址,(*arr+i)也就是第i行第一个元素的地址    //printf("%d ", (*(arr+i)+j));printf("%d ", arr[i][j]);}printf("\n");}}int main(){int arr[3][5] = { 1,2,3,4,5, 6,7,8,9,10, 11,12,13,14,15 };print_arr1(arr, 3, 5);print_arr2(arr, 3, 5);return 0;}
  • 数组名arr,表示首元素的地址
  • 但是二维数组的首元素是二维数组的第一行
  • 所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
  • 可以数组指针来接收

结果如下:👇🏻

【C语言详解】一步一步带你手撕指针 ~ You got it?

所以最优解就是:使用数组指针来接收二维数组

请添加图片描述

📜 小练习

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

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

arr是一个整型数组,每个元素是int类型的,有5个元素
.
parr1 首先和[ ]结合,所以是一个数组,数组10个元素,每个元素的类型是int*
.
parr2 首先和*结合,所以是一个指针, 指向的数组有10个元素,每个元素的类型是int
.
parr3 首先和[ ],所以是一个数组,数组有10个元素,每个元素的类型是:int(*)[5]
parr3 是存放数组指针的数组
在这里插入图片描述

请添加图片描述

🐾数组参数和指针参数

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

🌅 一维数组传参

判断以下接收方式“河里”吗?👇🏻【C语言详解】一步一步带你手撕指针 ~ You got it?

void test(int arr[])//ok?{}void test(int arr[10])//ok?{}void test(int arr[100])//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);}

以上方式都合理,接下来我们逐一分析一波:【C语言详解】一步一步带你手撕指针 ~ You got it?

在这里插入图片描述

形参写成数组形式:形参部分的数组大小可以省略
形参写成指针形式:要注意传的元素的类型,若传的是一级指针,则用二级指针来接收

🌅 二维数组传参

void test(int arr[3][5])//ok?{}void test(int arr[][])//ok?{}void test(int arr[][5])//ok?{}int main(){ int arr[3][5] = {0}; test(arr);}

分析如下:
在这里插入图片描述那我们是不是有疑问说:为什么可以省略二维数组的行数,但不能省略列数❓

答:二维数组在内存中的地址排列方式是按行排列连续存放,第一行排列完之后再排列第二行,以此类推

  • 如果我们不知道列数,也就不知道一行能放多少个。
  • 只要知道了列数,放完后就会知道放了多少行

继续判断以下接收方式“河里”吗?👇🏻【C语言详解】一步一步带你手撕指针 ~ You got it?

void test(int *arr)//ok?{}void test(int* arr[5])//ok?{}void test(int (*arr)[5])//ok?{}void test(int **arr)//ok?{}int main(){ int arr[3][5] = {0}; test(arr);}

分析如下:
在这里插入图片描述
在这里插入图片描述

总结:

  1. 二维数组传参,函数形参的设计只能省略第一个[]的数字
    因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。
  2. 形参类型要相匹配:指向数组首行的地址,类型要相匹配

开个小灶:

int arr[10];test(arr);

在这里插入图片描述

🤔形参为数组时,其本质还是指针 !编译器会把数组转化成指针

请添加图片描述

🌅 一级指针传参

#include void print(int *p, int sz) //一级指针传参,一级指针接收{ int i = 0; for(i=0; i<sz; i++) { printf("%d\n", *(p+i)); }}int main(){ int arr[10] = {1,2,3,4,5,6,7,8,9}; int *p = arr; int sz = sizeof(arr)/sizeof(arr[0]); //一级指针p,传给函数 print(p, sz); return 0; }

一级指针传参,形参可以用一级指针接收,也可以用数组接收(但不推荐)

思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
比如:

void test1(int* p)//test1函数能接收什么参数?{//...}int main(){int a = 10;int* p = &a;int arr[10];test1(arr);test1(&a);test1(p);return 0;}

分析如下:
【C语言详解】一步一步带你手撕指针 ~ You got it?

形参为一级指针,实参可以是数组,也可以是一级指针(地址)

🌅 二级指针传参

void test(int** ptr) //二级指针接收{printf("num = %d\n", **ptr);}int main(){int n = 10;int* p = &n;int** pp = &p;//ppa是一个二级指针test(pp);//二级指针test(&p);//取地址一级指针,类型为二级指针return 0;}

二级指针传参时,形参最好用二级指针来接收(指针数组不推荐)

思考:
当函数的参数为二级指针的时候,可以接收什么参数?
比如:

void test(char** p) {}int main(){char c = 'b';char* pc = &c;char** ppc = &pc;char* arr[10];test(&pc);//取地址一级指针,类型是二级指针test(ppc);//传的是二级指针test(arr);//Ok?    ok  因为arr数组名是首元素,char*的地址——类型为char**return 0;}

提问:如果arr2是个二维数组,可以吗?
【C语言详解】一步一步带你手撕指针 ~ You got it?
不可以,要写成char(*p)[5]才可以。
【C语言详解】一步一步带你手撕指针 ~ You got it?

形参为二级指针时,传参可以是二级指针,也可以是数组指针首元素的地址

请添加图片描述

🐾函数指针

🌅 函数指针的定义

数组指针:是指向数组的指针。
函数指针:类比可知是指向函数的指针,存放函数地址的指针。

int Add(int x, int y){return x + y;}int main(){int arr[10];int(*p)[10] = &arr;//p是一个数组指针变量printf("%p\n", &Add);printf("%p\n", Add);}

【C语言详解】一步一步带你手撕指针 ~ You got it?

我们可以得知:函数名 == &函数名(完全等价)
都可以用来表示函数地址

  • 数组名 != &数组名(完全不同)
  • 函数名 == &函数名(完全等价)

🌅 函数指针的类型

了解了函数指针的定义,那么函数指针类型该怎么样写呢?

int (*pf)(int x, int y) = &Add;//函数指针变量

分析:
【C语言详解】一步一步带你手撕指针 ~ You got it?接下来,我们试一下这个:int test(char* str)的函数指针咋写?

int test(char* str){}int main(){int (*pf)(char*) = &test;}

那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:

void test(){ printf("hehe\n");}//下面pfun1和pfun2哪个有能力存放test函数的地址?void (*pfun1)();void *pfun2();

首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?
答案是:

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。

阅读两段有趣的代码:

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

我们慢慢把代码逐层剖析:
【C语言详解】一步一步带你手撕指针 ~ You got it?

代码1: ①:首先是把0强制类型转换成一个函数指针类型,这就意味着0地址处放着一个返回类型是void,无参的一个函数
②:调用0地址处的这个函数

【C语言详解】一步一步带你手撕指针 ~ You got it?

上面的代码是不是太废眼睛了呢?我们接下来用重定义typedef简化一下吧:

typedef void(*pf_t)(int);//给函数指针类型void(*)(int)重新起名叫:pf_tpf_t signal(int, pf_t);//替换后等效于void (*signal(int, void(*)(int)))(int)

注 :推荐《C陷阱和缺陷》,这本书中提及这两个代码。
请添加图片描述

🐾函数指针数组

🌅 函数指针数组的定义

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,
比如:

int *arr[10];//数组的每个元素是int*

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

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

答案是:parr1
parr1 先和 [ ] 结合,说明 parr1是数组,数组的内容是什么呢?
int (*)( ) 类型的函数指针。

函数指针数组的用途:转移表

🌅 函数指针数组的实现

例子:(计算器)

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

我在测试中发现了,有bug!
【C语言详解】一步一步带你手撕指针 ~ You got it?

当input等于0和5的时候,发现程序出错了

接下来我们对此进行修改:
把以下代码加入到switch语句的case中:

 printf("请输入2个操作数:> "); scanf("%d %d", &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;int x = 0;int y = 0;int ret = 0;do{menu();printf("请选择:>");scanf("%d", &input);switch (input){case 1:printf("请输入2个操作数:>");scanf("%d%d", &x, &y);ret = Add(x, y);printf("ret = %d\n", ret);break;case 2:printf("请输入2个操作数:>");scanf("%d%d", &x, &y);ret = Sub(x, y);printf("ret = %d\n", ret);break;case 3:printf("请输入2个操作数:>");scanf("%d%d", &x, &y);ret = Mul(x, y);printf("ret = %d\n", ret);break;case 4:printf("请输入2个操作数:>");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;}

但是我觉得还不够好:

  1. 代码冗余,出现了多次的printf 、scanf(每个case中都有)
  2. 可读性低

对此我们利用函数数组指针来优化,通过函数数组的下标来进行跳转到目标函数:

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;int x = 0;int y = 0;int ret = 0;//转移表int (*pfArr[])(int, int) = {0, Add, Sub, Mul, Div};    //pfArr就是函数数组指针do{menu();printf("请选择:>");scanf("%d", &input);if (input == 0){printf("退出计算器\n");}else if(input >= 1 && input<=4){printf("请输入2个操作数:>");scanf("%d%d", &x, &y);ret = pfArr[input](x, y);printf("ret = %d\n", ret);}else{printf("选择错误\n");}} while (input);return 0;}

【C语言详解】一步一步带你手撕指针 ~ You got it?

上面应用了函数指针数组,通过函数数组的下标来进行跳转到对应的目标函数
我们把这种函数指针称为转移表

请添加图片描述

🐾指向函数指针数组的指针

指向函数指针数组的指针是一个 指针
指针指向一个 数组 ,数组的元素都是 函数指针 ;
如何定义?

void test(const char* str) {printf("%s\n", str);}int main(){//函数指针pfunvoid (*pfun)(const char*) = test;//函数指针的数组pfunArrvoid (*pfunArr[5])(const char* str);pfunArr[0] = test;//指向函数指针数组pfunArr的指针ppfunArrvoid (*(*ppfunArr)[5])(const char*) = &pfunArr;return 0;}

【C语言详解】一步一步带你手撕指针 ~ You got it?

请添加图片描述

🐾回调函数

🌅回调函数的定义

回调函数就是一个通过函数指针调用的函数

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

举个简单的例子:

void test(){printf("hehe\n");}void print_hehe(void (*p)())//函数指针接收,该形参与test函数类型相同{if (1)p();}int main(){print_hehe(test);return 0;}

图例分析如下:
【C语言详解】一步一步带你手撕指针 ~ You got it?

test的地址传给了print_hehe函数,通过print_hehe函数来调用test函数,所以我们称print_hehe函数为回调函数

🌅回调函数的应用

我们拿switch版本的计算机来实践:

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 (*pf)(int, int)){    int x = 0;    int y = 0;    printf("请输入2个操作数:>");    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 0:     printf("退出\n");     break; default:     printf("选择错误\n");     break; }    } while (input);    return 0;}

把冗余的代码封装成一个Calc函数。
把输入的函数地址传给Calc,Calc通过传入的地址,从而找到了目标函数

【C语言详解】一步一步带你手撕指针 ~ You got it?请添加图片描述

🐾sqort函数

🌅sqort函数的定义

sqort是一个包含在 头文件下的库函数,主要根据一定的比较条件进行快速排序。
也可以对所有类型的数据进行排序,一个函数解决所有类型的排序问题,不需要根据不同的类型些不同的函数,提高效率!🔥

接下来我们来回顾一下冒泡排序:

void bubble_sort(int arr[], int sz){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语言详解】一步一步带你手撕指针 ~ You got it?

void qsort(void *base,    size_t num,    size_t width,    int (*cmp)(const void *, const void*))

(1):base 👉 待排序数组首地址(可直接输入待排序数组名,或是指向数组的指针)
.

(2):num👉数组中待排序元素数量(可以用sizeof()来求)
.
(3):width👉 各元素的占用空间大小(可以用sizeof(arr[0])来求)
.
(3):cmp 👉 用来比较待排序数据种两个元素的函数。如果,返回的是大于0的数字表示第一个元素大于第二个元素、等于0的话就是表示第一个元素等于第二个元素、小于0的话就是第一个元素小于第二个元素。

ps:程序员们一定要学会看英文资料,对此我们可以下载一个MSDN(可以私聊我拿安装包哦),里面会有各种函数的解析(英文),以后的工作也会接触到英文资料等,加油吧!

那么为什么qsort函数可以排序多种数据类型呢❓

首先:因为void* base指针和num(待排序数组的元素个数)size(待排序数组的元素大小)可以描述出任意类型。

眼尖的同学会发现👉参数base的类型是viod*

因为void*“海纳百川”,无具体类型,它可以接收任意类型的指针。

接下来我们来看*cmp函数:

这个比较函数指定元素的比较方式,要求使用者自行定义函数。
【C语言详解】一步一步带你手撕指针 ~ You got it?
elem1小于elem2,返回值小于0
elem1大于elem2,返回值大于0
elem1等于elem2,返回值为0

🌅sqort函数的使用

题目:将arr数组当中元素进行排序,用qsort函数实现!

int int_cmp(const void* e1, const void* e2){return *(int*)e1 - *(int*)e2;}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]);qsort(arr, sz, sizeof(arr[0]), int_cmp);print_arr(arr, sz);return 0;}

结果:
【C语言详解】一步一步带你手撕指针 ~ You got it?

排序的整型数据:用> <
排序的结构体数据:需要使用者提供一个函数,实现两个数据的比较
那如何用qsort排序结构体呢?

#include#include#includestruct Stu{char name[20];int age;};int sort_by_age(const void* e1, const void* e2)//年龄{return ((struct Stu*)e1) -> age - ((struct Stu*)e2) -> age;//升序}int sort_by_name(const void* e1, const void* e2)//名字{return strcmp(((struct Stu*)e1) -> name, ((struct Stu*)e2) -> name);//升序}int main(){struct Stu stu[] = { {"zhangsan", 30}, {"lisi", 34}, {"wangwu", 20} };//按年龄来qsort(stu, sizeof(s) / sizeof(s[0]), sizeof(s[0]), sort_by_age);//按名字来qsort(stu, sizeof(s) / sizeof(s[0]), sizeof(s[0]), sort_by_name);return 0;}

e1 - e2为升序;e2 - e1为降序

【C语言详解】一步一步带你手撕指针 ~ You got it?

我们摸透了sqort接下来模拟实现一下吧

🌅sqort函数的模拟实现

(采用冒泡的方式)

void swap(char* buf1, char* buf2, int width){    for (int i = 0; i < width; i++)    { char t = *buf1; *buf1 = *buf2; *buf2 = t; buf1++; buf2++;    }}int cmp_int(const void* e1, const void* e2){    return (*(int*)e1 - *(int*)e2);//void强制类型转换为元素的地址的类型}void bubble_sort(void* base, int num, int width,int(*cmp)(const void* e1, const void* e2)){    for (int i = 0; i < num - 1; i++)    { for (int j = 0; j < num - 1 - i; j++) {     //if(arr[j]>arr[j+1])比较          if (cmp((char*)base + (j)*width, (char*)base + (j + 1) * width) > 0)     {  //交换  // int t=arr[j];  // arr[j]=arr[j+1];  // arr[j+1]=t;  swap((char*)base + (j)*width, (char*)base + (j + 1) * width,width);     } }    }}void print_arr(int arr[], int sz){    for (int i = 0; i < sz; i++)    { printf("%d ", arr[i]);    }    printf("\n");}int main(){    int arr[] = { 9,8,7,6,5,4,3,2,1,0 };    int sz = sizeof(arr) / sizeof(arr[0]);    bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);//0 1 2 3 4 5 6 7 8 9    print_arr(arr, sz);}

因为base是void*类型 ,并且语法不支持(指针±整数),而char类型是所以类型中的最小的。所以(char)base + j * size可以确定元素地址。

请添加图片描述【C语言详解】一步一步带你手撕指针 ~ You got it?

📢写在最后

能看到这里的都是棒棒哒🙌!
想必指针也算是C语言中最难🔥的部分了,如果认真看完以上部分,肯定有所收获。
指针内容比较难懂,我们一定多敲代码,都说了2w行代码才算入门了C语言!你写了多少行呢?
接下来我还会继续写关于📚《指针练习题目》等…
💯如有错误可以尽管指出💯
🥇想学吗?我教你啊🥇
🎉🎉觉得博主写的还不错的可以一键三连撒🎉🎉
【C语言详解】一步一步带你手撕指针 ~ You got it?

神唱ktv下载