C语言指针超详解——进阶篇

C语言指针系列文章目录

入门篇
强化篇
进阶篇

文章目录

  • C语言指针系列文章目录
  • 1. 字符指针变量
  • 2. 数组指针变量
  • 2. 1 概念
    • 2. 2 数组指针变量的初始化
  • 3. 二维数组传参的本质
  • 4. 函数指针变量
    • 4. 1 函数指针变量的创建
    • 4. 2 指针变量的使用
    • 4. 3 两个有趣的代码
      • 4. 3. 1 代码一
      • 4. 3. 1 代码二
      • 4. 3. 3 typedef 关键字
  • 5. 函数指针数组
  • 6. 转移表


1. 字符指针变量

在指针的类型中我们知道有一种指针类型为字符指针 char* 。
一般的是使用方式:

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

这里介绍另一种使用方式:

#include<stdio.h>
int main()
{const char* pstr = "hello world.";//这里是把一个字符串放到pstr指针变量里了吗?printf("%s\n", pstr);return 0;
}

代码 const char* pstr ="hello world.";特别容易让同学以为是把字符串 hello world 放到字符指针 pstr 里了,但是本质是把字符串 hello world. 的首字符的地址放到了pstr中
图解
所以上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中

《剑指offer》中收录了一道和字符串相关的笔试题,我们一起来学习一下!

#include <stdio.h>
int main()
{char str1[] = "hello world.";char str2[] = "hello world.";const char* str3 = "hello world.";const char* str4 = "hello world.";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 和 str2 不一样相信你能够理解。
这里解释一下为什么 str3 为什么和 str4 一样。

因为这里str3和str4指向的是一个同一个常量字符串。C / C++ 会把常量字符串存储到单独的一个内存区域(文字常量区),当几个指针指向同一个字符串的时候,他们实际会指向同一块内存,所以str3和str4相同。

2. 数组指针变量

2. 1 概念

之前我们学习了指针数组:
指针数组是一种数组,数组中存放的是地址(指针)

那么数组指针变量是指针变量还是数组?
答案是:指针变量

我们已经熟悉:

整形指针变量  :int *pint;存放整形变量的地址,能够指向整形数据的指针变量。
浮点型指针变量:float *pf;存放浮点型变量的地址,能够指向浮点型数据的指针变量。

那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。

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

int *p1[10];
int (*p2)[10];

想一想,这两个变量分别是什么类型的?

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

p的类型是什么?
我们类比 int double 等我们熟悉的变量,可以发现:在变量声明中去掉变量名就是变量类型,所以 p 的类型就是:int (*)[10]

2. 2 数组指针变量的初始化

数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?就是之前的博客中提及的 &数组名。

int arr[10] = { 0 };
&arr;//得到的就是数组的地址

如果要存放单个数组的地址,就得存放在数组指针变量中,比如:

int (*p)[10] = &arr;

我们通过调试来看一看
调试可以发现,p 和 &arr 的类型是相同的。

总结:

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

3. 二维数组传参的本质

理解了数组指针的概念,就可以来讲一讲二维数组传参的本质了。
过去我们有一个二维数组的需要传参给一个函数的时候,我们是这样写的:

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

实际上,这也可以解开一个疑惑:为什么写成二维数组的形式传参时,形参的行可以省略,而列不行,因为这是和以指针的形式传参保持一致的。

总结:二维数组传参,形参的部分可以写成数组,也可以写成指针形式。

4. 函数指针变量

4. 1 函数指针变量的创建

什么是函数指针变量呢?

根据前面学习整型指针,数组指针的时候,我们的利用的类比关系,我们不难得出结论:
函数指针变量应该是用来存放函数地址的,未来通过地址能够调用函数的。
那么函数是否有地址呢?
我们做个测试:

#include <stdio.h>
void test()
{printf("hehe\n");
}
int main()
{printf("test : %p\n", test);// %p 打印地址printf("&test: %p\n", &test);return 0;
}

测试结果
确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的方式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法其实和数组指针非常类似。如下:

我们以一个简单的加法函数做例子:

int Add(int a, int b)
{return a + b;
}

我们将它的声明变成指针:

int (*Add)(int a,int b);

那么按照之前类比出来的规则:在变量声明中去掉变量名就是变量类型,可以得知 Add 的类型为:

int (*)(int a,int b)

除此之外,函数形参里的变量名称也是可以省略的,即:

int (*)(int ,int)

这样就知道函数指针变量的类型了。

那么就有:

#include<stdio.h>
void test()
{printf("hehe\n");
}int Add(int x, int y)
{return x + y;
}
int main()
{void (*pf1)() = &test;void (*pf2)() = test;int(*pf3)(int, int) = Add;int(*pf4)(int x, int y) = &Add;//x和y写上或者省略都是可以的return 0;
}

函数指针变量分析:

int (*pf3) (int x, int y)|     |    ------------ |     |          ||     |          pf3指向函数的参数类型和个数的交代|     函数指针变量名pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型

4. 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));   //因为函数名也是地址,所以 pf 无论是否解引用return 0;                    //都可以进行传参
}

4. 3 两个有趣的代码

4. 3. 1 代码一

(*(void (*)())0)();

来分析一下
分析
实际上,这个代码是用来模拟实现开机的(开机时会调用 0 地址处的函数)

4. 3. 1 代码二

void (*signal(int , void(*)(int)))(int);

分析
所以说,上面的代码实际上是对函数 signal 的声明,至于为什么不把整个返回类型放在函数名的前面,是因为C语言的语法要求(函数名放在 * 的右边)。
signal 返回类型为 void ,两个参数分别为 int 和 函数指针变量(这个函数指针变量返回类型为 void ,参数为 int),返回类型为函数指针类型(这个函数指针变量的类型为:void(*)(int) 返回类型为 void ,参数为 int)。

两段代码均出自《C陷阱和缺陷》这本书

4. 3. 3 typedef 关键字

typedef 是用来类型重命名的,可以将复杂的类型简单化。
比如说可以将 unsigned int 这个很长的变量名称改短:

typedef unsigned int uint;
//将unsigned int 重命名为uint

这样在之后就可以用 unit 代替 unsigned int 了。

typedef 同样适用于指针类型。

 typedef int* ptr_t;

但是对于数组指针和函数指针稍微有点区别:
比如:有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:

typedef int(*parr_t)[5]; //新的类型名必须在*的右边

函数指针类型的重命名也是一样的,比如,将 void(*)(int)类型重命名为 pf_t,就可以这样写:

typedef void(*pfun_t)(int);//新的类型名必须在*的右边

这样,我们可以尝试简化一下第两个有趣的代码:

typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

这样就简单多了,更方便代码阅读了。

5. 函数指针数组

如果把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
应该是下面这三个中的哪一个呢?

int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];

答案是第一个。
我们来分析一下第一个:
parr1 是数组名,首先与[3]结合,说明它是一个数组,那么剩下的部分就是这个数组存储的变量类型(int (*)())。

注意:只有类型返回类型,形参完全相同的几个函数的指针才能放进一个函数指针数组中。

6. 转移表

那么函数指针数组有什么用呢?
我们来实现一个计算器:
不使用函数指针数组:

#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;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);switch (input){case 1:printf("输入操作数:");scanf("%d %d", &x, &y);ret = add(x, y);printf("ret = %d\n", ret);break;case 2:printf("输入操作数:");scanf("%d %d", &x, &y);ret = sub(x, y);printf("ret = %d\n", ret);break;case 3:printf("输入操作数:");scanf("%d %d", &x, &y);ret = mul(x, y);printf("ret = %d\n", ret);break;case 4:printf("输入操作数:");scanf("%d %d", &x, &y);ret = div(x, y);printf("ret = %d\n", ret);break;case 0:printf("退出程序\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}

可以发现,这样写的话,在 main 函数中会十分得臃肿,有大量的相同的重复内容,我们可以使用函数指针数组优化这个代码:

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

那么在 main 函数中使用的函数数组就是转移表,是函数指针数组的最主要功能。

谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会尽快更新完毕指针全系列!

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

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

相关文章

汽车底盘控制系统Autosar初步接触

最近接触到汽车底盘控制部分&#xff0c;作为小白&#xff0c;原以为汽车底盘也是要自己手敲代码&#xff0c;结果发现完全不是。记录一下最近的学习心得&#xff0c;初步接触东西不全&#xff0c;但可以当作参考。 对于底盘控制部分的简单理解&#xff1a;simulink做汽车底盘的…

大数据技术基础

一、大数据平台 1.大数据平台方案步骤&#xff1a; ①市场上有哪些大数据平台 ②硬件、系统、业务增长等方面 ③方案是否通过 通过后&#xff1a;按照一期目标投入 先虚拟环境部署联系&#xff0c;再实际部署 《大数据架构介绍》《Hadoop架构解析》《Hadoop集群规划》 《H…

PX4 运行 make px4_sitl_default gazebo 报错

报错原因&#xff1a;最开始我把依赖一直都是在base环境下安装的&#xff0c;没有conda deactivate&#xff0c;而pip install的东西应该装在系统环境&#xff0c;不能装在base环境下&#xff0c;sudo apt 是装在系统环境的 1.检查ros 用鱼香ros安装 wget http://fishros.…

南平建网站公司推荐 好用的b2b独立站模板

床品毛巾wordpress独立站模板 床单、被套、毛巾、抱枕、靠垫、围巾、布艺、枕头、乳胶枕、四件套、浴巾wordpress网站模板。 https://www.jianzhanpress.com/?p4065 打印耗材wordpress自建独立站模板 色带、墨盒、碳粉、打印纸、硒鼓、墨盒、墨水、3D打印机、喷头wordpress…

Pr 2024下载安装,Adobe Premiere pro2024剪辑软件下载合集获取

Premiere Pro 2023中文版简称Pr&#xff0c;pr2023是一款视频编辑软件。 pr 2023不仅可以帮助用户对各种视频进行剪辑、旋转、分割、合并、字幕添加、背景音乐等基础的处理&#xff0c;还能帮助用户进行视频颜色校正、颜色分级、稳定镜头、调整层、更改片段的持续时间和速度、效…

Sentinel规则持久化Push模式两种实现方式

文章目录 sentinel持久化push推模式微服务端的实现具体实现源码分析读数据源写数据源的实现 微服务端解析读数据源流程 修改源码的实现官方demo修改源码实现配置类flowauthoritydegreadparamsystemgateway修改源码 测试补充 前置知识 pull模式 sentinel持久化push推模式 pull拉…

Pycharm 导入 conda 环境

使用时经常在此处卡壳&#xff0c;在此做个记录。 这个位置选择 conda 安装路径下的 python.exe 文件即可

自学鸿蒙HarmonyOS的ArkTS语言<十>@BuilderParam装饰器

作用&#xff1a;当子组件多处使用时&#xff0c;给某处的子组件添加特定功能 一、初始化 1、只能被Builder装饰的方法初始化 2、使用所属自定义组件的builder方法初始化 3、使用父组件的builder方法初始化 - 把父组件的builder传过去&#xff0c;参数名和子组件的builderPar…

ESP32部署TensorFlow Lite

本来是想找一篇中文教程&#xff0c;不过只看到一个英文官方的&#xff0c;也行吧&#xff0c;虽然效率会慢丢丢。 GitHub - espressif/esp-tflite-micro: TensorFlow Lite Micro for Espressif Chipsets 看了一圈&#xff0c;有个中文的&#xff1a; esp-dl/README_cn.md a…

TS 入门(七):TypeScript模块与命名空间

目录 前言回顾泛型编程1. 模块a. 导入和导出b. 默认导出c. 重命名导入和导出 2. 命名空间a. 定义命名空间b. 嵌套命名空间 3. 动态导入与条件导入a. 动态导入b. 条件导入 结语 前言 在前几章中&#xff0c;我们学习了 TypeScript 的基础知识、函数与对象类型、接口与类、以及泛…

K8S 上部署 Emqx

文章目录 安装方式一&#xff1a;快速部署安装方式二&#xff1a;定制化部署1. 使用 Pod 直接部署 EMQX Broker2. 使用 Deoloyment 部署 Pod3. 使用 Services 公开 EMQX Broker Pod 服务4. 通过 kubernetes 自动集群 EMQX MQTT 服务器5. 修改 EMQX Broker 的配置 安装方式一&am…

Large Language Model系列之二:Transformers和预训练语言模型

Large Language Model系列之二&#xff1a;Transformers和预训练语言模型 1 Transformer模型 Transformer模型是一种基于自注意力机制的深度学习模型&#xff0c;它最初由Vaswani等人在2017年的论文《Attention Is All You Need》中提出&#xff0c;主要用于机器翻译任务。随…

【ollama】ollama运行GLM4-9B和CodeGeeX4-ALL-9B

一、下载GGUF模型 glm-4-9b-chat-GGUFcodegeex4-all-9b-GGUF 使用modelscope下载 先安装 pip install modelscope 命令1 modelscope download --modelLLM-Research/glm-4-9b-chat-GGUF --local_dir . glm-4-9b-chat.Q5_K.gguf命令2 modelscope download --modelLLM-Researc…

昇思25天学习打卡营第02天|张量 Tensor

一、什么是张量 Tensor 张量是一种特殊的数据结构&#xff0c;与数组和矩阵非常相似。张量&#xff08;Tensor&#xff09;是MindSpore网络运算中的基本数据结构。 张量可以被看作是一个多维数组&#xff0c;但它比普通的数组更加灵活和强大&#xff0c;因为它支持在GPU等加速…

【D3.js in Action 3 精译_015】1.3 D3 视角下的数据可视化最佳实践(下)

当前内容所在位置 第一部分 D3.js 基础知识 第一章 D3.js 简介 ✔️ 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知 1.2.1 HTML 与 DOM1.2.2 SVG - 可缩放矢量图形1.2.3 Canvas 与 WebGL1.2.4 CSS1.2.5 JavaScript1.2.6 Node 与 JavaScript 框架1.2.7 Observable 记事…

<数据集>猫狗识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;3686张 标注数量(xml文件个数)&#xff1a;3686 标注数量(txt文件个数)&#xff1a;3686 标注类别数&#xff1a;2 标注类别名称&#xff1a;[cat, dog] 序号类别名称图片数框数1cat118811892dog24982498 使用标…

美团看向7亿老铁的钱包,王莆中还有底牌吗?

文&#xff1a;互联网江湖 作者&#xff1a;刘致呈 7月12日&#xff0c;快手、美团宣布战略合作全面升级&#xff0c;未来三年快手美团合作范围将扩大至全国的“百城万店”。 数据上&#xff0c;过去双方的合作是有正向结果的。 美团商家在快手平台的GMV同比提升超38倍&…

FPGA CFGBVS 管脚接法

说明 新设计了1个KU040 FPGA板子&#xff0c;回来之后接上JTAG FPGA不识别。做如下检查&#xff1a; 1、电源测试点均正常&#xff1b; 2、查看贴片是否有漏焊&#xff0c;检查无异常&#xff0c;设计上NC的才NC&#xff1b; 3、反复检查JTAG接线是否异常&#xff0c;贴片是…

关于R语言单因素与多因素线性回归的平均值.

&#x1f3c6;本文收录于《CSDN问答解答》专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&…

【内网穿透】打洞笔记

文章目录 前言原理阐述公网sshfrp转发服务 实现前提第一步&#xff1a;第二步第三步第四步 补充第五步&#xff08;希望隧道一直开着&#xff09;sftp传数据&#xff08;嫌云服务器上的网太慢&#xff09; 前言 租了一个云服务器&#xff0c;想用vscode的ssh远程连接&#xff…