20251104 How Pointer Works? | 指针是怎么工作的?

发布于 10 天前  145 次阅读


Pointers 地址与指针

指针是C语言里的一个重要概念,也被很多人认作学习C语言路上的一座大山。
我们先从最基础的概念开始。

一件被忽略的小事

我们从变量说起。从初学C语言开始,我们就一直进行声明变量、使用变量、对变量赋值等等操作。

终于有一天你疑惑:

这一切究竟是怎么实现的?

计算机明明是由各种电子元件组成的,为什么我们可以这样随意声明一个变量?计算机是怎么知道它的类型?它是怎么存储的?我们又怎么对变量进行读取、赋值等等操作?

又或者更根本一些的问题是:“变量”究竟在哪里?

就好像生理结构决定了机体的功能,我们要探讨这个问题,不得不回到硬件层面探讨 “存储” 这件事实现的机制。

浅浅溯源

我们的程序如果能用,聪明的你发现我们默认了这两样东西的存在:

  1. 一个干活的“人”:它从上到下执行代码,遇到不同的“语句”做出不同操作。例如:遇到if就判断条件,遇到for就进行循环。等等
  2. 一个存放数据的地方:程序运行的时候,我们不可避免地用到常量、变量等等数据。我们需要一个地方来存放它们。并且,这个地方还得能让这个人方便地取用这些数据。

这个干活的“人” 就是 CPU,而存放数据的地方就是内存(Memory)

探讨到地址和指针,我们重点要来看看内存

内存

我们很多时候都在用“内存”这个词,比如 “我买了个手机,256GB的”,你有时候也听到所谓运行内存内存条之类的说法,可能更少有时候听到缓存的说法。

宽泛一点来讲,它们都至少能叫作“存储器”,因为都能存储数据。

我们先不忙着区分它们,来看看它们共性的部分:我们这里提到了一个单位GB,你有时也听到MBTB的说法,这又是些啥?

位(bit)

既然内存能“存东西”,它总得有个最小的单位吧!就像我们用来量长度,来量重量一样。在“存储”这件事上,人们用一个,也叫比特(bit) 来存储东西。

一个是一个“盒子”,它里面存储的要么是0,要么是1

物理世界中,这样一个“盒子”可能是一个电容器,我们利用它“有电”和“没电”两种状态来存储01,像这样:

+---+      +---+
| 0 |  或  | 1 |
+---+      +---+

因此,是计算机存储的最小单位。1b表示12b表示2,以此类推。

这时你疑惑,明明GBMB这些单位,好像B是大写的?和b代表的位bit不太一样吧?

字节(Byte)

非常好的观察!

一个位只能存储一个状态,要么是0,要么是1。例如灯是开着的还是关着的?我今天吃早饭了还是没吃早饭? 这些只涉及到“是”或“非”两种状态的问题,可以用一个来存储。

但是,如果我们要存储更多的信息呢?比如今天星期几? 这个问题,就不能用01来回答。

于是我们组合出更大的单位,将8个位组合在一起,称为一个字节(Byte),用大写的B表示:

字节是连在一起的8个,例如,这是一个字节:

+---+---+---+---+---+---+---+---+
| 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
+---+---+---+---+---+---+---+---+

而这分散的8个位不是一个字节:

+---+     +---+---+     +---+---+---+---+     +---+
| 1 | ... | 1 | 0 | ... | 0 | 1 | 0 | 1 | ... | 0 |
+---+     +---+---+     +---+---+---+---+     +---+

对工程符号的一些更正

你很可能自豪地说:我熟知kMG这些单位,在工程上,它们分别表示10^310^610^9!就好比1km = 1000m(米)1MV = 1000000V(伏特)

但我们必要地对他们做一些更正:
10^310^610^9这些单位在计算机存储中有一些丑陋,它们并不能被表示为2的幂次。因此,我们声称
k表示1024=2^10
M表示1024*1024=2^20
G表示1024*1024*1024=2^30

再进一步...

很好!我们现在知道了“存储”这件事是怎么实现的,但还有一件更重要的事情没有解决:

我们最开始不是要回答变量么?变量是怎么存储的?不同的变量占有的字节数一样么?计算机是怎么在茫茫的内存盒子中找到一个个变量的?

变量的存储

我们来看看,当我们声明一个变量的时候,在内存里发生了什么:

int a = 10;
char b = 'A';

int是一个整数类型,人们规定,它在内存中要占用4个字节(也就是32)。因此,一个int类型的变量最多能表示2^32种不同的状态(这样能对应足够多的整数)。

因此,当程序执行到int a = 10;这行代码时,内存中:

      ...□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ □□□...
(内存某一处)  └──────────划给变量a的空间─────────┘

同理,char是一个字符类型,人们规定,它在内存中要占用1个字节(也就是8)。因此,一个char类型的变量最多能表示2^8种不同的状态(这样能对应相对足够多的字符)。

因此,当程序执行到char b = 'A';这行代码时,内存中:

            ...□□□ □□□□□□□□ □□□...
(内存某一处)        └───┬──┘
                划给变量b的空间

这里我们不探讨像10这样的值是怎么对应到那庞大的2^32种状态中的某一个状态中去的。

但就如一个1b的空间的01两种状态,就能表示我今天吃饭了么? 这问题的真假一样,可以确定地是,10一定能对应于2^32种状态中的某一个。

就要到那个地方了...!

终于可以回到最开始的问题了!变量到底存储在哪里?地址是什么?指针又是什么?!

地址(Address)

要让程序能用,我们现在已经知道了可以向内存 “要一小块地” 拿来存储变量了!可计算机是不是还得知道这些变量在哪才能对它们读取、赋值呢?

答案是肯定的!因此,人们给内存中的每一个字节都编上了号,这个号就叫做地址(Address)

形象地说,从内存的第一个字节开始,依次编号为0123,以此类推:

□□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ ...
↑        ↑        ↑        ↑        ...
No.0     No.1     No.2     No.3     ...

这样一来,程序就能通过地址来找到变量所在的位置,从而对它们进行读取、赋值等操作。

而这和我们写程序有什么关系?这就引出了指针的概念! 如果我们能用一个变量来存储另一个变量的地址,那我们是不是就能在程序层面,通过这个变量来间接地访问另一个变量了?

指针(Pointer)

指针就是用来存储地址的变量。它的值是另一个变量的地址。指针的类型决定了它所指向的变量的类型。

我们知道,整数类型的“名字”是int,字符类型的“名字”是char。那么,指针类型的“名字”是啥呢?
指针类型的“名字”是在变量类型的基础上加上一个*号。例如:

  • int*是一个新的数据类型,这个类型的变量是一个指向整数类型变量的指针,存储一个整数变量的地址。
  • char*是一个新的数据类型,这个类型的变量是一个指向字符类型变量的指针,存储一个字符变量的地址。

声明指针变量和声明普通变量没有任何区别。

int a;  // 声明一个名为a的变量
        // 它的类型是整型,可以储存一个整数值
int* p; // 声明一个名为p的变量
        // 它的类型是“指向整型的指针”
        // 可以储存一个整数变量的地址

原理讲完了,我们最后补充一点代码层面的实现:
你会说,如果我有一个变量int a;,我怎么知得到它在内存中的地址?或者,如果我有一个指针变量int* p;,我知道它存储的地址处有一个整型变量,我怎么通过它来访问那个整型变量?

我们隆重介绍两个运算符:

取址运算符&和解引用运算符*

  • 取址运算符&:用于获取一个变量的地址。例如,&a表示变量a的地址。
  • 解引用运算符*:用于访问指针所指向的变量。例如,*p表示,指针p所存储的地址那个位置的变量。

你可以尝试:

int a = 10;      // 声明一个整型变量a,并初始化为10
int* p = &a;     // 声明一个指向整型的指针p,并将a的地址赋给p
printf("a的值: %d\n", a);        // 输出a的值,结果是10
printf("a的地址: %p\n", &a);     // 输出a的地址
printf("p的值: %p\n", p);        // 输出p的值,结果是a的地址
printf("p指向的值: %d\n", *p);   // 输出p指向的值,结果是10

运行结果类似于:

a的值: 10
a的地址: 0x7ffee3b8c9ac
p的值: 0x7ffee3b8c9ac
p指向的值: 10

就如整型变量需要%d,字符型变量需要%c,这里%p是指针变量对应的格式说明符

几个Remark

  1. 等等,等等,我记得明明说好的int类型占4个字节来着,这里得到的地址究竟指向的是哪个字节?
    人们规定,变量的地址是它所占用的第一个字节的地址。因此,a的地址指向的是它所占用的4个字节中的第一个字节。
  2. 上面的例子里,我们得到变量a的地址是0x7ffee3b8c9ac,你一定会疑惑:明明地址的编号不是0,1,2...么?这一串是啥?
    事实上,现在生产的内存实在是“太能存”了,它含有的总字节数太多了。因此相比于十进制,地址通常用十六进制数表示,更紧凑,更适合表示内存地址。
    如果换成人能看懂的十进制表示,变量a的地址在第140732718959020号字节。
  3. 最后你没有忘掉问一个问题:指针变量也是变量,占几个字节?
    指针变量的大小和它所指向的变量类型占几个字节无关。曾经的一段时间,指针变量占4个字节。而在大多数现代计算机系统中,指针变量通常占用8个字节(也就是64个位bit)。
    这是因为现代计算机理论上最多能支持搭载的内存大小是2^64个字节,大约是16EB(1EB=2^50 Byte),因此需要2^64种状态来表示每一个字节的地址。
    于是,指针变量就需要8个字节来存储这些地址。

这里是 /* Huajidawang */ 的个人主页