指针专题
数组的指针、指针数组以及指向指针的指针
考虑数组的指针的时候我们要同时考虑类型和维数这两个属性。换一句话,就是说一个数组排除在其中存储的数值,那么可以用类型和维数来位置表示他的种类。
A)一维数组
在c和c++中数组的指针就是数组的起始地址(也就第一个元素的地址),而且标准文档规定数组名代表数组的地址(这是地址数值层面的数组表示)。例如:
int a[10];int *p;
p=&a[0]//和p=a是等价的:
因为a是数组名,所以他是该数组的地址,同时因为第一个元素为a[0],那么&a[0]也代表了该数组的地址。但是我们是不是就说一个数组名和该数组的第一个元素的&运算是一回事呢?在一维的时候当时是的,但是在高维的时候,我们要考虑到维数给数组带来的影响。
a[10]是一个数组,a是数组名,它是一个包含10个int类型的数组类型,不是一般的指针变量噢!(虽然标准文档规定在c++中从int[]到int*直接转换是可以的,在使用的时候似乎在函数的参数为指针的时候,我们将该数组名赋值没有任何异样),a代表数组的首地址,在数字层面和a[10]的地址一样。这样我们就可以使用指针变量以及a来操作这个数组了。
所以我们要注意以下问题:
(1) p[i]和a[i]都是代表该数组的第i+1个元素;
(2) p+i和a+i代表了第i+1个元素的地址,所以我们也可以使用 *(p+I)和*(a+I)来引用对象元素;
(3)p+1不是对于指针数量上加一,而是表示从当前的位置跳过当前指针指向类型长度的空间,对于win32的int为4byte;
B)多维数组
对于二维数组a[4][6];由于数组名代表数组的起始地址,所以a(第一层)和第一个元素a[0][0]地址的数字是相同的,但是意义却是不同的。对于该数组我们可以理解为:a的一维数组(第一层),它有四个元素a[0]、a[1]、a[2]、a[3](第二层),而每个元素又含有6个元素a[0][0],a[0][1],a[0][2],a[0][3],a[0][4],a[0][5](第三层),…到此我们终于访问到了每个元素了,这个过程我们经历了:a->a[0]->a[0][0];
整体来讲:a是一个4行5列的二维数组,a表示它指向的数组的首地址(第一个元素地址&a[0]),同时a[0]指向一行,它是这个行的名字(和该行的第一个元素的首地址相同(第一个元素为地址&a[0][0]))。所以从数字角度说:a、a[0]、&a[0][0]是相同的,但是他们所处的层次是不同的。
既然a代表二维数组,那么a+i就表示它的第i+1个元素*(a+i)的地址,而在二维数组中
*(a+i)又指向一个数组,*(a+i)+j表示这个数组的第j+1个元素的地址,所以要访问这个元素可以使用 *(*(a+i)+j)(也就是a[i][j])。
他们的示意图为(虚线代表不是实际存在的):
对照这个图,如下的一些说法都是正确的(对于a[4][6]):
- a是一个数组类型,*a指向一个数组;
- a+i指向一个数组;
- a、*a和&a[0][0]数值相同;
- a[i]+j和*(a+i)+j是同一个概念;
总结一下就是:我们对于二维指针a,他指向数组a[0,1,2,3],使用*,可以使他降级到第二层次,这样*a就指向了第一个真正的数组。对于其他的情况我们也可以采用相同的方式,对于其他维数和类型的数组我们可以采用相类似的思想。
说到指向数组的指针,我们还可以声明一个指针变量让它指向一个数组。例如:
int (*p)[5];
这时p就是一个指针,要指向一个含有5个int类型元素的数组,指向其他的就会出现问题。
这个时候我们可以使用上面的什么东西来初始化呢?
我们可以使用*a,*(a+1),a[2]等。
原因很简单:我们在一个二维的数组中,那么表达方式有上面的相互类似的意义呢?只有 *a,*(a+1),a[2]等,
C)指针数组
一个指针数组是指一个数组中的每个元素都是一个指针,例如:
int *p[10];//而不能是int (*p)[10]
或者
char *p[10];
此时p是一个指针(数值上和&p[0]一样);
在前面有int t[10];
int * pt=t;//使用pt指向t
那么这里我们用什么指向int *t[10]中的t呢?我们要使用一个指针的指针:
int **pt=t;
这是因为:在int *t[10]中,每个元素是指针,那么同时t又指向这个数组,数组上和&t[0]相同,也就是指向t[0],指向一个指针变量,可以说是一个指针的指针了,所以自然要用
int **pt;
D)指针的指针
一个指针变量内部可以存储一个值,这个值是另外一个对象的地址,所以我们说一个指针变量可以指向一个普通变量,同样这个指针变量也有一个地址,也就是说有一个东西可以指向这个指针变量,然后再通过这个指针变量指向这个对象。那么如何来指向这个指针变量呢?由于指针变量本身已经是一个指针了(右值),那么我们这里就不能用一般的指针了,需要在指针上体现出来这些特点,我们需要定义指针的指针(二重指针)。
int *p1=&i;int**p2=&p1;
综合以上的所有点,下面是我们常常看到一些匹配(也是经常出错的地方):
int a[3],b[2][3],c,*d[3];void fun1(int *p);void fun2(int (*p)[3]);void fun3(int **p);void fun4(int p[3]); void fun5(int p[]);void fun6(int p[2][3]);void fun7(int (&p)[3]);
函数 不会产生编译时刻的可能值(但逻辑上不一定都对)
函数 |
不会产生编译时刻的可能值(但逻辑上不一定都对) |
fun1 |
a, &a[i], *b ,b[i],&b[i][j] ,&c ,d[i] |
fun2 |
b,b+i, |
fun3 |
d |
fun4 |
a, &a[i], *b ,b[i],&b[i][j] ,&c ,d[i] |
fun5 |
a, &a[i], *b ,b[i],&b[i][j] ,&c ,d[i] |
fun6 |
b |
fun7 |
a |
为什么可以有这样的搭配,原因如下:
- 对于fun1 fun4 fun 5: 在编译器看来fun1,fun4,fun5的声明是一样,在编译时候,编译器把数组的大小舍去不考虑,只考虑它是一个指针,也就是说有没有大小说明是一样的,所以三者的形式都是fun1的形式(其实只要提供了int*指针就可以了);
- 对于fun7 :以上的解释对于引用是不适用的,如果变量被声明为数组的引用,那么编译器就要考虑数组的大小了,那么必须和声明一模一样(所以fun7就只有a合适);
- 对于fun2:p是一个指向一个含有3个元素的数组,这样b和b+i正好合适,而a却不是(它是指向a[0]的,不是指向这个数组的);
- 对于fun3:p是一个指针的指针,而d指向d[0],同时d[0]又是一个指针,所以d就是一个指针的指针。但是b却不是(它是一个2*3的矩阵也就是年int [2][3]类型);
- 对于fun6,p是一个2*3的数组类型,和b恰好完全匹配;
二、函数指针、函数的指针参数以及返回指针的函数
A) 函数指针
C++规定,一个函数的地址就是这个函数的名字。我们需要指出的就是一个指针需要指定类型是为了后来的指针解析时候使用,通过指针有效快速访问对象。那么对于函数的指针,它要表示出该函数的那些特性才能满足解析的唯一性呢?答案就是一个函数的特性有它的参数列表和返回类型。
下面是一个函数指针的例子:
int (*p)(int I,int j);
不能是
int *p(int I,int j),
这样就变成了返回指针的函数声明了。
在C++中处于对安全性的考虑,指针和它指向的对象要类型一致,也就说上面的指针所指向的函数的特性要和它一模一样:例如指向int min(int I,int j);是可以的。但是指向int min(double I ,double j);是不可以。函数指针也和其他的指针一样,在使用的时候很怕发生"悬空",所以在使用的时候同样要判断有效性,或者在定义的时候就初始化。
int (*p)(int I,int j)=min;int (*p)(int I,int j)=&min;int (*p)(int I,int j)=0;
B) 函数的指针参数
函数指针可以作函数的参数:例如我们有一个积分的算法,对于不同的数学函数可以进行积分(我们这里假设函数都是一元的);
那么我们的算法接口可以定义为:
template<class T>T integrate( T lower, T upper , T (*)(T)=0 )throw(integrated_exp);
这里的最后的参数是一个函数的指针,并且被设定缺省值为0。这个函数返回一个值,同时需要一个参数。假如加入我们有这样的一个函数:
double line(double x){ return a*x+b;}
那么我就可以使用了。
函数指针还可以作为返回类型(注意不是函数!!,某个特定的函数是不可以作为返回类型的。)假设:
typedef int (*PF)(int );PF getProcessMethod( );//true
C) 返回指针的函数
一个函数的返回是函数的重要接口之一,c++的一个重要的强大的功能就是能够设计足够复杂和好用的用户自定义类型。而同时处理和传递这些类型也是很麻烦的一件事情,我们不想把我们的时间都花在这些对于我们的实际工作没有很实质帮助的拷贝上,解决这个问题就要依赖我们的接口设计:c和c++都提供了相应的解决方案,在c++中我们可是使用引用,讲他们作为函数的实际参数,或者我们在函数的实际参数中使用一个指针等。同样我们还可以使用一个函数返回一个指针:但是这是一个很不好解决的问题!
我们首先容易出错的是:将一个局部变量的地址传出来!例如:
UserType * Process( ){ UserType ut(param-list); //process ut; return &ut;//}
这个变量在我们的函数结束的时候就被销毁了,尽管地址可以传出去,但是这个地址已经不存在了,已经不能使用的东西,在这个函数之外却不知道,难免要出错!
同时我还会有一个比较麻烦的问题:使用new,又容易造成内存泄露
UserType * Process ( ){ UserTpye *put=new UserType(param-list ); //process put; return put;}
我们在函数内部使用了一个new,分配了一个空间,这样传出来也是可以!
就是说不会发生上面的问题了。但是用户通常都会忘记在程序的外面在把这个借来的空间还回去!内存空间就这样泄露了!
可能也是这些另人无奈的问题,所以很多程序员把函数的参数设定为指针或者引用,以此来代替这种向外传输吧!总之,使用这种返回指针的函数要小心!
三、类成员的指针
类成员和一般的外部变量相互比较,不同就是它所在的域不同,这个域很重要,它决定了该变量可以使用的范围。那么一个指针如果要指向类的成员函数或者成员变量,那么除了要表达它的返回类型、参数列表或者类型之外,那么还要说明它所指向的变量(或者函数)的域,为了说明该域我们要使用类域限定:
class NJUPT{ static double money=20000000; int num; public: NJUPT():num(10){}; int get(){return num;}; double getMoney(){reuturn money;}}
我们定义成员的指针为
int NJUPT:: *p;//指向int型成员变量int (NJUPt::*)p()//指向int f()型成员函数。
为了使用这些指针,我们需要使用该类型的变量或者指针。
NJUPT s,*ps;
那么调用的方式为:
cout<<s.*p;cout<<(s->*p)();
这个看起来似乎很奇怪!但是只要你想到我们定义的指针被限定在了类域中了(我们在开始定义的使用使用了NJUPT:: ),这么使用也是很自然的。
如果一个类还有一些静态成员变量和静态成员函数,那么我是否也想这样使用呢?
答案是不用,静态成员我们就可以象使用外部的普通成员一样定义一个指针或者函数指针来访问就可以了,究其原因主要是这个成员的类型性质决定的。
double *p=&NJUPT::money;double (*p)()=&NJUPT::getMoney():