深入理解指针
在初步了解了指针的用法之后,我们可以想一想,既然一个变量有地址,而且在上一篇文章中我们知道了一个数组也有地址,那么函数、字符串这些东西有没有地址呢?如果有,那这些地址有什么用?我们又要怎样来使用这些地址?看了这篇文章,相信你对指针会有更深入的理解!
目录
字符指针变量
数组指针变量
二维数组传参的本质
函数指针变量
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 概述 定义与原理 发展…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...
边缘计算医疗风险自查APP开发方案
核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
音视频——I2S 协议详解
I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议,专门用于在数字音频设备之间传输数字音频数据。它由飞利浦(Philips)公司开发,以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...
Netty从入门到进阶(二)
二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架,用于…...
云安全与网络安全:核心区别与协同作用解析
在数字化转型的浪潮中,云安全与网络安全作为信息安全的两大支柱,常被混淆但本质不同。本文将从概念、责任分工、技术手段、威胁类型等维度深入解析两者的差异,并探讨它们的协同作用。 一、核心区别 定义与范围 网络安全:聚焦于保…...
二叉树-144.二叉树的前序遍历-力扣(LeetCode)
一、题目解析 对于递归方法的前序遍历十分简单,但对于一位合格的程序猿而言,需要掌握将递归转化为非递归的能力,毕竟递归调用的时候会调用大量的栈帧,存在栈溢出风险。 二、算法原理 递归调用本质是系统建立栈帧,而非…...
Python爬虫(52)Scrapy-Redis分布式爬虫架构实战:IP代理池深度集成与跨地域数据采集
目录 一、引言:当爬虫遭遇"地域封锁"二、背景解析:分布式爬虫的两大技术挑战1. 传统Scrapy架构的局限性2. 地域限制的三种典型表现 三、架构设计:Scrapy-Redis 代理池的协同机制1. 分布式架构拓扑图2. 核心组件协同流程 四、技术实…...
篇章一 论坛系统——前置知识
目录 1.软件开发 1.1 软件的生命周期 1.2 面向对象 1.3 CS、BS架构 1.CS架构编辑 2.BS架构 1.4 软件需求 1.需求分类 2.需求获取 1.5 需求分析 1. 工作内容 1.6 面向对象分析 1.OOA的任务 2.统一建模语言UML 3. 用例模型 3.1 用例图的元素 3.2 建立用例模型 …...
