目录
前言
一、数组和指针的参数
1.一维数组传参
2.二维数组传参
3.一级指针传参
4.二级指针传参
二、函数指针
1.函数的地址
2.函数指针的形式
3.函数指针的使用
三、加深理解,两段有趣的代码
前言
之前的一篇文章讲到了指针的概念、指针和数组的关系,以及它们的各种组合。今天来补上上一篇文章挖的大坑,来详细介绍一下指针和数组的传参问题,以及函数、指针与数组的关系和组合。
需要看上篇文章的请跳楼👇
指针太难?手把手教你理解指针(指针?数组?)
一、数组和指针的参数
在写代码时,我们会遇到将数组和指针当作参数传给函数的情况。那么,数组或者指针如何传参呢?函数的指针如何设计呢?
1.一维数组传参
- 与其他变量一样,数组在传参时也可以在函数中创建临时拷贝,将数组的值传给函数(传值调用),如下:
#include <stdio.h>
void test(int arr[])
{}
void test(int arr[10])
{}
int main()
{
int arr[10] = { 0 };
test(arr);
}
- 与其他变量一样,数组传参也可以让函数接收数组的地址,通过地址对数组进行操作(传址调用),如下:
#include <stdio.h>
void test(int* arr)
{}
int main()
{
int arr[10] = { 0 };
test(&arr);
}
但是与其他变量不同的是,数组名可以表示首元素地址,直接传数组就可以用指针接收
- 对于指针数组,与上面两种方式一样,这里直接列出:
#include <stdio.h>
void test2(int* arr[20])//传值
{}
void test2(int** arr)//传址
{}
int main()
{
int* arr2[20] = { 0 };
test2(arr2);
test2(arr2);
}
需要注意,这里数组元素是指针,指针的地址要用二级指针来接收。
2.二维数组传参
二维数组和一维数组一样,也可以通过传值、传址两种方式传参。
首先是传值调用,如下:
void test(int arr[3][5])
{}
void test(int arr[][5])
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
但是,需要注意,
二维数组传参,函数形参的设计只能省略第一个“[ ]”中的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
其次是传址调用,如下:
void test(int (*arr)[5])
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
这里就已经和前面有所区别了。因为二维数组的数组名虽然也是首元素的地址,但是这个首元素其实是一整个数组。上面的二维数组相当于有三个元素,每个元素都是一个含有五个元素的数组。所以在传址时,要用数组指针来接收。
思考一下能不能这样?为什么?
void test(int *arr)//ok?
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
答案是不能。因为二维数组传递的参数是一整个数组的地址,不能用普通指针接收,只能用数组指针接收。
思考一下能不能这样,为什么?
void test(int **arr)//ok?
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
答案是不能。因为二级指针用来存放一级指针的地址,不能存放数组的地址。想要传数组的地址,还是要用数组指针。
3.一级指针传参
这里举一个一级指针传值调用的例子,如下:
#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;
}
这里思考一下,如果一个函数参数部分是一级指针,函数能接收什么样的参数?
举个例子:
void test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?
分析一下很容易得出,参数部分是一级指针时,函数可以接收不同元素的地址,也可以接收数组首元素地址,还可以接收一级指针的值(注意不是传址,传址就要用二级指针了)。
4.二级指针传参
二级指针作为参数,就可以接收一级指针的地址了。二级指针可以接收二级指针的值、接收一级指针的地址、还可以接收指针数组的首元素地址。例如:
void test(char** p)
{
}
int main()
{
char c = 'b';
char* pc = &c;
char** ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);
return 0;
}
二、函数指针
1.函数的地址
函数在创建时,会占用栈空间,因此函数也有它自己的地址,可以使用函数指针接收。如果你不敢相信,请看下面的代码:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
打印结果如下:
结果是两个地址。这样我们就很好的证明了,函数也是有地址的,而且函数名就可以表示函数的地址,&函数名也可以表示。
2.函数指针的形式
因此,想要存放函数的地址,我们就需要一个函数指针。还记得数组指针是什么形式吗?它长这样:
void (*parr)[10]
那么,我们也可以很容易推测出,函数指针的形式,它长这样:
void (*pfun)()
还是和上一篇一样的理解方式,pfun是指针,解引用之后相当于函数名,其余形式和函数一样。
3.函数指针的使用
从上面函数的地址处的示例可以发现,函数名本身就等同于函数的地址。也就是说,函数指针可以直接当作函数名来使用。如下:
void func()
{}
void (*pfunc) ();
pfunc = &func;
pfunc();//函数指针可以直接调用函数
当然,也可以通过解引用来使用,如下:
(*pfunc)();
三、加深理解,两段有趣的代码
学习了数组指针、指针数组、函数指针,我们就可以尝试理解一下下面的两段代码:
//代码1
(*(void (*)())0)();
这是什么意思呢?别着急,我们从里向外依次解剖。
从最里面有内容的括号开始,即 void (*) () ,假如说我在“*”后面放一个p,那么p是不是就是一个函数指针呐?但是现在把p去掉,剩下的void(*)() 就是一个函数指针类型。就比如 int n 表示定义一个整形变量n,去掉n,剩下的int就是整形。函数指针也一样。void(*p)()定义一个函数指针p,去掉p,剩下的void(*)()就是函数指针类型。函数指针类型加了括号,后面有一个0,也就是说把0这个数字强制类型转换,把它变成一个地址0x00000000,然后前面一个“*”表示解引用,也就是访问0x00000000这个地址处存放的函数。*(void (*)())0 就相当于函数名,后面再跟括号,就是说这个函数已经要被调用了。整个语句执行的是,调用0x00000000地址处存放的函数。
来看第二段代码:
//代码2
void (*signal(int , void(*)(int)))(int);
仍然先从最里面的有内容的括号开始,最里面的应该是( int,void(*)(int) )左边是int类型,右边是函数指针类型,中间用逗号隔开,括号括起来,是不是就是一个函数的参数类型啊?再看前面一个signal,()优先级比*优先级高,signal先跟后面括号结合,所以说signal是个函数。signal(int,void(*)(int))就是整个函数,把他取出来,剩下的void(*)(int)是不是依然是函数指针类型啊?也就是说,里面的函数,它的返回值,是一个函数指针。整个代码就是一个函数的声明,这个函数名为signal,有两个参数,一个是int类型,另一个是函数指针类型。这个函数有一个返回值,返回值是函数指针类型。
这样写代码其实是很复杂的,因为函数指针类型要写成类似“void(*)(int)”这样的形式,括号多还要嵌套,可读性差。我们可以用typedef来把它改一下名字!这样就会好看很多。
例如:
typedef void (*pfunc)(int);
注意类似于函数指针这种类型,在声明、定义、乃至重命名的过程中,变量或者新名字都是放在括号里面的,而不是类型名后面。这个重命名的意思是,把void (*)(int) 这个类型的名字改成 pfunc。这样代码2就可以这样写:
pfunc signal(int ,pfunc);
是不是更容易理解了?
这一部分的内容不是非常好理解,一次性学习太多不好消化。只有前面的完全理解了,才能继续后面的内容。函数,指针,数组有怎样的奇葩组合呢?这些组合又有什么用呢?回调函数是什么?跟函数指针有关系吗?下一篇文章,我将为大家一一解答。
To be continued...