面向对象的 C 语言入门
C 语言是一种神奇的语言,它既小巧又高效,既漂亮又丑陋。但它所缺乏的(让现代开发人员感到绝望的)是面向对象编程(OOP)设施。是的,有 C++,但在好的社交圈子里,人们不会谈论 C++。尤其是在可以用 C 语言模拟类、对象和方法的情况下。
本篇文章围绕面向对象编程概念展开。从最简单的开始,慢慢增加难度。请做好准备:面向对象编程是一套相当模糊的概念。我将尽可能地利用这种模糊性–既不脱离人们所说的 OOP,又不脱离 C 语言及其受人祝福的方式。剧透一下:最终的系统将是一个基于通用的单继承系统。
我们要模拟的问题领域是:动物。这里有动物(是的,一个经典的 OOP 例子)。动物有很多家族和种类。我有一只可爱的猫,名叫 Kalyam,所以我主要对猫科动物感兴趣。
我将只对生物层次结构的这一部分进行建模。希望我们有足够的材料来测试我建议的方法。
封装
这个很简单。最简单的定义是,封装就是把东西放到它们的桶(称为类)中。封装也可能意味着将数据隐藏在类中,但请参见可见性部分。封装也可能意味着将行为(方法)置于类中。但这一点值得商榷:有些语言采用面向泛型的 OOP,方法是独立的实体。如果有的话,方法在那里属于它们的泛型函数。这就是我要使用的方法。C 语言(从 C11 开始)已经有了泛型调度,为什么不使用已有的功能呢?
暂且抛开泛型不谈,让我们来封装一些数据,好吗?
struct animal { char *name; char *species; };
就这样,我们的父类就准备好了。数据包含在其中,所以我们已经有了封装。我们还没有动物类、目、属、种等。因此,让我们来创建一种已经灭绝的动物,请原谅我贫乏的生物学知识:
struct animal oldie = {.name = "Oldie", .species = "Miacid"}; printf("This really old animal is %s of %s specie\n", oldie.name, oldie.species);
如果您不喜欢结构体,您可以定义一个宏和一个类型别名(大写,以取悦 Java 用户):
#define class struct typedef class animal Animal;
这就是封装的全部内容,真的。
继承
说完了封装,就该说说继承了。这是一种让类之间相互依赖并共享行为/数据的方式。
几天前,我从 “Good Taste “系列文章中学到了一个小技巧:将结构相互嵌入。将一个结构作为另一个结构的第一个成员,就能使外部结构可投射到内部结构,从而共享两者之间的行为:
typedef class carnivoire { Animal parent; } Carnivoire;
现在,我们可以将任何动物转换为 (Animal *
) 并调用 Animal
方法:
Carnivoire sabre_tooth = {{.name = "Diego", .species = "Dinictis"}}; eats((Animal *)&sabre_tooth);
Example of Carnivoire use
不,等等,我们的动物还不知道怎么吃东西!让我们用多态性来教它们吧!
多态性
动物有一种默认行为:它们会吃(咄咄怪事)。这就是为什么上图包含了 eats()
方法:任何动物类都会吃东西。然而,动物有各种各样。有的吃植物。有的吃真菌。有的吃其他动物(呵呵,递归吗?下面我们用代码来表达:
void animal_eats (Animal *self) { printf("%s eats ???\n", self->name); } #define eats(animal) _Generic((animal), default: animal_eats) ((animal))
作为宏实现的多态 eats()
方法
目前,我们的 eats()
宏/泛型只有一个默认方法:animal_eat
。但你已经可以看到,只需再写一行 type+method 就能扩展它。让我们实际操作一下:
void carnivoire_eats (Carnivoire *self) { printf("%s eats meat (a shame—it involves killing other animals)\n", self->parent.name); } #define eats(animal) _Generic((animal), Carnivoire *: carnivoire_eats, default: animal_eats) ((__VA_ARGS__))
Carnivore eats() method
在泛型中只需再写一行,我们就能得到食肉动物的特定行为!这就是多态性的承诺:给定类型,指定行为。
eats(&sabre_tooth); // Diego eats meat...
Using carnivoire-specific eats() method
可见性
大多数 OOP 系统都有私有/公有/受保护的区别。基于 Python 没有可见性这一概念,我可以很容易地把它抛到一边。但无论如何,我会尝试实现它。
诀窍在于将结构视为不透明数据。我的意思是,代码的用户不必知道结构的数据布局。他们必须将其作为原始指针使用,无论如何都要依赖外部 extern
-ed 函数。许多代码库都利用了这一点。它们倾向于将指向 “private” 结构版本的指针隐藏起来,嵌套在”public” 结构版本中。WebKitGTK 就是这样做的:
class WebKit2.WebViewBase : Gtk.Container implements Atk.ImplementorIface, Gtk.Buildable { priv: WebKitWebViewBasePrivate* }
WebKit WebView private structure example
根据这一传统,我们可以说结构默认是私有的。公共的是它们的getter和setter。那么,为什么不为我们的结构定义一些getter和setter呢?
char *animal_get_name (Animal *self) { return self->name; } void animal_set_name (Animal *self, char *name) { self->name = name; }
Example getter/setter for animals
这太无聊了,所以我们来定义一个新类,其中包含私有字段和方法:
#define private typedef class feline { Carnivoire parent; private bool claws_out; } Feline; // Don't have to define name getter/setter: animal has it already. bool feline_get_claws_out (Feline *self) { return self->claws_out; } void feline_protract_claws (Feline *self) { self->claws_out = true; } void feline_retract_claws (Feline *self) { self->claws_out = false; }
Feline (cat-like) class with special behavior for claws
请注意,我们没有为 claws_out
-retract/protract 方法提供一个 setter 来处理修改。将实际数据隐藏在行为背后是一种重要的 OOP 技术。
使用 OOP 系统
到目前为止的代码非常简单,没有太多的 OOP。本节和示例将最终对系统进行测试。让我们定义猫(我一直在等这个!)和它们的行为:
typedef class cat { Feline parent; } Cat; void cat_purr (Cat *self) { printf("%s purrs...\n", animal_get_name(self)); feline_retract_claws(self); } void cat_eats (Cat *self) { printf("%s eats mice\n", animal_get_name(self)); } #define eats(animal) _Generic((animal), Carnivoire *: carnivoire_eats, Cat *: cat_eats, default: animal_eats) ((animal))
Cats and their methods
测试该系统输出:
// My little sweet boy Cat Kalyam = {{{{.name = "Kalyam", .species = "Felis catus"}}, .claws_out = true}}; printf("%s's claws are %stracted\n", animal_get_name(&Kalyam), (feline_get_claws_out(&Kalyam) ? "pro" : "re")); // Kalyam's claws are protracted eats(&Kalyam); // Kalyam eats mice cat_purr(&Kalyam); printf("%s's claws are %stracted\n", animal_get_name(&Kalyam), (feline_get_claws_out(&Kalyam) ? "pro" : "re")); // Kalyam's claws are retracted
Actually using the system
这些嵌套的大括号看起来不太对劲,应该是构造方法。不过这篇文章已经太长了,还是留到以后再说吧。重要的是:封装、继承、多态性和可见性已经存在。C 语言可以实现 OOP。这其实并不难–这篇文章所涉及的内容非常简单,也很容易扩展。
你可以查看 oop-c-primer-original.c 中的最终代码(在 GCC 和 Clang 中编译,即使有大量警告),以及 oop-c-primer-cleanup.c 中经过清理的代码(如果你想看更复杂的继承层次结构,还可以稍微扩展一下鸟类的具体细节)。感谢您陪伴我走过这段旅程!
本文文字及图片出自 Object-Oriented C: A Primer