【问题标题】:How do you implement a class in C? [closed]你如何在 C 中实现一个类? [关闭]
【发布时间】:2010-11-27 02:17:27
【问题描述】:

假设我必须使用 C(没有 C++ 或面向对象的编译器)并且我没有动态内存分配,我可以使用哪些技术来实现类或类的良好近似?将“类”隔离到单独的文件中总是一个好主意吗?假设我们可以通过假设固定数量的实例来预分配内存,甚至可以在编译时将每个对象的引用定义为常量。随意对我需要实现的 OOP 概念做出假设(它会有所不同),并为每个概念提出最佳方法。

限制:

  • 我必须使用 C 而不是 OOP 因为我正在为一个 嵌入式系统,以及编译器和 预先存在的代码库是 C 语言。
  • 没有动态内存分配 因为我们没有足够的内存 合理地假设我们不会用完 如果我们开始动态分配 它。
  • 我们使用的编译器在函数指针方面没有问题

【问题讨论】:

  • 必答题:你一定要写面向对象的代码吗?如果您出于任何原因这样做,那很好,但是您将进行一场相当艰苦的战斗。如果您避免尝试在 C 中编写面向对象的代码,这可能是最好的。这当然是可能的 - 请参阅 unwind 的优秀答案 - 但它并不完全“容易”,如果您正在使用内存有限的嵌入式系统,它可能不可行。不过,我可能是错的——我不是要反驳你,只是提出一些可能没有提出的对立点。
  • 严格来说,我们不必这样做。然而,系统的复杂性使得代码无法维护。我的感觉是,降低复杂性的最好方法是实现一些 OOP 概念。感谢所有在 3 分钟内回复的人。你们又疯又快!
  • 这只是我的拙见,但 OOP 并不能使代码立即可维护。它可能更容易管理,但不一定更易于维护。您可以在 C 中使用“命名空间”(Apache Portable Runtime 使用 apr_ 为所有全局符号添加前缀,而 GLib 使用 g_ 为它们添加前缀以创建命名空间)和其他没有 OOP 的组织因素。如果你无论如何都要重组应用程序,我会考虑花一些时间来尝试提出一个更易于维护的程序结构。
  • 这个问题之前已经被无休止地讨论过了——你看过之前的答案吗?
  • 这个来源,在我的已删除答案中,也可能有帮助:planetpdf.com/codecuts/pdfs/ooc.pdf 它描述了在 C 中执行 OO 的完整方法。

标签: c class oop embedded


【解决方案1】:

这取决于您想要拥有的确切“面向对象”功能集。如果您需要重载和/或虚拟方法之类的东西,您可能需要在结构中包含函数指针:

typedef struct {
  float (*computeArea)(const ShapeClass *shape);
} ShapeClass;

float shape_computeArea(const ShapeClass *shape)
{
  return shape->computeArea(shape);
}

这将让您通过“继承”基类并实现合适的功能来实现一个类:

typedef struct {
  ShapeClass shape;
  float width, height;
} RectangleClass;

static float rectangle_computeArea(const ShapeClass *shape)
{
  const RectangleClass *rect = (const RectangleClass *) shape;
  return rect->width * rect->height;
}

这当然需要您还实现一个构造函数,以确保正确设置函数指针。通常你会为实例动态分配内存,但你也可以让调用者这样做:

void rectangle_new(RectangleClass *rect)
{
  rect->width = rect->height = 0.f;
  rect->shape.computeArea = rectangle_computeArea;
}

如果你想要几个不同的构造函数,你将不得不“装饰”函数名,你不能有多个rectangle_new()函数:

void rectangle_new_with_lengths(RectangleClass *rect, float width, float height)
{
  rectangle_new(rect);
  rect->width = width;
  rect->height = height;
}

这是一个显示用法的基本示例:

int main(void)
{
  RectangleClass r1;

  rectangle_new_with_lengths(&r1, 4.f, 5.f);
  printf("rectangle r1's area is %f units square\n", shape_computeArea(&r1));
  return 0;
}

我希望这至少能给你一些想法。要获得成功且丰富的 C 面向对象框架,请查看 glib 的 GObject 库。

另外请注意,上面没有明确的“类”建模,每个对象都有自己的方法指针,这比您通常在 C++ 中找到的要灵活一些。此外,它会消耗内存。您可以通过将方法指针填充到 class 结构中来摆脱这种情况,并为每个对象实例发明一种方法来引用一个类。

【讨论】:

  • 不必尝试编写面向对象的 C 语言,通常最好编写以 const ShapeClass *const void * 作为参数的函数吗?后者似乎在继承方面可能更好一些,但我可以看到两种方式的论点......
  • @Chris:是的,这是一个很难的问题。 :| GTK+(使用 GObject)使用正确的类,即 RectangleClass *。这意味着您经常需要进行强制转换,但它们提供了方便的宏来帮助解决这个问题,因此您始终可以仅使用 SUBCLASS(p) 将 BASECLASS *p 转换为 SUBCLASS *。
  • 我的编译器在第二行代码失败:float (*computeArea)(const ShapeClass *shape);ShapeClass 是未知类型。
  • @DanielSank 这是由于缺少“typedef struct”所需的前向声明(在给出的示例中未显示)。因为struct 引用了自己,所以在定义它之前需要一个声明。这是explained with an example here in Lundin's answer。修改示例以包含前向声明应该可以解决您的问题; typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
  • 当矩形具有并非所有形状都具有的功能时会发生什么。例如,get_corners()。圆形不会实现这一点,但矩形可能会。您如何访问不属于您继承的父类的函数?
【解决方案2】:

我也必须做一次家庭作业。我遵循了这种方法:

  1. 在一个 结构。
  2. 定义你的函数成员 将指向您的结构的指针作为 第一个参数。
  3. 在一个标题和一个 c 中执行这些操作。 结构定义的标头 & 函数声明,c for 实现。

一个简单的例子是这样的:

/// Queue.h
struct Queue
{
    /// members
}
typedef struct Queue Queue;

void push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 

【讨论】:

  • 这是我过去所做的,但是通过根据需要将函数原型放置在 .c 或 .h 文件中来增加伪造范围(正如我在回答中提到的那样)。
  • 我喜欢这个,结构声明分配了所有内存。由于某种原因,我忘记了这会很好用。
  • 我认为你需要一个typedef struct Queue Queue;
  • 或者只是 typedef struct { /* members */ } Queue;
  • #Craig:感谢提醒,已更新。
【解决方案3】:

如果您只想要一个类,请使用structs 的数组作为“对象”数据并将指向它们的指针传递给“成员”函数。您可以在声明 struct _whatever 之前使用 typedef struct _whatever Whatever 来隐藏客户端代码的实现。这样的“对象”和 C 标准库 FILE 对象没有区别。

如果您想要多个具有继承和虚函数的类,那么通常将指向函数的指针作为结构的成员,或指向虚函数表的共享指针。 GObject 库同时使用了 this 和 typedef 技巧,并且被广泛使用。

还有一本关于此技术的在线书籍 - Object Oriented Programming with ANSI C

【讨论】:

  • 酷!对于 C 语言中的 OOP 书籍还有其他建议吗?或者 C 语言中的任何其他现代设计技术? (或嵌入式系统?)
【解决方案4】:

C 接口和实现:创建可重用软件的技术David R. Hanson

http://www.informit.com/store/product.aspx?isbn=0201498413

这本书很好地解决了您的问题。它在 Addison Wesley 专业计算系列中。

基本范式是这样的:

/* for data structure foo */

FOO *myfoo;
myfoo = foo_create(...);
foo_something(myfoo, ...);
myfoo = foo_append(myfoo, ...);
foo_delete(myfoo);

【讨论】:

    【解决方案5】:

    你可以看看 GOBject。它是一个 OS 库,可为您提供执行对象的详细方法。

    http://library.gnome.org/devel/gobject/stable/

    【讨论】:

    • 非常感兴趣。有人知道许可吗?就我的工作目的而言,从法律的角度来看,将开源库放入项目中可能行不通。
    • GTK+ 以及属于该项目的所有库(包括 GObject)都在 GNU LGPL 下获得许可,这意味着您可以从专有软件链接到它们。不过,我不知道这对于嵌入式工作是否可行。
    【解决方案6】:

    我将举一个简单的例子来说明如何在 C 中完成 OOP。我知道这个主题是 2009 年的,但还是想添加它。

    /// Object.h
    typedef struct Object {
        uuid_t uuid;
    } Object;
    
    int Object_init(Object *self);
    uuid_t Object_get_uuid(Object *self);
    int Object_clean(Object *self);
    
    /// Person.h
    typedef struct Person {
        Object obj;
        char *name;
    } Person;
    
    int Person_init(Person *self, char *name);
    int Person_greet(Person *self);
    int Person_clean(Person *self);
    
    /// Object.c
    #include "object.h"
    
    int Object_init(Object *self)
    {
        self->uuid = uuid_new();
    
        return 0;
    }
    uuid_t Object_get_uuid(Object *self)
    { // Don't actually create getters in C...
        return self->uuid;
    }
    int Object_clean(Object *self)
    {
        uuid_free(self->uuid);
    
        return 0;
    }
    
    /// Person.c
    #include "person.h"
    
    int Person_init(Person *self, char *name)
    {
        Object_init(&self->obj); // Or just Object_init(&self);
        self->name = strdup(name);
    
        return 0;
    }
    int Person_greet(Person *self)
    {
        printf("Hello, %s", self->name);
    
        return 0;
    }
    int Person_clean(Person *self)
    {
        free(self->name);
        Object_clean(self);
    
        return 0;
    }
    
    /// main.c
    int main(void)
    {
        Person p;
    
        Person_init(&p, "John");
        Person_greet(&p);
        Object_get_uuid(&p); // Inherited function
        Person_clean(&p);
    
        return 0;
    }
    

    基本概念包括将“继承的类”放在结构的顶部。这样,访问结构中的前 4 个字节也会访问“继承类”中的前 4 个字节(假设非疯狂优化)。现在,当结构的指针被强制转换为“继承的类”时,“继承的类”可以访问“继承的值”,就像它正常访问它的成员一样。

    这个和一些构造函数、析构函数、分配和deallocarion函数的命名约定(我推荐init、clean、new、free)会让你走得很远。

    对于虚函数,在结构体中使用函数指针,可能使用 Class_func(...);包装也是。 至于(简单)模板,添加一个 size_t 参数来确定大小,需要一个 void* 指针,或者需要一个只具有您关心的功能的“类”类型。 (例如 int GetUUID(Object *self); GetUUID(&p);)

    【讨论】:

    • 免责声明:所有代码都写在智能手机上。在需要的地方添加错误检查。检查错误。
    【解决方案7】:

    使用struct 模拟类的数据成员。在方法范围方面,您可以通过将 private 函数原型放在 .c 文件中并将 public 函数放在 .h 文件中来模拟私有方法。

    【讨论】:

      【解决方案8】:
      #include <stdio.h>
      #include <math.h>
      #include <string.h>
      #include <uchar.h>
      
      /**
       * Define Shape class
       */
      typedef struct Shape Shape;
      struct Shape {
          /**
           * Variables header...
           */
          double width, height;
      
          /**
           * Functions header...
           */
          double (*area)(Shape *shape);
      };
      
      /**
       * Functions
       */
      double calc(Shape *shape) {
              return shape->width * shape->height;
      }
      
      /**
       * Constructor
       */
      Shape _Shape() {
          Shape s;
      
          s.width = 1;
          s.height = 1;
      
          s.area = calc;
      
          return s;
      }
      
      /********************************************/
      
      int main() {
          Shape s1 = _Shape();
          s1.width = 5.35;
          s1.height = 12.5462;
      
          printf("Hello World\n\n");
      
          printf("User.width = %f\n", s1.width);
          printf("User.height = %f\n", s1.height);
          printf("User.area = %f\n\n", s1.area(&s1));
      
          printf("Made with \xe2\x99\xa5 \n");
      
          return 0;
      };
      

      【讨论】:

      • 避免使用_Shape 之类的名称。那将是未定义的行为。以下划线开头,后跟大写字母的名称为reserved identifiers
      【解决方案9】:

      在您的情况下,该类的良好近似值可能是ADT。但还是不一样。

      【讨论】:

      • 谁能简要介绍一下抽象数据类型和类之间的区别?我一直认为这两个概念密切相关。
      • 它们确实是密切相关的。一个类可以被视为一个 ADT 的实现,因为(假设)它可以被另一个满足相同接口的实现替换。我认为很难给出确切的差异,因为这些概念没有明确定义。
      【解决方案10】:

      我的策略是:

      • 在单独的文件中定义类的所有代码
      • 在单独的头文件中定义类的所有接口
      • 所有成员函数都采用代表实例名称的“ClassHandle”(而不是 o.foo(),调用 foo(oHandle)
      • 构造函数替换为函数 void ClassInit(ClassHandle h, int x, int y,...) 或 ClassHandle ClassInit(int x, int y,...) 取决于内存分配策略
      • 所有成员变量都作为静态结构的成员存储在类文件中,将其封装在文件中,防止外部文件访问它
      • 对象存储在上述静态结构的数组中,具有预定义的句柄(在界面中可见)或可以实例化的对象的固定限制
      • 如果有用,该类可以包含公共函数,这些函数将遍历数组并调用所有实例化对象的函数(RunAll() 调用每个 Run(oHandle)
      • Deinit(ClassHandle h) 函数在动态分配策略中释放分配的内存(数组索引)

      是否有人看到这种方法的任何一种变体存在任何问题、漏洞、潜在陷阱或隐藏的好处/缺点?如果我正在重新发明一种设计方法(我认为我必须这样做),你能指出它的名称吗?

      【讨论】:

      • 作为风格问题,如果您有信息要添加到您的问题中,您应该编辑您的问题以包含此信息。
      • 你似乎已经从 malloc 动态分配从一个大堆转移到 ClassInit() 动态地从一个固定大小的池中选择,而不是实际做任何关于当你请求另一个对象时会发生什么并不要'没有资源提供一个。
      • 是的,内存管理负担转移到调用 ClassInit() 以检查返回的句柄是否有效的代码上。本质上,我们已经为该类创建了自己的专用堆。如果我们想做任何动态分配,我不确定我是否有办法避免这种情况,除非我们实现了一个通用堆。我更愿意将堆中的风险继承隔离到一个类。
      【解决方案11】:

      另见this answerthis one

      这是可能的。这在当时似乎总是一个好主意,但后来它变成了维护的噩梦。您的代码中到处都是将所有内容联系在一起的代码。如果您使用函数指针,新程序员在阅读和理解代码时会遇到很多问题,因为调用什么函数并不明显。

      使用 get/set 函数进行数据隐藏很容易在 C 中实现,但仅止于此。我已经在嵌入式环境中看到过多次尝试,最终它始终是一个维护问题。

      既然你们都准备好了维护问题,我会避开。

      【讨论】:

        【解决方案12】:

        GTK 完全建立在 C 之上,它使用了许多 OOP 概念。我通读了 GTK 的源代码,它给人留下了深刻的印象,而且绝对更容易阅读。基本概念是每个“类”只是一个结构和相关的静态函数。静态函数都接受“实例”结构作为参数,然后做任何需要的事情,并在必要时返回结果。例如,您可能有一个函数“GetPosition(CircleStruct obj)”。该函数将简单地挖掘结构,提取位置编号,可能构建一个新的 PositionStruct 对象,将 x 和 y 粘贴到新的 PositionStruct 中,然后返回它。 GTK 甚至通过在结构中嵌入结构来实现继承。很聪明。

        【讨论】:

          【解决方案13】:

          我的方法是将struct 和所有主要关联的 函数移到单独的源文件中,以便可以“便携”地使用它。

          根据您的编译器,您可能可以将函数包含到 struct 中,但这是一个 非常 特定于编译器的扩展,与我经常使用的标准的最后一个版本:)

          【讨论】:

          • 函数指针都不错。我们倾向于使用它们来用查找表替换大型 switch 语句。
          【解决方案14】:

          第一个 c++ 编译器实际上是将 C++ 代码翻译成 C 的预处理器。

          所以很有可能在 C 中创建类。 您可以尝试挖掘一个旧的 C++ 预处理器,看看它会创建什么样的解决方案。

          【讨论】:

          • 那就是cfront;将异常添加到 C++ 时遇到了问题 - 处理异常并非易事。
          【解决方案15】:

          你想要虚拟方法吗?

          如果不是,那么您只需在结构本身中定义一组函数指针。如果您将所有函数指针分配给标准 C 函数,那么您将能够以与在 C++ 下的语法非常相似的语法从 C 中调用函数。

          如果你想拥有虚拟方法,它会变得更加复杂。基本上,您需要为每个结构实现自己的 VTable,并根据调用的函数将函数指针分配给 VTable。然后,您将需要结构本身中的一组函数指针,这些指针又调用 VTable 中的函数指针。这本质上就是 C++ 所做的。

          TBH 但是...如果您想要后者,那么您最好找到一个可以使用的 C++ 编译器并重新编译该项目。我从来没有理解过对 C++ 不能在嵌入式中使用的痴迷。我已经用过很多次了,它的工作速度很快,而且没有内存问题。当然,你必须对你所做的事情更加小心,但实际上并没有那么复杂。

          【讨论】:

          • 我已经说过并且会再说一遍,但会再说一遍:你不需要函数指针或从结构 C++ 风格调用函数的能力来创建 C 中的 OOP,OOP主要是关于功能和变量(内容)的继承,这两者都可以在 C 中实现,无需函数指针或复制代码。
          【解决方案16】:

          C 不是 OOP 语言,正如您正确指出的那样,因此没有内置的方法来编写真正的类。最好的办法是查看structsfunction pointers,这些可以让你建立一个类的近似值。但是,由于 C 是过程性的,您可能需要考虑编写更多类似 C 的代码(即不尝试使用类)。

          另外,如果你会使用 C,你可能会使用 C++ 并获取类。

          【讨论】:

          • 我不会反对,但仅供参考,函数指针或从结构调用函数的能力(我想这是你的意图)与 OOP 无关。 OOP 主要是关于功能和变量的继承,这两者都可以在 C 中实现,无需函数指针或重复。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-06-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-11-28
          • 2014-01-18
          相关资源
          最近更新 更多