指针太难?手把手教你理解指针(传参、函数指针)

news2024/12/23 18:42:20

目录

前言

一、数组和指针的参数

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...

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

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

相关文章

【机器学习】随机森林预测泰坦尼克号生还概率

目录 前言&#xff1a; 【一】数据清洗及可视化 介绍 知识点 环境准备 数据特征介绍 检查数据 相关系数 缺失值 偏态分布 数值化和标准化 离群点 实验总结一 【二】分类模型训练及评价 介绍 环境准备 模型评估 模型选择 性能度量 实验总结二 【三】随机森…

机器学习入门(全连接神经网络-1)

机器学习入门(全连接神经网络-1) 目录 机器学习入门(全连接神经网络-1)一、神经元简介1.概念2.例子二、常见的激活函数从神经元开始进行讲述,从零开始搭建全连接神经网络。 一、神经元简介 1.概念 神经元是神经网络的基本组成单位。 神经元接受输入,对它们进行一些数学运…

自主机器人运动规划|地图相关概念总结

自主机器人运动规划|地图相关概念总结地图表示占用栅格地图八叉树地图&#xff08;Octo-map&#xff09;Voxel hashing(哈希表地图)点云地图TSDF mapESDF map地图表示 地图分成两个模块&#xff1a; 地图装在数据的数据结构地图信息融合方法 占用栅格地图 使用最为广范的是 …

用PyTorch训练模型识别captcha库生成的验证码

目录 制作训练数据集 用Dataloader加载自定义的Dataset 训练模型 识别验证码 总结与提高 源码下载 在本节&#xff0c;我们将使用深度学习框架PyTorch来训练模型去识别一种难度稍大一点的数字字母混合验证码&#xff08;我们可以使用第三方库captcha生成这种验证码&#…

4.2 换元积分法

思维导图&#xff1a; 学习目标&#xff1a; 学习换元积分法时&#xff0c;可以遵循以下几个步骤&#xff1a; 理解换元积分法的基本思想&#xff1a;将一个复杂的积分变成一个简单的积分&#xff0c;通过引入一个新的变量来实现。 掌握换元积分法的基本公式&#xff1a;如果…

reviewSpringBoot

1.springboot简介说明 Springboot简化新Spring应用的初始搭建以及开发过程 SpringBoot是基于Spring的框架&#xff0c;该框架使用了特定的方式来进行配置&#xff0c;从而使开发人员不再需要定义样板化的配置。 SpringBoot集成了绝大部分目前流行的开发框架&#xff0c;就像…

FISCO-BCOS链节点黑名单对共识的影响

目录一、前言二、测试过程三、PBFT一、前言 目的&#xff1a;在fisco-bcos&#xff08;v2.8.0&#xff09;环境下&#xff0c;测试黑名单对节点共识的影响。 部署搭建四节点的链&#xff0c;并部署console&#xff1a; https://fisco-bcos-documentation.readthedocs.io/zh_…

3 个自定义防抖 Hooks 的实现原理

前言— 本文通过实现 useDebounceFn、useDebounce、useDebounceEffect 3 种自定义防抖 Hooks&#xff0c;来介绍在日常开发过程中自定义 Hooks 的思路及实现&#xff0c;帮助大家完成通用 Hooks 来提高开发效率。 防抖— 防抖的概念已经司空见惯了&#xff0c;这里稍作简单介…

00后也太卷了吧!进厂起薪18K,原来面试时候都说了这些......

都说00后躺平了&#xff0c;但是有一说一&#xff0c;该牛的还是牛。 这不&#xff0c;前段时间公司来了个00后&#xff0c;工作都没两年&#xff0c;跳槽起薪18K。本来还以为是个年少有为的技术大牛呢&#xff0c;结果相处一个月下来发现技术也就那样。 问起他是如何做到和老…

Java Web学习路线

⭐作者介绍&#xff1a;大二本科网络工程专业在读&#xff0c;持续学习Java&#xff0c;努力输出优质文章 ⭐作者主页&#xff1a;逐梦苍穹 ⭐所属专栏&#xff1a;Java Web ⭐如果觉得文章写的不错&#xff0c;欢迎点个关注一键三连&#x1f609;有写的不好的地方也欢迎指正&a…

(四)栈—中缀表达式转后缀表达式

一、基本介绍 二、应用实例 将中缀表达式"1((23)x4)-5" 转换为 后缀表达式"1 2 3 4 x 5 -" 思路&#xff1a; 1.初始化两个栈&#xff1a;运算符栈s1和存储中间结果的栈s2&#xff1b; 2.从左至右扫描中缀表达式&#x…

【c++初阶】第九篇:vector(常用接口的使用 + 模拟实现)

文章目录vector介绍vector的使用vector的定义vector iterator(迭代器) 的使用begin和endrbegin和rendvector 空间增长问题size和capacityreserve和resize&#xff08;重点&#xff09;测试vector的默认扩容机制emptyvector的增删查改push_back和pop_backinsert和erasefindswapo…

GPT-4创造者:第二次改变AI浪潮的方向

OneFlow编译 翻译&#xff5c;贾川、杨婷、徐佳渝 编辑&#xff5c;王金许 一朝成名天下知。ChatGPT/GPT-4相关的新闻接二连三刷屏朋友圈&#xff0c;如今&#xff0c;这些模型背后的公司OpenAI的知名度不亚于任何科技巨头。 不过&#xff0c;就在ChatGPT问世前&#xff0c;Ope…

Verilog Tutorial(10)如何实现可复用的设计?

写在前面在自己准备写verilog教程之前&#xff0c;参考了许多资料----FPGA Tutorial网站的这套verilog教程即是其一。这套教程写得不错&#xff0c;只是没有中文&#xff0c;在下只好斗胆翻译过来&#xff08;加了自己的理解&#xff09;分享给大家。这是网站原文&#xff1a;h…

【敬伟ps教程】平移、缩放、移动、选区

文章目录平移抓手工具旋转抓手缩放工具移动工具详解选区选区工具详解平移 抓手工具 当打开一张大图时&#xff0c;可以通过修改底部的百分比或使用抓手工具&#xff08;H或在任何时候按住空格键来使用抓手工具&#xff09;来查看更多细节 使用抓手工具时滚动所有打开的文档&…

几何的显式表示 - 曲线和曲面

点云&#xff1a;list of points&#xff08;x, y, z&#xff09; 将点云变成多边形的面&#xff0c;从而在计算机中输出多边形网格&#xff1a;Polygon Mesh 就是 拆成小三角 贝塞尔曲线 定义曲线只要满足起止点即可&#xff0c;P1&#xff0c;P2决定了它要往哪个方向弯 de Ca…

STM32 HAL库PID控制电机 第三章 PID控制双电机

STM32 HAL库PID控制电机 第三章 PID控制双电机 注:本文含全部PID控制代码,保证可以运行,如不能运行可以留言回复 1 基础配置 1.1 编码器电路图及配置 引脚定时器通道PA0TIM2_CH1PA1TIM2_CH2PB6TIM4_CH1PB7TIM4_CH2因此需要把TIM2、TIM4配置为编码器模式。在STM32CubeMX中…

如何在react中处理报错

本文为 360 奇舞团前端工程师翻译 原文地址&#xff1a;https://www.developerway.com/posts/how-to-handle-errors-in-react我们都希望我们的应用能稳定、完美运行&#xff0c;并且能够考虑到每一个边缘情况。但是现实情况是&#xff0c;我们是人&#xff0c;是人就会犯错&…

【区块链】走进web3的世界-DApp如何快速接入wall

在web3中&#xff0c;wall是您进入区块链的一个标识&#xff0c;每个用户使用的wall都不近相同&#xff0c;因此接入更多的wall是很有必要的&#xff0c;从用户角度来说&#xff0c;非必要情况下&#xff0c;我是不愿意去额外下载wall的。因此今天我们来聊一下&#xff0c;DApp…

手把手教你搭建 Docker API 未授权漏洞环境

环境&#xff1a;ubuntu 16.04 我是在虚拟机中安装了ubuntu 16.04的环境&#xff0c;里面暂时没有docker、ssh远程、vim等。 1. 更换国内源 直接输入docker 这里没有安装&#xff0c;那安装下 sudo apt install docker.io 开始安装 因为我没有修改源&#xff0c;所以这个速…