C和面向对象,这不就是C++么?为什么要搞出另一套东西,而不直接使用C++呢?关于C与C++之争是一个大坑。Linux之父Linus就是力挺C而批判C++的。讨厌C++的人似乎认为C++过于复杂,内部机制陷阱过多等等。自己的经历不多,用C++也很少,达不到大牛们的境界,如果让我给个非要用C而不用C++的理由,我也给不出一个有说服力的。
最原始的动力是,我在使用GTK+进行开发,而GObject是GTK+的基石。如果基础不牢,上层一定不会稳,因此很有必要把GObject给过一遍。知道了它的内部,才知道该如何使用它,明白它的机制与原理,做到心中有数。
但是研究GObject能带来更多。由于C里没有任何面向对象机制,因此GObject把这些机制全部实现了一遍。从中可以看到一些机制的实现原理,从而对面向对象有更多的理性了解。
面向对象的最基本需求就是封装。所谓封装,按我的理解,就是将一系列相关数据,及对这些数据有关的操作,有序的组织在一个结构中。一个圆形有x坐标、y坐标、半径三个参数,我们可以用这三个变量表示一个圆:
double x, y, radius;
这没什么问题。现在多了一个圆,我们又要用三个变量:
double x1, y1, radius1;
当我们有很多个圆的时候,可能要用到数组:
double x[100], y[100], radius[100];
问题在哪?x、y和radius是相互独立的。我完全可以定义100个x,200个y,150个radius。如果不只有圆,还有矩形,那么矩形的坐标叫什么呢?xx、yy?等你写了一堆代码之后回来看,到底x和y是圆的坐标,还是xx和yy是圆的坐标?
所以有了struct。一个struct对数据进行了很自然的封装:
struct Circle { double x, y, radius; };
好了,现在我们有了Circle这个类型。这个类型将圆的三个参数封装到了一起,从现在开始它们就是一个整体了。我可以很自然的声明一个圆,而不是它的三个参数:
struct Circle c;
我们也不用担心x、y、z的数量不等了,更不用担心坐标和矩形坐标命名冲突——它们定义在Rectangle这个struct里呢:)。
事情还没有完。有了圆这个类型,那么对圆的操作呢?假设一个圆的操作之一为移动(move)。我们可以定义如下函数:
void circle_move (struct Circle *self, double x, double y) { self->x = x; self->y = y; }
我们输入一个圆的指针,以及新的x、y坐标,移动操作帮助我们把指定的圆移动到新的坐标上。注意第一个参数self,是不是有点眼熟?它就是C++里的this。记得学C++时很多同学对this理解相当困难,如果看这个self就不难理解了:self就是我们要操作的那个变量,它是一个指针。C++在对象方法调用时省略了这个参数,它可以被编译器自动设置。在C里面,这个工作要我们自己做。因此移动一个圆要这么调用:
struct Circle cir; circle_move (&cir, 10.0, 5.0);
注意self是个指针,因为C里没有引用,所以我们只能使用指针来达到传递一个对象,而不是传递它的复制品的效果。
这个方法……不就是普通的函数调用嘛,根本就没把操作给封装呀。好,现给一个看起来像C++中的方法:
struct Circle { double x, y, radius; void (*move) (struct Circle *self, double x, double y); }; ... struct Circle cir; cir.move = circle_move; cir.move (&cir, 10.0, 5.0);
通过函数指针,可以让move调用看起来更像C++了。但是,有两个不爽的地方。其一,要显式地将circlemove函数赋值给move函数指针,如果有5个圆,那就要5行指定的代码(除非用数组+循环)。更为严重的是我们可以为不同的变量指定不同move操作。其二,调用时依然要显示地指定self,这带来的一个后果是,我们完全可以调用cir1的move,但是传入的是cir2的指针。
对于第一点,可以使用类结构+初始化函数来解决。对于第二点,C语言是没法避免显示的传入self指针(如果可以的话请告诉我)。因此这种写法只是“像”C++而已,没啥实际的好处。不过在之后我们会看到,GObject会在类结构中使用函数指针来表示对象的操作。
话说,很有可能懒筋发作,然后这篇又留下一个坑-_-。希望不要如此吧……