
本来指针写了一些,但总感觉一直写不清楚,这几天就去网上看了一个老师对指针的讲解,这个老师是结合计算机组成原理讲的,不得不说讲得非常牛逼啊,我也是把课看完,进行了大改,顺便还学了些计算机原理的知识,希望大家能够喜欢。
在内存这里,我们拿一个生活中的例子说,假设你在一个公寓楼里住着,楼上有100个房间,但是房间没有编号,这时候你的一个朋友来找你玩,为了找到你,就要挨个房间去找,这样效率很低,但是如果我们如果给每个房间编上编号,你的朋友就可以快速找到你所在的房间了。
这里反映到计算机里: 我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会再写入内存中,我们在买电脑的时候,电脑上内存是8GB/16GB/32GB等,那这么大的内存空间如何高效管理呢?
其实也是把内存划分为一个个的内存单元,每个内存单元的大小取一个字节。
计算机中常见的单位:
bit - 比特位
Byte - 字节 1Byte = 8bit
KB 1KB = 1024Byte
MB 1MB = 1024KB
GB 1GB = 1024MB
TB 1TB = 1024GB
PB 1PB = 1024TB一个比特位可以存储一个2进制的位1或者0

绿色的区域就是一个内存块,要使用就划分为一个个字节,一个字节也称为一个内存单元,其中,每个内存单元,相当于一个学生宿舍,一个字节空间里面能放8个比特位,就好比学生所住的8人间,每个人就是一个比特位。
每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号,即图片中的16进制数),有了这个内存单元的编号,CPU就可以快速找到一个内存空间。
生活中我们把门牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C语言/C++中给地址起了新的名字:指针。
所以我们可以理解为: 内存单元的编号== 地址==指针
计算机的许多硬件单元是要互相协同才能工作的,所谓的协同,至少互相之间要进行数据传递。这些硬件与硬件之间的通信就是靠“线”连接起来。

而CPU和内存之间也是有大量的数据交互的,所以两者也必须用线连起来,如下图:

比如说CPU读取数据的时候,首先控制总线发出Read信号,这时候告诉它一个地址0x0012ff40(胡写的地址),地址(化为二进制序列)通过地址总线传达给内存,假如就去图中红色块这里读取数据,然后通过地址找到了对应的空间,里面是20。(传的二进制序列与线上的数字一一对应),找到20后便通过数据总线传给CPU。
写数据也是一样的,先由控制总线发出一个写信号Write,这里写的位置也要给一个地址(0x0018ff36),依然要通过地址总线,把地址传过去,这个地址很快在内存中定位了一个空间,比如说图中的绿色方块,这里要写一个100进去,100便通过数据总线传过去放到内存中的空间里去。
这里我们再重点说地址总线。CPU访问内存中的某个字节空间,必须知道这个字节空间在内存中的什么位置,而因为内存中字节很多,所以需要给内存进行编址(就如同宿舍很多,需要给宿舍编号一样)。
计算机中的编址,并不是把每个字节的地址记录下来(因为记录也需要申请空间),而是通过硬件设计来完成,即内存单元的编号都是固定好的,这里怎么理解呢?
钢琴、吉他上面没有写上“剁、来、咪、发、唆、拉、西”这样的信息,但演奏者照样能够准确找到每一个琴弦的每一个位置,这是为何?因为制造商已经在乐器硬件层面上设计好了,并且所有演奏者都知道,本质上是一种约定出来的共识。
硬件编址也是如此
可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1(电脉冲有无),那么一根线,就能表示两种含义,两根线就能表示4种含义,这里这样理解

依次类推,32根地址线,就能表示2的32次方种含义,每一种含义都代表一个地址,64位电脑地址总线64根与之对应。
所以想访问某个内存单元时,地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据再通过数据总线传入CPU内寄存器。
首先要理解,在C语言中变量创建的本质是在内存中申请一块空间,用来存放数据
int main()
{
int a = 10;
return 0;
}向内存申请4个字节的空间,用来存储10 空间的名字叫a,这个a名字不是给编译器、计算机看的,而是给程序员自己看的。怎么体现这句话呢,可以转到反汇编代码来看

只有这里还有a 再右键取消符号名

直接连a都没有了
0000 0000 0000 0000 0000 0000 0000 1010 - 10的二进制序列
0x 0 0 0 0 0 0 0 a - 10的十六进制序列
0x 00 00 00 0a10写成16进制,4个二进制位就写一个0,最后一行是4个字节的写法,一个字节8个bite位
这里打开内存来看,在地址栏那里输入&a:取出a的地址

红色方框便是a的地址对应的4个字节,左边及地址栏内便是a的地址,可以看出这4个字节数在红框内是倒着放的。
而且这4个字节其实都是有地址的,内存里换成一个字节显示就可以看到了:

可以看出地址的存放是由小到大存放的,所以仅管整型变量占用4个字节,我们只要知道了第一个字节地址,顺藤摸瓜访问到4个字节的数据也是可以的。
再通过取地址操作符(&)拿到的地址是一个数值,比如:0x006FFD70,这个数值有时候也需要存储起来,方便后期再使用,这样的地址值是存放再指针变量(存放指针的变量)中的。 比如:
int main()
{
int a = 10;
int* p = &a;//取出a的地址并存储在指针变量p中
return 0;
}指针变量(p)也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。
这里可以看到p的类型是int*,我们如何理解指针的类型呢?
int a = 10;
int* p = &a;这里p左边的int*,*是在说明p是指针变量,而前面的int是说明p指向的是整型(int)类型的对象a
那如果有一个char类型的变量ch,ch的地址,要放在什么类型的指针变量中呢?
char ch ='w';
char* pc =&ch;将地址保存起来,未来肯定是要使用的,那要怎么使用呢? 在现实生活中,我们使用地址找到一个房间,在房间里可以拿去或存放物品。 C语言中其实是一样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里就必须认识一个操作符叫解引用操作符( * )。

上面代码的第7行就是用了解引用操作符,* p的意思是通过p中存放的地址,找到指定的空间,* p其实就是a变量了,所以* p=0这个操作符是把a改成了0。
有兄弟就说了,这里直接写a=0,岂不是更方便,为什么要用指针呢?其实这里是把a的修改交给了p来操作,这样对a的修改就多了一种途径,写代码会更灵活。
int a = 10;
int* p = &a;对于这串代码有的兄弟可能会好奇,既然p是指针变量,指针变量也是变量,也是需要向内存申请空间的 那么p这个指针变量大小是多少?要向内存申请多大空间呢?
根据前面可以知道,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制当成一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。
如果指针变量是用来存放地址的,那么指针变量的大小就要是4个字节空间才可以。
同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。


总结来说:
这里有人就要说了,既然指针变量的大小和类型无关,只要是指针变量,在同一平台下,大小都是一样的,为什么还要各种各样的指针类型呢?
解释这里之前先看这串代码

正如图中注释所说,变量a的四个字节被占满了,这里如果我们* pa=0,相当于把a变为0,再看a在内存中的变化

先不着急说,对比另一串代码来看:

通过调试结果来看,图1会将pa的4个字节全部改为0,但是图2只是将pa的第一个字节改为0。
结论:指针的类型决定了,对指针解引用的时候有多大的权限(一次能够操作几个字节)。 比如:char * 的指针解引用就只能访问一个字节,而int *的指针的解引用就能访问4个字节。

这里如果使用double型指针

这时候 * pd=0解引用回访问一个double类型的变量,一下需要访问8个字节,这里一共才4个字节,就会越界访问,访问到后面,如上图,这时候就会非法访问内存,程序可能会崩溃。
先看一段代码,调试观察地址变化

我们可以看出,char *类型的指针变量+1跳过一个字节,int *类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素,指针既然可以+1,那也可以-1。 结论:指针的类型决定了指针向前或者向后一步有多大(距离)。

这个代码里,将一个int类型的变量的地址赋值给一个char *类型的指针变量。编译器会给出一个警告,这是因为指针类型不匹配,而使用void *类型就不会有这样的问题。

这里我们可以看出,void *类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。
简而言之:void *可以存,但不能随便用,使用时可以将其转换为需要的类型(强制类型转换),所以一般void类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的涉及可以实现泛型编程的效果。
指针的基本运算有三种,分别是:
我们知道,数组在内存中是连续存放的,只要知道第一个元素的地址,就可以顺藤摸瓜找到后面的所有元素。
int arr[10]={1,2,3,4,5,6,7,8,9,10};
想打印该数字的10个元素,不使用指针是这样写的:

加入指针之后,就可以这样:

稍作解释,打印的时候需要把1拿出来,即* p,p++使得int *p往后走一步,因为是整型指针,所以跳过一个整型,到了2这里,就这样循环往复,配合下图来看

还有第二种写法:

例如:p+0产生的为下标为0元素的地址,循环往复以此类推,如下图

p+i就是数组中下标为i的元素的地址,+i也可以理解为跳过i个元素,正好指向下标为i这个元素 找到对应元素的地址后,要访问里面的元素,解引用就可。
倒着打印,就先拿到最后一个元素的地址:

也可以使用第一种写法

还有兄弟问了,这里打印字符串OK不OK?当然是OK的 不用指针是这样打印的,这样在逻辑上就是一次性全打印出来

加入指针后:

这串代码在逻辑上就是一个字符一个字符打印的,依旧是拿到该数组的第一个元素的地址,不是\0就打印,是就停下来。
因为\0的ASCII是0,其他字符的ASCII不是0,所以循环里的判断条件还可以这样写:

|指针-指针|:得到的是两个指针之间的元素个数 用一串代码来体现一下这句话:

可以看到这里系统报错了,原因是相减的结果超出整型范围了,换用%lld就好

可以看出,这两个地址相减的结果是9。

起始地址是右边箭头
我们可以知道,数组随着下标的增长,地址是由低向高变化的
&arr[0] + 9 == &arr[9]
&arr[0] - &arr[9] == -9
9 == &arr[9] - &arr[0]可以看出下标为0的元素的地址+9就是下标为9的元素的地址 也可以这里+9是跳过9个元素之后下标为9的地址
就与下面的这个关系类似了 日期 - 日期 = 天数 日期 + 天数 = 日期 日期 - 天数 = 日期
这里我们再写一个程序,程序中的函数作用是求一个字符串的长度 在不用指针之前我们用strlen库函数来达到这个目的:

使用指针后

这些都是用指针±整数写的,我们再使用指针-指针去实现这样的逻辑

当循环停下来的时候,p指向图中的两个位置

这两个指针相减得到的便是中间元素的个数了。
地址是有大小的,地址是个数值,有大小就可以比较大小

这里arr+sz是个地址,arr是首元素地址,sz表示跳过sz个元素,p不断+1,找边界在哪,当p=p+10的时候,就超过了数组的边界了。附加下图理解:

这就是结合计算机组成原理所写的指针内容了,这些只是指针的冰山一角,不得不说这里确实有些费劲,我写着写着有时候也会发懵,应该还是相关的练习太少了,如果觉得作者文章内容好的话不要忘记一键三连给予支持哦~