C语言(Head First C)-8:高级函数:函数指针 qsort() 可变参数函数

来源:互联网 发布:应变数据采集仪 编辑:程序博客网 时间:2024/05/17 23:36

该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!

 8:高级函数:函数指针 qsort() 可变参数函数

 

 基本函数很好用,但有时需要更多功能;

 本章学习:

     如何把函数作为参数传递,从而提高代码的智商;

     学会使用比较器函数排序;

     使用可变参数让代码伸缩自如;

 

 场景:

     过滤某个字符串数组中的数据;

 

 首先用字符串函数过滤数组:

 (Code8_1)

/* * 过滤某个字符串数组中的数据 */#include <stdio.h>#include <string.h>int NUM_ADS = 5;char * ADS[] = {    "Amn",    "Bxyz",    "Cmn",    "Dxyz",    "Emn",};//非常量字符串子串查找函数int findSubstring(char str[],char substr[]){    int lengthstr = strlen(str);    int lengthsubstr = strlen(substr);        int i = 0;    int j = 0;        while (i<lengthstr && j < lengthsubstr) {        if (str[i] == substr[j]) {            i++;            j++;        }else{            i++;        }    }    if (j == (lengthsubstr)) {        return 0;    }else{        return 1;    }    }//非常量字符串子串查找调用void findWord(){    int i;    char word[20];    puts("需要过滤的字符串:");    fgets(word,sizeof(word),stdin);    word[strlen(word) - 1] = '\0';    for(i = 0;i<NUM_ADS;i++){        if(findSubstring(ADS[i],word) == 0){            printf("%s\n",ADS[i]);        }    }}//常量字符串子串查找void find(){    int i;    for(i = 0;i<NUM_ADS;i++){        if(strstr(ADS[i],"A")){            printf("%s\n",ADS[i]);        }    }}int main(int args,char * argv[]){    //    find();    findWord();        return 0;}

 log:

 需要过滤的字符串:

 xyz

 Bxyz

 Dxyz

 

 在这个代码示例中我提供了几个函数:

     之所以自己写了一个查找字符串子串的方法是因为库函数strstr只能用于比较常量字符串;

     所以我们自己写了一个查找字符串子串的通用方法;

 

 和预想的一样,在遍历数组之后,我们找到了匹配的字符串;值得注意的是我们的匹配条件是固定的,当然我们可以通过复制这个函数来修改匹配条件进行使用;

 但,复制函数会产生更多代码;而且,每个函数只能进行一种固定的条件匹配来进行过滤;

 

 我们需要更高端的东西;

 

 把代码传给函数:

     我们可以把测试(条件)代码传给find()函数;如果可以把代码打包传给函数,就相当于传给find()函数一台测试机,函数可以再用测试机测试所有数据;

     这样,find()函数中大部分都可以原封不动;而用传进来的代码进行条件匹配;

 

 把函数名告诉find():

     把原来代码中的搜索条件提取出来,并改写成函数;

 int xy_no_A(char * s){

     return strstr(s,"xy") && !strstr(s,"A");

 }

 

 现在如果能把这个函数作为参数传给find(),就能在find()中进行调用,注入测试;

 如果有这样的方式:那么只要能写一个接受字符串并返回真/假的函数,就可以复用同一个find()函数了;

 

 问题:

     如何在形参中保存函数名?如果你有函数名,又如何用它来调用函数呢?

 

 答案:

     函数名是指向函数的指针:它可以引用存储器中某段代码;

     需要注意的是函数名和指向函数的指针还是有区别的,函数名是L-value(Location),指针变量是R-value(Readout),因此函数名不能像指针变量那样自加或自减;

 

 说明:

     在C语言中,函数名也是指针;当创建一个叫int go_to_warp(int speed)函数的同时,也会创建一个叫go_to_warp的指针变量(在存储器的常量区),变量中保存了函数的地址;只要把函数指针类型的参数传给find(),就能调用它指向的函数了;

     go_to_warp(4);//当调用函数时,你在使用函数指针;

 

 函数指针的语法:

     C中没有函数类型,因为函数的类型不止一种;创建函数时,返回类型或形参列表的变化,都会引起函数类型的变化;

     函数类型是由这些东西组合定义的;

 

 如何创建函数指针:

     对于int go_to_warp(int speed)函数来说,想创建一个指针变量指向这个函数的地址,可以像这样做:

     int (*warp_fn)(int);//创建一个warp_fn变量,保存函数地址;

     warp_fn = go_to_warp;

     warp_fn(4);

 

 之所以这样做,是因为需要把函数的返回类型和接收参数类型告诉C编译器;一旦声明了函数指针变量,就可以向其他变量一样使用它,可以赋值,添加到数组中,或传给函数;

 

注意:char **是一个指针,通常用来指向字符串数组,即指向字符指针的指针;

 

 修改find()函数,重新运行代码:

 (Code8_2)

8_2-find.h

extern int NUM_ADS;extern char * ADS[];//条件提取出来的函数int xy_no_A(char * s);int xy_no_(char * s);void find(int (*func)(char *));
8_2-find.c
/* * 过滤某个字符串数组中的数据 */#include <stdio.h>#include <string.h>int NUM_ADS = 5;char * ADS[] = {    "Amn",    "Bxyz",    "Cmn",    "Dxyz",    "Emn",};int xy_no_A(char * s){    return strstr(s,"xy") && !strstr(s,"A");}int xy_no_B(char * s){    return strstr(s,"xy") && !strstr(s,"B");}void find(int (*func)(char *)){    int i;    for(i = 0;i<NUM_ADS;i++){        if(func(ADS[i])){            printf("%s\n",ADS[i]);        }    }}
8_2-findmain.c
/* * 过滤某个字符串数组中的数据 */#include <stdio.h>#include <string.h>#include "8_2-find.h"int main(int args,char * argv[]){        find(xy_no_A);        return 0;}

 log:

 Bxyz

 Dxyz

 

 我们运行的是 find(xy_no_A);

 这样,find()函数每次就可以运行不同的结构;有了函数指针,就可以吧函数传给函数,用更少的代码创建更强大的程序;

 函数指针是C最强大的特性之一;

 

 小结:

 -函数指针是指针,调用函数时,函数名前面的*可加可不加,两者等价;

 -也可以用&取得函数的地址,也可以不写;

 -即使省略*和&,C编译器也能识别它们,这样的代码更好读;

 

 用C标准库排序:

     排序函数如何才能对任何类型的数据进行排序;

 

用函数指针设置顺序:

     答案是,C标准库的排序函数会接收一个比较器函数指针,用于判断两条数据是大于、小于还是等于;

 

 qsort()函数:

     头文件:#include <stdlib.h>

     qsort(void * array,    //数组指针

         size_t length,     //数组长度

         size_t item_size,  //数组元素长度

         int (*compar)(const void*,const void*));//用来比较数组中两项数据大小的函数指针;

 别忘了,void*指针可以指向任何数据类型;

 

 使用比较器函数,会告诉qsort()两个元素哪个排在前边:

     1)第一个>第二个,返回正数;

     2)第一个<第二个,返回负数;

     3)两值相等,返回0;

 

 int排序聚焦:

     观察qsort()函数接收的比较器函数的签名,会发现它接收两个void*,也就是两个void指针;

     void指针可以保存任何类型数据的地址,但使用前必须把它转换为具体类型;

 比较器函数声明如下:

     int compare_scores(const void * score_a,const void* socre_b);

     值以指针的形式传给函数,因此做的第一件事就是从指针中提取整型值;

 (Code8_3)

/* * 过滤某个字符串数组中的数据 */#include <stdio.h>#include <stdlib.h>#include <string.h>//比较数字int compare_scores(const void * score_a,const void* socre_b){    int a = *(int *)score_a;    int b = *(int *)socre_b;        return a - b;}//比较字符串int compare_names(const void * score_a,const void* socre_b){        char ** a = (char **)score_a;    char ** b = (char **)socre_b;        return strcmp(*a,*b);}int main(int args,char * argv[]){        int scores[] = {9,1,8,2,7,3,6,4,5};    qsort(scores,9,sizeof(int),compare_scores);        for (int i = 0; i<9; i++) {        printf("%i ",scores[i]);    }        printf("\n");        char * name[] = {"af","dw","ee"};    printf("%lu\n",sizeof(char *));    qsort(name,3,sizeof(char *),compare_names);        for (int i = 0; i<3; i++) {        printf("%s ",name[i]);    }        printf("\n");        return 0;}

 log:

 1 2 3 4 5 6 7 8 9

 8

 af dw ee

 

 小结:

     如果用b-a,可以交换最终的排序;

     需要注意在使用前需要对void*类型进行转换;

     别忘了,名字数组是一个字符指针数组,每一项的大小是sizeof(char *);

     qsort()改变了数组元素的顺序,是在原数组上进行的改动;

     字符串数组中的每一项都是字符指针(char *),当qsort()调用比较器函数时,会发送两个指向数组元素的指针,也就是说比较器函数接收的是指向字符指针的指针;

 

 创建函数指针数组:

     如果想在数组中保存函数,必须告诉编译器函数的具体特征:返回类型以及接收什么参数;

     void (*funcs[])(int) = {"函数名1","函数名2",...};

 

 注:C语言的枚举,对应的值是整形数从0开始,依次递增的;

 

 函数指针数组可以配合枚举数组将代码进行简化:

     函数指针数组让代码易于管理,它们让代码变得更短、更易于扩展,从而可以伸缩;

 

 要点:

 -函数指针中保存了函数的地址;

 -函数名其实是函数指针;(注意两者的不同,函数名是L-value,在存储器中不分配变量);

 -如果你有函数shoot(),那么shoot和&shoot都指向了shoot()函数;

 -可以用“返回类型(*变量名)(参数类型)”来声明新的函数指针;

 -如果fp是函数指针,那么可以用fp(参数,...)调用函数;

 -也可以用(*fp)(参数,...),两种都能工作;

 -C标准库(stdlib.h头文件)中有一个qsort()的排序函数;

 -qsort()接收指向比较器函数的指针,比较器函数可以比较两个值的大小;

 -比较器函数接收两个指针,分别指向待排序数组中的两项;

 -如果数据保存在数组中,就可以用函数指针数组将函数与数据项关联起来;

 

 让函数能伸能缩:

     printf()函数可以根据传入的格式化参数进行打印:想打印几个就传递几个;

 

 我们的函数如何能做到?

     现在我们有4种酒(枚举),每种酒的单价,我们可以通过一个price方法来获取price(AWINE)(使用switch-case匹配);

     但如果我们想计算一个酒单上的罗列的酒的总价的话,可以定义一个total函数,然后像这样调用:

     total(3,AWINE,CWINE);//接收的是酒的杯数和名字

     参照后续代码;

 (Code8_4)

 

/* * */#include <stdio.h>enum drink{    AWINE,    BWINE,    CWINE,    DWINE,};double price(enum drink d){    switch (d) {        case AWINE:            return 1.0;            break;        case BWINE:            return 2.0;            break;        case CWINE:            return 3.0;            break;        case DWINE:            return 4.0;            break;        default:            break;    }}int main(int args,char * argv[]){        price(drink.AWINE);        return 0;}

 可变参数函数:

     参数数量可变的函数被称为可变参数函数(variadic function);

     C语言标准库中有一组宏(macro)可以帮助建立自己的可变参数函数;

     你可以把宏想象成一种特殊的函数,他可以修改源代码;

 

 举个例子来说:

 (Code8_5)

/* *  */#include <stdio.h>#include <stdarg.h>//处理可变参数代码的头文件void print_ints(int args,...){    va_list ap;    va_start(ap,args);    int i;    for (i = 0; i<args; i++) {        printf("Argument:%i\n",va_arg(ap,int));    }    va_end(ap);}int main(int args,char * argv[]){        print_ints(3,78,90,100);        return 0;}

 log:

 Argument:78

 Argument:90

 Argument:100

 

 分析:

     可变参数跟在普通参数后边;

     va_start表示可变参数从哪里开始,va_start(ap,args)表示从args参数开始后面都是可变参数;

     args中保存了变量的数量;

 

 过程分解:

 1)包含stdarg.h头文件:

     所有处理可变参数函数的代码都在stdarg.h中;

 2)使用‘...’告诉函数还有更多参数:

     在C语言中,函数参数后的省略号'...'表示还有更多参数;

 3)创建va_list:

     va_list用来保存传给函数的其他参数;

 4)说明可变参数从哪里开始:

     需要把最后一个普通参数的名字告诉C,在这个例子中就是args变量;

 5)然后逐一读取可变参数:

     参数现在全保存在va_list中,可以用va_arg读取他们;

     va_arg接收两个值:va_list和要读取参数的类型;本例中所有参数都是int;

 6)最后,销毁va_list:

     当读完所有参数,要用va_end宏告诉C你做完了;

 7)现在可以调用函数了;

     print_ints(3,78,90,100);

 

 函数与宏:

     宏用来在编译前重写代码,这里的几个宏va_start、va_arg、va_end看起来很像函数,但实际隐藏在它们背后的是一些神秘的指令;在编译前,预处理器会根据这些指令在程序中插入巧妙的代码;

     它们只是被设计成不同函数的样子,预处理会把它们替换成其他代码;

 

 预处理器:

     预处理器在编译之前运行,他会做很多事,包括把头文件包含进代码;

 

 注意:

     我们不能只使用可变参数,而不用普通参数;至少需要一个普通参数,只有这样才能把它的名字传给va_start;

     如果从va_arg中读取比传入函数更多的参数会发生未知错误;以相异类型读取参数,也会发生不确定错误;

 

 现在继续之前的问题:计算一个酒单上的罗列的酒的总价;

 (Code8_6)

/* *  */#include <stdio.h>#include <stdarg.h>enum drink{    AWINE,    BWINE,    CWINE,    DWINE,};double price(enum drink d){    switch (d) {        case AWINE:            return 1.0;            break;        case BWINE:            return 2.0;            break;        case CWINE:            return 3.0;            break;        case DWINE:            return 4.0;            break;        default:            break;    }}double total(int args,...){//    double total = 0;    va_list ap;//    va_start(ap,args);//    int i;    for (i = 0; i<args; i++) {//        total += price(va_arg(ap,enum drink));//    }    va_end(ap);//    return total;}int main(int args,char * argv[]){        printf("%.2f\n", total(4, AWINE, BWINE, CWINE, DWINE));        return 0;}

 log:

 10:00

 

 要点:

 -接收数量可变参数的函数叫可变参数函数;

 -为了创建可变参数函数,需要包含stdarg.h头文件;

 -可变参数将保存在va_list中;

 -可以用va_start()、va_arg()和va_end()控制va_list;

 -至少需要一个普通参数;

 -读取参数时不能超过给出参数个数;

 -需要知道读取参数的类型;

 

 C语言工具箱:

 -有了函数指针就可以把函数当数据传递;

 -每个函数的名字都是一个指向函数的指针;

 -函数指针是唯一不需要加*和&运算符的指针(当然也可以加上);

 -qsort()会排序数组;

 -排序函数接收比较器函数指针;

 -比较器函数决定如何排序两条数据;

 -有了函数指针数组,就可以根据不同类型的数据运行不同的函数;

 -参数数量可变的函数叫“可变参数函数”;

 -包含stdarg.h就可以创建可变参数函数;

 

 

原创粉丝点击