【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.rhkb.cn/news/136952.html

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

相关文章

单元测试的重要性:编写更安全、更可靠的代码

在软件开发过程中&#xff0c;测试是非常重要的一环。而在众多的测试方法中&#xff0c;单元测试占据了不可忽视的地位。那么&#xff0c;为什么我们需要进行单元测试呢&#xff1f;以下将从理论和实践两方面进行详细的解释。 一、单元测试的定义和目的 单元测试是指对软件中的…

企业架构LNMP学习笔记45

失效机制&#xff08;了解&#xff09; 1&#xff09;如果key过期了&#xff0c;value会及时删除么&#xff1f;空间会及时清理么&#xff1f; 2&#xff09;如果分配的存储空间&#xff0c;写满了&#xff0c;还允许写么&#xff1f; -m可以配置内存大小。 memcached 内部不…

计算机视觉与深度学习-卷积神经网络-纹理表示卷积神经网络-卷积神经网络-[北邮鲁鹏]

这里写目录标题 参考文章全连接神经网络全连接神经网络的瓶颈全连接神经网络应用场景 卷积神经网络卷积层(CONV)卷积核卷积操作卷积层设计卷积步长(stride)边界填充特征响应图组尺寸计算 激活层池化层(POOL)池化操作定义池化操作作用池化层超参数常见池化操作 全连接层(FC)样本…

【vue3页面展示代码】展示代码codemirror插件

技术版本&#xff1a; vue 3.2.40、codemirror 6.0.1、less 4.1.3、vue-codemirror 6.1.1、 codemirror/lang-vue 0.1.2、codemirror/theme-one-dark 6.1.2 效果图&#xff1a; 1.安装插件 yarn add codemirror vue-codemirror codemirror/lang-vue codemirror/theme-one-dar…

分类预测 | MATLAB实现WOA-CNN-LSTM-Attention数据分类预测

分类预测 | MATLAB实现WOA-CNN-LSTM-Attention数据分类预测 目录 分类预测 | MATLAB实现WOA-CNN-LSTM-Attention数据分类预测分类效果基本描述模型描述程序设计参考资料 分类效果 基本描述 1.MATLAB实现WOA-CNN-LSTM-Attention数据分类预测&#xff0c;运行环境Matlab2021b及以…

VLAN相关知识点

文章目录 前言VLANVLAN数据帧格式QinQ报文封装格式总结 前言 本博客仅做学习笔记&#xff0c;如有侵权&#xff0c;联系后即刻更改 科普&#xff1a; 参考网址 VLAN VLAN&#xff08;Virtual Local Area Network&#xff09;即虚拟局域网 是将一个物理的LAN在逻辑上划分成多…

电视盒子什么品牌好?数码小编盘点网络电视盒子排行榜

电视盒子什么品牌好&#xff1f;每个品牌的优势并不一样&#xff0c;我们要根据自己的需求选择&#xff0c;看视频选无广告的&#xff0c;投屏频繁选投屏功能完善的&#xff0c;不懂的新手们可以参考小编分享的网络电视盒子排行榜&#xff0c;堪称目前最专业权威的电视盒子排名…

el-image 和 el-table冲突层级冲突问题

其中原理&#xff0c;很多博客已经所过了&#xff0c;table组件中使用图片&#xff0c;会出现层级过低问题&#xff0c; 网上大部分解决方式是 使用穿透 // 单元格样式 ::v-deep(.el-table__cell) {position: static !important; }我在此不推荐这种解决方式&#xff0c;原因&a…

亚马逊,沃尔玛,速卖通卖家如何实现高效且安全的测评补单

对于卖家而言&#xff0c;测评资源确实是一项宝贵的财富。通过自养号和广告相结合的策略&#xff0c;可以快速有效地提升产品销量、增加转化率以及提高Listing的权重。这是一个很好的方法来优化产品在平台上的表现 现在很多卖家都选择自己养号下单&#xff0c;自己做测评 1.可…

ESD最常用的3种模型?|深圳比创达EMC

为了定量表征 ESD的特性&#xff0c;一般将 ESD 转化成模型表达方式&#xff0c;ESD 的模型有很多种&#xff0c;下面介绍最常用的三种。 1.HBM&#xff1a;Human Body Model&#xff0c;人体模型 该模型表征人体带电接触器件放电&#xff0c;Rb 为等效人体电阻&#xff0c;Cb…

6-1 汉诺塔

汉诺&#xff08;Hanoi&#xff09;塔问题是一个经典的递归问题。 设有A、B、C三个塔座&#xff1b;开始时&#xff0c;在塔座A上有若干个圆盘&#xff0c;这些圆盘自下而上&#xff0c;由大到小地叠在一起。要求将塔座A上的圆盘移到塔座B上&#xff0c;并仍按同样顺序叠放。在…

【论文检索】待更新补充

&#xff08;一&#xff09;相关网址&#xff1a; 1.谷歌学术镜像网址&#xff1a;dailyheadlines.cc 能查到年限、引用量、发表的期刊 2.dblp: 看不到pdf&#xff0c;可以用于查某个作者最近几年都发表了什么论文 作者消歧&#xff1a;通过邮箱和机构判断是不是同一个人 …

亚马逊、ozon、阿里狗等平台如何获取销量和评价?

在亚马逊、Ozon和Allegro上有很多商家还在沿用这国内电商平台的那一套玩法&#xff0c;给店铺不断的做测评&#xff0c;以此提高店铺的流量和销量等&#xff0c;具体应该怎么做测评呢? 方法一&#xff1a;找站点当地的外国人帮助进行产品的测评&#xff0c;这可以通过Faceboo…

ISP技术概述

原本或许是为了对冲手机系统和APP设计无力感而诞生的拍照功能,现今却成为了众手机厂家除背部设计外为数不多可“卷”的地方,自拍、全景、夜景、小视频等旺盛的需求让这一技术的江湖地位迅速变化。对圈内人士而言,这一波变化带来的后摄、双摄、多摄、暗光、防抖、广角、长焦、…

C# Onnx Yolov8 Detect 物体检测

效果 项目 代码 using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System…

C#使用DirectX SDK 加载.x三维模型

最近因为项目要做显示一个三维模型&#xff0c;所以研究了下如何在Winform中加载并显示三维模型。在Windows平台巨硬公司提供了DirectX SDK用于渲染图形&#xff0c;参考了几篇文章做了个demo记录下&#xff0c;以便日后温习只用。这个SDK涉及到了计算机图形学的一些基础知识&a…

uniapp如何实现路由守卫、路由拦截,权限引导

因为uniapp路由的实现方式和以往vue开发的router路由时不太一样&#xff0c;故官方这么说&#xff1a; 经过一番网上冲浪发现&#xff0c;有两种方式可以实现&#xff0c; 第一种方式&#xff1a; 在上述代码中&#xff0c;我们通过监听beforeRouterEnter事件来实现路由守卫。…

功能测试自动化测试流程

1概述 本流程是描述软件功能自动化测试过程中的步骤、内容与方法&#xff0c;明确各阶段的职责、活动与产出物。 2流程活动图 3活动说明 3.1测试计划&#xff08;可选&#xff09; 与以前的测试计划过程一致&#xff0c;只是在原来的测试计划中&#xff0c;添加对项目实施自动…

STM32f103入门(12)USART串口信息发送+接收

USART 介绍串口发送使用工具初始化发送数据接收数据 介绍 电平标准是数据1和数据0的表达方式&#xff0c;是传输线缆中人为规定的电压与数据的对应关系&#xff0c;串口常用的电平标准有如下三种&#xff1a; TTL电平&#xff1a;3.3V或5V表示1&#xff0c;0V表示0 RS232电平&…

通过uni.chooseImage返回的临时路径转为base64

uniapp官方API文档&#xff1a;https://uniapp.dcloud.net.cn/api/media/image.html#chooseimage 代码在后面 chooseimage的succes函数中的res.tempFilePaths&#xff0c;是图片的一个临时路径&#xff0c;没法直接传给后端接口使用&#xff0c;且接口需要的是base64格式的 ge…