函数和指针(接前面的指针)

来源:互联网 发布:咸阳三原县广电网络 编辑:程序博客网 时间:2024/06/06 06:29

                                                                 函数和指针

  正像关于声明的讨论中指出的那样, 声明指向函数的指针是可以的. 你可能想知道这样讨厌的家伙有什么用处. 典型的用法是, 一个函数指针可以作为另一个函数的参数, 告诉第二个函数使用哪一个函数. 例如, 对一个数组进行排序涉及到比较两个元素以决定哪个元素放在前面. 如果元素是数字, 可以使用 > 运算符. 更普遍的是, 元素可能是一个字符串或一个结构, 需要一个函数调用来执行比较. C 库里的 qsort() 函数是对任何类型的数组都适用的, 只要告诉它用哪个函数来比较元素. 为此, 它接受一个指向函数的指针来作为一个参数. 然后, 无论数组元素的类型是整数, 字符串或是结构, qsort() 都使用这个函数对元素进行排序.


  我们更进一步介绍函数指针. 首先, 函数指针是什么意思? 假定一个指针指向一个 int 变量, 它保存着这个 int 变量在内存中存储的地址. 同样, 函数也有地址, 这是因为函数的机器语言实现是由载入到内存的代码组成的. 指向函数的指针中保存着函数代码起始处的地址.


  其次, 当声明一个数据指针时, 必须声明它指向的数据的类型. 当声明一个函数指针时, 必须声明它指向的函数类型. 要指定函数类型, 就要指出函数的返回类型以及函数的参量类型. 例如, 考虑以下原型:

void ToUpper (char *);  /* 把字符串转换大写 */

  函数 ToUpper() 的类型是 "具有 char * 类型的参量, 返回类型是 void 的函数 ". 要声明指向这种类型的函数的指针 pf, 可以这样做:

void (*pf) (char *);   /* pf 是一个指向函数的指针 */

 从这个声明中可以看出, 第一对圆括号将运算符 * 和 pf 结合在一起, 这意味着 pf 是一个指向函数的指针. 这就使得 (* pf) 是一个函数, 并使 (char *) 作为该函数的参量列表, void 作为其返回类型. 可能创建这个声明最简单的方法是注意到它用表达式 (*pf) 来代替函数名 ToUpper. 因此, 如果想要声明一个指向某一特定类型函数的指针, 可以声明一个这种特定类型的函数, 然后用一个 (* pf) 形式的表达式来替代函数名, 以创建一个函数指针声明. 就像先前提到过的那样, 由于有运算符优先级的规则, 所以第一个圆括号是必需的. 省略掉圆括号会导致完全不同的情况:

void *pf (char *);     /* pf 是返回一个指针的函数 */

---------------------------------------------------------

PS  提示

  声明一个指向特定函数类型的指针, 首先声明一个该类型的函数, 然后用 (* pf) 形式的表达式代替函数名称; pf 就成为可指向那种类型函数的指针了.


------------------------------------------------------------


   有了函数指针之后, 可以把适当类型的函数的地址赋给它. 在这种场合中, 函数名可以用来表示函数的地址:

void ToUpper (char *);
void ToLower (char *);
int round (double);
void (*pf) (char *);

pf = ToUpper;    /* 合法, ToUpper 是函数 ToUpper() 的地址 */
pf = ToLower;    /* 合法, ToLower 是函数 ToLower() 的地址 */
pf = round;      /* 无效, round 是错误类型的函数 */
pf = ToLower();  /* 无效, ToLwer() 不是地址 */

   最后一种赋值方式也是不正确的, 因为能在一个赋值的语句中使用一个 void 类型的函数. 注意, 指针 pf 可以指向任何接受一个 char * 参数并且返回类型为 void 的函数, 而不能指向具有其他特性的函数.


   正像可以使用一个数据指针来访问数据一样, 也可以使用函数指针来访问函数. 奇怪的是, 有两个逻辑上不一致的语法规则来实现这样的操作, 请看下面的举例说明:

void ToUpper (char *);
void ToLower (char *);
void (*pf) (char *);
char mis[] = "Nina Metier";
pf = ToUpper;
(*pf) (mis);    /* 把 ToUpper 作用于 mis (语法1) */
pf = ToLwer; 
pf (mis);       /* 把 ToLower 作用于 mis (语法2) */


   每种方法听起来都有道理的. 第一种方法: 因为 pf 指向 ToUpper 函数, * pf 就是 ToUpper 函数, 因此表达式 (* pf) (mis) 与 ToUpper (mis) 一样. 从 ToUpper 和 pf 的声明中就能看出 ToUpper 和 (* pf) 是等价的. 第二种方法: 因为函数名是一个指针, 可以互换地使用指针和函数名, 因此 pf (mis) 与 ToLower (mis) 一样. 从 pf 的赋值语句中就能看出 pf 和 ToLower 是等价的. 历史上, 贝尔实验室的 C 和 UNIX 的开发者采用第一种观点, 而 Berkeley 的 UNIX 的扩展者采用第二个种观点.  K&R C 不允许第二种形式. 但是为了保持与现在代码的兼容性, ANSI C 把这二者作为等价形式全部接受.


  正如数据指针最常见的用法之一是作为函数的参数一样, 函数指针最普遍的用法之一也是作为函数的参数. 例如, 考虑以下函数原型:

void show (void (* fp) (char *), char * str);

  这看起来很杂乱, 但它声明了两个参量 fp 和 str. 参量 fp 是一个函数指针, str 是一个数据指针. 更具体一点, fp 指向接受一个 char * 参量且返回类型为 void 的函数, str 指向一个 char 值. 因此, 给定前面的声明, 可以使用像下面这样的函数调用:

show (ToLower, mis);    /* show () 使用 ToLower(0 函数 : fp = ToLower */
show (pf, mis);         /* show () 使用由 pf 指向的函数 fp = pf */

 show() 如何使用传递过来的函数指针呢? 它使用语法 fp() 或 (* fp) () 来调用函数:

void show (void (* fp) (char *), char * str)
{
    (*fp) (str);   /* 把所选函数作用于 str */
    puts (str);    /* 显示结果 */
}

   例如, 这里 show() 首先把 fp 指向的函数作用于字符串 str 来转换 str, 然后显示转换后的字符串.


  顺便提一句, 带有返回值的函数能以两种不同的方式作为其他函数的参数. 例如, 考虑下面的情况:

function1 (sqrt);         /* 传递 sqrt 函数的地址  */
function2 (sqrt (4.0));   /* 传递 sqtr 函数的返回值 */

  第一个语句传递了函数 sqtr() 的地址, functionl(0 可能会在代码中使用该函数. 第二个语句先调用函数 sqtr(), 求出它的值, 然后将返回值 (在本例中是 2.0) 传递给 function2().

 
  为了说明基本概念, 程序清单 14.16 中的程序使用一个以各种各样的转换函数作为参数的 show() 函数. 该程序清单也说明了一些处理菜单的有用的技术.


   程序清单 14.16  func_ptr.c 程序
-----------------------------------------------------------------
/* func_ptr.c  --  使用函数指针 */
#include <stdio.h>
#include <string.h>
#include <ctype.h>

char showmenu (void);
void eatline (void);    /* 读至行末 */
void show (void (* fp) (char *), char * str);
void ToUpper (char *);   /* 把字符串转换为大写 */
void ToLower (char *);   /* 把字符串转换为小写 */
void Transpose (char *); /* 大小写转置 */
void Dummy (char *);     /* 不改变字符串 */

int main (void)
{
    char line[81];
    char copy[81];
    char choice;
    void (*pfun) (char *);   /* 指向一个函数, 该函数接受一个 char * 参数, 并且没有返回值 */

    puts ("Enter a string (empty line to quit) : ");
    while (gets (line) != NULL && line[0] != '\0')
    {
         while ((choice = showmenu ()) != '\n')
         {
               switch (choice)   // switch 语句用来设置指针
               {
                     case 'u':  pfun = ToUpper; break;
                     case 'l':  pfun = ToLower; break;
                     case 't':  pfun = Transpose; break;
                     case 'o':  pfun = Dummy; break;
               }
               strcpy (copy, line);  /* 为 show() 制作一份拷贝 */
               show (pfun, copy);    /* 使用用户选择的函数 */
          }
          puts ("Enter a string (empty line to quit) : ");
     }
     puts ("Bye !");

     return 0;
}

char showmenu (void)
{
   char ans;
   puts ("Enter menu choice :");
   puts ("u) uppercase  l) lowercase ");
   puts ("t) transposed case o) original case");
   puts ("n) next string");
   ans = getchar();       /* 获取用户的响应 */
   ans = tolower (ans);   /* 转换为小写 */
   eatline();             /* 剔除行中剩余部分 */
   while (strchr ("ulton", ans) == NULL)
   {
        puts ("please enter a u, l, t, o, or n :");
        ans = tolower (getchar());
        eatline();
    }
    return ans;
}

void eatline (void)
{
    while (getchar() != '\n')
        continue;
}

void ToUpper (char * str)
{
    while (*str)
    {
         *str = toupper (*str);
         str++;
    }
}

void ToLower (char *str)
{
    while (*str)
    {
        *str = tolower (*str);
         str++;
    }
}

void Transpose (char * str)
{
    while (*str)
    {
        if (islower (*str))
           *str = toupper (*str);
        else if (isupper (*str))
           *str = tolower (*str);
        str++;
    }
}

void Dummy (char * str)
{
    // 不改变字符串
}

void show (void (* fp) (char *), char * str)
{
    (*fp) (str);    /* 把用户选择的函数作用于 str */
    puts (str);     /* 显示结果 */
}

  下面是一个运行示例;

Enter a string (empty line to quit) :
Does C make you feel loopy?
Enter menu choice :
u) uppercase  l) lowercase
t) transposed case o) original case
n) next string
t
dOES c MAKE YOU FEEL LOOPY?
Enter menu choice :
u) uppercase  l) lowercase
t) transposed case o) original case
n) next string
l
does c make you feel loopy?
Enter menu choice :
u) uppercase  l) lowercase
t) transposed case o) original case
n) next string
n
Enter a string (empty line to quit);

Bye!

   注意, 函数 ToUpper(), ToLower(), Transpose() 和 Dummy() 都是相同类型的, 因此 4 个函数都可以赋值给指针 pfun. 这个程序用 pfun 作为 show() 的参数, 但是也可以直接将 4 个函数名称中的任何一个作为参数, 就像 show (Transpose, copy) 一样  .


  在这种情况下你可以使用 typedef. 例如, 示例程序还可以这样做:

typedef void (*V_FP_CHARP) (char *);
void show (V_FP_CHARP fp, char *);
V_FP_CHARP pfun;

  如果你具有探险精神, 你可以声明并初始化一个这类指针的数组:

V_FP_CHARP arpf[4] = {ToUpper, ToLower, Transpose, Dummy};

  然后, 修改函数 showmenu(), 使它是 int 类型的, 并且在用户输入 u 时返回值 0, 键入 1 时返回1, 键入 t 时返回 2 , 等等. 你就可以用下面的语句代替包含 switch 语句的循环:

index  = showmenu();
while (index >= 0 && index <= 3)
{
     strcpy (copy, line);         /* 为 show() 制作一份拷贝 */
     show (arpf[index], copy);    /* 使用用户选择的函数 */
     index = showmenu();
}

   不能拥有一个 "函数的数组", 但可以拥有一个 "函数指针的数组".

   现在你已经了解使用函数名的所有 4 种方法: 定义函数, 声明函数, 调用函数以及作为指针. 图 14.4 总结了这些用法.


  至于处理菜单, 函数 showmenu() 给出了几种技术. 首先, 代码:

  ans = getchar();      /* 获取用户响应 */
  ans = tolower (ans);  /* 转换为小写 */

  和:
 
  ans = tolower (getchar ());

  给出两种方法. 这两种方法都可以将用户的输入转换为一种大小写形式, 这样就不用既检测 'u', 又检测 'U', 等等.

  函数 eatline() 剔除输入行的剩余部分, 这在这两个方面很有用. 第一, 要输入一个选择, 用户键入一个字母, 然后按下回车键, 这将产生一个换行符. 如果不事先去掉这个换行符, 它将作为下一个用户响应被读入. 第二, 假设用户键入整个单独的响应. 有了 eatline(), 程序只处理 u, 并丢弃该输入行的剩余部分.


  其次, showmenu() 函数是设计用来只将正确的选择返回给程序. 为了完成该任务, 程序使用了头文件 string.h 中的标准库函数 strchr():

  while (strchr ("ulton", ans) == NULL)

  这个函数在字符串 "ulton" 中找出字符 ans 首次出现的位置, 并返回一个指向该位置的指针. 如果没找到这个字符, 函数返回空指针. 因此, 上面这个 while 循环判断条件和以下判断条件的作用相同, 但使用起来更方便:

  while (ans != 'u' && ans != 'l' &7 ans != 't' && ans != 'o' && ans !='n')

  需要检查的选择越多使用 strchr() 就会越方便.