13 指针高级

server/2025/3/17 14:29:16/

指针高级

指针做函数参数

学习函数的时候,讲了函数的参数都是值拷贝,在函数里面改变形参的值,实参并不会发生改变。

在这里插入图片描述

如果想要通过形参改变实参的值,就需要传入指针了。

在这里插入图片描述

注意:虽然指针能在函数里面改变实参的值,但是函数传参还是值拷贝。不过指针虽然是值拷贝,但是却指向的同一片内存空间。

在这里插入图片描述

指针做函数返回值

返回指针的函数,也叫作指针函数。

和普通函数一样,只是返回值类型不同而已,先看一下下面这个函数,非常熟悉对不!

int fun(int x,int y);

接下来看另外一个函数声明

int* fun(int x,int y);

这样一对比,发现所谓的指针函数也没什么特别的。

注意:

  • 不要返回临时变量的地址
  • 可以返回动态申请的空间的地址
  • 可以返回静态变量和全局变量的地址

函数指针

如果在程序中定义了一个函数,那么在运行时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

函数指针定义

函数返回值类型 (* 指针变量名) (函数参数列表);

  • “函数返回值类型”表示该指针变量所指向函数的 返回值类型;

  • “函数参数列表”表示该指针变量所指向函数的参数列表。

那么怎么判断一个指针变量是指向变量的指针,还是指向函数的指针变量呢?

  • 看变量名的后面有没有带有形参类型的圆括号,如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。

  • 函数指针没有++和 --运算

函数指针使用

定义一个实现两个数相加的函数。

int add(int a,int b)
{return a+b;
}
int main()
{int (*pfun)(int,int) = add;int res = pfun(5,3);printf("res:%d\n",res);return 0;
}

在给函数指针pfun赋值时,可以直接用add赋值,也可以用&add赋值,效果是一样的。

在使用函数指针时,同样也有两种方式,1,pfun(5,3); 2,(*pfun)(5,3)

案例

计算器

用函数指针实现一个简单的计算器,支持+、-、*、/、%

//plus sub multi divide mod		//加 减 乘 除 取余

当功能太多时,switch语句太长,因此不是一种好的编程风格。好的设计理念应该是把具体的操作和和选择操作的代码分开。

函数指针作为转换表

转换表就是一个函数指针数组。

#include<stdio.h>
#include<math.h>// 转换表
// 转换表 step1:
//(1.1)声明 转台转移函数
double add(double, double);
double sub(double, double);
double mul(double, double);
double div(double, double);
double hypotenuse(double, double);
//(1.2)声明并初始化一个函数指针数组    pfunc:数组   数组元素:函数指针  返回值:double型数据
double(*pfunc[])(double, double) = { add, sub, mul, div, hypotenuse };//5个转移状态//状态转移函数的实现
double add(double a, double b){	return a + b;}
double sub(double a, double b){ return a - b; }
double mul(double a, double b){ return a * b; }
double div(double a, double b){ return a / b; }
double hypotenuse(double a, double b){ return sqrt(pow(a, 2) + pow(b, 2)); }void test()
{//转换表 step2:调用 函数指针数组int n = sizeof(pfunc) / sizeof(pfunc[0]);//转移表中 包含的元素个数(状态转移函数个数)for (int i = 0; i < n; ++i){printf("%.2lf\n",pfunc[i](3, 4));}
}
int main()
{test();return 0;
}

typedef

一,使用typedef为现有类型创建别名,给变量定义一个易于记忆且意义明确的新名字。

  • 类型过长,用typedef可以简化一下
typedef unsigned int UInt32
  • 还可以定义数组类型
typedef int IntArray[10];
IntArray arr;				//相当于int arr[10]

二、使用typedef简化一些比较复杂的类型声明。

例如:

typedef int (*CompareCallBack)(int,int);

上述声明引入了PFUN类型作为函数指针的同义字,该函数有两个类型分别为int、int、char参数,以及一个类型为int的返回值。通常,当某个函数的参数是一个回调函数时,可能会用到typedef简化声明。
例如,承接上面的示例,我们再列举下列示例:

int callBackTest(int a,int b,CompareCallBack cmp);

callBackTest函数的参数有一个CompareCallBack类型的回调函数。在这个示例中,如果不用typedef,callBackTest函数声明如下:

int callBackTest(int a,int b,int (*cmp)(int,int));

从上面两条函数声明可以看出,不使用typedef的情况下,callBackTest函数的声明复杂得多,不利于代码的理解,并且增加的出错风险。

所以,在某些复杂的类型声明中,使用typedef进行声明的简化是很有必要的。

回调函数

介绍

钩子函数、回调函数、注册函数,挂钩子这些我们代码中经常涉及到的东西,是否已经困扰你很久了?它们究竟是怎么回事,究竟怎么用?下面我来为你一一解答。

什么是钩子函数?

钩子函数也叫回调函数,是通过函数指针来实现的,那我们来看看什么是函数指针。

函数指针

首先要明确一点,函数也可以作为函数参数来传递的。

例如,定义函数指针:

int (* g_callback) (int x, int y);

有两个函数:

/*返回两个参数中的最大值*/
int Max(int x, int y){return x>y?x:y;}/*返回两个参数中的最小值*/
int Min(int x, int y){return x<y?x:y;}int main(int argc, char* argv[])
{int a = 10;int b = 15;/*我们让函数指针先后指向不同的函数*/   g_callback = Max;printf("%d\n", g_callback(a,b));	/*相当于调用函数Max*/g_callback = Min;printf("%d\n", g_callback(a, b); /*相当于执行函数Min*/return 0;
}

分别输出:15 10

这样,同样调用callback,两次却完成不同的功能,神奇吧?这就是函数指针的妙用。

Max,Min函数就是钩子函数了,把函数指针g_pFun指向函数Max,Min的过程,就是“挂钩子”的过程,把钩子函数“挂”到函数指针上,很形象。

钩子函数作用

有人可能有疑问,那么这里为什么不直接调用Max和Min函数呢?

这是因为,我们在写main函数的时候,可能还不知道它会完成什么功能,这时候留下函数指针作为接口,可以挂上不同的函数完成不同的功能,究竟执行什么功能由钩子函数的编写者完成。

钩子函数使用

那我们平时怎么用的呢?

在我们的代码中,常常把挂钩子的过程叫做注册,会提供一个注册函数,让使用者把自己编写的钩子函数挂在已经声明的函数指针上,这个注册函数的参数就是我们的函数指针了,比如,我们可以给刚才的函数指针提供一个注册函数:

int RegFun( int (* pFun)(int x, int y) ) /*注册函数的参数是函数指针*/
{g_pFun = pFun;return 0;
}

调用RegFun(Max)和RegFun(Min),就可以把钩子函数挂上去了。

注意:为了便于使用,函数指针往往被声明为全局变量,这也是刚才把函数指针的名字命名为g_callback的原因。

下面我们来进行一下实战演习,比如,我们要实现定时器的功能,但是具体的操作还不确定,我们完成这样的代码:

#include<stdio.h>
#include<time.h>
#include<stdbool.h>#if 0
typedef void(*TimeOutCallback)();
//定时器函数
void timer(int ms, TimeOutCallback func)
{static int start = 0;if (clock() - start >=  ms){	func();start = clock();}
}void show()
{printf("%s\n", "show");
}int main()
{while (true){timer(1000,show);}return 0;
}
#else#define TIMER_MAX_SIZE 10typedef void(*TimeOutCallback)(int);
int startTime[TIMER_MAX_SIZE];
int timeout[TIMER_MAX_SIZE];
TimeOutCallback timerFunc[TIMER_MAX_SIZE];
int size;//注册定时器
int registerTimer(int ms, TimeOutCallback func)
{startTime[size] = 0;timerFunc[size] = func;timeout[size] = ms;return size++;
}
//移除定时器
void removeTimer(int id)
{for (int i = id; i < size - 1; i++){startTime[i] = startTime[i + 1];timerFunc[i] = timerFunc[i + 1];timeout[i] = timeout[i + 1];}startTime[size - 1] = 0;size--;
}//定时器更新函数
void updateTimer()
{for (int i = 0; i < size; i++){//如果第一次开始初始化一次,防止开始就调用一次//if (startTime[i] == 0) startTime[i] = clock();if (clock() - startTime[i] >= timeout[i]){timerFunc[i](i);startTime[i] = clock();}}
}void show(int id)
{printf("%s\n", "show");
}void show1(int id)
{printf("%s\n", "show1");
}void show2(int id)
{printf("%s\n", "show2");removeTimer(id);
}int main()
{registerTimer(1000, show);registerTimer(2000, show1);registerTimer(3000, show2);while (true){updateTimer();}return 0;
}
#endif

因为应用模块无法修改平台的代码,只能调用平台提供的注册函数。

这样,平台部分无需修改任何代码,只是应用模块注册了不同的钩子函数,就能够完成不同的功能,这就是钩子函数的妙用。

其他案例

  • 实现一个与类型无关的查找函数

如何看懂复杂的指针

指针大家都学过了,简单的指针相信大家都不放在眼里,就不再赘述,但是复杂的你能理解吗?能理解指针就学的差不多了,至于如何运用只要你看懂指针就知道应该给它赋什么值,怎么用。

  • 首先咱们一起来看看这个: int (*fun)(int *p)
    • 首先需要分析这个是不是一个指针,如果是,是什么指针?如果不是,那是什么?
      1. 根据(*fun)可知,fun是一个指针
      2. 然后看fun的后面是一个函数参数列表,可以确定是一个指向函数的指针
      3. 指向的函数的返回值是什么类型呢,再回头看看最前面发现是一个int
      4. 最后我们可以根据这个函数指针写出对应的函数

结果如下:

int foo(int *p)
{reutrn 0;
}

右左法则

上面我们分析了一个函数指针,那结果是如何得出来的呢?全靠经验吗,NO,其实是有方法的。

这个方法叫做右左法则

  • 右左法则不是C标准里面的内容,它是从C标准的声明规定中归纳出来的方法。C标准的声明规则,是用来解决如何创建声明的,而右左法则是用来解决如何辩识一个声明的。

  • 右左法则使用:

      1. 首先从最里面的圆括号(应该是标识符)看起,然后往右看,再往左看;
      2. 每当遇到圆括号时,就应该调转阅读方向;
      3. 一旦解析完圆括号里面所有东西,就跳出圆括号;
      4. 重复这个过程知道整个声明解析完毕。

在这里插入图片描述

案例走起

1.int (*p[5])(int*)

解析:

  1. 从标识符p开始,p先与[]结合形成一个数组,然后与*结合,表示是一个指针数组;
  2. 然后跳出这个圆括号,往后看,发现了一个函数的参数列表,说明数组里面装的是函数指针;
  3. 在跳出圆括号,往前看返回类型,可以确定函数指针的类型。
2. int (*fun)(int *p,int (*pf)(int *))

解析:

  1. fun与*结合形成指针;
  2. 往后看是一个参数列表,说明是一个函数指针,只不过参数里面还有一个函数指针;
  3. 往前看可以确定函数指针的返回类型。
3. int (*(*fun)[5])(int *p)

解析:

  1. fun与*结合,形成指针;
  2. 往后看发现了一个[5]说明是一个指向数组的指针;
  3. 再往前看,发现有一个*,说明数组里面存的是指针;
  4. 跳出圆括号往后看,发现了参数列表,说明数组里面存的是函数指针;
  5. 再往前看可以确定函数指针的返回类型。

在这里插入图片描述

4. int (*(*fun)(int *p))[5]

解析:

  1. fun与*结合,形成指针;

  2. 往后看发现了参数列表,说明fun是一个函数指针;

  3. 往前看遇到了*说明,函数指针的返回类型是一个指针,是什么指针继续往后解析;

  4. 往后看发现了[5] 说明是一个数组指针,最前面一个int,说明fun这个函数指针的返回类型是一个数组的指针

    类型为int (*)[5]

    在这里插入图片描述

5. int(*(*fun())())()

解析:

  1. fun与()结合,说明fun是一个函数;
  2. 往前看发现了一个*,说明函数返回类型为指针,什么指针呢?
  3. 往后看发现了参数列表,fun函数返回的是一个函数指针,那这个函数指针的返回类型是什么呢?
  4. 往前看又发现了一个*,说明函数指针返回类型也是一个指针,那这个指针是什么指针呢?
  5. 往后看又发现了一个参数列表,说明是个函数指针,往前看这个函数指针返回的是int类型

在这里插入图片描述

总结

实际当中,需要声明一个复杂指针时,如果把整个声明写成上面所示的形式,对程序可读性是一大损害。应该用typedef来对声明逐层分解,增强可读性

**指针变量有两种类型:**指针变量的类型和指针所指向的对象的类型

指针变量的类型
只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。

  • int* ptr; //指针的类型是int

  • char* ptr; //指针的类型是char

  • int** ptr; //指针的类型是int**

  • int(*ptr)[3]; //指针的类型是int()[3]

  • int*(*ptr)[4]; //指针的类型是int*(*)[4]

指针变量指向的对象的类型

  • 你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。
    • int*ptr; //指针所指向的类型是int
    • char*ptr; //指针所指向的的类型是char
    • int**ptr; //指针所指向的的类型是int*
    • int(*ptr)[3]; //指针所指向的的类型是int()[3]
    • int*(*ptr)[4]; //指针所指向的的类型是int*()[4]

注意事项:

  • 指针变量也是变量,也有存储空间,存的是别的变量的地址。

    • 要注意指针的值,和指向的对象的值得区别

    • 普通变量中的内存空间存放的是,数值或字符等。 ----直接存取

    • 指针变量中的内存空间存放的是,另外一个普通变量的地址。----间接存取

  • 连续定义多个指针变量时,容易犯错误,比如:int *p,p1;只有p是指针变量,p1是整型变量

  • 避免使用为初始化的指针,很多运行错误都是由于这个原因导致的,而且这种错误又不能被编译器检查所以很难被发现,解决方法:初始化为NULL,报错就能很快找到原因

  • 指针赋值时一定要保证类型匹配,由于指针类型确定指针所指向对象的类型,操作指针是才能知道按什么类型去操作

  • 在用动态分配完内存之后一定要判断是否分配成功,分配成功后才能使用。

  • 在使用完之后一定要释放,释放后必须把指针置为NULL

nt*

  • int(*ptr)[3]; //指针所指向的的类型是int()[3]
  • int*(*ptr)[4]; //指针所指向的的类型是int*()[4]

注意事项:

  • 指针变量也是变量,也有存储空间,存的是别的变量的地址。

    • 要注意指针的值,和指向的对象的值得区别

    • 普通变量中的内存空间存放的是,数值或字符等。 ----直接存取

    • 指针变量中的内存空间存放的是,另外一个普通变量的地址。----间接存取

  • 连续定义多个指针变量时,容易犯错误,比如:int *p,p1;只有p是指针变量,p1是整型变量

  • 避免使用为初始化的指针,很多运行错误都是由于这个原因导致的,而且这种错误又不能被编译器检查所以很难被发现,解决方法:初始化为NULL,报错就能很快找到原因

  • 指针赋值时一定要保证类型匹配,由于指针类型确定指针所指向对象的类型,操作指针是才能知道按什么类型去操作

  • 在用动态分配完内存之后一定要判断是否分配成功,分配成功后才能使用。

  • 在使用完之后一定要释放,释放后必须把指针置为NULL


http://www.ppmy.cn/server/175718.html

相关文章

【Agent】OpenManus 项目架构分析

这是我录制的一个视频&#xff0c;主要是描述我理解的 OpenManus 的思维逻辑&#xff0c;通过这个小的思维逻辑的复现&#xff0c;为后面要再分析其他 Agent 的实现做一个准备。 1. 项目概述 OpenManus 是一个基于大语言模型的智能体框架&#xff0c;旨在提供一个无需邀请码的…

【2025】基于python+django的慢性病健康管理系统(源码、万字文档、图文修改、调试答疑)

系统功能结构图如下 慢性病健康管理系统 课题背景 随着全球人口老龄化的加剧以及生活方式的改变&#xff0c;慢性病的发病率呈上升趋势&#xff0c;给个人健康和社会医疗资源带来了巨大压力。传统的慢性病管理模式存在信息不畅、患者参与度低、医疗资源分配不均等问题&#xf…

Linux防火墙

centos7 通过firewall-cmd命令添加防火墙白名单 。 查看防护墙状态 firewall-cmd --state 或 systemctl status firewalld active (running)-->表示防火墙已经开启&#xff1b;inactive (dead)-->表示防火墙已经关闭 如果是图片这样就是关闭的 开关防火墙 启动防火墙…

VSTO(C#)Excel开发9:处理格式和字体

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 源码指引&#xff1a;github源…

【贪心算法4】

力扣452.用最少数量的剪引爆气球 链接: link 思路 这道题的第一想法就是如果气球重叠得越多那么用箭越少&#xff0c;所以先将气球按照开始坐标从小到大排序&#xff0c;遇到有重叠的气球&#xff0c;在重叠区域右边界最小值之前的区域一定需要一支箭&#xff0c;这道题有两…

基础输入输出技术深度解析与实践指南

1.理解文件 1-1 狭义理解 • ⽂件在磁盘⾥。 • 磁盘是永久性存储介质&#xff0c;因此⽂件在磁盘上的存储是永久性的。 • 磁盘是外设&#xff08;即是输出设备也是输⼊设备&#xff09;。 • 磁盘上的⽂件 本质是对⽂件的所有操作&#xff0c;都是对外设的输⼊和输出 简称 I…

【Java代码审计 | 第十四篇】MVC模型、项目结构、依赖管理及配置文件概念详解

未经许可&#xff0c;不得转载。 文章目录 MVC模型模型&#xff08;Model&#xff09;视图&#xff08;View&#xff09;控制器&#xff08;controller&#xff09;MVC工作流程 项目结构java目录resources目录webapp目录 依赖管理配置文件 MVC模型 MVC&#xff08;Model-View-…

【C语言】函数和数组实践与应用:开发简单的扫雷游戏

【C语言】函数和数组实践与应用&#xff1a;开发简单的扫雷游戏 1.扫雷游戏分析和设计1.1扫雷游戏的功能说明&#xff08;游戏规则&#xff09;1.2游戏的分析与设计1.2.1游戏的分析1.2.2 文件结构设计 2. 代码实现2.1 game.h文件2.2 game.c文件2.3 test.c文件 3. 游戏运行效果4…