Hello,OOC World![Chap1sec3-X][DRAFT 0.1.0518]
Posted on Tue, 17 May 2011 18:49:05 -1100
项目地址 OOC-GCC : http://code.google.com/p/ooc-gcc/ 源码以LGPL发布,文档为GFDL,文档相关测试用例为GPL3
转载请注明出处
1.3 为什么不用CPP?
CPP过于冗杂,标准不够统一.编译器干了太多的活,想弄明白需要相当的时间去折
腾,而想弄精则更难,学习曲线既长又陡. 而且很多人因为入门时拜师不慎(比如“邪恶
的M”),把C和C++混为一谈,而忽略了C++的复杂性,另外这导致了相当一部人拿着C++来
写C的代码! 这本没错,只是你不觉得这很另类很诡异么?!!!
我个人罗列几个问题,有些并不难,有的甚至都不算严格的问题,只是一些概念.但我
相信只学了两三个月的C++新手,总有没听说过或是不会的.
1. 引用和指针有哪些区别
2. 通过哪种方式可以屏蔽C++中默认构造函数的使用
3. 大陆编程书籍中常出现的接口这个概念,C++中有借口么或是有相关对应么,怎么用?
4. 友元这一概念存在的意义以及如何使用
5. C++结构体和类相比有哪些限制,可以在C++结构体中定义函数,静态函数,虚函数么?
6. C++重载是否适用于使用同名同参数但不同返回值的情形.
7. 解释虚表虚函数的概念
8. C++中auto_ptr(只能指针)是怎么回事儿,有何作用,怎么用?!
9. C++中多重继承如何避免名称冲突
10.解释下隐式类型转换
11.C++可以用哪些类型转换措施,有静态动态之分么,向上向下都允许么,如果有限制硬是
使用会产生哪类问题.
12.我想定义个函数指针指向某类中的一个方法,怎么做?如果是指向类中的一个虚方法或
是静态方法,有却别么?
13.virtual和rtti有何关系
14.如何使用c++中的rtti
15.类中的函数,虚函数,静态函数是按类来分配还是按实例来分配.
16.类中的函数,虚函数,静态函数的具体内存分布大致是什么样的,先后顺序如何.
17.不同类的函数,虚函数,静态函数是在统一的一大块区域中分配,还是离散的毫无关联
的.
18.如何获得一个类的虚表指针.
19.如何重载或是能否重载+,++,<<,=,==这些运算符,重载时有限制么?
20.如何或是能否声明一个函数指针,其参数或返回值有模板参数.
21.rtti的使用有何副作用
22...bla..bla..
这里面大部分问题我还是知道或是有印象的,有一小部分我也比较好奇.不过上面的
问题还不涉及C语法中相对复杂的东西(比如复杂指针的使用). 总之想真正掌握C已经不
容易了,再要引入这么多东西真是有点吃饱撑的感觉了,总之我表示我的脑容量有限,估计
从一点不知道开始学, 要花很长时间来学,而且如果用的不多或是一段时间不用,可能又
忘了,可能细节就往干了,可能要从头再来.人生苦短,如果你真想学的又快又好又高级的
语言个人建议你还是用python吧.如果你要编译后的高效,不妨试试go语言.或者仅仅学
习C语言,再学习下这份简单的OOC教程.
在开始下一节前说一下,本手册“热身运动”一节介绍了一种简单且对称的OO风格,
“伸展运动”介绍了一些宏来简化这一部分的代码. 当然这两节中所涉及的都比较简
单,主要是为了便于大家的理解和使用.后面的章节则会循序渐进的加强这些代码! 从下
节开始真正就有代码了,如果你是从头看到这里,相信你已经受够我的啰嗦了.
1.4 一个结构体+一个函数?!
#include <stdio.h> struct A { int a; }; void iniA(struct A *THIS){ THIS->a=100; } int main(){ struct A obj; iniA(&obj); printf("the value of obj's a is %d\n",obj.a); return 0; }
这个代码?干啥的?
这个例子很是简单,只涉及一个结构体的定义和一个相关初始化函数,但在这份手册
中却有着非同一般的意义,而一切用C去模拟OO的东西也源于此.
所谓面向对象的编程,其本质目的是把数据一层一层封装起来从而使代码看上去更
加易懂,更易维护.单从封装数据的角度来看,C语言中的结构体足矣.而封装之后如何
使用它,自然要涉及所谓的构造函数,对应到上面的代码,就是那个名为iniA的函数啦.
1.5 不得不说的函数指针
前面的代码似乎过于简单了,肯定会有人说这不是面向对象的编程,从个人观点来看,
面向对象只是一种思想,而这种思想也主要用在数据的封装上,而上面的代码其实已经
体现了一点点.而下面将要做的就是一点点对其加强.
#include <stdio.h> struct A { int a; void (*showA)(struct A *); }; static void A_showA(struct A *THIS){ printf("the value of obj’s a is %d\n",THIS->a); } void iniA(struct A *THIS){ THIS->a=100; THIS->showA=A_showA; } int main(){ struct A obj; iniA(&obj); obj.showA(&obj); return 0; }
C语言中没有C++所谓的“方法”,其实也没有这个必要,因为本质上那就是一个函
数.还有些语言只有所谓的“过程”,他们本质上都是一样的(后面的文字不再区分这三个
概念,如遇到都理解成C语言中的函数就好,同样后面关于类和结构的称呼只要是在C语言
中本质也是一样的),只是细节上有些许差异. 另外要说的是C语言的结构体是可以包含
函数指针的,这一特性也使得用C去模拟对象变得可行且有意义.比如上面的代码就演示如
何使用“封装”在结构体重的函数指针.
1.6 栈内存vs堆内存
指针对于初学C的人来说是个难点,而更要命的是堆内存和栈内存的问题(如果你仍不
清楚这个问题,那么先回去补一补C语言的基础,我在这里不在赘述,因为这一块是会者不
难,但是如果不会讲起来可啰嗦了). 个人认为国内学生初学C语言时容易犯这样的错误
很大的一个原因就是入门教材一直用的是国内最流行的那本...
对于模拟OO的编程,分清堆内存和栈内存是很有必要的,特别是堆内存的使用会使你
的程序具备一定的“动态”特性. 但使用堆内存有利也有弊,特别对于C乃至C++这样的
语言来说,使用不当就杯具了,内存泄露这样的问题也源于此.下面再看一段代码
#include <stdio.h> #include <stdlib.h> struct A { int a; void (*showA)(struct A *); }; static void A_showA(struct A *THIS){ printf("the value of obj’s a is %d\n",THIS->a); } void iniA(struct A *THIS){ THIS->a=100; THIS->showA=A_showA; } struct A * newA(){ struct A * mem=(struct A*)malloc(sizeof(struct A)); iniA(mem); return mem; } int main(){ struct A * obj=newA(); obj->showA(obj); free(obj); return 0; }
这里要说明一下,iniA和newA这两个函数是为了模拟构造函数,本质上iniA进行真正
的初始化,newA实际上在外边又封装了一次,在堆内存上分配这个结构体. 这两个函数让
我们可以按需在堆上(newA)或栈上(iniA)声明一个结构实例,其实也就是本手册要模拟
的所谓的“类”.
值得注意的是上面的代码有一点很不爽的地方就是最后还要free一下堆内存,而与之
对应的malloc则放在了newA这个函数中, 这样的代码让我很是不爽,所以下面继续完善,
引入“析构”函数的模拟.
1.7 对称之美
在C++中有构造也有析构,只是编译器替我们做了太多的工作,以致有些初学者对此毫
不知情.这是我反感C++的原因之一,它掩饰了太多的东西,虽然有时看上去简单了, 但如
果你不了解底层,很多东西会觉得很不明晰,也因此这是门初学更容易犯错的语言.
下面要说的就是析构函数的模拟了.有了构造,再有析构,代码才会有对称性,至少看
上去才更加的OO.
#include <stdio.h> #include <stdlib.h> struct A { int a; void (*showA)(struct A *); }; static void A_showA(struct A *THIS){ printf("the value of obj’s a is %d\n",THIS->a); } void iniA(struct A *THIS){ THIS->a=100; THIS->showA=A_showA; } struct A * newA(){ struct A * mem=(struct A*)malloc(sizeof(struct A)); iniA(mem); return mem; } void finA(struct A *THIS){ THIS->a=0; THIS->showA=NULL; } void delA(struct A **THIS){ finA(*THIS); free(*THIS); (*THIS)=NULL; } int main(){ struct A * obj=newA(); obj->showA(obj); delA(&obj); printf("is obj NULL ?\n%s\n",obj==NULL?"True":"False"); return 0; }
finA函数比较简单,其对应构造用函数iniA,是真正的析构部分. 而delA则对
应newA,仅仅是又把finA再次封装了一下,但要注意的是这个例子中用到了指向指针的
指针(其实也可以普通的指针), 这样做有一个好处,比如上面的代码,在delA执行之
后,obj已经指向NULL了,如果我们再次使用已经执行过析构的obj对象,则错误会比较明
显. 最后的那个打印函数也是为了验证obj释放后置零这一特性.
现在趁热打铁,总结一下本手册以后常用的几个重要函数(假定我们有一个类名
曰Class),下面为了加强记忆放在一起总结一下.
1. void iniClass(Class *THIS); →→ 用于栈上对象的构造,
ini作为一个prefix(前缀),表示初始的意思,相关词汇initialization,initiate,initial
2. void finClass(Class *THIS); →→ 用于栈上对象的析构,
fin作为一个prefix(前缀),表示终止的意思,相关词汇finish,final
3. Class * newClass(void); →→ 用于堆上对象的构造,是iniClass的封装,
new作为一个prefix(前缀),表示新建的意思,相关词汇new,neo-系部分词汇,
在某些语言中直接 对应new这个KeyWord
4. void delClass(Class **THIS); →→ 用于堆上对象的析构,是finClass的封装,
del作为一个prefix(前缀),表示删除的意思,相关词汇delete,de-系部分词汇,
在某些语言中直接对应delete这个KeyWord
1.8 继承与多态
在C++的编程中关于“数据封装”有两种关系很是重要,一是继承关系,另一是包含关
系.本质上其实一样的,都是“包含”, 只不过C++的编译器再度不辞劳苦的帮我们做了
点工作.让所谓的继承关系使用起来似乎容易了些.
C语言本身没有继承方面的语法糖,但是包含关系应该是所有的计算机语言都能描述
的,因为汇编都可以,其它更高级的语言自然也能描述. 在C语言中,一种常见的模拟继承
的做法是把父类放在子类的首部. 具体见下面的代码.
#include <stdio.h> #include <stdlib.h> struct A { int a; void (*show)(void *); }; static void A_showA(struct A *THIS){ printf("the value of obj’s a is %d\n",THIS->a); } void iniA(struct A *THIS){ THIS->a=100; THIS->show=(void *)A_showA; } struct B { struct A A; int b; }; static void B_showB(struct B *THIS){ printf("the value of obj’s a is %d\n" "the value of obj’s b is %d\n", THIS->A.a,THIS->b); } void iniB(struct B *THIS){ iniA((struct A*)THIS); THIS->b=200; ((struct A*)THIS)->show=(void *)B_showB; } int main(){ struct B obj; iniB(&obj); struct A *s=&(obj.A); s->show(&obj); return 0; }
因为上面这段代码只使用了栈内存,而对象本身也没有指向堆内存的指针,所以为了
简便只保留了“四大函数”中的ini系列. 同时也有一些细节上的修改,比如A中的函
数showA改为show,这是为了演示如何在子类中重写父类的函数.
我们假定子类B继承了父类A,自然用C的包含关系去模拟继承关系时应将父类放在首
部(当然也有些特殊应用要求统一放在尾部),这样做的一个好处是方便指针型的强制转
换. 而在使用的时候这种子类指针型强制转型成父类函数指针型的做法一般被称为“向
上转型”,在使用的时候我们用的其实是一个父类指针,这样的“数据抉择” 体现了“多
态”的思想.在父类中定义方法(接口) ,子类中具体实现.使用时则通过父类的形式来
调用.
1.9 贴心的匿名结构体
上一段代码相比会让大家觉得有些丑陋,因为用到了不少强制转换.其实现代的
主流C编译器都支持匿名结构体(anonymous struct或unamed struct)这一特性.这
样用C去模拟OO的继承关系时就更加舒服了.下面的代码和上一段代码功能完全相同,但得
益于匿名结构体这一特性,看上去更加悦目了.
#include <stdio.h> #include <stdlib.h> struct A { int a; void (*show)(void *); }; static void A_showA(struct A *THIS){ printf("the value of obj’s a is %d\n",THIS->a); } void iniA(struct A *THIS){ THIS->a=100; THIS->show=(void *)A_showA; } struct B { struct A; int b; }; static void B_showB(struct B *THIS){ printf("the value of obj’s a is %d\n" "the value of obj’s b is %d\n", THIS->a,THIS->b); } void iniB(struct B *THIS){ iniA((struct A*)THIS); THIS->b=200; THIS->show=(void *)B_showB; } int main(){ struct B obj; iniB(&obj); obj.show(&obj); return 0; }
匿名结构体的使用可以让我们省去不少强制转换的麻烦.但是,有一个很大的问题就
是当有重名成员时到底如何处理,是直接报错, 还是不额外多分配重名成员所占的内存,
还是额外分配重名成员所占的内存.关于这个问题GCC4.5和GCC 4.6的处理方式多少有些
不同, 具体可以参见GCC testsuite中关于anonymous struct的部分.
这里多啰嗦几句,GCC原生支持的如下这种匿名结构体,暂用Type A简记
stuct B{ struct A{ int a; }; int b; };
注意A是在B中声明的
而据GCC官方文档所说,M$的编译器则原生支持如下形式的,暂用Type B简记
struct A{ int a; }; stuct B{ struct A; int b; };
如果想让GCC支持Type B这种类型的匿名结构体,4.5版本在编译时须加
上-fms-extensions, 而4.6版本则加上-fplan9-extensions,比如上面的代码如果
要用GCC编译,就应加上这些选项.
1.10 休息一下
这一节,仅仅是回顾与总结
个人认为前面已经大致说清了OO思想中关于“数据封装”部分最为重要一些东西.
而要用C来模拟OO,首先要解决则是如下的一些问题
1. 结构定义 2. 构造方法
3. 析构方法 4. 包含关系
如果你逐行的看完前面的代码,一定会觉得很累,虽然他们实现的功能很简单,仅仅是
打印一两个数值而已.由此看来上面的工作似乎时间扯淡无比的事情啊!!!
没错,上面的代码的确无比扯淡,很适合我们吃饱了撑着的时候去研究,也省的吃吗丁
啉了.但是,这种一层层的封装本身还是很有必要的, 只是我们不应重复的去敲这么多的
代码,也不应去记忆那么多繁琐的细节.
值得庆幸的是C语言是支持宏定义的,想想<<电子世界争霸战>>中那些华丽的nested
macros!但是程序毕竟不是时装秀,宏的滥用也绝不是件好事. 也因此很多更为高级的语
言抛弃了这一可能带来灾难错误的特性.但是不可否认,一旦某些宏称为一种约定,则是既
简洁有好用的.
在下面的章节中, 我会使用三个宏CLASS,CTOR( 对应CONSTRUCTOR) 和DTOR( 对
应DESTRUCTOR),通过它们来完成类本身的设计, 以及构造函数和析构函数的设计,进而
简化上面的OO风格所带来的冗余代码. 如果把这三个宏展开来看,和本章的代码并无差
异.