Nibbles Blog Nibbles Blog
  • 专题汇总
    • 技术学习
    • 个人成长
    • 运营技能
    • 自媒体
    • Minecraft
  • 推荐
    • 好物推荐
    • APP推荐
  • 关于我
    • Nibbles Studio
  • 友情链接
首页 › 技术学习 › 从本质理解指针的那些知识 | C语言基础

从本质理解指针的那些知识 | C语言基础

Avatar photo
Nibbles 尼布
9 5 月, 2025

指针在C语言中是个很重要的概念,我更多从类比并结合计算机硬件的角度开始引入概念。

我也是初学者,指针也确实是个很容易被误解的概念,请多指教,评论区留言误区一般时间1h内会回。

1. 引入指针

1.1 简要理解

在之前的《谈谈printf()和scanf() | C语言基础》文章中,讲到变量的时候说过,变量每次创建同时会在内存中分配其一个空间。

我们从高中时期或者更早就了解过计算机硬件的简单知识,如下导图(=>完整版指路)。

从本质理解指针的那些知识 | C语言基础-Nibbles Blog

我们知道计算机上CPU(Central Processing Unit 中央处理器)在处理数据的时候,会从内存中读取数据,处理后的数据也会放回内存中。内存中为了高效管理空间,会把内存分为一个个的内存单元,一单元一字节,且每个单元都有自己的既定的地址。

更简单地,可以把内存想象成为一栋按房间编号管理的宿舍楼,然后每个“房间”就容纳一个字节,如果寝室内发生什么事儿,门牌号就是宿管找到你们寝室的地址。

所以类比下,当CPU需要一段数据时候,必须要有个准确的门牌号,否则就无从找到目标。C语言中这个门牌号就叫做“指针”,指针你可以看作地址的另一个叫法。

1.2 拓展:从硬件角度理解 【想了解的可以点此展开】

一般理解到这里就差不多了,不过也许有人觉得这样解释地还是过于简单,那从再从硬件角度来看指针是如何起作用的(博主大一还没学计算机组成之类的课,仅为自主课外拓展和理解)。

从本质理解指针的那些知识 | C语言基础-Nibbles Blog

当我们写下 int* pa = &a; 并执行时,(假设 a 是前文已经定义过的变量,基础知识回顾:&是取地址操作符,故 &a 即代表 a 变量存储在内存中的地址),编译器会把 pa (指针本身)的数值写入CPU的MAR(寄存器)中。

随后地址总线将这一串二进制位送往内存控制器,内存条内部的译码电路接收到地址后找到确切的内存单元。接着数据总线把地址 &a 存储到MDR,再将其返回寄存器或缓存。

部分概念再类比上述例子,按执行顺序排列:

  • Memory Address Register (MAR):CPU 内部的寄存器,用来暂存即将访问的内存地址;相当于把“门牌号”写在便签上准备递给内存系统。一般为单向的(CPU → 内存/IO)。
  • 地址总线 (Address Bus):把 MAR 中的地址位输出到主板/芯片上的地址导线上,送往内存控制器。如同走廊/道路网络,负责把门牌号传递到内存。(现代主流芯片物理地址线 39–52 位,但方便说明)假设其线的条数=CPU的字长(64位或32位,32位有时也表达为x86),比如32位机器下常见有32根地址总线产生的2进制序列作为一个地址,一个地址有32位,常需要4个字节。(32bit=4Byte)存储。
  • 行列译码器:含译码电路,在内存芯片内部决定“第几层楼、第几号房”,锁定内存单元。
  • 数据总线 (Data Bus):把目标单元中的数据从内存传回 CPU,也可以在写操作时把数据送入内存。是种双向的总线(读时内存 → CPU,写时 CPU → 内存)。
  • Memory Data Register (MDR):又叫 Memory Buffer Register (MBR),也是一种CPU内部的寄存器,负责在 CPU 与内存之间暂存刚读到的数据或待写出的数据;可将其看作各个房间之间投递的快递员的手中的包裹。
  • 缓存(Cache):热数据暂存地,加快访问(若命中缓存,即可直接MAR → MDR),但不影响“地址=定位信息”的本质。

从本质上讲,指针只是一串能够唯一定位某个内存单元的地址数字(比如x64下Debug时形如0x000000CD3450F6E4的十六进制数)。它本身不保存数据,而是告诉程序“你要的数据在哪里”。

2. 指针变量

2.1 基本格式和大小

在 C 语言里,我们仍然需要一个变量来存放这串地址数字,这样的变量我们称之为“指针变量”。

#include <stdio.h>

int main()
{
    int a = 10;
    int* pa = &a;
    printf("%p", pa);
}

如上,声明时候的* 号在说明 pa 即为指针变量,指针变量内保存的是 a 变量的值的地址,然后通过 printf() 来打印出来。

如下,a 变量赋值为10,&a定位到地址发现已经被存入了内存中(00 00 00 0a,逆序,这里的a是代表十六进制表达下的10)。也就是 pa 并不直接持有 10 整数的值,而是保存了 a 所在内存单元的地址。
VS中内存窗口

你可能会好奇,那指针变量是存的地址,每个地址的大小在机器里就已经固定好了的,那么指针变量大小和地址大小一样,为什么还要在声明指针变量的时候去写上类型呢?

从本质理解指针的那些知识 | C语言基础-Nibbles Blog

原因在于:当我们沿着地址取数时,必须事先知道打算读取多少字节。int*
“告诉”编译器,一步跨四格;char* “告诉”它,一次只走一格。这样既能保证数据对齐,也能避免读写越界。

后文 2.2指针操作中的“± 整数值”部分也有讲解。

一次走几格,我们来看看指针变量的各种操作。

2.2 指针操作/应用

解引用操作符 *

又称间接寻址运算符。这个主要是为了解决找到地址指向的对象。你看我们存地址也是有需要取出来用的时候。C语言中利用解引用操作符 * ,来通过已知指针变量找到其指向的变量。

看下面的案例,注意只有在指针变量名前面加的 * 才算是解引用操作符。int* 中的 * 不是。

#include <stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    *pa = 0;
    printf("%d", a);
}

如上这个很简单的案例,其中*pa 就是通过 pa 指针变量中存放的地址,找到指向的 a 变量。而且可以利用此方法间接修改了a 变量的值,此代码执行后输出0。

这样的间接修改的方式,相比直接 a = 0; 来说,对变量的调用等操作更加灵活。

注意:在 C 里进行解引用必须满足以下前提——

  • 指针非空(不为 NULL);
  • 指针指向的地址处于程序已分配的有效内存范围;
  • 解引用类型与目标对象匹配,以免因字节对齐或大小差异造成未定义行为。

± 整数值

这个主要是为了解决上文2.1末说的“一步跨几格”的问题。看下示意图 ptr-2 的部分更易理解:
加减整数值效果图解

#include <stdio.h>
int main()
{
    // [模拟Debug逐行执行过程]
    int ages[4] = { 0, 1, 2, 3 };  
    // 创建数组ages: { 0, 1, 2, 3 }
    int* ptr = &ages[2];  
    // ptr是ages数组第三个元素的指针变量
    *ptr = 8;  
    // 修改第三个元素为8 => 数组ages: { 0, 1, 8, 3 }
    ptr++;
    // ptr是ages数组第四个元素的指针变量
    *(ptr - 2) = 9;
    // 修改第二个元素为9 => 数组ages: { 0, 9, 8, 3 }
}

按上面这个案例,因为数组元素在内存中是连续存放的。指针往前移动一步两步是直接由加减后面的整数来得知的,非常显然。

但是如果从其十六进制地址数字来看,地址的数字又增加了多少呢?

#include <stdio.h>
int main()
{
    int n = 10;
    int* pi = &n;  
    // 原先int类型的地址赋给指针变量pi
    char* pc = (char*)&n; 
    // 强转int类型的地址为char类型的地址,赋给指针变量pc

    // 自C99标准起,把指针强转为 void* 才是完全符合标准
    // 往右滑,有对齐的便于观察的输出结果
    printf("  &n   = %p\n", (void*)&n);     // Output:   &n   = 00AFF974
    printf("  pi   = %p\n", (void*)pi);     // Output:   pi   = 00AFF974
    printf("pi + 1 = %p\n", (void*)pi + 1); // Output: pi + 1 = 00AFF978
    printf("  pc   = %p\n", (void*)pc);     // Output:   pc   = 00AFF974
    printf("pc + 1 = %p\n", (void*)pc + 1); // Output: pc + 1 = 00AFF975
}

我们看到原来的 int* 声明的指针变量 pi 的十六进制地址 ,在 pi + 1 后 +4(sizeof(int)),这就是“一步跨四格”;原来的 char* 声明的指针变量 pc,在 pc + 1 后 +1(sizeof(char)),这就是“一步跨一格”。

指针 – 指针

实际应用案例比如在数组中可以表示元素个数差,也可以在数组中限定要遍历的范围。(如下)

在算法场景中也有用于“快慢指针”算法的长度统计等等。

#include <stdio.h>

int main()
{
    int nums[5] = {10, 20, 30, 40, 50};
    int *left  = &nums[1];   // 左指针
    int *right = &nums[4];   // 右指针

    // gap元素个数差
    int gap = right - left; 
    printf("gap = %d\n", gap); 
    //Output: gap = 3

    // 用左右指针遍历区间 [left, right) 
    for (int *p = left; p != right; ++p)
    {
        printf("%d ", *p);      
    }              // Output:20 30 40
    puts("");      // 换行
}

注意:两个指针必须指向同一个连续对象(通常是同一数组,或者刚好其一指针指向结尾位置)才能安全相减,否则跨数组或随意相减会导致未定义行为。

函数内实现两个值的改变

比如说写一个交换函数 swap() :

#include <stdio.h>
void swap(int*, int*);
int main()
{
    int a=3, b=5;
    swap(&a, &b);
    printf("%d, %d\n", a, b);
}
void swap(int *p1, int *p2)
{
    int t;
    t = *p1;
    *p1 = *p2;
    *p2 = t;
}

实际上,这样的值传递,地址没有改变,存放的变量的值改变了。

总结下要使某个变量的值通过函数调用发生改变:

  1. 在主调函数中,把该变量的地址作为实参
  2. 在被调函数中,用形参(指针)接受地址
  3. 在被调函数中,改变形参(指针)所指向变量的值

3* 写在最后:

Q:为什么我还是继续在更C语言内容的原因?

A:首先我们学校的进度偏慢吧,而且我自己也对这部分的知识点的学习发散得也多。(我后续的学习计划会加入Java,静待更新吧~)

由此就会有很多的知识堆积在脑海中,我希望通过费曼的方式把这些知识用公众号(博客)文章的形式进行结构化输出。

所以其实我不是一个技术大牛,也是个不断学习者。如果你有发现什么错误,欢迎留言!我将速速赶到然后与你交流技术话题。

声明:本站原创文章文字版权归本站所有,转载务必注明作者和出处;本站转载文章仅仅代表原作者观点,不代表本站立场,图文版权归原作者所有。如有侵权,请联系我们删除。
C语言
1
0
Avatar photo
Nibbles 尼布
向着终生成长者迈进!
赞赏

评论 (0)

取消
    发表评论

猜你喜欢

  • 从猜数字程序来理解结构和随机数 | C语言基础
  • 谈谈printf()和scanf() | C语言基础
  • CTF Web 信息搜集 知识总结
Avatar photo

Nibbles 尼布

向着终生成长者迈进!
24
文章
4
评论
34
获赞

多平台更新

▲ “尼布是我”小程序
▲ 个人公众号

热门文章

TOP1
Python数据分析 Ⅰ:pandas模块
7 6 月, 2023
TOP2
更大挑战,更多机遇 | 2023 年终总结
8 1 月, 2024
TOP3
【杂谈】真的需要系统地学那么多编程语言吗?
14 8 月, 2024

标签云

AI (2) anaconda (1) Bilibili (1) Cookie (1) CS50x (1) CTF (1) C语言 (4) Git (1) Github (1) HTTP (1) Java (1) matplotlib module (1) Minecraft (2) NamelessMC (1) pandas module (1) PHP (1) python (2) Serverless (1) vim (1) VuePress (1) WordPress (1) zeabur (1) 公众号 (2) 年终总结 (2) 思维导图 (1) 换源 (1) 数据库 (1) 杂谈 (1) 浪前 (2) 自媒体 (2) 解密 (1) 运营 (2) 飞书 (1)

支持我!

Blog Circle

开往-友链接力
博客录(boke.lu) 随机博客
博客圈
  • SiteMap
  • Umami
  • xLog
  • Substack
Copyright © 2022-2025 Nibbles Blog. Designed by nicetheme. 浙公网安备33038102332481号 浙ICP备2024120131号
技术博客: AHdark Blog Tkong Blog iVampireSP 的物语 我不是咕咕鸽 热心市民 L 先生 ncc的个人网站 岚天小窝 天远笔记
  • C语言4
  • python2
  • AI2
  • 公众号2
  • 运营2
  • 专题汇总
    • 技术学习
    • 个人成长
    • 运营技能
    • 自媒体
    • Minecraft
  • 推荐
    • 好物推荐
    • APP推荐
  • 关于我
    • Nibbles Studio
  • 友情链接