深入理解指针
在初步了解了指针的用法之后,我们可以想一想,既然一个变量有地址,而且在上一篇文章中我们知道了一个数组也有地址,那么函数、字符串这些东西有没有地址呢?如果有,那这些地址有什么用?我们又要怎样来使用这些地址?看了这篇文章,相信你对指针会有更深入的理解!
目录
字符指针变量
数组指针变量
二维数组传参的本质
函数指针变量
typedef关键字
函数指针数组
转移表
字符指针变量
在对指针有了一个初步的了解之后,我们知道了存放一个char类型的数据的地址要用char*类型的指针变量,所以字符指针便是指向字符型数据的指针变量。
其实字符指针还有其他的用法,这里先补充一点字符串的知识:每一个字符串在内存中都占用一段连续的存储空间,并有唯一确定的首地址。因此,只要将字符串的首地址赋值给字符指针,即可让字符指针指向一个字符串。对于常量字符串而言,常量字符串本身代表的就是存放它的常量存储区的首地址,是一个地址常量。
来看下面这段代码
#include<stdio.h>
int main()
{char* pc = "hello";printf("%c\n", *pc);printf("%s\n", pc);return 0;
}
运行结果
像这段代码中的定义的字符串"hello"是常量字符串(也叫字符串字面量),而常量字符串的一个特性是不能被修改。在这段代码中,其实是将字符串"hello"的首地址(也是首字符'h'的地址)赋值给了字符指针pc。所以对pc变量解引用之后打印出来就是h,而在使用%s打印字符串的时候,我们只需要提供首字符的地址就可以了,在打印的时候通过字符串的首地址就可以找到该地址所指向的空间,然后像后打印,直到遇到'\0'为止。
注意:因为数组名是一个地址常量,其值是不能被修改的,所以这里不能使用++操作使其指向字符串中的某个字符。
那为什么常量字符串不能被修改呢?来看下面这段代码
#include<stdio.h>
int main()
{char str1[] = "hello world";char str2[] = "hello world";char* str3 = "hello world";char* str4 = "hello world";printf("str1 = %p\n", str1);printf("str2 = %p\n", str2);printf("str3 = %p\n", str3);printf("str4 = %p\n", str4);return 0;
}
运行结果
在这段代码中,str1和str2都是字符数组的数组名,而数组名又是数组首元素的地址,str3和str4都是字符指针变量,它们都指向的是一个常量字符串。所以str1,str2,str3,str4这4个变量都是char*类型的指针变量,而这4个变量所指向的字符串的内容都是一样的,然而我们通过打印地址的方式来打印这4个变量时发现,str1和str2是不同的两个指针变量,说明str1和str2指向的是两块不同的空间,而str3和str4存储的地址是一样的,这意味着str3和str4指向的是同一块空间。
其实在c/c++中,会把常量字符串存储到单独的一块内存区域,这个区域叫只读数据区,而只读数据区中的数据是不能被修改的。这里str3和str4存储的地址之所以一样,是因为这两个常量字符串的内容是一样的,而且常量字符串中的内容是不会被修改的,所以内容相同的常量字符串,只需要保存一份就够了!
数组指针变量
前面我们说过的指针数组和现在要说的数组指针听起来很类似,但他们是完全不同的两个概念,下面做一个简单的辨析
指针数组:是数组,是存放指针变量的数组
数组指针:是指针,是存放数组地址的指针
下面举个例子
int main()
{int arr[3] = { 1,2,3 };int(*parr1)[3] = &arr;//parr1是数组指针变量int arr1[3] = {1,2,3};int arr2[3] = { 4,5,6 };int arr3[3] = { 7,8,9 };int* parr2[3] = { arr1,arr2,arr3 };//parr2是指针数组return 0;
}
在这段代码中,parr1是数组指针,存放的是数组arr的地址,在int(*parr1)[3] = &arr;这句代码中,*先和parr1结合,表示parr1是指针变量,要注意的是,*parr1两边的圆括号()是必不可少的(圆括号的优先级最高),int表示parr1指向的数组元素类型,parr1是数组指针变量名,[10]中的10表示parr1指向的数组的元素个数,而parr1的类型是int(*)[3],即去掉变量名之后剩下的东西
parr2是指针数组,是存放int*类型元素的数组,在int* parr2[3] = { arr1,arr2,arr3 };这句代码中,*先和int结合,表示parr2存放的元素类型是int*,因为parr2中存放的是整型数组的数组名,而数组名表示的是数组首元素的地址,即地址的类型就是int*
二维数组传参的本质
说完了数组指针之后,我们可以会想数组指针有什么用呢?下面我们来看看数组指针在二维数组传参时的使用场景
在刚学完数组时,我们是用下标来访问数组元素的,例如
#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;
}
在这个代码中,我们在传参的时候,传的是二维数组的形式,打印的时候也是通过arr[i][j]来访问二维数组的第i行j列的元素的,运行结果:
接下来我们看看二维数组传参的时候用指针来接收,并用数组指针访问二维数组的场景
#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;
}
这里首先要说明几点:
1:二维数组的数组名也是数组首元素的地址
2:二维数组可以理解为一维数组的数组,所以二维数组的首元素就是第一行(是一个一维数组)
3:二维数组的数组名就是二维数组第一行的地址
这里二维数组arr的每一行(一维数组)的类型是int [5],所以每一行的地址类型就是数组指针类型int(*)[5]
总结:二维数组传参的本质是传地址,传递的是二维数组第一行的一维数组的地址
所以在打印的时候, *(*(p + i) + j)这句代码相当于上面的代码中用数组下标a[i][j]来访问数组元素,*(p + i)相当于找到了二维数组的第i行,就相当于是二维数组第i行的数组名,因为数组名是数组首元素的地址,所以*(p + i)等价于a[i][0],*(*(p + i) + j)则相当于找到了二维数组第i行的第j个元素,等价于 *(*(p + i) + j)
运行结果:
函数指针变量
函数指针变量顾名思义,应该就是用来存放函数地址的变量。
通过前面的学习,我们了解到数组名就是数组首元素的地址,同理可知,一个函数名就是这个函数(的源代码)在内存中的起始地址,编译器会将不带()的函数名解释为该函数的入口地址。
要得到函数的地址,可以通过&函数名的方式,也可以直接用函数名,因为函数名就是该函数的地址。
说了这么多,那函数指针到底应该如何定义呢?来看下面这段代码
#include<stdio.h>
void test()
{printf("hello\n");
}
int Add(int x, int y)
{return x + y;
}
int main()
{//定义存放test函数地址的指针变量void (*pf1)() = &test;void (*pf2)() = test;//定义存放Add函数地址的指针变量 int(*pf3)(int, int) = Add;int(*pf4)(int, int) = &Add;int(*pf5)(int x, int y) = Add;int(*pf6)(int x, int y) = &Add;printf("%p\n", pf1);printf("%p\n", pf2);printf("%p\n", pf3);printf("%p\n", pf4);printf("%p\n", pf5);printf("%p\n", pf6);return 0;
}
这段代码中,对函数指针变量的定义五花八门,但其实本质上都是一样的。这儿以int(*pf5)(int x, int y) = Add;这句代码为例,首先int表示变量pf5指向的函数的返回类型是int类型,pf5是函数指针的变量名,它旁边的*表示pf5是一个指针变量,其次(int x, int y)表示的是pf5指向函数的参数类型和个数,这里的x和y也可以不写,只要说明清楚这个函数的参数类型和个数就可以了,而在定义test函数的指针变量时,因为该函数没有参数,所以在参数部分就只有一个(),最后要说的一点就是函数名和&函数名没有区别,用哪个都可以
既然函数指针已经定义好了,那么函数指针的参数又是什么呢?还是以int(*pf5)(int x, int y) = Add;这句代码为例,去掉变量名之后,剩下的int(*)(int x, int y)就是它的类型。
运行结果:
可以看出,pf1、pf2是同一种函数指针类型,pf3、pf4、pf5、pf6是同一种函数指针类型。
函数指针变量定义好了之后,接下来就是如何使用了。
#include <stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{int(*pf)(int, int) = Add;printf("%d\n", (*pf)(2, 1));printf("%d\n", pf(0, 5));return 0;
}
在这段代码中,定义了一个加法函数Add,又定义了一个指向Add函数的函数指针pf,然后就是通过pf指针来调用Add函数了
在调用的过程中,我们发现不管是对pf变量解引用还是直接用pf指针来调用Add函数,都是可以的。实际上这里的*就相当于是一个摆设,甚至在调用的时候可以写成(****pf),只不过是加一颗*可以明显的看出pf是一个指针变量,而对函数的调用是通过对函数指针的解引用来实现的
运行结果:
typedef关键字
typedef关键字是用来对类型进行重命名的
例如,定义一个无符号的整型变量本来是
unsigned int n = 0;
但是每次定义都用unsigned int比较麻烦,就可以用typedef对unsigned int类型重命名一下,就比如叫uint
typedef unsigned int uint;
这样一命名之后,用uint定义一个无符号的整型变量和用unsigned int定义一个无符号的整型变量是一样的
unsigned int n1;
uint n2;
n1和n2都是无符号的整型变量
typedef关键字同样也可以对指针类型重命名,这样做除了是代码写起来更简单外,还可以将一些复杂的代码给简单化,就比如下面这句代码
void (*signal(int , void(*)(int)))(int);
我想,每一个看到这句代码的人,尤其是对于初学者来说都会忍不住头大,那这句代码到底是什么意思呢?
其实这句代码是一次函数调用,函数名就是signal,而这个函数有两个参数,第一个参数的类型是int,第二个参数的类型是一种函数指针void (*)(int),而void (*)(int)这个函数指针指向的函数的参数是int类型,返回类型是void,那signal这个函数的返回类型是什么呢?其实去掉函数名和函数参数之后,剩下的就是这个函数的返回类型,即void (*)(int)。要注意的是,这里的函数名和函数的参数必须要写在这个函数的返回类型void (*)(int)的*旁边的,而不是直接写在返回类型void (*)(int)的后面,这是语法规定的
那这句代码可不可以写的更简单一点呢?当然是可以的
这里我们可以用typedef关键字对函数指针类型void (*)(int)重命名,例如叫pfun_t。那上面那句看着很复杂的代码就可以简化成下面的样子
pfun_t signal(int, pfun_t);
相比于上面的代码而言,重命名之后写的代码明显要简单很多
函数指针数组
函数指针数组,顾名思义就是存放函数指针的数组。
那函数指针数组应该如何定义呢?来看下面的代码
int (*parr[2])(int, int);
在这句代码中,parr先和[ ]结合,说明parr是数组,而数组中存放的是int (*)(int, int)类型的函数指针,该函数的两个参数都是int类型,返回值也是int类型
转移表
说完了函数指针数组,接下来我们来看看这个东西到底要如何使用,即用函数指针数组来实现这里的转移表
首先我们来实现一个可以计算两个数的加、减、乘、除的计算器,代码如下
#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;
}
这个代码,主要是由打印的菜单,先告诉使用者每个数字代表的功能,然后由使用者选择的不同数字,来调用不同的函数,以此来模拟实现这个简单的计算器,可以供道友们参考,具体的细节这里不再过多赘述
但是我们在这里也可以看到,在这段代码中,每次选择了一个数字之后,所执行的代码是类似的,所以这里有代码冗余的情况
下面我们通过函数指针数组来实现一下
#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;
}
这段代码中,先是定义好了计算两个数的加、减、乘、除的函数,由于这四个函数的地址的类型都是int (*)(int, int),所以可以用函数指针数组来保存这四个函数的地址,通过访问数组元素的方式来调用这四个函数,这样做的一个好处是避免了上面那个代码中由于所执行的操作类似而出现代码冗余的情况,代码相对更加灵活
相关文章:

深入理解指针
在初步了解了指针的用法之后,我们可以想一想,既然一个变量有地址,而且在上一篇文章中我们知道了一个数组也有地址,那么函数、字符串这些东西有没有地址呢?如果有,那这些地址有什么用?我们又要怎…...

自动驾驶合集(更新中)
文章目录 车辆模型控制路径规划 车辆模型 车辆模型基础合集 控制 控制合集 路径规划 规划合集...

Chapter 14 scoped样式以及data函数
欢迎大家订阅【Vue2Vue3】入门到实践 专栏,开启你的 Vue 学习之旅! 文章目录 1 scoped样式1.1 全局样式1.2 局部样式1.3 工作原理2 data函数 1 scoped样式 1.1 全局样式 全局样式是指作用于整个应用程序的样式,不论在哪个组件中定义&#x…...

Golang | Leetcode Golang题解之第557题反转字符串中的单词III
题目: 题解: func reverseWords(s string) string {length : len(s)ret : []byte{}for i : 0; i < length; {start : ifor i < length && s[i] ! {i}for p : start; p < i; p {ret append(ret, s[start i - 1 - p])}for i < le…...

区块链技术在电子政务中的应用
💓 博客主页:瑕疵的CSDN主页 📝 Gitee主页:瑕疵的gitee主页 ⏩ 文章专栏:《热点资讯》 区块链技术在电子政务中的应用 区块链技术在电子政务中的应用 区块链技术在电子政务中的应用 引言 区块链技术概述 定义与原理 发…...

Simulink中Matlab function使用全局变量
目录 一. 引言二. 普通Matlab function使用全局变量三. Simulink中的Matlab function使用全局变量四. 如何利用Matlab function的全局变量施加随机噪声 一. 引言 最近发现了之前仿真中的一个问题,记录一下备忘。 Matlab function中有时候需要用到全局变量…...

WPF-控件的属性值的类型转化
控件的属性值需要转成int、double进行运算的,可以使用一下方法 页面代码 <StackPanel Margin"4,0,0,0" Style"{StaticResource Form-StackPanel}"> <Label Content"替换后材料增加金额ÿ…...
海思Hi3516DV300上播放G711U音频文件
在海思Hi3516DV300芯片运行Linux系统下,可通过如下函数来实现G711U音频文件的播放: int ADecPlayVoiceFile(const char* filename) {FILE* pfd;AUDIO_STREAM_S stStream;HI_S32 s32Ret,length,decLength,allLength 0,aChn 0;HI_U8 buf[SAMPLE_AUDIO_P…...

Linux源码阅读笔记-V4L2框架基础介绍
V4L2视频设备驱动基础 V4L2 是专门为 Linux 设备设计的整套视频框架(其主要核心在 Linux 内核,相当于 Linux 操作系统上层的视频源捕获驱动框架)。为上层访问系统底层的视频设备提供一个统一的标准接口。V4L2 驱动框架能够支持多种类型设备&…...

列表(list)
一、前言 本次博客主要讲解 list 容器的基本操作、常用接口做一个系统的整理,结合具体案例熟悉自定义内部排序方法的使用。如有任何错误,欢迎在评论区指出,我会积极改正。 二、什么是list list是C的一个序列容器,插入和删除元素…...
使用Python抓取数据的实战指南
引言 在当今信息爆炸的时代,数据已成为一种宝贵的资源。无论是学术研究、市场分析,还是个人兴趣,数据的获取都是至关重要的一步。Python,凭借其强大的库和简洁的语法,成为了数据抓取(也称为网络爬虫或网页…...

GIC寄存器介绍
往期内容 本专栏往期内容,interrtupr子系统: 深入解析Linux内核中断管理:从IRQ描述符到irq domain的设计与实现Linux内核中IRQ Domain的结构、操作及映射机制详解中断描述符irq_desc成员详解Linux 内核中断描述符 (irq_desc) 的初始化与动态分…...

c++实现B树(下)
书接上回小吉讲的是B树的搭建和新增方法的实现(blog传送门🚪:B树实现上)(如果有小可爱对B树还不是很了解的话,可以先看完上一篇blog,再来看小吉的这篇blog)。那这一篇主要讲的是B树中…...

外星人入侵
学习于Python编程从入门到实践(Eric Matthes 著) 整体目录:外星人入侵文件夹是打包后的不必在意 图片和音效都是网上下载的 音效下载网站:Free 游戏爆击中 Sound Effects Download - Pixabay 运行效果:可以上下左右移…...
【数据仓库】hbase的安装与简单操作
HBase 是一个分布式的、面向列的开源数据库,它支持大规模数据存储,并且是 Hadoop 生态系统的一部分。HBase 能够在廉价的硬件上运行,并提供对大量数据的随机、实时读写访问。下面是关于如何在 Linux 系统上安装 HBase 以及进行一些基本操作的…...

为什么RNN(循环神经网络)存在梯度消失和梯度爆炸?
1️⃣ 原理分析 RNN前向传播的公式为: x t x_t xt是t时刻的输入 s t s_t st是t时刻的记忆, s t f ( U ⋅ x t W ⋅ s t − 1 ) s_tf(U\cdot x_tW\cdot s_{t-1}) stf(U⋅xtW⋅st−1),f表示激活函数, s t − 1 s_{t-1} …...

【数据库】数据库迁移的注意事项有哪些?
数据库迁移是一个复杂且关键的过程,需要谨慎处理以确保数据的完整性和应用程序的正常运行。以下是一些数据库迁移时需要注意的事项: 1. 充分的前期准备 1.1 评估迁移需求 明确目标:确定迁移的具体目标,例如添加新字段、修改现…...

MQTT协议解析 : 物联网领域的最佳选择
1. MQTT协议概述 1.1 MQTT协议是什么 MQTT : Message Queuing Telemetry Transport 模式 : 发布 / 订阅主题优点 : 代码量小、低带宽、实时可靠应用 : 物联网、小型设备、移动应用MQTT 常用端口 : 1883 MQTT是一个网络协议,和HTTP类似,因为轻量简单&…...

pycharm中from[本地包]import文件/模块出现问题(最最最全方法!)
1.通过PYTHONPATH的方法在此处将路径添加上,能够让IDE访问得到。 2.通过选中目标文件所在的文件的文件夹单击右键,如下图所示可以看到下方的mark directory as选项中存在 存在excluded,选择此项可解决问题,如果仍有问题可以尝试其…...

MongoDB在现代Web开发中的应用
💓 博客主页:瑕疵的CSDN主页 📝 Gitee主页:瑕疵的gitee主页 ⏩ 文章专栏:《热点资讯》 MongoDB在现代Web开发中的应用 MongoDB在现代Web开发中的应用 MongoDB在现代Web开发中的应用 引言 MongoDB 概述 定义与原理 发展…...

【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...

如何在看板中体现优先级变化
在看板中有效体现优先级变化的关键措施包括:采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中,设置任务排序规则尤其重要,因为它让看板视觉上直观地体…...

visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...

1.3 VSCode安装与环境配置
进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件,然后打开终端,进入下载文件夹,键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...
大模型多显卡多服务器并行计算方法与实践指南
一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...
关于 WASM:1. WASM 基础原理
一、WASM 简介 1.1 WebAssembly 是什么? WebAssembly(WASM) 是一种能在现代浏览器中高效运行的二进制指令格式,它不是传统的编程语言,而是一种 低级字节码格式,可由高级语言(如 C、C、Rust&am…...

tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
服务器--宝塔命令
一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行! sudo su - 1. CentOS 系统: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...