16

我来自 OO(C#、Java、Scala),我非常重视代码重用和类型安全的原则。上述语言中的类型参数完成了这项工作,并启用了类型安全且不会“浪费”代码的通用数据结构。

当我陷入 C 中时,我意识到我必须做出妥协,我希望它是正确的。要么我的数据结构void *在每个节点/元素中都有一个并且我失去了类型安全性,要么我必须为我想要使用它们的每种类型重新编写我的结构和代码。

代码的复杂性是一个明显的因素:遍历数组或链表是微不足道的,向*next结构添加 a 也不是额外的努力;在这些情况下,不要尝试和重用结构和代码是有意义的。但对于更复杂的结构,答案并不那么明显。

还有模块化和可测试性:将类型及其操作从使用该结构的代码中分离出来,使测试变得更容易。反之亦然:在一个结构上测试一些代码的迭代,同时它试图做其他事情会变得混乱。

那么你的建议是什么?void *和重用或类型安全和重复的代码?有什么一般原则吗?当它不适合时,我是否试图强制 OO 进入程序?

编辑:请不要推荐 C++,我的问题是关于 C 的!

4

11 回答 11

13

我会说使用void *,以便您可以重复使用代码。与确保正确获取/设置列表中的数据相比,重新实现例如链接列表需要更多的工作。

尽可能多地从glib中获取提示,我发现它们的数据结构非常好用且易于使用,并且由于失去了类型安全性而几乎没有遇到什么麻烦。

于 2009-12-14T09:46:36.410 回答
6

正如您所建议的,我认为您必须在两者之间取得平衡。如果代码只有几行而且微不足道,我会复制它,但如果它更复杂,我会考虑使用void*以避免在多个地方进行任何潜在的错误修复和维护,并减少代码大小。

如果您查看 C 运行时库,有几个“通用”函数可以与 . 一起使用void*,一个常见的示例是使用qsort. 为您想要排序的每种类型复制此代码是很疯狂的。

于 2009-12-14T09:47:37.503 回答
4

使用 void 指针没有任何问题。在将它们分配给指针类型的变量时,您甚至不必强制转换它们,因为转换是在内部完成的。可能值得一看:http ://www.cpax.org.uk/prg/writings/casting.php

于 2009-12-14T09:44:38.767 回答
3

这个问题的答案与在 C++ 中获取有效的链接列表模板相同。

a) 创建使用 void* 或一些抽象类型的算法的抽象版本

b) 创建一个轻量级的公共接口来调用抽象类型算法和它们之间的种姓。

例如。

typedef struct simple_list
  {
  struct simple_list* next;
  } SimpleList;

void add_to_list( SimpleList* listTop, SimpleList* element );
SimpleList* get_from_top( SimpleList* listTop );
// the rest

#define ListType(x) \
    void add_ ## x ( x* l, x* e ) \
        { add_to_list( (SimpleList*)l, (SimpleList*)x ); } \
    void get_ ## x ( x* l, x* e ) \
        { return (x*) get_from_to( (SimpleList*)l ); } \
   /* the rest */

typedef struct my_struct
  {
  struct my_struct* next;
  /* rest of my stuff */
  } MyStruct;

ListType(MyStruct)

MyStruct a;
MyStruct b;

add_MyStruct( &a, &b );
MyStruct* c = get_MyStruct(&a);

等等等等

于 2009-12-14T13:35:35.967 回答
2

我们在这里大量使用 C 中的 OO,但仅用于封装和抽象,没有多态性之类的。

这意味着我们有特定的类型,例如 FooBar(Foo a, ...) 但是,对于我们的集合“类”,我们使用 void *。只需在可以使用多种类型的地方使用 void *,但这样做,确保您不需要参数是特定类型。根据集合,有 void * 是可以的,因为集合不关心类型。但是,如果您的函数可以接受类型 a 和类型 b,但不能接受其他类型,请创建两个变体,一个用于 a,一个用于 b。

要点是仅在您不关心类型时才使用 void * 。

现在,如果您有 50 个具有相同基本结构的类型(例如,int a;int b;作为所有类型的第一个成员),并且想要一个函数作用于这些类型,只需将常见的第一个成员本身设为一个类型,然后让函数接受这个,并传递 object->ab 或 (AB*)object 是你的类型是不透明的,如果 ab 是你的结构中的第一个字段,两者都将起作用。

于 2009-12-14T13:12:39.753 回答
1

您可以使用宏,它们适用于任何类型,编译器将静态检查扩展代码。缺点是代码密度(在二进制中)会变差,并且更难调试。

前段时间我问过这个关于泛型函数的问题,答案可以帮助你。

于 2009-12-14T09:51:59.690 回答
1

您可以有效地将类型信息、继承和多态性添加到 C 数据结构中,这就是 C++ 所做的。( http://www.embedded.com/97/fe29712.htm )

于 2009-12-14T09:52:26.320 回答
1

绝对通用void*,绝不重复代码!

考虑到许多 C 程序员和许多主要的 C 项目都考虑过这种困境。我遇到的所有严肃的 C 项目,无论是开源的还是商业的,都选择了通用的void*. 当仔细使用并封装到一个好的 API 中时,它对库的用户几乎没有负担。此外,void*是惯用的 C,直接在 K&R2 中推荐。这是人们期望编写代码的方式,其他任何事情都会令人惊讶并被错误地接受。

于 2009-12-19T05:56:09.217 回答
0

您可以使用 C 构建(某种)OO 框架,但是您会错过很多好处……例如编译器可以理解的 OO 类型系统。如果你坚持用类 C 语言做 OO,C++ 是更好的选择。它比 vanilla C 更复杂,但至少你得到了对 OO 的适当语言支持。

编辑:好的......如果你坚持我们不推荐 C++,我建议你不要在 C 中做 OO。快乐吗?就您的 OO 习惯而言,您可能应该从“对象”的角度来考虑,但将继承和多态性排除在您的实现策略之外。应谨慎使用泛型(使用函数指针)。

编辑 2:实际上,我认为void *在通用 C 列表中使用 of 是合理的。它只是尝试使用宏、函数指针、调度和我认为是个坏主意的那种废话来构建一个模拟 OO 框架。

于 2009-12-14T09:42:53.663 回答
0

在 Java 中,java.util包中的所有集合实际上都相当于void*指针 ( the Object)。

是的,泛型(在 1.5 中引入)添加了语法糖并防止您编写不安全的分配,但是存储类型仍然存在Object

void*因此,我认为当您使用通用框架类型时,不会犯下 OO 犯罪。

如果您经常在代码中执行此操作,我还将添加特定类型的内联或宏包装器,以从通用结构中分配/检索数据。

PS 你不应该做的一件事是使用void**返回分配/重新分配的泛型类型。如果您检查签名,malloc/realloc您会发现您可以在没有可怕void**指针的情况下实现正确的内存分配。我之所以这么说,是因为我在一些开源项目中看到了这一点,我不想在这里命名。

于 2009-12-14T18:44:21.690 回答
0

可以通过一些工作来包装通用容器,以便可以在类型安全的版本中对其进行实例化。这是一个示例,下面链接的完整标题:

/* 通用实现 */

struct deque *deque_next(struct deque *dq);

void *deque_value(const struct deque *dq);

/* Prepend a node carrying `value` to the deque `dq` which may
 * be NULL, in which case a new deque is created.
 * O(1)
 */
void deque_prepend(struct deque **dq, void *value); 

从可用于实例化特定包装类型的双端队列的标头中

#include "deque.h"

#ifndef DEQUE_TAG
#error "Must define DEQUE_TAG to use this header file"
#ifndef DEQUE_VALUE_TYPE
#error "Must define DEQUE_VALUE_TYPE to use this header file"
#endif
#else

#define DEQUE_GEN_PASTE_(x,y) x ## y
#define DEQUE_GEN_PASTE(x,y) DEQUE_GEN_PASTE_(x,y)
#define DQTAG(suffix) DEQUE_GEN_PASTE(DEQUE_TAG,suffix)

#define DQVALUE DEQUE_VALUE_TYPE

#define DQREF DQTAG(_ref_t)

typedef struct {
    deque_t *dq;
} DQREF;

static inline DQREF DQTAG(_next) (DQREF ref) {
    return (DQREF){deque_next(ref.dq)};
}

static inline DQVALUE DQTAG(_value) (DQREF ref) {
    return deque_value(ref.dq);
}

static inline void DQTAG(_prepend) (DQREF *ref, DQVALUE val) {
    deque_prepend(&ref->dq, val);
}
于 2011-11-04T16:01:05.167 回答