首页 > 解决方案 > 在 C 中建立 OOP 设计风格的名称

问题描述

在 C 中,当实现对某些数据起作用的函数时,我至少可以想到以下常用样式来实现它们:

风格1:最简单的一种。用户直接调用函数并提供指向数据的指针作为参数。

typedef struct
{
    int data;
}SomeStruct;

void setData(SomeStruct *s, int value)
{
    s->data = value;
}

    // construction
    SomeStruct s;
    
    // usage
    setData(&s, 123);

风格二:用户通过函数指针调用函数,但仍需要手动提供指向数据的指针。

typedef struct SomeStruct SomeStruct;
struct SomeStruct
{
    void (*set)(SomeStruct*, int);
    int data;
};

void setData(SomeStruct *s, int value)
{
    s->data = value;
}

    // construction
    SomeStruct s;
    s.set = setData;
    
    // usage
    s.set(&s, 123);

风格三:用户通过函数指针调用函数,不需要手动提供指向数据的指针。

typedef struct SomeStruct SomeStruct;
struct SomeStruct
{
    void (*set)(int);
    int data;
};
#define MAX_INSTANCES 2
SomeStruct structs[MAX_INSTANCES];

void setData(SomeStruct *s, int value)
{
    s->data = value;
}

void setData0(int value)
{
    setData(&structs[0], value);
}
void setData1(int value)
{
    setData(&structs[1], value);
}

    // construction
    structs[0].set = setData0;
    structs[1].set = setData1;
    
    SomeStruct *s0 = &structs[0];
    
    // usage
    s0->set(123);

在样式 2 和 3 中,数据和 setData 可以对用户隐藏,但为了简单起见,我在这里省略了这一点。

问题是:这些风格是否有既定的名称?

标签: c

解决方案


风格 1 是传统的 C.

Style 2 是面向对象 C 的开端。如果它有一个名字,那就是“方法实现模式”

问题是样式 2 的方法名称不正确。

void SomeStruct_setData(SomeStruct *s, int value)
{
    s->data = value;
}

或者

void SomeStruct_data(SomeStruct *s, int value)
{
    s->data = value;
}

好一点,因为现在它表明了SomeStruct被视为对象的意图。

存在模拟更多面向对象系统的更高级的方法。他们通常提供(在标题中)

struct SomeStruct;

并将私有结构放在SomeStruct.c文件中。这阻止了直接访问字段的能力,迫使所有修改SomeStruct者使用这些SomeStruct_*(struct SomeStruct*, ...)方法。通常,这些方法将使用 typdefs 将稍微重命名的结构呈现struct SomeSturct_s*SomeStruct.

样式 3 是在 C 中可能被称为“vtable 实现”的众多变体之一。它通常是多态性的前身

请记住,C 没有真正的多态类型系统,因此实际类型通常void *在实现中转换为;但是,通过所有对象struct字段的模式,您可以将类型嵌入到结构中(通常使用uint32_torenum完成,有时使用更复杂的类型处理例程完成)。

无论如何,最终您将需要一个改变行为的函数,并且该函数被实现为一个函数指针,其中许多不同的 C 风格函数提供行为,以及类型分配/处理选择要分配给的正确函数实现类型行为的结构。基本上这是一个迷你 vtable,如果你想把它翻译成 C++ 术语。

一些系统从基于对象类型的 switch 语句开始,通常从结构中读取(在每个对象结构所需的固定位置字段中)。最终他们意识到它们只是一个 switch 语句,它将参数传递给基于基类类型的一个方法。通过使用函数指针并利用构造时间分配,可以优化 switch 语句。

面向对象的 C 是一个东西,但它不是一个标准。

很多时候人们都想使用面向对象的 C。维护的便利性和清晰的测试描述通常是人们选择这种方法的驱动因素。也就是说,实际系统将类似于“你用 C 编写面向对象的模拟”,而且你通常不会完成你正在模拟的任何 OO 系统的所有功能。

为了使这更容易,有多种方法:

  • 编写一种扩展为 C 的迷你语言。这是 C++ 的原始方法,直到他们认为在编译阶段一直进行类型检查的好处值得将预处理语言与编译器结合起来。

  • 使用预先构建的面向对象的 C 环境。GNOME 已经构建了他们的 GObject 库,虽然语法和宏的选择可能需要一些时间来适应,但整个库是完整的、健壮的和经过测试的。这个“C”作为基础层,允许 GNOME 被移植到比其传统竞争对手 KDE 更多的平台上,后者需要 C++ 编译器(在更少的平台上可用)。

  • 将您的代码重构为可重用的组件,以加快构建功能不全的 OO 系统。通过宏扩展或包含方法使组件可重用,您将加快新对象的创建速度,并减少调试它们以验证它们作为对象工作的时间。

第一步很容易实现(非动态方法,没有复杂的内置类型检查的 vtables),但随着您的进行,它会变得越来越难(但不是太难)。我已经构建了一个系统(为了好玩),它甚至可以正确地使用宏实现“try/catch”语义,这些宏使用setjmplongjmpC 函数来处理具有多态性的抛出对象,完成重新抛出、重新捕获和重新处理程序类似 Java 的行为。

如果你喜欢玩这些玩具,我建议你熟练使用 C 语言进行单元测试,使用类似autotools或类似的工具;因为,至少在早期阶段,您会发现最基本的方法无法捕捉到接近完成时所需的所有行为,并且拥有一个测试套件可以防止您破坏之前的工作。


推荐阅读