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

How do you implement a class in C?

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

限制:

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


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

1
2
3
4
5
6
7
8
typedef struct {
  float (*computeArea)(const ShapeClass *shape);
} ShapeClass;

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

这将允许您通过"继承"基类并实现适当的函数来实现类:

1
2
3
4
5
6
7
8
9
10
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;
}

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

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

如果需要多个不同的构造函数,则必须"修饰"函数名,不能有多个rectangle_new()函数:

1
2
3
4
5
6
void rectangle_new_with_lengths(RectangleClass *rect, float width, float height)
{
  rectangle_new(rect);
  rect->width = width;
  rect->height = height;
}

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

1
2
3
4
5
6
7
8
9
int main(void)
{
  RectangleClass r1;

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

我希望这能给你一些建议,至少。为了在C中获得成功和丰富的面向对象框架,请查看glib的gobject库。

还要注意,上面没有建模的"类",每个对象都有自己的方法指针,它比在C++中发现的更灵活一些。而且,它需要内存。您可以通过在class结构中填充方法指针来避免这种情况,并为每个对象实例发明一种引用类的方法。


我也得做一次作业。我采用这种方法:

  • 在中定义数据成员结构。
  • 定义您的函数成员将指向结构的指针作为第一个论点。
  • 在一个标题和一个C中执行这些操作。结构定义的标题&;函数声明,c表示实施。
  • 一个简单的例子是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /// Queue.h
    struct Queue
    {
        /// members
    }
    typedef struct Queue Queue;

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


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

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

    还有一本关于使用ANSIC进行在线面向对象编程的技术的书。


    你可以看一下戈壁特。它是一个操作系统库,为您提供了一个详细的对象处理方法。

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


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

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

    这本书很好地涵盖了你的问题。它在Addison-Wesley专业计算系列中。

    基本模式是这样的:

    1
    2
    3
    4
    5
    6
    7
    /* for data structure foo */

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

    我将给出一个简单的例子,说明OOP应该如何在C中完成。我意识到这个thead是从2009年开始的,但无论如何我都想添加这个。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    /// 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),这和一些命名约定将为您带来很大的帮助。

    对于虚拟函数,请在结构中使用函数指针,也可能与类_Func(…);包装一起使用。对于(简单)模板,添加一个大小参数来确定大小,需要一个void*指针,或者需要一个"class"类型,只需要您关心的功能。(例如,int getuuid(object*self);getuuid(&;p);)


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


    也可以看到这个答案和这个

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

    使用get/set函数隐藏数据很容易在C中实现,但到此为止。我在嵌入式环境中见过多次这样的尝试,最终它总是一个维护问题。

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


    关于这个问题有一本非常广泛的书,值得一看:

    ANSI-C中的面向对象编程


    我的策略是:

    • 在单独的文件中定义类的所有代码
    • 在单独的头文件中定义类的所有接口
    • 所有成员函数都接受一个"classhandle",它代表实例名(而不是o.foo(),调用foo(ohandle)
    • 根据内存分配策略,构造函数将替换为函数void classinit(classhandle h,int x,int y,…)或classhandle classinit(int x,int y,…)。
    • 所有成员变量都作为静态结构的成员存储在类文件中,将其封装在文件中,防止外部文件访问它。
    • 对象存储在上面静态结构的数组中,带有预定义的句柄(在接口中可见)或可以实例化的对象的固定限制。
    • 如果有用,类可以包含公共函数,这些公共函数将在数组中循环并调用所有实例化对象的函数(run all()调用每个run(ohandle)
    • deinit(classhandle h)函数在动态分配策略中释放分配的内存(数组索引)。

    是否有人看到任何问题、漏洞、潜在缺陷或这种方法的任何一种变化的隐藏好处/缺点?如果我正在重新设计一种设计方法(我想我一定是这样),你能告诉我它的名字吗?


    也许这本在线书可以帮助你解决你的问题。


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    #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

    "
    );

        printf("User.width = %f
    "
    , s1.width);
        printf("User.height = %f
    "
    , s1.height);
        printf("User.area = %f

    "
    , s1.area(&s1));

        printf("Made with \xe2\x99\xa5
    "
    );

        return 0;
    };

    在您的例子中,类的良好近似值可以是ADT。但还是不一样。


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

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


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


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

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


    是否需要虚拟方法?

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

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

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


    正如您正确指出的,C不是OOP语言,因此没有内置的方法来编写真正的类。最好的选择是查看结构和函数指针,这些可以让您构建类的近似值。但是,由于C是过程性的,所以您可能需要考虑编写更类似C的代码(即,不使用类)。

    此外,如果你可以使用C,你可以使用C++和获得类。