Nisy 发表于 2008-6-23 20:41:12

浅谈 C 指针 [09.04.02更新]o(∩_∩)o...

浅谈 C 指针


   指针是 C 语言的精华所在,利用这几天的时间,正好把之前对指针的理解做一个总结。由于自己水平有限,文中一些自己对指针妄下的论断和理解,难免会引起大家的争议,若有出错之处还望各位指正。
                                                                                    By: Nisy08.06.23

============================================


第一节 传参和寻址

      在研究变量前,我们先来看一下 Debug 描述的 Dos 的内存情况。



【选读内容】图中 debug 显示的内存环境自左向右分为三部分:中间部分为内存中的数据,在 debug 中以16进制显示;其左为该行第一个字节在内存中的地址;其右显示本行各存储单元内数据对应的字符形式。这里的地址就是人为的为其顺序编号,每前进一个字节地址 + 1。在 debug 中每行显示16个字节,即每向下一行地址 +10H 。我们向内存 0000:0200 地址处写入字符串"ABCDEFGHIJKLMNOP",可以看到内存中相应字节保存值为对应字符的ASCII值(ASCII表详见《C语言程序设计》的附录A)。
【了解内容】在16位的程序中,寻址地址可达20位,通过用段地址:偏移地址来寻址,段地址和偏移地址均为16位占两个字节。(16位程序的寻址可参阅《汇编语言》前两章内容)

      内存中最小存储单元为字节,我们可以把内存比作一张方格纸,每格存放一个字节的数据. 程序将自身加载到内存中方能运行,那么程序如何访问内存中的数据呢,通过对《汇编语言》的学习我们得知程序访问内存的数据首先要得到其地址和的长度。所以我们在程序中定义的变量也就有了两个最基本的属性:该变量在内存中的首地址和所占内存的长度。我们通过一个程序来分析变量的基本属性。

main()
{
      char a;
      a='A';
      printf("%c",a);
}

本文中将用以下格式图来描述变量在内存中的的存储(为方便描述段地址省略不写)


地址:   数据      ASCII
XX00: 41 00 00 00| A ...   // 变量 a 的地址为XXXX
.
    我们定义一个字符型变量 a 并赋值字符'A' ,数据'A'以ASCII值的形式保存在内存地址 XX00 对应的单元(一个字节)。
   输出指令 printf("%c",a); 中参数(变量) a 传递的是地址还是数据? a 等于 &a 吗?
   从格式图中我们可以看到 a 的ASCII值=41H,即字母'A'。程序输出的结果也是字母'A'。所以我们得出:格式输出中 %c 对应的输出列表是内存单元的数据,而非内存数据的地址。(%d 在格式输出中对应的也是内存数据)

   下面我们来看一下指针变量传参是否也符合这个结论。

main()
{
      char a;
      char *b=&a;
      *b='A';
      printf("%c",*b);
}


地址:   数据      ASCII
XX00: 41 00 00 00| A ...   // 变量 a 的地址为XX00
XX10: 00 XX ?? ??| ....    // 变量 b 的地址为 XX10 其保存的数据为 a 的地址(注意这里低位保存在低字节)

      输出语句中 printf("%c",*b); 变量 b 仍旧为( &b 指向)内存中的数据,*b 指向以 b 为地址的内存单元中存放的数据,即a=’A’。我们进一步得出结论:我们在程序中用到的变量都是该变量在内存中地址所对应的数据,即:变量 = 内存数据,长度由变量类型决定。

    我们从上文中得知字符变量在内存中保存的数值为对应的ASCII码,1字节可表示的大小为0~255,ASCII代码对照表中正好有256个字符与之一一对应(128个标准字符和128个扩展字符),所以字符型数据保存其对应的ASCII数值即可。那么其他类型的变量呢?保存的还是其对应的ASCII码吗?我们通过下例来分析:

main()
{
      int a=9;
      int b=0x39;
      printf("%d ",a);
      printf("%X ",a);
      printf("%d ",b);
      printf("%c ",b);
}


地址:    数据         ASCII
XX00: 09 00 39 00| ..9.   //变量 a 的地址为XX00 ;变量 b 的地址为 XX02

输出结果:9 9 57 9

    我们通过查ASCII表可以找到字符 9 对应的ASCII值:57=39H 。我们对变量 a 赋值以十六进制输出,看到其内存数据就是我们所输入的数值,而 b 以 %c 形式输出时,却显示了其数值在ASCII表中对应的字符。于是我们可以得出:char 型变量以其对应的ASCII码来存放;int 型变量保存的就是自身,在Debug(内存)中以其数值的16进制保存;所以内存中数据自身是没有属性可言,只是在使用时(或输出时)人为的为其声明了数据类型。

    在进行字符串输出时,我们只需要得知其首地址即可,因为内存中保存的字符串都以 00 (即\0)结束,所以输出字符串时遇到 00 则终止。


===========================================


第二节 二维数组的指针

    上文中我们了解到变量的二重属性:首地址和长度。下面我们来研究一下指针变量的属性:

main()
{
      int a;
      int *b;
      char c;
      char *d;
      printf("%d ",sizeof(b));          /*我们可以使用 sizeof() 来测定变量的长度*/
      printf("%d ",sizeof(*b));
      printf("%d ",sizeof(d));
      printf("%d ",sizeof(*d));
}

    输出的结果为: 4 2 4 1 。说明指针变量在内存中的长度为4个字节,指针变量指向的数据长度和指针变量类型的长度相等。所以指针变量有这样几个性质:变量存放的数据为地址、长度=4字节、其指向的数据属性等于指针变量所定义的类型。

【说明内容】C 语言中提供了6种编译模式:微模式(Tiny),小模式(Small),中模式(Medium),紧凑模式(Compact),大模式(Large)和巨模式(Huge)。默认的编译模式是Small型的,前三种编译的程序只有一个程序段,大小不超过64KB,指针是near型(近指针,内存中占两个字节)。后三种可以有多个程序段,每个程序段不超过64KB,但总程序量可超过64KB,指针是far型(远指针,内存中占四个字节)。教学上为了让大家更多的去了解指针,我们这篇文章中用的都是紧凑模式(Compact),在内存中都是四个字节,这些内容大家了解即可,TC2.0中可以在 Options -> Compiler -> Model 中修改编译模式。

下面我们定义一个二维数组 a(数组在内存中是以线性方式依次存放的),并通过一个指针变量来输出字符数据 a。

    如何去定义一个指向二维数组的指针变量呢,我们先来看char型变量 a , a+1 地址将增加 5( 增加地址 = 变量类型基本长度 * 第二维长度 ) ,该变量由于第二维的出现影响了 char 型变量的长度属性,长度属性扩展为:变量类型基本长度 * 第二维长度。所以我们也只需要定义一个 char 型指针变量并将其第三属性(指向数据的长度)相应扩展即可。如何扩展其第三维属性呢?我们可以效仿二维数组变量的扩展方法。所以得到定义指向二维数组的指针变量的格式是: char (*b); ( n 为第二维的长度 )。由于运算符的优先级,在进行对指针变量第三属性扩展前首先要将指针变量用()进行保护。

注:文中所提及的变量的第三属性仅是为了方便大家理解来虚构的,我们可以理解为第三属性是用来通知编译器在编译过程中如何来处理内存中的该数据。内存中的数据本无任何属性可言,仅是使用时找到起始地址和长度而已。

程序实现如下:

main()
{
      char a={'C','h','i','n','a','H','e','l','l','o'};
      char (*b)=a;
      printf("%c \n",*(*(b+1)+1));      
}

输出结果为:e

地址:                      数据                            ASCII
XX00: 43 68 69 6E 61 48 65 6C-6C 6F 00 00 00 00 00 00 | ChinaHello......

    我们知道使用指向二维数组的指针变量 b 进行操作时,b+1 相当于地址 +5个字节( 基本长度 * 第二维长度 ),而格式输出中 %c 能访问的只是一个字节,所以在访问被扩展长度后的内存单元时,首先要将其扩展属性收回,这个操作可以在指向扩展单元后在加 * 来实现,即 *(b+n) 就可以实现指向该扩展单元的首地址,这里 ‘*’的意义就是指向该单元的首地址。注意:这里的属性收回操作是强制的,我们引用其扩展长度后内存单元中的数据时必须先强制收回扩展属性,否则无法定位其中的任何数据。如我们如果想输出a这个字符,就需要使用 printf("%c",**b); ,这里显然 **b 不是指向指针的指针。


===========================================


第三节指向函数的指针和返回指针值的函数

上文中我们介绍了指针变量的第三属性的扩展,我们先做一个总结:

int    a;   // 定义   整型      的      变量:
int   *a;   // 定义   整型      的   指针变量:
int   *a[];// 定义   整型      的   指针数组;
int (*a)[];   // 定义 指向整型二维数组的   指针变量:

我们可以将指向二维数组的指针变量写成如下格式:

int    ( *a )                  [];

定义整型 指针变量 (属性扩展) 指向二维数组

即先定义指针变量,然后对第三属性进行扩展即可扩充指针变量的类型和属性。下文我们来讨论定义指向函数的指针。

要求:我们输入三个整形数据,输出最大值,并将这三个数值按从大到小的顺序输出。

我们先来看输入两个整数时的情况:

main()
{
      int a,b;
      void swap(int *,int *);
      scanf("%d %d",&a,&b);
      swap(&a,&b);
      printf("%d \n",a);
      printf("%d %d\n",a,b);
}

void swap(int *a,int *b)
{
      int c;
      if(*a < *b)
      {
                c=*a;
                *a=*b;
                *b=c;
      }
}

    我们看到定义一个函数的格式为 void f();,所以我们猜测定义一个指向函数的指针变量只需要先定义一个指针变量,并将其第三属性做相应扩展:void (*a)(); ,我们用指向函数的指针变量来实现上方的程序。

main()
{
      int a,b;
      int swap(int *,int *);
      int (*p)()=swap;
      scanf("%d %d",&a,&b);
      printf("%d \n",p(&a,&b));/* p(&a,&b) 函数可以当其返回值来用 */
      printf("%d %d\n",a,b);
}

int swap(int *a,int *b)
{
      int c;
      if(*a < *b)
      {
                c=*a;
                *a=*b;
                *b=c;
      }
      return *a;               /*返回最大值*/
}

说明两点:
01.函数名等于该函数的首地址,将函数名给指针变量就等于将函数首地址给指针变量。
02.我们调用非void型的函数可以其返回值来使用。

了解了定义指向函数的指针变量后,我们来设计输入三个整型数据的程序:

main()
{
      int a,b,c;
      int *swap(int *,int *);
      int *(*p)()=swap;         
      scanf("%d %d %d",&a,&b,&c);
      printf("%d\n",*(p(p(&a,&b),&c))); /*非void型的函数可以当其返回值来用 该语句实现最大值同 a 互换*/
      p(&b,&c);
      printf("%d %d %d",a,b,c);
}

int * swap(int *a,int *b)
{
      int c;
      if(*a < *b)
      {
                c=*a;
                *a=*b;
                *b=c;
      }
      return a;                        /*变量 a 是地址 而 *a 为整型数据*/
}

我们来看一下这两个指向函数的指针变量:int (*p)(); 和 int *(*p)(); ,貌似有些复杂,我们将其拆开分析或许就会一目了然:

int         (*p)                  ();
整型       指针变量    (属性扩展)指针变量指向函数

int *       (*p)                  ();
整型指针   指针变量    (属性扩展)指针变量指向函数

==> int(*p)();指向返回整型    变量的   函数的指针
==> int *(*p)();指向返回整型指针变量的   函数的指针


===========================================


第四节 指向指针的指针


我们看一下这个例子,定义一个指针数组,并输出该指针数组中首元素所指向的字符串:

main()
{
         char *a[]={"Hello","Nisy!"};/* 赋值的字符串在这里相当于该字符串的地址 */
         printf("%s \n", a );
}

输出结果为:Hello

我们用一个指向来实现输出第一个字符串 "Hello",应该如何定义呢?

main()
{
         
         char *a[]={"Hello","Nisy!"};
         char *p;
         p=(char *)a;
         printf("%s \n", (?)p );
}

假若我们使用 printf("%s \n", *p ); 结果会是如何呢? 我们可以肯定的是 变量p 就是 a (即&a),那*p呢,是a吗? 我们用内存图标来描述一下:

地址:   数据      
XXXX: 22 11 ?? ?? 44 33 ?? ??//数组a的地址是XXXX, a=????1122, a=????3344

char *p 其中 变量p 存放的就是 数组a的地址 xxxx,而 *p 指向的是 xxxx 指向的内存数据 22,他指向一个字节。而我们需要的从xxxx开始的四个字节的数据,即a。我们看以下对比来分析:

我们来分析指针的定义模式:

int   *p;*p 指向的是内存起始地址为 p 对应的两个字节
char*p;*p 指向的是内存起始地址为 p 对应的一个字节

==> 所以我们可以认为 该指针是通过类型声明来通知编译器要取内存数据的长度,我们把 int 和 char 来看做是对指针变量类型的声明,我们这里需要取四个字节,所以我们推断:只要把指针的类型声明为一个指针型就可以用 *p 取到指针指向的四个字节了。

(char)*p;==> *p 指向的是 char   型的数据   一个字节
(char *)*p;==> *p 指向的是 char * 型的数据   四个字节

程序修改如下:

main()
{
         
         char    *a[]={"Hello","Nisy!"};
         char **p;
         p=a;
         printf("%s \n",   *p );
         printf("%s \n",   *(p+1));       // 输出第二个字符
         printf("%c \n", *(*(p+1)+2));    // 输出第二个字符串中的第三个字符
}

也可以写成:

main()
{
         
         char   *a[]={"Hello","Nisy!"};
         char   *p;
         p=(char *)a;
         printf("%s \n", *(char **)p );// 使用前对指针前进行声明
}

我们称这样的指针变量叫做 指向指针的指针,其实就是对指针变量的类型声明为指针型。

说明几点:
01. char a[]为字符数组,即数组存放的数据为 char 型;char *a[] = char * a[] 即数组存放的数据为 char * 型。而 char (*a)[] 为指向二维数组的指针变量,仅一个()之差,但意义相差甚远,我们要根据其意来对比理解。
02. 指向指针的指针,其实就是对变量的第三属性(对编译器)进行了声明:该变量指向的数据为指针类型,以该变量为地址指向的数据类型同该变量相同。
03. 对比法:char *p 中 *p 指向的是一个字节的字符型数据,若我们指向的数据是四个字节的地址,就需要将定义类型进行扩展,使其指向 char * 的一个数据,即 (char * ) *p ;

    从上文我们可以得出: b = a 的地址; *b = a, b+1 = XX20 + 4 (因为b为指针型 +1 就等于 b 加上4个字节,即&a),*(b+1) = a 。下面我们再通过一个程序来进一步的分析指针变量:

char a[]="ChinaPYG";
char *b=a;
char **c=a;
char *e=&a;

main()
{
      printf("%X ",b);
      printf("%X ",c);
      printf("%X ",e);
}

在程序中我们定义了一个全局数组变量 a ,其数据为"ChinaPYG" ,又连续定义了三个指针变量用于接受 a 的首地址。

程序输出结果为: 94 94 94三个指针变量中保存的都为 a ,即该字符串的地址为 0094 ,我们用反汇编工具IDA来定位程序中的该字符串:





dseg:009000 00 00 00 43 68 69 6E-61 50 59 47 00 94 00 A9 "....ChinaPYG.??
dseg:00A001 94 00 A9 01 94 00 A9-01 25 58 20 00 00 00 00 "????%X ...

   我们发现字符串之后出现了三个" 94 00 A9 01 "的数据,长度为4 = 指针变量的长度,前两位数据为 0094 正好吻合我们程序输出的该变量的地址。所以这三组 DWORD 数据就是我们在全局数组变量之后定义的三个全局指针变量。看来程序编译时是将全局变量放到一起了。程序输出了指针变量的前两个字节,那后边这两个字节是什么呢?这两个字节在三个指针变量中相同,莫非这两个字节代表指针变量的属性?还是其它?

我们用 debug 来加载程序 输入 S 0 FFFF "ChinaPYG" 来定位全局变量



   这时,我们发现 全局指针变量的数值发生了变化:94 00 1E 0D为什么后两个字节发生了变化呢?既然和反编译程序中看到的结果不同,那么后两位肯定不是指针变量的专用属性。那又是为何不同呢?

   我们退出CMD后,再次使用 debug 加载 a.exe ,查看这些全局变量,发现后两位又发生了改变:94 00 05 0D这就太奇怪了,怎么后两位一直变来变去。内存中的地址由段地址:偏移地址构成,莫非后两位保存的是段地址?

我们输入 d0D05:0094 回车,发现在 debug 中指向了字符串 "ChinaPYG" , 看来后两位保存的确实是段地址。



   既然指针变量中保存了数据的段地址,为何输出时只输出偏移地址呢?我们简单的推测一下:全局变量数据保存在程序的数据段,我们从静态分析中可以得出变量的段地址在编译后是定值,而当程序加载到内存之后,偏移值仍是固定的,但数据的段地址和内存环境有关。那此时如果输出段地址结果则可能出现亦同,所以输出地址时屏蔽了后两字节即段地址。

   我们看下边这段代码,我们将字符串的内容作为一个指针变量进行输出:

char a[]="ChinaPYG";
char **b=a;

main()
{
      int i;
      for(i=0;i<4;i++)
      printf("%X",*(a+i));            /*输出 Chin 对应的ASCII码*/
      printf("\n");
      printf("%d \n",sizeof(*b));   /*将内存中的 Chin 作为指针变量 并输出其长度*/
      printf("%X ",*b);               /*输出 该指针变量的ASCII码*/
}

运行结果如图:




    我们将字符串数据作为指针变量传给 b ,测试其长度为4,但输出结果仍是两个字节的内容,可见输出指针变量确实是自动屏蔽后两个字节的内容。

   指针变量的四个字节保存的数据为偏移地址和段地址,没有可以代表这四个字节为指针变量的专有数据,所以指针变量的属性是相同的:前两个字节代表偏移地址,后两个字节代表段地址。不同点在于第三属性(通知编译器如何处理),下面我们通过一个程序谈一下通过指针类型的强制转化来改变指针变量的第三属性:

main()
{
      char *a[]={"Hello","BeiJing2008"};
      char b[]={"ChinaPYG","World!"};
      
      char **c=a;
      char *d=(char *)a;
      char *e=(char *)b;

      printf("%c \n",*(*(c+1)+2));         /*输出a指向的字符串中的第三个字符 */
      printf("%s \n",*((char **)d+1));       /*输出a指向的字符串 */
      printf("%s \n",(char (*))e+1);   /*输出b指向的字符串 */
}

    这个例子中我们将指针型变量强制转化为指向指针的指针和指向二维数组的指针,可见指针变量的第三属性,即其所指向的内存数据的属性全在于我们调用时对其的声明。

思考:既然我们了解了指针的机制,那TC中是否支持多层指针呢,我们可以设计一个游戏来做一个验证:

main()
{
      char *a[]={"china","Beijing2008"};
      char *c=a;
      char *d=&c;
      char *e=&d;
      char *****p=&e;
      
      printf("%s \n",****p);
      printf("%s \n",*(***p+1)+1);
      printf("%c \n",*(*(***p+1)+1));
}

输入结果:
china
eijing2008
e

在程序中使用指针是为了我们的程序执行起来更有效率,课本中没有提及更多层的指针多半是因为使用频率太小了吧。但只要我们对一个问题好奇,就可以通过设计试验来做出验证我们的假设,知识就在自己的手下。


===========================================


第五节 main函数传参


   main 函数也是一个函数,具有函数的基本属性,所以也可以带参数。main 函数带两个参数,一个用来接受参数个数,另一个用于接受一个地址。我们先通过个视图来讲解 main 函数的传参:


地址:                      数据                            ASCII
XX00: 43 3A 5C 41 2C 45 58 45-00 00 00 00 00 00 00 00| C:\A.EXE........程序目录
XX10: 68 65 6C 6C 6F 00 00 00-00 00 00 00 00 00 00 00| hello...........字符串 I
XX20: 63 68 69 6E 61 00 00 00-00 00 00 00 00 00 00 00| china...........字符串 II
XX30:62 65 69 6A 69 6E 67 32-30 30 38 00 00 00 00 00| beijing2008....   字符串 III
……
……
XX60: 70 XX ?? ?? 00 00 00 00-00 00 00 00 00 00 00 00| ................存放指针数组的指针变量p
XX70: 00 XX ?? ?? 10 XX ?? ??-20 XX ?? ?? 30 XX ?? ??| ................指针数组-保存字符串地址的数组
XX80: 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00| ................

    参数 n 来获取参数的个数, 指针 p 用于接收存放字符串的指针数组的地址。了解传参格式后,我们便可以写出使用 main 函数传参的程序:

void main(int n ,char *p[])
{
      int i;
      for(i=0;i<n;i++)
      printf("%s \n",*(p+i));
}




    其实 main 函数就是接收两个参数,一个int型变量,通知参数有多少;另一个存放一个指针变量,指向保存这些字符串的指针数组。我们直接用指向指针的指针来接收参数:

void main(int n ,char **p)
{
      int i;
      for(i=0;i<n;i++)
      printf("%s \n",*(p+i));
}

   上文中我们得出,指针变量占四个字节,前两个字节为程序的偏移地址,后两个字节为段地址。所以定义的指针变量区别仅在于第三属性:变量指向的数据为何类型。我们在第一节中学到:内存中的数据自身是没有属性的,只是在调用时或输出时人为的为其添加或声明了数据类型。即指针变量的第三属性属性我们可以在使用时进行强制转化,其属性取决于调用前对其的声明。所以我们随意定义一个指针变量类接受传参,只是在输出时对其再次声明即可。

void main(int n ,int *p)
{
      int i;
      for(i=0;i<n;i++)
      printf("%s \n",*((char **)p+i));   /* 在这里强制定义指针为 指向指针的指针 */
}

    一个如果我们想输出每个字符串的第一个字符,只需对 printf 语句做如下修改:

printf("%c \n",**((int **)p+i));

    由此可知,指针变量在我们使用时只要修改其第三属性,即通知编译器如何处理就可以随意的使用了。

    我们了解了 main 函数传参的格式后,来写一个通过 main 函数传参可以计算加减法的计算器:

#include<stdlib.h>
#include<string.h>

void main(int n ,int **p)
{
      float a,b;
      if(n!=4)
      {
                printf("Please enter 4 strings!");
                return;
      }
      a=atof(*(p+1));
      b=atof(*(p+3));
      if(!(strcmp(*(p+2),"+")))
      {
                printf("%5.2f + %5.2f = %5.2f \n",a,b,a+b);
                return;
      }
      if(!(strcmp(*(p+2),"-")))
      {
                printf("%5.2f - %5.2f = %5.2f \n",a,b,a-b);
                return;
      }
      printf("What is \'%s\' ? \n",*(p+2));
}

说明两点:
01.将字符串转化为浮点型数据的函数为 atof 包含在库文件 stdlib.h 中。
02.比较字符串的函数为 int strcmp(char *str1,char *str2); 若str1==str2 返回 0 ;strcmp 函数包含在库文件 string.h 中。

    我们写的这个小计算器使用 main 函数传参,带4个参数:第一个为程序路径;第二个和第四个为以字符串形式输入的浮点型数据;第三个为运算符。参数间以空格分开。我们程序的设计关键在于对运算符的判断。上文中我们使用了 strcmp 函数来做判断。当然我们也可以将 **(p+2) 赋值为一个字符变量来做判断,如下例所示:

#include<stdlib.h>
#include<string.h>

void main(int n ,int **p)
{
      float a,b;
      char c;
      if(n!=4)
      {
                printf("Please enter 4 strings!");
                return;
      }
      a=atof(*(p+1));
      b=atof(*(p+3));
      c=**(p+2);
      if(c=='+')
      {
                printf("%5.2f + %5.2f = %5.2f \n",a,b,a+b);
                return;
      }
      if(c=='-')
      {
                printf("%5.2f - %5.2f = %5.2f \n",a,b,a-b);
                return;
      }
      printf("What is \'%s\' ? \n",*(p+2));
}


===========================================


第六节 构造函数指针数组

    在第五节中我们设计了一个可计算加减法的计算器,这一节我们将其功能完善,使其可以计算加减乘除。我们在程序设计中将加减乘除的运算分别写成函数,并保存与一个函数指针数组中,我们知道定义一个指针数组的格式为: char *a[]; 那如何将数组中保存的指针属性扩展为函数地址呢?上文中我们也讲过定义一个指向函数的指针变量的格式为:char (*p)(); 那么我们将两种定义格式想融合是否可以构造出函数指针数组呢,格式应该是 char (*p[])(); 呢还是 char *p[]() 呢?我们来尝试一下:

float add(float *a,float *b){return *a+*b;}
float sub(float *a,float *b){return *a-*b;}
float (*p[])()={add,sub};

main()
{
      float a,b;      
      scanf("%f %f",&a,&b);
      printf("%5.2f + %5.2f = %5.2f \n",a,b,p(&a,&b));
      printf("%5.2f - %5.2f = %5.2f \n",a,b,p(&a,&b));
}

    从上例中我们已经得出定义函数指针数组的格式为:char (*p[])(); 即先定义一个指针数组,然后将其属性进行扩展。下面我们来设计这个程序:

#include<stdlib.h>
#include<string.h>

float add(float *a,float *b){return *a+*b;}
float sub(float *a,float *b){return *a-*b;}
float mul(float *a,float *b){return *a**b;}
float divi(float *a,float *b){return *a/(*b);}

char str[]="+-*/";
float (*f[])()={add,sub,mul,divi};

void main(int n,char **p)
{
      float a,b;
      int find(char *);
      if(n!=4)
      {
                printf("Please enter 4 strings!");
                return;
      }
      a=atof(p);
      b=atof(p);      
      if(find(p)!=-1)
                printf("%5.2f %c %5.2f = %5.2f \n",a,str)],b,f)](&a,&b));
      else
                printf("What is \'%s\'",p);
}

int find(char *c)
{
      int i;
      for(i=0;i<4;i++)
      {
                if( str[ i ] == *c )
                return i;
      }      
      return -1;
}   

    在程序中我们使用了函数指针数组,在定位指针数组元素时又引入了 find 函数来寻址,函数传参均采用了指针变量。在程序设计中,使用指针将有利于提高程序的执行效率。本例大量的使用函数及指针,目的就在于强化大家对指针的认识,我们对指针理解的越深入,在编写程序时就越轻松自如。


===========================================

附录: 关于 main 函数的参数

我们用VC6.0编译以下代码,执行效果如图:

#include <iostream>
using namespace std;
void main(int n ,char *p[])
{
      int i;
      for(i=0;i<n;i++)
      printf("%s \n",*(p+i));
      
      printf("%x\n",p);
      printf("%x\n",&p);
      printf("%x\n",*p);
      printf("%x\n",&(*p));
      
      getchar();
}




    C语言的程序在编译时,main函数将作为程序的一个子函数来执行,所以main函数所带的参数将压栈(全局变量在数据段,局部变量都在栈中)。我们运行程序后,用OD附加,根据程序显示结果来查看内存数据,main函数的指针变量p保存在堆栈中,地址为0012FF8C,该变量指向内存中保存输入各字符串地址的指针数组。

PS: 32位的程序指针保存的就是实际的地址,因为32位的程序可支配的内存足够大,不再使用CS、DS等段寄存器来存放代码和数据。在内存00440E90中,存放98 0E 44 00就是第一个字符串的地址:00440E98。为什么main函数不直接接收各字符串的指针,而是接受一个指向指针数组的指针呢?我们试想一下,main函数的参数作为局部变量是需要压栈的,而我们将输入多少个的字符串是未知的,如果直接压入各字符串的地址显然是不可取的,所以压入一个指向指针数组的指针更为便捷。

main接受传参时先申请一块内存,存放指针数组和输入的各字符串,指针变量p其实就是这个样子的,:

char*a;
char **p=a;
main(int n,char **p)


后记:

    指针运行中的精妙远不止此,本文仅作抛砖引玉,帮大家来理解指针的使用,更多指针的奥妙之处还要靠大家在编程中去体会感受。

===========================================


还要补充一个结构体变量
不用printf 一个分号在屏幕中间输出 HelloWorld!
各种数据结构的源码 (这个放到下一篇好了)

Nisy 发表于 2008-6-24 16:26:51

随笔 写了两三天吧 本来是想自己做个CHM玩的 后来发现 做成网页后怎么也解决不了代码对其的问题 于是让大家看到了论坛上的这个帖子 没啥技术含量 高手勿笑~~

PS:我保存网页后 修改了源码 然后做了一个CHM 奇怪的是第一次打开 图片显示不正常 /:L不清楚为什么

Luckly 发表于 2008-6-24 17:42:36

谢谢兄弟~~~... 是沙发???

puti67 发表于 2008-6-24 18:20:23

学习中,谢谢老大!

iamok 发表于 2008-6-24 18:49:56

学习一下,C语言只会学校讲的一点基础。。。

rabbithua 发表于 2008-6-24 20:49:11

谢谢老大的分享,下来珍藏!
另:把Nisy老大的文章作了个CHM,方便大家收藏学习!

VC8 发表于 2008-6-24 21:23:19

/:013 目前不会

sdprtf 发表于 2008-6-24 22:55:31

谢谢Nisy兄了……

qq500com 发表于 2008-6-25 00:36:17

看不懂/:010 /:010

blitz 发表于 2008-6-25 01:47:32

当你弟弟真幸福了...

呵呵,开玩笑的.

学习了.
页: [1] 2 3 4
查看完整版本: 浅谈 C 指针 [09.04.02更新]o(∩_∩)o...