指针在C语言中是个很重要的概念,我更多从类比并结合计算机硬件的角度开始引入概念。
我也是初学者,指针也确实是个很容易被误解的概念,请多指教,评论区留言误区一般时间1h内会回。
1. 引入指针
1.1 简要理解
在之前的《谈谈printf()和scanf() | C语言基础》文章中,讲到变量的时候说过,变量每次创建同时会在内存中分配其一个空间。
我们从高中时期或者更早就了解过计算机硬件的简单知识,如下导图(=>完整版指路)。

我们知道计算机上CPU(Central Processing Unit 中央处理器)在处理数据的时候,会从内存中读取数据,处理后的数据也会放回内存中。内存中为了高效管理空间,会把内存分为一个个的内存单元,一单元一字节,且每个单元都有自己的既定的地址。
更简单地,可以把内存想象成为一栋按房间编号管理的宿舍楼,然后每个“房间”就容纳一个字节,如果寝室内发生什么事儿,门牌号就是宿管找到你们寝室的地址。
所以类比下,当CPU需要一段数据时候,必须要有个准确的门牌号,否则就无从找到目标。C语言中这个门牌号就叫做“指针”,指针你可以看作地址的另一个叫法。
1.2 拓展:从硬件角度理解 【想了解的可以点此展开】
一般理解到这里就差不多了,不过也许有人觉得这样解释地还是过于简单,那从再从硬件角度来看指针是如何起作用的(博主大一还没学计算机组成之类的课,仅为自主课外拓展和理解)。

当我们写下 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
所在内存单元的地址。
你可能会好奇,那指针变量是存的地址,每个地址的大小在机器里就已经固定好了的,那么指针变量大小和地址大小一样,为什么还要在声明指针变量的时候去写上类型呢?

原因在于:当我们沿着地址取数时,必须事先知道打算读取多少字节。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;
}
实际上,这样的值传递,地址没有改变,存放的变量的值改变了。
总结下要使某个变量的值通过函数调用发生改变:
- 在主调函数中,把该变量的地址作为实参
- 在被调函数中,用形参(指针)接受地址
- 在被调函数中,改变形参(指针)所指向变量的值
3* 写在最后:
Q:为什么我还是继续在更C语言内容的原因?
A:首先我们学校的进度偏慢吧,而且我自己也对这部分的知识点的学习发散得也多。(我后续的学习计划会加入Java,静待更新吧~)
由此就会有很多的知识堆积在脑海中,我希望通过费曼的方式把这些知识用公众号(博客)文章的形式进行结构化输出。
所以其实我不是一个技术大牛,也是个不断学习者。如果你有发现什么错误,欢迎留言!我将速速赶到然后与你交流技术话题。
评论 (0)