2010年5月20日星期四

C_to_CPP - OO系列之一

现在去查我的草稿页面,在#091222的时候,为此话题开过一个帖子。当时只是列了写可以开始的话题,不过当初其实没写就后来被闲置了。由于去写另开了一个“OO系列”的其他一组帖子,于是那贴的内容就留着做系列开篇吧。
貌似是以上说的这么回事。只是因为我觉得“就是这么的吧”,就把以上的文字先放在上面了。

先来跑会儿题,这都快要成这里写贴的传统了,有时甚至可以怀疑话题什么的是不是就是为跑题做的一个引子。不过关于写贴这个过程,有些矛盾的东西。
一种想法是去多凭借大脑的思考,相信懂得的越多,那么可写的东西也越丰富。文字本身的功能就是记录。它不是种负担,知道的,和话题有关的,可以写下来。不知道的,和话题有关的,可以知道了再写。不知道的,和话题无关的,那就不考虑了。总之呢,写文就像填空一样。先把话题分解了,列个子话题的提纲来补完,然后再附加点临时的想法就行了。
按照以上的写法来做个日志里普通的一帖也没什么问题来着,除了口水可能会多一点而已。那么下面来说另一种想法,大概是想中国传统水墨画强调的精神境界吧。就像王羲之写兰亭集序那种游山微醉的特别的状态,作品是一种仅存于场合的。尽管可以肯定其作者的卓越水准与才华,也可去想其作者也理当有相当优秀的卓越作品,但是人们还是会更偏向于从他代表作中窥探其作者的全貌。
这里举这个例子(有时就是简单的大家都知道的例子啊,也不多我写一遍),是想表达“珍惜”。也就是说我自己的矛盾在于,是相信有足够数量的文字才留下了足够珍藏的东西呢,还是相信自己能有从全局的角度已经从其中挑选了最重要的东西来写。

跑完题话题还是要收回来的,下面的内容将以《TC++PL》一书为主要参照,然后就以其中的话题来跑题。恩,既然标题是“OO系列”了,况且本文是做规划的开篇,与实际编写时间上的收尾文字。
所以内容的侧重本身就在于某个具体的地方(接口啊,不然下面的子标题是什么东西)了。
然后嘛,不要相信下面的文字是在同一个时间内按从上到下的顺序写的。下面的东西就是罗列性质的,想看心情贴的把滚动条往上回拉。

从C到C++
本文的标题就是这个来着,则中点是语言发展和对象机制的使用。
C++并不是早期的对象机制的先驱,但它这种编程方法的推动者:
1.高性能的实现。在C++之前的语言,对象是放在heap上的,使用时需要动态分配,在使用上也带来的性能代价。而C++通过他的对象机制,将绑定尽可能转化到编译时,并让对象可以在heap上有固定的分配方式。C++中没有带垃圾收集也是性能和实现上的角度考虑的。
2.对C的兼容和扩充。与C兼容是C++设计的一个出发点,这可以让已有的代码和经验继续工作,同时C++中严格的类型的检查(C在标准化时也引入了,只是严格度差别)也提高了C代码的质量(K&R第二版里代码当初就是用C++编译器试验的)。C++在语言上的扩充却能为语言的使用带来方便,例如class就是有struct转化而来,添加了成员方法和访问空是。此外还有命名空间和函数默认值以及overload什么的。
3.用语言表达设计。C++对C在语法上的扩充,用C可能使用OO的方法来编写代码。而C++为了推动OO,为此实现了方便使用的语法。也可让使用者通过对class相关语法的使用来规范地学习并使用OO,实现了OO设计与OO实现之间的一个纽带。

Qt与Glib
以C语言的方式来处理字符串,可以在实现方式上有很多中优化技巧。不过通常就是指要一个能工作正确的字符串类型,而且对C的使用者而言,也会用对某种实现方法的复用,而不是用到字符串都去考虑实现细节的。
不去考虑宽字符的问题,C中的字符串就是一个char[],并可用char*来表达对字符串的引用。除了以数组和指针方式的使用,还可用标准库中string.h里预先实现好的一些提供便利功能。而字符串的储存分配可以用静态的常量指针或定长数组,也可用动态的malloc来分配再free。
C++在std中提供了string类型,它是对C中字符串处理的OO化包装好处。通过string类的成员函数的使用,可以实现对字符串的访问和修改以及迭代和转化,而内存管理是自动进行的,甚至可以有简单的引用计数(参见TC++PL中的那个例子)。这在字符串操作的实现和接口上都相比C提供了进一步功能。
下面不考虑cpp在接口上带来差异,去看Glib中是如何对C语言中字符串操作进行扩充的:
typedef struct {
gchar *str;
gsize len;
gsize allocated_len;
} GString;
GString结构用来表示表示一个标准的C字符串,不过实现了储存空间的自动管理。用"GString* g_string_new(const gchar *init);"来分配,对应有g_string_free()来释放。然后提供了一组"g_string_"开头并以GString*为第一参数的函数,用于执行对GString的操作。
这种风格是不是很熟悉呢,在C标准库stdio.h中对文件的操作就是使用了FILE结构,然后提供了一组f开头的函数来提供对文件的操作的。
那么到了C++中我们获得了那些便利呢:
1.自动析构。对象会在它作用域(块block或者行line)结束的时候自动调用析构函数。免去手写析构不仅是少些一行的事情,能避免错误和麻烦才是更有用的。有时还临时包装一个需要在{}自动执行的语句包装成一个对象(算cpp的固定技巧了)。关于构造,可以直接用cpp的构造函数,有时也有显示init方法来着。
2.隐式的this。拿string("abcdefg").size()举个例子,它等价于string("abcdefg").string::size)()。这里调用对象的方法就是调用它所属类的成员函数,编译器会自动识别的。同时在以方法的形式调用类的非静态函数时,会将对象本身以隐式参数this的形式传入,让函数知道它在操作的对象。
Glib是由GTK的核心组件中分离出来的,用于对c++标准库在使用c语言时功能的替代。同时,GLib有个基于它对象系统叫GObject,因为处于GTK这个GUI库,所以和Qt的QObject比较会更有价值来着,同用于GUI而且都有信号机制的。不过用C手写GObject据说很痛苦的,会重复代码很多。不过GTK+有个c++绑定叫gtkmm,这里拿来对比也很有趣。
这里说OO,模拟和GUI也两个领域可以算起源了吧。
这里又把Qt拿出来说事了,因为它确实很好用来着。首先是充分利用了C的对象结构,还通过预编译器增加了语法上扩充的便利(C++本身就是对C的语法扩充,只不过后来标准化后进化转移到库上。好用就行,我没有语言洁癖。况且Qt文档好查)。然后是有一个很丰富的库,比如QtCore就是对std的扩充,关键还是要用QtGui来着。再然后嘛,Qt也是一个框架,可以供对类的设计上的复用和参考。

Javascript框架实现的对象模型
Javascript的对象模型参照self语言,是基于原型继承的,而且还提供了函数以构造器的形式调用。不过在由于类继承使用得更加广泛,众多js框架就在这一方提供的充满技巧而有趣的库来实现对语言的扩充。
先来看prototype和mootools,前者提供"Class.create()"方法,后者使用"var MyClass = new Class(properties);"的形式。它们得到的返回值例如创建一个Person类,可以用"var guy = new Person('Miro');"的方式来使用。
而且prototype用"create([superclass][, methods...])",在创建类时指定父类,并可立刻或随后添加方法。mootools的类有Extends和Implements这两个属性,用来实现对象间功能的扩展。
下面来看jQuery的对象,其实我也只用过这个框架来着,当然也是很喜欢的(而且不说它的选择器和ajax功能来着)。
1.$/jQuery对象。$对象有两种用法,一种是$.method,直接使用jQuery提供的函数。另一种是用$()去修饰一个以存在的对象,然后可以其实现的方法如show,each...来实现一个代理(这个词用的不大严格,暂求可以表意吧)对象供快捷的使用。
2.链式方法调用。这个cpp里也有的,就是"return *this"然后得到一个自身类型的引用,不过没jQ里用得普遍。这样貌似可以把SQL语句写成一串方法调用,LinQ这样干过,不过后来还是偏语法(非库)扩充了。
3.setter/getter。在jQuery里面这两个操作用同一个方法实现,被操作的属性名称即方法名称,如果有参数,就执行set。其他的API风格可以是两个函数,在方法的参数里指定属性名,或者提供对"="符号的左右两种情况的指定。
说继承的话,jQuery就是用Javascript基本的new+prototype了,不过貌似和代码中见到的$对象无直接使用上的视觉关联。

多态
写成标准的名词叫polymorphism来着,不过我现在对这个名词已经有陌生感了。因为下面要说的其实就是类继承中的override和运用模板的泛型编程,可以去那样使用名词什么的是概括用的符号。虽然cpp主张用keyword表示功能,不过在引入新关键字上还是很节约的(风格啦)。
首先来说虚函数,用关键字表示就是virtual。它和一般成员方法的区别在于,方法调用时所实际执行的函数是在运行时才决定的。通常的方法是用虚函数表,在类的vtbl中储存一组函数的指针。
编译器对多态方法会知道它是表中哪一个,而不会在编译时去和特性的函数的绑定。这样的作用是在以指针的方式传递对象时,派生类可以传给指向基类的指针。由于子类需要覆盖父类的方法,那么在通过指针调用方法时不能在确定只固定调用某具体的函数。
说完了cpp,来看c语言中怎样做。其实就相当于是一个显示的vtbl,一个明显的做法是用一个结构来储存一组函数指针。相比于一般成员函数是作为类的函数,虚函数就就可算是对象的方法了。
这里被我说乱了,貌似也说错什么了。总之呢RTTI也是通过vtbl来对对象的类型进行的运行时类型判定的。也就是说非多态对象在运行的时候是不知道自己所属的类的,方法的绑定全部由编译的时候完成了。
下面来说另一种多态,也就是模板来着。模板的原理就是根据类型对代码进行展开,也就相当于代替手写代码因为类型不同而重复一样。不过由于展开规则的存在,也被用来玩一些技巧性的编译时运算(这里仅仅说常用的啦)。
以模板的方式来使用对象的话,不需要一个共同的基类。它只需要一组对象可以按照特定的方法使用(例如相同名称的成员函数),这样的话就不需要每个类都去继承一个"fat interface"了。一个明显的应用是STL中的序列和算法。
具体的代码实例的话,不想举了。总之呢,对类型的编译时检查还是工作的,可基于继承的类型检查一样。
(关于switch语句与类层次见的转化也是一个常被提及的话题,也算是用类的层次结构减少或分配代码了。这里这点略过。)

错误处理
一个函数返回错误信息可以有很多种方法,cpp中的"Exception Handling"也仅仅是一种可选的机制而已。传统的一种方式(c和cpp里都用的)是用返回值或一个标志变量,由调用者在使用完一个函数后进行判断。
不过就像C中setjmp.h提供的setjmp/longjmp一样,cpp的异常机制也实现了一种特殊的控制流程。它可以在一个嵌套式的函数调用stack上将控制权一层层往外传递,直到有一层捕获了异常并不在向外throw。
不过貌似出于兼容原因,异常机制就在一个限定的代码范围类,没看过在库之间使用的。

类型
C++有个想法是用户定义类型能像原生类型一样使用(C的兼容问题不说,int就是从C继承来的),所以有了操作符override。可以在类中定义操作符的方法,也可以overload一个操作符表示的函数。
由于operator()的存在,也可让一个对象以函数调用的形式来,C++中称这个叫Function Objects。然后也在<functional>中,提供了代表一元和二元操作符的对象,例如"equal_to Binary arg1==arg2",外加Binder和Adapters,用处是在Algorithm函数中做表示操作的参数(通常传入一个函数的)。
TC++PL中还有一个例子,我已分不清是语言的组成还是技巧了:矩阵运算U=M*V+W可以优化为mul_add_and_assign(&U,&M,&V,&W),方法是为每一步运算结构都创建一个新对象,然后最后一步根据得到的对象再运算。
像numeric_limits这样的东西是以类型为参数用模板做了一个映射表了。
在C++中其他机制的,本篇就不可能都说到了,况且本文要有重点的!
或者,以后再补充啊。

参考
《The C++ Programming Language Special 3rd Edition》
http://www2.research.att.com/~bs/
http://www.cplusplus.com
http://www.boost.org
----
果然写成浅薄的炫耀贴了,这种贴最大的特点就是罗列而不深入。
刚才看了别人的日志,来反省一下自己写的东西。
不过这里写下的帖子,也算是以生活很痕迹给人生以勇气吧。

没有评论: