1、何为抽象?
从小到大,咱们接触到的抽象,最熟悉的莫过于数学了。为何这样说呢?
好比说,在小学的时候,老师老是拿了几个苹果来引诱咱们:同窗们,这里有几个苹果啊?因而咱们流着口水一个个地数,一个苹果,两个苹果,三个苹果,而后说三个苹果!第二回,老师又拿了三只葡萄来引诱咱们:同窗们,这里有几只葡萄啊?因而咱们又一只只数过来:一只葡萄二只葡萄三只葡萄,三只葡萄!第三天,老师又拿了三颗糖果来问咱们:同窗们,这里有几颗糖果啊?咱们又数开了:一颗糖果,两颗糖果,三颗糖果!回答:三颗糖果。
每一次老师拿了不一样的东西来的时候,咱们都要从头至尾数一次:一个苹果,两个苹果,三个苹果……
稍稍长大了一些后,老师再拿了三个雪梨来问咱们:同窗们,这里有几个雪梨啊?这回咱们学精了,不用一个雪梨二个雪梨地数,咱们直接数:一二三,一共三个。
请注意到最后一次数雪梨的时候,咱们不是数多少个雪梨,咱们只是在数一二三,并无说一个雪梨二个雪梨三个雪梨,换句话说,咱们并非在数讲台上的雪梨,咱们只是在数一二三。而得出来结果倒是正确的。为何呢?
咱们来分析一下。每次老师叫咱们数的东西都是不一样的,苹果,葡萄,糖果,雪梨。它们之间好像没有什么关系。而每次咱们都能得出一个相同的结果:三。
这个“三”是何方神圣,能把这几堆风马牛不相及的东西联系起来?这几堆东西之间有什么共性是隐藏在它们各不相同的是外表之下的呢?
答案你们都知道了,这个共性就是“数量”,这几堆东西的数量,是它们惟一相同的地方。而这个“三”,就是用来刻划这一个共性——数量的。这几堆东西的数量是相同的,所以,这个“三”也就同时刻划了这三堆东西的数量这个共性。
这个例子很简单,道理也很明显,可是倒是咱们在数学上所作的第一次抽象!
至此,咱们不妨这样诠释抽象的含义:抽象就是对某类事物的共同特征的刻划。
什么叫“共同特征”?在上面的例子中,“数量为三”就是那四堆东西的共同特征,把它抽象出来,就是一个“三”。当咱们认识了一个苹果后,之后再看到苹果,就知道它是苹果,由于这两个苹果有共同的特征,咱们把它抽象出来,造成一个“苹果”的概念,之后再遇到苹果,把这个实在的苹果跟抽象后的“苹果”一对照,就能知道那个是否是真的苹果。抽象的威力就是,把事物的共同特征刻划出来以后,就能把它应用到这一类事物中,而没必要去对付这一类事物的具体的个体了。
把这个“三”抽象出来后,咱们数苹果就方便多了,咱们只须要数“一二三”,而后在结果后面接上苹果葡萄糖果雪梨后,咱们就知道那里有三个苹果三个葡萄三颗糖果三个雪梨。
请注意把“三”抽象出来先后,咱们数苹果方法的变化。而抽象以前,咱们是对着一个苹果一个苹果地数,数完后一个葡萄一个葡萄地数,数苹果的方法不能用在葡萄上,而数葡萄的方法不能用在数苹果上。每数同样东西,咱们就要学习一种数数的方法,多不方便。
而咱们把“三”抽象出来后,咱们数苹果的方法就成了数“一二三”,结果后面接上“个苹果”,数葡萄的方法就成了数“一二三”,结果后面接上“个葡萄”,数苹果的方法稍做修改就能够应用到数葡萄上。咱们只学会了一种数数方法,却能够用它来数各类各样的东西,包括香蕉,椰子,桔子等等等等的东西,这就是抽象的威力。
同时,对这个“三”咱们也能够发出这样的疑问:这个“三”是什么?这个“三”是那几堆东西各自的数量吗?不是,它不是那几堆东西各自的数量,它只是一个汉字,一个代号,3也能够叫“三”,III也能够叫“三”。把这个数量拿走之后,“三”,又是什么?(武林外传看多了……)
其实,“三”的确也不是什么,它只是一个刻划,在世界上找不到任何一个具体的东西能够直接对应到“三”的。咱们运算的只是一个抽象的东西,得出一个抽象的结果,再把这个抽象的结果从新映射到具体的事物中去。咱们处理事物的方法和具体的事物分离开来了!
小学里咱们学习的抽象,主要是把数量抽象出来,并用具体数字来表现它们。对这些具体数字的运算,就表明了对这些具体的个体的运算。所以,小学的数学又叫“算术”,它是对具体的数进行“运算”的“技术”。而到了初中,咱们学会了用变量来代替数字,从而把某一类运算抽象出来。
好比说求长方形的面积。在小学的时候,咱们只会求那些长宽都是给定的“具体的”(这里这个“具体的”意思不是说它们能够在世界上找到对应物,什么意思?观众本身琢磨去吧,呵呵)数的长方形的面积,而上初中后,咱们学会了用字母来表示数字,因而咱们学会了任何一个长方形的面积,学会了用S=ab来表示一个长方形的面积。
再好比说说路程的长度。小学的时候,咱们只会求速度与时间都是给定的“具体的”数的那种运动的路长度,而初中的时候,咱们就学会了用S=vt来表示各类各样的运动的路程长度。
这又是一次进步!咱们在已经抽象过的数字上面又进行了一次抽象。此次抽象中,咱们看到了,其实求长方形的面积跟求路程的长度,本质上是同样的!因而咱们又“无师自通”地学会那种具备“结果与两个变量成正比例”的特征的问题的解法,把这个“结果与两个变量成正比例”的特征抽象出来,就是S=ab这个等式。
中学的时候遇到的时候抽象,主要是用字母来代替数字,从而把数量之间的关系与具体的数字分离开来,这种数量之间的关系,就是数学中的“函数”了。中学的数字又叫“代数”。
上到大学,咱们学到的数学,则是更高层次的抽象。大学的数学叫“数学分析”,是研究函数之间的关系,用f(x)来表明一个函数,正是对各类函数之间的共同特征的刻划。
好比说,一个有界函数函数在某个有限区间上是否可积,就要看它在这个有限区间上是否到处连续。具体这个函数是什么,咱们不用理会。只要它知足这个条件,那它就是可积的。
上面说了一堆,就是要说明,抽象的威力,就是在于让咱们能够专心处理某类事件的共同特征自己,而不用关系具体的个体。
2、C语言中的抽象
经过上面的分析,咱们知道了抽象的威力。而对于一个程序语言来讲,它的能力大小取决于它对具体事物的抽象能力。一种语言抽象的能力越大,它能对事物的描述就越本质。
有些观众可能会以为,上面所说的抽象,好像跟程序设计中的某些原则有些相似。其实这很正常,程序设计原本就是从数学中来的,数学中的某些思想能在程序设计中获得体现也不奇怪。只是可能某种思想在某些语言中体现得比较明显面而另外一些思想则体如今其它语言罢了。
而在C语言中,也具备三个层次的抽象能力,正好与上面所说的三个抽象层次相对应。
其一,在C语言中存在各类数据类型,能够对现实中的数量进行映射。这是第一个层次的抽象。若是没有这个抽象能力,那基本上这个语言就没有什么用了。一个例子是HTML,这个语言实际上不算编程语言,由于单靠它本身,连1+1都不能计算。由于它缺乏表示数字的机制。它只能用于标记,属于一个标记符号。
其二,在C语言中能够定义函数,与代数中的函数有相似之处。可让咱们以相同的方法处理那些具备相同逻辑特征的运算。
好比说,咱们要计算1,9,10,111的平方,咱们固然能够这样写:
#include <stdio.h>
int main(void)
{
printf("%d\n", 1 * 1);
printf("%d\n", 9 * 9);
printf("%d\n", 10 * 10);
printf("%d\n", 111 * 111);
return 0;
}
可是这样写,并无把“平方”这个共同的概念表示出来,因而咱们在学习了C语言中的函数后,咱们会把x * x这个模式抽象出来,把程序写成下面的样子:
#include <stdio.h>
int square(int x)
{
return x * x ;
}
int main(void)
{
printf("%d\n", square(1));
printf("%d\n", square(9));
printf("%d\n", square(10));
printf("%d\n", square(111));
return 0;
}
或者更简单的:
#include <stdio.h>
int square(int x)
{
return x * x ;
}
int print_int_square(int x)
{
return printf("%d\n", square(x));
}
int main(void)
{
print_int_square(1);
print_int_square(9);
print_int_square(10);
print_int_square(111);
return 0;
}
在第一个程序中,咱们直接运算了1,9,10,111的平方,把它们打印出来。在第二个程序中,咱们把这个“平方运算”抽象成函数square(),这下,咱们不只能够计算1,9,10,111的平方了,还能够计算任何一个整型数的平方。换句话说,square()不理会这个数是什么,只要求它是个整型数。在第三个程序中,咱们是把打印语句也放在一个函数中,不过这并无什么本质的不一样。
其三,C语言中一个很是重要的特性,让它具备刻划第三个抽象层次的能力,这就是函数指针。
回头看上面第三个程序,咱们为何必定要让它打印一个数的平方呢?要是能让它在调用的时候再决定打印什么这不是更好吗?这个函数不是更通用吗?
因而咱们写出第三个程序:
#include <stdio.h>
typedef int(*F_T)(int);
int square(int x)
{
return x * x ;
}
int print_int_fun(int x, F_T fun)
{
return printf("%d\n", fun(x));
}
int main(void)
{
print_int_fun(1, square);
print_int_fun(9, square);
print_int_fun(10, square);
print_int_fun(111, square);
return 0;
}
如今,print_int_fun()不关心这个数是什么,也不关心打印这个数的什么“亲戚朋友”了。它只管打印。
咱们如今能够定义一个函数来计算一个整型数的立方,而且把它传递给这个print_int_fun(),就能够打印出这个数的立方了。
换句话说,print_int_fun()不只处理变量,同时也处理函数,它具有了第三层抽象的能力。
3、主角——函数指针
使C语言具有第三层抽象能力的,是C语言中的函数指针。使用函数指针,咱们能够实现模拟把函数做为一个参数传递进另外一个函数中以供后者调用,使得调用者有一种模板的性质。
做为一个练习,观众们不妨看一下下面几个函数的做用:
T* map(T (*fun)(T), T arr[], int len)
{
for (int i = 0; i < len; ++i)
{
arr[i] = fun(arr[i]);
}
return arr;
}
R reduce(R (*fun)(R, T), R init, T arr[], int len)
{
R res = init; // 最终要返回的结果
for (int i = 0; i < len; ++i)
{
res = fun(res, arr[i]);
}
return res;
}
int filter(bool (*fun)(T), T arr[], int len)
{
int res = 0; // 在arr中能使fun()返回真值的元素个数
for (int i = 0; i < len; ++i)
{
if (fun(arr[i]))
{
arr[res++] = arr[i];
}
}
return res;
}
T* range(T arr[], T init, int len)
{
for (int i = 0; i < len; ++i)
{
arr[i] = init + i;
}
return arr;
}
在C++的STL中和Python,它们有对应的泛型算法,可是名字有些不一样。在这里,我更喜欢用Python的术语,由于我不懂C++。-_-!
这四个函数都很简单易懂,这里就不做解释了。
4、例子
若是咱们要写一个程序来计算1到100的各个数的和,咱们可能会这样写:
int res = 0;
for (int i = 1; i <= 100; i++)
{
res += i;
}
printf("%d\n", res);
若是咱们想要计算1到100之间的数的平方和的话,咱们可能会这样写:
int res = 0;
for (int i = 1; i <= 100; i++)
{
res += i * i;
}
printf("%d\n", res);
若是要计算1/(1*3) + 1/(5*7) + 1/(9*11) + ... + 1/(397 * 399),咱们可能会这样写:
double res = 0.0;
for (int i = 1; i <= 100; i++)
{
res += 1.0 / ((4 * i - 3) * (4 * i - 1));
}
printf("%lf\n", res);
若是要计算((2 * 4) / (3 * 3)) * ((4 * 6) / (5 * 5)) * ... * ((22 * 24) / (23 * 23)),咱们可能会这样写:
double res = 1.0;
for (int i = 1; i <= 10; i++)
{
res += ((2.0 * i) * (2.0 * i + 2.0)) / ((2.0 * i + 1) * (2.0 * i + 1));
}
printf("%lf\n", res);
很明显,这四个程序具备相同的结构,只是在如下几个方面不一样:结果的初值不一样,每次的增量不一样,增长增量的方法不一样,项数长度不一样。若是咱们把这四个不一样给提取出来做为参数,则能够把这个四个程序合并为一个:
#include <stdio.h>
double delta1(double n)
{
return n;
}
double delta2(double n)
{
return n * n;
}
double delta3(double n)
{
return 1.0 / ((4 * n - 3) * (4 * n - 1));
}
double delta4(double n)
{
return (((2 * n) * (2 * n + 2)) / ((2 * n + 1) * (2 * n + 1)));
}
double add(double x, double y)
{
return x + y;
}
double multi(double x, double y)
{
return x * y;
}
double sum(double (*fun)(double), double init, int len, double (*attach)(double, double))
{
double res = init;
for (int i = 1; i <= len; i++)
{
res = attach(res, fun(i));
}
return res;
}
int main(void)
{
double res1 = sum(delta1, 0.0, 100, add);
printf("%lf\n", res1);
double res2 = sum(delta2, 0.0, 100, add);
printf("%lf\n", res2);
double res3 = sum(delta3, 0.0, 100, add);
printf("%lf\n", res3);
double res4 = sum(delta4, 1.0, 10, multi);
printf("%lf\n", res4);
return 0;
}
这样是否是简单不少?
计算一个数的阶乘的程序你们都写过,可是你们写过这样的阶乘程序没有?
reduce(multi, 1, range(arr, 1, LEN), LEN);
其中,multi是把两个整型相乘的函数,arr是一个整型数组,LEN是它的长度,为10。而若是要计算1到1000的数中,平方数的个位数为4的数的立方和,则能够这样写:
int len = filter(square_end_with_4, range(arr, 1, 1000));
R res = reduce(add, 0.0, map(cube, arr, len), len);
利用上面的map(),reduce(),filter(),range()函数也能够把上面的程序改写:
int main(void)
{
#define LEN 100
R arr[LEN];
R res1 = reduce(add, 0.0, range(arr, 1.0, LEN), LEN);
printf("%lf\n", res1);
R res2 = reduce(add, 0.0, map(delta2, range(arr, 1.0, LEN), LEN), LEN);
printf("%lf\n", res2);
R res3 = reduce(add, 0.0, map(delta3, range(arr, 1.0, LEN), LEN), LEN);
printf("%lf\n", res3);
R res4 = reduce(multi, 1.0, map(delta4, range(arr, 1.0, 10), 10), 10);
printf("%lf\n", res4);
return 0;
}
最后,以一个求二叉树中的子结点的例子来结束这篇文章。
若是把二叉树的类型定义为Tree,而且定义Tree*的类型为PTree,而且已经定义好如下几个函数:
// 创建二叉树结点
PTree make_tree(PTree t, PTree l, PTree r, int val)
{
t->value = val;
t->left = l;
t->right = r;
return t;
}
// 得到二叉树的左子树
PTree get_left(PTree t)
{
return t == NULL ? NULL : t->left;
}
// 得到二叉树的右子树
PTree get_right(PTree t)
{
return t == NULL ? NULL : t->right;
}
// 得到二叉树的结点值
int get_value(PTree t)
{
return t == NULL ? -1 : t->value;
}
假如咱们以结点N来表示根结点的右子树的右子树的左子树的右子树的左子树的左子树这个结点,计算N的结点的值,若是N不存在则返回-1。咱们会怎么计算呢?
有些人可能会这样计算:
PTree n = root;
if (n->right)
{
n = root->right;
if (n != NULL)
{
n = root->right;
....
}
}
return get_value(n);
这个办法虽然可行,可是很笨重。或者有人会这样计算:
typedef PTree (*F_T)(PTree);
PTree n = root;
F_T arr[6] = {get_right, get_right, get_left, get_right, get_left, get_left};
for (int i = 0; i < 6; i++)
{
n = arr[i](n);
}
return get_value(n);
可是若是换了是我,我会这样计算:
typedef R PTree*;
typedef PTree (*T)(PTree);
R cat(R res, T n)
{
return n(res);
}
F_T fs[LEN] = {get_right, get_right, get_left, get_right, get_left, get_left};
Tree res = reduce(cat, &t01, fs, LEN);
return get_value(res);
别看这四个函数很小,可是它们在处理列表的时候很是有用,由于它们经过函数指针的方式,把列表的生成,遍历,筛选,求和都抽象起来了,因此它们能用于许多列表操做里面去!
函数指针是对解决某一类问题方法的抽象描述,抽象是由于它并不知道它所指向的方法到底是怎么实现的,它并不能识别赋给他的方法的多样性,全部的函数都被转化成函数指针类型,它提供方法的统一接口。只有被调用时,才被具体化了,调用者按照本身的数据类型分配空间,只作一些参数压栈的工做,被调用者按照本身规定的参数数据类型去解释这些参数,按照本身的方法去运算。 算法