【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;一级…

云原生之使用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版本不兼容地方问题。

稀土系储氢合金 压力-组成等温线 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;但是实际的方波是存在一些时钟抖动的。那么什么是时钟抖动呢?时钟抖动&#…

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

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

【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;但本题…

【Training versus Testing】Positive intervals

GPT的解释&#xff1a; 这段话解释了在一维中&#xff0c;针对 "Positive intervals"&#xff08;正间隔&#xff09;假设类别 H 的成长函数 mq(N) 的计算方式&#xff0c;以及如何得出这个成长函数。 1. "Positive intervals" 指的是一维情境中的假设&a…

推荐书目:Python从入门到精通(文末送书)

目录 编辑推荐 内容简介 作者简介 前言/序言 本书特点 编辑推荐 “软件开发视频大讲堂”丛书是清华社计算机专业基础类零售图书畅销品牌之一。 &#xff08;1&#xff09;2008—2023年&#xff0c;丛书累计修订7次&#xff0c;销售400万册&#xff0c;深受广大程序员喜爱。…

Spring安全配置: 构建安全稳固的Java应用

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

2023国赛B题:多波束测线问题 评阅要点完整分析

本文所有分析仅代表个人观点&#xff0c;不代表官方&#xff0c;仅供参考 制作人&#xff1a;川川徒弟 demoo CSDN&#xff1a;川川菜鸟公众号&#xff1a;川川带你学AI 全文采用非编程做法  需要工具&#xff1a; geogebra、matlab工具箱   注&#xff1a; 本文全文不考虑…

前端Layui框架介绍

当涉及到前端UI框架时&#xff0c;Layui&#xff08;简称layui&#xff09;是一个备受欢迎的框架之一。在这篇博客中&#xff0c;我们将深入了解layui&#xff0c;包括其市场占有率、开发语言、使用场景、框架特点以及一些使用案例。 1. 市场占有率 Layui 是一款流行的前端UI框…

Mysql

视频链接 黑马Mysql 基础篇 通用语法及分类 DDL: 数据定义语言&#xff0c;用来定义数据库对象&#xff08;数据库、表、字段&#xff09;DML: 数据操作语言&#xff0c;用来对数据库表中的数据进行增删改DQL: 数据查询语言&#xff0c;用来查询数据库中表的记录DCL: 数据控…

网络原理,了解xml, json,protobuffer的特点

目录 外卖服务器场景带入 大佬们通用的规范格式 一、&#x1f466; 外卖服务器场景 外面服务器沟通有很多模式——展示商家列表等等&#xff0c;只是其中一个&#xff0c;因此需要一个统一的规划了——不同应用程序&#xff0c;里面的自定义格式是不一样的&#xff0c;这样的…