指针学习注意点

来源:互联网 发布:数字电子网络配线架 编辑:程序博客网 时间:2024/05/17 02:58

昨天给一个学生讲解指针的问题,发现指针中存在一些问题,为了让大家尽力避免这种情况,特此在这里稍作说明。

在我们的培训机构里啊,指针的课程大多是6-8个小时,可以说上课时间很长,但是啊,正是由于时间过于长,在关注细节的同时,很容易忽略指针学习的主线。以谭浩强书为例来说明,什么是指针学习的主线。

指针学习主要是从p+1究竟加了几个元素来分,可以分成两大类:一个是普通指针另一个是两个特殊指针,分别是数组指针和函数指针,一般的指针“加1”都是加一个元素的长度,而数组指针加1加一行,函数指针不能进行加1操作(当然void *也不能进行“++”操作,但是毕竟void 指针比较好理解,就不单独列出来了)。为什么是“+1” 不是“++”呢,因为数组名不能进行”++“操作啊。这就是指针学习的主线,当学习指针的时候很容易迷失在众多 的细节当中而不能自拔,每当这个时候就需要需要想想这个分类方法,你会突然间感觉很轻松。

那么说说什么是指针呢?指针就是地址,地址就是指针。这一点必须时刻记在心里,这样能够减少你很多的脑力劳动。用VC6.0举个例子看看。

int main(){int a = 3;return 0;}
代码非常简单,调试一下看看内存如下:

从图中可以看出,变量的首地址就是指向该变量的指针,如果刚一开始不习惯可以这样说,变量a的指针就是变量a的地址。


那么咱们接着看看如何存储一个指针呢,一个地址指向一个字节,32位计算机的内存总共有(2^32-1)个字节,也就是说总共有42亿多个地址,而这些地址刚好可以用一个unsigned int来存储,但是这样做不好,因为编译器很难区分出这样的一个无符号整型变量里放的究竟是地址还是普通的数字。编译器采用了另外一种方法来保存地址,就是专门声明一类变量,这类变量只能存放地址,不能存放整数(好奇怪),那么这类变量就是指针变量。

上面有一个”好奇怪“三个字,咱们来看一下为什么奇怪。

int main(){int *p;p = 0x18ff44;return 0;}
这段程序编译的时候会有警告如下:
 warning C4047: '=' : 'int *' differs in levels of indirection from 'const int '
说是类型不兼容,稍作修改即可编译通过:

int main(){int *p;p = (int *)0x18ff44;return 0;}
不管你是否强制类型转化,p里面存放的都是0x18ff44这样一个整型数字,那么为什么非得给我报警告呢,这主要是为程序员着想,因为刚一开始学习的学生,哪怕你学了一两年了,传递指针的时候也很容易出现类型不兼容,为了让你及早的发现错误,编译器才给出这样贴心的提醒。从这一点我们要知道,含有指针类型不兼容的警告,要特别注意,不能像普通处理警告那样置之不理(当然了,所有警告都不能置之不理)。


好了,接着看什么是二级指针,咱们的课貌似进行的太快了,上来就说二级指针啊。没事很容易的,看看程序:

int main(){int a = 3;int *p;p = &a;return 0;}
看看内存:

看到了吧,多简单啊,a的地址是0x18ff44,p是个变量,是个指针变量,里面保存a的地址(看个人喜好,指针也行),那么p的地址也就是图中的0x18ff40就是a的二级指针了。

简单的一句话就是,一级指针变量的地址是个二级指针,二级指针变量的地址是个三级指针,依次类推就是各种多级指针,但是三级以上指针,基本不用。


这时候可能又出问题了,就是老分不清*p不同地方的不同意思。

因为我以前学习的时候啊,总是试图给*p找一个统一的解释,劝你别找了,越找脑子越乱,这个问题一定要分开来看,一个是声明和定义的时候,一个是除了声明和定义的时候。也就是分清哪些是指针变量的声明和定义。这个非常简单,看看那句话是否有类型说明,只要有类型声明不是声明就是定义,除此之外都是在使用指针。

为什么说这一点呢,因为昨天那个学生就是和我以前犯了同样的错误,老是想给*p一个统一的解释。千万千万不能这样去想啊。

看几个例子吧

<span style="white-space:pre"></span>int *p;
定义一个指针p,能保存整型变量的地址。

int a;int *p = &a;
定义一个指针p,并同时给p赋初始值。

extern int *p;
声明一个指针p

稍微说一下声明和定义的区别啊,在本源程序开辟空间就是定义,不在本源程序中开辟空间就是声明。

void fn(int *p, int **q){}
这里面的p也是定义,q也是定义。

就记住一句话举行,只要有类型声明就是定义或声明一个指针,这样做只是为了说明该变量是个指针,能存储何种数据类型的地址。


接着看看数组和指针,如果想学好这类指针,需要使用三个术语来说明,列指针,行指针和块指针(这些名词书上没有明确使用,但是事实却是如此)。

一维数组比较简单,数组名是一个列指针。

二维数组就稍微复杂一点,比方说a[5][5],那么数组名a是行指针,a+1,a+2 都是行指针,a[0],a[1],a[2],都是一维数组的数组名,因此是列指针。二维数组就是数组的数组,每一行是个一维数组。只要上面概念清晰,学习二维数组和指针就能轻松点。

看个例子

#include <stdio.h>int main(){int a[5][5];printf("%#x--------------------%#x\n",a[0],a[0]+1);printf("%#x--------------------%#x\n",a,a+1);printf("%#x--------------------%#x\n",&a,&a+1);}
输出结果如下图:



可以看到a和&a和a[0]的值虽然相同,但是+1的结果却有很大的不同,也就是说他们所表示的指针类型并不一样。

这里有几点需要说明,当对数组名取地址(&)时,这个&不再是普通的取变量地址的意思,而只是一种地址运算方式而已。也就是说&符号其实有两个用途,一个是取变量的地址,一个是地址运算。

从上面结果我们能得出如下结论:一维数组的首地址是列指针,对列指针取地址得到行指针,对行地址取地址得到块指针。列指针一加加一个,行指针一加加一行,块指针一加加一块,只有在对数组名操作的时候才有列,行,块的概念。

好了,那么来做个题吧,看看掌握的如何:

#include <stdio.h>int main(){int a[5];printf("%#x\n"&a+1);}
这个会输出什么,&a+1指向哪里呢?


现在回过头来看看文章刚一开始说的那句话,指针分两种那句话,那个数组指针就是咱们说的行指针。也就是说,只有在对数组名取地址的时候才会出现这样一种特殊类型的指针,其他任何指针都是普通指针。

这也就是为什么二维数组传参的时候会写成下面这个样子:

#include <stdio.h>void fun(int (*p)[5]){}int main(){int a[5][5];fun(a);}
因为a是个行指针,而指针传递时必须类型兼容,因此必须用行指针去接收,也就是用数组指针去接收。


学生在做练习的时候还有一个比较容易困惑的地方就是指针数组,看个小例子:

#include <stdio.h>void fun(char **p){}int main(){char *a[5] = {"abc","def","ghi","jkl","mn"};fun(a);}

为什么传递数组的首地址,需要用二级指针来接收呢?

1,像“abc”这样的字符串是存储在代码区的,你可以这样认为,“abc”返回的一个字符串的首地址,也就是“a”的地址。

2,指针数组,是数组(怎么看,汉语中后面的是主要的,前面大多是修饰,也就是说指针是修饰数组的,指针数组是数组),数组里面存放的是一个个指针。a[0]中放的是“abc”的地址,a[1]中放的是“def”的地址。

3,数组名代表数组首元素的地址,数组中每个元素都相当于一个变量。

4,那么这是不是形成了,a[0]中放着一个一级指针,而a == &a[0] 是不是说a中保存的是一个地址的地址,是不是就是二级指针。

那么请问*p 一加加多少,p 一加加多少?

这个问题如果你从实参开始看,估计得想破脑袋也想不通,那么怎么看呢,只看形参。我不管你传递进来的是什么,既然可以用char **p来接收,那么就说明我必然能用char **p所具备的所有属性对p或者是*p进行操作。

看例子:

#include <stdio.h>void fun(char **p){printf("%#x\n",p+1);printf("%#x\n",*p+1);}int main(){char *a[5] = {"abc","def","ghi","jkl","mn"};fun(a);}

运行结果是:



从中能够看出,p+1 加了四个字节,*p+1 加了1个数据元素的长度。

我们如何更简单的解释这个问题呢,三句话

*p操作是什么意思:

1,取p中保存的偏移地址作为起始地址

2,以p的基类型的长度作为偏移长度

3,以p类型基类型的类型作为类型 

接着咱们用这个理论去解释:

把char **p 看成char *(*p),可以知道(*p)的基类型是char 型,那么一加加一个sizeof(char)大小,而char **(p)是一个二级指针,它的基类型是char * (所谓基类型,就是这个指针能保存什么类型),根据第二条,char *是四个字节长度,那么一加加四个字节。


如果你感觉这有些不妥,那么也可以这样来看:

*p操作的意思:

1,取p中保存的偏移地址作为起始地址

2,以p中保存的数据类型的大小作为偏移长度

3,以p中保存的数据类型作为数据类型

p是二级指针,那么p里面保存的肯定是(char *)类型的数据,那么这个数据的长度是四个字节,因此我要加上四个字节。*p里面放的是字母,那么我每次加的时候就加一个字母长度。


不管使用什么方法,都希望你能够理解理解清楚。

总结一句话:二级和二级以上指针,一加加四个字节(严格点说就是指针的大小),对二级指针解引用(*p)一加加一个元素长度。


其他还有指向函数的指针,和返回指针的函数,都不怎么难,稍作说明。

指向函数的指针作为函数参数时也叫回调函数,中国大部分程序员很少用这个,因为可以用直接调用函数来代替,典型应用就是线程函数。

返回指针的函数,必须切记,不能返回一个局部空间的地址,返回malloc出来的地址是可以的。


希望大家在学习指针的时候能够把握住指针的脉络,要不,你会发现指针里面的东西太多,根本就记不住,会浪费很多很多时间的。











0 0
原创粉丝点击