指针之矢:C 语言内存幽境的精准飞梭
一、内存和编码
指针理解的2个要点:
- 指针是内存中一个最小单元的编号,也就是地址
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量。
1. 内存
先看一个⽣活中的案例:
假设有⼀栋宿舍楼,把你放在楼⾥,楼上有100个房间,但是房间没有编号,你的⼀个朋友来找你玩, 如果想找到你,就得挨个房⼦去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:
//⼀楼:101,102,103... //⼆楼:201,202,203... //...
有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。
如果把上⾯的例⼦对照到计算机中,⼜是怎么样呢?
- 计算机内存的角色:计算机的 CPU 处理数据时,从内存读取数据,处理后的数据也存回内存。常见内存容量有 8GB、16GB、32GB 等。
- 内存单元划分:为高效管理内存,将其划分为一个个内存单元,每个内存单元大小通常为 1 个字节。
- 计算机存储单位:
- bit(比特位):计算机最小信息单位。
- Byte(字节):1Byte = 8bit 。
- 其他单位换算:1KB = 1024Byte,1MB = 1024KB,1GB = 1024MB,1TB = 1024GB,1PB = 1024TB 。
2. 编码
- 编址的必要性:CPU 访问内存字节空间,需明确其位置。因内存字节众多,所以要对内存编址。
- 编址的实现方式:计算机编址依靠硬件设计,而非记录每个字节地址。CPU 与内存间的地址总线发挥关键作用。
- 地址总线原理:以 32 位机器为例,它有 32 根地址总线,每根线有 0、1 两态(类似电脉冲有无)。一根线表示 2 种含义,两根线表示
= 4 种含义,32 根线可表示
种含义,每种含义对应一个地址。地址信息经地址总线下达给内存,内存找到对应数据,再通过数据总线传入 CPU 内寄存器。
![]()
二、指针和指针类型
指针是什么?
指针理解的2个要点:
- 指针是内存中一个最小单元的编号,也就是地址
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量。
1. 取地址操作符
在 C 语言中,创建变量意味着向内存申请空间。
当我们定义
int a = 10;
时,会在内存中申请 4 个字节来存放整数 10,每个字节都有其对应的地址。如这 4 个字节的地址可能分别为
- 0x006FFD70
- 0x006FFD71
- 0x006FFD72
- 0x006FFD73
要获取变量
a
的地址,我们使用&
操作符。通过以下代码:#include <stdio.h> int main() {int a = 10;&a;//取出a的地址 printf("%p\n", &a);return 0; }
打印获得:
006FFD70
详细过程:
&a取出a所占4个字节中地址较⼩的字节的地址
虽然整型变量占用 4 个字节,但只要知道第一个字节的地址,就可以顺藤摸瓜访问到全部 4 个字节的数据。
2. 指针变量(存储地址的容器)
通过
&
获取的地址是数值,⽐如:0x006FFD70,需存储以便后续使用,指针变量就是专门存放地址的变量。例如:#include <stdio.h> int main() {int a = 10;int * pa = &a;//取出a的地址并存储到指针变量pa中 return 0; }
指针变量中存储的值被视为地址。
3. 指针变量类型
指针变量类型由所指向对象类型和
*
构成,例如:int a = 10; int * pa = &a;
int *pa
,*
表明pa
是指针变量,int
表示它指向整型对象,即存储何种类型对象的地址,指针变量类型就是:对象类型 +*
。
4. 解引用操作符(通过地址访问对象)
获取地址(指针)后,使用解引用操作符
(*)
能找到指针指向的对象。例如:#include <stdio.h> int main() {int a = 100;int* pa = &a;*pa = 0;printf("%d", a);return 0; }
这里
*pa
借助pa
中的地址找到对应空间,实际*pa
就是变量a
,所以*pa = 0
会将a
的值改为 0。5. 指针变量的大小
指针变量大小取决于地址大小。
- 32 位机器有 32 根地址总线,一个地址由 32 个 bit 位组成,需 4 字节存储,所以指针变量大小为 4 字节。
- 64 位机器有 64 根地址线,一个地址由 64 个二进制位组成,需 8 字节存储,指针变量大小为 8 字节。
例如:
#include <stdio.h> int main() {printf("%zd\n", sizeof(char *));printf("%zd\n", sizeof(short *));printf("%zd\n", sizeof(int *));printf("%zd\n", sizeof(double *));return 0; }
64位情况下 :
32位情况下:
结论:
- 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
- 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
- 注意指针变量的⼤⼩和类型⽆关,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的
6. void* 指针
特性与限制:
void*
是特殊指针类型,可理解为无具体类型或泛型指针,能接受任意类型地址。但它不能直接进行指针的 ± 整数和解引用运算。例如:#include <stdio.h> int main() {int a = 10;void* pa = &a;*pa = 10;return 0; }
![]()
应用场景:
void*
指针常用于函数参数,接收不同类型数据地址,实现泛型编程,使一个函数能处理多种类型数据。7. const修饰指针
(1)const 在 * 左边
const 在 * 左边,修饰指针指向的内容,保证该内容不能通过指针改变,但指针变量本身内容可变。例如在
test2
函数中:void test2() {int n = 10;int m = 20;const int* p = &n;*p = 20; // 报错p = &m; // 允许 }
(2)const 在 * 右边
const 在 * 右边:修饰指针变量本身,保证指针变量内容不能修改,但指针指向的内容可通过指针改变。例如在
test3
函数中:void test3() {int n = 10;int m = 20;int * const p = &n;*p = 20; // 允许p = &m; // 报错 }
(3)两边都有 const
两边都有 const:指针指向的内容和指针变量本身都不能修改。例如在
test4
函数中:void test4() {int n = 10;int m = 20;int const * const p = &n;*p = 20; // 报错p = &m; // 报错 }
const修饰指针变量时:
- const如果放在 * 的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
- const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
三、指针类型的意义
- 指针的类型决定了指针向前或者向后走一步有多大(距离)。
- 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
1. 指针±整数
指针类型决定指针前后移动的距离。
#include <stdio.h>int main() {int n = 10;char* pc = (char*)&n;int* pi = &n;printf("%p\n", &n);printf("%p\n", pc);printf("%p\n", pc + 1);printf("%p\n", pi);printf("%p\n", pi + 1);return 0; }
char*
指针 +1 跳过 1 字节,int*
指针 +1 跳过 4 字节。指针 +1 实际跳过 1 个指针指向的元素,指针也可 -1.2. 指针的解引用
指针类型决定解引用时的权限,即一次能操作的字节数。
例如以下两段代码:
#include <stdio.h> int main() {int n = 0x11223344;int *pi = &n; *pi = 0; return 0; }
#include <stdio.h> int main() {int n = 0x11223344;char *pc = (char *)&n;*pc = 0;return 0; }
第一段代码会将 n 的4个字节全部改为0,但是第二段代码却不行。
char*
的指针解引⽤就只能访问⼀个字节,⽽int*
的指针的解引⽤就能访问四个字节。
四、指针运算
指针的基本运算有三种,分别是:
- 指针±整数
- 指针-指针
- 指针的关系运算
1. 指针±整数
原理:由于数组在内存中是连续存放的,只要知道第一个元素的地址,通过指针加减整数可以方便地找到后续元素。
示例代码:
#include <stdio.h> //指针+- 整数 int main() {int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];int i = 0;int sz = sizeof(arr)/sizeof(arr[0]);for(i=0; i<sz; i++){printf("%d ", *(p+i));//p+i 这里就是指针+整数 }return 0; }
p
是指向数组arr
第一个元素的指针。通过p + i
可以将指针移动到数组的第i
个元素的位置,再使用*(p + i)
进行解引用,就能访问该元素。在for
循环中,我们遍历整个数组,依次输出元素。2. 指针 - 指针
原理:当两个指针都指向同一块内存空间时,可以进行指针相减运算,其结果表示两个指针之间元素的数量。
示例代码:
#include <stdio.h> int my_strlen(char *s) {char *p = s;while(*p!= '\0' )p++;return p - s; } int main() {printf("%d\n", my_strlen("abc"));return 0; }
在
my_strlen
函数中,s
指向字符串的起始位置,p
从s
开始向后移动,直到遇到'\0'
终止符。p - s
的结果就是字符串的长度。3. 指针的关系运算
原理:指针本质是地址,可视为一组二进制数(通常以十六进制显示),有大小之分,即低地址和高地址。可以对指针进行大小比较等关系运算。
示例代码:
#include <stdio.h> int main() {int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];int sz = sizeof(arr)/sizeof(arr[0]);while(p < arr + sz) //指针的大小比较 {printf("%d ", *p);p++;}return 0; }
在上述代码中,
arr + sz
指向数组最后一个元素之后的位置。通过p < arr + sz
的关系运算,可确保p
在遍历数组元素时不会越界。在循环中,使用*p
输出元素,并将p
指针向后移动。
五、野指针
1. 野指针的概念
野指针是指指针指向的位置不可知(随机、不正确、无明确限制)。
2. 野指针的成因
(1)指针未初始化
#include <stdio.h> int main() { int *p;//局部变量指针未初始化,默认为随机值 *p = 20;return 0; }
这里
p
作为局部变量未初始化,其值是随机的,对*p
赋值会导致未定义行为,因为不知道p
指向何处。(2)指针越界访问
#include <stdio.h> int main() {int arr[10] = {0};int *p = &arr[0];int i = 0;for(i=0; i<=11; i++){//当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i;}return 0; }
在
for
循环中,当i
大于等于 10 时,p
超出了数组arr
的范围,导致越界,p
成为野指针。(3)指针指向的空间释放
#include <stdio.h> int* test() {int n = 100;return &n;//函数栈帧使用完销毁 } int main() {int*p = test();//但p还能找到这块空间printf("%d\n", *p);return 0; }
test
函数返回局部变量n
的地址,函数调用结束后栈帧销毁,但p
仍指向原位置,此时p
为野指针,访问*p
会导致问题。3. 如何规避野指针
(1)指针初始化
原理:明确指针指向时直接赋值地址,不知指针应指向何处时赋值
NULL
。示例代码:
#include <stdio.h> int main() {int num = 10;int*p1 = #int*p2 = NULL;return 0; }
p1
指向num
的地址,而p2
被初始化为NULL
,表示不指向任何可用地址,访问NULL
会报错,从而避免意外操作。(2)注意指针越界
原理:程序只能访问已申请的内存空间,超出范围即为越界。
示例代码:
int main() {int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];int i = 0;for(i=0; i<10; i++){*(p++) = i;}//此时p已经越界了,可以把p置为NULL p = NULL;//下次使用的时候,判断p不为NULL的时候再使用 //...p = &arr[0];//重新让p获得地址 if(p!= NULL) //判断 {//...}return 0; }
使用
p
遍历数组后将其置为NULL
,后续使用前检查p
是否为NULL
,避免使用野指针。(3)避免返回局部变量的地址
原理:局部变量在函数结束时销毁,其地址不再有效。
#include <stdio.h> int* test() {int n = 100;return &n; } int main() {int* p = test();printf("%d\n", *p);return 0; }
不要返回局部变量的地址,以防止产生野指针。
(4)assert 断言
原理:
assert.h
头文件中的assert()
宏可在运行时确保程序符合指定条件,不符合时报错终止运行示例代码:
#include <assert.h> int main() {int *p = NULL;assert(p!= NULL);return 0; }
如果
p
为NULL
,程序运行到assert(p!= NULL)
会终止,并给出报错信息,包括文件名和行号。通过定义#define NDEBUG
可关闭assert()
宏,在 Debug 阶段使用可方便排查问题,在 Release 版本可选择禁用,避免影响性能。
六、传值调用和传址调用
1. 传值调用
原理:函数调用时,形参是实参的一份临时拷贝,改变形参不影响实参。
代码示例:
#include <stdio.h> void Swap1(int x, int y) {int tmp = x;x = y;y = tmp; } int main() {int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d b=%d\n", a, b);Swap1(a, b);printf("交换后:a=%d b=%d\n", a, b);return 0; }
调用
Swap1
函数,由于是传值调用,x
和y
只是a
和b
的副本,交换x
和y
的值不影响a
和b
的值。2. 传址调用
原理:通过指针传递地址,可在被调函数中修改主调函数的变量。
示例代码:
#include <stdio.h> void Swap2(int*px, int*py) {int tmp = 0;tmp = *px;*px = *py;*py = tmp; } int main() {int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d b=%d\n", a, b);Swap2(&a, &b);printf("交换后:a=%d b=%d\n", a, b);return 0; }
调用
Swap2
函数,将a
和b
的地址传递给px
和py
,在函数内部通过解引用修改指针所指变量的值,实现了a
和b
的交换。总结
- 传址调用能让被调函数和主调函数建立真正联系,当需要修改主调函数中的变量时使用。
- 若仅使用主调函数的变量值进行计算,可采用传值调用。
相关文章:

指针之矢:C 语言内存幽境的精准飞梭
一、内存和编码 指针理解的2个要点: 指针是内存中一个最小单元的编号,也就是地址平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量 总结:指针就是地址,口语中说的指针通常指的是指针变量。…...
uniapp下载打开实现方案,支持安卓ios和h5,下载文件到指定目录,安卓文件管理内可查看到
uniapp下载&打开实现方案,支持安卓ios和h5 Android: 1、申请本地存储读写权限 2、创建文件夹(文件夹不存在即创建) 3、下载文件 ios: 1、下载文件 2、保存到本地,需要打开文件点击储存 使用方法&…...

免费干净!付费软件的平替款!
今天给大家介绍一个非常好用的电脑录屏软件,完全没有广告界面,非常的干净简洁。 电脑录屏 无广告的录屏软件 这个软件不需要安装,打开就能看到界面直接使用了。 软件可以全屏录制,也可以自定义尺寸进行录制。 录制的声音选择也非…...
软路由系统 iStoreOS 中部署 Minecraft 服务器
商业转载请联系作者获得授权,非商业转载请注明出处。协议(License): 知识共享署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)作者(Author): lhDream链接(URL): https://blog.luhua.site/archives/1734968846131 软路由系统 iStoreOS 中部署 Minecraft…...

第 29 章 - ES 源码篇 - 网络 IO 模型及其实现概述
前言 本文介绍了 ES 使用的网络模型,并介绍 transport,http 接收、响应请求的代码入口。 网络 IO 模型 Node 在初始化的时候,会创建网络模块。网络模块会加载 Netty4Plugin plugin。 而后由 Netty4Plugin 创建对应的 transports࿰…...

细说STM32F407单片机IIC总线基础知识
目录 一、 I2C总线结构 1、I2C总线的特点 2、I2C总线通信协议 3、 STM32F407的I2C接口 二、 I2C的HAL驱动程序 1、 I2C接口的初始化 2、阻塞式数据传输 (1)函数HAL_I2C_IsDeviceReady() (2)主设备发送和接收数据 &#…...

从头开始学MyBatis—04缓存、逆向工程、分页插件
介绍了MyBatis的缓存、逆向工程和分页插件的使用 目录 1.Mybatis的缓存 1.1MyBatis的一级缓存 1.2MyBatis的二级缓存 1.3二级缓存的相关配置 1.4MyBatis缓存查询的顺序 1.5整合第三方缓存EHCache 1.5.1添加依赖 1.5.2各jar包功能 1.5.3创建EHCache的配置文件ehcache.x…...

Artec Space Spider助力剑桥研究团队解码古代社会合作【沪敖3D】
挑战:考古学家需要一种安全的方法来呈现新出土的陶瓷容器,对比文物形状。 解决方案:Artec Space Spider, Artec Studio 效果:本项目是REVERSEACTION项目的一部分,旨在研究无国家社会中复杂的古代技术。研究团队在考古地…...

《探索PyTorch计算机视觉:原理、应用与实践》
《探索PyTorch计算机视觉:原理、应用与实践》 一、PyTorch 与计算机视觉的奇妙相遇二、核心概念解析(一)张量:计算机视觉的数据基石(二)神经网络:视觉任务的智慧大脑(三)…...

【C#设计模式(21)——状态模式(State Pattern)】
前言 状态模式:在对象内部发生改变时改变其行为,使得对象在不同的状态下具有不同的行为表现。 代码 #region 状态模式-类/// 抽象 交通灯状态public abstract class TrafficLightState{public abstract void Display();}//红灯public class RedLight : TrafficLight…...
nvm日常使用中常用命令总结
日常开发vue项目中,不同的项目 我们可能需要安装不同的node版本,但是为了方便切换node,我们一般会安装一个名称为nvm的工具,这里总结一下,nvm常用的命令: 1、为了查看可用的 Node.js 版本,你可…...

【数据仓库】SparkSQL数仓实践
文章目录 集成hive metastoreSQL测试spark-sql 语法SQL执行流程两种数仓架构的选择hive on spark数仓配置经验 spark-sql没有元数据管理功能,只有sql 到RDD的解释翻译功能,所以需要和hive的metastore服务集成在一起使用。 集成hive metastore 在spark安…...
PessimisticLock
想象你和你的朋友都想去图书馆借同一本非常受欢迎的小说。为了确保你们中的一位能够成功借到这本书,图书馆采用了悲观锁机制来管理借阅过程。 悲观锁的方式 查看书籍状态:当你到达图书馆并决定要借这本小说时,你先告诉图书管理员你想借这本…...

【Maven】属性管理
1. 属性 问题导入 定义属性有什么好处? 1.1 属性配置与使用 ①:定义属性 <!--定义自定义属性--> <properties><spring.version>5.2.10.RELEASE</spring.version><junit.version>4.12</junit.version> </prop…...

微信小程序性能优化、分包
性能优化是任何应用开发中的重要组成部分,尤其是在移动环境中。对于微信小程序而言,随着用户量的增加和应用功能的丰富,性能优化显得尤为关键。良好的性能不仅提升用户体验,还能增加用户留存率和应用的使用频率。我们将探讨如何在…...

TDengine 新功能 VARBINARY 数据类型
1. 背景 VARBINARY 数据类型用于存储二进制数据,与 MySQL 中的 VARBINARY 数据类型功能相同,VARBINARY 数据类型长度可变,在创建表时指定最大字节长度,使用进按需分配存储,但不能超过建表时指定的最大值。 2. 功能说明…...

【Maven】工程依赖下载失败错误解决
在使用 Maven 构建项目时,可能会发生依赖项下载错误的情况,主要原因有以下几种: 下载依赖时出现网络故障或仓库服务器宕机等原因,导致无法连接至 Maven 仓库,从而无法下载依赖。 依赖项的版本号或配置文件中的版本号错…...

windows系统下使用cd命令切换到D盘的方法
windows系统下使用cd命令切换到D盘的方法 系统环境配置 win10系统原装C盘后期自己安装的硬盘D盘 python3.8安装在D盘中 问题说明 winR打开终端,使用 cd d:命令,无法将当前目录切换到D盘 解决方法 方法一:使用下面这条命令 cd /d d:运…...
嵌入式小白
1、看门狗 看门狗”就是一个计数器,由于位数有限计数器能够装的数值是有限的(比如8位的最多装256个数、16位的最多装65536个数),从开启“看门狗”那刻起,它就开始不停的数机器周期,数一个机器周期就计数器加1…...

xilinx 芯片使用vivado导出pindelay文件——FPGA学习笔记24
1、创建一个空的工程 2、在TCL命令窗输入 link_design -part xc7a35tfgg484-2(芯片型号)回车 3、输入write_csv xc7a35tfgg484-2(文件类型和文件名字) 回车,导出文件在该目录下 4、导出文件...
基于matlab策略迭代和值迭代法的动态规划
经典的基于策略迭代和值迭代法的动态规划matlab代码,实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...

tauri项目,如何在rust端读取电脑环境变量
如果想在前端通过调用来获取环境变量的值,可以通过标准的依赖: std::env::var(name).ok() 想在前端通过调用来获取,可以写一个command函数: #[tauri::command] pub fn get_env_var(name: String) -> Result<String, Stri…...
uniapp 实现腾讯云IM群文件上传下载功能
UniApp 集成腾讯云IM实现群文件上传下载功能全攻略 一、功能背景与技术选型 在团队协作场景中,群文件共享是核心需求之一。本文将介绍如何基于腾讯云IMCOS,在uniapp中实现: 群内文件上传/下载文件元数据管理下载进度追踪跨平台文件预览 二…...

Linux部署私有文件管理系统MinIO
最近需要用到一个文件管理服务,但是又不想花钱,所以就想着自己搭建一个,刚好我们用的一个开源框架已经集成了MinIO,所以就选了这个 我这边对文件服务性能要求不是太高,单机版就可以 安装非常简单,几个命令就…...

针对药品仓库的效期管理问题,如何利用WMS系统“破局”
案例: 某医药分销企业,主要经营各类药品的批发与零售。由于药品的特殊性,效期管理至关重要,但该企业一直面临效期问题的困扰。在未使用WMS系统之前,其药品入库、存储、出库等环节的效期管理主要依赖人工记录与检查。库…...

rm视觉学习1-自瞄部分
首先先感谢中南大学的开源,提供了很全面的思路,减少了很多基础性的开发研究 我看的阅读的是中南大学FYT战队开源视觉代码 链接:https://github.com/CSU-FYT-Vision/FYT2024_vision.git 1.框架: 代码框架结构:readme有…...
比特币:固若金汤的数字堡垒与它的四道防线
第一道防线:机密信函——无法破解的哈希加密 将每一笔比特币交易比作一封在堡垒内部传递的机密信函。 解释“哈希”(Hashing)就是一种军事级的加密术(SHA-256),能将信函内容(交易细节…...

简约商务通用宣传年终总结12套PPT模版分享
IOS风格企业宣传PPT模版,年终工作总结PPT模版,简约精致扁平化商务通用动画PPT模版,素雅商务PPT模版 简约商务通用宣传年终总结12套PPT模版分享:商务通用年终总结类PPT模版https://pan.quark.cn/s/ece1e252d7df...

MLP实战二:MLP 实现图像数字多分类
任务 实战(二):MLP 实现图像多分类 基于 mnist 数据集,建立 mlp 模型,实现 0-9 数字的十分类 task: 1、实现 mnist 数据载入,可视化图形数字; 2、完成数据预处理:图像数据维度转换与…...

以太网PHY布局布线指南
1. 简介 对于以太网布局布线遵循以下准则很重要,因为这将有助于减少信号发射,最大程度地减少噪声,确保器件作用,最大程度地减少泄漏并提高信号质量。 2. PHY设计准则 2.1 DRC错误检查 首先检查DRC规则是否设置正确,然…...