指针复习 ( 下 )

这是指针复习的第二篇,主要介绍指针与数组、函数的应用。

一、指针与数组

1.1 数组名的理解

在说明指针与数组的关系之前,我们必须要先了解「数组名」的意义

我们在指针复习 ( 下 ) 中,指针±整数的部份有写过下面这段代码

 int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];

这里我们是使用 &arr[0] 来取得数组首元素的地址,但事实上,数组名本就代表了首元素的地址

#include <stdio.h>int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];printf("%p\n", &arr[0]);printf("%p\n", arr);return 0;
}

在这里插入图片描述

从输出结果可以看到,直接使用数祖名打印地址的结果是和取首元素地址打印的结果相同。这也就印证了我们上面所述,数组名代表了首元素的地址

那如果使用 sizeof(数组名) 结果会是多少呢 ?

如果依照我们在指针复习 ( 下 ) 所说,地址的大小不是 4 就是 8。 那这里的结果是 4 或 8 吗 ?

#include <stdio.h>int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];printf("%zd\n", sizeof(arr));return 0;
}

在这里插入图片描述

输出结果发现,sizeof(arr) 的结果是整个数组的大小而不是地址大小。因此我们就需要提到两个数组名的例外

  1. sizeof(数组名) : 当 sizeof 中单独放数组名,这里的数组名不是代表首元素的地址,而是整个数组,因此计算的是整个数组的大小,单位为字节。
  2. &数组名 : 这里的数组名也是代表整个数组,取出的是整个数组的地址

除了这两个例外,其他地方如果看到数组名原则上都是代表数组首元素地址

#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("&arr[0] = %p\n", &arr[0]);printf("arr = %p\n", arr);printf("&arr = %p\n", &arr);return 0;
}

在这里插入图片描述

我们前面说, &arr 取出的是整个数组的地址,但是代码中打印地址的结果却又是相同的,那 arr 和 &arr 到底有什么区别 ?

这和我们在指针复习 ( 一 ) 所提到,取出的地址是取出最小字节的地址。但如果我们去对地址进行± 就会体现这两者的不同

#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("&arr[0] = %p\n", &arr[0]);printf("&arr[0]+1 = %p\n", &arr[0] + 1);printf("arr = %p\n", arr);printf("arr+1 = %p\n", arr + 1);printf("&arr = %p\n", &arr);printf("&arr+1 = %p\n", &arr + 1);return 0;
}

在这里插入图片描述

也就说明了 &arr 是取出整个数组地址而不是首元素地址,当然往后偏移多少字节是取决于数据类型

1.2 使用指针访问数组

如果我们访问数组内容可以使用**数组名[下标]来进行访问,那可以使用指针变量[下标]** 的方式来访问吗 ?

#include <stdio.h>
int main()
{int arr[10] = {0};//输⼊int i = 0;int sz = sizeof(arr)/sizeof(arr[0]);//输⼊int* p = arr;for(i=0; i<sz; i++){scanf("%d", p+i);//scanf("%d", arr+i);//也可以这样写}//输出for(i=0; i<sz; i++){printf("%d ", p[i]); // 也可以写成 printf("%d ",*(p+i));// 同理,事实上我们如果打印 arr[i] 相当于 *(arr+i); // 数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的}return 0;
}

在这里插入图片描述

1.3 一维数组传参的本质

我们知道函数中是可以传入数组的,但是传入的真的是整个数组吗 ?

#include <stdio.h>
void test(int arr[])
{int sz2 = sizeof(arr)/sizeof(arr[0]);printf("sz2 = %d\n", sz2);
}
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int sz1 = sizeof(arr)/sizeof(arr[0]);printf("sz1 = %d\n", sz1);test(arr);return 0;
}

输出结果

sz1 = 10
sz2 = 1

我们发现函数内部并没有正确获得数组的元素个数,但我们不是将数组传入了吗 ? 这时就必须要说明一维数组传参的本质了

在本质上,数组传参传递的是首元素的地址,既然传入的是地址,那函数形参理论上也就应该使用指针变量来接收首元素的地址。

因此如果我们 test 函数中测试 sizeof(arr) 得到的会是 4 or 8。而不是数组的大小。

这也就说明了,当我们使用一维数组当做函数的形参时,这个一维数组会退化为指针

以 test 函数为例 形参是 int arr[] 事实上会退化成 int* arr

1.4 指针数组

指针数组是指针还是数组呢 ?

/*
我们称 int a[5] = {0}; 做整型数组 -- 用来存储整型的数组
我们称 char a[] = "abc"; 做字符数组 -- 用来存储字符的数组
因此如果我们写了这样的一行代码
int* a[5] = {...}; 就叫做指针数组 -- 也就是用来存储 「指针」 的数组*/

在这里插入图片描述

1.5 指针数组模拟二维数组

我们可以把**二维数组看作一维数组的数组,**如此一来,二维数组中的每个一维数组也有首元素地址。

#include <stdio.h>
int main()
{int arr1[] = {1,2,3,4,5};int arr2[] = {2,3,4,5,6};int arr3[] = {3,4,5,6,7};//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中int* parr[3] = {arr1, arr2, arr3};int i = 0;int j = 0;for(i=0; i<3; i++){for(j=0; j<5; j++){printf("%d ", parr[i][j]);// 相当于 printf("%d ", *(*(parr+i)+j));}printf("\n");}return 0;
}

它是如何体现的呢 ?

在这里插入图片描述

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数 组中的元素

上述的代码模拟出⼆维数组的效果,实际上并非完全是⼆维数组,因为每⼀⾏并非是连续的

后面会更正式的探讨二维数组传参的本质

1.6 字符指针变量

指针类型中有一个 char*

一般使用

int main()
{char ch = 'w';char *pc = &ch;*pc = 'w';return 0;
}

另一种使用方式

int main()
{const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?printf("%s\n", pstr);return 0;
}

实际上, const char* pstr = "hello bit."; 意思是把⼀个常量字符串的⾸字符 h 的地址存放到指针变量 pstr 中。而不是把整个字符串存放到 pstr 中

1.7 数组指针变量

我们前面有提到,指针数组是用来存放指针的数组。那数组指针呢 ? 数组指针就是用来存放数组的指针 !

// p1 p2 个别是什么 ?
int *p1[10];  // p1先和[10]结合,因此 p1是指针数组
int (*p2)[10]; // p2先和*结合,因此 p2是数组指针
// 这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合

数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是我们之前说的 &数组名

如果要存放数组的地址

#include <stdio.h>int main(){int arr[10] = {0};int (*p)[10] = &arr;return 0;
}

在这里插入图片描述

我们可以透过调试看到, &arrp 的类型是一样的 !

数组指针的定义

int (*p) [10] = &arr;|   |    ||   |    ||   |    p指向数组的元素个数|   p是数组指针变量名p指向的数组的元素类型

有了数组指针的概念,我们就可以进一步的探讨二维数组传参的本质了

1.8 二维数组传参的本质

一般情况下,我们要传递二维数组到函数中,是这样写的

#include <stdio.h>
void test(int a[3][5], int r, int c)
{int i = 0;int j = 0;for(i=0; i<r; i++){for(j=0; j<c; j++){printf("%d ", a[i][j]);}printf("\n");}
}
int main()
{int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};test(arr, 3, 5);return 0;
}

那可以使用其他方式写吗 ?

我们先再次理解一下二维数组

⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维 数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组
在这里插入图片描述
根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址。因此形参也可以写成指针的形式

#include <stdio.h>
void test(int (*p)[5], int r, int c)
{int i = 0;int j = 0;for(i=0; i<r; i++){for(j=0; j<c; j++){printf("%d ", *(*(p+i)+j));}printf("\n");}
}
int main()
{int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};test(arr, 3, 5);return 0;
}

二、指针与函数

2.1 函数指针变量

什么是函数指针变量呢 ? 根据前面的说明了这么多指针变量应该不难理解,函数指针变量就是用来存放函数地址的指针

当然,函数名就是函数的地址。因此 &函数名 和 直接写函数名都是可以的两者等价

不过要定义一个函数指针变量就不是像前面一样这么简单了

// 函数指针变量的定义
int      (*pf3)      (int x, int y) // 形参名称可以不写,但是类型和个数都必须和指向的函数相同|          |         ------------|          |              ||          |        pf3指向函数的参数类型和个数|      函数指针变量名pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型
void test()
{printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;

2.2 函数指针变量的使用

#include <stdio.h>
int Add(int x, int y)
{return x+y;
}
int main()
{int(*pf3)(int, int) = Add;printf("%d\n", (*pf3)(2, 3));printf("%d\n", pf3(3, 5));return 0;
}

输出结果

5
8

使用函数指针变量的作用就是可以透过函数指针去调用目标函数,而不是直接透过函数名调用。

2.3 函数指针数组

把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组。要注意函数的类型要是一样的!

函数指针数组该如何定义呢 ?

int (*parr1[3])(); // parr1先和[]结合 说明 parr1是数组 数组的类型就是除掉parr1和[] 也就是 int(*)()
// 因此parr1 是数组,类型是函数指针

2.4 函数指针数组的使用

#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 }; //转移表do{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");printf( "请选择:" );scanf("%d", &input);if ((input <= 4 && input >= 1)){printf( "输⼊操作数:" );scanf( "%d %d", &x, &y);ret = (*p[input])(x, y); // 我们可以直接透过解引用数组中所存储的函数地址,来调用目标函数!printf( "ret = %d\n", ret);}else if(input == 0){printf("退出计算器\n");}else{printf( "输⼊有误\n" );}}while (input);return 0;
}

2.5 回调函数

回调函数的概念
什么是回调函数呢 ? 回调函数就是通过函数指针调用的函数。
如果我们将某一个函数的指针 ( 地址 ) 作为参数传递给另外一个函数,这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。
回调函数不是由函数实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用,对于该事件或条件的响应。
简单说,将A函数地址传递给B,在B函数内通过指针调用A函数实现操作,那么A函数就称为回调函数,也就是不直接调用A,而是通过B调用A。

我们以上面的简易计算机代码为例。除了使用函数指针数组的方式之外,还能不能再简化一些 ?

//使⽤回到函数改造后
#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;
}
void calc(int(*pf)(int, int))
{int ret = 0;int x, y;printf("输⼊操作数:");scanf("%d %d", &x, &y);ret = pf(x, y);printf("ret = %d\n", ret);
}
int main()
{int input = 1;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:calc(add);break;case 2:calc(sub);break;case 3:calc(mul);break;case 4:calc(div);break;case 0:printf("退出程序\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}
// 这段代码中的 add、sub、mul、div 都可以称作回调函数

三、二级指针

我们都说 指针变量 指针变量。既然它是变量,它就应该要有地址,有地址我就应该可以使用指针来存储。

那存储普通变量 我们使用指针变量。要存储指针变量是不是就应该使用二级指针指针变量 ?

/* 
其实就是这样的概念
存储 int变量 -> 用int*
存储 char变量 -> 用char*要存储 int*变量 -> 用 int** -- 二级指针*/

3.1 二级指针视图

在这里插入图片描述

如果对 ppa 解引用则得到的会是pa的地址 ( *ppa)

如果再对 *ppa 解引用 得到的就是 a 的内容 ( **ppa )

我们在指针复习 ( 一 ) 中也有说过,如果希望在函数内部改变函数外部的变量内容,就使用传址传递。

那如果我们希望在函数内部改变一级指针的指向内容呢 ? 也就是把一级指针的地址传到函数当中,此时,形参就要使用二级指针 !

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ElementUI 的组件 Switch(开关)如何让文字显示在按钮上

效果图&#xff1a; 一、引入switch组件 给组件自定义一个类&#xff1a;tableScopeSwitch&#xff0c;设置开关的值和对应展示的文字&#xff08;开为 1&#xff0c;并展示启用&#xff1b;关为 0&#xff0c;并展示禁用&#xff09;。 <div class"tableScopeSwitch…

Linux---系统函数

一、打开文件open #include <fcntl.h> int open(const char *pathname&#xff0c;int flags); int open(const char *pathname&#xff0c;int flags&#xff0c;mode_t mode); 功能: 打开或创建文件 参数: pathname //打开的文件名 flags //操作的标志…

智能编程助手功能革新与价值重塑之:GitHub Copilot

引言&#xff1a; GitHub Copilot 的最新更新为开发者带来了显著变化&#xff0c;其中 Agent Mode 功能尤为引人注目。该模式能够自动识别并修复代码错误、自动生成终端命令&#xff0c;并具备多级任务推理能力&#xff0c;这使得开发者在开发复杂功能时&#xff0c;可大幅减少…

Elasticsearch+Logstash+Kibana可视化集群部署

文章目录 1.组件介绍简述2.集群规划3.Es组件部署4.Logstash组件部署5.Kibana组件部署6.Kibana的基础使用 1.组件介绍简述 Elasticsearch&#xff1a;开源实时分布式搜索和分析引擎&#xff0c;支持大规模数据存储和高吞吐量&#xff0c;提供丰富的搜索功能和可扩展性。 Logsta…

SpringBoot多数据源实践:基于场景的构建、实现和事务一体化研究

1. 多数据源应用场景剖析 1.1 业务驱动的多数据源需求 数据量与业务复杂度引发的分库分表&#xff1a;在现代企业级应用中&#xff0c;随着业务的不断拓展和用户量的持续增长&#xff0c;数据量呈爆炸式增长。例如&#xff0c;在大型电商平台中&#xff0c;用户数据、订单数据…

LabVIEW 中dde.llbDDE 通信功能

在 LabVIEW 功能体系中&#xff0c;位于 C:\Program Files (x86)\National Instruments\LabVIEW 2019\vi.lib\Platform\dde.llb 的 dde.llb 库占据着重要的地位。作为一个与动态数据交换&#xff08;DDE&#xff09;紧密相关的库文件&#xff0c;它为 LabVIEW 用户提供了与其他…

数据结构-栈和队列的应用

目录 前言一、栈的应用&#xff08;迷宫问题&#xff09;1.1 问题描述1.2 算法选择1.3 算法精化1.4 算法实现1.5 问题结果 二、队列的应用&#xff08;农夫过河问题&#xff09;2.1 问题描述2.2 算法选择2.3 算法精化2.4 算法实现2.5 问题结果 总结 前言 本篇文章使用两个例子…

SkyWalking 10.1.0 实战:从零构建全链路监控,解锁微服务性能优化新境界

文章目录 前言一、集成SkyWalking二、SkyWalking使用三、SkyWalking性能剖析四、SkyWalking 告警推送4.1 配置告警规则4.2 配置告警通知地址4.3 下发告警信息4.4 测试告警4.5 慢SQL查询 总结 前言 在传统监控系统中&#xff0c;我们通过进程监控和日志分析来发现系统问题&…

AIGC图生视频保姆级教程

一、AI文生图高阶技巧 推荐工具 ▸ MidJourney&#xff08;艺术感最强&#xff09; ▸ DALLE 3&#xff08;与ChatGPT深度联动&#xff09; ▸ Leonardo.ai&#xff08;精细化参数控制&#xff09; 核心策略 提示词架构&#xff1a; [主体描述][环境氛围][镜头语言][风格参数…

springboot整合mybatis-plus【详细版】

目录 一&#xff0c;简介 1. 什么是mybatis-plus2.mybatis-plus特点 二&#xff0c;搭建基本环境 1. 导入基本依赖&#xff1a;2. 编写配置文件3. 创建实体类4. 编写controller层5. 编写service接口6. 编写service层7. 编写mapper层 三&#xff0c;基本知识介绍 1. 基本注解 T…

利用亚马逊云科技RDS for SQL Server配置向量数据存储

生成式人工智能&#xff08;AI&#xff09;正迎来又一个快速发展期&#xff0c;引起了开发者们的广泛关注。将生成式能力集成到商业服务和解决方案中变得非常重要。当前的生成式AI解决方案是机器学习和深度学习模型逐步进化迭代的结果。从深度学习到生成式AI的质变飞跃主要是由…

c++ 多线程知识汇总

一、std::thread std::thread 是 C11 引入的标准库中的线程类&#xff0c;用于创建和管理线程 1. 带参数的构造函数 template <class F, class... Args> std::thread::thread(F&& f, Args&&... args);F&& f&#xff1a;线程要执行的函数&…

H5接入支付宝手机网站支付并实现

小程序文档 - 支付宝文档中心 1.登录 支付宝开放平台 创建 网页/移动应用 2.填写创建应用信息 3.配置开发设置 4.网页/移动应用&#xff1a;需要手动上线。提交审核后&#xff0c;预计 1 个工作日的审核时间。详细步骤可点击查看 上线应用 。应用上线后&#xff0c;还需要完成…

字节二面:DNS是什么?是什么原理?

写在前面 最近有个同学后台私信让我出一个DNS的工作原理&#xff0c;面试的时候居然问到了&#xff0c;所以就简单聊聊DNS的工作原理吧&#xff01; 1. DNS 的核心作用 DNS&#xff08;域名系统&#xff0c;Domain Name System&#xff09;是互联网中用于将人类可读的域名转…

【Unity3D】Jenkins Pipeline流水线自动构建Apk

目录 一、准备阶段 二、创建Pipeline流水线项目 三、注意事项 四、扩展 1、Pipeline添加SVN更新项目Stage阶段 一、准备阶段 1、安装tomcat 10.0.5 Index of apache-local/tomcat/tomcat-10 2、安装jdk 17 Java Archive Downloads - Java SE 17.0.13 and later 3、…

【数据结构】(9) 优先级队列(堆)

一、优先级队列 优先级队列不同于队列&#xff0c;队列是先进先出&#xff0c;优先级队列是优先级最高的先出。一般有两种操作&#xff1a;返回最高优先级对象&#xff0c;添加一个新对象。 二、堆 2.1、什么是堆 堆也是一种数据结构&#xff0c;是一棵完全二叉树&#xff0c…

2025.2.15

web [HNCTF 2022 Week1]Interesting_include&#xff1a; 直接打开 PHP代码片段包含两部分&#xff1a;一个主脚本和一个潜在的被包含文件。主脚本负责处理GET请求&#xff0c;特别是filter参数&#xff0c;而被包含文件&#xff08;假设为./flag.php&#xff09;似乎包含了我…

CentOS 7.8 安装MongoDB 7教程

文章目录 CentOS 7.8 安装MongoDB 7教程一、准备工作1. 系统更新2. 权限 二、添加MongoDB软件源1. 创建MongoDB的yum源文件2. 添加以下内容3. 保存并退出编辑器 三、安装MongoDB1. 更新yum缓存2. 安装MongoDB 四、启动MongoDB服务1. 启动MongoDB2. 设置MongoDB开机自启动 五、配…

ElasticSearch基础和使用

ElasticSearch基础 1 初识ES相关组件 &#xff08;1&#xff09;Elasticsearch是一款非常强大的开源搜索引擎&#xff0c;可以帮助我们从海量数据中快速找到需要的内容。Elasticsearch结合kibana、Logstash、Beats组件 也就是elastic stack&#xff08;ELK&#xff09; 广泛应…

[C++]多态详解

目录 一、多态的概念 二、静态的多态 三、动态的多态 3.1多态的定义 3.2虚函数 四、虚函数的重写&#xff08;覆盖&#xff09; 4.1虚函数 4.2三同 4.3两种特殊情况 &#xff08;1&#xff09;协变 &#xff08;2&#xff09;析构函数的重写 五、C11中的final和over…