如何在 C 中模拟 OO 风格的多态性?

Posted

技术标签:

【中文标题】如何在 C 中模拟 OO 风格的多态性?【英文标题】:How can I simulate OO-style polymorphism in C? 【发布时间】:2010-10-06 03:49:36 【问题描述】:

有没有办法用 C 编程语言编写类似 OO 的代码?


另见:

Can you write object-oriented code in C? Object-orientation in C

通过搜索“[c] oo”找到。

【问题讨论】:

感谢方括号的提示。不知道。 是的,这需要变得更容易被发现或其他东西。我一直在尝试为 uservoice 提出一个好的建议,但它没有结合在一起。 _Generic 可用于编译时多态性。 __attribute__((cleanup ())) 可用于 RAII。并且错误代码优于异常处理。 【参考方案1】:

常见的方法是用指向函数的指针来定义结构。这定义了可以在任何类型上调用的“方法”。然后子类型在这个公共结构中设置自己的函数,并返回它。

例如在linux内核中,有struct:

struct inode_operations 
    int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
    struct dentry * (*lookup) (struct inode *,struct dentry *, 
                               struct nameidata *);
    ...
;

然后,每个已注册的文件系统类型都会为 createlookup 和其余函数注册自己的函数。其余代码可以使用通用 inode_operations:

struct inode_operations   *i_op;
i_op -> create(...);

【讨论】:

这基本上就是 cfront(最初的 C++ 编译器)如何将 C++ 转换为 C,然后用 pcc 编译。我从中学到了很多关于如何处理核心文件的知识。 这不是 C++ 风格的多态性。多态相对于函数指针的一大优势是多态允许数据绑定,而函数指针则不允许。在这种情况下,没有数据绑定的机会,它只是一个函数指针的结构。另外,我认为该结构在此评论中是多余的。【参考方案2】:

第一个 C++ 编译器(“C with classes”)实际上会生成 C 代码,所以这绝对是可行的。

基本上,您的基类是一个结构;派生结构必须在第一个位置包含基结构,因此指向“派生”结构的指针也将是指向基结构的有效指针。

typedef struct 
   data member_x;
 base;

typedef struct 
   struct base;
   data member_y;
 derived;

void function_on_base(struct base * a); // here I can pass both pointers to derived and to base

void function_on_derived(struct derived * b); // here I must pass a pointer to the derived class

函数可以作为函数指针作为结构的一部分,因此像 p->call(p) 这样的语法成为可能,但您仍然必须显式地将指向结构的指针传递给函数本身。

【讨论】:

这并没有解释方法覆盖如何在 C 中工作。如何覆盖 function_on_base 以访问派生的 memeber_y,就像在 C++ 多态调用中一样? 在 C 中无法覆盖。 这个答案不正确。将struct derived* 传递给function_on_base 将无法编译; struct derived* 是与struct base* 不同的类型,即使地址正确;但是,如果您将指针从derived* 转换为base*,它将起作用(但您会错过编译时类型检查,而是在运行时崩溃)。 @PatrickCollins 覆盖 is 可能在 C:pastebin.com/W5xEytbv @JohnK 见上面的评论。 @weberc2 没错,我不确定我在写这篇文章时在想什么。我可能已经想到了“重载”,您的粘贴也暗示了这一点。【参考方案3】:

C++ 与 C 相差不远。

类是具有隐藏指针的结构,该指针指向称为 VTable 的函数指针表。 Vtable 本身是静态的。 当类型指向具有相同结构但指针指向其他实现的 Vtable 时,就会得到多态性。

建议将调用逻辑封装在以struct为参数的函数中,避免代码混乱。

您还应该在函数中封装结构实例化和初始化(这相当于 C++ 构造函数)和删除(C++ 中的析构函数)。无论如何,这些都是很好的做法。

typedef struct

   int (*SomeFunction)(TheClass* this, int i);
   void (*OtherFunction)(TheClass* this, char* c);
 VTable;

typedef struct

   VTable* pVTable;
   int member;

 TheClass;

调用方法:

int CallSomeFunction(TheClass* this, int i)

  (this->pVTable->SomeFunction)(this, i);

【讨论】:

示例不完整。没有解释如何实现具有不同成员的新类(除了 int 之外)。在这种情况下,似乎所有“多态”对象都将具有相同的 int 成员。如果要定义具有不同成员的类怎么办?我认为在这种情况下,您的结构应该有一个指向数据成员的 void 指针,而不是一个具体的 int。 @user2445507 该示例实现了一个在运行时调用函数的 vtable,这正是 C++ 在创建类时会在幕后执行的操作。 @NiclasLarsson 在 C++ 中,可以创建另一个继承纯虚函数的类,但具有不同的成员变量。然后,在该类中实现的虚函数可以使用这些成员变量。这是多态性的一个关键方面。该示例未演示如何执行此操作。对于这个例子,你不妨只使用函数指针。【参考方案4】:

文件函数 fopen、fclose、fread 是 C 中的 OO 代码示例。它们不是类中的私有数据,而是用于封装数据的 FILE 结构,C 函数充当成员类函数. http://www.amazon.com/File-Structures-Object-Oriented-Approach-C/dp/0201874016

【讨论】:

所指的书只有C++。注意:我有一份,今天不推荐。如果我今天有一个 C 语言的 OO 建议,那就是 C 接口和实现:David Hanson (amzn.com/0201498413) 创建可重用软件的技术。绝对精彩的书,大多数程序员都会很好地理解它,其中的大多数示例都取自编译器后端,因此代码具有示范性。【参考方案5】:

VPRI 的 Ian Piumarta 和 Alessandro Warth 的文章 Open Reusable Object Models 的附录 B 是 GNU C 中对象模型的实现,大约 140 行代码。这是一本引人入胜的读物!

这是使用 GNU 对 C 的扩展(语句表达式)向对象发送消息的宏的未缓存版本:

struct object;

typedef struct object *oop; 
typedef oop *(*method_t)(oop receiver, ...);

//...

#define send(RCV, MSG, ARGS...) ( \ 
    oop r = (oop)(RCV); \ 
    method_t method = _bind(r, (MSG)); \ 
    method(r, ##ARGS); \ 
) 

在同一个文档中,查看objectvtablevtable_delegatedsymbol 结构,以及_bindvtable_lookup 函数。

干杯!

【讨论】:

【参考方案6】:

我查看了其他人的答案并想出了这个:

#include <stdio.h>

typedef struct

    int (*get)(void* this);
    void (*set)(void* this, int i);
    int member;

 TheClass;

int Get(void* this)

    TheClass* This = (TheClass*)this;
    return This->member;


void Set(void* this, int i)

    TheClass* This = (TheClass*)this;
    This->member = i;


void init(TheClass* this)

    this->get = &Get;
    this->set = &Set;


int main(int argc, char **argv)

    TheClass name;
    init(&name);
    (name.set)(&name, 10);
    printf("%d\n", (name.get)(&name));
    return 0;

我希望这能回答一些问题。

【讨论】:

很好的例子。如果您有 2 个具有不同 init/get/set 的“派生”类会更好。 “私人”成员/功能可以使用opaque structs 完成。命名约定也很重要:mylib_someClass_aMethod(this) 是一个很好的可能性。 这看起来很整洁。我把它放到了一个 c 文件中,并为一个 ARM 微控制器编译好了。我还没有走多远,只是想写个便条作为对 red_hax0r 帖子的致谢。 多态性在哪里?【参考方案7】:

来自***: 在编程语言和类型论中,多态性(来自希腊语 πολύς,polys,“many, much”和 μορφή,morphē,“form, shape”)是为不同类型的实体提供单一接口。

所以我想说在 C 中实现它的唯一方法是使用可变参数以及一些(半)自动类型信息管理。 例如,在 C++ 中,您可以编写(对不起,琐碎):

void add( int& result, int a1, int a2 );
void add( float& result, float a1, float a2 );
void add( double& result, double a1, double a2 );

在 C 中,在其他解决方案中,您能做的最好的事情是这样的:

int int_add( int a1, int a2 );
float float_add( float a1, fload a2 );
double double_add( double a1, double a2 );

void add( int typeinfo, void* result, ... );

那么你需要:

    用枚举/宏实现“typeinfo” 用 stdarg.h 东西实现后一个函数 告别 C 静态类型检查

我几乎可以肯定,多态性的任何其他实现都应该与这个非常相似。 相反,上面的答案似乎试图解决继承而不是多态!

【讨论】:

在 C11 中,新的 _Generic 关键字大大简化了这种设计模式。我强烈推荐那些选择这种方法的人。【参考方案8】:
#include <stdio.h>

typedef struct 
    int  x;
    int z;
 base;

typedef struct 
    base;
    int y;
    int x;
 derived;

void function_on_base( base * a) // here I can pass both pointers to derived and to base

    printf("Class base [%d]\n",a->x);
    printf("Class base [%d]\n",a->z);

void function_on_derived( derived * b) // here I must pass a pointer to the derived class

    printf("Class derived [%d]\n",b->y);
    printf("Class derived [%d]\n",b->x);


int main()

    derived d;
    base b;
    printf("Teste de poliformismo\n");

    b.x = 2;
    d.y = 1;
    b.z = 3;
    d.x = 4;
    function_on_base(&b);
    function_on_base(&d);
    function_on_derived(&b);
    function_on_derived(&d);
    return 0;

输出是:

Class base [3]
Class base [1]
Class base [4]
Class derived [2]
Class derived [3]
Class derived [1]
Class derived [4]

所以它可以工作,它是一个多态代码。

UncleZeiv 一开始就解释过了。

【讨论】:

【参考方案9】:

为了在 C 中也构建 OO 功能,您可以查看以前的答案。

但是,如果您想通过 C 语言中的示例了解什么是多态性,(正如在其他问题中已经提出的重定向到这个问题)。也许我错了,但我想不出像 C 指针算术这样容易理解的东西。在我看来,指针算术在 C 中本质上是多态的。在以下示例中,相同的函数(OO 中的方法),即加法 (+),将根据输入结构的属性产生不同的行为。

例子:

double a*;
char str*;

a=(double*)malloc(2*sizeof(double));
str=(char*)malloc(2*sizeof(char)); 

a=a+2; // make the pointer a, point 2*8 bytes ahead.

str=str+2; // make the pointer str, point 2*1 bytes ahead.

免责声明:我是 C 的新手,非常期待得到纠正和向其他用户的 cmets 学习,甚至完全删除这个答案,如果它是错误的。非常感谢,

【讨论】:

【参考方案10】:

我通常喜欢做的是将结构包装在另一个包含有关包装类的元信息的结构中,然后构建像函数列表一样作用于通用结构的访问者。这种方法的优点是您不需要修改现有结构,并且可以为任何结构子集创建访问者。

举个常见的例子:

typedef struct 
    char call[7] = "MIAO!\n";
 Cat;
    
typedef struct 
    char call[6] = "BAU!\n";
 Dog;

我们可以将这两个结构包裹在这个新结构中:

typedef struct 
    const void * animal;
    AnimalType type;
 Animal;

类型可以是简单的 int 但不要偷懒:

typedef enum  
    ANIMAL_CAT = 0,
    ANIMAL_DOG,
    ANIMAL_COUNT
 AnimalType;

如果有一些包装函数就好了:

Animal catAsAnimal(const Cat * c) 
    return (Animal)(void *)c, ANIMAL_CAT;


Animal dogAsAnimal(const Dog * d) 
    return (Animal)(void *)d, ANIMAL_DOG;

现在我们可以定义我们的“访问者”了:

void catCall ( Animal a ) 
    Cat * c = (Cat *)a.animal;
    printf(c->call);


void dogCall ( Animal a ) 
    Dog * d = (Dog *)a.animal;
    printf(d->call);


void (*animalCalls[ANIMAL_COUNT])(Animal)=&catCall, &dogCall;

那么实际使用会是:

Cat cat;
Dog dog;

Animal animals[2];
animals[0] = catAsAnimal(&cat);
animals[1] = dogAsAnimal(&dog);

for (int i = 0; i < 2; i++) 
    Animal a = animals[i];
    animalCalls[a.type](a);

这种方法的缺点是每次您想将其用作泛型类型时都必须包装结构。

【讨论】:

【参考方案11】:

简单函数重载的一个非常粗略的例子,可以使用可变参数宏来实现。

#include <stdio.h>
#include <stdlib.h>

#define SCOPE_EXIT(X) __attribute__((cleanup (X)))

struct A

  int a;
;

struct B

 int a, b;
;

typedef struct A * A_id;
typedef struct B * B_id;


A_id make_A()

   return (A_id)malloc(sizeof(struct A));


void destroy_A(A_id * ptr)

   free(*ptr);
   *ptr = 0;


B_id make_B()

  return (B_id)malloc(sizeof(struct B));


void destroy_B(B_id * ptr)

  free(*ptr);
  *ptr = 0;


void print_a(A_id ptr)

  printf("print_a\n"); 

void print_b(B_id ptr)

  printf("print_b\n"); 


#define print(X) _Generic((X),\
          A_id : print_a, \
          B_id : print_b\
)(X)

int main()

  A_id aa SCOPE_EXIT(destroy_A) = make_A();
  print(aa);

  B_id bb SCOPE_EXIT(destroy_B) = make_B();
  print(bb);
  return 0;

【讨论】:

以上是关于如何在 C 中模拟 OO 风格的多态性?的主要内容,如果未能解决你的问题,请参考以下文章

Haskell 演示 OOP 设计模式的等价物 [重复]

面向对象的编程风格(不带继承的多态)

OO第七到九次作业总结

C++编译期多态与运行期多态

编译期多态和运行时多态

纯C语言实现简单继承机制