目录
正文开始
1.内存和地址
在这里,我们先提一下存储器的概念。在计算机中,存储器可以分为:
- 主存储器:又称主存、内存。用来存放计算机运行期间所需的程序和数据,CPU可以直接随机地对其进行访问。特点是容量小、存取速度快、造价高。
- 辅助存储器:又称辅存、外存。用来存放当前暂时不用的程序和数据,以及一些需要永久性保存的信息。外存的内容需要调入主存后才能被CPU访问。特点是容量大、存取速度慢、造价低。我们常见的硬盘就属于外存。
- 高速缓冲存储器:又称cache。位于主存和CPU之间,用来存放当前CPU经常使用的指令和数据,以便CPU能够高速地访问它们。特点是容量小、价格高。现代计算机一般将cache制作到CPU中。
可知,内存其实是电脑上的存储设备,程序在运行的过程中,需要向内存申请空间来使用。
我们可以举一个生活中的例子,在大学里,同一个专业的同学,一般是在同一栋宿舍楼中的。而在一栋宿舍楼中,有不同的房间号,比如:1楼有101、102、103...,2楼有201、202、203...,3楼有301、302、303...。如果你想找到你班上的某个同学,要是一层层楼,挨个房间去找,效率会非常低。但是,如果我们知道某个同学的地址,也就是所在宿舍的门牌号,就可以根据地址直接找到这个同学了。
如果把上面的例子推广到计算机中,是怎么样的呢?
我们知道,在计算机中,CPU处理数据的时候,所需的数据是从内存中读取的,处理后的数据也会放回到内存中。我们日常使用的电脑,有8G、16G、32G......,这些内存空间是如何高效管理的呢?
其实就是把内存划分为一个个的内存单元,每个单元的大小为1个字节,1个字节含有8个比特位,每个比特位可以存放1个二进制的0或1。
计算机中常见的单位如下:
- bit:比特位,简称位,缩写为b。是计算机中最小的存储单位,1bit表示1个二进制位(0或1)。
- Byte:字节,缩写为B。1Byte = 8bit。
- KB:千字节。1KB = 1024B, 即2的10次方B。
- MB:兆字节。1MB = 1024KB,即2的20次方B。
- GB:吉字节。1GB = 1024MB,即2的30次方B。
- TB:太字节。1TB = 1024GB,即2的40次方B。
- PB:拍字节。1PB = 1024TB, 即2的50次方B。
如上图,从下往上,给每一个内存单元编了号:0,1,2,3...,用16进制来表示。
每个内存单元,都有1个字节的空间,也就是8个比特位的空间,相当于1个宿舍里,有8个床位的空间。
每个内存单元,都有一个编号,相当于宿舍的门牌号。有了这个门牌号,就可以快速找到一个同学。有了这个内存单元的编号,CPU就可以快速找到一个内存空间。
生活中, 我们把门牌号也叫作地址。在计算机中,我们把内存单元的编号也叫地址。在C语言中,给地址起了新的名字:指针。
可以这样理解:在C语言中, 内存单元的编号 = 地址 = 指针,三种说法是一样的。
那么,我们怎么理解编址呢?
如上图,我们知道, CPU和内存之间,有大量的数据交互,为了实现这种交互,两者之间是用线连接起来的。
CPU是完成计算工作的,首先要从内存中读数据,计算完毕后,又将结果的数据写入内存。
这个过程是:首先,控制总线发出Read指令,然后CPU会给出一个地址,通过地址总线向内存传递一个地址信号,从内存中找到该空间,再将该空间中的数据通过数据总线传递给CPU。在CPU利用所读的数据进行计算,得出结果之后,控制总线发出Write指令,然后CPU给出一个写入地址,通过地址总线向内存传递有一个地址信号,在内存中找到该空间,把结果的数据写入这个空间中。
我们再来讨论一下编址:
CPU要访问内存中的某个字节空间,必须知道这个字节空间在内存中的什么位置, 而内存中有很多个字节,就需要给内存进行编址了。就好比,一栋楼中有很多个宿舍,需要给每个宿舍进行编号。
计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
就好比,吉他上面没有写:哆、来、咪、发、梭、拉、西。但是演奏者也能准确找到每一个音所在的位置,这是为什么呢?因为制造商已经在乐器的硬件层面上设计好了,并且所有的演奏者也知道,本质上是一种约定的共识。硬件编址也是如此。
我们可以简单理解为:32位机器有32根地址总线,每根线只有2种状态,电脉冲的有无代表0和1。那么1根线能表达2种含义,2根线能表达4种含义,......,32根线能表达2的32次方种含义。每一种含义都代表一个地址。
地址信息被下达给内存,在内存中,通过硬件的设计,就可以直接找到该地址中对应的数据,这个数据再通过数据总线传给CPU内的寄存器。
2.指针变量和地址
2.1 取地址操作符(&)
在了解了内存和地址之后,我们回到C语言。在C语言中,创建1个变量,其实就是在向内存申请空间。比如:对于int a = 10;,实质就是,向内存申请4个字节的空间,把10存放进去。如下图:
如上,创建了整型变量a,向内存申请了4个字节的空间,用于存放整数10。
我们使用取地址操作符(&),取出了4个字节中地址较小的那个地址。在printf打印时,对应的占位符是%p。
int变量占4个字节,我们只要知道了第一个字节的地址,就可以顺藤摸瓜访问到剩下3个字节的数据,因为相邻地址之间相差1。
2.2 指针变量
刚刚,我们使用取地址操作符(&),取出了变量a的地址,如果我们想把这个地址存到一个变量里面去,该怎么操作呢?如下:
指针就是地址,指针变量是专门用来存放地址的变量。应该知道的是,上面的pa,在日常生活中,指针变量有时会被口头语叫做指针,而指针实际上是地址,而非变量。
对于int类型的变量a,它的地址存放在int*类型的pa中。那么举一反三,如果有一个char类型的变量c,它的地址应该存放在什么类型的指针变量中呢?如下:
2.3 解引用操作符(*)
指针变量是用来存放地址的,存起来的地址有啥用呢?当然是用来使用的,怎么使用呢?我们可以看下面的例子:
如上,变量pa中存放了变量a的地址。*pa的意思就是 ,通过pa中存放的地址,找到这个地址所指向的空间,也就是找到变量a。其实,*pa就是a,*pa=20这个操作就是把a改成了20。我们可以直接通过a=20来修改a的值,但是用指针来修改,就多了一种途径,写起代码来会更灵活,而且在某些情况下,指针是会起到它的作用的。
2.4 指针变量的大小
我们知道,各种类型的变量,都是有其大小的,比如:int的大小是4个字节,char的大小是1个字节。那么,指针变量的大小是多少呢?我们来看如下代码:
如上图,在x64环境,即64位环境下,int*和char*的长度都是8。在x86环境,即32位环境下,int*和char*的长度都是4。这是为什么呢?
在前面,我们提到过,对于32位机器,一般有32根地址总线,每根线传递的电信号转换成数字信号,是0或1。我们把32根地址线产生的二进制序列当做一个地址,那么地址在总线上传输的时候,长度就是32个bit位,即4个字节,在内存中存储时,和它传递时的长度是一样的,也是4个字节。
同理,对于64位机器,一般有64根地址总线,存储的地址就有64个bit位的空间, 也就是8个字节,在总线上传输和内存中存储的长度就是4个字节了。
结论:
- 32位平台下,地址是32个bit位,指针变量的大小是4个字节。
- 64位平台下,地址是64个bit位,指针变量的大小是8个字节。
- 指针变量的大小,与指针变量的类型无关。
3.指针变量类型的意义
刚刚我们发现,无论什么类型的指针变量,在相同的平台上,它的大小都是相同的,和类型没有关系。那么,指针变量的类型有什么意义呢?
3.1 指针的解引用
请看代码:
如上,在创建变量a之后,我们取a的地址,此时4个字节中依次存放着:44、33、22、11。
如上,在使用*pa修改a的值为0之后,内存中的4个字节全部被修改为0,呈现出:00、00、00、00、00。
在上面,我们是用int* pa来接受a的地址的,如果换成char*类型会怎么样呢?
如上,我们发现,如果指针变量pa的类型为char*,那么在执行*pa=0后,内存中只有第1个字节被修改为00,呈现出:00、33、22、11。
如上,运行结果显示,变量a的值确实没有修改为0,而是为第1个字节修改为0之后的结果。
其实,指针的类型决定了,在指针解引用的时候,一次能操作几个字节。
比如:char*的指针解引用就只能访问1个字节,而int*的指针解引用能访问4个字节。
我们还可以用下面的方式验证一下,pa由于是char*的类型,所以在打印的时候,也只能打印出第1个字节的内容,只有使用int*才能完整地打印出4个字节所表示的值。如下:
3.2 指针 + - 整数
请看代码:
如上,a、pa、pc的地址都是一样的,这当然应该一样。
但是,pa+1后, 地址增加了4,而pc+1后,地址只增加了1。
我们来看看图示:
结论:
- 指针的类型决定了指针向前或向后走一步,有多大(距离)。
- 指针+n,其实就是跳过n个指针所指向元素的长度。
- 指针可以+n,也可以-n。
3.3 void*指针
在前面我们已经了解到, 指针变量是有很多种类型的,比如:int*表示指针指向的是int类型的变量、char*表示指向char类型的变量、short*表示指向short类型的变量、double*表示指向double类型的变量......
其实,在指针类型中,有一种特殊的类型叫void*类型,可以理解为,无具体类型的指针(或泛型指针)。
void*指针可以接收任意类型的地址。
void*指针的局限性在于,不能直接进行指针的解引用操作和+-整数操作。
我们可以看到,void*指针可以接收不同类型的地址,但是无法直接进行指针运算。那么,void*指针到底有什么用呢?
一般,void*指针是在函数的参数部分使用的,用来接收不同类型的数据,这样的设计可以实现泛型编程的效果,使得一个函数可以处理多种类型的数据。
4.指针运算
指针的基本运算有3种,如下:
- 指针+-整数
- 指针-指针
- 指针的关系运算
4.1 指针 + - 整数
假如要遍历一个数组,打印出每一个元素,我们首先想到的可能是如下方法:
其实,我们还可以使用指针的方式来遍历数组。
我们知道,数组在内存中是连续存放的,只要知道了第一个元素的地址,就能顺藤摸瓜找到后面的所有元素。我们可以将首元素的地址存放在变量p中,*p访问的是第1个元素,*(p+1)访问的是第2个元素,*(p+2)访问的是第3个元素......循环下去,我们就能遍历数组了,请看下图:
当然了,我们也可以用指针实现倒序打印数组,把数组的尾元素地址交给指针变量p,*p访问的是倒数第一个元素,*(p-1)访问的是倒数第二个元素......循环下去即可,请看代码:
4.2 指针 - 指针
关于指针减去指针,我们要注意的是:
- 指针 - 指针的前提是:2个指针指向同一块空间。
- 指针 - 指针得到的是:2个指针之间的元素个数。
请看代码:
我们再来看看图示:
我们可以联想到前面所提到的指针 + - 整数:
那么,指针 - 指针有什么用呢?我们来看看它的应用:
如上,为了统计字符串中,\0之前的字符个数,我们使用了strlen这个库函数。那么,我们能不能不使用库函数,而是自己写一个函数,来统计字符个数呢?
我们先来看看指针+1的方式:
我们再来思考一下,用指针 - 指针的方式来解决这个问题。如果我们能够找到'\0'的指针,用'\0'的指针减去首元素的指针,就可以得到元素个数了。请看代码:
4.3 指针的关系运算
指针的关系运算,其实就是地址比较大小。我们知道,地址其实就是一种编号,是数值,当然可以比较大小了。
我们还是以遍历数组为例子:
完结