C程序的OO最佳实践


19

“如果您真的想要OO糖-请使用C ++” –当我问到这一点时,我立即从我的一位朋友那里得到了答复。我知道这里有两件事是完全错误的。首先,OO不是“糖”,其次,C ++没有吸收C。

我们需要用C语言编写服务器(前端将使用Python),因此我正在探索更好的方法来管理大型C程序。

根据对象和对象交互对大型系统进行建模,使其更具可管理性,可维护性和可扩展性。但是,当您尝试将此模型转换为不包含对象(及其所有其他对象)的C语言时,您将面临一些重要的决策。

您是否创建了自定义库来提供系统所需的OO抽象?诸如对象,封装,继承,多态性,异常,发布/订阅(事件/信号),名称空间,自省等(例如GObjectCOS)。

或者,您仅使用基本的C构造(struct和函数)以即席方式近似所有对象类(和其他抽象)。(例如,关于SO的此问题的一些答案)

第一种方法为您提供了一种结构化的方法,用于在C中实现整个模型。但是,这也增加了您必须维护的复杂性。(请记住,首先要通过使用对象来减少复杂性)。

我不知道第二种方法,以及它在逼近您可能需要的所有抽象方面的效果如何。

因此,我的简单问题是:在C语言中实现面向对象设计的最佳实践是什么。记住,我并不是在问如何做到这一点。这个这个问题都在谈论,甚至有一本书。我更感兴趣的是一些现实的建议/示例,这些示例/示例解决了此问题时出现的实际问题。

注意:请不要建议为什么不应该使用C来支持C ++。我们已经超越了那个阶段。


3
您可以编写C ++服务器,以便它的外部接口extern "C"可以从python使用。您可以手动执行此操作,也可以让SWIG来帮助您。因此,对于python前端的渴望没有理由不使用C ++。这并非如此说,有没有正当的理由想和C.
扬邬达克

1
这个问题需要澄清。当前,第4段和第5段基本上询问了应该采取的方法,但是您说的是,您“不是在问如何做”,而是要(列出?)最佳实践。如果您不希望在C语言中做到这一点,那么您是否正在寻求与OOP相关的“最佳实践”列表?如果是这样,那就这样说,但要注意,由于主观性,这个问题很可能会结束。
Caleb

:)我要询问实际的示例(代码或其他方式),以及执行过程中遇到的问题。
treecoder

4
您的要求似乎令人困惑。您无缘无故地坚持使用面向对象(在某些语言中,这是使程序更具可维护性的一种方法,但不是在C语言中),并且坚持使用C。面向对象是一种手段,而不是目的或灵丹妙药。 。此外,语言支持也使它受益匪浅。如果您确实想要OO,则应该在选择语言时考虑到这一点。有关如何使用C制作大型软件系统的问题将更加有意义。
David Thornley

您可能要看一下“面向对象的建模和设计”。(鲁博等人):有上映射OO部设计到语言,如C.
乔治

Answers:


16

从我的答案到如何在C中构造复杂的项目(不是OO,而是关于在C中管理复杂性):

关键是模块化。这更易于设计,实现,编译和维护。

  • 识别应用程序中的模块,例如OO应用程序中的类。
  • 每个模块都有单独的接口和实现,仅将其他模块需要的接口放入接口。请记住,C中没有名称空间,因此您必须使接口中的所有内容都是唯一的(例如,带前缀)。
  • 在实现中隐藏全局变量,并使用访问器函数进行读/写。
  • 不要考虑继承,而要考虑组成。通常,不要试图用C模仿C ++,这将很难阅读和维护。

从我对OO C公共和私有功能的典型命名约定有什么看法(我认为这是最佳做法):

我使用的约定是:

  • 公共功能(在头文件中):

    struct Classname;
    Classname_functionname(struct Classname * me, other args...);
  • 私有功能(在实现文件中为静态)

    static functionname(struct Classname * me, other args...)

而且,许多UML工具都能够从UML图生成C代码。开源的是Topcased



1
好答案。旨在模块化。OO应该提供它,但是1)在实践中,以OO意大利面结尾太普遍了; 2)这不是唯一的方法。对于现实生活中的一些示例,请查看linux内核(C型模块化)和使用glib(C型OO)的项目。我曾有机会同时使用这两种样式,并且IMO C样式模块化获得了成功。
荷兰Joh

以及为什么确切地说,组合是比继承更好的方法?欢迎原理和支持参考。还是您仅指的是C程序?
Aleksandr Blekh

1
@AleksandrBlekh-是的,我仅指的是C。
mouviciel 2014年

16

我认为您需要在此讨论中区分OO和C ++。

在C语言中可以实现对象,而且非常容易-只需使用函数指针创建结构即可。那是您的“第二种方法”,我会同意的。另一种选择是不使用结构体中的函数指针,而直接将数据结构体作为“上下文”指针传递给函数。更好,恕我直言,因为它更具可读性,更容易跟踪,并且允许在不更改结构的情况下添加函数(如果不添加数据,则易于继承)。实际上,这就是C ++通常实现this指针的方式。

由于没有内置的继承和抽象支持,多态性变得更加复杂,因此您必须要么在子类中包含父结构,要么进行大量复制粘贴,尽管从技术上讲,这两个选项中的任何一个都坦率地令人恐惧。简单。大量错误等待发生。

同样,可以通过指向所需功能的函数指针轻松实现虚拟函数,这同样非常容易出错,手动完成时很容易出错,在正确初始化这些指针方面进行了大量繁琐的工作。

至于名称空间,异常,模板等-我认为如果您限于C-您应该放弃这些。我一直在用C语言编写OO,如果我有选择的话就不会做(在那个工作地点,我确实想尽办法引入C ++,并且管理人员对集成它的难易程度感到“惊讶”其余的C模块都放在末尾。)

不用说,如果您可以使用C ++,请使用C ++。没有真正的理由不这样做。


实际上,您可以从结构继承并添加数据:只需将子结构的第一项声明为类型为父结构的变量即可。然后根据需要进行投射。
mouviciel

1
@mouviciel-是的。我说过的。“ ...因此,您必须在孩子类中包括父结构,或者...
littleadv

5
没有理由尝试实现继承。作为实现代码重用的一种方法,一开始这是一个有缺陷的想法。对象组成更容易,更好。
KaptajnKold 2011年

@KaptajnKold-同意。
littleadv

8

这是如何在C语言中创建面向对象的基础知识

1.创建对象和封装

通常-人们创建一个像

object_instance = create_object_typex(parameter);

方法可以在这里以两种方式之一进行定义。

object_type_method_function(object_instance,parameter1)
OR
object_instance->method_function(object_instance_private_data,parameter1)

请注意,在大多数情况下,object_instance (or object_instance_private_data)返回的类型为void *.:应用程序不能引用此单个成员或函数。

除此之外,每个方法都将这些object_instance用于后续方法。

2.多态性

我们可以使用许多函数和函数指针在运行时覆盖某些功能。

例如,-所有object_methods都定义为一个函数指针,可以扩展到公共方法和私有方法。

我们还可以在有限的意义上应用函数重载,方法是使用重载var_args 与在printf中定义可变数量的参数非常相似。是的,这在C ++中并不十分灵活-但这是最接近的方式。

3.定义继承

定义继承有些棘手,但是可以对结构进行以下操作。

typedef struct { 
     int age,
     int sex,
} person; 

typedef struct { 
     person p,
     enum specialty s;
} doctor;

typedef struct { 
     person p,
     enum subject s;
} engineer;

// use it like
engineer e1 = create_engineer(); 
get_person_age( (person *)e1); 

这里的doctorand engineer是从person派生的,可以将其转换为更高级别,例如person

最好的例子是在GObject及其派生对象中使用。

4.创建虚拟类 我引用一个名为libjpeg的库的真实示例,该库被所有浏览器用于jpeg解码。它创建了一个名为error_manager的虚拟类,应用程序可以创建具体的实例并提供回馈-

struct djpeg_dest_struct {
  /* start_output is called after jpeg_start_decompress finishes.
   * The color map will be ready at this time, if one is needed.
   */
  JMETHOD(void, start_output, (j_decompress_ptr cinfo,
                               djpeg_dest_ptr dinfo));
  /* Emit the specified number of pixel rows from the buffer. */
  JMETHOD(void, put_pixel_rows, (j_decompress_ptr cinfo,
                                 djpeg_dest_ptr dinfo,
                                 JDIMENSION rows_supplied));
  /* Finish up at the end of the image. */
  JMETHOD(void, finish_output, (j_decompress_ptr cinfo,
                                djpeg_dest_ptr dinfo));

  /* Target file spec; filled in by djpeg.c after object is created. */
  FILE * output_file;

  /* Output pixel-row buffer.  Created by module init or start_output.
   * Width is cinfo->output_width * cinfo->output_components;
   * height is buffer_height.
   */
  JSAMPARRAY buffer;
  JDIMENSION buffer_height;
};

在此请注意,JMETHOD通过宏扩展函数指针,该宏需要分别使用正确的方法加载。


我试图说太多事情,而没有太多个人解释。但我希望人们可以尝试自己的事情。但是,我的目的只是展示事物的映射方式。

而且,会有很多论点认为这将不是C ++等效项的完全正确的属性。我知道C语言中的OO对其定义没有那么严格。但是像这样工作会理解一些核心原则。

重要的是,OO不是像C ++和JAVA那样严格。这是一个可以在结构上以面向对象的思想组织代码并以这种方式进行操作的代码。

我强烈建议人们看到libjpeg的真实设计和以下资源

一种。C
b中的面向对象编程。这是人们交流想法的好地方
。这是整本书


3

面向对象归结为三件事:

1)具有自主类的模块化程序设计。

2)使用私有封装保护数据。

3)继承/多态和其他有用的语法,例如构造函数/析构函数,模板等。

到目前为止,1是最重要的,它也是完全独立于语言的,它与程序设计有关。在C中,您可以通过创建由一个.h文件和一个.c文件组成的自治“代码模块”来做到这一点。将此视为与OO类等效。您可以通过常识,UML或用于C ++程序的任何OO设计方法来决定应在此模块中放置什么。

2也是非常重要的,不仅要保护有意访问私有数据,还要防止无意访问,即“命名空间混乱”。C ++以比C更优雅的方式做到这一点,但是仍然可以通过使用static关键字在C中实现。您将在C ++类中声明为私有的所有变量,应在C中声明为静态,并放在文件范围内。它们只能从自己的代码模块(类)中访问。您可以像在C ++中一样编写“ setters / getters”。

3是有帮助的,但不是必需的。您可以编写没有继承或没有构造函数/析构函数的OO程序。这些东西很不错,它们当然可以使程序更优雅,甚至更安全(如果不小心使用,则相反)。但是它们不是必需的。由于C不支持这些有用功能,因此您只需要不使用它们。构造函数可以替换为init / destruct函数。

继承可以通过各种结构技巧来完成,但是我建议不要这样做,因为它可能只会使您的程序变得更加复杂而没有任何收获(一般而言,继承不仅应使用C语言,而且应使用任何语言,都应谨慎使用)。

最后,每个OO技巧可以在C语言中完成。Axel-Tobias Schreiner的书“用ANSI C进行面向对象的编程”从90年代初就证明了这一点。但是,我不会向任何人推荐这本书:它给您的C程序增加了令人不愉快的奇怪复杂性,值得大惊小怪。(尽管有我警告,但仍对有兴趣的人可以在这里免费获得该书。)

所以我的建议是实施上面的1)和2),并跳过其余的部分。这是一种编写C程序的方法,该方法已经成功使用了20多年。


2

借鉴各种Objective-C运行时的经验,用C编写动态的,多态的OO功能并不是一件容易的事(另一方面,使其变得快速且易于使用,似乎在25年后仍在继续)。但是,如果您在不扩展语言语法的情况下实现了Objective-C风格的对象功能,那么最终得到的代码将非常混乱:

  • 每个类都由一个结构定义,该结构声明其超类,它所遵循的接口,所实现的消息(作为“选择器”的映射,消息名称,“实现”,提供行为的函数)以及该类的实例可变布局。
  • 每个实例都由一个结构定义,该结构包含一个指向其类的指针,然后是其实例变量。
  • 消息发送是使用类似于的功能实现的(考虑到某些特殊情况)objc_msgSend(object, selector, …)。通过知道对象是哪个类的实例,它可以找到与选择器匹配的实现,从而执行正确的功能。

这就是通用OO库的一部分,该库旨在允许多个开发人员使用和扩展彼此的类,因此对于您自己的项目可能会显得过分杀伤。我经常使用结构和函数将C项目设计为“静态”的面向类的项目:-每个类都是指定ivar布局的C结构的定义-每个实例只是相应结构的一个实例-对象不能“消息化”,但MyClass_doSomething(struct MyClass *object, …)定义了类似方法的函数。与ObjC方法相比,这使代码中的内容更清晰,但灵活性更差。

需要权衡的地方取决于您自己的项目:听起来像其他程序员不会使用您的C接口,因此选择取决于内部偏好。当然,如果您决定要使用objc运行时库之类的东西,那么可以使用跨平台的objc运行时库。


1

GObject并没有真正隐藏任何复杂性,而是引入了自己的一些复杂性。我要说的是,即席事情比GObject容易,除非您需要信号或接口机制之类的高级GObject。

与COS稍有不同,因为它带有一个预处理器,该预处理器使用某些OO构造扩展了C语法。GObject有一个类似的预处理器,即G Object Builder

您也可以尝试使用Vala编程语言,它是完全高级语言,可以编译为C,并且可以使用纯C代码中的Vala库。它可以使用GObject,它自己的对象框架或临时方式(功能有限)。


1

首先,除非这是家庭作业或目标设备没有C ++编译器(我认为您不必使用C),否则可以很容易地从C ++提供带有C链接的C接口。

其次,我将研究您将在多大程度上依赖多态性和异常以及框架可以提供的其他功能,如果不是那么简单的话,具有相关功能的简单结构将比功能齐全的框架容易得多,如果您的相当一部分设计需要他们,然后硬着头皮使用框架,这样您就不必自己实现功能。

如果您还没有真正的设计方案可以做出决定,那么请花点时间看看代码告诉您什么。

最后,它不必是一个选择或其他选择(尽管如果从一开始就开始使用框架,那么您可能还会坚持使用它),应该有可能从简单结构的简单部分开始,只添加库如所须。

编辑:因此,其管理权决定让我们忽略第一点。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.