该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
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就可以创建可变参数函数;