你如何在 C 中实现一个类? [关闭]

Posted

技术标签:

【中文标题】你如何在 C 中实现一个类? [关闭]【英文标题】:How do you implement a class in C? [closed] 【发布时间】: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 的完整方法。 【参考方案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】:

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

    在一个 结构。 定义你的函数成员 将指向您的结构的指针作为 第一个参数。 在一个标题和一个 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 answer和this 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 语言,正如您正确指出的那样,因此没有内置的方法来编写真正的类。最好的办法是查看structs 和function pointers,这些可以让你建立一个类的近似值。但是,由于 C 是过程性的,您可能需要考虑编写更多类似 C 的代码(即不尝试使用类)。

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

【讨论】:

我不会反对,但仅供参考,函数指针或从结构调用函数的能力(我想这是你的意图)与 OOP 无关。 OOP 主要是关于功能和变量的继承,这两者都可以在 C 中实现,无需函数指针或重复。

以上是关于你如何在 C 中实现一个类? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

在 C 中实现 HashMap [关闭]

如何在颤动中实现以下小部件? [关闭]

如何在 jquery 中实现 MVC [关闭]

如何在 Qt 程序中实现 IDv3 标签? [关闭]

如何在java中实现接口类

在 Java 中实现 Mixin? [关闭]