C语言的精髓——指针详解
朱有鹏
1.2、指针变量的类型作用
对于普通变量的来说,其类型的作用主要有这么几个方面:
(1)程序员写代码时识别用:不涉及强制转换时,知道该变量中应该存放什么类型的数值。
(2)空间大小的说明:比如int为4个字节空间
(3)存储结构说明:float和int虽然空间大小都是4字节,但是其存储结构完全不同
对于指针变量来说,其类型的作用与上面基本一致,只是其类型是由普通类型+*构成,*个数表明了指针变量的级数,指针变量用来存放地址。当不涉及强制转换时,其对应关系为:
某类型一级指针变量 = 该类型一级地址
某类型二级指针变量 = &(该类型一级指针变量)
n+1级指针变量 = &(n级指针变量)
所有普通变量的地址都是一级地址,所有一级指针变量的地址都是二级地址,类推n级指针变量的地址就是n+1级地址,但是我们这里必须强调一下,使用指针的目的就是为了更加方便的访问空间,但是如果级数超过3级以上,实际上不但降低了程序的可读性,也会降低对空间的访问速度,所以过高级数的指针变量没有太大的意义。
Int *p = &a;
p中只存放了a首字节地址,但是int说明了*p希望访问的空间有4个字节,所以从p所指a的第一个字节向后数3个字节空间,一共4个字节空间才是*p的实际希望访问到的完整空间。
1.3、为什么需要指针
不管多么高大上的程序,最终都是运行在硬件上的,所有对于硬件的设置和访问,全部都是通过对内存操作实现的,广义上的内存可以包括,寄存器、缓存、常说的内存等,这些内存空间都是由一个个的字节构成的,每个字节都有地址,对于这些空间的访问,大多都是通过地址实现的。
如果就站在c自身的角度,也可以看到指针的好处,举个例子的话,比如fun1函数有一个自动局部变量a,他的作用域被局限在了fun1函数内部,fun2函数是无法访问的,如果fun2想要访问fun1中a的话,我们可以将a的地址传递给fun2函数,当然前提是fun1的a没有被自动释放。
对于c语言有了解的都知道,函数也是有作用域的,跨文件的作用域又称为链接域,为了防止本文件的函数(比如名叫fun的函数)不被其它文件的同名函数干扰,同时也希望不去干扰别人,我们往往会在fun函数的前面加一个static标志,将其作用域固定为本文件,其它文件通过fun函数名是无法访问的。但是如果其它文件的function函数,又确实希望访问这个函数时,怎么办呢?我们只需要将fun函数的地址传递给function函数,就可以跨文件访问fun函数,并且不会受到static的影响。
所以在c语言中,地址还是扩大变量或者函数作用范围的有效手段,当然指针好处很多,这里不再赘述。
1.4、高级语言如java、c#的指针到哪里去了
C++就不说了,里面保留了指针的使用,初学java,c#等高级语言的同学,都会因为里面没有指针而困惑,甚至在想,难道这些高级语言就不需要访问内存空间了吗?凡是有过java异常处理的同学,都会发现java中有一种空指针异常,大多是因为我们使用了没有实例化的对象名导致的,因为没有实例化就没有分配内存空间。
既然有空指针异常,就证明java是使用了指针的,只是全部都由类的底层封装好了,不需要我们关心,目的就是为了省去指针这一难点,使得java简单实用,但是由于不能直接操作指针,面对频繁使用指针的底层开发而言,java和c#多少会显得有心无力。
1.5、指针使用值三部曲
(1)定义(声明)
例: int *p=NULL; //初始化一下,防止野指针
(2)关联
例: int a= 10;
p = &a;//a空间的首地址给了p,所以p里面的地址常量指向了a空间,因此简称p->a空间
(3)引用
(a)读空间:读值操作,前提是里面存有数据才行
例:int b = *p;//等价于 b = a;
(b)写空间:向空间写入新的值
例: int *p = 30;//等价于a = 30;
2、指针涉及到的一些符号的理解
2.1、*的理解
在c语言中,*的用途有两个,一个是用于表示乘号,第二个与指针有关,虽然这两种用途用都会用到*,但这两者没有任何关系。*在指针中的用途主要有两个方面,第一种是用在指针定义的时候,与前面的类型结合,用于表示被定义指针变量的类型,*个数表明了定义的指针变量的级数,例如:
int *p;int* p;
*靠前靠后都没关系,这时的*与p是两个不同的东西,星号表明p是一个一级指针变量,用于存放一个一级地址。但是需要注意下面的情况:
int *p1,*p2;//p1和p2都是int型的一级指针变量。
int *p1,p2;//p1是int型的一级指针变量,p2只是一个普通的int型变量。
第二种就是解引用,解引用时,*p表示的就是p所指向的空间,这时的*也称为取空间操作,找到p所指向的空间,必须强调的是,这时的*p是一个整体,不能割裂来看,比如:
int *p = &a;
*p = 10;//等价于a=10;但是写成* p = 10;就不对了
*作为解引用时(也就是取空间操作时),得到p所指向的空间后,我们的用途有两种,一个是读空间内容,还 有就是向空间写入新的内容。
2.2、取地址符&的理解
取地址符使用时,直接写在变量名称的前面,然后&和变量一起构成了一个新的符号,表示变量空间的首地址,准确讲是变量的首字节地址。比如int a;int *p = &a;这里必须注意,&a是一个完整的不可分割的整体,之所以用这种方式来表示空间的地址,是因为我们没有办法直接得到变量a的地址,只能是使用&a来表示,当编译时会将&a变成a空间的地址赋值给p;
2.3、指针变量的初始化和指针变量赋值之间的区别
首先必须强调,指针变量的初始化与普通变量的初始化没有任何区别,只是指针变量里面的存放的是一个特殊的值“地址”,这个值具有指向作用,可以用来访问它所指向的空间,如果你刨去它地址的含义,实际上变量中存放的不过就是一个普通值。
(1)指针变量的初始化
Int a=10;
Int *p=&a;
此时的*,只是说明p是一个一级指针变量,不能把这时的*当成了解引用。
(2)指针变量的赋值
Int a=10;
Int *p=NULL;
p=&a;//将a空间地址的赋值给p
不少同学可能一直觉得p=&a,应该写成*p=&a;这是错误的理解,这时的*是取空间操作,如果写成*p=&a,就表示将a的空间地址存放到p所指向的空间,p所指向的空间其实就是a,*p=&a的等价写法就是a=&a;相当于把地址给了a自己,显然是不对的。
(3)初始化和赋值注意点
从形式上看,我们已经知道了初始化和赋值的区别,同时要知道初始化只能有一次,但是赋值可以有多次。
2.4、左值与右值
(1)什么是左值和右值
比如:int a=10;等式的左边称为左值,右边称为右值。
(2)左值
在c语言中,左值指的都是变量空间。对左值执行的操作都是写空间操作。
(3)右值
在c中,右值有两种形态,一种是直接写一个数值,比如int a=10;就是典型的这种情况。那么另外的一种情况就是,右值可能也是一个变量,比如int b=10; int a=b;这个时候右值就是一个变量。当变量作为左值是,对变量实现的是写操作,如果变量作为右值时,对变量实现的是读操作,读出后赋给右值,这一点要了解。
2.5、定义指针后,需要会关心的一些内容
(1)例子1
Int a =10;
Int *p=&a;
合理我们必须了解与指针变量p相关的一些内容。
p:表示int*型的一级指针变量空间,里面存放的是变量a的地址。
*p:表示p所指向的空间,指的就是a的空间,只不过是通过地址找到的。
&p:表示指针变量p自己的空间地址,它需要是int **的二级指针变量来存放。
思考一下,**p=20可不可以?
回答:不可以,**p改写成等价形式*(*p),里面的*p等价于a,最后变成了*a,由于a的值等于10,*a就是*10,引用地址10所指向的空间,显然10这个地址指向的空间是不存在的,所以错误。
(2)例子2:
Int a =10;
Int *p=&a;
Int **p1=&p;
对于指针变量p,需要关心p、*p、&p,但是在上例中已经描述过了,不再赘述。
对于指针变量p1来说,需要如下几个方面的问题:
p1:一个int **型的二级指针变量空间,用于存放一个二级地址,恰好*p的地址就是二级地址。
*p1:引用取空间操作,找到p1所指向的空间,指的就是p的空间。
**p1:将其中的*p1替换成为p,**p1就变成了*p,指的就是a的空间。
&p1:指的是二级指针变量p1的空间地址,是一个三级地址。
只要大家理解前面的例子1和例子2,对于三级指针的情况,道理是类似的,但是我们前面就说过,构建三级以上的指针实际上没有太大的意义,除了某些极少数的情况外,并不会为我们的程序带来多少好处。
思考一下:***p1可不可以?
答:不可以,根据取空间操作,***p1最终变成了*10,显然也是错误的。
(3)多级指针链断线的问题
Int a =10;
Int *p;
Int **p1=&p;
Int b=**p1;
**p1,原是想通过**p1访问到a的空间,将a空间的内容赋给b,但是这里是不对的,因为中间的指针变量p并没有指向a,指针链断线了,所以我们在使用多级指针的时候,必须注意构建的指针链是否完整。当然这里因为是直接写的比较好理解,如果多级指针链是通过传参的方式来构建的话,很容易出现断链的情况,造成的影响就是,要么访问到是空指针,要么访问到了不该访问的地方,导致严重错误。
|