【C指针详解】进阶篇之——数组传参、指针传参、函数指针、函数指针数组、指向函数指针数组的指针

举报
YIN_尹 发表于 2023/08/04 10:22:00 2023/08/04
【摘要】 4. 数组参数、指针参数在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?4.1 一维数组传参比如,有这样一个一维数组:int arr[10];//一维整型数组int* arr2[20];//一维整型指针数组我们把它们作为参数传给两个函数:test(arr);test2(arr2);那现在函数test和test2的参数应该如何设计呢?1. 先来看函数test(接收...

4. 数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

4.1 一维数组传参

比如,有这样一个一维数组:

int arr[10];//一维整型数组
int* arr2[20];//一维整型指针数组

我们把它们作为参数传给两个函数:

test(arr);
test2(arr2);

那现在函数test和test2的参数应该如何设计呢?


1. 先来看函数test(接收arr):


test函数要接收arr,首先我们想到,arr是一个一维数组,那我们是不是可以用一个同类型一维数组来接收,这当然是没问题的,所以test(假设不需要返回值)的参数可以这样设计:

void test (int arr[10])

当然【】里的10其实可以省略的:

void test (int arr[]),这样也是可以的,因为这里设计的形参我们只是写成数组的形式,本质上还是指针(因为接收的是地址),所以不要求必须指定大小。


然后,


因为arr是数组名,表示的是数组首元素的地址,所以我们当然也可以把直接设计成指针,那传过来的是数组首元素(整型变量)的地址,我们应当用一个整型指针变量来接收:

void test (int* arr)


所以。函数test的形参,我们可以设计成这三种:


void test (int arr[10])

void test (int arr[])

void test (int* arr)


2. 然后我们来看函数test2(接收arr2):


那test2其实还是一个一维数组,只不过是整型指针数组,那我们的参数设计还是用同类型的数组数组,或者用指针:

同类型的指针数组:

void test2(int* arr[20]

void test2(int* arr[]

数组arr2的首元素是一个一级整型指针变量,一级指针的地址我们要用一个二级指针来接收:

void test2(int** arr)


4.2 二维数组传参

那现在我们要把二维数组作为参数传递给函数:

int main()
{
 int arr[3][5] = {0};
 test(arr);
}

此时,函数test的参数可以如何设计呢?

首先,传过去的是二维数组,我们当然可以用一个同类型的二维数组来接收:

void test(int arr[3][5])

void test(int arr[][5]

但注意不能写成int arr[][],因为二维数组的列数是不能省略的,二维数组传参,函数形参的设计只能省略第一个[]的数字。

那然后我们当然也可以用指针接收。

在【3.3 数组指针的使用】我们已经知道了,二维数组的首元素是二维数组的第一行(相当于一个一维数组),所以这里传递的arr其实相当于第一行的地址,是一维数组的地址,既然是数组的地址,当然要用数组指针来接收了。

所以我们可以这样设计:

void test(int (*arr)[5])

4.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;
}

e2c689f93e4c442fa6dc17ed014cb811.png

那现在我们思考这样一个问题:

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

void test1(int *p)
{}

test1函数能接收什么参数?


首先实参传一个同类型的一级指针变量,这肯定是没问题的。

int a=9; int *p=&a; test1(p);

然后我们是不是还可以传一个变量的地址,形参为int *p,当然可以接收一个整型变量的地址了。

int b=0;test1(&b);

那我们是不是还可以传一个一维数组的数组名,因为数组名也是一个地址,是数组首元素的地址,那形参为int *p,当然我们要传一个整型数组的数组名。

int arr[10];test1(arr);

4.4 二级指针传参

若实参为二级指针,那形参应该是同类型的二级指针,这样肯定是可以的。

举个例子:

#include <stdio.h>
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 c = 'b';
    char* pc = &c;
    char** ppc = &pc;
    char* arr[10];
    test(&pc);
    test(ppc);
    test(arr);//Ok?
    return 0;
}

调用test函数,我们可以传什么参数?


1.形参为二级指针,实参也传二级指针,这样肯定可以:

test(ppc);

2. 二级指针,当然可以接收一级指针变量的地址:

test(&pc);

我们是不是还可以传一个一级指针数组的数组名,因为它是该数组首元素的地址,即还是一级指针变量的地址:

test(arr);


5. 函数指针

什么时函数指针呢?


函数指针,即指向一个函数的指针,用来存放函数的地址。


5.1函数的地址

那既然要存放函数的地址,那函数的地址怎么来表示呢?


首先,我们已经知道,对于数组来说,比如:

int arr[10]={0};

数组名和&数组名的意义是不同的:

数组名arr表示数组首元素地址,而&arr才是整个数组的地址。

  1. 那现在如果这里有一个函数,函数的地址要如何表示:
void test()
{
 printf("hehe\n");
}

函数test的地址要如何表示呢?

会不会像数组一样,&test表示函数地址呢?那函数名test表示啥呢?函数可没有首元素这一说。

我们来看一段代码:

#include <stdio.h>
void test()
{
    printf("hehe\n");
}
int main()
{
    printf("%p\n", test);
    printf("%p\n", &test);
    return 0;
}

我们一起来看一下test和&test打印出来是什么:

b1378ed9c8b2422fb87df8222113d820.png

输出的是两个地址,且两个地址都是 test 函数的地址。

因为对于函数来说,函数名和&函数名表示的意义是完全一样的,都表示函数的地址。

即函数名==&函数名

5.2函数指针如何书写

那函数指针又应该怎么写呢?

现在有这样一个函数:

int add(int x, int y)
{
    return x + y;
}

如果我们要写一个函数指针来存储上面add函数的地址,我们可以这样写:

int (*p) (int,int)=&add;
或
int (*p) (int ,int)=add;

这里的P就是一个函数指针,解释一下:


首先,p和*结合,说明p是一个指针变量,然后该指针指向的是一个函数,函数有两个参数,都是 int 类型,函数的返回值类型也是 int 。


其它类型的函数指针书写也是同样的方法,大家按函数自己的参数类型,返回值类型写就行了。


5.3函数指针如何使用

那么,接下来我们怎么通过函数指针去调用上面的add函数呢?

我们知道,如果我们要通过函数名调用的话,可以这样写:

int ret=add(3,5);

5.4练习

我们一起来阅读两段有趣的代码:


(*(void (*)())0)();

大家思考一下,这段代码是什么意思。

这段代码的效果其实是:调用首地址为0的地址处的函数

给大家解释一下:


我们先来看中间这一部分(void (*)())0的意思:

数字0前面一个括号,括号里面放的是啥,是不是一个函数指针类型啊,首先一个(*)表明是一个指针,指针指向一个函数,该函数没有参数,也不需要返回值(void)。

也就是说将0强制类型转换为一个函数指针。

然后我们再看整个表达式, (*(void (*)())0)();:

其实是 对该函数指针解引用,并调用该函数。


在《C陷阱与缺陷》这本书中提及该代码,我们来看一下:

83e6db81d2c245cc847d4de3312e6c8f.png

void (*signal(int , void(*)(int)))(int);

这句代码看起来很复杂,大家思考一下它的意思。

这句代码其实是一个:函数声明

解释一下:


我们直接去看这句代码可能不容易理解,我们可以将这句代码写成这样:

void(*)(int) signal (int,void(*)(int));

函数返回类型 、函数名、 参数类型

这样相信大家很容易就看懂了,就是一个函数声明。

但是我们要知道,这种写法是语法不支持的。


void (*signal(int , void(*)(int)))(int);这句代码看上去可能太复杂了,不过我们可以简化一下它:


我们使用关键字 typedef 对 void(*)(int)进行一个类型重命名。

typedef void(*)(int) pfun_t;,

将void(*)(int)重命名为pfun_t,这样写对吗?

错误的!!!

语法规定正确的写法是这样的:

正确的:typedef void (*pfun_t) (int);

那现在我们就可以这样写了:

pfun_t signal (int, pfun_t);


这句代码同样在《C陷阱与缺陷》中提及:

9a7e2b130f2c48a793db8f9f5c7d1e58.png

6. 函数指针数组

6.1如何定义

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组

比如:

int *arr[10];
//数组的每个元素是int*

那函数指针数组就是存放函数指针(或函数地址)的数组,那函数指针的数组如何定义呢?

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];

这3句代码那一句正确定义了一个函数指针数组?

答案是:parr1


parr1 先和 [] 结合,说明 parr1是数组,数组有10个元素,每个元素的类型是 int (*)() 类型的函数指针。

(把数组名及元素个数parr1[10]去掉剩下的就是元素类型。)


6.2函数指针数组的使用

函数指针数组的用途:转移表


比如我们想要写代码实现一个计算器的功能(加减乘除),在没学函数指针数组之前,我们可能会这样写:

#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;
}

使用switch、case语句选择相应的功能,就去调用对应的函数来实现对操作数的加减乘除。

但这样写好像不是特别好。

d8018adcee83486f851ca8a1fccfb2a3.png

那有没有更好的办法呢?

当然有,那我们就可以使用函数指针数组去实现。

#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;
}

这次代码就没有那么多重复的部分了,更加简洁。

解释一下:我们定义了一个函数指针数组int(*p[5])(int x, int y),5个元素,每个元素是一个函数指针,指向的函数两个参数为int类型,返回类型也是int。

然后对数组初始化:{ 0, add, sub, mul, div }; ,把加减乘除4个函数的地址存入数组。

为啥数组最前面要加一个0呢?


因为我们四个函数对应的选项是1,2,3,4,这样使得它们的下标正好是1,2,3,4。

我们可以通过下标直接找到并调用函数。


7. 指向函数指针数组的指针

什么是指向函数指针数组的指针?


即指向函数指针数组的指针,用来存放函数指针数组的地址。


那 指向函数指针数组的指针 如何定义呢?

举个例子:

#include <stdio.h>
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;
}

函数指针和函数指针数组我们已经知道怎么回事了。


解释一下:void (*(*ppfunArr)[5])(const char*)

首先,ppfunArr和*结合(*ppfunArr),说明它是一个指针。

然后指向一个数组,数组有5个元素(*ppfunArr)[5],每个元素是一个函数指针void (*) (const char*)。

该函数指针指向一个函数,函数一个参数,参数类型为const char* str类型,不需要返回值。


以上就是对指针进阶内容的讲解,希望能帮助到大家,如果有写的不好的地方,欢迎大家指正!!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。