如何用 C 语言实现一个类?

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

限制:

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

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

取决于您的编译器,您的 也许吧能够将函数包含到 struct中,但这是一个特定于 非常编译器的扩展,与我通常使用的标准的最新版本没有任何关系:)

C 不是一种面向对象编程语言,正如您所指出的那样,因此没有内置的方法来编写真正的类。你最好看看 结构函数指针函数指针,它们会让你建立一个类的近似值。但是,由于 C 是过程性的,您可能需要考虑编写更多类似于 C 的代码(即不尝试使用类)。

另外,如果你能使用 C 语言,你很可能可以使用 C + + 获得类。

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

这取决于您希望具有的确切的“面向对象”特性集。如果你需要像重载和/或虚方法这样的东西,你可能需要在结构中包含函数指针:

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 函数,那么你就可以用非常类似于 C + + 的语法从 C 调用函数。

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

但是,如果你想要后者,那么你最好找到一个 C + + 编译器,你可以使用和重新编译该项目。我一直不明白为什么 C + + 不能在嵌入式系统中使用。我已经使用了很多次,它的工作很快,没有记忆问题。当然,你必须对你所做的事情更加小心一点,但它真的没有那么复杂。

使用 struct模拟类的数据成员。在方法范围方面,您可以通过将 二等兵函数原型放置在。文件中的 公众人士函数。H 档案。

我也曾经为了家庭作业做过一次,我采用了这种方法:

  1. 中定义数据成员 结构。
  2. 定义函数成员 取一个指向结构的指针作为 第一次辩论。
  3. 做这些在一个标题和一个 c。 结构定义 & 函数声明 实施。

一个简单的例子是:

/// 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库同时使用了这个技巧和 typedef 技巧,并得到了广泛的应用。

网上还有一本关于这方面技术的书—— 基于 ANSI C 的面向对象程序设计

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

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

你可以看看 GOObject。这是一个操作系统库,它提供了一个冗长的方法来处理一个对象。

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

我的策略是:

  • 在单独的文件中定义类的所有代码
  • 在单独的头文件中定义类的所有接口
  • 所有成员函数都采用“ 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)函数释放动态分配策略中分配的内存(数组索引)

是否有人看到任何问题,漏洞,潜在的陷阱或隐藏的利益/缺点,这种方法的变化?如果我正在重新发明一种设计方法(我假设我必须这样做) ,你能告诉我它的名称吗?

也请参阅 这个答案这个

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

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

既然你们都有维修问题,那我就不插手了。

C 接口和实现: 创建可重用软件的技术 ,< strong > 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);

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

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

我将给出一个简单的例子,如何 OOP 应该做在 C。我意识到这个头是从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)的一些命名约定将为您带来很大帮助。

至于 Virtual 函数,可以在 struct 中使用函数指针,也可以与 Class _ func (...) ; 包装器一起使用。 对于(简单的)模板,添加 size _ t 参数来确定大小,需要 void * 指针,或者只需要一个具有您关心的功能的“ class”类型。(例如,int GetUUID (Object * self) ; GetUUID (& p) ;)