上一篇:
从0开始学c语言-27-字符指针,指针数组和数组指针_阿秋的阿秋不是阿秋的博客-优快云博客
如若没有上边这篇链接内容的基础,可能会看不懂。
目录
数组参数、指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
接下来这些代码展示了什么样的可以,什么样的不可以。
一维数组传参
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[][])//err
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int* arr)//err
{}
void test(int* arr[5])//err
{}
void test(int(*arr)[5])//ok
{}
void test(int** arr)//err
{}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
一级指针传参
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; }
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?答案:同类型的一级指针,比如char*可以接收char[ ]数组的数组名。
(不知道答案为什么是这样的,翻到文章顶部,有链接解释)
二级指针传参
void test(int** ptr) {
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
//以下这两种传参方式都可以
test(pp);
test(&p);
return 0; }
思考以下代码的传参对不对?
void test(char **p) {
}
int main()
{
char* arr[10];
test(arr);//Ok?
return 0; }
答案:OK。
arr的类型是char*[10],指向10个char*,本质上是char**指针。
函数指针
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
他们的输出结果(地址)是一样的,和数组名不一样的是,
函数名 == &函数名,而 数组名!= &数组名 。
我们监视看一下类型。
根据我们监视的结果,请看看接下来这段代码,哪个会有能力存放函数的地址呢?
void (*pfun1)();
void *pfun2();
是第一个哦,它的类型是void(*)(),是一个函数指针。
意思是说这个函数指针指向的函数,函数返回类型是void,参数没有。
而第二个, 它的类型是void*(),是一个返回类型是void*的函数。
这段代码也告诉我们,如果不加()把*和指针变量括起来,那么就会先和()结合,解读成一个函数。
有趣的代码
(*(void (*)())0)();
猜猜这段代码的意思是?
如图:
void (*signal(int , void(*)(int)))(int);
看看这个代码的意思是?
所以
void (*signal(int , void(*)(int)))(int);
也可以写成
void(*)(int) signal(int,void(*)(int));
简化一下代码,
typedef void(*pfun_t)(int);
//把void(*)(int)函数指针重定义为pfun_t
那么
void(*)(int) signal(int,void(*)(int));
就可以写成
pfun_t signal(int, pfun_t);
函数指针数组
数组是一个存放相同类型数据的存储空间,
把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];
根据这两个标志能很快判断出来类型。
parr1 先和 [ ] 结合,说明 parr1是数组名,数组指向 int (*)() 类型的函数指针,数组名本质是int (*)() *指针,因为指向10个nt (*)() 类型的函数指针,故parr1的类型是int (*)() [10]。
函数指针数组的用途:转移表
这是一个简答的计算器代码,分别写了加减乘除四种算法的函数,主函数用do……while循环配合switch分支来选择不同的计算方法。
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int div(int a, int b) {
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
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(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
*p[5]其实就相当函数名,类型是int(int x,int y),函数返回类型是int,函数参数是int x和int y,这是一个函数。
p先和[5]结合,*p[5]就相当于*(p[5]),
p[5]的类型就是在*p[5]类型中间加(*),就像这样int(*)(int x,int y),这是一个函数指针。
p的类型就是在p[5]的类型后面加[5],就是这样int(*)(int x,int y)[5],这是一个函数指针数组。(实际上p是指向int(*)(int x,int y)的 int(*)(int x,int y)* 指针)
当我们调用函数指针数组中的函数指针的时候,首先需要找到对应的数组下标,因为我们设置的菜单分别是1 2 3 4 来选择功能,便加了个0来让下标对上。
比如我们现在要用加法,下标是1,那么就是p[1],p[1]是一个int(*)(int x,int y)的函数指针,要达到我们调用函数的目的还需要加上*来解引用才能完成调用,*p[1],然后加上后面的函数参数,就像下面这样,注意一定要用()来把*p[1]整体括起来。
(*p[1])(x, y)
但是你有没有想过一个问题,观察一下这个表达式
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
我们说p的类型是int(*)(int x,int y)[5],指向5个int(*)(int x,int y),那后面初始化的类型也应该是int(*)(int x,int y)这个函数指针才对,为什么却直接写了函数呢?
实际上我们前面提到过 函数名就等于&函数名 ,所以函数指针当中的(*)实际上就是个摆设而已,没有实际作用,为的就是让我们好区分而已。
紧接着,我们把switch语句换成了if语句
if ((input <= 4 && input >= 1))
{
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y); //调用函数指针
//input相当于函数指针数组中的下标
}
else
printf( "输入有误\n" );
printf( "ret = %d\n", ret);
完整的代码就是这样
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int div(int a, int b) {
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
while (input)
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf("输入有误\n");
printf("ret = %d\n", ret);
}
return 0;
}
指向函数指针数组的指针
void test(const char* str) {
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0; }
分析一下怎么写出来的 ,我们要写 指向 函数指针 数组 的 指针
也就是说有一个指向 函数指针数组 的指针,这个函数指针数组中的元素都是函数指针。
那就先需要写一个函数指针,函数指针指向的是函数,就需要先写个函数,
test是函数名,它的类型是void(const char*),返回类型是void,函数参数是const char*。
void test(const char* str) {
printf("%s\n", str);
}
现在写一个函数指针指向test函数,我们可以把*pfun看做函数名,而*pfun中的*代表的意思就是pfun是一个函数指针变量,而这个pfun函数指针指向的函数返回类型是void,函数参数是const char*。
void (*pfun)(const char*) = test;
然后写一个函数指针数组(存放)指向5个同一类型的函数指针。我们直接在*pfunArr后面加上[5]来表示数组,因为*pfunArr[5]会相当于*(pfunArr[5]),而
*pfunArr[5]的类型是void(const char*)函数,那么
去掉*后的pfunArr[5]类型是void(*)(const char*)函数指针,再
去掉[5]后的pfunArr类型是void(*)(const char*)[5]类型,是指向 5个void(*)(const char*)函数指针 的函数指针数组。
最后把pfunArr中的第一个元素初始化,这里想想为什么不是=pfun函数指针,而是=test函数本身呢?
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
因为函数名就等于&函数名,这就说明其实&函数名类型中的(*)其实是个摆设,没有实际作用,只是为了方便我们人为区分而已。
实际上你选择=pfun函数指针 来初始化也是一样的效果。
(或者更加离谱一些,函数本身就是指针,指向了函数返回类型和函数参数。)
终于到了我们的目的地,要写一个 指向 函数指针数组 的指针 ,而被指向的 函数指针数组 已经写出来了,现在只需要一个指向这个对象的指针。
那当然是把(*pfunArr[5])中的 pfunArr函数指针数组名 换个名字加上*再整体括起来后,ppfunArr就代表指向void(*)(const char*)[5]了。
此时 *ppfunArr 的类型是void(*)(const char*)[5]
去掉*后的ppfunArr类型是void(*)(const char*)[5] *,指向void(*)(const char*)[5] 的指针。
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
最后进行初始化等于&pfunArr。
回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。 回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
相当于高中学的复合函数,f(u(x)),将u(x)替换x。
qsort函数的使用

qsort函数是用来给数组排序的,返回类型为void(也就是说没有返回值),函数参数共有4个。
base就是需要排序的数组,
num是需要排序的元素个数(因为传递过来的数组是首元素的地址,必须要有个数才能确定排序几个元素)
size是指一个元素占几个字节的意思,(因为每个数组存放数据类型不同,char是一个字节,int是4个字节)
compare是指向返回类型为int,函数参数为两个const void*的函数指针。
int compar (const void* p1, const void* p2);

如上图,如果compare函数的返回值<0,那么p1元素会放到p2元素后, >0则相反,=0说明p1=p2
现在演示它的使用方式
#include <stdlib.h>
int int_cmp(const void * p1, const void * p2) {
return (*( int *)p1 - *(int *) p2);
//强制类型转换,把指针能够访问的字节变为4byte
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0; }
输出结果也达到了我们的目的,如果想升序的话,思考一下cmp函数中应该怎么写呢?
我的答案的是乘-1。

模仿qsort函数进行冒泡排序
int main()
{
test();
return 0;
}
然后在test函数中写好我们需要的大体框架。
test()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
//调用冒泡排序函数
bubble(arr,sz,sizeof(arr[0]),cmp_arr);
//打印arr数组
print(arr,sz);
}
把bubble函数需要的四个参数都确定好,然后写个print函数来打印排序后的arr数组。
我先写了print函数
void print(int* arr,int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(arr + i));
}
}
对比 使用bubble函数 和 接收函数参数
为了具有通用性,所以我们选择了空指针来接收,其他的没啥好解释了。
bubble(arr,sz,sizeof(arr[0]),cmp_arr);
void bubble( void* arr, int sz,int width, int(*cmp)(const void* e1, const void* e2) )
然后在bubble函数中写好冒泡排序的大体框架
void bubble( void* arr, //需要排序的数组中第一个元素的地址
int sz, //需要排序的元素个数
int width, //一个元素的大小,单位:字节
int(*cmp)(const void* e1, const void* e2)
//函数指针,指向了 (比较2个元素) 的函数
)
{
int i = 0;
int j = 0;
//趟
for (i = 0; i < sz; i++)
{
//一趟比较
for (j = 0; j < sz - 1 - i; j++)
{
}
}
}
现在需要确定的是,我们该如何确定一趟比较中应该 交换元素的条件。
这就需要我们的 cmp函数 了,专门比较两个元素大小并返回值。
前面我们知道的 qsort函数 利用 cmp函数 传回来的三种不同范围的值来确定是否进行交换,怎么交换。
现在我们模仿 qsort函数 来写一下我们需要 用cmp函数 传回来的值干些什么。
先写一下 cmp函数,其实和 qsort函数 中的 cmp函数 差不多,返回两个元素相减的值(也就是判断谁大谁小)就可。
int cmp_arr(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
//空指针不能进行解引用,所以
//要进行强制类型转换
}
因为我们打算排序成一个升序的数组,那么,如果cmp返回一个<0的值,便需要进行交换,也就是说,需要在 确定一趟比较中应该 交换元素的条件 中调用cmp函数把返回值带回来,那便需要确定传递过去什么参数。
我们在学习qsort函数的时候知道cmp函数是比较两个元素的,而冒泡排序中是根据arr[ j ]和arr[ j+1]来比较进行数字的交换,同样的道理我们可以把这两个参数传到cmp函数中进行比较。
但是需要知道的是,我们bubble接收数组的指针是空指针,不能进行向后挪动的操作来访问下一个元素,前面我们设置的四个参数中有表示 一个元素大小 的参数——width,它的单位是字节,为了方便移动,我们需要把空指针接收的数组首元素地址强制类型转换为(char*),因为char*的步长是一个字节,那么在 j变量乘上width变量后便可以表示 我们需要排序的数组的 一个元素大小的步长了。当然,随着j变量每次循环的增长,移动的步数也是紧挨着的,符合我们的需求。
总之:1· char*强制类型转换 2·确认好步长
if ( cmp ( (char*)arr + j * width , (char*)arr + (j + 1) * width ) < 0)
那么满足条件后便需要进行交换,这个交换函数很简单,直接上代码了
void swap( int width, char*a,char*b)
{
//一个元素大小的交换
int i = 0; //挪动几个字节(width
for (i = 0; i < width; i++)
{
char tmp = *a;
*a = *b;
*b = tmp;
a++;
b++;
}
}
需要传递的函数参数就交给你自己思考了哦!
是步长和两个需要交换的元素。(属于是字节为单位的交换)
现在给上完整的代码
//模仿qsort函数使用冒泡排序
int cmp_arr(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
//空指针不能进行解引用,所以
//要进行强制类型转换
}
void swap( int width, char*a,char*b)
{
//一个元素大小的交换
int i = 0; //挪动几个字节(width
for (i = 0; i < width; i++)
{
char tmp = *a;
*a = *b;
*b = tmp;
a++;
b++;
}
}
void bubble(void* arr, //需要排序的数组中第一个元素的地址
int sz, //需要排序的元素个数
int width, //一个元素的大小,单位:字节
int(*cmp)(const void* e1, const void* e2)
//函数指针,指向了 (比较2个元素) 的函数
)
{
int i = 0;
int j = 0;
//趟
for (i = 0; i < sz; i++)
{
//一趟比较
for (j = 0; j < sz - 1 - i; j++)
{
//void*是空指针,不能解引用 也不能 向前或者向后走
//因为你不知道要访问几个字节,但是你可以往里面放任意类型的地址
if ( cmp ( (char*)arr + j * width , (char*)arr + (j + 1) * width ) < 0)
{
//交换
swap( width, ((char*)arr + j * width), ((char*)arr + (j + 1) * width));
//传地址,让函数内外产生联系
}
}
}
}
void print(int* arr,int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(arr + i));
}
}
test()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
//调用冒泡排序函数
bubble(arr,sz,sizeof(arr[0]),cmp_arr);
//打印arr数组
print(arr,sz);
}
int main()
{
test();
return 0;
}
如果还想优化,就考虑我们之前讲的assert和const来修饰一下吧。
(头文件我没给,不知道的自己查一下,https://cplusplus.com/reference/)
现在你可以看看和我们之前写的冒泡排序代码有何不同。给上链接。
从0开始学c语言-16-数组以及数组传参应用:冒泡排序_阿秋的阿秋不是阿秋的博客-优快云博客
结构体调用qsort函数
同样的道理,我们试试结构体。
首先创建一个结构体
struct Stu
{
char name[20];
int age;
//结构体大小=20个char + 一个int
};
这是一个tag为Stu的结构体,结构体大小为24byte。
接下来写主函数和test函数
void test()
{
struct Stu s [3]= { { "aqiu",9 } ,{"aka",56},{"as",4} };
int sz = sizeof(s) / sizeof(s[0]);
}
int main()
{
test();
return 0;
}
在test函数中我们进行了结构体数组的初始化,并且计算了结构体数组的元素个数,这些都是为了使用qsort函数。
qsort(s, sz, sizeof(s[0]), cmp_age);
s代表结构体,sz代表结构体数组的元素个数,sizeof(s[0])代表一个结构体的大小,cmp_age是我们需要调用的函数,用来比较两个元素的大小并带回来一个返回值。
cmp_age函数的返回类型和函数参数类型都没变化,和我们上面写的不太一样的地方是,访问两个元素的方式不太一样了,因为我们是结构体元素的比较,便需要转换为结构体指针来访问结构体成员并且比较。
int cmp_age(const void* e1, const void* e2)
{
return (((struct Stu*)e1)->age) - (((struct Stu*)e2)->age);
}
好了,调用结束。
现在我们试试比较名字来排序的cmp函数怎么写,其实道理一样的,只不过我们比较名字就是在比较字符串,比较字符串又需要使用比较字符串的函数strmp。
int cmp_name(const void* e1, const void* e2)
{
return strcmp( ( (struct Stu*)e1 )->name, ( (struct Stu*)e2 )->name);
//很容易括号出问题
}
同样的道理,我们把自己上面写的bubble函数中的cmp函数换成cmp_age或者cmp_name函数也可以进行结构体排序。
完整的代码太长了,bubble函数的代码就在上面,这里只给上结构体的完整代码。
#include <string.h>
#include <stdlib.h>
struct Stu
{
char name[20];
int age;
//结构体大小=20个char + 一个int
};
int cmp_age(const void* e1, const void* e2)
{
return (((struct Stu*)e1)->age) - (((struct Stu*)e2)->age);
}
int cmp_name(const void* e1, const void* e2)
{
return strcmp( ( (struct Stu*)e1 )->name, ( (struct Stu*)e2 )->name);
//很容易括号出问题
}
void test()
{
struct Stu s [3]= { { "aqiu",9 } ,{"aka",56},{"as",4} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_age);
/*bubble(s, sz, sizeof(s[0]), cmp_age);*/
//bubble(s, sz, sizeof(s[0]), cmp_name);
}
int main()
{
test();
return 0;
}