你如何在C中实现一个类?

时间:2009-09-10 07:47:29

标签: c class oop embedded

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

限制:

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

18 个答案:

答案 0 :(得分:79)

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

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结构中填充方法指针来远离它,并为每个对象实例创建一种引用类的方法。

答案 1 :(得分:23)

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

  1. 在a中定义您的数据成员 结构体。
  2. 定义您的功能成员 将指针指向你的结构 第一个论点。
  3. 在一个标题和标题中执行这些操作一个c。 结构定义的标题& 函数声明,c表示 的实施方式。
  4. 一个简单的例子是:

    /// Queue.h
    struct Queue
    {
        /// members
    }
    typedef struct Queue Queue;
    
    void push(Queue* q, int element);
    void pop(Queue* q);
    // etc.
    /// 
    

答案 2 :(得分:12)

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

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

还有一本关于这方面技术的书籍 - Object Oriented Programming with ANSI C

答案 3 :(得分:7)

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);

答案 4 :(得分:7)

你可以看看GOBject。它是一个操作系统库,为您提供了一个简单的方法来完成一个对象。

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

答案 5 :(得分:4)

我将举一个简单的例子,说明如何在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)将为你提供很长的路要走。

对于虚函数,使用struct中的函数指针,可能使用Class_func(...);包装也是。 对于(简单)模板,添加size_t参数以确定大小,需要void *指针,或者需要一个'类'键入只有你关心的功能。 (例如,int GetUUID(Object * self); GetUUID(& p);)

答案 6 :(得分:4)

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

答案 7 :(得分:3)

在你的情况下,类的良好近似可以是ADT。但它仍然不一样。

答案 8 :(得分: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)函数释放动态分配策略中分配的内存(数组索引)

有没有人看到这种方法的变化有任何问题,漏洞,潜在的陷阱或隐藏的利弊?如果我正在重新设计一种设计方法(我认为我必须这样做),你能指出我的名字吗?

答案 9 :(得分:3)

关于这个主题有一本非常广泛的书,可能值得一试:

Object Oriented Programming in ANSI-C

答案 10 :(得分:3)

另见this answerthis one

有可能。它在当时似乎总是一个好主意,但之后它变成了维护的噩梦。您的代码中充斥着将所有内容捆绑在一起的代码片段。如果使用函数指针,新程序员在阅读和理解代码时会遇到很多问题,因为调用哪些函数并不明显。

使用get / set函数隐藏数据很容易在C中实现但是停在那里。我已经在嵌入式环境中看到了多次尝试,最终它总是一个维护问题。

由于你们都准备好了维护问题,我会明确指出。

答案 11 :(得分:3)

可能this online book可以帮助您解决问题。

答案 12 :(得分:3)

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

答案 13 :(得分:2)

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

所以很有可能在C中有课。 您可以尝试挖掘旧的C ++预处理器,看看它创建了哪种解决方案。

答案 14 :(得分:2)

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

答案 15 :(得分:2)

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

根据您的编译器,可能能够将函数包含到struct中,但这是非常特定于编译器的扩展,并且没有任何内容使用常规使用的标准的最新版本:)

答案 16 :(得分:1)

你想要虚拟方法吗?

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

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

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

答案 17 :(得分:0)

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

另外,如果你可以使用C,你可以使用C ++并获得类。