人们在C语言中构建对象时使用哪些技术/策略?

7

我特别关注于在C语言内部使用的对象,而不是像Python这样的解释型语言的核心对象实现。


1
虽然我一直使用的是可以称之为“基于对象”的C编程风格,但在C++中进行这种操作(以及几乎所有其他操作)要容易得多。我不得不问一下,为什么您似乎不想使用它呢? - anon
1
@Neil:如果没有理由,就不会有 GObject。C 有很多原因:过时的平台、可移植性、二进制 ABI 兼容性等等,你可以自己命名。 - EFraim
@EFraim 我很清楚那些原因,但我想知道提问者的原因是什么。 - anon
2
@Niel - 不想将C++作为依赖项注入应该足够理由了。此外,有很多聪明的解决方案...我希望在这里记录/比较它们。 - Setjmp
6个回答

9

我通常会像这样做:

struct foo_ops {
    void (*blah)(struct foo *, ...);
    void (*plugh)(struct foo *, ...);
};
struct foo {
    struct foo_ops *ops;
    /* data fields for foo go here */
};

有了这些结构定义,实现foo函数的代码看起来像这样:

static void plugh(struct foo *, ...) { ... }
static void blah(struct foo *, ...) { ... }

static struct foo_ops foo_ops = { blah, plugh };

struct foo *new_foo(...) {
   struct foo *foop = malloc(sizeof(*foop));
   foop->ops = &foo_ops;
   /* fill in rest of *foop */
   return foop;
} 

然后,在使用foo的代码中:

struct foo *foop = new_foo(...);
foop->ops->blah(foop, ...);
foop->ops->plugh(foop, ...);

这段代码可以通过宏或内联函数来优化,使其看起来更像C语言。

foo_blah(foop, ...);
foo_plugh(foop, ...);

尽管对于“ops”字段选择一个相对较短的名称,仅写出最初显示的代码并不特别冗长。这种技术完全足以在C语言中实现相对简单的基于对象的设计,但它无法处理更高级的要求,例如显式表示类和方法继承。对于这些情况,您可能需要像GObject这样的东西(正如EFraim所提到的),但我建议确保您真正需要更复杂框架的额外功能。

7
您使用“对象”一词有些模糊,因此我假设您想知道如何使用C语言实现面向对象编程的某些方面(如果我的假设不正确,请随时更正)。 方法多态: 通常使用函数指针来模拟方法多态。例如,如果我有一个用于表示图像缩放器的结构体(将图像调整为新尺寸的工具),我可以这样做:
struct image_scaler {
    //member variables
    int (*scale)(int, int, int*);
}

然后,我可以制作几个如下的图像缩放器:

struct image_scaler nn, bilinear;
nn->scale = &nearest_neighbor_scale;
bilinear->scale = &bilinear_scale;

这让我可以通过简单地传递不同的image_scaler,为任何需要输入image_scaler并使用其scale方法的函数实现多态行为。 继承 通常通过以下方式实现继承:
struct base{
   int x;
   int y;
} 

struct derived{
   struct base;
   int z;
}

现在,我可以自由使用派生类的额外字段,并获取基类的所有“继承”字段。此外,如果您有一个仅接受结构体基类的函数,您可以将您的结构体派生指针简单地转换为没有任何后果的结构体基类指针。


谢谢Falain(我点赞了)。你能指出一个库,这样我或其他人就可以检查整个实现吗? - Setjmp

4

类库例如GObject

基本上,GObject提供了一种通用的描述不透明值(整数、字符串)和对象(手动描述接口 - 作为函数指针结构,基本上对应于C++中的VTable - 更多关于该结构的信息可以在其参考文献中找到)的方式。

你经常也会像"COM in plain C"中那样手动实现vtables。


EFraim - 你能描述一下你的 GObject 实现策略,这样我们就可以将其与其他解决方案进行比较吗? - Setjmp

3

从浏览所有答案可以看出,有库、函数指针、继承、封装等等可用的方法(C++最初是C的前端)。

然而,我发现软件中非常重要的一个方面是可读性。你试过读10年前的代码吗?因此,在使用C中做对象等事情时,我倾向于采取最简单的方法。

问以下问题:

  1. 这是为有期限的客户吗(如果是,考虑OOP)?
  2. 我能用OOP吗(通常代码更少,开发速度更快,更易读)?
  3. 我能用库吗(现有代码,现有模板)?
  4. 我受到内存或CPU的限制吗(例如Arduino)?
  5. 还有其他技术原因要使用C吗?
  6. 我能保持我的C非常简单和易读吗?
  7. 我真正需要哪些OOP特性来完成我的项目?

我通常会回到像GLIB API这样的东西,它允许我封装我的代码并提供非常易读的接口。如果需要更多,我会添加函数指针以进行多态。

class_A.h:
  typedef struct _class_A {...} Class_A;
  Class_A* Class_A_new();
  void Class_A_empty();
  ...

#include "class_A.h"
Class_A* my_instance;
my_instance = Class_A_new();
my_instance->Class_A_empty();  // can override using function pointers

0
与Dale的方法类似,但更容易出错的是PostgreSQL在内部表示解析树节点、表达式类型等。有默认的NodeExpr结构,例如:
typedef struct {
    NodeTag n;
} Node;

其中NodeTag是无符号整数的typedef,有一个头文件,其中包含描述所有可能节点类型的一堆常量。节点本身看起来像这样:

typedef struct {
    NodeTag n = FOO_NODE;
    /* other members go here */
} FooNode;

一个 FooNode 可以毫不顾虑地转换为一个 Node,因为 C 结构的一个怪癖:如果两个结构体具有相同的第一个成员,则它们可以互相转换。

是的,这意味着一个 FooNode 可以转换为一个 BarNode,这可能不是你想要的。如果你想要正确的运行时类型检查,GObject 是一种好的选择,尽管在掌握它的过程中要准备好讨厌生活。

(注意:这些例子是从记忆中得出的,我已经有一段时间没有对 Postgres 内部进行过修改了。开发者常见问题解答 中有更多信息。)


很久很久以前(大约1988年),我曾经参与开发一个C编译器,其中解析节点是各个节点类型的位联合体,并且有一个类型标签在联合体外部,用于决定哪个联合体分支是有效的:这在道义上等同于您所描述的内容。今天,我几乎肯定会按照我上面的建议来做。我发现使用函数指针确实迫使我定义所需的公共API——因为调用者不知道它正在调用的函数的名称,所以很难让它越过该函数并使用实现的内部数据。 - Dale Hagglund
唉...在上面的代码中,s/bit union/big union/。抱歉打错字了。 - Dale Hagglund
函数指针方法绝对是更优秀的——首先,它让你可以近似地将绑定方法应用于对象。我喜欢你的例子,并投了赞成票。 - Meredith L. Patterson

0
看看IJG的实现。他们不仅使用setjmp/longjmp进行异常处理,还有虚函数表等。这是一个编写得很好且足够小的库,可以为您提供非常好的示例。

你知道他们是否正在使用 Dale 和 Falaina 建议的实现方式?还是更加动态一些的实现方式? - Setjmp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接