【c语言】——深入理解指针2

文章目录

  • 一、指针数组
    • 指针数组模拟二维数组
  • 二、数组指针
    • 二维数组传参的本质
  • 三、字符指针变量
  • 四、函数指针变量
    • 4.1. 函数指针的应用
    • 4.2 两端有趣的代码
    • 4.3. typedef关键字
      • 4.3.1 typedef 的使用
      • 4.3.2. typedef与#define对比
  • 五、函数指针数组
    • 函数指针数组的应用

一、指针数组

指针数组到底是指针还是数组呢?

整型数组 int arr[] : 存放整型的数组;
字符数组 char arr[] : 存放字符的数组;
可以类比得到:
指针数组 int* arr[] : 存放指针的数组.

因此,指针数组本质上是数组,数组中的每个元素都存放的是地址(指针),每个元素的类型为指针类型.

示例:下方代码中
parr先和 [ ] 结合,因此它是一个数组,这个数组有3个元素,每个元素的类型为int*

int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 1,2,3,4,5 };
int arr3[5] = { 1,2,3,4,5 };
int* parr[3] = {arr1,arr2,arr3};

将数组名去掉就可以看出它的类型,即int* [3],也就是指针数组

指针数组模拟二维数组

int main()
{//创建三个整型数组int arr1[5] = { 1,2,3,4,5 };int arr2[5] = { 1,2,3,4,5 };int arr3[5] = { 1,2,3,4,5 };//由于数组名就是数组首元素地址,将每个数组名存放到指针数组parr中int* parr[3] = {arr1,arr2,arr3};int i = 0;for (i = 0;i < 3;i++)//遍历三个数组名{int j = 0;for (j = 0;j < 5;j++)//遍历每个数组{printf("%d ", parr[i][j]);}printf("\n");}return 0;
}

对 parr [ i ] [ j ] 进行解释:

  1. parr [ i ] 访问的是parr中的元素,找到的是第 i 个数组名,指向一维整形数组.
    比如 parr [ 0 ]找到的是 arr1, parr [ 1 ]找到的是 arr2, parr [ 2 ]找到的是 arr3, 因此 parr [ 0 ] 就是arr1数组名,本质就是第一个数组首元素地址
  2. parr [ i ] [ j ] 指向一维数组中的元素,找到的是第 i 个数组的第 j 个元素.
    比如parr [ 0 ] [ 0 ] == arr1 [ 0 ] == 1
  3. arr1, arr2, arr3本质上是指针,因此可以存到指针数组 parr 中.
  4. parr模拟的二维数组并非是真正的二维数组,因为二维数组在内存中是连续存放的.

在这里插入图片描述

二、数组指针

数组指针到底是数组还是指针呢?

整型指针 int* p = &a : p 是存放整型变量地址、指向整型变量的指针
字符指针char* pc = 'c' : pc 是存放字符变量地址、指向字符变量的指针
浮点型指针 float* pf = &f: pf 是存放浮点型变量地址、指向浮点型变量的指针
数组指针:int (*p) [ 5 ] = &arr : p 是指向整型数组的指针.

因此,数组指针本质上是指针,是指向数组的指针,存放的是数组的地址
示例:

int arr[] = { 1,2,3 };
int (*p)[3] = &arr;
  1. p先与*结合,说明p是一个指针变量,然后与 [ ] 结合,说明它指向的是一个数组,数组中有三个元素,每个元素的类型为 int
  2. 数组指针的初始化:数组指针存的是数组的地址,因此将整个数组取地址赋值给数组指针就可以

指针数组与数组指针的区分:
指针数组存放指针的数组,“指针”是定语,修饰“数组”
数组指针指向数组的指针,“数组”是定语,修饰“指针”

二维数组传参的本质

int arr[3][5] = { { 1,2,3,4,5 }, {2,3,4,5,6},{3,4,5,6,7 } }

在这里插入图片描述
arr[3][5]可以看作是一个有三个元素的一维数组每个元素是一个一维数组,第一个元素是{1,2,3,4,5},第二个元素是{2,3,4,5,6},第三个元素是{3,4,5,6,7},因此,遵循数组名是数组首元素地址的原则,那么认为二维数组的数组名其实就是第一行数组的地址,数组的地址类型为数组指针,那么二维数组传参的本质传递的是第一行数组的地址,需要用数组指针类型来接收

用数组指针来打印二维数组

void print(int (*p)[5])
{int i = 0;for (i = 0;i < 3;i++){for (int j = 0;j < 5;j++){printf("%d ", *(*(p + i) + j));//也可以写成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 } };print(arr);return 0;
}

在这里插入图片描述

p 是数组指针,p + i 得到的是第 i 行元素的地址,*( p + i ) 得到的是第 i 行元素的数组名,*( p + i ) + j 得到的是第 i 行第 j 个元素的地址,再进行解引用就得到该元素

有人会问,p + i 是地址,*( p + i ) 得到的应该是数据,*( p + i ) + j得到的还是数字,*(*( p + i ) + j)就是对数字进行解引用,没有意义了.

实际上 p + i 是二维数组中第 i 行数组的地址,那么 p + 0 == p == arr [ 0 ], 以第一行数组为例,第一行数组是一维数组,arr [ 0 ] [ j ] 得到的是第一行数组第 j 个元素,那么arr [ 0 ]本质上就是第一行数组的数组名,也就是第一行数组首元素 1 的地址. 因此*(p + i)实际得到的是第 i 行的数组名本质还是地址,想得到 i 行 j 列 这个元素需要 *(*(p + i) + j).

二维数组在想象中是以行和列形式存在的,但是在内存中的存储是连续的, 因此只需要知道首元素地址就可以连续访问

在这里插入图片描述
当然,二维数组传参,形参部分也可以写成二维数组(不能省略列),也可以写成数组指针形式.

三、字符指针变量

char* a = "abcdef";

其中,a是一个字符指针,存放的是字符串第一个字符的地址,并不是整个字符串的地址,并且该字符串为常量字符串,不能被改变,因此严谨来说应该在前加上const

const char* a = "abcdef";

示例:

int main()
{char str1[] = "hello bit.";char str2[] = "hello bit.";const char* str3 = "hello bit.";const char* str4 = "hello bit.";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");
  1. str3 和 str4 是两个相同的常量字符串, 在内存中只会开辟一块内存空间,不同的指针指向相同的常量字符串的时候,指向的是同一块内存空间,因此 str3 和 str4 是相同的
  2. 用相同的常量字符串去初始化两个字符数组,内存会为两个数组单独开辟两个内存空间,因此str1和str2是不同的,他们分别指向两个不同的地址

在这里插入图片描述

四、函数指针变量

函数指针:指向函数的指针,存放的是函数的地址.
首先了解什么是函数的地址:

void test()
{printf("666\n");
}
int main()
{test();printf("test = %p\n", test);printf("&test = %p\n", &test);return 0;
}

在这里插入图片描述

可以看出,函数名 test 和 &test 的地址是一样的,因此函数名就表示函数的地址,想存储函数的地址,就需要函数指针变量

函数指针的初始化:

int Add(int x, int y)
{return x + y;
}
int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = &Add;

Add是一个函数,pf1 (pf2) 先与*结合,表明其是一个指针,然后与后面的( )结合,表明其指向的是一个函数,函数内部的参数有两个,分别都是 int 类型,函数的返回类型是 int 型.

去掉变量名pf1,就可以看出他的类型是 int ( * ) ( int , int )函数指针类型
在这里插入图片描述

4.1. 函数指针的应用

用函数指针实现两个数相加:
在这里插入图片描述

可以看出,上述三种方法都可以实现Add函数的调用

  1. 直接用函数名调用函数.
  2. 利用函数指针 pf 取得Add函数的地址,然后对其解引用*pf得到函数名,再进行调用.
  3. 直接用函数指针pf进行调用.
    总结:函数名Add实际就是函数的地址,函数指针pf也是函数的地址,函数名直接调用函数就相当于用地址调用函数,那么直接用pf调用,不解引用也是可以的

比如:
在这里插入图片描述

实际上可以看出:
005513CF为函数Add的地址
函数指针pf,*pf得到的都是005513CF,即函数Add的地址,因此利用函数指针调用函数的时候可以不用解引用,直接进行调用

4.2 两端有趣的代码

1.第一段代码

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

在这里插入图片描述

从内部向外看

  1. 黄色部分void ( * ) ( ),表明类型为函数指针类型
  2. 蓝色括号 ( ) 0,表示将整型 0 强制类型转换成函数指针类型,得到的是一个地址 00000000,意味着我们假设0地址处放着无参,返回类型为void的函数
  3. 绿色括号内部是对其进行解引用,得到的是该地址处的函数名
  4. 右侧黑色括号表示调用这个函数
    因此调用的是0地址处的函数,函数的返回类型为void,函数参数为无参
  1. 第二段代码
void (*signal(int, void(*)(int)))(int);

在这里插入图片描述

signal,先与后面接的蓝色括号结合,表明它指向一个函数,说明它是一个函数名,该函数内部有两个参数,类型分别是 整型int 和 函数指针类型 void(*)(int),然后将函数名和函数参数去掉可以得到:
在这里插入图片描述
函数还缺一个返回类型,因此该函数的返回类型为函数指针类型

将这段代码改写以下就可以更清楚看到:
在这里插入图片描述

  1. signal 是函数名,指向的函数内部有两个参数,参数类型分别是整型和函数指针类型,该函数的返回类型为函数指针类型
  2. 这种写法只是便于分析,在编译器中这种写法是不允许的,只能写成第一种形式

4.3. typedef关键字

4.3.1 typedef 的使用

如果类型写起来太复杂,我们可以将其重新定义,进行简化,比如
将unsigned int 重命名为uint

typedef unsigned int uint;

对于指针数组来说,正确的重命名为 parr_t 的方式是,parr_t 必须在 * 的右边

typedef int(*parr_t)[5] 

同样对于函数指针,将其重命名为pfun_t,必须放在*的右边

typedef void(*pfun_t)(int,void(*)())//新的类型名必须放在*的右边 
pfun_t signal(intpfun_t);//对代码进行简化

4.3.2. typedef与#define对比

typedef int* ptr_t;
ptr_t p1,p2;

int*类型重命名为 ptr_t ,p1, p2均为指针变量

#define PTR_t int*
PTR_t p1,p2;

将PTR_t 定义为int* 类型,那么p1为指针变量,p2为整型变量 ,因为替换之后为: int* p1, p2,是一个逗号表达式,*只会与p1结合

五、函数指针数组

函数指针数组:存放函数指针数组,本质是一个数组,用来存放函数指针变量.
函数指针数组的定义:

int (* parr [ 3 ] ) ( )

parr先与[ ]结合,表明它是一个数组,该数组有3个元素,每个元素的类型为函数指针类型 int ( * ) ( )

函数指针数组的应用

转移表的实现:
实现一个具有加减乘除的计算器:

void menu()
{printf("1.add   2. sub \n");printf("3.mul   4. div \n");printf("0.exit \n");
}
int Add(int x, int y)
{return x + y;
}
int Sub(int x, int y)
{return x - y;
}
int Mul(int x, int y)
{return x * y;
}
int Div(int x, int y)
{return x / y;
}int main()
{int a = 0;int b = 0;int input = 0;int ret = 0;do {menu();printf("请选择:");scanf_s("%d", &input);switch (input){case 1:printf("请输入两个参数:");scanf_s("%d %d", &a, &b);ret = Add(a, b);printf("%d \n", ret);break;case 2:printf("请输入两个参数:");scanf_s("%d %d", &a, &b);ret = Sub(a, b);printf("%d \n", ret);break;case 3:printf("请输入两个参数:");scanf_s("%d %d", &a, &b);ret = Mul(a, b);printf("%d \n", ret);case 4:printf("请输入两个参数:");scanf_s("%d %d", &a, &b);ret = Div(a, b);printf("%d \n", ret);break;case 0:printf("退出\n");break;default:printf("请重新输入:");break;}} while (input);return 0;
}

通过选择1、2、3、4实现计算器的运算,但是这种方法有一个缺点,每一个case下面几乎都是相同的功能,唯独不同的就是进行函数的调用,因此我们应该想办法将这一重复的功能封装成一个函数去实现

利用函数指针数组实现计算器:

void menu()
{printf("1.add   2. sub \n");printf("3.mul   4. div \n");printf("0.exit \n");
}
int Add(int x, int y)
{return x + y;
}
int Sub(int x, int y)
{return x - y;
}
int Mul(int x, int y)
{return x * y;
}
int Div(int x, int y)
{return x / y;
}int main()
{int a = 0;int b = 0;int input = 0;int ret = 0;int (*pf[5])(int, int) = { 0,Add,Sub,Mul,Div };do {menu();printf("请选择:");scanf_s("%d", &input);if (input <= 4 && input >= 1){printf("请输入操作数:");scanf_s("%d %d", &a, &b);ret = pf[input](a,b);printf("%d\n", ret);}else if(input==0){printf("退出\n");}else{printf("重新输入");}} while (input);return 0;
}

在这里插入图片描述
利用函数指针实现转移表:

将重复的功能封装到calc函数中实现,通过函数指针调用加减乘除函数
main是主调函数,calc函数是回调函数

void menu()
{printf("1.add   2. sub \n");printf("3.mul   4. div \n");printf("0.exit \n");
}
int Add(int x, int y)
{return x + y;
}
int Sub(int x, int y)
{return x - y;
}
int Mul(int x, int y)
{return x * y;
}
int Div(int x, int y)
{return x / y;
}
void calc(int (*pf)(int, int))
{int a, b;printf("请输入两个参数:");scanf_s("%d %d", &a, &b);int ret = pf(a, b);printf("%d \n", ret);
}
int main()
{int a = 0;int b = 0;int input = 0;int ret = 0;do {menu();printf("请选择:");scanf_s("%d", &input);switch (input){case 1:calc(Add);break;case 2:calc(Sub);break;case 3:calc(Mul);case 4:calc(Div);break;case 0:printf("退出\n");break;default:printf("请重新输入:");break;}} while (input);return 0;
}

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

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

相关文章

python20-while和for in的美

课程&#xff1a;B站大学 记录python学习&#xff0c;直到学会基本的爬虫&#xff0c;使用python搭建接口自动化测试就算学会了&#xff0c;在进阶webui自动化&#xff0c;app自动化 分支语句那些事儿 循环的类型循环的作用循环的构成要素while 循环while 循环实战循环语句 for…

私人笔记:动手学大模型应用开发llm-universe项目环境创建

项目代码&#xff1a;datawhalechina/llm-universe: 本项目是一个面向小白开发者的大模型应用开发教程&#xff0c;在线阅读地址&#xff1a;https://datawhalechina.github.io/llm-universe/ 项目书&#xff1a;动手学大模型应用开发 一、初始化项目 uv init llm-universe-te…

剖析 Rust 与 C++:性能、安全及实践对比

1 性能对比&#xff1a;底层控制与运行时开销 1.1 C 的性能优势 C 给予开发者极高的底层控制能力&#xff0c;允许直接操作内存、使用指针进行精细的资源管理。这使得 C 在对性能要求极高的场景下&#xff0c;如游戏引擎开发、实时系统等&#xff0c;能够发挥出极致的性能。以…

详细讲解一下Java中的Enum

Java 中的 枚举&#xff08;Enum&#xff09; 是一种特殊的类&#xff0c;用于表示一组固定且有限的常量&#xff08;如状态、类型、选项等&#xff09;。它提供类型安全的常量定义&#xff0c;比传统的常量&#xff08;如 public static final&#xff09;更强大和灵活。以下是…

首席人工智能官(Chief Artificial Intelligence Officer,CAIO)的详细解析

以下是**首席人工智能官&#xff08;Chief Artificial Intelligence Officer&#xff0c;CAIO&#xff09;**的详细解析&#xff1a; 1. 职责与核心职能 制定AI战略 制定公司AI技术的长期战略&#xff0c;明确AI在业务中的应用场景和优先级&#xff0c;推动AI与核心业务的深度…

LeetCode【剑指offer】系列(位运算篇)

剑指offer15.二进制中1的个数 题目链接 题目&#xff1a;编写一个函数&#xff0c;输入是一个无符号整数&#xff08;以二进制串的形式&#xff09;&#xff0c;返回其二进制表达式中数字位数为 ‘1’ 的个数&#xff08;也被称为 汉明重量).&#xff09;。 思路一&#xff…

前端路由缓存实现

场景&#xff1a;以一体化为例&#xff1a;目前页面涉及页签和大量菜单路由&#xff0c;用户想要实现页面缓存&#xff0c;即列表页、详情页甚至是编辑弹框页都要实现数据缓存。 方案&#xff1a;使用router-view的keep-alive实现 。 一、实现思路 1.需求梳理 需要缓存模块&…

Buildroot编译过程中下载源码失败

RK3588编译编译一下recovery&#xff0c;需要把buildroot源码编译一遍。遇到好几个文件都下载失败&#xff0c;如下所示 pm-utils 1.4.1这个包下载失败&#xff0c;下载地址http://pm-utils.freedesktop.org/releases 解决办法&#xff0c;换个网络用windows浏览器下载后&…

Operator 开发入门系列(一):Hello World

背景 我们公司最近计划将产品迁移到 Kubernetes 环境。 为了更好地管理和自动化我们的应用程序&#xff0c;我们决定使用 Kubernetes Operator。 本系列博客将记录我们学习和开发 Operator 的过程&#xff0c;希望能帮助更多的人入门 Operator 开发。 目标读者 对 Kubernete…

Java基础知识面试题(已整理Java面试宝典pdf版)

什么是Java Java是一门面向对象编程语言&#xff0c;不仅吸收了C语言的各种优点&#xff0c;还摒弃了C里难以理解的多继承、指针等概念&#xff0c;因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表&#xff0c;极好地实现了面向对象理论…

科学视角下的打坐:身心获益的实证探究

在快节奏的现代生活中&#xff0c;人们在追求物质丰富的同时&#xff0c;也愈发关注身心的健康与平衡。古老的打坐修行方式&#xff0c;正逐渐走进科学研究的视野&#xff0c;并以大量实证数据展现出对人体多方面的积极影响。​ 什么是打坐&#xff1a; 打坐是一种养生健身法…

javaSE————网络编程套接字

网络编程套接字~~~~~ 好久没更新啦&#xff0c;蓝桥杯爆掉了&#xff0c;从今天开始爆更嗷&#xff1b; 1&#xff0c;网络编程基础 为啥要有网络编程呢&#xff0c;我们进行网络通信就是为了获取丰富的网络资源&#xff0c;说实话真的很神奇&#xff0c;想想我们躺在床上&a…

MySQL性能调优(三):MySQL中的系统库(mysql系统库)

文章目录 MySQL性能调优数据库设计优化查询优化配置参数调整硬件优化 MySQL中的系统库1.5.Mysql中mysql系统库1.5.1.权限系统表1.5.2.统计信息表1.5.2.1.innodb_table_stats1.5.2.2.innodb_index_stats 1.5.3.日志记录表1.5.3.1. general_log1.5.3.2. slow_log 1.5.4.InnoDB中的…

多个路由器互通(静态路由)无单臂路由(简单版)

多个路由器互通&#xff08;静态路由&#xff09;无单臂路由&#xff08;简单版&#xff09; 开启端口并配ip地址 维护1 Router>en Router#conf t Router(config)#int g0/0 Router(config-if)#no shutdown Router(config-if)#ip address 192.168.10.254 255.255.255.0 Ro…

关于 AI驱动的智慧家居、智慧城市、智慧交通、智慧医疗和智慧生活 的详细解析,涵盖其定义、核心技术、应用场景、典型案例及未来趋势

以下是关于 AI驱动的智慧家居、智慧城市、智慧交通、智慧医疗和智慧生活 的详细解析&#xff0c;涵盖其定义、核心技术、应用场景、典型案例及未来趋势&#xff1a; 一、AI智慧家居 1. 定义与核心功能 定义&#xff1a;通过AI与物联网&#xff08;IoT&#xff09;技术&#…

【ESP32|音频】一文读懂WAV音频文件格式【详解】

简介 最近在学习I2S音频相关内容&#xff0c;无可避免会涉及到关于音频格式的内容&#xff0c;所以刚开始接触的时候有点一头雾水&#xff0c;后面了解了下WAV相关内容&#xff0c;大致能够看懂wav音频格式是怎么样的了。本文主要为后面ESP32 I2S音频系列文章做铺垫&#xff0…

端侧大模型综述On-Device Language Models: A Comprehensive Review

此为机器翻译&#xff0c;仅做个人学习使用 设备端语言模型&#xff1a;全面回顾 DOI&#xff1a;10.48550/arXiv.2409.00088 1 摘要 大型语言模型 &#xff08;LLM&#xff09; 的出现彻底改变了自然语言处理应用程序&#xff0c;由于减少延迟、数据本地化和个性化用户体验…

推流265视频,网页如何支持显示265的webrtc

科技发展真快&#xff0c;以前在网页上&#xff08;一般指谷歌浏览器&#xff09;&#xff0c;要显示265的视频流&#xff0c;都是很鸡肋的办法&#xff0c;要么转码&#xff0c;要么用很慢的hls&#xff0c;体验非常不好&#xff0c;而今谷歌官方最新的浏览器已经支持265的web…

redis的sorted set的应用场景

Redis 的 Sorted Set&#xff08;有序集合&#xff0c;简称 ZSet&#xff09; 结合了 Set 的去重特性 和 按分数&#xff08;score&#xff09;排序 的特性&#xff0c;非常适合需要 高效排序 或 范围查询 的场景。以下是它的典型应用场景及示例&#xff1a; 实时排行榜 场景&…

18-21源码剖析——Mybatis整体架构设计、核心组件调用关系、源码环境搭建

学习视频资料来源&#xff1a;https://www.bilibili.com/video/BV1R14y1W7yS 文章目录 1. 架构设计2. 核心组件及调用关系3. 源码环境搭建3.1 测试类3.2 实体类3.3 核心配置文件3.4 映射配置文件3.5 遇到的问题 1. 架构设计 Mybatis整体架构分为4层&#xff1a; 接口层&#…