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);
別看這四個函數很小,可是它們在處理列表的時候很是有用,由於它們經過函數指針的方式,把列表的生成,遍歷,篩選,求和都抽象起來了,因此它們能用於許多列表操做裏面去!
函數指針是對解決某一類問題方法的抽象描述,抽象是由於它並不知道它所指向的方法到底是怎麼實現的,它並不能識別賦給他的方法的多樣性,全部的函數都被轉化成函數指針類型,它提供方法的統一接口。只有被調用時,才被具體化了,調用者按照本身的數據類型分配空間,只作一些參數壓棧的工做,被調用者按照本身規定的參數數據類型去解釋這些參數,按照本身的方法去運算。 算法