指针 (下面我写了一个假指针, 刚接触计算机100多天,知识方面有限,如有错误望指出,在改错完善的路上不断前进!)
纯属个人理解,如有不当之处希望各路大佬多多指出
我推荐在学习指针的空余时间你可以去看看 编码 —(美)Charles Petzold 著的这本书,主要讲的是数据是通过什么方式存储的内存中的。
对于刚接触计算机的学生来说直接去学指针确实有点迷糊,没一点计算机原理的基础,不明白到底是为什么可以这样写;
先看一下物理存储在计算机中到底是怎么回事:
电脑中的内存条大家应该都很熟悉
我们的数据大部分都存储在里面,为什么它会和指针联系在一起呢?不论你懂不懂指针是什么,但你要知道指针可以修改电脑的物理地址,一旦你的物理地址被修改,那么你的数据将面临很大的风险;指针可以说一直和地址联系着,明白地址的真实含义有助于你指针的学习;
假设我们有一跟内存条,我们形象的把里面的空间抽象成下面的表格(一个小格代表一个存储空间):
接着我们给每个格子编上号(因为在计算机中每个格子都有自己的编号,我们为了方便理解,先做个比喻)
紧接着我们取出其中其中一个存储空间:
计算机中最小的存储的单位是位,我们先将其看成线性的存储结构;
假设每一位只能存储一个0或者一个1,我们学过下面的对应关系:
1字节=8位
1kb=1024字节
1mb=1024kb
1G=1024mb;
我们用char 类型来解释位,我们都知道一个char 类型的数据占用一个字节,一个字节是8位,每一位上都能存储一个0或者1,下面用这8个位表示一个十进制的数字:
通过运算我们可以知道这是十进制中的99 ,提到99你又能想到什么?在ASCLL代码表中可以得知99代表的正是小写字母c,这就是他们之间存在的联系;
我是个好奇心比较重的人,计算机是如何产生0和1的呢?计算机有 有电,无电,电压高,电压低这几种状态,其中计算机中有几个神奇的门,什么是门?我们在计算机中不少接触“或” “与” “非” 对应的符号也就是“||“ ”&“ ”!“。
来看一下或门的作用:(确实没找到合适的画图工具)
这里的 1和0可以理解为通电和断电;
我们再来看一下与门:
还可以是另一种表示方法:
这种结构其实和我们看到的电路图差不多:
有兴趣的小伙伴可以研究一下;
我们接着来讲指针:
除了二进制在计算机中怎么存储, 你也可以了解下面来进一步对计算机有所了解:虚拟内存和物理内存
先来看一下计算机的存储系统:寄存器-高速缓存器-高速缓存-主存-本地二级存储(本地磁盘)-远程二级存储(分布式文件系统,web服务器);这个也可以是按照价格的顺序排列,寄存器最贵。。
寄存器:是直接和中央处理器接触的部件,所有的程序都是通过寄存器进入到cpu中,因为cpu的处理速度实在是太快了,其它的部件跟不上cpu的速度,所以需要寄存器来进行缓冲;
为什么要有虚拟内存:
因为早期的计算器在运行多个程序时进程地址空间不隔离,不隔离可想而知,因为运行程序时通过调用物理地址存储的数据来进行,不隔离就可能造成一个进程运行的过程中修改其他的地址;做个假设你的电脑的运行空间2G,当你同时运行A,B,C三个程序时物理空间地址不进行隔离时,我把它抽象成下面图像:
现在是不隔离的情,可能出现下图的情况:
当A和B同时运行时就有可能出现在相同的物理地址上,所以不用说两个人就可能打起来,打起来你的数据就会受损;
(2)内存使用效率低
(3)程序运行的地址具有不确定性:开始我们也说了将内存中的存储空间抽象成一个个地址,一个程序运行时可能会在不同的地址区出现;
虚拟内存是如何和真实的物理内存发生联系的呢?
我们以Linux的虚拟内存技术做解释:
Linux把虚拟空间分成若干个大小相等的存储分区,把它们称为页,为了和物理地址之间传递的方便,物理地址也就按大小分成若干块,由于物理内存中的块空间是用来容纳虚拟页的容器,所以块被称作页框,页与页框是linux实现虚拟内存的基础;
为了方便下面的理解引入数学中映射:映射是指的两个集合之间的一种对象关系;
为了省略大量文字,上图解释(简单比喻为下图):
知识有限只能解释到这一步。
为什么要说虚拟内存呢?不知道我下面说的对不对:当运行程序时从虚拟内存中通过页表找到相应的物理地址,从中提取存储的数据;
好吧,进入正题~指针
一:本文主要通过分析地址的值来介绍指针:
先来搞清两个符号*
和&
,这两个在指针这里是逆运算,一个是取地址,一个是取数据;
int a=3;
int *p;
p=&a;
printf("%d",*p);
printf("%d",p);
printf("%d",&*p);
运行结果:
3
7339540
7339540
可以发现没有改变;
指针是什么?你如果想弄清楚指针这个奇妙的东西,必须知道数据在内存中是如何存储的(上面已经解释了数据是如何存储的);大家在定义变量的时候都会指出类型,就比如Int,char ,bool…,这些类型计算机是如何给他们分配字节的呢?我想各位大佬都知道吧?就来分析一下int 类型,计算机给他分配4字节(4*8=32位)的空间,什么意思呢?下面有请 泡面 同学给 鸡蛋 同学讲一下
泡面同学:鸡蛋同学来听我说,博主他就是懒,他不给你讲我给你讲,来下面给你上个例子:
int main(int argc, char** argv) {
int A,B;
scanf("%d%d",&A,&B);
printf("%d \n",&A);
printf("%d \n",&B);
printf("%d\n",A);
printf("%d\n",B);
return 0;
}
鸡蛋你看一下运行结果:
3 4
7470620
7470616
3
4
泡面同学:鸡蛋请注意观察7470620和7470616,你是不是发现了什么?
鸡蛋同学:相差4啊
泡面同学:没错就是4,int的字节数也是4,7470620和7470616分别是A和B的物理地址,我用下图来表示:
鸡蛋同学:奥~这样啊 ~,明白了,那为什么是7470620和7440616?
泡面同学:你要是想知道为什么知道是他们,你就要先了解,数据在内存中怎么存储的,7470620其实是一个物理地址,7470620~7470623是3存储的空间。
鸡蛋同学:那为什么要有物理地址?
泡面同学:因为你是要向计算机中存储一个数据,计算机要在内存中分配一个地方给这个数据。就比如我给你一个香肠,你是不是要把香肠放在你的兜里?根据夹逼准则可知内存就是你的兜,香肠就是数据,明白了吧?
鸡蛋同学:原来这样啊~
泡面同学:来上个图给你详解一下:
鸡蛋你现在再回头看我刚才给你展示的代码,"&“是取地址符scanf(”%d%d",&A,&B);这句话你现在是不是有点眉目了?
鸡蛋同学:我好像知道了点什么了~~,他和指针又有什么联系呢?
泡面同学:来下面讲重点啦~ 咱俩想像一下现在有一个酒店,我们两个人要入住这个酒店,酒店的服务员会告诉我们俩应该去哪个房间号入住,我们根据他所说的房间号找到了我们的房间的地方。在这里酒店这栋楼就相当于计算机的内存,房间号就是我们所入住房间的地方,强行比喻的话就是房间号就是这所房间的地址这个房间号代表这个房间,我们两个就相当于你想要向计算机中输入的数据;
鸡蛋同学:奥~原来如此
泡面同学:在计算机中我们给房间号一个好听的名字:指针。 来看下面这张图
泡面同学:鸡蛋同学懂了吗?
鸡蛋同学:我看书上有指针和指针变量,他们两个有区别吗?
泡面同学:当然有区别,指针变量是存储指针的变量,指针是你所指的那个数据的地址,比如你定义了一个Int *p,a; p=&a; 此时的p是一个变量名,就和a一样的变量名,而指针p是存储了a的地址;
鸡蛋同学:我还是有点迷啊,什么一会p的一会指针p的呀,这都是啥啊
泡面同学:先别急,你只需要知道 *p 中的p是变量,这个变量p可以存储另一个变量的地址,就和你int A;A=3;是一个道理。
来吧下面再给你上一段代码:
int main(){
int *p,a;
a=10;
p=&a;
printf("%d\n",p);
printf("%d\n",*p);
printf("%d\n",a);
printf("%d\n",&a);
return 0;
}
运行结果:
7339540
10
10
7339540
鸡蛋同学这里要注意了我们定义的是int类型的指针,所以只能指向Int类型的数据;,再注意观察p的值是一个地址值,而*p才是你所指的变量。p的值和&a的地址相同,说明p和&a是属于同一类型的。
我们作图来表示以下关系:
这样你就应该清楚一点,指针变量和指针之间的区别;
鸡蛋同学:搜嘎~ 那指针有什么用呢?
泡面同学:用处大了去了,可以修改你内存中的物理地址,你想想多么恐怖吧;我们来看一个用指针交换变量:
#include <iostream>
int main(int argc, char** argv) {
int a=10,b=1;
int *p1,*p2,*p;
p1=&a;
p2=&b;
printf("%d %d\n",a,b);
printf("%d %d\n",&a,&b);
printf("%d %d\n",p1,p2);
p=p1;
p1=p2;
p2=p;
printf("%d %d\n",a,b);
printf("%d %d\n",&a,&b);
printf("%d %d\n",p1,p2);
printf("%d %d\n",*p1,*p2);
printf("%d %d\n",*p1,*p2);
printf("%d %d",a,b);
return 0;
}
切记这里只能给指针变量赋值,不可给指针直接赋值!!!
来看一下运行结果:
可以清楚的看到a和b代表的值,以及地址始终未发生变化,在此过程中变化的是指针的值和地址,数值存储在物理地址中,地址中所代表的值是不变的,你可以通过交换地址的值来达到交换指针值的目的(用图片清楚的解释一下):
当指针指向他们时:
注意:赋值时只能给指针变量赋值!不可直接给指针赋值!! ,上图中的&表示他们在计算机中的物理地址,可以看出指针变量是存储物理地址的变量,他们在交换过程中交换了地址,这样表述比较不准确,因为指针变量是个变量,所以我们举个例子来模拟上面指针变量交换的过程:
int number1=99,number2=66,number3;
number3=number1;
number1=number2;
number2=number3;
int a=10,b=1;
int *p1,*p2,*p;
p1=&a;
p2=&b;
p=p1;
p1=p2;
p2=p;
交换的过程也就是本身所代表的值发生了变化,因为在这是在指针中,变量表示的数是一个地址:
通过上面的图你应该明白了交换过程是怎么回事;
从上面代码可以得知a和b的值始终没有改变,下面讲一个好玩的,我们在原代码的基础上加上最后几行代码:
#include <iostream>
int main(int argc, char** argv) {
int a=10,b=1;
int *p1,*p2,*p;
p1=&a;
p2=&b;
printf("%d %d\n",a,b);
printf("%d %d\n",&a,&b);
printf("%d %d\n",p1,p2);
p=p1;
p1=p2;
p2=p;
printf("%d %d\n",a,b);
printf("%d %d\n",&a,&b);
printf("%d %d\n",p1,p2);
printf("%d %d\n",*p1,*p2);
*p1=10;
*p2=1;
printf("%d %d\n",*p1,*p2);
printf("%d %d",a,b);
return 0;
}
可以看到a和b的值发生了变化,因为什么呢?我还是喜欢用图来进行解释:
此时的p1代表的是b的内存地址,p2代表的是a的内存地址,指针被重新赋值,他所指向内存地址存放的数据也被重新赋值,内存地址中的数据发生改变(也就是计算机中真正存储数据的位置),a和b只是代表数据的变量名,所以他们存放的数据发生改变;
到这里不要迷糊,有人会问不是说只能给指针变量赋值吗?怎么又给指针赋值了?一开始我们定义的指针没有指向特定的地址,他在计算机中没有实际的意义(也可以称他为野指针),当你让他指向某一地址时,此时的指针指向了一个固定的地址,不论地址中存放的什么数据,此时的指针有了实际的意义;
二 通过指针引用数组
鸡蛋和泡面刚才回家过年了,下面还是我来讲:
数组大家很熟悉,数组名其实本身就是一个指针;比如:
int a[100];
他在计算机内存中:计算机给他开辟100个Int类型的空间,int为4字节 32位 ,我们用图简单比喻一下:
假设他在内存中是这样存储的;
他的第一个存储空间我们将它称为首地址,这个首地址对于我理解后面的知识可谓是起到了非常重要的作用;我们输出一下首地址的值
#include <iostream>
int main(int argc, char** argv) {
int a[100];
printf("%d",&a[0]);
return 0;
}
因为int是四字节,可以自行计算后面地址的值,这里用一下首地址;
在计算机中定义了一个数组,计算机给它分配一连串的内存地址,如何找到这个内存地址,这时候我们就需要找到他的第一个地址,当你找到第一个地址之后后续的地址随之就能找到;
如何用指针指向数组:
int a[10]={1,2,34,5,64,43,4543,53,24,44};
int *p;
p=&a[0];
在这里我们也可以让指针变量指向a;
p=a;
因为a代表的就是数组的第一个地址(也就是第一个数组元素),其实a也是指针变量,所以p=a进行的是赋值操作,将地址值赋给p;
当指针已经指向一个数组元素时,可以对指针进行一下运算:
加一个整数:p+1;
减一个整数:p-1;
也可以p++,p–;
两个指针也可以进行运算:p1-p2(必须指向同一数组)
==要注意的是p是什类型加的一就是此类型的字节数,在数组中也就是下一个元素,减一也就是上一个元素;我们用代码来说明一下:
#include <iostream>
int main(int argc, char** argv) {
int a[10]={1,2,34,5,64,43,4543,53,24,44};
int *p;
p=&a[0];
printf("%d\n",p);
printf("%d\n",&a[0]);
printf("%d\n",p+1);
printf("%d\n",&a[1]);
printf("%d\n",*(p+1));
printf("%d\n",a[1]);
return 0;
}
他在内存中的情况大致可以比喻成:
当在传递形参的过程中传递的也是首地址;
多维数组元素的地址:
我们拿二维数组当做例子:int a[3][3]
;
#include <iostream>
int main(int argc, char** argv) {
int a[3][3]={{1,2,3},{4,5,6},{7,8,9}};
printf("%d\n",&a[0][0]);
printf("%d\n",&a[0][1]);
printf("%d\n",&a[0][2]);
printf("%d\n",&a[1][0]);
printf("%d\n",&a[1][1]);
printf("%d\n",&a[1][2]);
printf("%d\n",&a[2][0]);
printf("%d\n",&a[2][1]);
printf("%d\n",&a[2][2]);
return 0;
}
在计算机中可以把它类比成这种形式;
二维数组和一维数组不同,关键点要分清他的行地址和列地址,我们如果想要用指针指向二维数组,首先要清楚此时的a代表的是什么。从二维数组的角度来看,a代表二维数组首元素的地址,现在的首元素不是一个简单的整形元素,而是由四个整形元素所组成的一维数组,因此a代表的是首行(即序号为0的行)的首地址,a+1代表序号为1的行的起始地址;我们要理解这句话其实可以从二维数组如何组成的角度去看;
如何定义指针指向二维数组:
#include <iostream>
int main(int argc, char** argv) {
int a[3][3]={{1,2,3},{4,5,6},{7,8,9}};
int *p;
p=a[0];
printf("%d\n",p+1);
printf("%d\n",p+2);
printf("\n");
printf("%d\n",&a[0][0]);
printf("%d\n",&a[0][1]);
printf("%d\n",&a[0][2]);
printf("%d\n",&a[1][0]);
printf("%d\n",&a[1][1]);
printf("%d\n",&a[1][2]);
printf("%d\n",&a[2][0]);
printf("%d\n",&a[2][1]);
printf("%d\n",&a[2][2]);
return 0;
}
关于二维数组还有很多知识点,在这里就不多解释了;
三 指针数组和多重指针
什么是指针数组?指针数组中的每一个元素都是指针,他们都可以存放一个地址;
int *p[4];
如何对指针数组进行初始化,我们来看一下下面的例子,并对他们的地址进行分析:
#include <iostream>
int main(int argc, char** argv) {
char *name[]={"Follow me","BASIC","GREAT wall","FORTA","compter"};
printf("%d\n",&name[0][0]);
printf("%d\n",&name[0]);
printf("%s\n",name[0]);
printf("%s\n",name[1]);
printf("%d\n",name[0]);
printf("%d\n",*name[0]);
printf("%c\n",*name[0]);
return 0;
}
这样看可能没什么感觉,我们把它抽象的更形象一点(我个人还是喜欢画图来解释):
可以清楚的看到指针变量的地址和字符串的地址,他们在内存中存储在不同的地方,当调用指针是通过寻找他们的地址来寻找相应的数据;
我们来看这几行代码
printf("%d\n",&name[0]);-------7339520
printf("%d\n",&name[0][0]);----4751360
printf("%d\n",*name[0]);--------70
printf("%c\n",*name[0]);---------F
name是指针变量,本身是一个地址,所以对自己&(取地址)是一个数字,因为name[0]指向的是“Follow me",所以*name[0]
是F本身,不同的形式输出,&name[0][0]
是第一个字符串的第一个字母的地址,以此类推可以推导出其他字符的存储地址;
这里必须注意的是name[0]是指针,&name[0]是指针的地址,name[0]是他所指向数据的首地址;
下面有填了两张图,方便理解:
指针所存放的地址:
下面来看一个比较重点的:多重指针;
如果想要容易理解多重指针,你要熟悉定义数据的类型,指针和指针变量的类型是什么;看一下书上(C程序设计基础,第五版)的一张图,对于学习多重指针很重要:
可能刚看有点蒙圈,简单的解释就是你定义的*p
可以指向一个常变量int *p; int A=3;p=A;
当你定义一个**p
时,你可以指向另一个一重指针*p
,当你定义一个***p
时,可以指向一个二重指针…
为什么前面要强调类型?
因为在编写代码的时候只有同一类型才能相互赋值,不同类型可以通过强制转化(但数据可能会造成丢失)来进行赋值操作;
上个代码举个栗子:
#include <iostream>
int main(int argc, char** argv) {
int *p;
int **p1;
int a=3;
p=&a;
p1=&p;
printf("%d\n",a);
printf("%d\n",&a);
printf("%d\n",*p);
printf("%d\n",p);
printf("%d\n",&p);
printf("%d\n",**p1);
printf("%d\n",*p1);
printf("%d\n",p1);
printf("%d\n",&p1);
return 0;
}
我们再次将他抽象成大概能看懂的样子:
这样都应该能看懂他们之间存在的联系了,这里再提赋值就方便理解了,比如下列赋值操作不允许:
int a=3;
int *p;
*p=&a;
再看上图*p是用来存放数值的,而不是用来存放地址的,两边的类型不相同,所以赋值不会成功;
同样当你拿二重指针赋值时,只能取一重指针的地址:
int **p;
int *p1;
p=&p1;
可以根据上图来观察为什么这样赋值,保证两边属于同一类型;
char **p;
char str[100];
p=(char)&str;
如果不是同一类型可以通过强制转换。
关于指针我笔记就写了这么多,书上还有很多地方没有写到,比如指针传递函数,用指针操作等等;
人的潜力就像函数的上界,人的实力就像函数的上确界,不断改变函数的轨迹,提高自己的上确界!!!
情人节快乐~