【C语言】关于指针——初学者入门后的第一道门槛 📅 发布时间:2026/9/4 17:53:52 👁 浏览次数: 为了将抽象概念落到实处我们不妨先来看一段具体的代码#includestdio.hintmain(){inta10;printf(%p\n,a);int*pa;*p20;printf(%d\n,a);return0;}这段简单代码包含了指针变量并能体现其基础用法其运行过程更是分析易混淆概念的极佳范例。下面我会先引入核心概念再结合该代码进行分析最后梳理每个语句的作用及其运行过程从而建立指针的初步分析框架。【指针值进程地址空间内的地址编号】在讨论指针前明晰内存地址的概念十分必要存储在内存中的变量会占据一定字节的连续存储空间该空间的首字节编号称为该变量的内存地址(通常用十六进制数表示。该地址在当前进程内是唯一的。—补充并非一切变量都有与之对应的内存地址——例如被优化或存放在寄存器中的变量。回归正题本段仅讨论指针的数值部分指针值特指内存中某个存储单元的编号也就是上文所提到的内存地址值。【指针类型编译器的解析规则】指针类型是一种由基本数据类型被指向的类型如int、char等派生而来的派生数据类型。它本身并不存储具体数据但为内存中的地址值附加了解析规则确定视域长度编译器确定该指针指向类型的大小即解引用时访问的字节数推导运算步长当指向完整对象类型的指针进行加减整数运算如p1时编译器会以指向类型的大小sizeof(*p)作为缩放因子计算地址偏移的字节数即编译器按上述视域长度跳过相应字节数绑定解析视图将目标字节序列依照编码规则如补码、IEEE 754解释为对应的类型值即绑定解引用表达式的类型语义决定编译器如何理解这些字节的数学意义。—补充void*是特殊的“无类型”指针指针变量自身的字节长度在既定架构下是确定的。但其指向类型的大小视域长度未定编译器无法确定缩放因子因此不可对该类型指针直接进行算术运算。【指针变量存放地址的存储容器】指针变量是一个具体的存储容器由系统分配内存空间用以存放地址值。作为一种变量如未被register存储类关键字修饰指针变量本身也拥有可通过取地址符获取的独立内存地址。—补充指针变量具体存放在栈区、静态区还是堆区取决于它的定义方式和存储类别而非指针类型本身。如示例代码中的局部指针变量p被分配在栈上但若指针变量定义在函数外部或带有static关键字则分配在静态区若指针变量本身是动态分配内存块的一部分如结构体成员则可能驻留于堆上。有了以上铺垫我们回到本文开头的代码逐条拆解其中涉及指针的声明、赋值、解引用等具体写法并指出易混淆的细节。【目标对象的声明与初始化】代码行int a 10;定义了一个整型变量a为后续指针操作提供了一个可寻址的目标对象但其本身不涉及指针。该语句在内存通常为运行时栈中为标识符a分配了固定大小的存储空间int类型通常占4字节并用初值10填充该空间。单次程序运行期间这片空间拥有唯一的内存首地址该地址可通过取地址运算符获取。【取地址运算】代码行printf(%p\n,a);使用了取地址运算符该运算的结果是一个指针常量不可修改的地址值属于右值。单目运算符作用于左值如变量名返回该左值对应内存单元的首地址。其结果的类型为指向该变量类型的指针如a的类型是int*指向int的指针。【指针类型声明与指针初始化赋值】int* p声明了一个指针变量p语法形式为类型名* 变量名;。注意区分此处的是指针声明符类型修饰符表示p是一个指针变量类型名int决定了该指针的指向类型即后续解引用时可读写的数据宽度及指针算术运算的步长指针变量本身是pint则是其完整类型。代码行int* p a;将指针变量p初始化为a的地址。赋值号右侧必须是一个合法的地址值此处为a其类型必须与左侧指针类型严格匹配均为int*。p的存储内容在初始化后变为a的内存地址此时称p指向a。【指针解引用】*p 20;的作用为通过指针间接修改目标变量的值。单目运算符是解引用运算符间接访问运算符作用于指针变量p返回该指针所指向内存单元的左值(即可读可写的内存位置)因此p完全等价于它所指向的变量a本身p20等价于a20。其中p出现在赋值号左侧时执行的是写操作向该单元写入新值若出现在右侧或表达式中执行的则是读操作获取该单元的值。真实的代码运行场景总能带给我们对知识更加深刻的理解而亲手在调试器中观察内存中的变量正是让理论落实的最佳方式通过调试器正确查找变量的内存地址也无疑是落实这些理解的第一步。由此我想到了一种可能被踩中的误区【如何查找内存地址】不知你是否曾试图在调试器的内存窗口地址栏里直接输入变量名“a”来跳转这样尝试后我并未达到目的只得到了铺满窗口的“??”——这个窗口不支持变量名解析。不像监视窗口可以计算a这类表达式VS的内存窗口地址栏不支持表达式求值。当我们在内存窗口的地址栏里输入“a”时调试器无法将其关联到源代码中的符号表而是将a按照十六进制字面量的规则解析为数字0xA。至于图中显示的地址0x0000000A它在Windows中属于NULL指针保护区接近0地址操作系统禁止任何程序访问这片区域因而调试器无法读取数据只能显示“??”。下面提供三种手动输入地址以外的常见查找方法在监视窗口或自动窗口中找到变量a右键点击它并选择“转到内存位置”。内存窗口会自动跳转在监视窗口中输入“a”复制其显示的纯数字地址后粘贴到内存窗口的地址栏在代码编辑器选中a并拖到内存窗口的地址栏。部分VS版本支持这个操作但并不会尝试解析表达式而是传递变量对应的地址值。相信你还记得前文内存地址概念处所述的“该地址在当前进程内是唯一的。”与【目标对象的声明与初始化】处的“单次程序运行期间分配给标识符a的固定大小的存储空间拥有唯一内存首地址。”为何“当前进程”和“单次程序运行”值得强调难道存储空间中还藏有一片世外桃源接下来的内容便会探究我多次运行同一段代码时的一个小发现……【“唯一”的内存地址为何会变化】在反复运行本文的示例代码时我注意到一个有趣的现象每次打印出变量a的地址都不相同——一切困惑的肇因。#includestdio.hint main(){int a 10;printf(“%p\n”,a);int* p a;*p 20;printf(“%d\n”,a);return 0;}为了观察这一现象我在代码行int* p a;设置断点并连续启动程序两次不修改任何代码通过调试器查看a的地址从两次运行的截图中我们可以看到因尚未执行*p 20变量a的值始终为10但打印出的内存地址却不相同。这是因为现代操作系统出于安全性考虑默认启用了地址空间布局随机化ASLR。该机制使得每次程序启动时栈的起始地址都随机偏移因此局部变量a的地址每次运行都会变化。正因如此我们不可在代码中强行编码具体的地址值因为该地址在每次运行时都可能不同。必须始终通过取地址运算符或指针变量来动态获取当前运行时的真实地址。通过调试器查看到的地址“0x00AFF7D0”和“0x00F8F93C”同样让我十分好奇——前缀0x的含义是什么输出格式规范究竟是什么样的我的查询结果如下我们可以将这样的地址看作两个部分在C语言中0x为十六进制基数前缀是一个不可分割的整体词法单元。编译器会按十六进制基数为16来解读0x后跟随的数字。—0在C语言语法规则里以数字0开头表示这是一个数字常量。—x或X是十六进制hexadecimal的缩写标记用以声明其后的数字按照16进制来解读。十六进制数码如00AFF7D0和00F8F93C地址的实际有效数值。其中的字母a-f分别代表十进制的10-15且不区分大小写即0x00AFF7D0和0x00aff7d0在数值上完全等价。—补充0x前缀在代码中写十六进制的整型常量时必须遵守而在 printf(“%p”) 打印地址时由标准库的实现自动添加目的是方便我们在调试信息中快速识别其十六进制地址的身份。【始终如一的main函数起始地址】仍以这两次运行为例但这次的截图中我们观察到的内存地址相同其原因如下【代码段地址的固定性】内存窗口当前显示的是main函数在代码区.text 段中的虚拟内存起始地址。与每次运行时地址都会变化的栈变量如a不同现代操作系统的ASLR机制通常不对主可执行文件的代码段.text进行随机化故main函数的地址在连续调试中是相同的其入口地址在链接时就已经确定。main函数地址在代码段存放指令变量a的地址在栈段存放数据。【如何确定图中所示的正是main函数的地址】内存窗口中的前几个字节“55 8b ec”是回答这个问题的关键在32位x86汇编指令中55是push ebp指令用于保存旧的栈底指针ebp8b ec是mov ebp, esp指令用于设置新的栈帧。它们组成了C/C函数尤其是main函数开头的标准序言机器码。因此这行数据正是CPU即将执行的指令。最后让我们对整段代码的运行过程进行再度梳理int a 10;在栈上分配一块内存大小通常为4字节将其命名为a并存入初始值10。printf(“%p\n”, a);取a的地址并将其以十六进制格式输出。int* p a;定义指针变量p并将a的地址存入pp与a建立指向关系。*p 20;解引用p找到a所在的内存位置并写入20a的值被覆盖。printf(“%d\n”, a);直接读取a的值20并打印。return 0;程序正常结束。以上便是本篇博客的全部内容如有错漏还望您不吝赐教我会努力提升自己的写作质量并持续分享所学所获共勉