学习和使用
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#的时候)。
这些内容只要编排得当,用法标准,学习者不需要花费很长的时间即可掌握,大概两三个月即可,如有半年的时间,便可以纯熟。这样训练出来的程序员基础非常扎实,无论将来学习什么语言或技术,都可以驾轻就熟。如果他还喜欢C++,那么可以进一步学习C++的高级机制,加入库开发者的行列。
对于自学者,也可以进行这样的训练。但必须要正确选择教材。Lippman的《essential C++》和《C++ Primer》是我所知的最好的教材。认真地遵循书中的线路,反复做好练习,就行了。入门书是很重要的,千万不要选择那些C(++)的书(大半本C,带上那么一点点C++的内容)。
这样,我们实际上是将C++的使用分成了两个层面,一个是应用层面,另一个是基础开发层面。当然,基础开发层面还可以分成应用性的基础开发,比如帐务管理系统专用的基础库;和工具库的开发,象boost之类的库。应用层面的开发人员不必了解(甚至可以不知道)C++所有的古怪特性,以及那些库的内部机制,只需学会使用即可。要达到这种要求,我相信对能够学会高等数学的人而言,是易如反掌的。
对于一个使用C++的企业,无需每个程序员熟悉所有的C++特性。有少量高手专注于企业级的库的设计和开发,而其余的程序员只需达到上面所说的基础C++的程度。这种配置可以获得非常高的开发效率。但前提是程序员接受正规的、扎实的基础C++培训,这在前面已经讲过了。
再强调一遍,“规矩”是至关重要的。因为C#和Java易学,主要得益于去掉了许多危险(但却非常有用和重要)的语言机制。在C++的教学中引入严格的“规矩”,是在保留这些危险但重要的机制的同时,使学习者避免其损害的手段。必须让初学者知道很多机制是不能碰的,因为这些机制不是给他们用的,是给那些充分了解其危害的人用的。
相比之下,在路上开车压死人要比在C++中犯错误容易得多(毕竟C++不会在马路上乱窜)。之所以我们没有天天出事故,是因为我们遵循了规则(交规和驾驶技巧)。C++编程也一样(任何编程都这样)。但有时人们可以在受控的情况下合理违反交规,比如执行任务的警车和救护车可以逆向行驶、闯红灯。显然这是有条件的,驾驶员受过特别训练,鸣笛和亮警示灯。就像在C++中,只有受过特别训练,在有保护的情况下方能使用象placement new这样的超危险机制一样。
不错,即便如此,C++的学习依然不是一个轻松的过程,所学的内容也比C#和Java的多。但是在付出的同时,还应看到收获。学习C++的收获不仅仅是可以使用Lambda表达式这样优雅的语法,而且能够真正地掌握编程技术中的核心,为将来的发展打好基础。当你能够熟练地使用标准库时,正宗的抽象思维的能力已经悄悄地潜入了你的骨髓,这是成为一名优秀的软件设计师的基本条件,谁不想要呢。
好了,我这里展示了C++易用性的冰山一角,解释了难学难用的原因,也提供了学好C++的方法。“C#和Java比C++容易学习和使用”这句话尽管不算诽谤,但也是被严重地夸大了。我希望大家能象Discovery Channel的Mythbusters(Discovery的保留节目,专门用实验的方式检验流言的真实性)那样,勇于尝试和实践,用自己的切身体会打破关于C++的流言。