`
hqs7636
  • 浏览: 215585 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

C 指针 介绍

阅读更多
http://bbs.eccn.com/ecbbs/dispbbs.asp?BoardID=180&id=16566

    指针为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编译程序中使用,有些只能在少数特定的编译程序中使用,还有一些只能在特定的附加函数库的支持下使用。如果你手头有能提供这种技术的软件,你最好看一下它的文档,以了解更详细的信息。

  7.1 什么是间接引用(indirection)?

  对已说明的变量来说,变量名就是对变量值的直接引用。对指向变量或内存中的任何对象的指针来说,指针就是对对象值的间接引用。如果p是一个指针,p的值就是其对象的地址;*p表示“使间接引用运算符作用于p”,*p的值就是p所指向的对象的值。

  *p是一个左值,和变量一样,只要在*p的右边加上赋值运算符,就可改变*p的值。如果p是一个指向常量的指针,*p就是一个不能修改的左值,即它不能被放到赋值运算符的左边,请看下例:

例 7.1 一个间接引用的例子
 #include 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赋值一样。
   请参见:    7.4 什么是指针常量?   7.2 最多可以使用几层指针?
  对这个问题的回答与“指针的层数”所指的意思有关。如果你是指“在说明一个指针时最多可以包含几层间接引用”,答案是“至少可以有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  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要有趣一些。

请参见:  7.1 什么是间接引用(indirection)?   7.3 什么是空指针?

  有时,在程序中需要使用这样一种指针,它并不指向任何对象,这种指针被称为空指针。空指针的值是NULL,NULL是在中定义的一个宏,它的值和任何有效指针的值都不同。NULL是一个纯粹的零,它可能会被强制转换成void*或char*类型。即NULL可能是0,0L或(void*)0等。有些程序员,尤其是C++程序员,更喜欢用0来代替NULL。  指针的值不能是整型值,但空指针是个例外,即空指针的值可以是一个纯粹的零(空指针的值并不必须是一个纯粹的零,但这个值是唯一有用的值。在编译时产生的任意一个表达式,只要它是零,就可以作为空指针的值。在程序运行时,最好不要出现一个为零的整型变量)。
  注意:空指针并不一定会被存为零,见7.10。

  警告:绝对不能间接引用一个空指针,否则,你的程序可能会得到毫无意义的结果,或者得到一个全部是零的值,或者会突然停止运行。  请参见:  7.4 什么时候使用空指针?   7.10 NULL总是等于0吗?    7.24 为什么不能给空指针赋值? 什么是总线错误、内存错误和内存信息转储?   7.4 什么时候使用空指针?

  空指针有以下三种用法:
  (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*/
    /* ... */
  }

  对上述两类函数来说,调用成功或失败时的返回值含义都是不同的。另外一些函数在调用成功时可能会返回一个正值,在调用失败时可能会返回一个零值或负值。因此,当你使用一个函数之前,应该先看一下它的返回值是哪种类型,这样你才能判断函数返回值的含义。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics