Java 中有 interface 关键字,C++ 中有抽象类或纯虚类可以与 interface 比拟,C 语言中也可以实现类似的特性。
在面试 Java 程序员时我经常问的一个问题是:接口和抽象类有什么区别。
很多编程书籍也经常说要面向接口编程,我的理解是,接口强制派生类必须实现基类(接口)定义的契约,而抽象类则允许实现继承从而导致派生类可以不实现(重写)基类(接口)定义的契约。通常这不是问题,但在有一些特定的情况,看起来不那么合适。
比如定义一个 Shape 基类,其中定义一个 draw() 方法,给一个什么都不做的默认实现(通常是空函数体),这实际没有任何意义。
再比如基类改变某个方法的实现,而派生类采用实现继承并没有重写这个方法,此时可能会导致一些奇怪的问题。以鸟为例,基类为 Bird ,我们可能会定义一个 fly() 方法,一个 walk() 方法,因为有的人认为鸟既可以走又可以飞。开始时我们在 walk() 的实现里作了假定,认为双脚交叉前进才是 walk ,可是后来发现有些鸟是双脚一齐蹦的,不会交叉前进。这个时候怎么办?基类 Bird 的 walk() 方法是否要修改、如何修改?
在 C++ 中,没有接口关键字 interface ,同时为了代码复用,经常采用实现继承。在 C 语言中,我们前面几篇文章讨论了封装、隐藏、继承、虚函数、多态等概念,虽然都可以实现,但使用起来总不如自带这些特性的语言(如 C++ 、Java )等得心应手。一旦你采用我们前面描述的方法来进行面向对象编程,就会发现,在 C 语言中正确的维护类层次是一件非常繁琐、容易出错的事情,而且要比面向对象的语言多写很多代码(这很容易理解,面向对象语言自带*,而 C 要自己造*,每实现一个类都要造一遍)。但有一点,当我们使用 C 语言作面向对象编程时,比 C++ 有明显的优势,那就是接口。
接口强制派生类实现,这点在 C 中很容易做到。而且我们在编程中,实际上多数时候也不需要那么多的继承层次,一个接口类作为基类,一个实现类继承接口类,这基本就够了。在 C 语言中采用这种方式,可以不考虑析构函数、超过 3 层继承的上下类型转换、虚函数调用回溯、虚函数表装配等等问题,我们所要做的,就是实现基类接口,通过基类指针,就只能操作继承层次中最底层的那个类的对象;而基类接口,天生就是不能实例化的(其实是实例化了没办法使用,因为结构体的函数指针没人给它赋值)。
一个示例如下:
struct base_interface { void (*func1)(struct base_interface* b); void (*func2)(struct base_interface* b); int (*func_3)(struct base_interface* b, char * arg); }; struct derived { struct base_interface bi; int x; char ch; char *name; };
上面是头文件,derived 结构体通过包含 base_interface 类型的成员 bi 来达到继承的效果;而 base_interface 无法实例化,我们没有提供相应的构造函数,也没有提供与 func_1 , func_2 等函数指针对应的实现,即便有人 malloc 了一个 base_interface ,也无法使用。
derived 类可以提供一个构造函数 new_derived ,同时在实现文件中提供 func_1 , func_2 ,func_3 的实现并将函数地址赋值给 bi 的成员,从而完成 derived 类的装配,实现 base_interface 定义的契约。
示例实现如下:
static void _derived_func_1(struct base_interface *bi) { struct derived * d = (struct derived*)bi; d->x *= 2; printf("d->name = %s\n", d->name); } /* _derived_func_2 impl */ /* _derived_func_3 impl */ struct derived *new_derived() { struct derived *d = malloc(sizeof(struct derived)); d->bi.func_1 = _derived_func_1; d->bi.func_2 = _derived_func_2; d->bi.func_3 = _derived_func_3; d->x = 0; d->ch = ‘a‘; d->name = NULL; return d; }
我们可以这么使用 base_interface 接口:
void do_something(struct base_interface *bi) { bi->func_1(bi); } int main(int argc, char **argv) { struct derived * d = new_derived(); do_something((struct base_interface*)d); return 0; }
上面的代码中 do_something 函数完全按照接口编程,而 bi 可以实际指向任意一个实现了 base_interface 接口的类的实例,在一定程序上达到多态的效果,花费的代价相当小,却可以让我们的程序提高可扩展性,降低耦合。
这种简单的方法也是我在自己的项目中使用的方法,效果不错。
好啦,C 语言面向对象编程系列就告一段落,下面是前几篇的链接,有兴趣的可以回头看看: