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://xiahunao.cn/news/3248814.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

c++初阶知识——内存管理与c语言内存管理对比

目录 前言&#xff1a; 1.c&#xff0b;&#xff0b;内存管理方式 1.1 new和delete操作自定义类型 2.operator new与operator delete函数 2.1 operator new与operator delete函数 3.new和delete的实现原理 3.1 内置类型 3.2 自定义类型 new的原理 delete的原理 new…

完整教程 linux下安装百度网盘以及相关依赖库,安装完成之后启动没反应 或者 报错

完整教程 linux下安装百度网盘以及相关依赖库&#xff0c;安装完成之后启动没反应 或者 报错。 配置国内镜像源&#xff1a; yum -y install wget mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak wget -O /etc/yum.repos.d/CentOS-Base.repo ht…

数据库端口LookUp功能:从数据库中获取并添加数据到XML

本文将为大家介绍如何使用知行之桥EDI系统数据库端口的Lookup功能&#xff0c;从数据库中获取数据&#xff0c;并添加进输入的XML中。 使用场景&#xff1a;期待以输入xml中的值为判断条件从数据库中获取数据&#xff0c;并添加进输入xml中。 例如&#xff1a;接收到包含采购…

pyqt/pyside QTableWidget失去焦点后,选中的行仍高亮的显示

正常情况下pyqt/pyside的QTableWidget&#xff0c;点击input或者按钮失去焦点后 行的颜色消失了 如何在失去焦点时保持行的选中颜色&#xff0c;增加下面的代码&#xff1a; # 获取当前表格部件的调色板 p tableWidget.palette()# 获取活跃状态下的高亮颜色和高亮文本颜色&a…

AWS-S3实现Minio分片上传、断点续传、秒传、分片下载、暂停下载

文章目录 前言一、功能展示上传功能点下载功能点效果展示 二、思路流程上传流程下载流程 三、代码示例四、疑问 前言 Amazon Simple Storage Service&#xff08;S3&#xff09;&#xff0c;简单存储服务&#xff0c;是一个公开的云存储服务。Web应用程序开发人员可以使用它存…

HZNUCTF2023中web相关题目

[HZNUCTF 2023 preliminary]guessguessguess 这道题目打不开了 [HZNUCTF 2023 preliminary]flask 这道题目考察SSTI倒序的模板注入&#xff0c;以及用env命令获得flag 看题目&#xff0c;猜测是SSTI模板注入&#xff0c;先输入{7*7},发现模板是倒序输入的 输入}}7*7{{返回77…

Postgresql主键自增的方法

Postgresql主键自增的方法 一.方法&#xff08;一&#xff09; 使用 serial PRIMARY KEY 插入数据 二.方法&#xff08;二&#xff09; &#x1f388;边走、边悟&#x1f388;迟早会好 一.方法&#xff08;一&#xff09; 使用 serial PRIMARY KEY 建表语句如下&#xf…

学生管理系统(C语言)(Easy-x)

课 程 报 告 课 程 名 称&#xff1a; 程序设计实践 专 业 班 级 &#xff1a; XXXXX XXXXX 学 生 姓 名 &#xff1a; XXX 学 号 &#xff1a; 231040700302 任 课 教 师 &a…

C++类与对象(补)

感谢大佬的光临各位&#xff0c;希望和大家一起进步&#xff0c;望得到你的三连&#xff0c;互三支持&#xff0c;一起进步 个人主页&#xff1a;LaNzikinh-CSDN博客 文章目录 前言一.默认成员函数二.static三.友元四.匿名对象总结 前言 类的默认成员函数&#xff0c;默认成员…

Mongodb数据库(上)

介绍 是一个基于磁盘存储的开源的、文档类型(数据存储格式)的非关系型数据库。 其数据首先是存放到内存中,当内存不够时,它还可以存放到磁盘里面去 优点 基本概念 数据库 mongodb中的数据库默认是’test‘(就是一进去就是直接使用用的test数据库),如果想要使用其他…

【LabVIEW作业篇 - 2】:分数判断、按钮控制while循环暂停、单击按钮获取book文本

文章目录 分数判断按钮控制while循环暂停按钮控制单个while循环暂停 按钮控制多个while循环暂停单击按钮获取book文本 分数判断 限定整型数值输入控件值得输入范围&#xff0c;范围在0-100之间&#xff0c;判断整型数值输入控件的输入值。 输入范围在0-59之间&#xff0c;显示…

【Python进阶】正则表达式、pymysql模块

目录 一、正则表达式的概述 1、基本介绍 2、快速使用re模块 二、正则的常见规则 1、匹配单个字符 2、原始字符串 3、匹配多个字符 4、匹配开头和结尾 5、匹配分组 三、Python与MySQL交互 1、pymysql模块的安装 2、pymysql的操作步骤 3、connection对象 4、cursor…

基于ANSIBLE中的YAML非标记语言Role角色扮演

YAML-YAML Ain’t Markup Language-非标记语言 语法 列表 fruits:​ - Apple​ - Orange​ - Strawberry​ - Mango 字典 martin:​ name : Martin D’vloper​ job : Developer​ skill : Elite 示例1 需求 通过YAML编写一个简单的剧本&#xff0c;完成web的部署&#xff0c…

【Mongodb-04】Mongodb聚合管道操作基本功能

Mongodb系列整体栏目 内容链接地址【一】Mongodb亿级数据性能测试和压测https://zhenghuisheng.blog.csdn.net/article/details/139505973【二】springboot整合Mongodb(详解)https://zhenghuisheng.blog.csdn.net/article/details/139704356【三】亿级数据从mysql迁移到mongodb…

【Springboot】新增profile环境配置应用启动失败

RT 最近接手了一个新的项目&#xff0c;为了不污染别人的环境&#xff0c;我新增了一个自己的环境配置。结果&#xff0c;在启动的时候总是失败&#xff0c;就算是反复mvn clean install也是无效。 问题现象 卡住无法进行下一步 解决思路 由于之前都是能启动的&#xff0c…

视频素材网站无水印的有哪些?热门视频素材网站分享

当我们走进视频创作的精彩世界时&#xff0c;一个难题常常摆在面前——那些高品质、无水印的视频素材究竟应该在哪里寻找&#xff1f;许多视频创作者感叹&#xff0c;寻找理想的视频素材难度甚至超过了寻找伴侣&#xff01;但不用担心&#xff0c;今天我将为您介绍几个优质的视…

宝塔安装RabbitMq教程

需要放开15672端口&#xff0c;默认账号密码为guest/guest

浅说区间dp(下)

文章目录 环形区间dp例题[NOI1995] 石子合并题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示思路 [NOIP2006 提高组] 能量项链题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示思路 [NOIP2001 提高组] 数的划分题目描述输入格式输出格式样例 #1样例输…

车载音视频App框架设计

简介 统一播放器提供媒体播放一致性的交互和视觉体验&#xff0c;减少各个媒体应用和场景独自开发的重复工作量&#xff0c;实现媒体播放链路的一致性&#xff0c;减少碎片化的Bug。本文面向应用开发者介绍如何快速接入媒体播放器。 主要功能&#xff1a; 新设计的统一播放U…

进程空间的回收以及执行当前进程空间内的另一进程

1.进程的退出 1.exit 功能: 让进程退出,并刷新缓存区 参数&#xff1a; status:进程退出的状态 返回值: 缺省 exit -> 刷新缓存区 -> atexit注册的退出函数 -> _exit 2._exit 功能: 让进程退出,不刷…