代码之家  ›  专栏  ›  技术社区  ›  Joe

通用性与类型安全?在C中使用void*

  •  16
  • Joe  · 技术社区  · 15 年前

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

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

    代码的复杂性是一个明显的因素:遍历数组或链接列表是很简单的,并且添加 *next 对于一个结构来说并不是额外的工作;在这些情况下,不尝试重用结构和代码是有意义的。但对于更复杂的结构,答案并不那么明显。

    还有模块性和可测试性:将类型及其操作与使用该结构的代码分离,使测试更加容易。反过来也是正确的:在一个结构上测试一些代码的迭代,而它试图做其他事情会变得混乱。

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

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

    11 回复  |  直到 13 年前
        1
  •  13
  •   unwind    15 年前

    我会说使用 void * 所以你可以重复使用代码。重新实现例如链表的工作要比确保正确获取/设置链表中的数据要多。

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

        2
  •  6
  •   Andreas Brinck    15 年前

    我想你必须在两者之间取得平衡,正如你所建议的。如果代码只有几行,而且很琐碎,我会复制它,但是如果它更复杂,我会考虑使用 void* 避免在多个地方进行任何潜在的错误修复和维护,同时也减少了代码大小。

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

        3
  •  4
  •   theprole    15 年前

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

        4
  •  3
  •   David Allan Finch    15 年前

    这个问题的答案与在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);
    

        5
  •  2
  •   Nicolas Goy    15 年前

    我们在C中经常使用OO,但只用于封装和抽象,没有多态性。

    这意味着我们有特定的类型,比如foobar(foo a,…),但是对于我们的集合“类”,我们使用void*。只需在可以使用多个类型的地方使用void*,但是这样做可以确保您不需要参数是特定类型的。根据集合,使用void*是可以的,因为集合不关心类型。但是,如果您的函数可以接受类型A和类型B,但是不能接受其他类型,那么可以制作两个变体,一个用于A,一个用于B。

    主要的一点是,只有在你不关心类型的时候才使用void*。

    现在,如果您有50个具有相同基本结构的类型(例如,int a;int b;作为所有类型的第一个成员),并且希望函数对这些类型执行操作,那么只需将公共的第一个成员单独设置为类型,然后使函数接受它,并且pass object->a b或(a b*)object是不透明的类型,如果a b是第一个字段,这两个类型都可以工作。在您的结构中。

        6
  •  1
  •   Community omersem    7 年前

    您可以使用宏,它们可以与任何类型一起使用,编译器将静态检查扩展的代码。缺点是代码密度(二进制)会恶化,而且更难调试。

    我问这个 question about generic functions 不久前,答案会对你有所帮助。

        7
  •  1
  •   Bertrand Marron    15 年前

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

        8
  •  1
  •   Eli Bendersky    15 年前

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

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

        9
  •  0
  •   Stephen C    15 年前

    您可以使用C构建(某种)OO框架,但是您错过了很多好处…就像一个编译器能理解的OO类型系统。如果你坚持在C语言中做OO,C++是一个更好的选择。它比普通的C语言更复杂,但至少您可以获得适当的OO语言支持。

    编辑:确定…如果你坚持我们不推荐C++,我建议你不要在C中做OO?就你的OO习惯而言,你应该 认为 就“对象”而言,但是不要将继承和多态性放在实现策略中。应谨慎使用泛型(使用函数指针)。

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

        10
  •  0
  •   Alexander Pogrebnyak    15 年前

    在Java中的所有集合 java.util 有效包装持有相当于 void* 指针 Object )

    是的,泛型(在1.5中引入)添加了语法糖分并防止对不安全的赋值进行编码,但是存储类型仍然是 对象 .

    所以,我认为当你使用 空洞* 对于一般框架类型。

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

    另外,你不应该做的一件事就是 void** 返回已分配/重新分配的泛型类型。如果你检查 malloc/realloc 您将看到您可以实现正确的内存分配而不必担心 无效* * 指针。我之所以这么说,是因为我在某个开源项目中看到过这个,我不想在这里说出它的名字。

        11
  •  0
  •   u0b34a0f6ae    13 年前

    一个通用容器可以用一点工作来包装,这样它就可以在类型安全的版本中被实例化。下面是一个例子,完整的标题链接在下面:

    /*通用实现*/

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

    来自可用于实例化特定包装类型deque的头

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