【C语言】指针进化:传参与函数(2)

news2024/11/26 18:36:50

莫道君行早,更有早行人。— 出自《增广贤文·上集》
解释:别说你出发的早,还有比你更早的人。

在这里插入图片描述


    这篇博客我们将会深入的理解数组传参和函数指针等指针,是非常重要的内容,学好这部分才能算真正学懂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这两个函数哪些形参是对的哪些又是错误的。

    1. 简单的分析

    主函数里有一个整型数组 arr和一个指针数组存放整型指针,test(arr)实参是arr的地址,test2(arr2)的实参是arr2的地址。现在你可以试一试。

    1. 开始真正分析
    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);
    }
    
    1. 简单的分析
      简单定义并初始化了一个数组名为arr的二维数组。然后将数组名传参。
    2. 开始真正分析
    • 先从没有 * 号开始吧。
    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;
    }
    
    1. 简单的分析
      创建一个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);
    
    

    我们不要畏惧这种代码其实只要我们用心加上经验就能看出来。
    开始分析

    1. (*(void (*)())0)();
      首先我们能看到void ( * )( )这个函数指针和 0我们看到( * )里面没有写变量,
      那么说明这是一种类型,而0是什么呢?其实这里的0指的是0地址
      (void ( * )( ))0 那么这一部分的意思就是将0地址的类型转换为
      void ( * )( )
      这就意味着0地址处放一个返回类型是void,无参的一个函数
      现在我们可以把代码简化一下变成:( * 0)( );也就是说调用0地址处的函数刚好也
      与我们void ( * )( )相对应,只是我们不需要再写返回值,只需要写参数
      这里刚好无参
      这就是在表达调用0地址处的这个函数
    2. 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;
    }
    

    在这里插入图片描述
    这便是如何使用。

    总结😈

    我们在这篇博客中详细的介绍了详数组传参指针传参的方法和细节,以及一些不太常用但是你必须要知道的一些指针类型,貌似在操作系统开发上用的比较多。完结。在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/742673.html

    如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

    相关文章

    【网络】socket——预备知识 | 套接字 | UDP网络通信

    &#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《网络》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 在前面本喵对网络的整体轮廓做了一个大概的介绍&#xff0c;比如分层&#xff0c;协议等等内容&#x…

    UFS 14 - UFS RPMB安全读写命令

    UFS 14 - UFS RPMB安全读写命令 1 SECURITY PROTOCOL IN/OUT Commands1.1 CDB format of SECURITY PROTOCOL IN/OUT commands1.2 Supported security protocols list description1.3 Certificate data description 2 CDB format of SECURITY PROTOCOL IN/OUT commands demo2.1 …

    Spring Boot原理分析(一):项目启动流程、自动装配

    文章目录 一、项目启动流程二、SpringBootApplication.java源码解析1.准备工作2.源码3.自定义注解4.组合注解5.注解ComponentScan过滤器 6.注解SpringBootConfigurationConfiguration 7.注解EnableAutoConfiguration&#xff08;1&#xff09;Spring手动装配使用XML配置文件使用…

    Nerf论文阅读笔记Neuralangelo: High-Fidelity Neural Surface Reconstruction

    Neuralangelo&#xff1a;高保真神经表面重建 公众号&#xff1a;AI知识物语&#xff1b;B站暂定&#xff1b;知乎同名 视频入门介绍可以参考 B站——CVPR 2023最新工作&#xff01;Neuralangelo&#xff1a;高保真Nerf表面重建 https://www.bilibili.com/video/BV1Ju411W7…

    杨氏矩阵,字符串左旋,字符串旋转结果题目解析

    杨氏矩阵 题目要求:有一个数字矩阵&#xff0c;矩阵的每行从左到右是递增的&#xff0c;矩阵从上到下是递增的&#xff0c;请编写程序在这样的矩阵中查找某个数字是否存在。 示例 分析:我们仔细分析&#xff0c;不难发现&#xff0c;对于杨氏矩阵老说&#xff0c;右上角和左下…

    leetcode1020. 飞地的数量

    https://leetcode.cn/classic/problems/number-of-enclaves/description/ 给你一个大小为 m x n 的二进制矩阵 grid &#xff0c;其中 0 表示一个海洋单元格、1 表示一个陆地单元格。 一次 移动 是指从一个陆地单元格走到另一个相邻&#xff08;上、下、左、右&#xff09;的…

    哈希的应用->位图

    ps&#xff1a;左移位并不是向左移动位&#xff0c;而是低数据位向高数据位挪动 位图&#xff08;主要接口&#xff0c;set(size_t)标识、reset(size_t)取消、test(size_t) 查看 给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何快速判断一…

    做软件测试到底要不要学编程?

    乔布斯曾经说过“每个人都应该学习编程&#xff0c;因为它会教你如何思考”&#xff0c;看&#xff0c;乔帮主都觉得所有人都应该学编程&#xff0c;那你说做测试的要不要学&#xff1f;当然要。 作为测试人员&#xff0c;除了上面这个原因&#xff0c;我觉得如果会编程&#x…

    Android架构之MVC,MVP,MVVM解析

    MVC架构 View&#xff1a;Acitivity(View)、Fragment(View)视图&#xff0c;在android里xml布局转成View后&#xff0c;加载到了Activity/Fragment里了。 Controller&#xff1a;Controller对应着Activity/Fragment&#xff0c;绑定UI&#xff0c;处理各种业务。 Model&#xf…

    python接口自动化(三十)--html测试报告通过邮件发出去——中(详解)

    简介 上一篇&#xff0c;我们虽然已经将生成的最新的测试报告发出去了&#xff0c;但是MIMEText 只能发送正文&#xff0c;无法带附件&#xff0c;因此我还需要继续改造我们的代码&#xff0c;实现可以发送带有附件的邮件。发送带附件的需要导入另外一个模块 MIMEMultipart。还…

    java版电子招标采购系统源码之电子招标采购实践与展望-招标采购管理系统

    统一供应商门户 便捷动态、呈现丰富 供应商门户具备内外协同的能力&#xff0c;为外部供应商集中推送展示与其相关的所有采购业务信息&#xff08;历史合作、考察整改&#xff0c;绩效评价等&#xff09;&#xff0c;支持供应商信息的自助维护&#xff0c;实时风险自动提示。…

    springboot+MySQL实现4S店车辆管理系统

    本系统为了数据库结构的灵活性所以打算采用MySQL来设计数据库&#xff0c;而java技术&#xff0c;B/S架构则保证了较高的平台适应性。本文主要介绍了本系统的开发背景&#xff0c;所要完成的功能和开发的过程&#xff0c;主要说明了系统设计的重点、设计思想。

    计算机体系结构基础知识介绍之使用多问题和静态调度来利用 流水线

    为了提高处理器的性能&#xff0c;我们需要让每个时钟周期内发出多条指令&#xff0c;而不是只发出一条。这种多发射处理器有三种主要类型&#xff1a; 1. 静态调度的超标量处理器 2. VLIW&#xff08;非常长指令字&#xff09;处理器 3. 动态调度的超标量处理器。 这三种类型的…

    lua 请求ftp服务器数据,下载文件

    1、装入ftp库 2、调用ftp的get()方法 3、get()方法参数格式&#xff1a; 4、将返回到的数据写入文件中 例如&#xff0c;本次获取专利数据系统 http://patdata1.cnipa.gov.cn/ 的ftp站点数据 local ftp require("socket.ftp")--此处我没填端口号 file,err ftp.g…

    findfont: Font family ‘Times New Roman‘ not found.

    问题 Linux 使用 matplotlib.pyplot 画图时为了使字体和英文论文中的 Times of Roman 一致&#xff0c;通常会用到如下文本格式 font1 {family: Times New Roman, # x and y labelsweight: normal,size: 16}但在实际使用时会出现如下报警信息&#xff1a; findfont: Font …

    element ui 导入模块的封装

    导入组件的封装 <template><Modal :visible"visible" title"导入" onSave"onSave" onCancal"closeDialog"><template #default><el-upload ref"upload" class"upload-demo"action"ht…

    Scala中使用 break 和 continue

    Scala中没有 break 和 continue 关键字&#xff0c;但是我们可以用 Breaks 类提供的相应方法来实现对应功能。 在Java中&#xff0c;break continue return的区别 1、break&#xff1a;break不仅可以结束其所在的循环&#xff0c;还可结束其外层循环&#xff0c;但一次只能结束…

    Vulnhub: Hackable:II靶机

    kali&#xff1a;192.168.111.111 靶机&#xff1a;192.168.111.142 信息收集 端口扫描 nmap -A -sC -v -sV -T5 -p- --scripthttp-enum 192.168.111.142 网站的files目录 ftp存在匿名登录&#xff0c;所在目录为网站的files目录 ftp上传反弹shell 提权 目标根目录下的.ru…

    预付费智能水表远程控制系统

    预付费智能水表远程控制系统是一种基于物联网技术的智能水表管理系统&#xff0c;它通过远程通信技术和云计算平台&#xff0c;实现了对水表的实时监控、数据采集、费用计算、远程控制等功能。该系统不仅可以提高水务公司的管理效率&#xff0c;还可以为用户提供更加便捷、可靠…

    [疑难杂症2023-004]停止服务器自动启动的服务,解决端口占用的问题

    本文由Markdown语法编辑器编辑完成。 1. 背景 前段时间&#xff0c;在linux上启动一个目录下的docker-compose.yml中的服务时&#xff0c;遇到了一个3000端口被占用的问题. 凭借经验&#xff0c;一般可能是之前的服务没被正常的停止掉&#xff0c;导致该服务占用的端口未被释…