您如何在C中实现类?[关闭]


139

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

限制条件:

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

26
强制性问题:是否必须编写面向对象的代码?如果您出于任何原因这样做,那都很好,但是您将要进行一场艰苦的战斗。如果您避免尝试使用C编写面向对象的代码,那可能是最好的选择。当然有可能-看到unwind的出色答案-但这并不完全“容易”,并且如果您在内存有限的嵌入式系统上工作,可能不可行。不过,我可能是错的-我并不是想与您争辩,只是提出一些可能没有提出的对策。
克里斯·卢茨

1
严格来说,我们不必这样做。但是,系统的复杂性使代码无法维护。我的感觉是,降低复杂度的最佳方法是实现一些OOP概念。感谢所有在3分钟内回复的人。你们都快疯了!
本·加特纳

8
这只是我的拙见,但是OOP不能使代码立即可维护。它可能使管理起来更容易,但不一定更易于维护。您可以在C中使用“命名空间”(Apache Portable Runtime在所有全局符号apr_前面加上GLib,在GLib前面加上它们g_来创建一个命名空间)以及其他没有OOP的组织因素。如果您仍然要对应用程序进行重组,我会考虑花一些时间尝试提出一个更具可维护性的程序结构。
克里斯·卢兹

之前对此进行了无休止的讨论-您是否看过之前的任何答案?
拉里·渡边

此源,这是我的答案被删除,也可能会有所帮助:planetpdf.com/codecuts/pdfs/ooc.pdf它描述了一个完整的方法为C.做OO
鲁本斯坦

Answers:


86

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

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 Lutz

1
@克里斯:是的,这是一个很难的问题。:| GTK +(使用GObject)使用适当的类,即RectangleClass *。这意味着您通常必须进行强制转换,但是它们提供了方便的宏来帮助您完成转换,因此您始终可以仅使用SUBCLASS(p)将BASECLASS * p强制转换为SUBCLASS *。
放松

1
我的编译器在第二行代码上失败:float (*computeArea)(const ShapeClass *shape);说这ShapeClass是未知类型。
DanielSank

@DanielSank,这是由于缺少'typedef struct`所需的前向声明(在给定的示例中未显示)。因为struct引用本身,所以定义之前需要声明伦丁的答案中用一个例子对此进行了解释。修改示例以包括前向声明应该可以解决您的问题;typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
S. Whittaker

当Rectangle具有并非所有Shape都具有的功能时,会发生什么。例如,get_corners()。圆形不会实现此功能,但矩形可能会实现。如何访问不继承自父类的函数?
Otus

24

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

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

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

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

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

这是我过去所做的事情,但是通过根据需要将函数原型放置在.c或.h文件中(如我在回答中提到的那样)来增加伪造范围。
泰勒里斯

我喜欢这样,struct声明分配所有内存。由于某种原因,我忘记了它会很好地工作。
本•加特纳

我认为您需要typedef struct Queue Queue;在那里。
Craig McQueen

3
或者只是typedef struct {/ * Members * /} Queue;
Brooks Moses

#Craig:感谢您的提醒,已更新。
erelender

12

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

如果您想要多个具有继承和虚拟函数的类,那么通常将指向函数的指针作为结构的成员,或者将指向虚拟函数的表的共享指针作为对象。该GObject的库同时使用这一点,typedef的把戏,而被广泛使用。

还有一本关于此可用在线技术的书-ANSI C的面向对象编程


1
凉!对于C语言中有关OOP的书籍还有其他建议吗?还是C中的任何其他现代设计技术?(或嵌入式系统?)
Ben Gartner,2009年

7

您可以看一下GOBject。它是一个OS库,为您提供了一种处理对象的详细方法。

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


1
很感兴趣。有人知道许可吗?就我的工作目的而言,从法律的角度来看,将开源库放入项目中可能无法正常工作。
本·加特纳

GTK +以及该项目中的所有库(包括GObject)均已获得GNU LGPL的许可,这意味着您可以从专有软件链接到它们。不过,我不知道对于嵌入式工作这是否可行。
克里斯·卢兹


5

我将给出一个简单的示例,说明如何在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个字节(假定非疯狂的优化)。现在,当将结构体的指针强制转换为“继承的类”时,“继承的类”可以像通常访问其成员一样访问“继承的值”。

对于构造函数,析构函数,分配和释放函数的命名约定和一些命名约定(我建议使用init,clean,new,free)将使您大有帮助。

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


免责声明:所有代码都写在智能手机上。在需要的地方添加错误检查。检查错误。
yyny

4

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


4
#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;
};

3

在您的情况下,该类的一个很好的近似值可以是ADT。但是仍然会不一样。


1
任何人都可以在抽象数据类型和类之间进行简短的区别吗?我一直将两个概念紧密联系在一起。
本·加特纳

它们确实紧密相关。可以将类视为ADT的实现,因为(据说)可以用满足相同接口的另一种实现替换它。我认为很难给出确切的区别,因为这些概念没有明确定义。
约根·福(JørgenFogh),2009年

3

我的策略是:

  • 在单独的文件中定义该类的所有代码
  • 在单独的头文件中定义该类的所有接口
  • 所有成员函数都使用“ 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()动态地从固定大小的池中选择,而不是实际上对当您请求另一个对象而没有资源提供一个对象时会发生的事情做任何事情。
Pete Kirkham

是的,内存管理负担转移到了调用ClassInit()的代码上,以检查返回的句柄是否有效。本质上,我们为该类创建了自己的专用堆。除非我们实现了通用堆,否则不确定是否要进行任何动态分配时会避免这种情况。我宁愿将继承的风险隔离到一个类中。
本·加特纳

3

另请参阅此答案这个

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

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

由于大家都准备好了维护问题,所以我会避免的。


2

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

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


2
函数指针都很好。我们倾向于使用它们用查找表替换大型switch语句。
本·加特纳

2

实际上,第一个c ++编译器是一个预处理器,它将C ++代码转换为C。

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


那将会是cfront; 当将异常添加到C ++时,它会遇到问题-处理异常并非微不足道。
乔纳森·莱夫勒

2

GTK完全基于C构建,并且使用许多OOP概念。我已经阅读了GTK的源代码,它给人留下了深刻的印象,并且绝对易于阅读。基本概念是每个“类”都只是一个结构以及相关联的静态函数。所有静态函数均接受“实例”结构作为参数,然后执行所需的任何操作,并在必要时返回结果。例如,您可能具有一个函数“ GetPosition(CircleStruct obj)”。该函数将简单地遍历该结构,提取位置编号,可能会构建一个新的PositionStruct对象,将x和y粘贴在新的PositionStruct中,然后将其返回。GTK甚至通过将结构嵌入结构中来实现继承。相当聪明。


1

您是否需要虚拟方法?

如果不是,则只需在结构本身中定义一组函数指针。如果将所有函数指针分配给标准C函数,则可以使用与C ++十分相似的语法从C调用函数。

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

但是,TBH ...如果需要后者,那么最好是找到一个可以使用并重新编译该项目的C ++编译器。我从未理解过对C ++无法在嵌入式系统中使用的痴迷。我已经使用了很多次,并且运行速度很快,并且没有内存问题。当然,您必须对自己的操作更加谨慎,但实际上并没有那么复杂。


我已经说过了,会再说一遍,但是会再说一遍:您不需要函数指针或从结构C ++样式调用函数的能力即可在C中创建OOP,OOP主要是关于功能和变量的继承(内容)都可以在C中实现,而无需使用函数指针或重复代码。
yyny

0

正如您正确指出的那样,C不是一种OOP语言,因此没有内置的方法可以编写真实的类。最好的选择是看struct函数指针,这些将使您构建类的近似值。但是,由于C是过程性的,因此您可能需要考虑编写更多类似C的代码(即,不尝试使用类)。

另外,如果可以使用C,则可以使用C ++并获取类。


4
我不会拒绝投票,但是FYI,函数指针或从结构调用函数的能力(我想这是您的意图)与OOP无关。OOP主要是关于功能和变量的继承,这两者都可以在C中实现,而无需使用函数指针或重复项。
yyny
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.