`

C语言编程常见问题解答之指针和内存分配

阅读更多
指针为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 <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赋值一样


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 <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要有趣一些。
 

7.3 什么是空指针?

    有时,在程序中需要使用这样一种指针,它并不指向任何对象,这种指针被称为空指针。空指针的值是NULL,NULL是在<stddef.h>中定义的一个宏,它的值和任何有效指针的值都不同。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*/
    /*  ...  */
    }

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

    (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  * /
    }



7.5  什么是void指针?

    void指针一般被称为通用指针或泛指针,它是C关于“纯粹地址(raw address)”的一种约定。void指针指向某个对象,但该对象不属于任何类型。请看下例:
    int    *ip;
    void    *p;
    在上例中,ip指向一个整型值,而p指向的对象不属于任何类型。

    在C中,任何时候你都可以用其它类型的指针来代替void指针(在C++中同样可以),或者用void指针来代替其它类型的指针(在C++中需要进行强制转换),并且不需要进行强制转换。例如,你可以把char *类型的指针传递给需要void指针的函数。

    请参见:
    7.6  什么时候使用void指针?
    7.27 可以对void指针进行算术运算吗?
    15.2 C++和C有什么区别?


7.6 什么时候使用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都仅仅是指向内存中的某个地址的指针。


7.7  两个指针可以相减吗?为什么?

    如果两个指针向同一个数组,它们就可以相减,其为结果为两个指针之间的元素数目。仍以本章开头介绍的街道地址的比喻为例,假设我住在第五大街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.27)。

    例 7.7 指针的算术运算

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

    struct stuff {
    char   name[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  * /
    }


请参见:
    7.8  把一个值加到一个指针上意味着什么?
    7.12 两个指针可以相加吗?为什么?
    7.27 可以对void指针进行算术运算吗?


7.8  把一个值加到一个指针上意味着什么?

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

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

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

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

    请参见:
    7.7两个指针可以相减吗?为什么?
    7.12两个指针可以相加吗,为什么?
    7.27可以对void指针进行算术运算吗?


7.9  NULL总是被定义为0吗?

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


7.10 NULL总是等于0吗?

    对这个问题的回答与“等于”所指的意思有关。如果你是指“与。比较的结果为相等”,例如:
   
if(/*  ...  */)

    {
    p=NULL;
    }
    else
    {
    p=/* something else */;
    }
    /*  ...  */
    if(p==0)

    那么NULL确实总是等于0,这也就是空指针定义的本质所在。

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

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


7.11  用指针作if语句的条件表达式意味著什么?

    当把一个指针作为条件表达式时,所要判断的条件实际上就是“该指针是否为一空指针”。在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程序中,你不必编写这样的代码,但要理解这些代码的作用。


7. 12  两个指针可以相加吗?为什么?

两个指针是不能相加的。仍以街道地址的比喻为例,假设你住在湖滨大道1332号,你的邻居住在湖滨大道1364号,那么1332+1364指的是什么呢?其结果是一个毫无意义的数字。如果你的C程序试图将两个指针相加,编译程序就会发出警告。

    当你试图将一个指针和另外两个指针的差值相加的时候,你很可能会误将其中的两个指针相加,例如,你很可能会使用下述语句:
    p=p+p2-p1;
    上述语句是不正确的,因为它和下述语句完全相同:
    p=(p+p2)-p1;
    正确的语句应该是:
    p=p+(p2-p1);
    对此例来说,使用下述语句更好:
    p+=p2-p1;

   
7.13  怎样使用指向函数的指针?

    在使用指向函数的指针时,最难的一部分工作是说明该指针。例如,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)  /*...*/


7.14  怎样用指向函数的指针作函数的参数?

    函数的指针可以作为一个参数传递给另外一个函数,这一点非常有意思。一个函数用函数指针作参数,意味着这个函数的一部分工作需要通过函数指针调用另外的函数来完成,这被称

    为“回调(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 语言编程常见问题解答.chm

    C语言编程常见问题解答(目录) 第l章 C语言 1. 1 什么是局部程序块(local block)? 1. 2 可以把变量保存在局部程序块中吗? 1. 3 什么时候用一条switch语句比用多条if语句更好? 1. 4 switch语句必须包含...

    C语言常见问题集.pdf

    C语言编程过程中一些常见问题的解答。比如如何决定使用那种整数类型,数据结构、指针、内存分配、预处理器、库函数、字符串、数组、编程风格等

    谭浩强《C程序设计》(第5版)配套题库【考研真题精选+章节题库】

    知识渗透:本书不仅介绍C语言的基本概念和语法,还涉及到一些进阶的内容,如函数指针、动态内存分配、文件操作等,使读者能够深入理解C语言的特性和应用。 综合练习:本书每章末尾都配有大量的习题和编程题,供读者...

    C程序设计 第四版 谭浩强 高清扫描版 带完整书签目录 加 学习辅导

    8.8 动态内存分配与指向它的指针变量 8.8.1 什么是内存的动态分配 8.8.2 怎样建立内存的动态分配 8.8.3 void指针类型 8.9 有关指针的小结 习题 第9章 用户自己建立数据类型 9.1 定义和使用结构体变量 9.1.1 自己建立...

    csdn 翁恺 C 语言程序设计(完) 视频.txt

    P879.2.2 指针运算:动态内存分配 P8810.1.1 字符串:字符串 P8910.1.2 字符串:字符串变量 P9010.1.3 字符串:字符串的输入输出 P9110.1.4 字符串:字符串数组,以及程序参数 P9210.2.1 字符串函数:单字符输入输出...

    寒江独钓-Windows内核安全编程(高清完整版).part1

    阅读本书,需要读者有C语言、数据结构、操作系统和计算机网络的基础知识。 目录: 封面 -25 扉页 -24 内容简介 -23 序 -22 关于本书作者和贡献者 -20 前言 -18 阅读注意 -16 目录 -12 正文 1 第1章 内核上机指导 1...

    寒江独钓-Windows内核安全编程(高清完整版).part4

    阅读本书,需要读者有C语言、数据结构、操作系统和计算机网络的基础知识。 目录: 封面 -25 扉页 -24 内容简介 -23 序 -22 关于本书作者和贡献者 -20 前言 -18 阅读注意 -16 目录 -12 正文 1 第1章 内核上机指导 1...

    寒江独钓-Windows内核安全编程(高清完整版).part7

    阅读本书,需要读者有C语言、数据结构、操作系统和计算机网络的基础知识。 目录: 封面 -25 扉页 -24 内容简介 -23 序 -22 关于本书作者和贡献者 -20 前言 -18 阅读注意 -16 目录 -12 正文 1 第1章 内核上机指导 1...

    寒江独钓-Windows内核安全编程(高清完整版).part5

    阅读本书,需要读者有C语言、数据结构、操作系统和计算机网络的基础知识。 目录: 封面 -25 扉页 -24 内容简介 -23 序 -22 关于本书作者和贡献者 -20 前言 -18 阅读注意 -16 目录 -12 正文 1 第1章 内核上机指导 1...

    寒江独钓-Windows内核安全编程(高清完整版).part6

    阅读本书,需要读者有C语言、数据结构、操作系统和计算机网络的基础知识。 目录: 封面 -25 扉页 -24 内容简介 -23 序 -22 关于本书作者和贡献者 -20 前言 -18 阅读注意 -16 目录 -12 正文 1 第1章 内核上机指导 1...

    寒江独钓-Windows内核安全编程(高清完整版).part3

    阅读本书,需要读者有C语言、数据结构、操作系统和计算机网络的基础知识。 目录: 封面 -25 扉页 -24 内容简介 -23 序 -22 关于本书作者和贡献者 -20 前言 -18 阅读注意 -16 目录 -12 正文 1 第1章 内核上机指导 1...

    C/C++程序员面试指南.杨国祥(带详细书签).pdf

    面试题18:简述C、C++程序编译的内存分配情况 面试题19:以下四段代码中哪段没有错误 第6章 字符串 6.1 数字字符串 面试题1:编码实现数字转化为字符串 面试题2:编码实现字符串转化为数字 6.2 字符串函数 面试题3:...

    Visual C++ 2010入门经典(第5版)--源代码及课后练习答案

     Ivor Horton是撰著Java、C和C++编程语言图书的杰出作家之一。大家一致认为,他的著作独具风格,无论是编程新手,还是经验丰富的编程人员,都很容易理解其内容。在个人实践中,Ivor Horton也是一名系统顾问。他从事...

    寒江独钓-Windows内核安全编程(高清完整版).part2

    阅读本书,需要读者有C语言、数据结构、操作系统和计算机网络的基础知识。 目录: 封面 -25 扉页 -24 内容简介 -23 序 -22 关于本书作者和贡献者 -20 前言 -18 阅读注意 -16 目录 -12 正文 1 第1章 内核上机指导 1...

    Windows内核安全与驱动开发光盘源码

    3.2.1 内存的分配与释放 40 3.2.2 使用LIST_ENTRY 41 3.2.3 使用长长整型数据 43 3.3 自旋锁 44 3.3.1 使用自旋锁 44 3.3.2 在双向链表中使用自旋锁 45 3.3.3 使用队列自旋锁提高性能 46 第4章 文件、注册表...

    Windows内核安全驱动开发(随书光盘)

    3.2.1 内存的分配与释放 40 3.2.2 使用LIST_ENTRY 41 3.2.3 使用长长整型数据 43 3.3 自旋锁 44 3.3.1 使用自旋锁 44 3.3.2 在双向链表中使用自旋锁 45 3.3.3 使用队列自旋锁提高性能 46 第4章 文件、注册表...

Global site tag (gtag.js) - Google Analytics