第三章 可恶的指针
一、开章有益
建章时间:一九九九年九月二十六日星期日晚上20:48:25秒
包括以下内容:
如果对一个指针所指向的最终地址不是很清楚的话,那么就使用作图法来理解吧!
指针、多级指针、指针数组、数组指针
数组、多维数组的各种交叉使用的心得
关键:确定一个指针,什么情况代表地址,什么情况代表所包含的地址中的变量的值
在printf使用%p可以按指针形式输出
指针的
指针概述和程序设计原理的讨论
二○○○年四月二十三日星期日晚上09:40:21秒
半年前我建立了这一章,可以说记录的很糙,可能是对指针使用的方法的常见情况用的不多,很难将书本上的内容真正转化成一个对指针理性的认识,从现在的角度看来指针真的是个很难也很重要的内容,要花很多的时间写程序和读程序才能慢慢的理解一个指针的含义,我认为如果要很清晰的理解指针的含义,要从汇编语言的角度就会比较容易。
一说到指针,使人难免想起数组,对于一开始学习C语言来说,数组可以是指针应用最多也最广的实体。
数组和指针加起来、混和起来,就连现在的我有时也要想上半天才能想明白,当然大面的东西是不会有什么难度,但有时人思路上的缺陷就难免产生很多的逻辑错误,所以我认为当你写程序用到指针时,比较好的办法就是将你绝对确定的值用指针表示后看它的输出结果是否是你的结果,这种办法如果使用几个极限数值的话,很大程度上正确性会有很高的把握。
初学指针时,对于指针十分善变的特性可能会将你搞的头晕脑胀,但这其实是正常的,这时学习对它们最好的方法就是把你见到所有关于指针的程序你先心算出结果后,再将程序编译后的结果对比,虽然你的结果错误率也许能达到80%,但你可以一点点的析程,从每一个错误中吸取经验。将它们记录下来,有朝一日你会发现你的经验变的重叠了的比例很大的时候,你那时可以说对指针就有60%的认识了,一般的问题是不会难到你的,但你要记住无论何时都把你心算的错误中得到一条条的理论,这对于指针是至关重要的。
对于一般的书本上说的很多的概念性的问题,我也不想重述。
对于一些比较成式化的东西说一说,在C中数组名代表数组的首地址,函数名代替着函数的入口地址。但在C++中对象名却并不代表着对象的地址。这些首地址的原因是在C或C++转化成汇编形式的时候,这些函数,数组等都是一个标号,使用jmp指令或other call指令调用的。
指针的类型有很多种,可以说所有的数据类型都可以定义成相应的指针,甚至是指针的指针。
指针为什么会应用如此的广泛和繁杂,这与程序的原理是分不开的,在我们学程序设计,应多读一些关于程序原理、系统原理的程序的实现方法的书,会很有裨益。有时可能你觉得你看的东西很难一时用的上,或者你认为是东一笔西一笔,这没有关系,不用把你看的东西死记硬背,只把你感兴趣的内容做一个一般了解即可,但以后你达到了一个新的深度的时候,那些让你冥思苦想的问题,有时会从你读的那些天书里想到一道光明的灵感。
在你面前的PC在其硬件上说不是会一时间就有多大的变化,因此PC程序的原理也就不会变。就象学习指针一样,当将四面八方的原理和经验汇集起来的时候,你就会发现曾经的苦难都已过去,眼前是一片的阳光明媚。你应该高兴一天,在第二天的时候再投入到一个新的层次的学习。
在程序中无论是小到一个变量大到一个函数,类,结构等等,它们都会在内存中有一个真实的物理地址,当然在这个物理地址我们可以暂不理,只管把握我们程序中的逻辑偏移就可以了(这个基址的加入是硬件和操作系统的事)
对于函数,它的应用原理就是在使用它是使用call instruction 调用函数在内存的地址。数组可以使用程序设计上的数组名加下标的引用方式,但它本身要用它的地址,同样也可以使用指针。
说到这,其实指针就是一个让你可以在物理上操作你的硬件的一条捷径,因此它才会显得如些的重要。
指针的发展方向
在C语言中指针的实现在编译上讲是很灵活的,这样规律的后果是指针的优点和缺点都是很极限化的,在C++上指针的使用被加入了很多的限制,比如在函数上使用原型,函数指针必须使用原型,这就要是为了重载函数。
虽然在很多的书上看到说C或C++的编译器本身并不检查指针或数组的越界问题,其实不是不检查而是不能检查,because 指针就是指向实际地址,而编译是无论如何都不可以知道programmer的idea(想法),so that(所以) ourselves must 保证指针指向的correctness(正确性)。
比如在C语言中我们对一个函数的声明很简单,即 [类型]<函数名>( ),无论这个函数有多少个或什么类型的参数,而在C++中函数的声明必须使用原型,虽然限制了指针的使用,但明确后的函数声明,使的C++中函数可以重载,安全性要大大的增强,可以说是对指针发展达到了一个新的境界。在C中指针的对数组的越界引用,编译程序是没有办法检查的。这时在C++中我们可以通过重载运算符的方法对引用进行必要的检查,当然这在很小的程序中是没有必要的。
函数指针,在C++中如果我们在定义函数指针的时候,必须要考虑到这样一个问题,即如果我们的函数的参数列表并不确定,即参数个数不定时,在C++中我们在声明这样的函数或者是函数指针的时候,我们必须使用以下方法:
For Example:(1ch.cpp)
二、指针的初始化
对于指针,如果它在定义时没有初始化,那么使用时动态的对它进行赋值是非常危险的,必须动态的为它分配内存(use malloc or new ect.),but if we在程序中静态的对它赋值,即在源程序中就对它指向的字符串的首地址赋给它是可以的,其实质就是这个给指针赋值的字符串本身在编译时会占用本身的长度的内存,这也是对指针变向的分配内存的方式。
对于一个数组,如果我们在定义时没有初始化,则我们不能在定义以后再对它进行整体的赋值,但字符数组可以使用函数strcpy将值传递给它(actually still是单个evaluate)。
首先从一个多维数组的存储形式上看,无论是几维的多维数组,最后在内存中都是按照一维数组的形式顺序排列的,数组名代表这一个数组的首地址,这就使我们可以通过指针的算术运算,很容易的引用数组元素,但这样也必然带来了一些引用容易使我们思想上产生逻辑错的混乱,特别是使用指针数组和二级指针引用多维数组时这个矛盾显得特别的突出。所以有必要搞清楚一些基本情况。你要记住的一点是对于指针运算是在它现在所指向的层次进行的。
!!!对于相同类型的指针间进行互相赋值,notice a problem please,,i.e.,这个两个指针指向的同一个地址,而不是为地址中的内容制作副本。
If we need to make a duplicate,we can first creat a that type object,then we use the content on the takeing a that type pointer of '*'。
But, there still have a very significate problem , if we copy object is a 'struct' or 'class' or 'union' and they contains some other pointer,then we use above method can but make a non-holonomic(不完整)duplicate,because they still share part content。
Example:
FILE *fp;
FILE duplicate=*fp;
The file pointer contains two charecter point,we must make two duplicate for them。
Except 函数以外,我们使用两个指针有时是unreachable 我们要求的major aim并且还会因此形成一些很难发现的logic error。
1、指针的类型和应用
指针可以指向任意一个类型的实体,关键的问题是要将指针定义成和被引用的实体,具有相同的类型,这样编译程序才能正确的处理指针。
1、取一个变量的地址 int var=10;int *P=&var;
2、取一个数组的首地址 int var[10];int *p=var
3、取一个数组元素的地址 int var[10];int *p=&var[i]
4、取一个同类型指针向指向的地址 int var[10];int *p1=var,*p=p1;
三、数组与指针的简单应用
这几个例子的目的在于理解指针的层次性
1、多维数组与指针概念点评一
程序如下:
# include <stdio.h>
# include <conio.h>
void main( )
{ int a[4][3]={{10,20,30},/*定义一个两维整型数组,4 行3列式*/
{40,50,60},
{70,80,90},
{100,110,120}
};
int *p[4], **pa , i , j ;/* 取值范围为:i>=0&&i<4 j>=0&&j<3 */
for(i=0;i<4;i++)
p[i]=a[i];/*a[i]是这个数组的第二层它代表着它所在行的首地址*/
pa=p;
}/*pa和p[4]它们具有相同的层次,即都是二维,所以它们可以相互进行赋值操作*/
/*这里是一个重点,可能你想a是一个数组,那么a[i]是对这个数组元素的引用。那p[i]是一个指针,为什么不给a[i]放上取地址符(&)呢?在这个程序前我曾说过,多维数组最后都要转化成一维数组在内存中顺序存储,这时a[i]代表的这个二维数组的第i行,每一行本身可以看成一个一维数组,因此a[i]这里起数组名的作用,所以a[i]是一个地址,它代表着a[i][0]~a[i][3]这个一维数组的首地址,也就是一个X维数组中如果要用下标引用数组元素,就必须达到X个下标是对于数组元素的引用。
对于一个多于二维的数组,例如:int a[3][4][3]这样一个数组来说,代表的意思是说有三个4行3列的数组(即12行3列),由于是顺序的存储也可以看成是一个3*4行3列的二维数组,在这个数组里a [0][0]~a[2][3]都代表着一个(一行)一维数组的首地址,而不是对数组元素的引用*/
对于这个数组的引用:
p=a[0];
p[3]+2与p[3][2]的异同点:
p[3]+2与p[3][2]都是对一个数组元素进行操作。首先我们看p[3]它是指针数组P中的一个元素,它指向二维数组a[4][3]中的a[3][0],从上面程序的赋值可以看出:
p[3]+2中p[3]实际等价于a[3]+2,那么对于一个地址加2,它便是指向a[3][2]元素的地址,注意它是一个地址!。
3、其次我们看p[3][2],既然p[3]是一个代表着a[3]的指针,那么就可以将p[3]与a[3]进行置换变成了a[3][2]那么这理所当然是一个对数组元素的正常引用p[3][2]代表着a[3][2]的值(120)
2、二维数组的指针表示图
*( *(p+i)+j)
原义如下: int a[4][3];int *p[4]=a;
在这个指针数组中,每个元素都是一个指向变量的指针,那么按着数组的共性,数组名代表着数组的首地址,因此p实际上指向着指针p[0]的地址,如果这个地址加上i就相当于p[0+i] /*p[0]+i*/,所以*(p+i)即表示指向p[i]指向的地址。
在这个程序中p[i]指向a[i]的地址,a[i]本身是一个一维数组的地址,它指向着a[i][0]的地址/*a[i]相等于&a[i][0]*/,这时如果从&a[i][0]这个地址开始+j那么指针自己就指向a[i]数组的第j个元素。那么*(a[i][0]+j)就是引用a[i]数组中的第j个元素的值。
如图所示:p指向p[0]的地址,*p就更深一层取a[0]的地址,**p指向a[0]地址中的内容。
即:p=&p[0],p[0]=a[0],a[0]=&a[0][0]
3、多维数组赏析三
使用指针数组元素下标的方式来引用一个一维数组
程序:
#include <stdio.h>
void main( )
{int a[20],*p[4],i,k=0;
for(i=0;i<4;i++)
p[i]=&a[i*(i+1)]; /*这里将按4行5列分解一个一维数组*/
for(i=0;i<4;i++)
k+=p[i][i];/*使用p[i][i]的形式<=>*(p[i]+i) */
printf("%d/n",k);
}
p[i]指向一维数组中元素的地址,它是一个确切的地址,然后使用下标的方式来引用一维数组的元素,看来可以把一个数组中元素的地址看做是另一个数组的首地址,而可以使用下标的方式来引用元素.
然后我做了如下的实验:
既然p[i][i]可以引用一个数组元素,那么我再多加一个下标呢?
即:首先取p[i][i]的地址,它肯定是数组元素的一个元素的地址
然后又加了一个下标变成 &p[i][i]由于下标的优先级高于&,所以这里不用加()强制运算符,但多一个下标后就必须加( )了变成如下:
(&p[i][i])[i],结果编译通过结果正确,于是我又加了一个下标成如下形式 (&((&p[i][i]) [i])) [i]),(一个难懂但正确的式子)再次编译又一次通过了看来必须得出结论.
可以使用一个地址量加下标的方式来引用内存中一个连续的区域(这里局限于数组)
这里我要说明的问题是采用地址量(也可以说是一个指针),加下标的方式可以引用数组。如果你不明白,请看下面的话。
其实上面的表达式,它其中表达的值是一个地址常量,即:如果把它表示地址时的值使赋值语句赋给另一个指针,然后用这个指针加下标的方式或者用*引用的方式来引用地址中的值。是可行的!
这时再回想数组最基本的方式引用数组元素,其实正是数组名作为一个最基本的地址常量,加上下标的方式来引用地址中的元素,数组越界并不检查的原因是,用地址(指针)的方式是无法确定所引的范围的,这句话很难懂对吗?但仔细想想,如果数组下标越界的话,得出的一般是不是我们想得到的数,因为我们根本就不知道这个内存中的地址中的值是什么,但如果它是另一个数组中的一个元素,我们不就可以得到一个确定的能理解的值了吗?
二级指针
两种表现形式: * * p
* p[ ]
这两种形式在使用时可以相互代替,都代表指向指针的指针
最后总结:
无论是一维数组,多维数组,指针数组,数组指针,多级指针,它们之间的关系只所以是千丝无缕的,原因在于指针和数组它们引用地址中的值都是采用一个基本地址加上一个下标的类似方式实现的.只不过是因为使用了下标后可以引用,使用*(间接寻址运算符)不过是另一个表达的形式(间接的引用),如果想真正的理解数组和指针的真实的所要表现的思想,只有通过大量的实践和长期的领悟才能理解,但要记住,使用*的形式要比下标要快。
*/
4、指针引用数组元素的逻辑层次
例:对于数组元素的逻辑层次的引用问题
#include <stdio.h>
void main()
{ int a[2][2][3]={1 ,2 ,3 ,/*两个二行三列的数组*/
4 ,5 ,6 ,
7 ,8 ,9 ,
10,11,12,
},*prt=a[0][0];
prt=a[0][0];
printf("%d",(*prt) * (*(prt+2)) * (*(prt+4)) );
}
/*对于指针方式和数组的下标方式来引用数组元素的不同点,
注意:
int a[2][3]={{1,2,3},{4,5,6}};
//此处一级指针,在C++中只能指向一维数组的地址
prt=a[0];
prt=&a[0][0];
由于下标的引用从0开始,因此就会有:
a[0][0]==(prt);
a[0][1]== *(prt+1);
使用指针进入数组元素的引用要注意指针指向的逻辑层次
如
int a[2][3]={{1,2,3},{4,5,6}};
a
a[0]
&a[0][0]
就其实际引用的地址本质来说都是元素a[0][0]的地址,但其逻辑层次却不同的,这时对指针进行运算,指向a的指针+1后将指向a[1] (a[1][0]的地址)数组的地址。指向a[0]的指针+1后将指向a[0][1]
因此在使用指针引用时应根据不同的维数来引用不同层次的数组元素
如*(a+1)代表的是a[1],而*(a[0]+1)代表a[0][1]
四、常见使用指针错误情况
1、指针类型错误
在C语言中,一个void *指针自动转换为赋值语句左侧的指针类型。然而,C++并不发生这种自动变换。进一步讲,在C++中,当把一个void*指针赋给另一类型的指针时,需要一个explict(显式)类型强制转换。
2、动态分配错误
指针使用前都必须检查是否为空,使用空指针几乎肯定会导致程序崩溃。一个错误指针故障之所以难以发现,并非是指针本身的问题。问题出在每次使用指针进行操作时,均会对某些未知的存储单元进行读写。若是读出操作,最坏的可能是得到无效数据。若是写入操作,就会重写其它的代码段或数据段。这种错误可能要到程序运行的后期才能显露出来并可能将程序员引入错误的位置去寻找故障。也许没有任何迹象能表明总是是出在指针上。这类错误会使你员一次又一次的失眠。
常见错误:典型例子就是使用未初始化的指针。
解决办法:使用指针之前,一定要明确它的指向。
3、使用指针的误解
例:int *p,x;
p=x;
此例中将X的值赋给了指针,这样是必然会导致错误的
另一个时而发生的错误是由于对内存中变量存放方式的错误假定,无法知道数据在内存中的确切位置;它们是否均以同样方式存放;每种编译程序是否均对其以同样方式处理。因而对两个不同数据的指针进行比较会产生意想不到的结果。(在极少情况下,可以利用这种方法确定变量间的相对位置)
认为两个邻接的数组可作为上一数组通过指针的递增进行越界存取。
这种错误的出现常常是不自觉的,即如我们用一个指针引用一个数组(*P++),到达数组尾部时,指针的值就已经在数组最后一个元素的地址,但我们有时却很难意识到这个问题,没有及时对指针进行归位处理,使指针越界,这种错误比较常见也很危险。
4、函数返回的指针错误
在使用函数时,参数的传递方法只有三种,即传值调用,传址调用,引用(C++)
函数的返回值可以是任意类型的,这主要是由函数声明决定的。
但当函数返回一个指针时却常常出现一个不易察觉的错误,那就是函数中我们声明的auto型变量,将会在函数运算结束时自动被释放掉。如果函数的返回值是一个指向它的指针,从人的心理意识上好象是正确的,但当函数结束运行后其中的变量被自动的释放时,指针指向的地址将很大机率上被其它的数据占据,得到的值也就不再是正确的。
解决的有二:
一是在函数中定义静态变量,这样在函数结束运算后,变量也不会被释放,但定义一个静态的变量,我觉得要根据这个变量本身的价值而定,如果它并不会在全局使用的话,我们就没有价值把它定义成一个全局的变量。
二是将我们定义的全局变量使用传址调用或引用,使函数对它们进行赋值
五、Funtion 参数的地址传递
1、函数中主调函数与被调函数的数据是采用数值传递的方式
这个定义也同样包括使用指针做形参的函数中即:我们可以改变一个指针所指向地址的值,但不能改变一个实参指针所指向的地址。
2、对于函数来说其实都是单向传递数据(压栈实参的值),那么一个函数也就只能通return语句来返回给主调函数一个值(RET 返回值),但我们通过指针的方式可以达到改变多个值的目的。
在C++中我们对于函数的参数,可以使用引用的方式来改变实现对实参的影响.
定义为 Examples:abs(int&m);
单向的数据传递不会影响实参的值,但可以改变外部的变量(全局变量指在函数体外定义的变量)的值!
3、在TC++3.0中main( )函数支持第三个参数(char *envp[ ])环境字符串参数
4、main(int argc,char*agrv[],char*envp[ ])函数的三个参数的作用分别为:
int argc是保存输入参数的个数,输入的程序名被看做第1个参数
char*argv[]保存输入的实际参数,argv[0]保存着有完整路径的upper的程序文件名。
char*envp[]保存现在所处的环境字串
这里再对于*argv[ ] and * envp[ ]做一点解释
在use function‘printf’output *argv[ ] and *envp[ ]时,由于argv[ ]是一个地址,它实际上代表着实际输入的参数串(字符串数组)的首地址,因此再全部引用这个一参数串时就可以直接使用这个地址使用%s输出
即:printf("%s/n",argv[i]);/*
i代表着第i个参数串数组*/
5、对于一个函数如果希望它返回多个数值可以采用地址传递方式;
例如:
int m,row,col;
func(array,&m,&row,&col,3,3);
在这个函数调用中需要返回row,col,m的值就可采取取地址的办法实现这一操作。
6、一个二维的整型数组,如果在编写函数调用的时候,如果我们要求这个数组的首地址。
7、printf函数格式转换说明符
%d按十进制整数输出
%o按八进制整数输出
%f按实数形式输出(6位小数)
%c输出单个字符
%s输出字符串
%p输出指针