原帖请看:http://yzfy.org/bbs/viewthread.php?tid=710&extra=page%3D1
《对函数调用的深入探讨》:http://yzfy.org/bbs/viewthread.php?tid=688&extra=page%3D1
首先声明,这篇文章是给那些学完了指针,但是对指针的使用和实质还存在疑问的朋友。
如果你根本就没有学过指针,请先去参考一下几本经典的教材,再来看本文。
因为在这里将不会解释操作符的用法。
有人说,C的精髓在于指针,掌握指针的使用就掌握了C语言。当然这种说法不一定严谨,
但毕竟突出了指针的重要性,也导致了一些初学者“谈指针色变”,避指针唯恐不及,
生怕一不小心就用错导致莫名其妙的问题。那么指针是什么呢?指针的本质又是什么呢?
因为Q群里面很多人都在问相类似的问题,这里发一个指针的教程,希望对大家有帮助。
在讲指针之前,我们先说说类型转换。对类型转换完全了解的人可以跳过这一部分,
直接看指针,但是还是建议大家看看,说不定会对你有所帮助呢。
无论是C还是C++,类型转换都是比较难的。何谓类型转换?从一种类型转换位另一种类型是也!
C++是强类型语言,而C要宽松一些,所有的强类型语言都会有类型转换的操作,
因此看完这里大家就可以举一反三,对Java等等其他语言的类型转换有比较深的了解。
类型转换分为两种,一种是隐式类型转换,一种是显式类型转换。这里举一个例子:
.
8.0是double类型(C规定,程序中出现的任何无后缀小数都为double类型,
而出现的任何无后缀整数都为int型,在写程序时一定要记住),而3是int型。
除法运算可否在两种不同类型的变量之间进行呢?显然是不行的。因此,这里就需要转换。
我们知道,double型可表示的范围远远大于int型。而C和C++在这类转换中的原则就是,
范围小的类型向范围大的类型转换。所以,3被转换成了double值3.0,然后做除法。
8.0/3.0=2.666666……
如果这时这个值被赋给一个double了,那么自然什么事都没有,double得到了这个小数,
万事和谐……可惜我们这里没有这么和谐了,这个double值被赋给了一个int……
我刚才说什么来着?隐式类型转换只能从小到大转,可是现在怎么从大到小了呢?
不急。你可以先编译一下这个语句,你会得到这样一个警告:
.
因为double比int要大,这样转换自然容易丢失数据。想象一下不是2.666……,
而是1e200,自然int就溢出了,你什么也得不到。这里只是警告,对于严格的编译器,
甚至会把它当作一个错误。
那么怎么办呢?这时我们就需要强制类型转换了。强制类型转换强迫编译器将一个
类型转换成较大的同一类类型。比如double到int,再比如int到char,这样,
我们就可以很简单地避免警告了:
.
这时,应该就没有警告了。
这里提一下,细心的人可能发现了,我加了两对小括号(int) 和(8.0/3),
第一对小括号是强制类型转换的语法,告诉编译器,将后面的值转换为括号里面的值。
而第二对小括号则要求先计算8.0/3,再对值做转换。
因为强制类型转换运算符的优先级高于除法,所以如果不加第二对小括号的话,
强制类型转换就针对8.0了,将8.0转换成int,然后和int做除法。除号得到了两个类
型一致的值,自然喜出望外,马上计算了结果为2余2,丢弃了余数,将商传给a。注意
到,这里加不加第二对括号,其实结果是一样的,但是大家一定要明白其中的区别,
遇到类似的问题才不会迷茫。
大家明白类型转换了么?类型转换有几个重点:
1 类型转换只能在相关类型里面转。C里面所有的内置类型都是相关的,
因此可以做类型转换。而结构体,共用体,和C++里面的类都不是相关的,因此不能转换。
(如果非要转换怎么办?看完了本教程,请看习题)
2 如果转换后的类型“大于等于”转换前的类型,编译器就可以代劳,顺便帮我们转了。
而不需要我们指定。这种转换叫做隐式类型转换。而如果转换后的类型“小于”转换前的类型。
编译器就会担心:哎呀,你是不是写错了呢?是不是粗心了呢?就会给出警告甚至错误,
要求你写明“这里请你强制转换成特定类型”,编译器才能乖乖地按照你的意图,进行转换。
稍稍提一下。如果转换前的类型“等于”转换后的类型,基本上两者在内存中的“原始数据”
是一样的,是0xffff的就都是0xffff,但是,如果转换前的类型和转换后的不等了,
那么在内存中两者很可能就不一样了。不要以为一个等于1.0的浮点数在内存里面看起来
和等于1的int是一样的。哪那么好的事情,你用起来觉得是一样的是因为编译器自动帮你转换了!
其实它们相差了十万八千里。因为这个缘故,在有符号类型和无符号类型之间转换的时候,
尤其要注意,不要弄错了值。
C和C++是“宽松”的语言。这里的“宽松”是指,编译器完全信任你,
将底层的一些操作的权利也赋予了你。所以你既然使用了C和C++,就要对得起这种信任。
如果你觉得转来转去好麻烦,都是数字么干嘛非要这么转来转去的。
你可以去用VB或者其他弱类型的语言。那些编译器不信任编程者,
什么事情都管得规规矩矩不让你造次,所以也不会提供类型转换给你,就算提供了也是隐式的。
你当了甩手掌柜,编起程来自然会方便快捷啦,但是也注定了你不容易写出简练强大的代码。
为什么C和C++如此流行,就是因为这种“信任哲学”。所以,大家编程的时候注意,
一定要弄清楚自己写的程序到底是怎么一个流程,数据在内存中到底是怎么流动的。
既然人家信任你,就一定要对得起这种信任。
OK,类型转换大家都知道了。现在我们来讲指针。听别人说指针多么多么有用啊,
多么多么神秘啊,指针是什么呢?一句话,指针其实就是地址。
指针是地址的形象说法。地址不一定指内存地址,虽然这是最常用的。
本教程我们只讨论内存地址,大家可以将下文中的“地址”简单地理解为“内存地址”。
在内存这座大厦里,到底是怎么可以方便快捷地找到想要的人的呢?
其实内存里面所有的房间都被编上了一个号码,这个号码就是所谓的地址了。
比如CPU老总要教训一下收购部的小伙子,他可以对内存总管说“兄弟,
你把住在1号的那个小子送过来”,这样内存就寻找1号房间,然后将一号房间里面的小伙子
拎着扔给CPU老总(-_-!!!),教训完了,CPU要给小伙子换个工作,于是,
CPU又对内存说“把这个小伙子放到3号房间吧”,于是内存接住CPU扔回的小伙子,
安置到了3号房间…………
呵呵呵,不搞怪了。大家明白,只要有了一个地址,就可以访问到特定的内存。
这就是指针。指针其实和刀差不多,同样是刀,可以切菜,可以杀牛,可以做手术,
可以杀人—— 慢!大家有没有想到?做手术的刀和切菜杀牛乱七八糟的刀其实都是不同的。
虽然都是刀,但有这样那样的区别!对,指针也是这样,因为用途不同(可能指向一个int啊,
也可能指向一个double啊),所以指针也分很多种。这些指针本质上都是一样的
(都是内存的地址嘛),如果指向一个地方,那么值也是一样的(同一个地方的地址自然
只有一个咯)。问题来了。刀还可以从形状成色看出来是干什么的,但单纯一个指针,
你怎么看出来它指着的是什么东西?除了从声明时候的类型看出以外,具体的使用时,
不同类型的指针之间到底会有什么差别呢?
我们来做一个实验。这里需要大家理解类型转换。看过上面对类型转换的介绍,
理解这个实验应该不难。
char *pt=(char*)a;
printf("%d %d",*(a+1),*(int*)(pt+4));
.
大家可以运行一下这段程序,看看输出多少。如果大家没有输错的话,结果应该是2,2。
我们来分析一下这个程序。首先是声明了一个有4个元素的int数组。数组名其实就是一个指针。
大家可以把它当作指向int型变量的常指针(所谓的常指针,就是值不会变的指针。你想,
你怎么改变一个数组名指向元素的位置呢?),我们可以把数组名的类型写成int *const。
这里const在星号的后面。所以标示指针本身是不能被改变的,而指针所指向的元素可以改变。
这样第二行就好理解了。将数组a的首地址强制类型转换成char*,
然后赋给pt这个char*类型的指针。这里的转换是int*const => char*。为什么可以这么转换呢?
上面已经跟大家说了,指针和指针实质上是一样的。请大家务必记住这句话。这表明,
所有的指针之间都是可以转换的。当然,必须用强制类型转换。因为指针的类型毕竟不同。
然后注意的是,所有指针都可以转换,但是不是所有的指针都可以被赋值,
你如果写了a=(int*const)pt;自然是会出错的了。
假设这个数组a在内存的0x4000(注意:目前大多数计算机,指针都是32位,
严格说来这里应该是0x00004000,但是为了简便我只写16位出来,大家看到以后不要感到困惑)处,
我们来看看现在的内存到底是什么样子的:(至于为什么是这样,
可以看我的另一篇文章《对函数调用的深入探讨》)
-------------------------------------------------------
0x3FF0: XX XX XX XX XX XX XX XX XX XX XX XX 00 40 00 00
0x4000: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00
.
这里,0x3FFC位置的,就是pt了。注意到了吗?虽然pt指向的是char型的变量,
但是pt仍然是四个字节长,因为是地址嘛。希望看到了内存布局,你能更加理解
“指针和指针之间实质上是一样”这句话。看,x3FFC开始的四个字节的值是0x00004000
(注意:X86的计算机,字节是倒着摆的,所以我们要正过来看)。
正好是数组首元素所在的位置。
然后我们看*(a+1),a的类型是int *const,a+1又是什么呢?C规定,
将一个指针加上一个数字,那么这个指针的值会递增这个数字乘以指针指向元素的大小。
晕了么?解释一下你就明白了。
int型是四个字节。如果你给指针加一,指针变成了0x4001的话,那int型不是被破坏了么?
因为如果你在这个地址写一个int的话,那么肯定有三个字节写到了第一个元素里,
一个字节写到了第二个元素里,估计离程序崩溃也就不远了。所以这里一次加了
1*sizeof(int)=1*4=4个字节。1代表加的数字,而4就是int的长度了。OK,现在和谐了,
a+1的值为0x4004,从图书可以很清晰地看出,0x4004是数组第二个元素的首地址。
于是我们将0x4004处的值用地址操作符(*)取出来,交给printf输出,
我们就得到了第一个值2。
再来看*(int*)(pt+4)。pt是一char型的指针。现在值仍然是0x4000,
因为char型大小为一个字节,所以pt+4其实就是直接相加,其值为0x4004。
但是pt是指向char的指针啊,就算加了4,其结果仍然是指向char型的指针。
这个时候如果取数的话,只能取到一位,这不行啊!于是,我们使用强制类型转换,
将计算结果0x4004强制转换成了指向int型的指针(注意指针本身的值并没有改变,
仍然是0x4004),再取出来,这里的转换是char* => int*。OK,仍然得到了2这个值。
我们来总结一下:
1 指针的内容是个32位的值。(位数是由计算机定的,目前大部分为32位)
2 指针和指针之间的差别在于,给指针一次递增或者递减的“跨度”不同,
以及使用地址操作符取出的元素的长度不同。
3 对指针进行类型转换,指针本身的值不改变。参照第二点可以知道,
对指针进行类型转换,只会改变指针递增递减时的跨度,
和使用地址操作符取出元素的长度。
请大家牢牢记住这三点,这样无论看到什么应用,脑子里都可以清清楚楚的。
准备好了吗?我们再看看看比较难一点的题目。
int *pa=(int*)a;
printf("%d %d\n",a[1][1],pa[5]);
int b[12]={1,2,3,4,5,6};
int (*pb)[4]=(int(*)[4])b;
printf("%d %d\n",b[5],pb[1][1]);
.
呵呵,是不是觉得眼花了?没关系,我们一个一个来看。
我们先看看,这段程序执行完了的时候,内存会是什么样子的。
我们仍然假设a数组首元素的地址为0x4000
-------------------------------------------------------
0X3FC0: XX XX XX XX XX XX XX XX CC 3F 00 00 01 00 00 00
0x3FD0: 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00
0x3FE0: 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x3FF0: 00 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00
0x4000: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00
0x4010: 05 00 00 00 06 00 00 00 00 00 00 00 00 00 00 00
0x4020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
.
大家可能看着有点晕。我来理一下头绪,0x3FC8处的四个字节是pb的,
0x3FCC是b数组的首元素地址。b数组有12个元素,自然掠过12*4=48个字节以后的0x3FFC
就是pa的地址,而0x4000自然是a数组的首地址啦。
a[1][1]自然比较好理解,a是个二维数组,a[1][1]指的是第一行第一列那个元素,
我们数数看,1,2,3,4,5,6。对,就是6。所以a[1][1]的值是6。我们来看内存布局,
为什么6以后的自解都是0了呢?因为C规定,如果初始化设定项没有完全指定初始化内容,
则剩下的元素默认为0,大家可以回想一下教材上的有关内容。
pa的内容是a的首地址。为0x4000。因为a是个二维数组(我们暂时不管它是什么类型),
要变成int型指针,肯定需要一个强制类型转换了。接下来就可以写pa[5]了……慢着?pa[5]?
是的,你没有看错,指针也是可以用取元素操作符的。这也是我为什么将一维数组的数组名
当作是常指针的原因。在这里。一维数组的数组名和int*型指针的行为是完全一样的。
大家可以试一下各种操作,比如加减,比如取地址等等,可以发现他们完全是等价的。
所以指针也是可以使用取元素操作符的。
那么没有什么疑问了,pa[5]取到了pa指向数组的第五个元素,自然也是6。
我们想想看,既然指向int类型的指针和int类型的数组完全等价。
而二维数组又可以当作是元素为一维数组的一维数组(比如a[3][4],
我们可以当作是一个一维数组a1[3],它的每个元素都是一个一维数组a2[4],
a[1]是第“二”个元素数组的首地址,而a[1][1]是第一个元素数组的第一个元素),
稍微等价代换一下,我们可以得出这样的结论:
指向int类型一维数组的指针和int类型的二维数组完全等价!
指向int类型一维数组的指针怎么写呢?首先它是个指针,所以应该是*pt,
然后它是个指向int类型一维数组的指针,int类型一维数组的写法是int a[4];,
那么我们用pt换掉a,得到指向int类型一维数组的指针的写法:
.
注意我加了一对小括号,原因同样是*操作符的优先级低于[]操作符,如果不加小括号,
就变成声明数组,而不是指针了。大家记得,和变量名先接触的是什么,变量的本质就是什么。这里pt先和*接触,因此它的本质就是指针,然后我们才去判断它是什么类型的指针。
那么,我们马上就明白pb的写法了,pb其实就是一个指向一维数组的指针,
因此等价于一个二维数组,因为它等价于二维数组,自然可以写pb[1][1]啦。所以,
后面那个printf输出的值仍然是6 6。请大家反复对照,认真理解。
我们总结如下:
1 N维数组的数组名完全等价于指向N-1维数组的常指针。
2 指向N-1维数组的指针完全等价于N维数组。
我们来做一个具体应用。假设我们需要动态分配一个int型二维数组,应该怎么做呢?
答案是:
然后,pt就可以如同二维数组那么使用了,比如pt[1][2]=3;。
见识到指针的威力了吗?
最后我们稍微提一下指针传参的问题。我们知道,C语言的函数传参只能是传值,
而不是传址。因此,如果要改变一个变量的值的话,唯一的方法就是将变量的地址传给函数,
这样函数就知道变量在哪儿了。哼,你不把变量本人给我?我自己找过去!这样,
函数就可以更改变量的值了。但是注意,这本质上仍然是传值,所以变量的地址是不会被更改的。
注意我说不会,而不是不能,是因为如果变量的地址被更改,不会影响到原变量。
如果要改变指针的值,可以使用指向指针的指针。如下:
{
static int a=10;
*pa=&a;
}
int main( void )
{
int *pb=NULL;
func(&pb);
return 0;
}
.
程序执行完以后,b的值被改变,转而指向静态变量a了。这里有个小窍门,
如果调用函数的时候使用的是func(&a)这样的写法,函数就可以改变a的值,
而不过不是类似这样的写法的话,就无法改变a的值,不管a是什么类型。
对于函数调用的探讨可以见我的文《对函数调用的深入探讨》,哈哈,广告做了两三遍了~~~~
看完本文,你应该对指针有了深刻的理解。不会再因为千奇百怪的指针声明而头疼了。
虽然有些知识点我没有展开来讲,但是同样很重要,大家可以在实践中去慢慢加深了解。
编程就是一个实践中认知的过程,老师怎么讲,只是一个引导作用,真正的学习还靠自身。
最后出两道题目给大家做,作为这个教程的结尾,也是为了弥补一些没有讲到的知识点。
1 请问下面的语句声明了一个什么变量呢?
.
2 请问变量b的值是多少。
{
short a;
char b,c;
};
int a=0x12345678;
int b=(int)(*(struct sm*)&a).c;
评论