OO Best Practices für C-Programme [geschlossen]

19

"Wenn du wirklich OO-Zucker willst, dann benutze C ++", war die sofortige Antwort, die ich von einem meiner Freunde erhielt, als ich danach fragte. Ich weiß, dass zwei Dinge hier absolut falsch sind. Erstens ist OO NICHT 'Zucker' und zweitens hat C ++ NICHT C absorbiert.

Wir müssen einen Server in C schreiben (das Front-End, auf das Python zugreifen soll), und deshalb suche ich nach besseren Möglichkeiten, um große C-Programme zu verwalten.

Die Modellierung eines großen Systems in Bezug auf Objekte und Objektinteraktionen macht es einfacher zu verwalten, zu warten und zu erweitern. Wenn Sie jedoch versuchen, dieses Modell in C zu übersetzen, das keine Objekte (und alles andere davon) enthält, stehen Sie vor einigen wichtigen Entscheidungen.

Erstellen Sie eine benutzerdefinierte Bibliothek, um die OO-Abstraktionen bereitzustellen, die Ihr System benötigt? Dinge wie Objekte, Kapselung, Vererbung, Polymorphismus, Ausnahmen, Pub / Sub (Ereignisse / Signale), Namespaces, Introspection usw. (zum Beispiel GObject oder COS ).

Oder Sie verwenden einfach die grundlegenden C-Konstrukte ( structund Funktionen), um alle Ihre Objektklassen (und andere Abstraktionen) auf Ad-hoc-Weise zu approximieren. (zum Beispiel einige der Antworten auf diese Frage auf SO )

Der erste Ansatz gibt Ihnen eine strukturierte Möglichkeit, Ihr gesamtes Modell in C zu implementieren. Er fügt jedoch auch eine Komplexitätsebene hinzu, die Sie beibehalten müssen. (Denken Sie daran, dass wir die Komplexität reduzieren wollten, indem wir zunächst Objekte verwendeten.)

Ich kenne den zweiten Ansatz nicht und weiß nicht, wie effektiv er bei der Annäherung aller Abstraktionen ist, die Sie möglicherweise benötigen.

Meine einfachen Fragen lauten also: Was sind die Best Practices für die Realisierung eines objektorientierten Entwurfs in C. Denken Sie daran, ich frage nicht, wie ich das tun soll. Dies und diese Fragen sprechen darüber und es gibt sogar ein Buch darüber. Was mich mehr interessiert, sind einige realistische Ratschläge / Beispiele, die die wirklichen Probleme ansprechen, die sich zeigen, wenn Sie dies tun.

Hinweis: Bitte raten Sie nicht, warum C nicht zu Gunsten von C ++ verwendet werden sollte. Wir haben diese Etappe weit hinter uns gelassen.

Baumkodierer
quelle
3
Sie können C ++ Server so schreiben, dass es eine externe Schnittstelle ist extern "C"und von Python aus verwendet werden kann. Sie können dies manuell tun oder sich dabei von SWIG unterstützen lassen. Der Wunsch nach Python-Frontend ist also kein Grund, C ++ nicht zu verwenden. Das heißt nicht, dass es keine triftigen Gründe gibt, bei C. zu bleiben
Jan Hudec
1
Diese Frage muss geklärt werden. In den Absätzen 4 und 5 wird derzeit im Wesentlichen gefragt, wie vorgegangen werden soll, aber dann sagen Sie, dass Sie "nicht danach fragen, wie es zu tun ist" und stattdessen (eine Liste?) Bewährter Verfahren wünschen. Wenn Sie nicht wissen möchten, wie es in C gemacht wird, fragen Sie dann nach einer Liste von "Best Practices" im Zusammenhang mit OOP im Allgemeinen? Wenn ja, sagen Sie das, aber seien Sie sich bewusst, dass die Frage wahrscheinlich als subjektiv eingestuft wird .
Caleb
:) Ich frage nach echten Beispielen (Code oder sonstiges), wo es gemacht wurde - und auf welche Probleme sie dabei gestoßen sind.
Treecoder
4
Ihre Anforderungen scheinen verwirrend. Sie bestehen darauf, die Objektorientierung ohne einen Grund zu verwenden, den ich sehen kann (in einigen Sprachen ist dies eine Möglichkeit, Programme wartbarer zu machen, aber nicht in C), und bestehen darauf, C zu verwenden. Die Objektorientierung ist ein Mittel, kein Zweck oder ein Allheilmittel . Darüber hinaus profitiert es stark von der Sprachunterstützung. Wenn Sie tatsächlich OO wollten, sollten Sie dies bei der Sprachauswahl berücksichtigen. Eine Frage, wie man mit C ein großes Softwaresystem erstellt, wäre viel sinnvoller.
David Thornley
Vielleicht möchten Sie einen Blick auf "Objektorientiertes Modellieren und Entwerfen" werfen. (Rumbaugh et al.): Es gibt einen Abschnitt über die Zuordnung von OO-Designs zu Sprachen wie C.
Giorgio,

Antworten:

16

Von meiner Antwort auf Wie soll ich komplexe Projekte in C strukturieren (nicht OO, sondern Komplexitätsmanagement in C):

Der Schlüssel ist die Modularität. Dies ist einfacher zu entwerfen, zu implementieren, zu kompilieren und zu warten.

  • Identifizieren Sie Module in Ihrer App, wie Klassen in einer OO-App.
  • Separate Schnittstelle und Implementierung für jedes Modul. Fügen Sie nur die Schnittstelle ein, die von anderen Modulen benötigt wird. Denken Sie daran, dass es in C keinen Namespace gibt, sodass Sie alle Elemente in Ihren Schnittstellen eindeutig definieren müssen (z. B. mit einem Präfix).
  • Blenden Sie globale Variablen in der Implementierung aus und verwenden Sie Accessorfunktionen zum Lesen / Schreiben.
  • Denken Sie nicht in Vererbung, sondern in Zusammensetzung. Versuchen Sie im Allgemeinen nicht, C ++ in C nachzuahmen, da dies sehr schwer zu lesen und zu warten ist.

Von meiner Antwort auf Was sind die typischen Namenskonventionen für öffentliche und private OO C-Funktionen (ich denke, dies ist eine bewährte Methode):

Die Konvention, die ich benutze, ist:

  • Öffentliche Funktion (in Header-Datei):

    struct Classname;
    Classname_functionname(struct Classname * me, other args...);
  • Private Funktion (statisch in Implementierungsdatei)

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

Darüber hinaus können viele UML-Tools C-Code aus UML-Diagrammen generieren. Ein Open Source ist Topcased .

mouviciel
quelle
+1 für Link Wie soll ich komplexe Projekte in C
treecoder 7.11.11
1
Gute Antwort. Streben Sie nach Modularität. OO soll es bereitstellen, aber 1) in der Praxis ist es allzu üblich, mit OO-Spaghetti zu enden, und 2) es ist nicht der einzige Weg. Schauen Sie sich für einige Beispiele im wirklichen Leben den Linux-Kernel (Modularität im C-Stil) und Projekte an, die glib (OO im C-Stil) verwenden. Ich hatte die Gelegenheit, mit beiden Stilen zu arbeiten, und IMO C-Modularität gewinnt.
Joh
Und warum genau ist Komposition ein besserer Ansatz als Vererbung? Begründung und unterstützende Referenzen sind willkommen. Oder Sie haben sich nur auf C-Programme bezogen?
Aleksandr Blekh
1
@AleksandrBlekh - Ja, ich beziehe mich nur auf C.
Mouviciel
16

Ich denke, Sie müssen OO und C ++ in dieser Diskussion unterscheiden.

Das Implementieren von Objekten ist in C möglich und ziemlich einfach - erstellen Sie einfach Strukturen mit Funktionszeigern. Das ist Ihr "zweiter Ansatz", und ich würde mitmachen. Eine andere Möglichkeit wäre, keine Funktionszeiger in der Struktur zu verwenden, sondern die Datenstruktur in direkten Aufrufen als "Kontext" -Zeiger an die Funktionen zu übergeben. Das ist besser, IMHO, da es besser lesbar und leichter nachvollziehbar ist und das Hinzufügen von Funktionen ermöglicht, ohne die Struktur zu ändern (einfache Vererbung, wenn keine Daten hinzugefügt werden). So implementiert C ++ normalerweise den thisZeiger.

Der Polymorphismus wird komplizierter, da es keine eingebaute Vererbungs- und Abstraktionsunterstützung gibt. Daher müssen Sie entweder die übergeordnete Struktur in Ihre untergeordnete Klasse aufnehmen oder viele Kopierpasten ausführen einfach. Enorme Anzahl von Fehlern, die darauf warten, passiert zu werden.

Virtuelle Funktionen können auf einfache Weise durch Funktionszeiger erreicht werden, die bei Bedarf auf andere Funktionen verweisen. Dies ist sehr fehleranfällig, wenn diese manuell ausgeführt werden.

In Bezug auf Namespaces, Ausnahmen, Vorlagen usw. - ich denke, wenn Sie auf C beschränkt sind - sollten Sie diese einfach aufgeben. Ich habe daran gearbeitet, OO in C zu schreiben, würde es aber nicht tun, wenn ich die Wahl hätte (an diesem Arbeitsplatz habe ich mich buchstäblich für die Einführung von C ++ eingesetzt und die Manager waren "überrascht", wie einfach es war, es zu integrieren mit dem Rest der C-Module am Ende.).

Es versteht sich von selbst, dass Sie, wenn Sie C ++ verwenden können, C ++ verwenden. Es gibt keinen wirklichen Grund, dies nicht zu tun.

littleadv
quelle
Tatsächlich können Sie von einer Struktur erben und Daten hinzufügen: Deklarieren Sie einfach das erste Element der untergeordneten Struktur als Variable, deren Typ die übergeordnete Struktur ist. Dann gießen Sie nach Bedarf.
mouviciel
1
@mouviciel - ja. Ich sagte, dass. " ... Sie müssen also entweder die übergeordnete Struktur in Ihre
untergeordnete
5
Kein Grund zu versuchen, Vererbung zu implementieren. Als Mittel zur Wiederverwendung von Code ist dies zunächst eine fehlerhafte Idee. Die Objektzusammensetzung ist einfacher und besser.
KaptajnKold
@KaptajnKold - stimme zu.
Littleadv
8

Im Folgenden finden Sie die Grundlagen zum Erstellen der Objektorientierung in C

1. Erstellen von Objekten und Kapselung

Normalerweise erstellt man ein Objekt wie

object_instance = create_object_typex(parameter);

Methoden können hier auf eine der beiden Arten definiert werden.

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

Beachten Sie, dass in den meisten Fällen object_instance (or object_instance_private_data)eine Rückgabe vom Typ void *.Die Anwendung kann nicht auf einzelne Member oder Funktionen dieser verweisen.

Darüber hinaus verwendet jede Methode diese object_instance für die nachfolgende Methode.

2. Polymorphismus

Wir können viele Funktionen und Funktionszeiger verwenden, um bestimmte Funktionen in der Laufzeit zu überschreiben.

Beispiel: Alle object_methods sind als Funktionszeiger definiert, der sowohl auf öffentliche als auch auf private Methoden erweitert werden kann.

Wir können auch eine Funktionsüberladung in einem begrenzten Sinne anwenden, indem wir eine verwenden, var_args die der in printf definierten variablen Anzahl von Argumenten sehr ähnlich ist. Ja, das ist in C ++ nicht so flexibel - aber das ist der naheliegendste Weg.

3. Vererbung definieren

Die Definition der Vererbung ist etwas schwierig, aber mit Strukturen kann man Folgendes tun.

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

hier ist das doctorund engineervon der Person abgeleitet und es ist möglich, es auf eine höhere Ebene zu tippen, sagen wir person.

Das beste Beispiel hierfür ist GObject und daraus abgeleitete Objekte.

4. Erstellen virtueller Klassen Ich zitiere ein reales Beispiel aus einer Bibliothek namens libjpeg, die von allen Browsern für die JPEG-Dekodierung verwendet wird. Es wird eine virtuelle Klasse mit dem Namen error_manager erstellt, die von der Anwendung erstellt und zurückgegeben werden kann.

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

Beachten Sie hier, dass JMETHOD in einem Funktionszeiger durch ein Makro erweitert wird, das jeweils mit den richtigen Methoden geladen werden muss.


Ich habe versucht, so viele Dinge zu sagen, ohne zu viele individuelle Erklärungen. Aber ich hoffe, die Leute können ihre eigenen Sachen ausprobieren. Meine Absicht ist es jedoch nur zu zeigen, wie die Dinge sich abbilden.

Es wird auch viele Argumente geben, dass dies nicht genau die wahre Eigenschaft des C ++ - Äquivalents ist. Ich weiß, dass OO in C seiner Definition nach nicht so streng sein wird. Aber wenn man so arbeitet, versteht man einige der Grundprinzipien.

Wichtig ist nicht, dass OO so streng ist wie in C ++ und JAVA. Es ist so, dass man den Code strukturell unter Berücksichtigung des OO-Denkens organisieren und auf diese Weise betreiben kann.

Ich würde den Leuten dringend empfehlen, das wahre Design von libjpeg und die folgenden Ressourcen zu sehen

ein. Objektorientierte Programmierung in C
b. Dies ist ein guter Ort, an dem Menschen Ideen austauschen.
c. und hier ist das ganze Buch

Dipan Mehta
quelle
3

Die Objektorientierung besteht aus drei Dingen:

1) Modularer Programmaufbau mit autonomen Klassen.

2) Datenschutz mit privater Kapselung.

3) Vererbung / Polymorphismus und verschiedene andere hilfreiche Syntax wie Konstruktoren / Destruktoren, Vorlagen usw.

1 ist bei weitem am wichtigsten, und es ist auch völlig sprachunabhängig, es dreht sich alles um Programmgestaltung. In C erstellen Sie dazu autonome "Codemodule", die aus einer .h-Datei und einer .c-Datei bestehen. Betrachten Sie dies als das Äquivalent einer OO-Klasse. Sie können anhand des gesunden Menschenverstands, der UML oder der für C ++ - Programme verwendeten OO-Entwurfsmethode entscheiden, was in dieses Modul eingefügt werden soll.

2 ist auch sehr wichtig, um nicht nur vor dem absichtlichen Zugriff auf private Daten zu schützen, sondern auch vor unbeabsichtigtem Zugriff, dh "Unordnung im Namespace". C ++ erledigt dies auf elegantere Weise als C, aber es kann immer noch in C mit dem Schlüsselwort static erreicht werden. Alle Variablen, die Sie in einer C ++ - Klasse als privat deklariert hätten, sollten in C als statisch deklariert und im Dateibereich abgelegt werden. Sie sind nur aus ihrem eigenen Codemodul (Klasse) zugänglich. Sie können "setters / getters" genau wie in C ++ schreiben.

3 ist hilfreich, aber nicht notwendig. Sie können OO-Programme ohne Vererbung oder ohne Konstruktoren / Destruktoren schreiben. Diese Dinge sind nett zu haben, sie können sicherlich die Programme eleganter und vielleicht auch sicherer machen (oder das Gegenteil, wenn sie unachtsam verwendet werden). Sie sind aber nicht notwendig. Da C keine dieser hilfreichen Funktionen unterstützt, müssen Sie einfach darauf verzichten. Konstruktoren können durch Init / Destruct-Funktionen ersetzt werden.

Die Vererbung kann durch verschiedene Tricks von struct erfolgen, aber ich würde davon abraten, da dies Ihr Programm wahrscheinlich nur komplexer macht, ohne dass es zu Gewinn kommt (die Vererbung sollte im Allgemeinen sorgfältig angewendet werden, nicht nur in C, sondern in einer beliebigen Sprache).

Schließlich jeder OO Trick kann aus den frühen 90er Jahren erweist sich diese in C. Axel-Tobias Schreiner Buch „Objektorientierte Programmierung mit ANSI C“ erfolgen. Ich würde dieses Buch jedoch niemandem empfehlen: Es fügt Ihren C-Programmen eine unangenehme, seltsame Komplexität hinzu, die sich einfach nicht lohnt. (Das Buch ist hier kostenlos erhältlich, auch wenn ich Sie gewarnt habe.)

Also ist mein Rat, 1) und 2) oben zu implementieren und den Rest zu überspringen. Es ist eine Methode, C-Programme zu schreiben, die sich seit mehr als 20 Jahren bewährt hat.


quelle
2

Das Schreiben einer dynamischen, polymorphen OO-Fähigkeit in C ist nicht allzu schwierig, da es sich nach 25 Jahren immer noch als schnell und einfach zu handhaben herausstellt. Wenn Sie jedoch eine Objektfunktion im Objective-C-Stil implementieren, ohne die Sprachsyntax zu erweitern, ist der Code, mit dem Sie enden, ziemlich chaotisch:

  • Jede Klasse wird durch eine Struktur definiert, die ihre Oberklasse deklariert, die Schnittstellen, mit denen sie konform ist, die Nachrichten, die sie implementiert (als Map von "Selector", dem Nachrichtennamen, "Implementierung", der Funktion, die das Verhalten liefert) und die Instanz der Klasse variables Layout.
  • Jede Instanz wird durch eine Struktur definiert, die einen Zeiger auf ihre Klasse und dann auf ihre Instanzvariablen enthält.
  • Das Senden von Nachrichten wird mithilfe einer Funktion implementiert (einige Sonderfälle angeben oder annehmen), die so aussieht objc_msgSend(object, selector, …). Wenn es weiß, zu welcher Klasse das Objekt gehört, kann es die Implementierung finden, die dem Selektor entspricht, und so die richtige Funktion ausführen.

Dies ist alles Teil einer universellen OO-Bibliothek, die es mehreren Entwicklern ermöglicht, die Klassen des jeweils anderen zu verwenden und zu erweitern. Dies kann für Ihr eigenes Projekt zu viel sein. Ich habe C-Projekte oft als "statische" klassenorientierte Projekte mit Strukturen und Funktionen entworfen: - Jede Klasse ist die Definition einer C-Struktur, die das IVAR-Layout spezifiziert - jede Instanz ist nur eine Instanz der entsprechenden Struktur - Objekte können nicht sein "messaged", aber methodenähnliche Funktionen, die so aussehen, MyClass_doSomething(struct MyClass *object, …)sind definiert. Dies macht die Dinge klarer im Code als der ObjC-Ansatz, hat aber weniger Flexibilität.

Der Kompromiss hängt von Ihrem eigenen Projekt ab: Es hört sich so an, als würden andere Programmierer Ihre C-Schnittstellen nicht verwenden, sodass die Wahl auf die internen Einstellungen zurückzuführen ist. Wenn Sie sich für etwas wie die objc-Laufzeitbibliothek entscheiden, gibt es natürlich plattformübergreifende objc-Laufzeitbibliotheken, die gewartet werden.


quelle
1

GObject verbirgt keine Komplexität und führt eine eigene Komplexität ein. Ich würde sagen, dass die Ad-hoc-Sache einfacher ist als GObject, es sei denn, Sie benötigen die fortgeschrittenen GObject-Dinge wie Signale oder die Schnittstellen-Maschinerie.

Bei COS ist das etwas anders, da dies mit einem Präprozessor geliefert wird, der die C-Syntax mit einigen OO-Konstrukten erweitert. Es gibt einen ähnlichen Präprozessor für GObject, den G Object Builder .

Sie können auch die Programmiersprache Vala ausprobieren , eine vollständige Hochsprache, die in C kompiliert wird und die die Verwendung von Vala-Bibliotheken aus einfachem C-Code ermöglicht. Es kann entweder GObject, sein eigenes Objekt-Framework oder die Ad-hoc-Methode (mit eingeschränkten Funktionen) verwenden.

Jan Hudec
quelle
1

Zunächst einmal, es sei denn, dies ist eine Hausaufgabe oder es gibt keinen C ++ - Compiler für das Zielgerät. Ich glaube nicht, dass Sie C verwenden müssen. Sie können eine C-Schnittstelle mit C-Verknüpfung aus C ++ einfach genug bereitstellen.

Zweitens würde ich untersuchen, inwieweit Sie sich auf Polymorphismus und Ausnahmen verlassen und welche anderen Funktionen die Frameworks bieten können, wenn es sich nicht um einfache Strukturen mit verwandten Funktionen handelt Das Design benötigt sie, beißt dann in die Kugel und verwendet ein Framework, damit Sie die Funktionen nicht selbst implementieren müssen.

Wenn Sie noch kein Design haben, um die Entscheidung zu treffen, machen Sie einen Spike und sehen Sie, was der Code Ihnen sagt.

Letztendlich muss es nicht die eine oder andere Wahl sein (wenn Sie von Anfang an auf Framework setzen, können Sie sich auch daran halten), es sollte möglich sein, mit einfachen Strukturen für die einfachen Teile zu beginnen und nur Bibliotheken hinzuzufügen wie benötigt.

EDIT: Es ist also eine Entscheidung des Managements, den ersten Punkt zu ignorieren.

jk.
quelle