什么是一组漂亮的预处理器黑客(ANSI C89 / ISO C90兼容),它在C中实现某种丑陋(但可用)的面向对象?
我熟悉一些不同的面向对象语言,所以请不要回答像"Learn C++!"这样的答案 . 我读过“Object-Oriented Programming With ANSI C”(小心: PDF format )和其他一些有趣的解决方案,但我最感兴趣的是你:-)!
什么是一组漂亮的预处理器黑客(ANSI C89 / ISO C90兼容),它在C中实现某种丑陋(但可用)的面向对象?
我熟悉一些不同的面向对象语言,所以请不要回答像"Learn C++!"这样的答案 . 我读过“Object-Oriented Programming With ANSI C”(小心: PDF format )和其他一些有趣的解决方案,但我最感兴趣的是你:-)!
18 回答
C Object System (COS)听起来很有希望(它仍然是alpha版本) . 为了简单和灵活,它试图保持最小的可用概念:统一的面向对象编程,包括开放类,元类,属性元类,泛型,多方法,委托,所有权,异常, Contract 和闭包 . 有一个描述它的draft paper(PDF) .
Exception in C是在其他OO语言中找到的TRY-CATCH-FINALLY的C89实现 . 它附带了一个测试套件和一些例子 .
两人都是Laurent Deniau,他在OOP in C上工作很多 .
我建议不要使用预处理器(ab)来尝试使C语法更像是另一种更面向对象的语言 . 在最基本的层面上,您只需使用普通结构作为对象并通过指针传递它们:
要获得继承和多态这样的东西,你必须更努力地工作 . 您可以通过让结构的第一个成员成为超类的实例来进行手动继承,然后您可以自由地转换指向基类和派生类的指针:
要获得多态(即虚函数),可以使用函数指针,也可以使用函数指针表,也称为虚拟表或vtable:
这就是你在C中做多态的方法 . 它并不漂亮,但它能完成这项任务 . 有一些棘手的问题涉及基类和派生类之间的指针转换,只要基类是派生类的第一个成员,它们是安全的 . 多重继承要困难得多 - 在这种情况下,为了除了第一个之外的基类之间的情况,你需要根据适当的偏移量手动调整指针,这非常棘手且容易出错 .
您可以做的另一件(棘手的事)是在运行时更改对象的动态类型!你只需重新分配一个新的vtable指针 . 您甚至可以选择性地更改某些虚拟功能,同时保留其他功能,从而创建新的混合类型 . 只需要小心创建一个新的vtable而不是修改全局vtable,否则你会意外地影响给定类型的所有对象 .
我曾经使用过一个C库,它的实现方式令我感到非常优雅 . 他们用C语言编写了一种定义对象的方法,然后从它们继承,以便它们像C对象一样可扩展 . 基本的想法是这样的:
每个对象都有自己的文件
公共函数和变量在.h文件中为对象定义
私有变量和函数仅位于.c文件中
To "inherit"创建一个新结构,结构的第一个成员是要继承的对象
继承很难描述,但基本上是这样的:
然后在另一个文件中:
然后你可以在内存中创建一个面包车,并由只知道车辆的代码使用:
它运行得很漂亮,.h文件确切地定义了你应该能够对每个对象做什么 .
用于Linux的GNOME桌面是用面向对象的C编写的,它有一个名为“GObject " which supports properties, inheritance, polymorphism, as well as some other goodies like references, event handling (called " signals”的对象模型,运行时输入,私有数据等 .
它包括预处理器hacks,用于在类层次结构中进行类型转换等操作 . 这是我为GNOME编写的一个示例类(像gchar这样的东西是typedef):
Class Source
Class Header
在GObject结构中,有一个GType整数,用作GLib动态类型系统的幻数(你可以将整个结构转换为“GType”来找到它的类型) .
如果您将对象调用的方法视为将隐式“
this
”传递给函数的静态方法,则可以使C中的OO更容易思考 .例如:
变为:
或类似的东西 .
在我知道OOP是什么之前,我曾经在C中做过这种事情 .
下面是一个示例,它实现了一个数据缓冲区,该缓冲区在给定最小大小,增量和最大大小的情况下按需增长 . 这个特别实现是基于“元素”的,也就是说它被设计为允许任何C类型的类似列表的集合,而不仅仅是一个可变长度的字节缓冲区 .
我们的想法是使用xxx_crt()实例化对象,并使用xxx_dlt()删除 . 每个“成员”方法都采用特定类型的指针进行操作 .
我以这种方式实现了链表,循环缓冲区和许多其他东西 .
我必须承认,我从未考虑过如何使用这种方法实现继承 . 我想Kieveli提供的一些混合可能是一条好路 .
dtb.c:
dtb.h
PS:vint只是int的typedef - 我用它来提醒我,它的长度在平台之间是可变的(用于移植) .
稍微偏离主题,但原始的C编译器,Cfront,编译C到C然后到汇编程序 .
保存here .
ffmpeg(用于视频处理的工具包)是用直接C(和汇编语言)编写的,但使用面向对象的样式 . 它充满了带有函数指针的结构体 . 有一组工厂函数使用适当的"method"指针初始化结构 .
如果您真的认为是餐饮,即使标准C库也使用OOP - 请考虑
FILE *
作为示例:fopen()
初始化一个FILE *
对象,并使用它使用成员方法fscanf()
,fprintf()
,fread()
,fwrite()
等,并最终使用fclose()
完成它 .您也可以使用伪Objective-C方式,这也不难:
使用:
如果使用了相当古老的Objective-C-to-C转换器,这可能是由于某些Objective-C代码导致的:
我认为Adam Rosenfield发布的是在C中进行OOP的正确方法 . 我想补充一点,他所展示的是对象的实现 . 换句话说,实际的实现将放在
.c
文件中,而接口将放在 Headers.h
文件中 . 例如,使用上面的猴子示例:界面看起来像:
您可以在界面
.h
文件中看到您只定义原型 . 然后,您可以将实现部分“.c
file”编译为静态或动态库 . 这会创建封装,您也可以随意更改实现 . 对象的用户需要几乎不了解它的实现 . 这也将重点放在对象的整体设计上 .我个人认为,oop是一种概念化代码结构和可重用性的方式,实际上与添加到重载或模板等其他内容无关 . 是的,它们是非常好的有用功能,但它们并不代表面向对象编程的真正含义 .
我的建议:保持简单 . 我遇到的最大问题之一是维护旧软件(有时超过10年) . 如果代码不简单,则可能很难 . 是的,人们可以在C中编写非常有用的OOP和多态,但它可能很难阅读 .
我更喜欢封装一些明确定义的功能的简单对象 . 一个很好的例子是GLIB2,例如哈希表:
关键是:
简单的架构和设计模式
实现基本的OOP封装 .
易于实施,阅读,理解和维护
如果我要在C中编写OOP,我可能会使用伪Pimpl设计 . 您最终会将指针传递给指向结构的指针,而不是将指针传递给结构体 . 这使得内容不透明并促进多态性和继承 .
C中OOP的真正问题是变量退出范围时会发生什么 . 没有编译器生成的析构函数,这可能会导致问题 . Macros可能有所帮助,但看起来总是很难看 .
输出:
这是用C编写OO编程的一个节目 .
这是真正的纯C,没有预处理器宏 . 我们有继承,多态和数据封装(包括对类或对象私有的数据) . 对于受保护的限定符,没有机会等同,也就是说,私有数据也是私有数据 . 但这不是一个不便,因为我认为没有必要 .
CPolygon
未实例化,因为我们仅使用它来操纵无遗传链中的对象具有共同方面但不同的实现(多态) .@Adam Rosenfield对如何用C实现OOP有很好的解释
此外,我建议你阅读
1)pjsip
一个非常好的VoIP库 . 您可以通过结构和函数指针表了解它是如何实现OOP的
2)iOS Runtime
了解iOS Runtime如何为Objective C提供支持 . 它通过isa指针,元类实现OOP
对我来说,C中的面向对象应该具有以下特征:
封装和数据隐藏(可以使用结构/不透明指针实现)
继承和支持多态(可以使用结构实现单继承 - 确保抽象基不可实例化)
构造函数和析构函数(不容易实现)
类型检查(至少对于用户定义的类型,因为C不强制执行任何操作)
引用计数(或要实现的内容RAII)
对异常处理的有限支持(setjmp和longjmp)
除此之外,它应该依赖于ANSI / ISO规范,不应该依赖于编译器特定的功能 .
看http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html . 如果没有别的东西通过文档阅读是一个启发性的经验 .
我在这里参加派对有点晚了但是我喜欢避免两个极端的极端 - 过多或过多的混淆代码,但是一些明显的宏可以使OOP代码更容易开发和阅读:
我认为这有很好的 balancer ,并且它产生的错误(至少对于默认的gcc 6.3选项)对于一些更可能的错误是有帮助的,而不是混淆 . 重点是提高程序员的工作效率吗?
如果你需要编写一些代码,试试这个:https://github.com/fulminati/class-framework