Pointers 地址与指针
指针是C语言里的一个重要概念,也被很多人认作学习C语言路上的一座大山。
我们先从最基础的概念开始。
一件被忽略的小事
我们从变量说起。从初学C语言开始,我们就一直进行声明变量、使用变量、对变量赋值等等操作。
终于有一天你疑惑:
这一切究竟是怎么实现的?
计算机明明是由各种电子元件组成的,为什么我们可以这样随意声明一个变量?计算机是怎么知道它的类型?它是怎么存储的?我们又怎么对变量进行读取、赋值等等操作?
又或者更根本一些的问题是:“变量”究竟在哪里?
就好像生理结构决定了机体的功能,我们要探讨这个问题,不得不回到硬件层面探讨 “存储” 这件事实现的机制。
浅浅溯源
我们的程序如果能用,聪明的你发现我们默认了这两样东西的存在:
- 一个干活的“人”:它从上到下执行代码,遇到不同的“语句”做出不同操作。例如:遇到
if就判断条件,遇到for就进行循环。等等 - 一个存放数据的地方:程序运行的时候,我们不可避免地用到常量、变量等等数据。我们需要一个地方来存放它们。并且,这个地方还得能让这个人方便地取用这些数据。
这个干活的“人” 就是 CPU,而存放数据的地方就是内存(Memory)。
探讨到地址和指针,我们重点要来看看内存:
内存
我们很多时候都在用“内存”这个词,比如 “我买了个手机,256GB的”,你有时候也听到所谓运行内存、内存条之类的说法,可能更少有时候听到缓存的说法。
宽泛一点来讲,它们都至少能叫作“存储器”,因为都能存储数据。
我们先不忙着区分它们,来看看它们共性的部分:我们这里提到了一个单位GB,你有时也听到MB、TB的说法,这又是些啥?
位(bit)
既然内存能“存东西”,它总得有个最小的单位吧!就像我们用米来量长度,克来量重量一样。在“存储”这件事上,人们用一个位,也叫比特(bit) 来存储东西。
一个位是一个“盒子”,它里面存储的要么是0,要么是1。
物理世界中,这样一个“盒子”可能是一个电容器,我们利用它“有电”和“没电”两种状态来存储0和1,像这样:
+---+ +---+
| 0 | 或 | 1 |
+---+ +---+
因此,位是计算机存储的最小单位。1b表示1个位,2b表示2个位,以此类推。
这时你疑惑,明明GB、MB这些单位,好像B是大写的?和b代表的位bit不太一样吧?
字节(Byte)
非常好的观察!
一个位只能存储一个状态,要么是0,要么是1。例如灯是开着的还是关着的?、我今天吃早饭了还是没吃早饭? 这些只涉及到“是”或“非”两种状态的问题,可以用一个位来存储。
但是,如果我们要存储更多的信息呢?比如今天星期几? 这个问题,就不能用0或1来回答。
于是我们组合出更大的单位,将8个位组合在一起,称为一个字节(Byte),用大写的B表示:
字节是连在一起的8个位,例如,这是一个字节:
+---+---+---+---+---+---+---+---+
| 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
+---+---+---+---+---+---+---+---+
而这分散的8个位不是一个字节:
+---+ +---+---+ +---+---+---+---+ +---+
| 1 | ... | 1 | 0 | ... | 0 | 1 | 0 | 1 | ... | 0 |
+---+ +---+---+ +---+---+---+---+ +---+
对工程符号的一些更正
你很可能自豪地说:我熟知k、M、G这些单位,在工程上,它们分别表示10^3、10^6、10^9!就好比1km = 1000m(米),1MV = 1000000V(伏特)。
但我们必要地对他们做一些更正:
10^3、10^6、10^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的空间的0和1两种状态,就能表示我今天吃饭了么? 这问题的真假一样,可以确定地是,10一定能对应于2^32种状态中的某一个。
就要到那个地方了...!
终于可以回到最开始的问题了!变量到底存储在哪里?地址是什么?指针又是什么?!
地址(Address)
要让程序能用,我们现在已经知道了可以向内存 “要一小块地” 拿来存储变量了!可计算机是不是还得知道这些变量在哪才能对它们读取、赋值呢?
答案是肯定的!因此,人们给内存中的每一个字节都编上了号,这个号就叫做地址(Address)。
形象地说,从内存的第一个字节开始,依次编号为0、1、2、3,以此类推:
□□□□□□□□ □□□□□□□□ □□□□□□□□ □□□□□□□□ ...
↑ ↑ ↑ ↑ ...
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
- 等等,等等,我记得明明说好的
int类型占4个字节来着,这里得到的地址究竟指向的是哪个字节?
人们规定,变量的地址是它所占用的第一个字节的地址。因此,a的地址指向的是它所占用的4个字节中的第一个字节。 - 上面的例子里,我们得到变量
a的地址是0x7ffee3b8c9ac,你一定会疑惑:明明地址的编号不是0,1,2...么?这一串是啥?
事实上,现在生产的内存实在是“太能存”了,它含有的总字节数太多了。因此相比于十进制,地址通常用十六进制数表示,更紧凑,更适合表示内存地址。
如果换成人能看懂的十进制表示,变量a的地址在第140732718959020号字节。 - 最后你没有忘掉问一个问题:指针变量也是变量,占几个字节?
指针变量的大小和它所指向的变量类型占几个字节无关。曾经的一段时间,指针变量占4个字节。而在大多数现代计算机系统中,指针变量通常占用8个字节(也就是64个位bit)。
这是因为现代计算机理论上最多能支持搭载的内存大小是2^64个字节,大约是16EB(1EB=2^50 Byte),因此需要2^64种状态来表示每一个字节的地址。
于是,指针变量就需要8个字节来存储这些地址。



Comments | NOTHING