文章目录
- 前言
- 一、字符指针
-
- 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基础部分已经讲过,铁汁们可以复习一下传送门,看完记得回来啊~
指针大小:32位平台下占4个字节,64位平台占8个字节。
指针类型:类型决定指针±
整数的步长和指针解引用时访问的大小。
指针运算:指针解引用,指针±
整数,指针-指针,指针的关系运算。
指针诞生:
文章图片
理解一遍后,就让我们正式起航扬帆吧!乌拉~
注: 我们习惯把指针变量叫作指针,本文指针本质是指针变量
一、字符指针 1.字符指针的定义 字符指针: 指向字符的指针,类型为
char*
2.字符指针的作用
- 指向单个字符变量
char ch = "w";
char* pch = &ch;
- 指向字符串首字符
char* pc = "hello";
printf("%s\n",pc);
看图说话:
(为了理解简单,地址用201、204表示)
文章图片
【C语言进阶|【详解C语言指针】我真的让C指针给我唱征服了~乌拉】①比较好理解,对于上面的解析,我们可以进行验证:pch
存有ch
的地址,因此可通过解引用操作访问ch
②并不是把字符串"hello"
放进指针,而是把字符串首字符的地址放进指针,通过首字符地址,可以找到整个字符串
文章图片
为什么靠一个首字符的地址就可以找到整个字符串呢?
下面我们来证明一下:
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
运行结果:
文章图片
3.字符指针的特点 这是一道题面试题:
- 字符串中每1个字符占1个字节,且内存的单元是1个字节,为了方便管理,字符串在内存空间上是连续存放的
即hello是连续的%s
:输出字符串,从所给地址开始,一直打印到\0
结束符号(不包括'\0'
)
#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;
}
运行结果:
文章图片
数组名是数组首元素的地址;指针存有字符串首字符的地址我们可以做出分析:
两者有异曲同工之妙,所以
(str1 == str2) ,表示比较存放相同字符串的两个数组的地址是否相同;
(str3 == str4),表示比较存放相同字符串的两个指针的值是否相同
1.str1[]和str2[]是字符数组,在内存上会开辟两块地址不一样空间,但存放相同的内容注: 常量区的常量不可修改,通常用"hello bit"
2.str3和str4是指向常量字符串的指针,而常量字符串存放在内存的常量区,常量区特点是常量值不可被修改且有唯一性(就没有存在2份或多份的必要),所以指针指向的是同一份数据,故地址是相同的
图解:
文章图片
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];
数组的数组名是首元素的地址:2.指针数组的使用 整型指针数组的使用:
整型数组的数组名arr
,是首元素(整型)的地址,所以arr
是一级指针。
整型指针数组的数组名parr
,也是首元素(整型指针)的地址,所以parr
是二级指针。
#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;
}
运行结果:
文章图片
通过指针数组访问整型数组的每一个元素字符指针数组的使用:
parr[i][j]
等价于*(*(parr + i) + j)
文章图片
#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");
}
}
运行结果:
文章图片
三、数组指针 1.数组指针的定义 我们已经知道:pch[i]
是字符串首字符的地址,%s
:打印字符串
字符指针:指向字符的指针
整型指针:指向整型的指针
C语言语法是有规律性的,也就是说,指针指向的类型<==>指针的类型(相互决定)
所以我们可以类比得:
数组指针:指向数组的指针,即数组的地址存放在数组指针中。
到此,我要说一下,指针部分会出现一些复杂的类型,那如何理解一个复杂的类型? |
类型显得复杂的原因是它由多种的运算符组成,当我们分清运算符的优先级,理清顺序,自然会柳暗花明又一村~
1.运算符主要有三种,它们的优先级是:下面我们一起来探索一下吧!* < [ ] < ( )
2.变量名第一次与运算符结合就决定了它的本质。(变量名的处女情结?_?)
如,先与*
结合是指针;先于[ ]
结合是数组;
( )
:1.先于( )
结合是函数,即p( )
2.用于改变优先级,如(*p)
int* p[10];
int(*p)[10];
int p(int);
int (*p)(int);
int *(*p(int))[10];
2.细说指针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.1.指针类型
上一章说过,去掉名字就是类型。
同理,若把指针声明语句中的指针的名字去掉,剩下部分就是该指针的类型。
指针类型的意义(C基础篇已经讲过):int* ptr;
//指针的类型是int*
char* ptr;
//指针的类型是char*
int** ptr;
//指针的类型是int**
int (*ptr)[5];
//指针的类型是int()[5]
int* (*ptr)[10];
//指针的类型是int(*)[10]
1.指针解引用访问几个字节(访问的内存空间大小)
2.指针类型决定了指针±整数跳过几个字节(步长)
2.2.指针所指向的类型
指针所指向的类型决定了编译器看待指针指向内存区的内容的方式
若把指针声明语句中的指针的名字和名字左边的指针声明符号
*
去掉,剩下部分就是指针所指向的类型。通过观察我们可以发现:int* ptr;
//指针所指向的类型是int
char* ptr;
//指针所指向的类型是char
int** ptr
; //指针所指向的类型是是int*
int (*ptr)[5];
//指针所指向的类型是int()[5]
int* (*ptr)[10];
//指针所指向的类型是int()[10]
二者可互推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;
}
运行结果:
文章图片
把数组名传给函数时,我们有两种接收方式:当数组指针用来接收并访问一维数组时:
①数组接收,编译器会将数组退化为指针
②指针接收
因为数组名是数组首元素的地址,用指针接收是正确的。
#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基础数组传送门
连续存储:1.每一行内部的元素连续存放 2.行与行之间连续存放
文章图片
- 因此,二维数组的数组名是首行的地址,类型是
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;
}
文章图片
实参为整个数组的地址,形参是数组指针,
*(pa+i)
解一层引用后,打印出来的为什么还是地址呢?四、数组传参和指针传参
&
是取地址运算符,*
是间接运算符。&
和*
互为逆运算- 则
*&x
的含义是,先获取变量 x 的地址,再获取地址中的内容,相当于抵消- 所以
&arr
传给pa
,执行*pa
,相当于*&arr
,实质就是arr
,还是整个数组的地址。- 整个数组的地址的类型是
int(*)[10]
,即指针pa
的类型是int(*)[10]
- 由指针类型决定指针±整数的步长可知,每个地址间相差40字节。
写代码时要把数组和指针传递给函数的情况在所难免,那函数参数该如何设计呢? |
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);
}
以上的接收方式都合理分析:
文章图片
数组作为函数形参,会退化为指针。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基础数组传送门。
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;
}
分析:
文章图片
综上,我们可以总结出二维数组传参时,函数形参的两种设计方法:另外,还有一种方式:形参声明为指针的指针
①形参给出第二维(列)的长度
②形参为指向数组的指针
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;
}
函数形参为二级指针时,实参可以是二级指针(一级指针的地址),也可以是指针数组的元素的地址(特殊的为数组名,因为它是数组首元素的地址嘛)。
我们为什么要学习如何设计实参和函数形参? |
2.根据函数的目的,来设计函数形参。
五、函数指针 1.函数指针的定义
类比得,函数指针:指向函数的指针,它存放着函数的地址。没错,函数也是有地址的,可以看下面一组代码:
文章图片
表示函数地址的方式:注:这里需要与数组名区别开来:
- 函数名
- &函数名
- 函数名 == &函数名 (都是函数地址,相同意义)
- 数组名 != &数组名 (不同意义)
其实很简单,上面我们学习了如何去理解一个负责的类型,相信我们也可以自己写出来:
//以Add函数为例
int (*pf)(int, int) = &Add;
解析:
文章图片
自我检测:如何保存
viod test(char* str){}
的地址答案:3.函数指针的使用 计算机硬件程序经常通过调用地址的方式来调用函数,因此需要使用函数指针调用函数。void (*pt)(char*) = &test;
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指针和陷阱》 )
文章图片
因为pf = Add;
所以pf(2,3)
相当于Add(2,3)
,但pf是函数指针。
(*pf)(2,3)
中的*
并没意义,可加可不加。
代码1:
(*(void (*)())0)();
解析:
文章图片
代码2:
void (*signal(int, void(*)(int)))(int);
解析:
文章图片
或者我们可以通过类型重定义来理解:
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;
}
2.函数指针数组的使用 实现一个简易计算器,来进行加减乘除运算。
- 类型相同的函数指针,放在同一个数组中。
- 数组去掉数组名和
[ ]
,剩下部分就是数组元素类型。pfArr
的元素类型为int(*)(int,int)
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;
}
运行并测试发现:
文章图片
将我们对main主函数部分修改:input = 0
和input = 5
放进程序就很容易发现问题所在。
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;
}
运行并测试
文章图片
经测试,虽然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;
}
运行并测试成功:
文章图片
七、(函数指针数组)指针 1.(函数指针数组)指针的定义
- 这是函数指针数组的一个应用。函数指针数组的元素是函数形参同类型,返回值也同类型的函数指针,我们通过数组下标找到对应的函数指针,可直接调用函数。
- 通常我们把这样的数组叫作转移表(《C和指针》中有所提及)。
(函数指针数组)指针:本质是一个指针,指针存放着函数指针数组的地址。给出一个函数,结合我们所学,请写出函数指针数组指针:
int Add(int x, int y)
{
return x + y;
}
答案:
文章图片
用咱们上面说过的方法来写,这岂不是洒洒水的事情~
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.回调函数的定义 回调函数就是一个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
用图来解析:
文章图片
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函数。3.qsort函数(quick sort快速排序函数) 回顾冒泡排序(我们在C基础数组谈过):
把所需要用的函数地址传给Calc函数,Calc函数通过传进来的地址,找到所需要用的函数。
图解:
文章图片
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的网站——菜鸟教程)
为什么qsort函数可以处理多种类型数组、字符串、结构体等呢? |
bubble_sort
函数和qsort
函数,可以从中找到答案:文章图片
因为
void* base
指针和nitems
(待排序数组的元素个数)、size
(待排序数组的元素大小)可以描述出任意类型。为什么将参数base的类型是viod*呢? |
int a;
char* pa = &a;
//从int* 到char* 类型不兼容
确定类型的地址赋值给不同类型的指针会警告类型不兼容,强制转化还可能精度丢失。而viod*:无(具体)类型,又称通用类型。它可以接收任意类型的指针,但无法进行指针运算(解引用,指针±整数等)
知道了待排序数组的元素个数、待排序数组的元素大小和遍历它们的指针
void* base
,但是按升序还是降序的顺序还得依靠一个比较函数。这个比较函数指定元素的比较方式,而且需要我们自行定义,所以qsort可以说是一个半库函数半自定义函数。现在我们把qsort函数内裤都摸透了,那我们一起来用一用吧~
compar
函数:
- elem1小于elem2,返回值小于0
- elem1大于elem2,返回值大于0
- elem1等于elem2,返回值为0
(注:elem1,elem2:进行比较的两个元素的地址作参数。)
文章图片
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;
}
运行结果:
文章图片
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;
}
3.2.qsort函数的模拟实现e1 - e2
为升序;e2 - e1
为降序
//打印函数
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;
}
运行结果:
文章图片
指针进阶内容丰富,比较难理解,我们一定要多看多想多敲代码~
- 对于
base + j * size
,若base
是void*
类型 ,语法不支持(指针±整数),而char*
类型是任意类型中的最小单元。所以(char*)base + j * size
可以确定元素地址。- Swap函数是以最小字节进行比较和交换,是代码具有普遍性。
你听到了吗?经过我们跋山涉水,走过了千沟万壑,我们终于让C指针唱响了征服
最后,各位老铁看了文章,请给我点赞关注评论吧,你的支持是我坚持的动力~
……未完待续
文章图片
推荐阅读
- 《C陷阱与缺陷》读书笔记--第一章语法陷阱1
- C语言|C语言两种方法计算一个数所有位上的数的总和
- C语言|C语言实现简单计算器
- 蓝桥杯|蓝桥杯第十届C语言b组——B: 年号字串
- 蓝桥杯|蓝桥杯.颠倒的价牌(暴力枚举)
- 蓝桥杯|萌新打卡 蓝桥杯 算法基础 暴力枚举——立方和等式 暴力大法好
- 每日刷题———蓝桥杯真题|蓝桥杯2018第九届C语言B组省赛总决赛习题题解——习题A.换零钞(暴力枚举法)
- 蓝桥杯算法训练|蓝桥杯算法训练 数字游戏 C语言实现
- 笔记|第十二届蓝桥杯——Java软件开发(省赛)(括号序列(笔记15))