目录
- 0 简介
- 1 进一步探讨指向指针的指针
- 2 高级声明
- 3 函数指针
- 3.1 回调函数
- 3.2 转移表
- 4 命令行参数
- 4.1 传递命令行参数
- 4.2 处理命令行参数
- 5 字符串常量
- 6 总结
0 简介
众所周知,指针是C语言的灵魂,所以本书(《C和指针》)才会将较多的笔墨放在指针的相关话题上,本章我们将看到更多关于指针的应用,更好地诠释了C语言的独特魅力。
本期内容概览:
1 进一步探讨指向指针的指针
如果学会了指针,也就不难理解指向指针的指针,也就是说,当前指针指向的地址中又存在一个指针,该指针指向的地址中才保存着我们需要访问的数值。
可以通过一个简单的例子验证一下:
#include <stdio.h>
int main()
{
int i = 10;
int *pi = &i;
int **ppi = π
printf("i = %d\n",i);
printf("*pi = %d\n", *pi);
printf("**ppi = %d\n", **ppi);
}
运行,打印输出:
可以看出,与我们预想的结果是一致的。
2 高级声明
书中列举了一些高级指针的声明问题,除非想融会贯通,否则只需要看其余部分就可以,因为其余部分已经列举了具体的声明方法。这部分只是有更深层次的解释而已。
3 函数指针
函数指针,顾名思义,就是指向函数的指针,本质上与普通的指针并没有太大的区别,只是在形式上有一些区别而已,在用法上也更加有意思。
3.1 回调函数
通俗来说,回调函数就把一个函数指针作为参数传递给其他函数,后者将“回调”用户的函数。被当作参数传递的函数就称作回调函数。
书上有这样一段描述:
任何时候,如果你所编写的函数必须能够在不同的时刻执行不同类型的工作或者执行只能由函数调用者定义的工作,你都可以使用这个技巧。许多窗口系统使用回调函数连接多个动作,如拖拽鼠标和点击按钮来指定用户程序中的某个特定函数。
听起来有点绕,具体说来就是,当我们设置好了某个接口(功能),但我们需要的某一部分无法提前预知,这个时候我们就需要回调我们后来好的函数去执行具体的任务,也就是说,需要根据具体情况去考虑具体的实现。
举个例子,在C语言中的stdlib
库中,封装好了一个实现快速排序算法的函数qsort
,在调用的时候,需要我们自己去写排序函数。
快速排序算是一种非常经典且常见的排序算法,对排序算法的具体实现原理感兴趣的请移步:
十大经典排序算法(C语言实现)
里面有非常详细的解释。
我们先实现一个简单的数组排序,
int a[] = {1,3,5,11,2};
qsort(a, 5, sizeof(int), compare);
for (int i = 0; i < 5; i++)
{
printf("a[%d] is %d\n", i, a[i]);
}
比较函数是这样实现的,
int compare(const void *a, const void *b)
{
int *p1 = (int *)a, *p2 = (int *)b;
if (*p1 > *p2)
{
return 1;
}
else if (*p1 == *p2)
{
return 0;
}
else if (*p1 < *p2)
{
return -1;
}
}
既然都封装好了排序函数,为什么不帮我们也写好比较函数呢?
这是因为,排序函数并无法提前预知我们需要比较的数据类型,是整形,浮点型抑或是结构体成员?所以需要我们自己来完成比较函数,好实现排序。书上举的例子也就是这样的思想。当然,回调函数的应用远不止如此。
3.2 转移表
转移表(或者叫转换表)就是一个函数指针数组,这样一来,调用相同类型的函数就变得非常简单,可以直接通过数组下标来找到相关的函数。
假设我们需要设计一个小型的计算器(可以执行加减乘除计算),如果我们采用传统的方法,会写出这样的程序:
enum OPER
{
ADD = 1,
SUB,
MUL,
DIV
};
double cal_add(double a, double b)
{
return a + b;
}
double cal_sub(double a, double b)
{
return a - b;
}
double cal_mul(double a, double b)
{
return a * b;
}
double cal_div(double a, double b)
{
return a / b;
}
//使用传统方法
//使用传统方法
double cal_all_1(int oper, double op1, double op2)
{
double result = 0;
switch (oper)
{
case ADD: result = cal_add(op1, op2); break;
case SUB: result = cal_sub(op1, op2); break;
case MUL: result = cal_mul(op1, op2); break;
case DIV: result = cal_div(op1, op2); break;
default:
break;
}
return result;
}
现在我们采用转移表实现相同的功能:
先定义如下的转移表
double(*oper_func[])(double, double) = { cal_add, cal_sub, cal_mul, cal_div };
现在算法的执行函数就变成了这样:
//使用函数指针数组
double cal_all_2(int oper, double op1, double op2)
{
double result = 0;
if (oper <= 4)
result = oper_func[oper - 1](op1, op2);
else
exit();
return result;
}
显然,第二种方法简洁了很多,想要执行相关的运算,只需要通过数组下标进行索引,然后传入实参即可。下面我们来看主函数的编写:
#include <stdio.h>
#include <stdlib.h>
#include "transfor_table.h"
int main()
{
double res1 = 0,res2 = 0;
res1 = cal_all_1(ADD, 10, 10);
printf("fun_1:10 + 10 = %f\n", res1);
res2 = cal_all_2(ADD, 10, 10);
printf("fun_2:10 + 10 = %f\n", res2);
system("pause");
return 0;
}
执行,打印输出如下的内容:
可以发现,二者执行后产生了相同的结果,只是这样的测试并不严谨,需要更多的测试案例去支撑,为了说明问题,这里只是做了简单比较而已。
注意:除法运算的函数不要写成书中那样(也就是div()),这样会与C语言库中的函数命名冲突,导致编译报错,提示重命名。
4 命令行参数
在有的C程序中,main
函数会比较特殊,其自身也接受两个参数,就像下面这样
int main(int argc, char **argv)
这连个形参接受的就是命令行传过来的参数!
4.1 传递命令行参数
在上述的main函数中,第1个参数称为argc,表示命令行参数的数目,第2个参数称为argv,它指向一组参数值(估计是argument counter
和 argument vector
的缩写)。
这个程序的编译和运行稍微麻烦一些,具体的操作方法可以参考VS使用Developer Command Prompt 命令行编译和执行C++代码
举个例子,编写如下的程序:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
printf("%d\n", argc);
while(*++argv != NULL)
printf("%s\n", *argv);
return EXIT_SUCCESS;
}
4.2 处理命令行参数
编译并运行以上程序,输出如下:
第一个参数是自动统计我们输入字符串参数个数的,显示3
,应该我们点击回车也算一个?然后正确打印出了我们输入的内容。
5 字符串常量
单纯的字符串也代表其第一个元素的指针,这点可以和数组对比,只不过数组是数组名。我们先看个例子:
void string_basic()
{
printf("%c\n",*("xyz" + 1));
printf("%c\n", "xyz"[2]);
}
打印输出
如果"xyz"
是首元素的指针的话,那么+1
表示往后移动一个元素的地址,然后再间接访问,得到第二个元素y
。
当然,与数组类似,字符串也可以通过下标来访问元素,这样一来,就自然而然得到了字符z
。
另外,书上给了个神秘函数,但并未揭晓谜底,这个神秘函数的功能到底是什么。一起来看看。
函数定义如下:
//参数是一个0~100的值
void mystery(int n)
{
n += 5;
n /= 10;
printf("%s\n", "**********" + 10 - n);
}
前两个语句很容易理解,就是将一个整数四舍五入之后再求其十位上的数值。最后一个printf
语句就需要理解字符串常量了,经过分析,打印输出的*号数量就是刚刚计算出来的十位上的数值大小。可以做个实验:
for(int i = 0; i < 10; i++)
mystery(i*10);
则打印输出:
若不是10的整数倍,也可以得到类似的输出,处理方法都是一样的。
书中最后一部分又增加了一个例子,将二进制值转换为字符串,一起来看看:
void binary_to_ascii(unsigned int value)
{
unsigned int quotient;
quotient = value / 10;
if (quotient != 0)
binary_to_ascii(quotient);
putchar(value % 10 + '0');
}
注意这里用了递归,并不是很难理解,在主函数中这样调用:
binary_to_ascii(1001100);
打印输出:
可见我们的算法没有问题。
书中随后又给了将16进制的某位转化成字符串的方法:
putchar("0123456789ABCDEF"[value % 16]);
同样地,我们可以仿照上述例子,写一个将16进制数转化为字符串的函数:
void hexadecimal_to_ascii(unsigned int value)
{
unsigned int quotient;
quotient = value / 16;
if (quotient != 0)
hexadecimal_to_ascii(quotient);
putchar("0123456789ABCDEF"[value % 16]);
}
然后在主函数中调用:
hexadecimal_to_ascii(0xF5);
打印输出:
可以看出,我们得到了正确的答案!
6 总结
回调函数和转移表,并不仅仅是理解其工作过程和主要思想,更重要的是知道如何在实际开发中运用它。
如果某个执行环境实现了命令行参数,这些参数是通过两个形参传递给main函数的。这两个形参通常是称为argc
和argv
。
拓展阅读:指针函数和函数指针