`
xpp02
  • 浏览: 1012944 次
社区版块
存档分类
最新评论

指针深入探讨

 
阅读更多

C语言变量的内存实质

一、先来理解C语言中变量的实质

要理解C指针,我认为一定要理解C中“变量”的存储实质,所以我就从“变量”这个东西开始讲起吧!先来理解理解内存空间吧!请看下图:
如上图所示,内存只不过是一个存放数据的空间,就好像我的看电影时的电影院中的座位一样。电影院中的每个座位都要编号,而我们的内存要存放各种各样的数据,当然我们要知道我们的这些数据存放在什么位置吧!所以内存也要象座位一样进行编号了,这就是我们所说的内存编址。座位可以是遵循“一个座位对应一个号码”的原则,从“第1号”开始编号。而内存则是按一个字节接着一个字节的次序进行编址,如上图所示。每个字节都有个编号,我们称之为内存地址。好了,我说了这么多,现在你能理解内存空间这个概念吗?

我们继续看看以下的C/C++语言变量声明:
int i;
char a;
每次我们要使用某变量时都要事先这样声明它,它其实是内存中申请了一个名为i的整型变量宽度的空间(DOS下的16位编程中其宽度为2个字节),和一个名为a的字符型变量宽度的空间(占1个字节)。

我们又如何来理解变量是如何存在的呢。当我们如下声明变量时:
int i;
char a;
内存中的映象可能如下图:
图中可看出,i在内存起始地址为6上申请了两个字节的空间(我这里假设了int的宽度为16位,不同系统中int的宽度可能是不一样的),并命名为i。a在内存地址为8上申请了一字节的空间,并命名为a。这样我们就有两个不同类型的变量了。

二、赋值给变量

再看下面赋值:
i = 30;
a = ’t’;
你当然知道个两个语句是将30存入i变量的内存空间中,将“t”字符存入a变量的内存空间中。我们可以利用这样的形象来理解啦:

三、变量在哪里?(即我想知道变量的地址)

好了,接下来我们来看看&i是什么意思?
是取i变量所在的地址编号嘛!我们可以这样读它:返回i变量的地址编号。你记住了吗?

我要在屏幕上显示变量的地址值的话,可以写如下代码:
printf("%x", &i);
以上图的内存映象为例,屏幕上显示的不是i值30,而是显示i的内存地址编号6了。当然,在你的实际操作中,i变量的地址值不会是这个数了。

这就是我所认为的作为初学者应该能够想象到的变量存储的实质了。请这样理解吧!
最后总结代码如下:
main()
{
int i = 39;
printf(“%d\n”, i); /*①*/
printf(“%d\n”, &i); /*②*/
return(0);
}
现在你可知道①、②两个printf分别在屏幕上输出的是i的什么东西啊?

好啦!下面我们就开始真正进入指针的学习了。


指针,想说弄懂你不容易啊!我们许多初学指针的人都要这样感慨。我常常在思索它,为什么呢?其实生活中处处都有指针,我们也处处在使用它。有了它我们的生活才更加方便了。没有指针,那生活才不方便。不信?你看下面的例子。

这是一个生活中的例子:比如说你要我借给你一本书,我到了你宿舍,但是你人不在宿舍,于是我把书放在你的2层3号的书架上,并写了一张纸条放在你的桌上。纸条上写着:你要的书在第2层3号的书架上。当你回来时,看到这张纸条,你就知道了我借与你的书放在哪了。你想想看,这张纸条的作用,纸条本身不是书,它上面也没有放着书。那么你又如何知道书的位置呢?因为纸条上写着书的位置嘛!其实这张纸条就是一个指针了。它上面的内容不是书本身,而是书的地址,你通过纸条这个指针找到了我借给你的这本书。

那么我们C/C++中的指针又是什么呢?请继续跟我来吧,下面看一条声明一个指向整型变量的指针的语句:
int *pi;
pi是一个指针,当然我们知道啦,但是这样说,你就以为pi一定是个多么特别的东西了。其实,它也只过是一个变量而已。与上一篇中说的变量并没有实质的区别。不信你看下面图:

说明:这里我假设了指针只占2个字节宽度,实际上在32位系统中,指针的宽度是4个字节宽的,即32位。
由图示中可以看出,我们使用“int *pi”声明指针变量—— 其实是在内存的某处声明一个一定宽度的内存空间,并把它命名为pi。你能在图中看出pi与前面的i、a 变量有什么本质区别吗?没有,当然没有!pi也只不过是一个变量而已嘛!那么它又为什么会被称为“指针”?关键是我们要让这个变量所存储的内容是什么。现在我要让pi成为具有真正“指针”意义的变量。请接着看下面语句:
pi = &i;
你应该知道&i是什么意思吧!再次提醒你啦:这是返回i变量的地址编号。整句的意思就是把i地址的编号赋值给pi,也就是你在pi里面写上i的地址编号。结果如下图所示:
你看,执行完pi=&i后,在图示中的内存中,pi的值是6。这个6就是i变量的地址编号,这样pi就指向了变量i了。你看,pi与那张纸条有什么区别?pi不就是那张纸条嘛!上面写着i的地址,而i就是那个本书。你现在看懂了吗?因此,我们就把pi称为指针。所以你要记住,指针变量所存的内容就是内存的地址编号!好了,现在我们就可以通过这个指针pi来访问到i这个变量了,不是吗?看下面语句:
printf("%d", *pi);
那么*pi什么意思呢?你只要这样读它:pi的内容所指的地址的内容(嘻嘻,看上去好像在绕口令了),就是pi这张“纸条”上所写的位置上的那本 “书”—— i 。你看,Pi的内容是6,也就是说pi指向内存编号为6的地址。*pi嘛,就是它所指地址的内容,即地址编号6上的内容了,当然就是30这个“值”了。所以这条语句会在屏幕上显示30。也就是说printf("%d", *pi)等价于printf("%d", i) ,请结合上图好好体会吧!各位还有什么疑问?

到此为止,你掌握了类似&i、*pi写法的含义和相关操作吗?总的一句话,我们的纸条就是我们的指针,同样我们的pi也就是我们的纸条!剩下的就是我们如何应用这张纸条了。最后我给你一道题:程序如下。
char a,*pa;
a = 10;
pa = &a;
*pa = 20;
printf("%d", a);
你能直接看出输出的结果是什么吗?如果你能,我想本篇的目的就达到了。好了,就说到这了。Happy Study! 在下篇中我将谈谈“指针的指针”即对
int **ppa;
中ppa的理解。

一、通过数组名访问数组元素

看下面代码:
int i, a[] = {3,4,5,6,7,3,7,4,4,6};
for (i = 0; i <= 9; i++)
{
printf("%d\n", a[i]);
}
很显然,它是显示a 数组的各元素值。我们还可以这样访问元素,如下:
int i, a[] = {3,4,5,6,7,3,7,4,4,6};
for (i = 0; i <= 9; i++)
{
printf("%d\n", *(a+i));
}
它的结果和作用完全一样。

二、通过指针访问数组元素

int i, *pa, a[] = {3,4,5,6,7,3,7,4,4,6};
pa = a; /*请注意数组名a直接赋值给指针pa*/
for (i = 0; i <= 9; i++)
{
printf("%d\n", pa[i]);
}
很显然,它也是显示a 数组的各元素值。另外与数组名一样也可如下:
int i, *pa, a[] = {3,4,5,6,7,3,7,4,4,6};
pa = a;
for (i = 0; i <= 9; i++)
{
printf("%d\n", *(pa+i));
}
看pa = a,即数组名赋值给指针,以及通过数组名、指针对元素的访问形式看,它们并没有什么区别,从这里可以看出:数组名其实也就是指针。难道它们没有任何区别?有,请继续。

三、数组名与指针变量的区别

请看下面的代码:
int i, *pa, a[] = {3,4,5,6,7,3,7,4,4,6};
pa = a;
for (i = 0; i <= 9; i++)
{
printf("%d\n", *pa);
pa++; /*注意这里,指针值被修改*/
}
可以看出,这段代码也是将数组各元素值输出。不过,你把循环体{}中的pa改成a试试。你会发现程序编译出错,不能成功。看来指针和数组名还是不同的。其实上面的指针是指针变量,而数组名只是一个指针常量。这个代码与上面的代码不同的是,指针pa在整个循环中,其值是不断递增的,即指针值被修改了。数组名是指针常量,其值是不能修改的,因此不能类似这样操作:a++。

前面4、5节中pa[i],*(pa+i)处,指针pa的值是使终没有改变。所以变量指针pa与数组名a可以互换。

四、声明指针常量

再请看下面的代码:
int i, a[] = {3,4,5,6,7,3,7,4,4,6};
int *const pa = a; /* 注意const的位置:不是const int *pa */
for (i = 0; i <= 9; i++)
{
printf("%d\n", *pa);
pa++ ; /*注意这里,指针值被修改*/
}
这时候的代码能成功编译吗?不能。因为pa指针被定义为常量指针了。这时与数组名a已经没有不同。这更说明了数组名就是常量指针。但是……
int *const a = {3,4,5,6,7,3,7,4,4,6}; /*不行*/
int a[]={3,4,5,6,7,3,7,4,4,6}; /*可以,所以初始化数组时必定要这样。*/
以上都是在VC6.0上实验。


一、从const int i 说起

你知道我们声明一个变量时象这样int i ;这个i是可能在它处重新变赋值的。如下:
int i = 0;
/* . . . */
i = 20; /*这里重新赋值了*/
不过有一天我的程序可能需要这样一个变量(暂且称它变量),在声明时就赋一个初始值。之后我的程序在其它任何处都不会再去重新对它赋值。那我又应该怎么办呢?用const 。
/* . . . */
const int ic =20;
/* . . . */
ic = 40; /*这样是不可以的,编译时是无法通过,因为我们不能对const 修饰的ic重新赋值的。*/
/*这样我们的程序就会更早更容易发现问题了。*/
/* . . . */
有了const修饰的ic 我们不称它为变量,而称符号常量,代表着20这个数。这就是const 的作用。ic是不能在它处重新赋新值了。

认识了const 作用之后,另外,我们还要知道格式的写法。有两种:
const int ic = 20;

int const ic = 20;
它们是完全相同的。这一点我们是要清楚。总之,你务必要记住const 与int哪个写前都不影响语义。有了这个概念后,我们来看这两个家伙:
const int *pi

int const *pi
按你的逻辑看,它们的语义有不同吗?呵呵,你只要记住一点:int 与const 哪个放前哪个放后都是一样的,就好比const int ic;与int const ic;一样。也就是说,它们是相同的。

好了,我们现在已经搞定一个“双包胎”的问题。那么
int *const pi;
与前两个语句又有什么不同呢?我下面就来具体分析它们的格式与语义吧!

二、const int *pi的语义

我先来说说const int *pi是什么作用 (当然int const *pi也是一样的,前面我们说过,它们实际是一样的)。看下面的例子:
/* 代码开始 */
int i1 = 30;
int i2 = 40;
const int *pi = &i1;
pi = &i2; /* 注意这里,pi可以在任意时候重新赋值一个新内存地址*/
i2 = 80; /* 想想看:这里能用*pi = 80来代替吗?当然不能!*/
printf("%d\n", *pi); /* 输出是80 */
/* 代码结束 */

语义分析:
看出来了没有啊,pi的值是可以被修改的。即它可以重新指向另一个地址的,但是,不能通过*pi来修改i2的值。这个规则符合我们前面所讲的逻辑吗?当然符合了!

首先const 修饰的是整个*pi(注意,我写的是*pi而不是pi)。所以*pi是常量,是不能被赋值的(虽然pi所指的i2是变量,不是常量)。
其次,pi前并没有用const 修饰,所以pi是指针变量,能被赋值重新指向另一内存地址的。你可能会疑问:那我又如何用const 来修饰pi呢?其实,你注意到int *const pi中const 的位置就大概可以明白了。请记住,通过格式看语义。哈哈,你可能已经看出了规律吧?那下面的一节也就没必要看下去了。不过我还得继续我的战斗!

三、再看int *const pi

确实,int *const pi与前面的int const *pi会很容易给混淆的。注意:前面一句的const 是写在pi前和*号后的,而不是写在*pi前的。很显然,它是修饰限定pi的。我先让你看例子:
/* 代码开始 */
int i1 = 30;
int i2 = 40;
int *const pi = &i1;
/* pi = &i2; 注意这里,pi不能再这样重新赋值了,即不能再指向另一个新地址。(第4行的注释)*/
/* 所以我已经注释了它。*/
i1 = 80; /* 想想看:这里能用 *pi = 80; 来代替吗?可以,这里可以通过*pi修改i1的值。(第5行的注释)*/
/* 请自行与前面一个例子比较。 */
printf("%d", *pi); /* 输出是80 */
/* 代码结束 */
语义分析:
看了这段代码,你明白了什么?有没有发现pi值是不能重新赋值修改了。它只能永远指向初始化时的内存地址了。相反,这次你可以通过*pi来修改i1的值了。与前一个例子对照一下吧!看以下的两点分析:
1)pi因为有了const 的修饰,所以只是一个指针常量:也就是说pi值是不可修改的(即pi不可以重新指向i2这个变量了)(请看第4行的注释)。
2)整个*pi的前面没有const 的修饰。也就是说,*pi是变量而不是常量,所以我们可以通过*pi来修改它所指内存i1的值(请看第5行的注释)。

总之一句话,这次的pi是一个指向int变量类型数据的指针常量。

我最后总结两句:
1) 如果const 修饰在*pi前,则不能改的是*pi(即不能类似这样:*pi=50;赋值)而不是指pi。
2) 如果const 是直接写在pi前,则pi不能改(即不能类似这样:pi=&i;赋值)。

请你务必先记住这两点,相信你一定不会再被它们给搞糊了。现在再看这两个声明语句int const *pi和int *const pi时,呵呵,你会头昏脑胀还是很轻松惬意?它们各自声明的pi分别能修改什么,不能修改什么?再问问自己,把你的理解告诉我吧,可以发帖也可以发到我的邮箱(我的邮箱yyf977@163.com)!我一定会答复的。

四、补充三种情况

这里,我再补充以下三种情况。其实只要上面的语义搞清楚了,这三种情况也就已经被包含了。不过作为三种具体的形式,我还是简单提一下吧!
情况一:int *pi指针指向const int i常量的情况
/* begin */
const int i1 = 40;
int *pi;
pi = &i1; /* 这样可以吗?不行,VC下是编译错。*/
/* const int 类型的i1的地址是不能赋值给指向int 类型地址的指针pi的。否则pi岂不是能修改i1的值了吗!*/
pi = (int *) &i1; /* 这样可以吗?强制类型转换可是C所支持的。*/
/* VC下编译通过,但是仍不能通过 *pi = 80来修改i1的值。去试试吧!看看具体的怎样。*/
/* end */

情况二:const int *pi指针指向const int i1的情况
/* begin */
const int i1=40;
const int * pi;
pi=&i1;/* 两个类型相同,可以这样赋值。很显然,i1的值无论是通过pi还是i1都不能修改的。 */
/* end */

情况三:用const int *const pi声明的指针
/* begin */
int i;
const int * const pi=&i; /*你能想象pi能够作什么操作吗?pi值不能改,也不能通过pi修改i的值。因为不管是*pi还是pi都是const的。 */
/* end */之前,我先请你做三道题目。(嘿嘿,得先把你的头脑搞昏才行……唉呀,谁扔我鸡蛋?)

考题一,程序代码如下:
void Exchg1(int x, int y)
{
int tmp;
tmp = x;
x = y;
y = tmp;
printf("x = %d, y = %d\n", x, y);
}
main()
{
int a = 4,b = 6;
Exchg1(a, b);
printf("a = %d, b = %d\n", a, b);
return(0);
}
输出的结果为:
x = ____, y=____.
a = ____, b=____.
问下划线的部分应是什么,请完成。

考题二,程序代码如下:
void Exchg2(int *px, int *py)
{
int tmp = *px;
*px = *py;
*py = tmp;
printf("*px = %d, *py = %d.\n", *px, *py);
}
main()
{
int a = 4;
int b = 6;
Exchg2(&a, &b);
printf("a = %d, b = %d.\n", a, b);
return(0);
}
输出的结果为为:
*px=____, *py=____.
a=____, b=____.
问下划线的部分应是什么,请完成。

考题三,程序代码如下:
void Exchg3(int &x, int &y)
{
int tmp = x;
x = y;
y = tmp;
printf("x = %d,y = %d\n", x, y);
}
main()
{
int a = 4;
int b = 6;
Exchg3(a, b);
printf("a = %d, b = %d\n", a, b);
return(0);
}
输出的结果为:
x=____, y=____.
a=____, b=____.
问下划线的部分应是什么,请完成。你不在机子上试,能作出来吗?你对你写出的答案有多大的把握?正确的答案,想知道吗?(呵呵,让我慢慢地告诉你吧!)

好,废话少说,继续我们的探索之旅了。
我们都知道:C语言中函数参数的传递有:值传递、地址传递、引用传递这三种形式。题一为值传递,题二为地址传递,题三为引用传递。不过,正是这几种参数传递的形式,曾把我给搞得晕头转向。我相信也有很多人与我有同感吧?

下面请让我逐个地谈谈这三种传递形式。

二、函数参数传递方式之一:值传递

(1)值传递的一个错误认识
先看考题一中Exchg1函数的定义:
void Exchg1(int x, int y) /* 定义中的x,y变量被称为Exchg1函数的形式参数 */
{
int tmp;
tmp = x;
x = y;
y = tmp;
printf("x = %d, y = %d.\n", x, y);
}
问:你认为这个函数是在做什么呀?
答:好像是对参数x、y的值对调吧?
请往下看,我想利用这个函数来完成对a,b两个变量值的对调,程序如下:
main()
{
int a = 4,b = 6;
Exchg1(a, b); /*a,b变量为Exchg1函数的实际参数。*/
printf("a = %d, b = %d.\n”, a, b);
return(0);
}
我问:Exchg1()里头的printf("x = %d, y = %d.\n", x, y);语句会输出什么啊?我再问:Exchg1()后的printf("a = %d, b = %d.\n”, a, b);语句输出的是什么?
程序输出的结果是:
x = 6, y = 4.
a = 4, b = 6.
为什么不是a = 6,b = 4呢?奇怪,明明我把a、b分别代入了x、y中,并在函数里完成了两个变量值的交换,为什么a、b变量值还是没有交换(仍然是a = 4、b = 6,而不是a = 6、b = 4)?如果你也会有这个疑问,那是因为你根本就不知实参a、b与形参x、y的关系了。

(2)一个预备的常识
为了说明这个问题,我先给出一个代码:
int a = 4;
int x;
x = a;
x = x + 3;
看好了没,现在我问你:最终a值是多少,x值是多少?
(怎么搞的,给我这个小儿科的问题。还不简单,不就是a = 4、x = 7嘛!)
在这个代码中,你要明白一个东西:虽然a值赋给了x,但是a变量并不是x变量哦。我们对x任何的修改,都不会改变a变量。呵呵!虽然简单,并且一看就理所当然,不过可是一个很重要的认识喔。

(3)理解值传递的形式
看调用Exch1函数的代码:
main()
{
int a = 4,b = 6;
Exchg1(a, b) /* 这里调用了Exchg1函数 */
printf("a = %d, b = %d.\n", a, b);
}
Exchg1(a, b)时所完成的操作代码如下所示。
int x = a; /* ← */
int y = b; /* ← 注意这里,头两行是调用函数时的隐含操作 */
int tmp;
tmp = x;
x = y;
y = tmp;
请注意在调用执行Exchg1函数的操作中我人为地加上了头两句:
int x = a;
int y = b;
这是调用函数时的两个隐含动作。它确实存在,现在我只不过把它显式地写了出来而已。问题一下就清晰起来啦。(看到这里,现在你认为函数里面交换操作的是a、b变量或者只是x、y变量呢?)

原来,其实函数在调用时是隐含地把实参a、b 的值分别赋值给了x、y,之后在你写的Exchg1函数体内再也没有对a、b进行任何的操作了。交换的只是x、y变量。并不是a、b。当然a、b的值没有改变啦!函数只是把a、b的值通过赋值传递给了x、y,函数里头操作的只是x、y的值并不是a、b的值。这就是所谓的参数的值传递了。

哈哈,终于明白了,正是因为它隐含了那两个的赋值操作,才让我们产生了前述的迷惑(以为a、b已经代替了x、y,对x、y的操作就是对a、b的操作了,这是一个错误的观点啊!)。

三、函数参数传递方式之二:地址传递

继续!地址传递的问题!
看考题二的代码:
void Exchg2(int *px, int *py)
{
int tmp = *px;
*px = *py;
*py = tmp;
printf("*px = %d, *py = %d.\n", *px, *py);
}
main()
{
int a = 4;
int b = 6;
Exchg2(&a, &b);
printf("a = %d, b = %d.\n”, a, b);
return(0);
}
它的输出结果是:
*px = 6, *py = 4.
a = 6, b = 4.
看函数的接口部分:Exchg2(int *px, int *py),请注意:参数px、py都是指针。再看调用处:Exchg2(&a, &b);
它将a的地址(&a)代入到px,b的地址(&b)代入到py。同上面的值传递一样,函数调用时作了两个隐含的操作:将&a,&b的值赋值给了px、py。
px = &a;
py = &b;
呵呵!我们发现,其实它与值传递并没有什么不同,只不过这里是将a、b的地址值传递给了px、py,而不是传递的a、b的内容,而(请好好地在比较比较啦)整个Exchg2函数调用是如下执行的:
px = &a; /* ← */
py = &b; /* ← 请注意这两行,它是调用Exchg2的隐含动作。*/
int tmp = *px;
*px = *py;
*py = tmp;
printf("*px =%d, *py = %d.\n", *px, *py);
这样,有了头两行的隐含赋值操作。我们现在已经可以看出,指针px、py的值已经分别是a、b变量的地址值了。接下来,对*px、*py的操作当然也就是对a、b变量本身的操作了。所以函数里头的交换就是对a、b值的交换了,这就是所谓的地址传递(传递a、b的地址给了px、py),你现在明白了吗?

四、函数参数传递方式之三:引用传递

看题三的代码:
void Exchg3(int &x, int &y) /* 注意定义处的形式参数的格式与值传递不同 */
{
int tmp = x;x = y;
y = tmp;
printf("x = %d, y = %d.\n", x, y);
}
main()
{
int a = 4;
int b = 6;
Exchg3(a, b); /*注意:这里调用方式与值传递一样*/
printf("a = %d, b = %d.\n”, a, b);
}
输出结果:
x = 6, y = 4.
a = 6, b = 4. /*这个输出结果与值传递不同。*/
看到没有,与值传递相比,代码格式上只有一处是不同的,即在定义处:
Exchg3(int &x, int &y)
但是我们发现a与b的值发生了对调。这说明了Exchg3(a, b)里头修改的是a、b变量,而不只是修改x、y了。

我们先看Exchg3函数的定义处Exchg3(int &x, int &y)。参数x、y是int的变量,调用时我们可以像值传递(如: Exchg1(a, b); )一样调用函数(如: Exchg3(a, b);)。但是x、y前都有一个取地址符号“&”。有了这个,调用Exchg3时函数会将a、b 分别代替了x、y了,我们称:x、y分别引用了a、b变量。这样函数里头操作的其实就是实参a、b本身了,也就是说函数里是可以直接修改到a、b的值了。

最后对值传递与引用传递作一个比较:
1)在函数定义格式上有不同:
值传递在定义处是:Exchg1(int x, int y);
引用传递在这义处是:Exchg3(int &x, int &y);

2)调用时有相同的格式:
值传递:Exchg1(a, b);
引用传递:Exchg3(a, b);

3)功能上是不同的:
值传递的函数里操作的不是a、b变量本身,只是将a、b值赋给了x、y。函数里操作的只是x、y变量而不是a、b,显示a、b的值不会被Exchg1函数所修改。
引用传递Exchg3(a, b)函数里是用a、b分别代替了x、y。函数里操作的就是a、b变量的本身,因此a、b的值可在函数里被修改的。


一、回顾指针概念

早在本书第贰篇中我就对指针的实质进行了阐述。今天我们又要学习一个叫做“指向另一指针地址”的指针。让我们先回顾一下指针的概念吧!
当我们程序如下声明变量:
short int i;
char a;
short int * pi;
程序会在内存某地址空间上为各变量开辟空间,如下图所示:
图中所示中可看出:
i 变量在内存地址5的位置,占2个字节。
a变量在内存地址7的位置,占1个字节。
pi变量在内存地址9的位置,占2个字节。(注:pi 是指针,我这里指针的宽度只有2个字节,32位系统是4个字节)
接下来如下赋值:
i = 50;
pi = &i;
经过上在两句的赋值,变量的内存映象如下:
看到没有:短整型指针变量pi的值为5,它就是i变量的内存起始地址。所以,这时当我们对*pi进行读写操作时,其实就是对i变量的读写操作。如:
*pi=5; /* 就是等价于i = 5; */
你可以回看本书的第贰篇,那里有更加详细的解说。

二、指针的地址与指向另一指针地址的指针

在上一节中,我们看到,指针变量本身与其它变量一样也是在某个内存地址中的,如pi的内存起始地址是9。同样的,我们也可能让某个指针指向这个地址。看下面代码:
short int **ppi; /* 这是一个指向指针的指针,注意有两个“*”号 */
*ppi = &pi;
第一句:short int **ppi; —— 声明了一个指针变量ppi,这个ppi是用来存储(或称指向)一个short int * 类型指针变量的地址。
第二句:&pi那就是取pi的地址,**ppi = &pi就是把pi的地址赋给了ppi。即将地址值9赋值给ppi。如下图:
从图中看出,指针变量ppi的内容就是指针变量pi的起始地址。于是……
ppi的值是多少呢?—— 9。
*ppi的值是多少呢?—— 5,即pi的值。
**ppi的值是多少呢?——50,即i的值,也是*pi的值。
呵呵!不用我说太多了,我相信你应明白这种指针了吧!

三、一个应用实例

(1)设计一个函数:void find1(char array[], char search, char *pa)
要求:这个函数参数中的数组array是以0值为结束的字符串,要求在字符串array中查找字符是参数search里的字符。如果找到,函数通过第三个参数(pa)返回值为array字符串中第一个找到的字符的地址。如果没找到,则为pa为0。

设计:依题意,实现代码如下。
void find1(char array[], char search, char *pa)
{
int i;
for (i = 0; *(array + i) != 0; i++)
{
if ( *(array+i) == search)
{
pa = array + i;
break;
}
else if (*(array+i) == 0)
{
pa = 0;
break;
}
}
}
你觉得这个函数能实现所要求的功能吗?

调试:我下面调用这个函数试试。
main()
{
char str[] = {"afsdfsdfdf\0"}; /* 待查找的字符串 */
char a = ’d’; /* 设置要查找的字符 */
char *p = 0; /* 如果查找到后指针p将指向字符串中查找到的第1个字符的地址。 */
find1(str, a, p); /* 调用函数以实现所要操作。 */
if (0 == p)
{
printf("没找到!\n"); /* 如果没找到则输出此句 */
}
else
{
printf("找到了,p = %d", p); /* 如果找到则输出此句 */
}
return(0);
}
分析:上面代码,你认为会是输出什么呢?运行试试。
唉!怎么输出的是:没有找到!而不是“找到了,……”。

明明a值为’d’,而str字符串的第四个字符是’d’,应该找得到呀!
再看函数定义处:void find1(char array[], char search, char *pa)
看调用处:find1(str, a, p);

依我在第伍篇的分析方法,函数调用时会对每一个参数进行一个隐含的赋值操作。整个调用如下:
array = str;
search = a;
pa = p; /* 请注意:以上三句是调用时隐含的动作。*/
int i;
for(i =0; *(array+i) != 0; i++)
{
if (*(array+i) == search)
{
pa = array + i;
break;
}
else if (*(array+i)==0)
{
pa=0;
break;
}
}
哦!参数pa与参数search的传递并没有什么不同,都是值传递嘛(小语:地址传递其实就是地址值传递嘛)!所以对形参变量pa值(当然值是一个地址值)的修改并不会改变实参变量p值,因此p的值并没有改变(即p的指向并没有被改变)。(如果还有疑问,再看一看《第五篇:函数参数的传递》了。)

修正:
void find2(char array[], char search, char **ppa)
{
int i;
for (i=0; *(array + i) != 0; i++)
{
if(*(array + i) == search)
{
*ppa = array + i;
break;
}
else if(*(array + i) == 0)
{
*ppa = 0;
break;
}
}
}
主函数的调用处改如下:
find2(str, a, &p); /*调用函数以实现所要操作。*/
再分析:这样调用函数时的整个操作变成如下:
array = str;
search = a;
ppa = &p; /* 请注意:以上三句是调用时隐含的动作。 */
int i;
for (i = 0; *(array + i) != 0; i++)
{
if (*(array + i) == search)
{
*ppa = array + i
break;
}
else if (*(array+i)==0)
{
*ppa=0;
break;
}
}
看明白了吗?ppa指向指针p的地址。对*ppa的修改就是对p值的修改。你自行去调试。

经过修改后的程序就可以完成所要的功能了。看懂了这个例子,也就达到了本篇所要求的目的。

二、通常的函数调用

一个通常的函数调用的例子:
/* 自行包含头文件 */
void MyFun(int x); /* 此处的声明也可写成:void MyFun(int) */
int main(int argc, char* argv[])
{
MyFun(10); /* 这里是调用MyFun(10) 函数 */
return(0);
}
void MyFun(int x) /* 这里定义一个MyFun函数 */
{
printf("%d\n",x);
}
这个MyFun函数是一个无返回值的函数,它并不“完成”什么事情。这种调用函数的格式你应该是很熟悉的吧!看主函数中调用MyFun函数的书写格式:
MyFun(10);
我们一开始只是从功能上或者说从数学意义上理解MyFun这个函数,知道MyFun函数名代表的是一个功能(或是说一段代码)。直到——学习到函数指针概念时。我才不得不在思考:函数名到底又是什么东西呢?

(不要以为这是没有什么意义的事噢!呵呵,继续往下看你就知道了。)

二、函数指针变量的声明

就象某一数据变量的内存地址可以存储在相应的指针变量中一样,函数的首地址也以存储在某个函数指针变量里的。这样,我就可以通过这个函数指针变量来调用所指向的函数了。
在C系列语言中,任何一个变量,总是要先声明,之后才能使用的。那么,函数指针变量也应该要先声明吧?那又是如何来声明呢?以上面的例子为例,我来声明一个可以指向MyFun函数的函数指针变量FunP。下面就是声明FunP变量的方法:
void (*FunP)(int) ; /* 也可写成void (*FunP)(int x)*/
你看,整个函数指针变量的声明格式如同函数MyFun的声明处一样,只不过——我们把MyFun改成“(*FunP)”而已,这样就有了一个能指向MyFun函数的指针FunP了。(当然,这个FunP指针变量也可以指向所有其它具有相同参数及返回值的函数了。)

三、通过函数指针变量调用函数

有了FunP指针变量后,我们就可以对它赋值指向MyFun,然后通过FunP来调用MyFun函数了。看我如何通过FunP指针变量来调用MyFun函数的:
/* 自行包含头文件 */
void MyFun(int x); /* 这个声明也可写成:void MyFun( int )*/
void (*FunP)(int ); /*也可声明成void(*FunP)(int x),但习惯上一般不这样。 */
int main(int argc, char* argv[])
{
MyFun(10); /* 这是直接调用MyFun函数 */
FunP = &MyFun; /* 将MyFun函数的地址赋给FunP变量 */
(*FunP)(20); /* (★)这是通过函数指针变量FunP来调用MyFun函数的。 */
}
void MyFun(int x) /* 这里定义一个MyFun函数 */
{
printf("%d\n",x);
}
请看(★)行的代码及注释。运行看看。嗯,不错,程序运行得很好。哦,我的感觉是:MyFun与FunP的类型关系类似于int 与int * 的关系。函数MyFun好像是一个如int的变量(或常量),而FunP则像一个如int * 一样的指针变量。
int i,*pi;
pi = &i; /* 与FunP = &MyFun比较。*/
(你的感觉呢?)呵呵,其实不然……

四、调用函数的其它书写格式

函数指针也可如下使用,来完成同样的事情:
/* 自行包含头文件 */
void MyFun(int x);
void (*FunP)(int );/* 声明一个用以指向同样参数,返回值函数的指针变量。 */
int main(int argc, char* argv[])
{
MyFun(10); /* 这里是调用MyFun(10)函数 */
FunP = MyFun; /* 将MyFun函数的地址赋给FunP变量 */
FunP(20); /* (★)这是通过函数指针变量来调用MyFun函数的。*/
return 0;
}
void MyFun(int x) //这里定义一个MyFun函数
{
printf("%d\n",x);
}
我改了(★)行(请自行与之前的代码比较一下)。运行试试,啊!一样地成功。咦?
FunP = MyFun;
可以这样将MyFun值同赋值给FunP,难道MyFun与FunP是同一数据类型(即如同的int 与int的关系),而不是如同int 与int*的关系了?(有没有一点点的糊涂了?)看来与之前的代码有点矛盾了,是吧!所以我说嘛!

请容许我暂不给你解释,继续看以下几种情况(这些可都是可以正确运行的代码哟!):
代码之三:
int main(int argc, char* argv[])
{
MyFun(10); /* 这里是调用MyFun(10)函数 */
FunP = &MyFun; /* 将MyFun函数的地址赋给FunP变量 */
FunP(20); /* 这是通过函数指针变量来调用MyFun函数的。 */
return 0;
}

代码之四:
int main(int argc, char* argv[])
{
MyFun(10); /* 这里是调用MyFun(10)函数 */
FunP = MyFun; /* 将MyFun函数的地址赋给FunP变量 */
(*FunP)(20); /*这是通过函数指针变量来调用MyFun函数的。*/
return 0;
}
真的是可以这样的噢!(哇!真是要晕倒了!)还有呐!看——
int main(int argc, char* argv[])
{
(*MyFun)(10); /*看,函数名MyFun也可以有这样的调用格式*/
return 0;
}
你也许第一次见到吧:函数名调用也可以是这样写的啊!(只不过我们平常没有这样书写罢了。)那么,这些又说明了什么呢?

呵呵!依据以往的知识和经验来推理本篇的“新发现”,我想就连“福尔摩斯”也必定会由此分析并推断出以下的结论:
1)其实,MyFun的函数名与FunP函数指针都是一样的,即都是函数指针。MyFun函数名是一个函数指针常量,而FunP是一个函数数指针变量,这是它们的关系。
2)但函数名调用如果都得如(*MyFun)(10)这样,那书写与读起来都是不方便和不习惯的。所以C语言的设计者们才会设计成又可允许MyFun(10)这种形式地调用(这样方便多了并与数学中的函数形式一样,不是吗?)。
3)为统一起见,FunP函数指针变量也可以FunP(10)的形式来调用。
4)赋值时,即可FunP = &MyFun形式,也可FunP = MyFun。


上述代码的写法,随便你爱怎么着!请这样理解吧!这可是有助于你对函数指针的应用喽!最后 ——

补充说明一点,在函数的声明处:
void MyFun(int); /*不能写成void (*MyFun)(int)。*/
void (*FunP)(int); /*不能写成void FunP(int)。*/
(请看注释)这一点是要注意的。

五、定义某一函数的指针类型

就像自定义数据类型一样,我们也可以先定义一个函数指针类型,然后再用这个类型来声明函数指针变量。
我先给你一个自定义数据类型的例子。
typedef int* PINT; /* 为int* 类型定义了一个PINT的别名*/
int main()
{
int x;
PINT px = &x; /* 与“int *px=&x;”是等价的。PINT类型其实就是int * 类型 */
*px = 10; /* px就是int*类型的变量 */
return 0;
}
根据注释,应该不难看懂吧!(虽然你可能很少这样定义使用,但以后学习Win32编程时会经常见到的。)下面我们来看一下函数指针类型的定义及使用:(请与上对照!)
/* 自行包含头文件 */
void MyFun(int x); /*此处的声明也可写成:void MyFun( int )*/
typedef void (*FunType)(int); /*(★)这样只是定义一个函数指针类型*/
FunType FunP; /*然后用FunType类型来声明全局FunP变量*/
int main(int argc, char* argv[])
{
FunType FunP; /*函数指针变量当然也是可以是局部的 ,那就请在这里声明了。 */
MyFun(10);
FunP = &MyFun;
return 0;
}
void MyFun(int x)
{
printf("%d\n",x);
}
看(★)行:
首先,在void (*FunType)(int)前加了一个typedef 。这样只是定义一个名为FunType函数指针类型,而不是一个FunType变量。
然后,“FunType FunP;”这句就如“PINT px;”一样地声明一个FunP变量。


其它相同。整个程序完成了相同的事。这样做法的好处是:
有了FunType类型后,我们就可以同样地、很方便地用FunType类型来声明多个同类型的函数指针变量了。如下:
FunType FunP2;
FunType FunP3;
/* . . . */

六、函数指针作为某个函数的参数

既然函数指针变量是一个变量,当然也可以作为某个函数的参数来使用的。所以,你还应知道函数指针是如何作为某个函数的参数来传递使用的。

给你一个实例:
要求:我要设计一个CallMyFun函数,这个函数可以通过参数中的函数指针值不同来分别调用MyFun1、MyFun2、MyFun3这三个函数(注:这三个函数的定义格式应相同)。
实现:代码如下:
/* 自行包含头文件 */
void MyFun1(int x);
void MyFun2(int x);
void MyFun3(int x);
typedef void (*FunType)(int ); /* ②. 定义一个函数指针类型FunType,与①函数类型一致 */
void CallMyFun(FunType fp,int x);
int main(int argc, char* argv[])
{
CallMyFun(MyFun1,10); /* ⑤. 通过CallMyFun函数分别调用三个不同的函数 */
CallMyFun(MyFun2,20);
CallMyFun(MyFun3,30);
}
void CallMyFun(FunType fp,int x) /* ③. 参数fp的类型是FunType。*/
{
fp(x);/* ④. 通过fp的指针执行传递进来的函数,注意fp所指的函数是有一个参数的。 */
}
void MyFun1(int x) /* ①. 这是个有一个参数的函数,以下两个函数也相同。 */
{
printf("函数MyFun1中输出:%d\n",x);
}
void MyFun2(int x)
{
printf("函数MyFun2中输出:%d\n",x);
}
void MyFun3(int x)
{
printf("函数MyFun3中输出:%d\n",x);
}
输出结果:略分析:看我写的注释。你可按我注释的①②③④⑤顺序自行分析。指针为C语言编程提供了强大的支持——如果你能正确而灵活地利用指针,你就可以直接切入问题的核心,或者将程序分割成一个个片断。一个很好地利用了指针的程序会非常高效、简洁和精致。

利用指针你可以将数据写入内存中的任意位置,但是,一旦你的程序中有一个野指针("wild”pointer),即指向一个错误位置的指针,你的数据就危险了——存放在堆中的数据可能会被破坏,用来管理堆的数据结构也可能会被破坏,甚至操作系统的数据也可能会被修改,有时,上述三种破坏情况会同时发生。此后可能发生的事情取决于这样两点:
第一,内存中的数据被破坏的程度有多大;
第二,内存中的被破坏的部分还要被使用多少次。

在有些情况下,一些函数(可能是内存分配函数、自定义函数或标准库函数)将立即(也可能稍晚一点)无法正常工作。在另外一些情况下,程序可能会终止运行并报告一条出错消息;或者程序可能会挂起;或者程序可能会陷入死循环;或者程序可能会产生错误的结果;或者程序看上去仍在正常运行,因为程序没有遭到本质的破坏。

值得注意的是,即使程序中已经发生了根本性的错误,程序有可能还会运行很长一段时间,然后才有明显的失常表现;或者,在调试时,程序的运行完全正常,只有在用户使用时,它才会失常。

在C语言程序中,任何野指针或越界的数组下标(out-of-bounds array subscript)都可能使系统崩溃。两次释放内存的操作也会导致这种结果。你可能见过一些C程序员编写的程序中有严重的错误,现在你能知道其中的部分原因了。

有些内存分配工具能帮助你发现内存分配中存在的问题,例如漏洞(leak,见7.21),两次释放一个指针,野指针,越界下标,等等。但这些工具都是不通用的,它们只能在特定的操作系统中使用,甚至只能在特定版本的编译程序中使用。如果你找到了这样一种工具,最好试试看能不能用,因为它能为你节省许多时间,并能提高你的软件的质量。

指针的算术运算是C语言(以及它的衍生体,例如C++)独有的功能。汇编语言允许你对地址进行运算,但这种运算不涉及数据类型。大多数高级语言根本就不允许你对指针进行任何操作,你只能看一看指针指向哪里。

C指针的算术运算类似于街道地址的运算。假设你生活在一个城市中,那里的每一个街区的所有街道都有地址。街道的一侧用连续的偶数作为地址,另一侧用连续的奇数作为地址。如果你想知道River Rd.街道158号北边第5家的地址,你不会把158和5相加,去找163号;你会先将5(你要往前数5家)乘以2(每家之间的地址间距),再和158相加,去找River Rd.街道的168号。同样,如果一个指针指向地址158(十进制数)中的一个两字节短整型值,将该指针加3=5,结

果将是一个指向地址168(十进制数)中的短整型值的指针(见7.7和7.8中对指针加减运算的详细描述)。

街道地址的运算只能在一个特定的街区中进行,同样,指针的算术运算也只能在一个特定的数组中进行。实际上,这并不是一种限制,因为指针的算术运算只有在一个特定的数组中进行才有意义。对指针的算术运算来说,一个数组并不必须是一个数组变量,例如函数malloc()或calloc()的返回值是一个指针,它指向一个在堆中申请到的数组。

指针的说明看起来有些使人感到费解,请看下例:
char *p;
上例中的说明表示,p是一个字符。符号“*”是指针运算符,也称间接引用运算符。当程序间接引用一个指针时,实际上是引用指针所指向的数据。

在大多数计算机中,指针只有一种,但在有些计算机中,指向数据和指向函数的指针可以是不同的,或者指向字节(如char。指针和void *指针)和指向字的指针可以是不同的。这一点对sizeof运算符没有什么影响。但是,有些C程序或程序员认为任何指针都会被存为一个int型的值,或者至少会被存为一个long型的值,这就无法保证了,尤其是在IBM PC兼容机上。

注意:以下讨论与Macintosh或UNIX程序员无关。

最初的IBM PC兼容机使用的处理器无法有效地处理超过16位的指针(人们对这种结论仍有争议。16位指针是偏移量,见9.3中对基地址和偏移量的讨论)。尽管最初的IBM PC机最终也能使用20位指针,但颇费周折。因此,从一开始,基于IBM兼容机的各种各样的软件就试图冲破这种限制。

为了使20位指针能指向数据,你需要指示编译程序使用正确的存储模式,例如紧缩存储模式。在中存储模式下,你可以用20位指针指向函数。在大和巨存储模式下,用20位指针既可以指向数据,也可以指向函数。在任何一种存储模式下,你都可能需要用到far指针(见7.18和7.19)。

基于286的系统可以冲破20位指针的限制,但实现起来有些困难。从386开始,IBM兼容机就可以使用真正的32位地址了,例如象MS-Windows和OS/2这样一些操作系统就实现了这一点,但MS—DOS仍未实现。

如果你的MS—DOS程序用完了基本内存,你可能需要从扩充内存或扩展内存中分配更多的内存。许多版本的编译程序和函数库都提供了这种技术,但彼此之间有所差别。这些技术基本上是不通用的,有些能在绝大多数MS-DOS和MS-WindowsC编译程序中使用,有些只能在少数特定的编译程序中使用,还有一些只能在特定的附加函数库的支持下使用。如果你手头有能提供这种技术的软件,你最好看一下它的文档,以了解更详细的信息。



对已说明的变量来说,变量名就是对变量值的直接引用。对指向变量或内存中的任何对象的指针来说,指针就是对对象值的间接引用。如果p是一个指针,p的值就是其对象的地址;*p表示“使间接引用运算符作用于p”,*p的值就是p所指向的对象的值。
*p是一个左值,和变量一样,只要在*p的右边加上赋值运算符,就可改变*p的值。如果p是一个指向常量的指针,*p就是一个不能修改的左值,即它不能被放到赋值运算符的左边,请看下例:

例 7.1 一个间接引用的例子
#include <stdio.h>
int
main()
{
int i;
int * p ;
i = 5;
p = & i; / * now * p = = i * /
/ * %Pis described in FAQ VII. 28 * /
printf("i=%d, p=%P, * p= %d\n" , i, P, *p);
* p = 6; / * same as i = 6 * /
printf("i=%d, p=%P, * p= %d\n" , i, P, *P);
return 0; / * see FAQ XVI. 4 * / }
}
上例说明,如果p是一个指向变量i的指针,那么在i能出现的任何一个地方,你都可以用*p代替i。在上例中,使p指向i(p=&i)后,打印i或*p的结果是相同的;你甚至可以给*p赋值,其结果就象你给i赋值一样。


对这个问题的回答与“指针的层数”所指的意思有关。如果你是指“在说明一个指针时最多可以包含几层间接引用”,答案是“至少可以有12层”。请看下例:
int i = 0;
int * ip0l = &d;
int ** ip02 = &ip01;
int ***ip03 = &ip02;
int **** ip04 = &dp03;
int ***** ip05 = &ip04;
int ****** ip06 = &ip05;
int ******* ip07 = &ip06;
int ******** ip08 = &ip07;
int ********* ip09 = &ip08;
int **********ip10 = &ip09;
int ***********ipll = &ip10;
int ************ ip12 = &ipll;
************ ip12 = 1; / * i = 1 * /

注意:ANSIC标准要求所有的编译程序都必须能处理至少12层间接引用,而你所使用的编译程序可能支持更多的层数。

如果你是指“最多可以使用多少层指针而不会使程序变得难读”,答案是这与你的习惯有关,但显然层数不会太多。一个包含两层间接引用的指针(即指向指针的指针)是很常见的,但超过两层后程序读起来就不那么容易了,因此,除非需要,不要使用两层以上的指针。

如果你是指“程序运行时最多可以有几层指针”,答案是无限层。这一点对循环链表来说是非常重要的,因为循环链表的每一个结点都指向下一个结点,而程序能一直跟住这些指针。请看下例:

例7.2一个有无限层间接引用的循环链表
/ * Would run forever if you didn't limit it to MAX * /
# include <stdio. h>
struct circ_list
{
char value[ 3 ]; /* e.g.,"st" (incl '\0') */
struct circ_list * next;
};
struct circ_list suffixes[ ] = {
"th" ,&.suffixes[ 1 ], / * Oth * /
"st" , &.suffixes[ 2 ], / * 1st * /
"nd" , & suffixes[ 3 ], / * 2nd * /
"rd" , & suffixes[ 4 ], / * 3rd * /
"th", &.suffixes[ 5 ], / * 4th * /
"th" , &.suffixes[ 6 ], / * 5th * /
"th" , & suffixes[ 7 ], / * 6th * /
"th" , & suffixes[ 8 ], / * 7th * /
"th", & suffixes[ 9 ], / * 8th * /
"th" , & suffixes[ 0 ], / * 9th * /
};
# define MAX 20

main()
{
int i = 0;
struct circ_list *p = suffixes;
while (i <=MAX) {
printf("%ds%\n", i, p->value);
+ +i;
p = p->next;
}
}

在上例中,结构体数组suffixes的每一个元素都包含一个表示词尾的字符串(两个字符加上末尾的NULL字符)和一个指向下一个元素的指针,因此它有点象一个循环链表;next是一个指针,它指向另一个circ_list结构体,而这个结构体中的next成员又指向另一个circ_list结构体,如此可以一直进行下去。

上例实际上相当呆板,因为结构体数组suffixes中的元素个数是固定的,你完全可以用类似的数组去代替它,并在while循环语句中指定打印数组中的第(i%10)个元素。循环链表中的元素一般是可以随意增减的,在这一点上,它比上例中的结构体数组suffixes要有趣一些。有时,在程序中需要使用这样一种指针,它并不指向任何对象,这种指针被称为空指针。空指针的值是NULL,NULL是在<stddef.h>中定义的一个宏,它的值和任何有效指针的值都不同。NULL是一个纯粹的零,它可能会被强制转换成void*或char*类型。即NULL可能是0,0L或(void*)0等。有些程序员,尤其是C++程序员,更喜欢用0来代替NULL。

指针的值不能是整型值,但空指针是个例外,即空指针的值可以是一个纯粹的零(空指针的值并不必须是一个纯粹的零,但这个值是唯一有用的值。在编译时产生的任意一个表达式,只要它是零,就可以作为空指针的值。在程序运行时,最好不要出现一个为零的整型变量)。

注意:空指针并不一定会被存为零。绝对不能间接引用一个空指针,否则,你的程序可能会得到毫无意义的结果,或者得到一个全部是零的值,或者会突然停止运行。


空指针有以下三种用法:

(1)用空指针终止对递归数据结构的间接引用。
递归是指一个事物由这个事物本身来定义。请看下例:
/*Dumb implementation;should use a loop */
unsigned factorial(unsinged i)
{
if(i=0 || i==1)
{
return 1;
}
else
{
return i * factorial(i-1);
}
}

在上例中,阶乘函数factoriai()调用了它本身,因此,它是递归的。

一个递归数据结构同样由它本身来定义。最简单和最常见的递归数据结构是(单向)链表,链表中的每一个元素都包含一个值和一个指向链表中下一个元素的指针。请看下例:
struct string_list
{
char *str; /* string(inthiscase)*/
struct string_list *next;
};
此外还有双向链表(每个元素还包含一个指向链表中前一个元素的指针)、键树和哈希表等许多整洁的数据结构,一本较好的介绍数据结构的书中都会介绍这些内容。

你可以通过指向链表中第一个元素的指针开始引用一个链表,并通过每一个元素中指向下一个元素的指针不断地引用下一个元素;在链表的最后一个元素中,指向下一个元素的指针被赋值为NULL,当你遇到该空指针时,就可以终止对链表的引用了。请看下例:
while(p!=NULL)
{
/*dO something with p->str*/
p=p->next;
}
请注意,即使p一开始就是一个空指针,上例仍然能正常工作。

(2)用空指针作函数调用失败时的返回值。
许多C库函数的返回值是一个指针,在函数调用成功时,函数返回一个指向某一对象的指针;反之,则返回一个空指针。请看下例:
if(setlocale(cat,loc_p)==NULL)
{
/* setlocale()failed;do something*/
/* ...*/
}
返回值为一指针的函数在调用成功时几乎总是返回一个有效指针(其值不等于零),在调用失败时则总是返回一个空指针(其值等于零);而返回值为一整型值的函数在调用成功时几乎总是返回一个零值,在调用失败时则总是返回一个非零值。请看下例:
if(raise(sig)!=0){
/* raise()failed;do something*/
/* ... */
}
对上述两类函数来说,调用成功或失败时的返回值含义都是不同的。另外一些函数在调用成功时可能会返回一个正值,在调用失败时可能会返回一个零值或负值。因此,当你使用一个函数之前,应该先看一下它的返回值是哪种类型,这样你才能判断函数返回值的含义。

(3)用空指针作警戒值
警戒值是标志事物结尾的一个特定值。例如,main()函数的预定义参数argv是一个指针数组,它的最后一个元素(argv[argc])永远是一个空指针,因此,你可以用下述方法快速地引用argv中的每一个元素:

/*
A simple program that prints all its arguments.
It doesn't use argc ("argument count"); instread.
it takes advantage of the fact that the last
value in argv ("argument vector") is a null pointer.
*/
# include <stdio. h>
# include <assert. h>
int
main ( int argc, char * * argv)
{
int i;
printf ("program name = \"%s\"\n", argv[0]);
for (i=l; argv[i] !=NULL; ++i)
printf ("argv[%d] = \"%s\"\n", i, argv[f]);
assert (i = = argc) ; / * see FAQ XI. 5 * /
return 0; / * see FAQ XVI. 4 * /
}

void指针是什么?

void指针一般被称为通用指针或泛指针,它是C关于“纯粹地址(raw address)”的一种约定。void指针指向某个对象,但该对象不属于任何类型。请看下例:
int *ip;
void *p;
在上例中,ip指向一个整型值,而p指向的对象不属于任何类型。
在C中,任何时候你都可以用其它类型的指针来代替void指针(在C++中同样可以),或者用void指针来代替其它类型的指针(在C++中需要进行强制转换),并且不需要进行强制转换。例如,你可以把char *类型的指针传递给需要void指针的函数。

什么时候使用void指针?

当进行纯粹的内存操作时,或者传递一个指向未定类型的指针时,可以使用void指针。void指针也常常用作函数指针。

有些C代码只进行纯粹的内存操作。在较早版本的C中,这一点是通过字符指针(char *)实现的,但是这容易产生混淆,因为人们不容易判断一个字符指针究竟是指向一个字符串,还是指向一个字符数组,或者仅仅是指向内存中的某个地址。

例如,strcpy()函数将一个字符串拷贝到另一个字符串中,strncpy()函数将一个字符串中的部分内容拷贝到另一个字符串中:
char *strepy(char'strl,const char *str2);
char *strncpy(char *strl,const char *str2,size_t n);
memcpy()函数将内存中的数据从一个位置拷贝到另一个位置:
void *memcpy(void *addrl,void *addr2,size_t n);

memcpy()函数使用了void指针,以说明该函数只进行纯粹的内存拷贝,包括NULL字符(零字节)在内的任何内容都将被拷贝。请看下例:
#include "thingie.h" /* defines struct thingie */
struct thingie *p_src,*p_dest;
/* ... */
memcpy(p_dest,p_src,sizeof(struct thingie) * numThingies);

在上例中,memcpy()函数要拷贝的是存放在structthingie结构体中的某种对象op_dest和p_src都是指向structthingie结构体的指针,memcpy()函数将把从p_src指向的位置开始的sizeof(stuctthingie) *numThingies个字节的内容拷贝到从p_dest指向的位置开始的一块内存区域中。对memcpy()函数来说,p_dest和p_src都仅仅是指向内存中的某个地址的指针。如果两个指针向同一个数组,它们就可以相减,其为结果为两个指针之间的元素数目。仍以本章开头介绍的街道地址的比喻为例,假设我住在第五大街118号,我的邻居住在第五大街124号,每家之间的地址间距是2(在我这一侧用连续的偶数作为街道地址),那么我的邻居家就是我家往前第(124-118)/2(或3)家(我和我的邻居家之间相隔两家,即120号和122号)。指针之间的减法运算和上述方法是相同的。

在折半查找的过程中,同样会用到上述减法运算。假设p和q指向的元素分别位于你要找的元素的前面和后面,那么(q-p)/2+p指向一个位于p和q之间的元素。如果(q-p)/2+p位于你要找的元素之前,下一步你就可以在(q-p)/2+p和q之间查找要找的元素;反之,你可以停止查找了。

如果两个指针不是指向一个数组,它们相减就没有意义。假设有人住在梅恩大街110号,我就不能将第五大街118号减去梅恩大街110号(并除以2),并以为这个人住在我家往回第4家中。

如果每个街区的街道地址都从一个100的倍数开始计算,并且同一条街的不同街区的地址起址各不相同,那么,你甚至不能将第五大街204号和第五大街120号相减,因为它们尽管位于同一条街,但所在的街区不同(对指针来说,就是所指向的数组不同)。

C本身无法防止非法的指针减法运算,即使其结果可能会给你的程序带来麻烦,C也不会给出任何提示或警告。

指针相减的结果是某种整类型的值,为此,ANSIC标准<stddef.h>头文件中预定义了一个整类型ptrdiff_t。尽管在不同的编译程序中ptrdiff_t的类型可能各不相同(int或long或其它),但它们都适当地定义了ptrdiff_t类型。

例7.7演示了指针的减法运算。该例中有一个结构体数组,每个结构体的长度都是16字节。

如果是对指向结构体数组的指针进行减法运算,则a[0]和a[8]之间的距离为8;如果将指向结构体数组的指针强制转换成指向纯粹的内存地址的指针后再相减,则a[0]和aL8]之间的距离为128(即十六进制数0x80)。如果将指向a[8]的指针减去8,该指针所指向的位置并不是往前移了8个字节,而是往前移了8个数组元素。

注意:把指针强制转换成指向纯粹的内存地址的指针,通常就是转换成void *类型,但是,本例将指针强制转换成char *类型,因为void。类型的指针之间不能进行减法运算。

例 7.7 指针的算术运算

# include <stdio. h>
# include <stddef.h>

struct stuff {
charname[l6];
/ * other stuff could go here, too * /
};
struct stuff array [] = {
{ "The" },
{ "quick" },
{ "brown" >,
{ "fox" },
{ "jumped" },
{ "over" },
{ "the" },
{ "lazy" },
{ "dog. " },
/*
an empty string signifies the end;
not used in this program,
but without it, there'd be no way
to find the end (see FAQ IX. 4)
*/
{ " " }
};
main ( )
{
struct stuff * p0 = &.array[0];
struct stuff * p8 = &-array[8];
ptrdiff_t diff = p8-p0;
ptrdiff_t addr.diff = (char * ) p8 - (char * ) p0;
/*
cast the struct stuff pointers to void *
(which we know printf() can handles see FAQ VII. 28)
*/
printf ("&array[0] = p0 = %P\n" , (void* ) p0);
printf ("&. array[8] = p8 = %P\n" , (void* ) p8) ;
*/
cast the ptrdiff_t's to long's
(which we know printf () can handle)
*/
printf ("The difference of pointers is %ld\n" , (long) diff) ;
printf ("The difference of addresses is %ld\n" , (long) addr_diff);
printf ("p8-8 = %P\n" , (void*) (p8-8));
/ * example for FAQ VII. 8 * /
printf ("p0 + 8 = %P (same as p8)\n", (void* ) (p0 + 8));
return 0; / * see FAQ XVI. 4 * /
}



当把一个整型值加到一个指针上后,该指针指向的位置就向前移动了一段距离。就纯粹的内存地址而言,这段距离对应的字节数等于该值和该指针所指向的对象的大小的乘积;但是,就C指针真正的工作机理而言,这段距离对应的元素数等于该整型值。

在例7.7末尾,当程序将8和&array[o]相加后,所得的指针并不是指向&array[0]后的第8个字节,而是第8个元素。

仍以本章开头介绍的街道地址的比喻为例,假设你住在沃克大街744号,在你这一侧用连续的偶数作为街道地址,每家之间的地址间距是2。如果有人想知道你家往前第3家的地址,他就会先将2和3相乘,然后将6和你家的地址相加,得到他想要的地址750号。同理,你家往回第1家的地址是774+(-1)*2,即742号。

街道地址的算术运算只有在一个特定的街区中进行才有意义,同样,指针的算术运算也只有在一个特定的数组中进行才有意义。仍以上一段所介绍的背景为例,如果你想知道你家往回第400家的地址,你将得到沃克大街-56号,但这是一个毫无意义的地址。如果你的程序中使用了一个毫无意义的地址,你的程序很可能会被彻底破坏。


NULL总是被定义为0吗?

NULL不是被定义为o,就是被定义为(void *)0,这两种值几乎是相同的。当程序中需要一个指针时(尽管编译程序并不是总能指示什么时候需要一个指针),一个纯粹的零或者一个void指针都能自动被转换成所需的任何类型的指针。

NULL总是等于0吗?

对这个问题的回答与“等于”所指的意思有关。如果你是指“与。比较的结果为相等”,例如:
if(/* ... */)
{
p=NULL;
}
else
{
p=/* something else */;
}
/* ... */
if(p==0)
那么NULL确实总是等于0,这也就是空指针定义的本质所在。

如果你是指“其存储方式和整型值。相同”,那么答案是“不”。NULL并不必须被存为一个整型值0,尽管这是NULL最常见的存储方式。在有些计算机中,NULL会被存成另外一些形式。

如果你想知道NULL是否被存为一个整型值0,你可以(并且只能)通过调试程序来查看空指针的值,或者通过程序直接将空指针的值打印出来(如果你将一个空指针强制转换成整类型,那么你所看到的很可能就是一个非零值)。

当把一个指针作为条件表达式时,所要判断的条件实际上就是“该指针是否为一空指针”。在if,while,for或do/while等语句中,或者在条件表达式中,都可以使用指针。请看下例:
if(p)
{
/*dO something*/
}
else
{
/* dOsomethingelse */
}

当条件表达式的值不等于零时,if语句就执行“then”子句(即第一个子句),即“if(/*something*/)”和“if(/*something*/!=0)”是完全相同的。因此,上例和下例也完全相同:
if(p !=0)
{
/* dO something(not anull pointer)*/
}
else
{
/* dOsomethingelse(a null pointer)*/
}
以上两例中的代码不易读,但经常出现在许多C程序中,你不必编写这样的代码,但要理解这些代码的作用。两个指针是不能相加的。仍以街道地址的比喻为例,假设你住在湖滨大道1332号,你的邻居住在湖滨大道1364号,那么1332+1364指的是什么呢?其结果是一个毫无意义的数字。如果你的C程序试图将两个指针相加,编译程序就会发出警告。

当你试图将一个指针和另外两个指针的差值相加的时候,你很可能会误将其中的两个指针相加,例如,你很可能会使用下述语句:
p=p+p2-p1;

上述语句是不正确的,因为它和下述语句完全相同:
p=(p+p2)-p1;
正确的语句应该是:
p=p+(p2-p1);
对此例来说,使用下述语句更好:
p+=p2-p1;


在使用指向函数的指针时,最难的一部分工作是说明该指针。例如,strcmp()函数的说明如下所示:
int strcmp(const char*,const char*);

如果你想使指针pf指向strcmp()函数,那么你就要象说明strcmp()函数那样来说明pf,但此时要用*pf代替strcmp:
int (*pr)(const char*,const char*);

请注意,*pf必须用括号括起来,因为
int *p{ (constchar * ,constchar * ); /* wrong */
等价于
(int *)pr(const char *,const char * ); /* wrong */
它们都只是说明了一个返回int *类型的函数。

在说明了pf后,你还要将<string.h>包含进来,并且要把strcmp()函数的地址赋给pf,即:
pf=strcmp;

pf=Slstrcmp; /* redundant& */
此后,你就可以通过间接引用pf来调用strcmp()函数:
if(pr(strl,str2)>0) /*...*/



函数的指针可以作为一个参数传递给另外一个函数,这一点非常有意思。一个函数用函数指针作参数,意味着这个函数的一部分工作需要通过函数指针调用另外的函数来完成,这被称为“回调(callback)”。处理图形用户接口的许多C库函数都用函数指针作参数,因为创建显示风格的工作可以由这些函数本身完成,但确定显示内容的工作需要由应用程序完成。

举一个简单的例子,假设有一个由字符指针组成的数组,你想按这些指针指向的字符串的值对这些指针进行排序,你可以使用qsort()函数,而qsort()函数需要借助函数指针来完成这项任务(关于排序的详细介绍请参见第3章“排序和查找”。qsort()函数有4个参数:
(1) 指向数组开头的指针;
(2) 数组中的元素数目;
(3) 数组中每个元素的大小;
(4) 指向一个比较函数的指针。
qsort()函数返回一个整型值。

比较函数有两个参数,分别为指向要比较的两个元素的指针。当要比较的第一个元素大于、等于或小于第二个元素时,比较函数分别返回一个大于o,等于。或小于。的值。一个比较两个整型值的函数可能如下所示:
int icmp(const int *p1,const int *p2)
{
return *p1-*p2;
}
排序算法和交换算法都是qsort()函数的部分内容。qsort()函数的交换算法代码只负责拷贝指定数目的字节(可能调用memcpy()或memmove()函数),因此qsort()函数不知道要对什么样的数据进行排序,也就不知道如何比较这些数据。比较数据的工作将由函数指针所指向的比较函数来完成。

对本例来说,不能直接用strcmp()函数作比较函数,其原因有两点:第一,strcmp()函数的类型与本例不符(见下文中的介绍);第二,srtcmp()函数不能直接对本例起作用。strcmp()函数的两个参数都是字符指针,它们都被strcmp()函数看作是字符串中的第一个字符;本例要处理的是字符指针(char *s),因此比较函数的两个参数必须都是指向字符指针的指针。本例最好使用下面这样的比较函数;
int strpcmp(const void *p1,const void *p2)
{
char * const *sp1 = (char * const *)p1;
char'const *sp2=(char *const *)p2;
return strcmp(*sp1,*sp2);
}

本例对qsort()函数的调用可以如下所示:
qsort(array,numElements,sizeof(char *),pf2);
这样,每当qsort()函数需要比较两个字符指针时,它就可以调用strpcmp()函数了。

为什么不能直接将strcmp()函数传递给qsort()函数呢?为什么strpcmp()函数中的参数是如此一种形式呢?因为函数指针的类型是由它所指向的函数的返回值类型及其参数的数目和类型共同决定的,而qsort()函数要求比较函数含两个const void *类型的参数:
void qsort(void *base,
size_t numElernents,
size_t sizeOfElement,
int(*compFunct)(const void *,const void *));

qsort()函数不知道要对什么样的数据进行排序,因此,base参数和比较函数中的两个参数都是void指针。这一点很容易理解,因为任何指针都能被转换成void指针,并且不需要强制转换。但是,qsort()函数对函数指针参数的类型要求就苛刻一些了。本例要排序的是一个字符指针数组,尽管strcmp()函数的比较算法与此相符,但其参数的类型与此不符,所以在本例中strcmp()函数不能直接被传给qsort()函数。在这种情况下,最简单和最安全的方法是将一个参数类型符合qsort()函数的要求的比较函数传给qsort()函数,而将比较函数的参数强制转换成strcmp()函数所要求的类型后再传给strcmp()函数;strpcmp()函数的作用正是如此。

不论C程序在什么样的环境中运行,char *类型和void。类型之间都能进行等价的转换,因此,你可以通过强制转换函数指针类型使qsort()函数中的函数指针参数指向strcmp()函数,而不必另外定义一个strpcmp()这样的函数,例如:
char table[NUM_ELEMENTS][LEMENT_SIZE);
/* ... */
/* passing strcmp() to qsort for array Of array Of char */
qsort(table,NUM_ELEMENTS,ELEMENT_SIZE,
(int(*)(const void *,const void *))strcmp);
不管是强制转换strpcmp()函数的参数的类型,还是强制转换指向strcmp()函数的指针的类型,你都必须小心进行,因为稍有疏忽,就会使程序出错。在实际编程中,转换函数指针的类型更容易使程序出错。


分享到:
评论

相关推荐

    深入探讨C++的this指针

    this指针作为一个隐含参数传递给非静态成员函数,用以指向该成员函数所属类所定义的对象

    C++指针探讨.pdf

    本文关于C++中最难学的指针作了深入探究,希望大家能够学到更多。

    深入探讨C++中的引用

    深入探讨C++中的引用,引用,const,多态,指针 int a ; const int &ra=a; ra=1; //错误 a=1; //正确

    指针和内存分配详解

    深入的探讨了,指针和内存问题,很好的帮你解决指针和内存的各种疑惑

    C++指针介绍及使用说明

    本文深入探讨了C++中指针的概念、特性及其在各种场景下的应用。通过细致的讲解和生动的实例,文章向读者展示了指针的基本语法和操作,以及其在内存管理、函数参数传递、数组和字符串操作、动态内存分配等方面的重要...

    内存分配深入探讨

    内存管理向来是C/C++程序设计的一块雷区,虽然利用C++中的smart pointer已经可以完全避免使用指针,但是对于对于指针的进一步了解,有助于我们编写出更有效率的代码,也有助于我们读懂以前编写的程序。

    描述一下C语言经典指针问答,并附带详细的解答举例说明.docx

    本文将深入探讨C语言指针的相关概念,帮助你更好地理解和运用它们。 C语言指针是程序设计中最重要的概念之一。它可以帮助你更好地管理内存,提高程序的效率,让你更加高效地编写代码。在这个问答集中,你将会学习到...

    VC++ 2005(5):指针与对象模型

    指针是C++语言的精髓,也是C++语言的难点,由于CLI平台的托管特性,C++/CLI中出现了各种指针的变体,可谓难上加难。...本课程将从本地对象模型和托管对象模型入手,步步深入,探讨C++/CLI中的各种指针。

    C 和指针.pdf

    本书通过对指针的基础知识和高级特性的探讨,帮助程序员把指针的强大功能融入到自己的程序中去。  全书共18章,覆盖了数据、语句、操作符和表达式、指针、函数、数组、字符串、结构和联合等几乎所有重要的C编程话题...

    c和指针课后题答案(完整版).pdf

    本书提供与c语言编程相关的全面资源和深入讨论。本书通过对指针的基础知识和高级特性的探讨,帮助程序员把指针的强大功能融入到自己的程序中去

    C语言编程,指针

    本书提供与C语言编程相关的全面资源和深入讨论。本书通过对指针的基础知识和高级特性的探讨,帮助程序员把指针的强大功能融入到自己的程序中去。

    c 和指针 part1

    本书提供与C语言编程相关的全面资源和深入讨论,通过对指针的基础知识和高级特性的探讨,帮助程序员把指针的强大功能融入到自己的程序中

    vc++2005系列课程

    指针是C++语言的精髓,也是C++语言的难点,由于CLI平台的托管特性,C++/CLI中出现了各种指针的变体,可谓难上加难。...本课程将从本地对象模型和托管对象模型入手,步步深入,探讨C++/CLI中的各种指针。

    深入浅出MFC-简体版(2)PDF

    第8章 Document-View深入探讨 第9章 消息映射与命令传递 第10章 MFC与对话盒 第11章 View功能的加强与重绘效率的提高 第12章 打印与预览 第13章 多重文件与多重显示 第14章 MFC多线程程序设计 第15章 定制一个...

    基于c中使用ftruncate()前需要fflush(),使用后需要rewind()的深入探讨

    今天用ftruncate截断文件, 但怎么都不能达到预料的效果, 截断后文件中的内容比较杂, 而且文件大小也保持原来的.添加 fflush() 和 rewind() 后OK.以下是测试代码: 代码如下:#include &lt;stdio&gt;#include &lt;sys&gt;#include ...

    JavaScript中对循环语句的优化技巧深入探讨

    循环是所有编程语言中最为重要的机制之一,几乎任何拥有实际意义的计算机程序(排序、查询等)都里不开循环。 而循环也正是程序优化中非常让人头疼的一环,我们往往需要不断去优化程序的复杂度,却因循环而纠结在...

    深入浅出MFC【侯捷】

    改用CEditView 第四篇 深入MFC程序设计 第8章 Document-View深入探讨 为什么需要Document-View(形而上) Document View Document Frame(View Frame) Document Template CDocTemplate管理CDocument/CView/...

    深入浅出MFC 2e

    第8章 Document-View深入探讨 为什么需要Document-View(形而上) Document View Document Frame(View Frame) Document Template CDocTemplate管理CDocument/CView/CFrameWnd Scribble Step1的Document——数据...

    现代C语言核心特性解析,C语言编程与宠物狗互动案例分析.pdf

    接着,我们将深入探讨指针和数组,包括指针的基本概念、指针的算术运算、指针和数组的关系等等。然后,我们将介绍C语言中的结构体和联合体,以及这些数据类型在实际编程中的应用。此外,我们还将讲解C语言中的函数和...

    二叉树的基本操作c语言实现

    本文将深入探讨二叉树这一重要的数据结构,包括其定义、特性、常见的二叉树类型(如满二叉树、完全二叉树、平衡二叉树等)以及其在计算机科学中的应用。通过阅读本文,读者将能够全面理解二叉树的基本概念和原理,并...

Global site tag (gtag.js) - Google Analytics