1 概述
指针为C语言编程提供了强大的支持——如果你能正确而灵活地利用指针,你就可以直接切入问题的核心,或者将程序分割成一个个片断。一个很好地利用了指针的程序会非常高效、简洁和精致。
利用指针你可以将数据写入内存中的任意位置,但是,一旦你的程序中有一个野指针("wild”pointer),即指向一个错误位置的指针,你的数据就危险了——存放在堆中的数据可能会被破坏,用来管理堆的数据结构也可能会被破坏,甚至操作系统的数据也可能会被修改,有时,上述三种破坏情况会同时发生。
此后可能发生的事情取决于这样两点:第一,内存中的数据被破坏的程度有多大;第二,内存中的被破坏的部分还要被使用多少次。在有些情况下,一些函数(可能是内存分配函数、自定义函数或标准库函数)将立即(也可能稍晚一点)无法正常工作。在另外一些情况下,程序可能会终止运行并报告一条出错消息;或者程序可能会挂起;或者程序可能会陷入死循环;或者程序可能会产生错误的结果;或者程序看上去仍在正常运行,因为程序没有遭到本质的破坏。
值得注意的是,即使程序中已经发生了根本性的错误,程序有可能还会运行很长一段时间,然后才有明显的失常表现;或者,在调试时,程序的运行完全正常,只有在用户使用时,它才会失常。
在C语言程序中,任何野指针或越界的数组下标(out-of-bounds array subscript)都可能使系统崩溃。两次释放内存的操作也会导致这种结果。你可能见过一些C程序员编写的程序中有严重的错误,现在你能知道其中的部分原因了。
有些内存分配工具能帮助你发现内存分配中存在的问题,例如漏洞(leak),两次释放一个指针,野指针,越界下标,等等。但这些工具都是不通用的,它们只能在特定的操作系统中使用,甚至只能在特定版本的编译程序中使用。如果你找到了这样一种工具,最好试试看能不能用,因为它能为你节省许多时间,并能提高你的软件的质量。
指针的算术运算是C语言(以及它的衍生体,例如C++)独有的功能。汇编语言允许你对地址进行运算,但这种运算不涉及数据类型。大多数高级语言根本就不允许你对指针进行任何操作,你只能看一看指针指向哪里。
C指针的算术运算类似于街道地址的运算。假设你生活在一个城市中,那里的每一个街区的所有街道都有地址。街道的一侧用连续的偶数作为地址,另一侧用连续的奇数作为地址。如果你想知道River Rd.街道158号北边第5家的地址,你不会把158和5相加,去找163号;你会先将5(你要往前数5家)乘以2(每家之间的地址间距),再和158相加,去找River Rd.街道的168号。同样,如果一个指针指向地址158(十进制数)中的一个两字节短整型值,将该指针加3=5,结果将是一个指向地址168(十进制数)中的短整型值的指针。
街道地址的运算只能在一个特定的街区中进行,同样,指针的算术运算也只能在一个特定的数组中进行。实际上,这并不是一种限制,因为指针的算术运算只有在一个特定的数组中进行才有意义。对指针的算术运算来说,一个数组并不必须是一个数组变量,例如函数malloc()或calloc()的返回值是一个指针,它指向一个在堆中申请到的数组。
指针的说明看起来有些使人感到费解,请看下例:
char *p;
上例中的说明表示,p是一个字符。符号“*”是指针运算符,也称间接引用运算符。当程序间接引用一个指针时,实际上是引用指针所指向的数据。
在大多数计算机中,指针只有一种,但在有些计算机中,指向数据和指向函数的指针可以是不同的,或者指向字节(如char。指针和void *指针)和指向字的指针可以是不同的。这一点对sizeof运算符没有什么影响。但是,有些C程序或程序员认为任何指针都会被存为一个int型的值,或者至少会被存为一个long型的值,这就无法保证了,尤其是在IBM PC兼容机上。
2 间接引用
对已说明的变量来说,变量名就是对变量值的直接引用。对指向变量或内存中的任何对象的指针来说,指针就是对对象值的间接引用。如果p是一个指针,p的值就是其对象的地址;*p表示“使间接引用运算符作用于p”,*p的值就是p所指向的对象的值。
*p是一个左值,和变量一样,只要在*p的右边加上赋值运算符,就可改变*p的值。如果p是一个指向常量的指针,*p就是一个不能修改的左值,即它不能被放到赋值运算符的左边。
3 空指针
有时,在程序中需要使用这样一种指针,它并不指向任何对象,这种指针被称为空指针。空指针的值是NULL,NULL是在中定义的一个宏,它的值和任何有效指针的值都不同。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 # include 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 * / }
|
4 void指针
void指针一般被称为通用指针或泛指针,它是C关于“纯粹地址(raw address)”的一种约定。void指针指向某个对象,但该对象不属于任何类型。请看下例:
int *ip;
void *p;
在上例中,ip指向一个整型值,而p指向的对象不属于任何类型。
在C中,任何时候你都可以用其它类型的指针来代替void指针(在C++中同样可以),或者用void指针来代替其它类型的指针(在C++中需要进行强制转换),并且不需要进行强制转换。例如,你可以把char *类型的指针传递给需要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都仅仅是指向内存中的某个地址的指针。
5 指向函数的指针
在使用指向函数的指针时,最难的一部分工作是说明该指针。例如,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后,你还要将包含进来,并且要把strcmp()函数的地址赋给pf,即:
pf=strcmp;
或
pf=Slstrcmp; /* redundant& */
此后,你就可以通过间接引用pf来调用strcmp()函数:
if(pr(strl,str2)>0) /*...*/
6 指向函数的指针做为函数的参数
函数的指针可以作为一个参数传递给另外一个函数,这一点非常有意思。一个函数用函数指针作参数,意味着这个函数的一部分工作需要通过函数指针调用另外的函数来完成,这被称
为“回调(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
|
本例对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()函数的指针的类型,你都必须小心进行,因为稍有疏忽,就会使程序出错。在实际编程中,转换函数指针的类型更容易使程序出错。
7 数组的大小
数组的大小不能在程序运行时定义。
在数组的定义中,数组的大小必须是编译时可知的,不能是在程序运行时才可知的。
例如,假设i是一个变量,你就不能用i去定义一个数组的大小:char array[i]; /*(notvalidc) */
有些语言支持这种定义,但C语言不支持。如果C语言支持这种定义,栈就会变得更复杂,调用函数的开销就会更大,而程序的运行速度就会明显变慢。
如果数组的大小在编译时是可知的,即使它是一个非常复杂的表达式,只要它在编译时能被计算出来,你就可以定义它。
如果你要使用一个在程序运行时才知道其大小的数组,你可以说明一个指针,并且调用malloc()或calloc()函数从堆中为这个数组分配内存空间。以下是一个拷贝传给main()函数的argv数组的例子:在运行时确定大小的数组,使用了指针和malloc()
* A silly program that copies the argv array and all the pointed-to strings. Just for fun, it also deallocates all the copies. */ # include # include
int main (int argc, char* * argv) { char* * new_argv; int i; /* Since argv[0] through argv [argc] are all valid, the program needs to allocate room for argc + 1 pointers. */ new_argv = (char* * ) calloc(argc + l, sizeof (char * )); / * or malloc ((argc +1) * sizeof (char * ) ) * / printf ("allocated room for %d pointers starting at %P\n", argc + 1, new_argv); /* now copy all the strings themselves (argv[0] through argv[argc-l]) */ for (i = 0;i { / * make room for '\0' at end, too * / new_argv [i]= (char* ) malloc(strlen(argv[i]) + l); strcpy(new_argv[i], argv[i]); printf ("allocated %d bytes for new_argv[%d] at %P", "copied\"%s\"\n", strlen(argv[i]) + l, i, new_argv[i], new_argv[i]) ; } new_ argv [argc] = NULL: /* To deallocate everything, get rid of the strings (in any order), then the array of pointers. If you free the array of poiners first, you lose all reference to the copied strings. */ for (i = 0;i free(new_argv[i]); printf ("freed new_argv[%d] at %P\n" , i, new_argv[i]) ; argv[i]=NULL; /* 习惯,见本例后面的注意 */ } free(new_argv); printf("freed new_argv itself at %P\n",new_argv); return 0; }
|
注意:在释放了new_argv数组中的每个元素之后,还要将这些元素赋值为NULL。这是一种在长期实践的基础上形成的习惯。在释放了一个指针之后,你就无法再使用它原来所指向的数据了,或者说,该指针被“悬挂”起来了,它不再指向任何有用的数据。如果在释放一个指针之后立即将它赋值为NULL,那么,即使程序再次使用该指针,程序也不会出错。当然,程序可能会间接引用这个空指针,但这种错误在调试程序时就能及时被发现。此外, 程序中可能仍然有一些该指针原来的拷贝,它们仍然指向已被释放的那部分内存空间,这种情况在C程序中是很自然的。总之,尽管上述这种习惯并不能解决所有问题,但确实有作用。
8 malloc和calloc的区别
函数malloc()和calloc()都可以用来分配动态内存空间,但两者稍有区别。
malloc()函数有一个参数,即要分配的内存空间的大小:
Void *malloc(size_t size);
calloc()函数有两个参数,分别为元素的数目和每个元素的大小,这两个参数的乘积就是要分配的内存空间的大小:
void *calloc(size_t numElements,size_t sizeOfElement);
如果调用成功,函数malloc()和calloc()都将返回所分配的内存空间的首地址。
malloc()函数和calloc()函数的主要区别是前者不能初始化所分配的内存空间,而后者能。如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之,如果这部分内存空间曾经被分配、释放和重新分配,则其中可能遗留各种各样的数据。也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常运行,但经过一段时间后(内存空间已被重新分配)可能会出现问题。
calloc()函数会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为零;如果你是为指针类型的元素分配内存,那么这些元素通常(但无法保证)会被初始化为空指针;如果你是为实数类型的元素分配内存,那么这些元素可能(只在某些计算机中)会被初始化为浮点型的零。
malloc()函数和calloc()函数的另一点区别是calloc()函数会返回一个由某种对象组成的数组,但malloc()函数只返回一个对象。为了明确是为一个数组分配内存空间,有些程序员会选用calloc()函数。但是,除了是否初始化所分配的内存空间这一点之外,绝大多数程序员认
为以下两种函数调用方式没有区别:
calloc(numElements,sizeOfElement);
malloc(numElements *sizeOfElement);
需要解释的一点是,理论上(按照ANSIC标准)指针的算术运算只能在一个指定的数组中进行,但是在实践中,即使C编译程序或翻译器遵循这种规定,许多C程序还是冲破了这种限制。因此,尽管malloc()函数并不能返回一个数组,它所分配的内存空间仍然能供一个数组使用(对realloc()函数来说同样如此,尽管它也不能返回一个数组)。
总之,当你在calloc()函数和malloc()函数之间作选择时,你只需考虑是否要初始化所分配的内存空间,而不用考虑函数是否能返回一个数组。
9 定义一个大于64k的数组的方法
保守的回答是,为了程序的可移植性,你不能说明这样一个数组。ANSI/ISOC标准规定编译程序能处理的单个对象的大小不得超过(32KB-1)个字节。
为什么64KB是一种上限呢?因为16位指针的最大寻址空间是64KB。
在有些环境中,你可以直接说明一个大于64KB的数组,这种说明是有效的,不会引起任何问题;在另外一些环境中,你不能说明这样大的一个数组,但你可以调用函数malloc()或calloc()从堆中分配一块这样大的内存空间。
在IBMPC兼容机上,这种限制更严格。在这种情况下,你至少要使用大数据存储模式(见本章开头部分的介绍)。此外,你还要调用函数malloc()或calloc()的"far"变体来分配内存空间。例如,为了分配一块70,000字节的缓冲区,在BorlandC和BorlandC++中,你可以使用以下函数调用形式:
far char *buffer=farmalloc(70000L);
在MicrosoftC和MicrosoftC++中,你可以使用以下函数调用形式:
far char *fbuffer=fmalloc(70000L);
注意:70000L尾部的L用来指明该常量是一个long int型常量。一个int型常量的长度为16位,其中还包括一位符号位,因此存不下70,000这个值。
阅读(766) | 评论(0) | 转发(0) |