莫道君行早,更有早行人。— 出自《增广贤文·上集》
解释:别说你出发的早,还有比你更早的人。
这篇博客我们将会深入的理解数组传参和函数指针等指针,是非常重要的内容,学好这部分才能算真正学懂C语言。
目录
- 一维数组传参🍀
- 二维数组传参🐽
- 一级指针传参🦑
- 二级指针传参🐸
- 函数指针🥶
- 函数指针数组🐣
- 指向函数指针数组的指针🦄
- 总结😈
一维数组传参🍀
我们必须要对这方面理解透彻,才能让我们在写函数形参的类型时,有更清晰的理解
我们来看一段代码
#include <stdio.h>
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);
}
这个题目就是想问test 和test2这两个函数哪些形参是对的哪些又是错误的。
- 简单的分析
主函数里有一个整型数组 arr
和一个指针数组
存放整型指针,test(arr)实参是arr的地址
,test2(arr2)的实参是arr2的地址
。现在你可以试一试。
- 开始真正分析
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int *arr)//ok?
{}
前面两个个应该是属于我们还没有学指针时,形参的写法,这两种也都是对的。第三个是我们学习指针之后的写法,也是正确的。
但是我们还是要说明一下其中的知识点。
如果数组的实参是arr,数组的形参可以写成int arr[ ],int arr[10],或者int * arr。那么[10]和[ ]的区别在哪里呢?
- 实际上这两个没有区别,只是给人的观感不同,为什么呢?
- 我们知道将数组名arr传给形参,形参并不会创建一个数组,而只是用变量记录这个地址,
- 而int [ ]的本质其实就是int * arr,这个[]中写数字其实是没有意义的,因为我们不会再创建一个数组,因为实际上传的还是指针,也就是说数字可以随便写,比如[1000]但是呢?为了不然别人理解错误最好只写int arr[].
然后我们看剩下两个
void test2(int *arr[20])//ok?
{}
void test2(int **arr)//ok?
{}
int *arr2[20] = {0};
test2(arr2);
- 分析:
其实指针数组与一维数组是类似的,只是两个数组存储的元素类型不一样。
我们传参传的还是首元素的地址,首元素地址,可以用一样的类型来接收,就想上面我们说的一样。因为这里并不会创建一个数组,只是将数组首元素地址传过去。所以第一个就是正确的。其实我们也还可以想上面的例子一样,这样写
void test2(int *arr[200])
{}
- 那么第二个呢?我们知道arr2的首元素是int *,那么int *类型的地址应该用什么接收呢? 当然是二级指针,一级指针的地址肯定用二级指针来接收。
int a = 10;
int *p = &a;
int **pp = &p;
这样的例子应该会更容易理解。
二维数组传参🐽
咱们不要觉得数组传参都是一样的
我们来看看二维数组到底区别在哪。
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
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);
}
- 简单的分析
简单定义并初始化了一个数组名为arr的二维数组。然后将数组名传参。 - 开始真正分析
- 先从没有 * 号开始吧。
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
我们可以看到这三种不同的写法的区别就在于[ ][ ]中的数字到底是多少,
首先联想一下我们的一维数组传参,[ ]里可以写也可以不写。
那么二维数组就不一样了,我们必须传过去列,行则是传不传都可以,我们可以联想一下我们二维数组的初始化。
这样我们就能明白了,第一个和第三个是正确的。
- 然后我们看带*号的。
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就到了第二行的首元素地址。
void test(int *arr)//
{}
void test(int* arr[5])//
{}
void test(int (*arr)[5])//
{}
void test(int **arr)//
{}
第一个:肯定不行
,整型指针一般接收整型元素的地址,我这里可是二维数组的第一行的地址,即一个一维数组的整个数组的地址。
第二个:这是一个指针数组,肯定不行,因为类型都对不上,我这里是一个数组的地址,放到一个数组里面肯定不行
。起码你要是一个指针啊。
第三个:这个就是正确的
,为什么呢?数组指针指向的就是数组,而我们这里恰好可以看作一个一维数组,也就是说可以将这个一维数组的地址存放到arr这个变量中。
第四个:也是错误的
,为什么?二级指针用来接收一级指针的地址,这里完全不搭啊。
最后还有一个不常用的内容,大家看看就好。
void test1(int (*p)[5])
{}
void test2(int(*p)[3][5])
{
*p;
}
int main()
{
int arr[3][5];
test1(arr);//传递的第一行的地址
test2(&arr);//传递的是整个二维数组的地址,很少这样传
return 0;
}
一级指针传参🦑
上面我们已经解析过数组的传参接下来我们试试指针,我们先从一级指针开始:
#include <stdio.h>
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;
}
- 简单的分析
创建一个arr数组并初始化,让int * p 指向这个数组,p中存储的是arr这个数组的首元素地址,再传p变量也就是arr的地址。整型元素的地址用一级指针接收是正确的。
了解了这一部分,那么下一个问题。
思考: 当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
其实很简单,(以整型元素为例)只要你传的是一个整型元素的地址
就行。
void test(int* ptr)
{
//...
printf("%p\n", ptr);
}
int main()
{
int a = 10;
int* p = &a;
int arr[10];
test(arr);//数组
test(&arr[0]);
test(&a);//p是一级指针
test(p);
return 0;
}
这就是一级指针传参的内容。
二级指针传参🐸
上面我们介绍了一级指针传参,下面我们看看二级指针到底在传参时有什么不同。
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;
}
这里我们注意一下 ** ptr,我们解引用一次得到p的地址,p的地址再解引用一次就得到n 也就是10。
二级指针我们知道,是用来接收一级指针地址的(注意指针变量的地址不要与指针变量中存储的地址搞混淆),那么一级指针地址有几种表示方法呢?
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
这里就是答案。
思考: 当函数的参数为二级指针的时候,可以接收什么参数
void test(int** ptr)
{
printf("%p\n", **ptr);
}
int main()
{
int a = 10;
int* p = &a;
int** pp = &p;
test(&p);
test(pp);
int* arr[10] = { p };
test(arr);
test(&arr[0]);
return 0;
}
我们从二级指针接收一级指针的地址
作为突破口,
那么除了这两种还有一类。
test(&p);
test(pp);
那就是指针数组,指针数组的每个元素都是指针变量的地址,这样我们就能理解了。
函数指针🥶
我们都知道变量是有地址的,那么函数有地址吗?
肯定也是有点既然有地址那么就可以与指针有联系。
这就是我们将会介绍的函数指针。
我们来看一段代码:
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);//直接打印函数名
printf("%p\n", &test);//取函数的地址打印
return 0;
}
发现这两种写法其实是一样的。
那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
答:pfun1可以存放。
pfun1先和结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
我们已经学会了这么函数指针的一般表达方式,那么我们来尝试使用使用。
使用函数指针
int Add(int a, int b)
{
return (a + b);
}
int main()
{
int x = 5;
int y = 3;
printf("%d\n", Add(x, y));
int (*p1)(int, int) = Add;
int (*p2)(int, int) = &Add;
printf("%d\n", p1(5,3));
printf("%d\n", (*p2)(5, 3));
printf("%d\n", Add(5,3));
printf("%d\n", (&Add)(5, 3));
return 0;
}
我们看到这几种写法都是可以的,
printf("%d\n", p1(5,3));
printf("%d\n", (*p2)(5, 3));
printf("%d\n", Add(5,3));
printf("%d\n", (&Add)(5, 3));
p1和p2都是存储Add函数的地址,而函数名就相当于函数地址(p1 | p2 == Add),那么解引用实际上有没有都一样,哪怕你写(********p2)都行,并没有什么太大的意义。
我们已经学会了函数指针的写法和用法接下来我们出两个题目。
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
我们不要畏惧这种代码其实只要我们用心加上经验就能看出来。
开始分析
(*(void (*)())0)();
首先我们能看到void ( * )( )
这个函数指针和0
我们看到( * )
里面没有写变量,
那么说明这是一种类型,而0是什么呢?其实这里的0指的是0地址
(void ( * )( ))0
那么这一部分的意思就是将0地址的类型转换为
void ( * )( )
这就意味着0地址处放一个返回类型是void,无参的一个函数
现在我们可以把代码简化一下变成:( * 0)( );
也就是说调用0地址处的函数刚好也
与我们void ( * )( )
相对应,只是我们不需要再写返回值,只需要写参数
这里刚好无参
这就是在表达调用0地址处的这个函数
void (*signal(int , void(*)(int)))(int);
我们还是先从内部找我们认识的,(int, void( * )(int))
这是一个函数的参数
第一个参数是int
,第二个参数是一个函数指针
那么signal
就应该是函数名
我们去掉这部分再观察void ( * )(int);
我们看到只剩下这一部分,还是一个函数指针类型,那这
是什么意思呢?其实是函数指针的返回值的表达形式
也就是说signal函数的返回值类型
也是:void( * )(int)的函数指针
这就是我们所讲解的函数指针类型。
函数指针数组🐣
上面我们讲解了函数指针,现在我们来看看函数指针数组又不同在哪一点
我们知道指针数组是这样的:
int *p[5] p先和[]结合,说明是数组然后,int * 为类型
那么函数指针数组呢?
其实只需要把类型变换一下
int (*parr[5])(int ,int)
parr1 先和 [ ] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。
然后我们看一下到底如何使用
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 (*parr[4])(int, int) = { Add,Sub,Mul,Div };
printf("%d\n", (parr[0](4, 2)));
printf("%d\n", (parr[1](4, 2)));
printf("%d\n", (parr[2](4, 2)));
printf("%d\n", (parr[3](4, 2)));
return 0;
}
其实函数指针数组的用途:转移表
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 };
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;
}
为什么要这样呢?
如果我们不使用转移表那么就会写出多个case
,这样做可以减少代码的冗余,简单明了。
指向函数指针数组的指针🦄
这也是我的博客中指针类型的最后一种,我们看到标题的最后是什么什么的指针,那么说明就是一个指针只不过类型复杂了那么一点。
int Add(int x, int y)
{
return x + y;
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7};
int(*p)[10] = &arr;//p得是数组指针
int* arr2[5];
int* (*p2)[5] = &arr2;//p2则是指针数组
//函数指针
int (*pf)(int, int) = &Add;
//函数指针数组
int (* pfarr[4])(int, int);
int (* (*p3)[4])(int, int) = &pfarr;
//p3是一个指向函数指针数组的指针
return 0;
}
注意指向函数指针数组的指针的写法。
(*p3)是指针然后再将类型往外面套就行了。
int ( * [4])(int, int)
= &pfarr;也就是这一部分。
接下来看看应该如何使用呢?
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 (*parr[4])(int, int) = { Add, Sub, Mul, Div };
int (*(*p)[4])(int, int) = &parr;
for (int i = 0; i < 4; i++)
{
int ret = (*p)[i](4, 2);
printf("%d\n", ret);
//int ret = p3[0][i](3, 4);
//printf("%d\n", ret);
}
return 0;
}
这便是如何使用。
总结😈
我们在这篇博客中详细的介绍了详数组传参指针传参的方法和细节,以及一些不太常用但是你必须要知道的一些指针类型,貌似在操作系统开发上用的比较多。完结。