面向对象的 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

阅读余下内容
 

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注


京ICP备12002735号