學習和使用
C++太復雜了。我猜想你一定是在計算機屏幕前不住地點頭。其實我也在使勁的點頭。沒錯,C++著實復雜,大概是迄今為止最復雜的語言。於是,便產生了一個推論:C++難學難用。
這句話也相當准確,但並非所有時候都這樣。C++的學習和使用可以分為若干個層面,這些層面由簡到繁,由易到難,分別適合不同層次的學習者和使用者。
先讓我們來看看C++究竟有多復雜。我大致列了個清單,包括C++的主要語言特性。基本的一些特性,象變量、條件、選擇等等,都掠去了,主要集中在體現C++復雜性的方面:
1.指針。包括變量指針、函數指針等。可以計算。
2.引用。包括變量的引用、函數的引用等。
3.自由函數。
4.類。包括具體類和抽象類。
5.重載。包括函數重載和操作符重載。
6.成員數據。包括static和非static的。
7.成員函數。包括static和非static的。
8.虛函數。包括對虛函數的override。
9.繼承。包括多繼承、虛繼承等。
10.多態。包括動多態和靜多態。
11.類型轉換。包括隱式的和顯式的,以及臭名昭著的強制轉換。
12.模板。包括類模板和函數模板。
13.模板特化。包括完全特化和部分特化。
14.非類型模板參數。
15.模板-模板參數。
我對照了C#的特性清單。C++比C#多的特性主要是:1、2、3、9的多繼承、10的靜多態、13、14、15。
而C#比C++多的特性主要有(這裡沒有考慮C++/CLI,C++/CLI補充了標准C++):
1.for each關鍵字。
2.as、is關鍵字。
3.seal關鍵字。
4.abstract關鍵字。
5.override/new關鍵字。
6.using關鍵字的一些補充用法。
7.implicit/explicit關鍵字。(C++有explicit關鍵字,但只用於構造函數)
8.interface關鍵字。
9.property。
10.delegate。
我們來分析一下。C++比C#所多的特性集中在編程技術方面,特別是編程模式,如由模板帶來泛型編程和元編程機能。在OOP方面,C++走得更遠,更徹底,主要表現在多繼承和操作符重載方面。在傳統的編程技術上,C++區分了對象和對象的引用(指針和引用)。而C#所有引用對象所持的都是引用。最後,C++擁有自由函數。
反過來再看C#,C#所多的都是些關鍵字。這些關鍵字都集中在OOP方面,並且都是以使編程技術易於理解為目的的。很多人(包括我在內)都希望C++也擁有C#的這些關鍵字(至少部分)。但與一般人的理解不同,C++標准委員會實際上是被編譯器開發商所把持著,他們對引入關鍵字尤其神經過敏。沒有辦法,現實總是有缺憾的。
從這些方面可以看出,C++在編程機制上遠遠多於C#(Java的機制甚至更少)。對於新入行的人而言,一口氣吞下這些內容,足以把他們撐死。相反,C#增加的關鍵字有助於初學者理解代碼的含義。這些就是C#和Java比C++易於學習(易於理解)的真正原因。
但是,必須要強調的是,C#和Java中易於學習和理解的是代碼,而不是這些代碼背後的技術原理和背景。
我看到過的絕大多數C#代碼都充滿了重復代碼和大量switch操作分派。如果這些程序員充分利用C#和Java的OOP機制,這些嚴重的代碼冗余可以消除一半。(如果C#和Java具備C++那樣的泛型編程能力,則另一半也可以消除。)這些程序員都是在沒有充分理解語言機制和OOP技術的情況下編寫軟件,事倍功半。
這種情況在C++也有發生,但相對少些。這大概是因為C++足夠復雜,使得學習者產生了“不徹底理解C++就學不會C++,就用不了C++”的想法。這種想法有利有弊,利在於促使學習者充分理解語言和語言背後的技術,而弊在於它嚇跑了很多人。實際上,我們一會兒就會看到,C++可以同C#和Java一樣,可以在不理解其中原理的情況下,僅僅按照既定規則編程。當然我們不希望這樣,這是不好的做法。但鑒於現在業界
的浮躁心態,我們也就入鄉隨俗吧。
注意了!下面這句話是最關鍵的,最重要的,也是被長期忽略的:C++之所以復雜,是為了使用起來更簡單。聽不明白!自相矛盾!胡說!別急,且聽我慢慢道來。
(限於篇幅,我這裡只給出最後一部分案例代碼,完整的案例在我的blog裡:)
有三個容器,c1、c2、c3。容器的類型和元素的類型都未知。要求寫一個算法框架,把c1裡的元素同c2裡的元素進行某種運算,結果放到c3裡。
由於容器類型未知,必須使用所有容器公共的接口。所以,我寫下了如下的C#代碼:
public delegate void alg<T1, T2, R>(T1 v1, T2 v2, R r);
public static void Caculate_Contain<C1, T1, C2, T2, C3, T3>
(C1 c1, C2 c2, C3 c3, alg<T1, T2, T3> a )
where C1: IEnumerable<T1>
where C2 : IEnumerable<T2>
where C3 : IEnumerable<T3>
{
IEnumerator<T1> eai1 = c1.GetEnumerator();
IEnumerator<T2> eai2 = c2.GetEnumerator();
IEnumerator<T3> eai3 = c3.GetEnumerator();
while (eai1.MoveNext() && eai2.MoveNext() && eai3.MoveNext())
{
a(eai1.Current, eai2.Current,eai3.Current);
}
}
//使用
public static void CaculThem(int v1, int v2,int r) {
r=v1*v2;
}
Caculate_Contain(ai1, ai2, ai3, new alg<int, int, int>(CaculThem));
public static void CaculThem2(float v1, int v2,double r) {
r=v1*v2;
}
Caculate_Contain(af1, ai2, ad3, new alg<float, int, double>(CaculThem2));
我使用了一個委托,作為傳遞處理容器元素的算法的載體。使用時,用具體的算法創建委托的實例。但具體的算法CaculThem()必須同相應的容器元素類型一致。
下面輪到C++:
template<typename C1, typename C2, typename C3, typename Alg>
Caculate_Container(const C1& c1, const C2& c2, C3& c3, Alg a)
{
transform(c1.begin(), c1.end(), c2.begin(), c3.begin(), a);
}
//使用
template<typename T1, typename T2, typename R>
R mul_them(T1 v,T2 u) {
returnv*u;
}
Caculate_Container(ai1, ai2, ai3, mul_them<int, int, int>);
Caculate_Container(af1, ad2, ad3, mul_them<float, double, double>);
如果容器元素有所變化,C#代碼必須重寫算法CaculThem()。但C++不需要,由於mul_them<>()本身是個函數模板,那麼只需將這個函數模板用新的類型實例化一下即可。
C++的代碼相對簡單些,靈活性也更高些。但這還不是全部,C++還有一個最終極的解法,不需要循環,不需要創建模板算法,不需要寫操作函數:
transform(c1.begin(), c1.end(), c2.begin(), c3.begin(), _1*_2);
沒看明白?我一開始也看不明白。這裡用到了boost庫的Lambda表達式。_1占位符對應c1的元素,_2的占位符對應c2的元素,_1*_2表示才c1的元素乘上c2的元素,其結果放在c3裡。表達式可以寫得更復雜,比如(_1*_2+3*_1)/(_1-_2)。Lambda表達式可以用在所有需要操作的算法中,比如我要去掉字符串中的“-”,可以這樣寫:
remove_if(s.begin(), s.end(), _1==’-’);
Lambda表達式基於一種叫做“模板表達式”的技術,通過操作符重載,將一個表達式一層一層地展開,構成一個解析樹。然後作為一個函數對象傳遞給算法,算法在循環內調用函數對象,執行相應的計算。
沒有比這更簡單的了吧。原理是夠復雜的,但我們可以完全不理睬其中復雜的原理,只管用就是了。別看只是一個小小的算法,要知道,再龐大的軟件(象JSF的代碼有1900萬行之多)都是由這些渺小的算法構成的。C++提供的算法和簡化算法使用的庫幾乎對所有的程序算法都有幫助,不僅僅對這種底層算法有效,在更高層次的算法作用更大。
這裡我就不再給出C#的代碼了,因為C#還不支持Lambda表達式,也無法模擬。如果想要的話,等C#3.0吧。
好了,應該是做小結的時候了。從上面的這些例子可以看出,在最基本的語句上,C#有時比C++簡單些,因為C#提供了更多的關鍵字。但是,隨著算法的逐步復雜,C++的抽象能力漸漸發揮作用。一旦需要建立抽象的算法和代碼時,C++的泛型編程能力立刻爆發出巨大的能量。最後,我們利用boost::lambda庫最大限度簡化了算法的使用。更重要的,Lambda表達式的實現極其復雜,但是使用卻異常簡單。
這便是開頭所說的:“C++之所以復雜,是為了使用起來更簡單”這句話的含義。C++提供的那些復雜的機制,是為了構建庫,以提供語言沒有實現的功能,這些功能可以大幅簡化開發工作。如標准庫裡的容器、算法,boost庫的Lambda表達式、BGL的命名參數、智能指針等等。
也就是說,一個程序員可以僅僅學習最基本的C++編程技術,便可以利用現成的各種庫開發軟件。只管用,別問為什麼。在這種情況下,學習和使用C++的難度同C#和Java相比沒有本質的差別。但由於C++可以提供更靈活高效的庫,在不少情況下,反而比C#和Java更好用。
要達到這種程度,程序員所需的訓練的確會比C#和Java多一些。所需的訓練主要集中在:標准庫的使用;區別對象、指針和引用;指針、內存、資源的處理方法,智能指針的使用;類使用的一些特別要點(構造函數、隱式轉換等等);多態的正確處理;模板的用法。另外還需要給學習者定下一些“規矩”,避免誤用一些敏感的語言機制。這些“規矩”只需遵守,不要問為什麼。一旦這些“規矩”成了本能的一部分(強化訓練可以達到這種效果),程序員就成熟了。即便回過頭使用C#或Java,也能很容易做到趨利避害,揚長避短。(要小心,這時候程序員很可能會罵人的。我是個比較斯文的人,一般不罵人,除了開車的時候和使用C#的時候)。
這些內容只要編排得當,用法標准,學習者不需要花費很長的時間即可掌握,大概兩三個月即可,如有半年的時間,便可以純熟。這樣訓練出來的程序員基礎非常扎實,無論將來學習什麼語言或技術,都可以駕輕就熟。如果