来自:网络 作者:不详
我时常听到这样一种论调:C++不是一种真正的OOP语言。(有时还会补上一句,C#(或Java)是真正面向对象的)。每隔几个星期就听到一次。
天晓得这种谣言是怎么出炉的。为此,我还特地找了几本关于OOP的书,温习了一下,看看是不是我学艺不精,漏掉了什么。其实这句谣言并不怎么高明,却传播甚广。我们只需对比一下两种语言的OOP机制就明白了,如果C++不是真正的OOP语言,那么肯定会缺少某些机制。
基本的类、继承、重载、虚函数等大家都有。C++多了多继承、操作符重载。C++没有接口,但抽象基类是接口的等价物。其实大家都差不多,OOP已经很成熟了,不会谁比谁多很多特性的。
我猜想,这则谣言可能来源于这样一句话:C#/Java是纯面向对象的语言(尽管它们都有了泛型,但通常还是划归纯面向对象语言),而C++是混合型的语言。对于不知情的人,混合型语言自然不如纯面向对象语言那么“面向对象”了。
这则谣言的另一层隐含的意思是纯面向对象的语言比非纯面向对象的好。这是人的本性所使。OOP革命后,人们发现OOP强大的抽象能力,大大简化了软件的分析和开发过程。但是好事总是会好过头。OOP功能强大,渐渐被神话成OOP无所不能。进而产生了只有纯面向对象才是最好的这种思想。
过去很多文章,包括我前一段发出的货币系统的案例,很好地证明了OOP的局限性。但在滥用OOP和合理使用OOP间存在着一道障碍:OOP也能把问题解决掉。(其实面向过程的结构化软件设计也能解决问题)。在货币系统案例中,我完全可以用OOP的方式构建货币系统,只要我耐心地反复重载操作符,总能完成的,毕竟货币只有这么几十种,常用的也只有这么十来种。
“能解决问题就行了”这成了很多程序员拒绝进步的最好借口。对于这一点,多数企业采取放任的态度。于是,开发效益也就无从谈起。
下面我们来看看OOP的其它问题。OOP的核心是多态和后期绑定(虚函数)。因为有了这两样法宝,OOP便可以使得类具备扩展的能力。考虑这样一组类:
class B
{
public:
virtual void fun() {
cout < <”I’m B”;
}
};
class D1 : public B
{
public:
virtual void fun() {
cout < <”I’m D1”;
}
};
class D1 : public B
{
public:
virtual void fun() {
cout < <”I’m D2”;
}
};
D1 d1;
D2 d2;
B* b=&d1;
b-> fun(); //显示”I’m D1”
b=&d2;
b-> fun(); //显示”I’m D2”
这是老生常谈了,这种机制使得这些类的使用者通过重定义虚函数fun()实现功能的扩展。或者说,实现原有代码的重用。
现在,我需要不止一个D1的对象,而是一组对象,那么就需要容器来存放。于是,我写了一组类,用于创建D1的各种容器:
class D1Array
{
public:
void add(const D1& v) {…}
D1& item(int index) {…}
const int count() const {…}
…
};
链表、栈等等都得写一遍。D2怎么办?继续写呗。
类太多了,为了少写一点类,我采用了OOP的一个标准用法:
class Array
{
public:
void add(const B* v) {…}
B* item(int index) {…}
const int count() const {…}
…
}
其他的容器类推。
如果我要把对象放进容器,则需要这样写:
Array a;
a.add(&b1);
取出来比较麻烦,必须要这样:
D1* d=dynamic_cast <D1*> (a.item(0));//应该使用了dynamic_cast <> 操作符, //不应该用(D*)这样的转换操作
这还不是最麻烦的,最麻烦的是:如果放进a的是D2的对象,而我不知道,以为是D1,那么事情就麻烦了:
D1* d=dynamic_cast <D1*> (a.item(0));//此时d==0
尽管我可以知道转型是否成功,但却不知道这个类型究竟是什么。要么把B所有的继承类都试一遍,要么利用type_info()获取类型信息,用switch分派。
这种情况是由于OOP的多态机制的单向性造成的:可以从继承类向基类隐式转换,但不能反过来。这种容器称为弱类型的,是很多麻烦的根源。比较好的做法是:在弱类型容器外做一个包装类(代理),把容器强类型化:
class D1Array
{
public:
void add(const D1& v) {…}
D1& item(int index) {…}
const int count() const {…}
private:
Array m_impl; //元素存储在这里,D1Array的成员函数只负责转发
};
尽管类还是很多,但代码重复少很多了。而且,类型安全比代码类的数量更重要。
解决的办法就是引入泛型编程。用模板(C++)或泛型(Ada、Java、C#),可以轻松地解决这个问题。详情就不再多说了,C++的标准库就是一本最好的教材。
弱类型的容器并非没有用,一个至关重要的用途,就是构建异类型容器。发挥点想象力,如果我把D1和D2的实例都放进一个Array,然后顺序调用容器内元素的fun()成员,会发生什么呢?
a.add(new D1);
a.add(new D2);
for(int i=0; i <a.count(); ++i)
{
a.item(i)-> fun();
}
会分别执行D1和D2的fun()。
这种技术有非常广泛的用途。可以说,绝大部分的多态应用都是以此形式出现的。
现在,让我们做些哲学思考。没有那样东西是万能的,也没有那样东西是无用的。老子说,福兮祸之所倚,福兮祸之所伏。这句话用在OOP上再恰当不过了。OOP既有强大的一面,也有虚弱的一面。同样,泛型编程也有无法解决的问题。前段时间的一个帖子《精通Template技术的高手请进!》(http://community.youkuaiyun.com/Expert/topic/5574/ 5574289.xml?temp=.9206049)就提出了一个泛型编程无法解决的问题。
说完强大的东西,我们再来看看C++中的一个小喽罗:自由函数。在Smalltalk、C#、Java这类纯OOP语言中,是不存在自由函数的。对于很多程序员而言,自由函数就是史前动物,只在博物馆里看见过。自由函数是面向过程开发的代表,是过时的象征。
纯OOP语言的热衷者通常都有一种倾向,将所有的操作和数据都塞进一个类里,不管是否必要。“嘿!”有人会说,“这可没办法,这些语言没有自由函数,不放进类也不行嘛。”不错,但是很多程序员把本该做成static的函数(而且应该集中放在一个独立的工具类里),写成了非static的。这种做法是对OOP的滥用。很可能严重破坏类的封装性。
其实,即便在纯面向对象的语言中,也应该将类的成员函数最小化,充分利用static成员函数,完成组合的操作。请看下面的例子:
class Rect
{
public:
point& left_top() {…}
point& right_bottom() {…}
double& width() {…}
double& high() {…}
const double area() const {
return m_width*m_length;
}
private:
point m_left_top;
double m_length; //长
double m_width; //宽
};
这种做法缺乏灵活性,如果修改Rect,不用左上角坐标、长和宽保存数据,而是用左上角坐标、右下角坐标来保存。那么,area函数必须修改。着增加了代码维护的工作量。
如果用一个自由函数(或者static成员)计算面积,便不会因为类内部结构的变化而需要修改了:
const double area(const Rect& rc) {
return rc.width()*rc.high();
}
尽管成员函数area也可以利用width()和high()计算,但自由函数可以提供更大的灵活性:
const double area(const Rect& rc, double margin=0){…}
这样,我们修改了面积函数,增加了功能,但使用的代码不用改变。也无需到处寻找,并修改每个图形类(Circle、Triangle什么的)。C#没有默认参数,可以用重载解决。并且,自由函数和static函数更有利于泛型化。总之,类的接口尽量小,用自由函数或static函数执行复杂的计算和操作。
但是,自由函数和static函数还是存在那么一丁点区别,(尽管一丁点,但有时也挺重要)。这种差别主要体现在操作符重载上(操作符可以看成是一种特殊的函数)。C#里,操作符重载必须在类中,必须是static成员:
class A
{
public static A operator+(A a, A b){…}
};
如果有个类我希望它参与+操作,就需要为其重载+操作符。但是这个类不是我写的,我也无法修改,那么此时,+操作符无法重载,也就无法让这个类参与+运算。而在拥有自由函数的C++中,操作符可以是自由函数(除了类型转换和=两个操作符)。于是,可以直接重载操作符即可:
A operator+(A a, B b) {…}
B operator+(B b, A a) {…}
重载可以非常自由,任何地方都可以,只要在用之前即可。
最后,轻松轻松,让我来抨击一下C#和Java。呵呵,开个玩笑。其实这个问题在C++中也存在,只是办法有回避而已。
先看C#和Java。有下列代码:
c.s();
c.f();
请告诉我,c是什么,类型还是对象?s()和f()是否static的?得看前面的代码,c的定义,对吧?在C++中:
c.f();
C::s();
这样两个问题都清楚了,C是类(结构),s()是static的。c是对象,f()…。等等问题没有清楚,f()不定。C++的代码比C#和Java更明确。但是,这个f()是个问题。按照C++标准,f()既可能是static的,也可能是非static的。为了代码阅读的方便(为了别人,也为了自己),我从不使用c.f()这种形式访问静态函数。
这个问题的影响不仅仅局限在代码的阅读上。对于初学者,类和对象本是一对夹缠不清的概念。尽管C#和Java(包括C++的c.f()),简化了使用,但是却不利于初学者区分类和对象的概念。尽量用两个冒号吧。包括初学者在内,不要贪图享乐。
好,总结。这里我们简单地回顾了OOP的力量和不足。也提到了GP的力量和不足。也拜访了微末的自由函数。于是我们可以得出这样的结论:C++中的众多编程技术都有各自的优缺点,但它们都是互补的。只要我们充分地运用这些技术,取长补短,任何问题都不在话下。
关于C++的编程模式,Bjarne Stroustrup给出了一下总结:C++是“多模式编程语言”,支持四种编程模式:面向过程、数据抽象、面向对象和泛型编程。(我觉得应该是四个半模式,应该算上模板元编程这半个)。这些模式整合在一起,相辅相成,构成了强大的威力。任何偏费都会削弱它的价值。合理使用C++,享受多模式编程带来的愉悦!