用 C 进行 OOP 编程?
在本文中,我们将研究 Glib 对象系统,也称为“GObject”,直到最近它还是 GTK+ 的一部分。但是在研究 Glib 2.0 中的这个新对象系统之前,我们需要解决一个更为基本的问题 - “对象系统”到底是什么以及它为何存在?毕竟,C 是一种非面向对象的语言。是有可能用 C 编写面向对象的程序,还是必须使用 C++ 编写面向对象的程序?
答案是有可能用 C 编写面向对象的程序。但是,由于对象的概念不属于 C 语言规范,因此需要用外部库来提供这方面的支持。在本文中,我们使用术语“对象系统”来描述一个提供 OOP 编程所需基础的库,而 Glib 便是这种库的一个示例。Glib 提供了类、继承、引用计数、信号、接口和对象特性的 C 实现。通过使用 Glib,C 程序员就可以轻松地编写面向对象的程序。
因此,有可能用 C 编写面向对象的程序。但是,您可能会感到疑惑:为何 GTK+ 开发人员不直接使用 C++。这里我们不讨论每种可能的解释,而只解释为什么拥有一个用于 C 的对象系统是有意义的。其一,比起 C++,有许多开发人员更喜欢用 C。另外,由于项目或平台的限制,可能不会选择使用 C++ 编译器。无论是什么原因,拥有了用于 C 的对象系统,可以使更多的潜在开发人员也进行 OOP 编程(尤其是 GNOME 编程),我们对此表示感谢。
C++ 包装器
可以这么说,所有那些 C++ 的支持者也不必担心 - 您也可以用 C++ 编写 GNOME 程序。由于 C++ 是 C 语言的超集,因此您可以方便地将 C 样式的 Glib/GTK+ 代码和现有的 C++ 项目结合在一起。另外,您也可以使用 Glib/GTK+ C++ 包装器。Glib/GTK+ C++ 包装器将允许您使用本机的 C++ 类和对象与 Glib 对象交互。
研究 GObject
好了,现在您知道了对象系统的意义和用途。现在,我们将深入研究 Glib 对象系统并探究一些基本的 GObject 编程概念。直到最近,GObject 系统还是在 GTK+ 1.2.x 库中实现的。现在,由于即将发行 GTK+ 2,该对象系统已向下移到了新 Glib 2 库这一层。由于这样的移动,Glib 对象系统现在就与 GUI 相关的问题完全无关了。对于我们这些喜欢在基于非 GTK+ 的程序(比如基于控制台的应用程序和工具)中使用类和对象的人而言,是个非常好的消息。
现在,让我们把 Glib 对象系统(也称为“GObject”)与 C++ 和 Java 语言的对象系统作一下比较。首先,让我们进行语法比较。使用 C++,您可以通过对象指针调用方法,如下所示:
object-function(arg_a, arg_b);
通过使用 C++ 对象引用,您可以输入:
object.function (arg_a, arg_b);
Java 语言只有引用,没有指针。Java 方法调用语法与 C++ 引用对象方法调用完全一样:
object.function (arg_a, arg_b);
与此相反,GObject 使用标准 C 函数调用语法。其对象指针作为函数的第一个参数被传递。还要注意的是,为了防止名称空间冲突,函数名用类名作为前缀:
classname_function (object, arg_a, arg_b);
您可能会感到疑惑:调用 GObject 方法与调用简单的 C 函数到底有何区别?嗯,从 C 程序语言本身的角度而言,没有什么区别。就 C 编译器而言,它只是调用一个函数,而该函数的第一个参数正好是指向标准 C 结构的指针。
因此您的 C 编译器甚至不知道我们正在编写面向对象的程序。但是,别让这个事实愚弄了您,让您误以为 GObject OOP 编程只是将良好的旧式 C 编程进行了一番改头换面。GObject 在幕后确实作了许多工作,允许您创建现有类的子类、创建类的接口(我们将在本文的后面对此进行讨论)等等。不过这个 OOP 的所有功能都旨在与标准的 C 编程构造完全兼容。
关于 GObject 方法调用,这里还要说明另一件重要的事。当(您调用方法时)将某个对象传递给类函数时,需要对该对象进行数据类型转换以便与您正在调用的函数的预期类型相匹配。例如,如果您想将 GtkButton 对象传递给带有 GtkWidget 参数的函数,您就要编写:
gtk_widget_show (GTK_WIDGET (button));
GObject 接口
除了对 GObject 进行的许多改进之外,Glib 2 还引入了称作“接口(interface)”的 OOP 概念。为了理解接口是什么,让我们研究一个示例。
假设我们想要为应用程序创建一个 Pegasus 对象。由于飞马(pegasus)是神话中有羽翼的马(horse),OOP 编程的习惯做法就是创建一个新 Pegasus 类,它使用 Horse 类作为其父类。然后,我们将给新类添加必要的代码以支持它拥有“羽翼”这一特性。在本例中,创建 Pegasus 作为 Horse 的子类很有意义,因为它表示了这两个类之间的关系。
但是,假如我们拥有多种不相关的类,比如 Horse、Car 和 House,并且我们希望使它们都可能进行通信。这个新能力与它们彼此的关系无关 - 事实上,这三个类对于我们而言根本没什么关联。但我们希望它们都支持与通信相关的新功能。我们该怎么办呢?
接口给这个问题提供了解决方案,它允许我们给全异类添加公共功能。因此回到上面的示例,我们只要为 Horse、Car 和 House 类编写“Talk”接口。突然之间这三个不相关的类都“能够通信”了,并且能够使用我们所创建的与通信相关的新函数。并且这些与通信有关的新函数使用“Talk”接口本身与我们的对象“愉快”地进行交互。因此,由于接口,这三个不相关的类现在“讲同一种语言了”。
在 Glib 中,您可以为一个类创建任意数量的接口。因此,如果我们为 House 类创建了一个 Talk 接口,我们可以如下定义 say() 函数:
void say (Talk *mytalk, const char *myphrase);
然后,我们可以调用 say() 函数,将 myhouse 变量的数据类型转换成“Talk”接口。
say (TALK(myhouse), "hello there!");
可以在 GTK+ 2 的 GtkEditable 接口中找到更加切实可信的接口示例(请参阅参考资料以获取链接)。文本窗口小部件和条目窗口小部件都实现了这个接口。
GObject 信号
一般而言,由事件驱动的 GUI 程序包含一个主循环。在该循环中,程序一直等待从 X 服务器发出的新消息。这些消息(称为事件)由程序进行解释,并允许程序对用户选择菜单项、单击按钮等操作作出反应。
信号除了将对象相互连接之外,与事件非常相似。它们允许对象在不需要显式的事件循环的情况下自动对另一个对象状态的变化作出“反应”。只需要将一个对象的信号连接到另一个对象的方法。然后,当第一个对象“发出”信号(由于状态的内部变化)时,第二个对象“捕获”该变化并作出适当的反应。之所以不需要事件循环,是由于信号是用回调实现的 - 发出信号的结果只是调用一个 C 函数。信号是一种有效和灵活的“粘合剂”,它们把程序中的对象“粘合”在一起。
事实上,如果您阅读过以前的 GNOMEnclature 专栏文章,您将看到我们拥有相当多类似于下面这样的示例代码行:
g_signal_connect (G_OBJECT (window), "destroy", gtk_main_quit, NULL);
在上面的代码段中,我们将主窗口对象的 "destroy" 信号连接到 gtk_main_quit() 函数。由于 g_signal_connect(),当我们的窗口发出 "destroy" 信号时(当该窗口关闭时),我们的程序将自动退出。如果您过去曾经编写过事件驱动的程序,那么我认为您会发现,对于设置显式事件处理循环这一常见做法,使用信号是一个令人耳目一新的变化。
这样,我们了解了如何将信号连接到标准的 C 函数。但是我们该如何将这同一个信号连接到某个特定对象的方法呢?很简单 - 只要使用下面这个模板:
g_signal_connect (G_OBJECT (window), "destroy", classname_function, object);
只要用要调用的方法替换 classname_function,并用方法应该操作的对象指针替换 object。要了解有关信号的更多信息,请查阅本文的参考资料一节以获取一些不错的链接。
但是,请注意,Glib 和 GTK+ 2.0 对信号方面作了一些更改。尽管基本概念并没有更改,但信号系统已从 GTK+ 2.0 迁移到 Glib。这产生了一个便利的副作用:可以在非 GUI 应用程序中使用信号。如果您正在编写新 GNOME 2.0 代码,则应当使用 Glib 新的 GSignal 类;GTK+ 2.0 信号仍然存在,但只是包含了幕后使用 Glib 的 GSignal 的“包装器”。
尽管存在所有这些变化,但 Glib 信号(用 g_signal_ 作为前缀)总体上还是非常类似于 GTK+ 中对应的信号(用 gtk_signal_ 作为前缀)。但是,如果您已经创建了或正打算创建自己的定制信号,则应当阅读 GObject GSignal 参考文档(请参阅参考资料)以了解可能适用于您的某些变化。
结束语
既然我们研究了 GObject 基础知识,您应当已经掌握必要的基本概念,可以开始您的 GNOME 编程生涯了。咱们在下一篇“GNOMEnclature”专栏文章中再见,继续为 GNOME 2 平台作准备。到时候再见!