C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉


文章目录

  • 前言
  • 一、字符指针
    • 1.字符指针的定义
    • 2.字符指针的作用
    • 3.字符指针的特点
  • 二、指针数组
    • 1.指针数组的定义
    • 2.指针数组的使用
  • 三、数组指针
    • 1.数组指针的定义
    • 2.细说指针
      • 2.1.指针类型
      • 2.2.指针所指向的类型
      • 2.3.指针的值
      • 2.4.指针大小
    • 3.数组名相关
    • 4.数组指针的使用
  • 四、数组传参和指针传参
    • 1.一维数组传参
    • 2.二维数组传参
    • 3.一级指针传参
    • 4.二级指针传参
  • 五、函数指针
    • 1.函数指针的定义
    • 2.函数指针的类型
    • 3.函数指针的使用
  • 六、函数指针数组
    • 1.函数指针数组的定义
    • 2.函数指针数组的使用
  • 七、(函数指针数组)指针
    • 1.(函数指针数组)指针的定义
    • 2.(函数指针数组)指针的使用
  • 八、回调函数
    • 1.回调函数的定义
    • 2.回调函数的使用
    • 3.qsort函数(quick sort快速排序函数)
      • 3.1.qsort函数的使用
      • 3.2.qsort函数的模拟实现

前言 C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

众所周知,C指针算是C最难的一部分。在这我会将和大家一起深究指针并征服C指针。
在正式学习进阶指针之前,我们先来简单回忆下指针基础:
指针定义:指针变量,用于存放地址。地址唯一对应一块内存空间。
指针大小:32位平台下占4个字节,64位平台占8个字节。
指针类型:类型决定指针±整数的步长和指针解引用时访问的大小。
指针运算:指针解引用,指针±整数,指针-指针,指针的关系运算。
指针诞生:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

这些内容在C基础部分已经讲过,铁汁们可以复习一下传送门,看完记得回来啊~
理解一遍后,就让我们正式起航扬帆吧!乌拉~
注: 我们习惯把指针变量叫作指针,本文指针本质是指针变量
一、字符指针 1.字符指针的定义 字符指针: 指向字符的指针,类型为char*
2.字符指针的作用
  1. 指向单个字符变量
char ch = "w"; char* pch = &ch;

  1. 指向字符串首字符
char* pc = "hello"; printf("%s\n",pc);

看图说话:
(为了理解简单,地址用201、204表示)
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

【C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉】①比较好理解,pch存有ch的地址,因此可通过解引用操作访问ch
②并不是把字符串"hello"放进指针,而是把字符串首字符的地址放进指针,通过首字符地址,可以找到整个字符串
对于上面的解析,我们可以进行验证:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

为什么靠一个首字符的地址就可以找到整个字符串呢?
下面我们来证明一下:
char* pc = "hello"; printf("%c\n", *(pc + 1)); //e printf("%c\n", *(pc + 2)); //l printf("%s\n", pc); //hello printf("%s\n", pc + 1); //ello

运行结果:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

  1. 字符串中每1个字符占1个字节,且内存的单元是1个字节,为了方便管理,字符串在内存空间上是连续存放的
    即hello是连续的
  2. %s:输出字符串,从所给地址开始,一直打印到\0结束符号(不包括'\0')
3.字符指针的特点 这是一道题面试题:
#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语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

数组名是数组首元素的地址;指针存有字符串首字符的地址
两者有异曲同工之妙,所以
(str1 == str2) ,表示比较存放相同字符串的两个数组的地址是否相同;
(str3 == str4),表示比较存放相同字符串的两个指针的值是否相同
我们可以做出分析:
1.str1[]和str2[]是字符数组,在内存上会开辟两块地址不一样空间,但存放相同的内容"hello bit"
2.str3和str4是指向常量字符串的指针,而常量字符串存放在内存的常量区,常量区特点是常量值不可被修改且有唯一性(就没有存在2份或多份的必要),所以指针指向的是同一份数据,故地址是相同的
图解:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

注: 常量区的常量不可修改,通常用const来修饰。防止被意外修改
二、指针数组 1.指针数组的定义 我们先看:
int arr[10]; 整型数组 char ch[5]; 字符数组 float f[20]; 浮点型数组

整型数组是存放整型的数组。
类比得指针数组是存放指针的数组。
int* parr[10]; 整型指针数组 char* pch[5]; 字符指针数组 float* pf[20]; 浮点型指针数组

关于指针数组的数组名:
int arr[10]; int* parr[10];

数组的数组名是首元素的地址:
整型数组的数组名arr,是首元素(整型)的地址,所以arr是一级指针。
整型指针数组的数组名parr,也是首元素(整型指针)的地址,所以parr是二级指针。
2.指针数组的使用 整型指针数组的使用:
#include int main() { //int a = 10; //int b = 20; //int c = 30; //int* arr[3] = { &a, &b, &c }; //不常用的写法int arr1[] = { 1,2,3,4,5 }; int arr2[] = { 2,3,4,5,6 }; int arr3[] = { 3,4,5,6,7 }; int* parr[] = { arr1,arr2,arr3 }; //常见的写法 for (int i = 0; i < 3; i++) { for (int j = 0; j < 5; j++) { //1. printf("%d ", parr[i][j]); //2. // printf("%d ", *(*(parr + i) + j)); } printf("\n"); } return 0; }

运行结果:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

通过指针数组访问整型数组的每一个元素
parr[i][j]等价于*(*(parr + i) + j)
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

字符指针数组的使用:
#include int main() { const char* pch[] = { "abcde", "bcdef", "cdefg" }; for (int i = 0; i < 3; i++) { //1. printf("%s", pch[i]); //2. //printf("%s", *(pch + i)); printf("\n"); } }

运行结果:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

pch[i]是字符串首字符的地址,%s:打印字符串
三、数组指针 1.数组指针的定义 我们已经知道:
字符指针:指向字符的指针
整型指针:指向整型的指针
C语言语法是有规律性的,也就是说,指针指向的类型<==>指针的类型(相互决定)
所以我们可以类比得:
数组指针:指向数组的指针,即数组的地址存放在数组指针中。
到此,我要说一下,指针部分会出现一些复杂的类型,那如何理解一个复杂的类型?
在这我先给大家抛出一个屡试不爽的技巧总结,有了这些法宝,后面的路就会容易走。
类型显得复杂的原因是它由多种的运算符组成,当我们分清运算符的优先级,理清顺序,自然会柳暗花明又一村~
1.运算符主要有三种,它们的优先级是:* < [ ] < ( )
2.变量名第一次与运算符结合就决定了它的本质。(变量名的处女情结?_?)
如,先与*结合是指针;先于[ ]结合是数组;
( ):1.先于( )结合是函数,即p( ) 2.用于改变优先级,如(*p)
下面我们一起来探索一下吧!
int* p[10]; int(*p)[10]; int p(int); int (*p)(int); int *(*p(int))[10];

int* p[10]; //由优先级知:p先于[]结合,则p是数组;后与*结合,则是指针数组;最后与int结合,则是整型指针数组
int(*p)[10]; //()优先级最高,p先与*结合,则p是指针;后[]结合,则是数组指针;最后与int结合,则是整型数组指针
int p(int); //p先与( )结合,则p是函数;参数是整型,返回值是整型,则是参数为整型,返回值是整型的函数
int (*p)(int); //p先与*结合,则p是指针;后与( )结合,则指针指向的是函数,函数参数是整型,返回值是整型,则是一个指向参数为整型,返回值是整型的函数的指针(函数指针)
int *(*p(int))[10]; //p先与( )结合,则p是形参为int的函数;后与*结合,则是返回指针的函数;再与[ ]结合,则是返回的指针指向的是一个数组;再与*结合,说明数组里的元素是指针;最后与int结合,指针指向的内容是整型数据。所以p是返回值为整型指针数组指针,形参为int型的函数。
2.细说指针
好了,现在我们已经学会了如何理解一个复杂的类型。那对于一个指针,我们要搞清楚它的什么呢?
指针是一个特殊的变量,它存放着内存中的地址。
要深入了解它从这四方面考虑:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

2.1.指针类型
上一章说过,去掉名字就是类型
同理,若把指针声明语句中的指针的名字去掉,剩下部分就是该指针的类型。
int* ptr; //指针的类型是int*
char* ptr; //指针的类型是char*
int** ptr; //指针的类型是int**
int (*ptr)[5]; //指针的类型是int()[5]
int* (*ptr)[10]; //指针的类型是int
(*)[10]
指针类型的意义(C基础篇已经讲过):
1.指针解引用访问几个字节(访问的内存空间大小)
2.指针类型决定了指针±整数跳过几个字节(步长)
2.2.指针所指向的类型
指针所指向的类型决定了编译器看待指针指向内存区的内容的方式
若把指针声明语句中的指针的名字名字左边的指针声明符号*去掉,剩下部分就是指针所指向的类型。
int* ptr; //指针所指向的类型是int
char* ptr; //指针所指向的类型是char
int** ptr; //指针所指向的类型是是int*
int (*ptr)[5]; //指针所指向的类型是int()[5]
int* (*ptr)[10]; //指针所指向的类型是int()[10]
通过观察我们可以发现:
二者可互推C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

2.3.指针的值
指针的值:指针里存放的地址
举个例子:
int *p; //定义一个指针 int a; //定义一个int类型的变量 p=&a; //使用取址运算符(&)将变量a的地址赋给p

指针的值:p本身的值,p里存放这变量a的内存的起始地址
而指针p所指向的内存区就是从a的起始地址开始,长度为size(int)的一片内存区。
2.4.指针大小
指针大小:32位平台下占4个字节,64位平台占8个字节。
3.数组名相关 老生常谈:
①sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
②&数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
除这两种外,数组名都是数组首元素的地址。
#include int main() { int arr[10] = {0}; int* p1 = arr; //arr是数组首元素地址,为int型 int (*p2)[10] = &arr; //&arr是整个数组的地址,为int [10]型 //arr和&arr值一样,但类型不一样 //p1和p2是相同的指向同一位置 printf("%p\n", p1); //204 printf("%p\n", p2); //204 //指针类型决定指针±整数的步长 printf("%p\n", p1 + 1); //跳过一个整型,208 printf("%p\n", p2 + 1); //跳过一个数组,244 return 0; //为了简单理解,204,208,244表示地址 }

4.数组指针的使用 当我们遍历一维数组时,可以这样做:
void Print1(int arr[], int sz) { for (int i = 0; i < sz; i++) { //printf("%d ", arr[i]); printf("%d ", *(arr + i)); } } void Print2(int* arr, int sz) { for (int i = 0; i < sz; i++) { printf("%d ", arr[i]); //printf("%d ", *(arr + i)); } } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); Print1(arr, sz); Print2(arr, sz); return 0; }

运行结果:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

把数组名传给函数时,我们有两种接收方式:
①数组接收,编译器会将数组退化为指针
②指针接收
因为数组名是数组首元素的地址,用指针接收是正确的。
当数组指针用来接收并访问一维数组时:
#include int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int (*parr)[10] = &arr; //指针指向数组,数组有10个元素,每个元素为int型 int i = 0; for(i = 0; i < 10; i++) { printf("%d ", *((*parr) + i)); //*parr相当于arr } return 0; }

通常这种写法显得小题大作,比较别扭,非常不推荐这种写法。
通常,数组指针用来接收并访问二维数组,会有很好的效果:
void Print1(int arr[3][5], int r, int c)//二维数组传参,用二维数组接收,实际上不会创建二维数组,编译器会将int arr[3][5]退化为int(*pa)[5] { for (int i = 0; i < r; i++) { for (int j = 0; j < c; j++) { //printf("%d ", arr[i][j]); printf("%d ", *(*(arr + i) + j)); } printf("\n"); } } void Print2(int(*pa)[5], int r, int c)//二维数组传参,用数组指针接收 { for (int i = 0; i < r; i++) { for (int j = 0; j < c; j++) { //1. printf("%d ", pa[i][j]); //2. // printf("%d ", *(*(pa + 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); //二维数组首元素是首行 return 0; }

运行结果:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

分析:
在C基础数组章节,我们知道二维数组在内存中也是连续存储的。
在这里简单提下,需要复习的,C基础数组传送门
连续存储:1.每一行内部的元素连续存放 2.行与行之间连续存放
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

  • 因此,二维数组的数组名是首行的地址,类型是int(*)[5]
  • 二维数组首元素地址和数组指针是等价的,即数组指针pa就是数组名。
  • 指针类型是int(*)[5],解一层引用找到的是二维数组的行
  • 指针所指向的类型是int[5],再解一层引用找到的是某行中的元素
综上,正确的做法是:使用数组指针来接收二维数组。
正确使用数组指针会有很好的效果,但如果随便用可能会很别扭。
下面是强行使用数组指针的错误用法:
void Print3(int(*pa)[10], int sz) { for (int i = 0; i < sz; i++) { //printf("%d ", pa[i]); printf("%d ", *(pa + i)); } } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; //一维数组 int sz = sizeof(arr) / sizeof(arr[0]); Print3(&arr, sz); //&arr是整个数组的地址 return 0; }

C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

实参为整个数组的地址,形参是数组指针,*(pa+i)解一层引用后,打印出来的为什么还是地址呢?
  • &是取地址运算符,*是间接运算符。&*互为逆运算
  • *&x 的含义是,先获取变量 x 的地址,再获取地址中的内容,相当于抵消
  • 所以&arr传给pa,执行*pa,相当于*&arr,实质就是arr,还是整个数组的地址。
  • 整个数组的地址的类型是int(*)[10],即指针pa的类型是int(*)[10]
  • 由指针类型决定指针±整数的步长可知,每个地址间相差40字节。
四、数组传参和指针传参
写代码时要把数组和指针传递给函数的情况在所难免,那函数参数该如何设计呢?
1.一维数组传参 一维数组传参,下面的接收方式它合理吗?
void test(int arr[])//合理吗? {} void test(int arr[10])//合理吗? {} void test(int *arr)//合理吗? {} void test2(int *arr[])//合理吗? {} void test2(int *arr[20])//合理吗? {} void test2(int **arr)//合理吗? {} int main() { int arr[10] = {0}; int* arr2[20] = {0}; test(arr); test2(arr2); }

以上的接收方式都合理
分析:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

数组作为函数形参,会退化为指针。
所以一维数组传参,数组和指针都可以作为函数形参。
2.二维数组传参 二维数组传参,下面的接收方式它合理吗?
void test(int arr[3][5])//合理吗? {} void test(int arr[][5])//合理吗? {} void test(int arr[3][])//合理吗? {} void test(int arr[][])//合理吗? {} int main() { int arr[3][5] = {0}; test(arr); //二维数组传参 return 0; }

分析:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

为什么二维数组作为函数形参,行可省略,列不可省略。
原因: 二维数组存储的时候是"先行后列",如果不指定列数, 它就不能知道一行放几个数据了。只要知道了列数, 全部放完就可以知道一共能放多少行。
我们C基础数组在已经讲过,C基础数组传送门。
二维数组传参,下面的接收方式它合理吗?
void test(int* arr)//合理吗? {} void test(int* arr[5])//合理吗? {} void test(int(*arr)[5])//合理吗? {} void test(int** arr)//合理吗? {} int main() { int arr[3][5] = { 0 }; test(arr); //二维数组传参 return 0; }

分析:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

综上,我们可以总结出二维数组传参时,函数形参的两种设计方法:
①形参给出第二维(列)的长度
②形参为指向数组的指针
另外,还有一种方式:形参声明为指针的指针void test(int**a){},该方式比较复杂,到后期学到CPP在详谈(关注我不迷路哦~)
3.一级指针传参 一级指针作为实参时,函数形参该如何设计?
void print(int* ptr, int sz)//一级指针传参,用一级指针接收 { int i = 0; for(i=0; i; 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]); print(p, sz); //p是一级指针,传给函数 return 0; //1 2 3 4 5 6 7 8 9 10 }

一级指针作为实参,函数形参可以是一级指针,也可以是一维数组(不推荐)。
反向思考: 若函数形参是一级指针,实参该如何设计?
void test(int* p) {} int main() { int a = 10; int* pa = &a; test1(&a); test1(pa); return 0; }

函数形参为一级指针,实参可以是地址(一级指针),也可以是数组。
4.二级指针传参 二级指针作为实参时,函数形参该如何设计?
void test(int** pp)//二级指针接收 { printf("%d\n", **pp); } void test(int* arr[])//指针数组,会退化为二级指针,不推荐该写法 { printf("%d\n", *arr[0]); } int main() { int a = 10; int* p = &a; int** pp = &p; test(&p); //一级指针的地址,类型为二级指针 test(pp); //实参二级指针 return 0; }

二级指针作为实参时,函数形参可以是二级指针,也可以是指针数组(不推荐)。
反向思考:若函数形参是二级指针,实参该如何设计?
void test(int** pp) { printf("%d\n", **pp); } int main() { int a = 10; int* p = &a; int** pp = &p; int* arr[10] = { &a }; test(&p); //一级指针p的地址,类型为二级指针 test(pp); //实参二级指针 test(arr); //数组名是数组首元素的地址,首元素是一级指针,它的地址是二级指针 return 0; }

函数形参为二级指针时,实参可以是二级指针(一级指针的地址),也可以是指针数组的元素的地址(特殊的为数组名,因为它是数组首元素的地址嘛)。
我们为什么要学习如何设计实参和函数形参?
1.调用别人设计好的函数时,我们需要清楚传什么实参。
2.根据函数的目的,来设计函数形参。
五、函数指针 1.函数指针的定义
类比得,函数指针:指向函数的指针,它存放着函数的地址。
没错,函数也是有地址的,可以看下面一组代码:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

表示函数地址的方式:
  • 函数名
  • &函数名
注:这里需要与数组名区别开来:
  • 函数名 == &函数名 (都是函数地址,相同意义)
  • 数组名 != &数组名 (不同意义)
2.函数指针的类型 函数的地址需要存放在函数指针里,那么函数指针的类型该如何写呢?
其实很简单,上面我们学习了如何去理解一个负责的类型,相信我们也可以自己写出来:
//以Add函数为例 int (*pf)(int, int) = &Add;

解析:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

自我检测:如何保存viod test(char* str){}的地址
答案:void (*pt)(char*) = &test;
3.函数指针的使用 计算机硬件程序经常通过调用地址的方式来调用函数,因此需要使用函数指针调用函数。
int Add(int x, int y) { return x + y; } int main() {//int(*pf)(int, int) = &Add; int(*pf)(int, int) = Add; int ret0 = pf(2, 3); int ret1 = (*pf)(2, 3); int ret2 = (**pf)(2, 3); int ret3 = (***pf)(2, 3); printf("ret1 = %d\n", ret0); printf("ret2 = %d\n", ret1); printf("ret3 = %d\n", ret2); printf("ret3 = %d\n", ret3); return 0; }

C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

因为pf = Add; 所以pf(2,3)相当于Add(2,3),但pf是函数指针。
(*pf)(2,3)中的*并没意义,可加可不加。
阅读两端有趣的代码:(注: 出自 《C指针和陷阱》 )
代码1:
(*(void (*)())0)();

解析:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

代码2:
void (*signal(int, void(*)(int)))(int);

解析:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

或者我们可以通过类型重定义来理解:
int main() { typedef void (*PFUN)(int); //把void(*)(int)类型重定义为PFUN PFUN signal(int, PFUN); //等效于void (*signal(int, void(*)(int)))(int); return 0; }

六、函数指针数组 1.函数指针数组的定义 整型指针数组:存放整型指针的数组。
类比得,函数指针数组:存放函数指针的数组。
int Add(int x, int y)//int(*)(int,int) { return x + y; } int Sub(int x, int y)//int(*)(int,int) { return x - y; } int main() { int (*pf1)(int, int) = Add; int (*pf2)(int, int) = Sub; int (*pfArr[2])(int, int) = { pf1, pf2 }; //等效于int (*pfArr[2])(int, int) = { Add, Sub }; //pfArr 就是函数指针数组 return 0; }

  • 类型相同的函数指针,放在同一个数组中。
  • 数组去掉数组名和[ ],剩下部分就是数组元素类型。pfArr的元素类型为int(*)(int,int)
2.函数指针数组的使用 实现一个简易计算器,来进行加减乘除运算。
void menu() { printf("*****************************\n"); printf("**1. add2. sub**\n"); printf("**3. mul4. 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; }

运行并测试发现:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

input = 0input = 5放进程序就很容易发现问题所在。
我们对main主函数部分修改:
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("请输入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 = Div(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; }

运行并测试
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

经测试,虽然bug得到了解决,但代码仍存在如下缺陷:
1.代码冗余,出现大量重复的代码(case中)
2.可维护性低(后期需要增加其他功能,就得多写一个case)
3.代码可读性差
针对缺陷,我们利用函数指针数组进行优化,达到通过数组下标"跳转"来调用不同的函数的目的。
#include void menu() { printf("*****************************\n"); printf("**1. add2. sub**\n"); printf("**3. mul4. 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 (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div}; //pfArr就是函数指针数组 int x = 0; int y = 0; int ret = 0; printf("请选择:> "); scanf("%d", &input); if(input >= 1 && input <= 4) { printf("请输入2个操作数:> "); 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; }

运行并测试成功:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

  • 这是函数指针数组的一个应用。函数指针数组的元素是函数形参同类型,返回值也同类型的函数指针,我们通过数组下标找到对应的函数指针,可直接调用函数。
  • 通常我们把这样的数组叫作转移表(《C和指针》中有所提及)。
七、(函数指针数组)指针 1.(函数指针数组)指针的定义
(函数指针数组)指针:本质是一个指针,指针存放着函数指针数组的地址。
给出一个函数,结合我们所学,请写出函数指针数组指针:
int Add(int x, int y) { return x + y; }

答案:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

用咱们上面说过的方法来写,这岂不是洒洒水的事情~
2.(函数指针数组)指针的使用 我们还是以Add函数,来说明(函数指针数组)指针的使用:
int Add(int x, int y) { return x + y; } int main() { int (*pa)(int ,int ) = Add; //函数指针pa int (*pArr[5])(int ,int ); //函数指针的数组pArr int (*(*ppArr)[5])(int ,int ) = &pArr; //(函数指针数组)指针ppArr return 0; }

八、回调函数 1.回调函数的定义 回调函数就是一个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
用图来解析:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

2.回调函数的使用 用刚才的 switch 版本的计算器为例:
void menu() { printf("*****************************\n"); printf("**1. add2. sub**\n"); printf("**3. mul4. 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))//Calc形参为函数指针 { 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语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

3.qsort函数(quick sort快速排序函数) 回顾冒泡排序(我们在C基础数组谈过):
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; }

我们实现的冒泡排序函数bubble_sort只能对整型数组排序,当我们想对其他数组或字符串等排序时,bubble_sort就显得很鸡肋。
因此,我们来了解一下在这方面无所不能的qsort函数:
说明:qsort函数是C库函数中的快速排序函数,可处理多种类型数组。
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

(注: 给大家推荐一个学C的网站——菜鸟教程)
为什么qsort函数可以处理多种类型数组、字符串、结构体等呢?
我们对比自己写的bubble_sort函数和qsort函数,可以从中找到答案:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

因为void* base指针和nitems(待排序数组的元素个数)、size(待排序数组的元素大小)可以描述出任意类型。
为什么将参数base的类型是viod*呢?
int a; char* pa = &a; //从int* 到char* 类型不兼容

确定类型的地址赋值给不同类型的指针会警告类型不兼容,强制转化还可能精度丢失。
而viod*:无(具体)类型,又称通用类型。它可以接收任意类型的指针,但无法进行指针运算(解引用,指针±整数等)
知道了待排序数组的元素个数、待排序数组的元素大小和遍历它们的指针void* base,但是按升序还是降序的顺序还得依靠一个比较函数。
这个比较函数指定元素的比较方式,而且需要我们自行定义,所以qsort可以说是一个半库函数半自定义函数。
compar函数:
  • elem1小于elem2,返回值小于0
  • elem1大于elem2,返回值大于0
  • elem1等于elem2,返回值为0
    (注:elem1,elem2:进行比较的两个元素的地址作参数。)
现在我们把qsort函数内裤都摸透了,那我们一起来用一用吧~
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

3.1.qsort函数的使用
qsort函数对整型数组排序:
int int_cmp(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2; //e1 - e2:升序//将void* 强转为int* //e2 - e1降序 } 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); qsort(arr, sz, sizeof(arr[0]), int_cmp); print_arr(arr, sz); return 0; }

运行结果:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

qsort函数实质是回调函数,int_cmp函数把地址传给qsort函数,qsort函数通过地址找到int_cmp函数。
qsort函数对结构体排序:
#include #include #include struct 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; //升序 return ((struct Stu*)e2)->age - ((struct Stu*)e1)->age; //降序 } int sort_by_name(const void* e1, const void* e2) { //return strcmp(((struct Stu*)e1) -> name, ((struct Stu*)e2) -> name); //升序 return strcmp(((struct Stu*)e2)->name, ((struct Stu*)e1)->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为降序
3.2.qsort函数的模拟实现
//打印函数 void print_arr(int arr[], int sz) { for (int i = 0; i < sz; i++) { printf("%d ", arr[i]); } printf("\n"); } //交换函数 void Swap(char* buf1, char* buf2, size_t size) { for (size_t i = 0; i < size; i++) { char tmp = *buf1; *buf1 = *buf2; *buf2 = tmp; buf1++; buf2++; } } //比较函数 int cmp(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2; } //排序函数 void my_bubble_sort(void* base, size_t num, size_t size, int(*cmp)(const void* e1, const void* e2)) { for (size_t i = 0; i < num - 1; i++) { for (size_t j = 0; j < num - 1 - i; j++) { if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)//以字节为单位 { Swap((char*)base + j * size, (char*)base + (j + 1) * size, size); } } } } 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); my_bubble_sort(arr, sz, sizeof(arr[0]), cmp); print_arr(arr, sz); return 0; }

运行结果:
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

  • 对于base + j * size,若basevoid*类型 ,语法不支持(指针±整数),而char*类型是任意类型中的最小单元。所以(char*)base + j * size可以确定元素地址。
  • Swap函数是以最小字节进行比较和交换,是代码具有普遍性。
指针进阶内容丰富,比较难理解,我们一定要多看多想多敲代码~
你听到了吗?经过我们跋山涉水,走过了千沟万壑,我们终于让C指针唱响了征服
最后,各位老铁看了文章,请给我点赞关注评论吧,你的支持是我坚持的动力~
……未完待续
C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉
文章图片

    推荐阅读