【C语言】指针的进阶(一)

目录

前言

1. 字符指针

2. 指针数组

3. 数组指针

3.1 数组指针的定义

 3.2 &数组名VS数组名

 3.3 数组指针的使用

4. 数组参数、指针参数

4.1 一维数组传参

 4.2 二维数组传参

 4.3 一级指针传参

 4.4 二级指针传参

5. 函数指针


 

前言

指针在C语言中可谓是有着举足轻重的存在,初学C语言的我们在《指针》章节已经接触过了一些指针的知识,知道了指针的概念:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
  3. 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
  4. 指针的运算。

 指针的基础知识已经了解完毕,那么在这一篇博客里,我们将深入探讨指针的一些高级使用。

1. 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* 

 一般使用:

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

 还有一种使用方式:

int main()
{const char* pstr = "abcdef";printf("%s\n", pstr);return 0;
}

上面这种用法很多人会有一个误区,以为是把字符串 abcdef 放到字符指针 pstr 里了,但是本质是把字符串 abcdef 中的首字符的地址放到了pstr中。

下面可以试着证明一下

  • 字符串"abcdef"的地址就是a所在地址,那么"abcdef"[3]相当于 ““a地址[3]”” ,侧面印证了确实是把首字符地址存入了指针pstr中。
  • 数组名就是首元素地址,既然说字符指针存放的是首字符的地址,那么试着用数组下标的方式访问字符指针指向的内容,结果发现一样可以打印出来。
  • 因此完全可以把常量字符串想象成一个数组,然后用字符指针接收,操作起来与数组一致。

注意:最好使用 const 修饰存放字符串的字符指针,因为字符串是常量,不允许被修改,如果修改了程序会崩溃。

一道经典【面试题】,出自《剑指offer》:

以下的最终输出是什么呢? 

#include <stdio.h>
int main()
{char str1[] = "nash.";char str2[] = "nash.";const char* str3 = "nash.";const char* str4 = "nash.";if (str1 == str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n");if (str3 == str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0;
}

【答案】

str1 and str2 are not same

str3 and str4 are same

【解释】

  • str1 和 str2 :相同的常量字符串初始化不同的数组的时候就会开辟出不同的内存块,str1和str2其实是各自创建了一个空间存放nash.,因此它们的地址是不一致的。
  • str3 和 str4 :当用指针指向字符串时,因为nash是常量字符串,是不会被修改的,那么既然不能被修改,编译器就没必要保存多份,只需要一份,然后让指针都指向同一块内存即可,因此地址值是一致的。

【扩展】

int main()
{char str1[] = "hello bit.";char str2[] = "hello bit.";const char* str3 = "hello bit.";const char* str4 = "hello bit.";if (&str3 == &str4)printf("Yes");elseprintf("No");return 0;

【答案】

 结果为No,因为str3和str4本身的地址值是不一样的,只是str3和str4指向的内容是一样的。

2. 指针数组

这里我们复习一下,下面指针数组是什么意思?

int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组

类比一下:

字符数组 —— 存放字符的数组

整型数组 —— 存放整形的数组

那么:

指针数组 —— 存放指针的数组,即存放在数组中的元素都是指针类型

 那么很多人有个疑问,指针数组到底有什么用呢?

很多人直观的感受就是定义abcd,然后将它们的地址都存入整型指针数组中。如下:

错误的使用方式,【没有意义】
int main()
{int a = 0;int b = 1;int c = 2;int* arr[3] = { &a,&b,&c };return 0;
}

但是很少有人会这样使用的,没有这种使用场景,也没有人会这样使用,这样写的没有任何意义。

正确的使用场景之一:

【可以使用指针数组模拟一个二维数组】
int main()
{int arr1[] = { 1,2,3,4,5 };int arr2[] = { 2,3,4,5,6 };int arr3[] = { 3,4,5,6,7 };int* arr[3] = { arr1,arr2,arr3 };//遍历三个数组int i = 0;for ( i = 0; i < 3; i++){int j = 0;for ( j = 0; j < 5; j++){printf("%d ", arr[i][j]);}printf("\n");}return 0;
}

 使用指针数组维护多个数组,模拟出一个二维数组,因此操作也与而二维数组类似。 

 还有使用指针数组维护多个字符串:

 当然还有很多其他的应用场景,但是由于篇幅有限,这里就不再一一列举。

3. 数组指针

3.1 数组指针的定义

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

类比一下:

整型指针 —— 指向整形的指针

字符指针 —— 指向字符的指针

那么:

数组指针 —— 指向数组的指针

下面代码哪个是数组指针?

int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?

 【解释】

p2是数组指针。

p2先和*结合,说明p2是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证p2先和*结合。

 3.2 &数组名VS数组名

往期博客(链接:点击前往)中有提到过:

数组名就是地址,通常来说:数组名是数组首元素的地址。

但是,存在两种特殊情况:

1、sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。

2、&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。

除此之外,所有遇到的数组名都是数组首元素的地址。

 由上图可以证明,第一组和第二组+1时都只是跳过四个字节,即arr表示的是首元素地址。

而第三组可以发现,&arr和&arr+1之间跳过了40个字节,即一整个数组的大小,因此证明&arr表示整个数组。

指针类型决定了指针+1到底+几个字节。

那么我们大胆猜测一下,arr的类型是int*,&arr[0]的类型也是int*,

那么&arr的类型就是int (*)[10],即数组指针存放数组。

 既然看完了什么是数组指针,那么做一道题来检测一下自己是否理解明白。

【练习】

下面代码中pc的类型是什么?

int main()
{char* arr[5];pc = &arr;  //给pc定义类型return 0;
}

 【答案】

char* (*pc)[5] = &arr; 

【解释】

首先pc得是个指针,即(*pc)

指向一个元素为5的数组,即(*pc)[5]

数组每个元素的类型是char*,即char* (*pc)[5]

那么数组指针有哪些使用场景呢?

 3.3 数组指针的使用

错误使用场景:

 这种场景一点也不方便,在使用数组时反而更麻烦了,有一种 “脱裤子放屁” 的感觉。这种使用方式基本上不会出现。

如果非要用指针接收数组,也应该使用指针接收首元素地址,而不是接收整个数组的地址。这才是正确的访问姿势。

其实,在一维数组传参过程中,形参既可以写成数组的形式,又可以写成指针的形式,因为本质上这两种方式都是传递的是数组首元素地址,是互等的。

既然本质上都是地址,那么为什么还要写成数组形式呢?

那是因为能够写成数组形式完全是为了照顾初学者,因为实参是一个数组,形参也定义一个数组来接收这个数组这种方式更能够让初学者理解。因此即使这么写,本质也还是指针。

同理,二维数组传参也有两种方式:

  1. 形参也是使用二维数组的形式。
  2. 形参使用数组指针的形式。

学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:

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

【答案】

  1. arr 是一个能够存放5个整型数据的数组。
  2. parr1 是一个数组,能够存放整型指针的数组,数组大小为10。
  3. parr2 是一个指针,指向元素为10的数组,每个元素的类型是int*
  4. parr3 是一个数组,数组有10个元素,指向一个指针,指针指向的内容是元素为5的数组,每个元素的类型是int,即是一个存放数组指针的数组。

当然parr3看不懂也不要着急,这种形式是很少用到的,只是作为拓展知识点了解即可。

4. 数组参数、指针参数

4.1 一维数组传参

 4.2 二维数组传参

二维数组传参,函数形参的设计只能省略第一个[]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。

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

 【思考】

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

【答案】

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

【思考】

 当函数的参数为二级指针的时候,可以接收什么参数?

【答案】

5. 函数指针

函数指针 —— 指向函数的指针 —— 存放的是函数的地址

 &Add和Add完全等价,下面的例子可以证明。

可以定义 int (*pf2)(int,int) = &Add; 将&Add赋值给pf2, pf2便是函数指针

  • Add引用函数返回ret1
  • 此时对pf2解引用在调用函数返回ret2
  • 对pf2直接调用函数返回ret3
  • 在pf2前面加很多解引用返回ret4

最后打印结果居然发现,四个结果都是一样的。

所以得出结论:函数指针在调用所指向函数时,可以不写*直接和函数名一样调用函数,而*号在这里其实就只是一个摆设,同样是为了照顾初学者的使用习惯,所以才会导致当加了很多*号去解引用时得出来的结果依然是正确的结果。

 下面来看两端有趣的代码:

1、

(*(void (*)())0)();
  •  void (*)()              ———— 函数指针类型
  • void (*)() )0         ———— 对0进行强制类型转换,转换成函数指针。即将地址0当做存放函数指针的地址。
  • (*( void (*)() )0 )()  ———— 通过函数指针调用函数,函数的参数为空。

即上面的代码实在调用0地址处的函数,这个函数没有参数,返回值是void。

 该代码出自《C陷阱和缺陷》

2、

void (*signal(int , void(*)(int)))(int);
  • signal(int, void(*)(int))  ———— signal是一个函数,它有两个参数,整型int和函数指针类型void(*)(int)。
  • void (*signal(int , void(*)(int)) )(int); ———— signal函数的返回类型也是函数指针类型void (*)(int)

但是这个代码看着太复杂了,有没有办法简化呢?

有没有办法将代码写成符合我们习惯的形式这样:返回类型在前,函数名在中间,函数参数在后的:void (*)(int) signal(int , void(*)(int)),这样直接写是肯定不支持的,但是可以通过typedef优化一下:

typedef void(*pfun_t)(int);  //对void(*)(int)重新起名为pfun_t
pfun_t signal(int, pfun_t);

对void(*)(int)重新起名为pfun_t,这样写出来的代码就清楚多了。

 


 如果觉得作者写的不错,求给作者一个大大的点赞支持一下,你们的支持是我更新的最大动力!

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

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

相关文章

Arm发布 Neoverse V2 和 E2:下一代 Arm 服务器 CPU 内核

9月14日&#xff0c;Arm发布了新的处理器内核&#xff1a;V2和E2&#xff0c;在官网已经可以看到相关的TRM 手册了。。 四年前&#xff0c;Arm发布了Neoverse系列的CPU设计。Arm决定加大力度进军服务器和边缘计算市场&#xff0c;专门为这些市场设计Arm CPU内核&#xff0c;而…

CocosCreator3.8研究笔记(十八)CocosCreator UI组件(二)

前面的文章已经介绍了Canvas 组件、UITransform 组件、Widget 组件 。 想了解的朋友&#xff0c;请查看 CocosCreator3.8研究笔记&#xff08;十七&#xff09;CocosCreator UI组件&#xff08;一&#xff09;。 今天我们主要介绍CocosCreator 常用容器组件&#xff1a;Layout …

[npm]脚手架本地全局安装1

[npm]脚手架本地全局安装1 npm link 全局安装npm install 全局安装卸载全局安装的脚手架 该文章是你的脚手架已经开发完成的前提下&#xff0c;你想要本地全局安装该脚手架&#xff0c;便于本地使用脚手架的命令的情况 npm link 全局安装 如果本地开发的项目是个脚手架&#…

【C语言】进阶——指针

目录 ①(●◡●)前言 1.字符指针 ✌字符指针和数组笔试题 2.指针数组 和数组指针 &#x1f44a;指针数组 &#x1f44a;数组指针 &#x1f44a;&数组名和数组名 3.数组传参和指针传参 &#x1f44a;一维数组传参 &#x1f44a;二维数组传参 &#x1f44a;一级…

SpringBoot + Redis + Token 解决接口幂等性问题,挑选最佳方案!

前言 SpringBoot实现接口幂等性的方案有很多&#xff0c;其中最常用的一种就是 token redis 方式来实现。 下面我就通过一个案例代码&#xff0c;帮大家理解这种实现逻辑。 原理 前端获取服务端getToken() -> 前端发起请求 -> header中带上token -> 服务端校验前端传…

云原生之使用Docker部署Nas-Cab个人NAS平台

云原生之使用Docker部署Nas-Cab个人NAS平台 一、Nas-Cab介绍二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍 三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本 四、下载Nas-Cab镜像五、部署Nas-Cab5.1 创建挂载目录5.2 创建Nas-Cab容…

利用idea新创建maven项目时的一些基本配置

1.修改项目默认的maven仓库 file->Settings->Build 2.设置项目的jdk版本 设置完点OK即可。 同样的我们还需要在项目配置中进行修改。 通过以上设置一般就可以解决jdk版本不兼容地方问题。

exgcd, 线性同余方程

878. 线性同余方程 - AcWing题库 878. 线性同余方程 给定 n 组数据 ai,bi,mi&#xff0c;对于每组数求出一个 xi&#xff0c;使其满足 aixi≡bi(modmi)&#xff0c;如果无解则输出 impossible。 输入格式&#xff0c; 第一行包含整数 n。 接下来 n 行&#xff0c;每行包含…

网络安全深入学习第一课——热门框架漏洞(RCE-代码执行)

文章目录 一、代码执行概述二、代码执行相关函数1、eval2、assert3、${ }执行代码 三、命令执行和代码执行的区别 一、代码执行概述 代码执行定义&#xff1a; ------ 应用程序在调用一些能够将字符串转换为代码的函数&#xff08;如PHP中的eval&#xff09;时&#xff0c;没有…

稀土系储氢合金 压力-组成等温线 PCI 的测试方法

声明 本文是学习GB-T 29918-2023 稀土系储氢合金 压力-组成等温线 PCI 的测试方法. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 5 方法2:电化学法 5.1 方法提要 以储氢合金作负极&#xff0c;烧结氢氧化亚镍作正极&#xff0c;氢氧化钾水溶液作电…

postgresql-视图

postgresql-视图 视图概述使用视图的好处 创建视图修改视图删除视图递归视图可更新视图WITH CHECK OPTION 视图概述 视图&#xff08;View&#xff09;本质上是一个存储在数据库中的查询语句。视图本身不包含数据&#xff0c;也被称为 虚拟表。我们在创建视图时给它指定了一个…

数字IC设计之时序分析基础概念汇总

1 时钟Clock 理想的时钟模型是一个占空比为50%且周期固定的方波。时钟是FPGA中同步电路逻辑运行的一个基准。理想的时钟信号如下图: 2 时钟抖动Clock Jitter 理想的时钟信号是完美的方波&#xff0c;但是实际的方波是存在一些时钟抖动的。那么什么是时钟抖动呢?时钟抖动&#…

17-垃圾回收相关概念

目录 一、System.gc()的理解二、内存溢出和内存泄漏2、内存泄漏 三、Stop the World1、什么是 stop the word ? 四、垃圾回收的并行和并发1、并发和并发2、垃圾回收的并行和并发 五、安全点与安全区域1、什么是安全点&#xff1f;2、安全区域 六、强引用&#xff08;不可回收&…

苹果电脑Mac系统运行速度又卡又慢是怎么回事?

通常大家处理Mac运行速度慢的方法不是重启就是清空废纸篓&#xff0c;但是这两种方法对于Mac提速性能的效果是微之甚微的&#xff0c;想要彻底解决Mac运行速度慢&#xff0c;你应该试试一下三种方法~ 1、清理磁盘空间 硬盘空间过少是Mac运行变慢很大的一个因素&#xff0c;各…

华纳云:如何进行Linux CPU中的Kernel space分析

分析 Linux CPU 中的 Kernel Space&#xff08;内核空间&#xff09;通常需要使用一些工具和技术&#xff0c;以便了解内核活动和性能问题。以下是一些方法和工具&#xff0c;可以帮助您在 Linux 系统上进行 Kernel Space 分析&#xff1a; dmesg 命令&#xff1a; dmesg 命令…

解决MySQL 8.0以上版本设置大小写不敏感的问题

MySQL 8.0以上版本默认区分大小写&#xff0c;但在低版本&#xff08;如5.7&#xff09;中&#xff0c;可以通过在my.cnf配置文件的[mysqld]节下添加lower_case_table_names1来设置大小写不敏感。然而&#xff0c;在MySQL 8.0以上版本中&#xff0c;添加此配置可能导致MySQL服务…

【LeetCode刷题笔记】动态规划 — 70.爬楼梯

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 更多算法知识专栏&#xff1a;算法分析&#x1f525; 给大家跳段街舞感谢…

Unity中 UI Shader的基本功能

文章目录 前言一、实现思路1、暴露一个 2D 类型的属性来接受UI的纹理2、设置shader的层级为TransParent半透明渲染层级&#xff0c;一般UI都是在这个渲染层级3、更改混合模式&#xff0c;是 UI 使用的纹理&#xff0c;该透明的地方透明 二、代码实现 前言 Unity中 UI Shader的…

Python爬虫逆向猿人学刷题系列——第七题

题目&#xff1a;采集这5页中胜点列的数据&#xff0c;找出胜点最高的召唤师&#xff0c;将召唤师姓名填入答案中 地址&#xff1a;https://match.yuanrenxue.cn/match/7 本题主要是考察字体的动态变化&#xff0c;同样也是从字体文件下手构造出映射关系就好&#xff0c;但本题…

一个Linux主机巡检脚本

功能&#xff1a; 对Linux主机的物理资源消耗进行监测和统计&#xff0c;对运行在本机上的服务进程进行监测和统计&#xff0c;列出本机上的账户有效期 使用场景&#xff1a; 在没有 Promethes、zabbix、夜莺等监控工具的条件下&#xff0c;通过 crontab 排程应用本程序完成对主…