Aus Linux-Magazin 02/2023

Wie man mit C objektorientiert programmieren kann

© Simon Dannhauer / 123RF.com

Gibt es etwas Besseres als Objektorientierung? Ja: Objektorientierung mit C! Wer behauptet, das ginge nicht, dem beweist der vorliegende Artikel das Gegenteil.

Objektorientierung ist prinzipiell eine gute Idee, wenn man sie maßvoll einsetzt. Trotz der vielfältigen Nutzung in Sprachen wie C++, Java, PHP, Python und noch vieler anderer Programmierkonzepte meine ich, dass das eine Übertreibung dieses Prinzips darstellt. Das Klassenkonzept und auch die Operatorüberladung als Beispiel für die entsprechende Methodenentwicklung lassen einen schnell den Überblick verlieren, und es ist teilweise sogar erlaubt, bewusst regelrechten mathematischen Unsinn zu schreiben. Die Sprachmittel »struct«, »typedef« sowie das ausgefeilte Funktions- und noch ausgefeiltere Zeigerkonzept von C hingegen erlauben es, dasselbe wesentlich einfacher, schneller und auch für die Mitmenschen lesbarer und damit verständlicher zu gestalten.

Mathe und PC

Sehr häufig bestehen der Wunsch und die Notwendigkeit, Fragestellungen der numerischen Mathematik für den Computer aufzubereiten, und zwar so, dass das Resultat auch für Programmiernovizen oder Laien selbsterklärend, lesbar und damit verständlich ist. Ich bin nicht der Erste, der so etwas fordert, sondern befinde mich damit in guter Gesellschaft: Linus Torvalds zum Beispiel verlangt dasselbe in noch viel schärferer Form in seiner Anleitung zum Coding Style in den Kernel-Quellen [1] – aus gutem Grund, wie ich finde. Quelltexte sollen ja auch von anderen gelesen und unter Umständen noch modifiziert werden können, damit die hineingesteckte Arbeit nicht mit dem Autor ausstirbt.

Es gibt noch einen anderen Grund, diese Forderung von Linus Torvalds zu unterstützen: Die mathematischen Beschreibungen in der entsprechenden Fachliteratur, die es in eine entsprechende Software zu gießen gilt, sind oft schon kompliziert genug. Noch unüberschaubarer wird es dann, wenn sie mit fluffigen Floskeln (“wie man leicht sieht …”) gespickt sind. Dann folgt oft noch der geradezu Mantra-artige Rat, man möge das Ganze doch bitte objektorientiert programmieren, weil das ja alle so machen (wirklich?), in einer Sprache, die auch alle nutzen (alle?), weil das doch modern sei. An dieser Stelle kommt man nicht umhin, einmal nachzufragen, was Objektorientierung überhaupt bedeutet?

Die zu diesem Thema bereits in unüberschaubarer Zahl erschienenen Artikel und Fachbücher helfen dabei nicht gerade weiter [2]. Lediglich ein Werk, das von Ben Klemens 2013 bei O’Reilly veröffentlichte “C im 21. Jahrhundert” [3], hat mich angespornt, es noch einmal mit Objektorientierung zu versuchen. Aber wie lautet nun eigentlich der Grundgedanke der Objektorientierung? Dem entsprechenden Wikipedia-Artikel [2] kann ich zwar in den ersten vier Zeilen noch folgen, aber warum OO zwingend “die Einführung verschiedener Konzepte, insbesondere Klassen, Vererbung, Polymorphie und spätes Binden (dynamisches Binden)” nach sich ziehen soll, erschließt sich mir in keiner Weise.

Um das Ganze einmal mit eigenen Worten zu formulieren: Es gibt Operanden und Operatoren (die man auch als Methoden bezeichnen kann), die es in sinnvoller Weise in eine einfache und nützliche Beziehung zueinander zu setzen gilt. Das soll dabei helfen, ein Problem beispielsweise aus der numerischen Mathematik zu bewältigen. Eigentlich klappt das aber auch schon mit den Sprachkonzepten von C, Klassendefinitionen sind hier gar nicht notwendig. Da kann man Ben Klemens nur zustimmen. Den Nagel auf den Kopf bringt ein Bonmot [4] von James Gosling, dem Entwickler von Java. Er antwortete einmal auf die Frage, was er denn anders machen würde, müsste er Java noch einmal entwickeln: “Ich würde die Klassen weglassen.”

Operatoren und Bibliotheken

Dass man das OO-Konzept auch übertreiben kann, möchte ich noch an einem anderen Beispiel demonstrieren, bevor ich es damit gut sein lasse und eine andere Art der Objektorientierung vorstelle.

Wenn man Operatoren im Sinne des Klassenhierarchiekonzepts überladen möchte, kann man das gern tun. Dann lässt sich aber beispielsweise der Operator »++« in einer Methodendefinition einfach mit »–« überschreiben – schon ist der allerschönste mathematische Unsinn programmiert. Ein Sprachkonzept, das so etwas zulässt, kann nicht im Sinne des Erfinders sein. Da bleibe ich lieber bei einer imperativen Sprache wie C, die bereits vieles möglich macht, wenngleich sie dem Programmierer viel Verantwortung für seine Quelltexte auferlegt. Doch dieser Verantwortung stelle ich mich gern.

Ein letztes Wort noch zum Thema numerische Mathematik: Freilich kenne ich Umsetzungen zum Bewältigen numerisch-mathematischer Probleme wie die Bibliotheken BLAS, LAPACK und GSL [5]. Die ersten beiden stammen noch aus Fortran-Blütezeiten und erfüllen nach wie vor vielfältige Zwecke, vor allem in Fortran-Quelltexten. Ein Paradebeispiel bietet die quantenmechanische Ab-initio-Software GAMESS-US [6], deren Strukturierung oder besser gesagt nur schwer zu entdeckende Strukturierung mir (und wahrscheinlich nicht nur mir) immer wieder Kopfzerbrechen bereitet.

Die GSL-Bibliothek hingegen kommt der von mir bevorzugten Art der Programmierung bereits sehr nahe, braucht aber für meine Begriffe immer noch zu viele Parameter in den jeweiligen Funktionsaufrufen. Es hat halt jeder so seinen Stil. Das möchte ich dennoch gern etwas anders haben, und wie, darauf kommen wir nun zu sprechen.

Fragestellung

Zur Erläuterung, wie man auch in C objektorientiert programmieren kann, genügt ein einfaches Beispiel: Die Berechnung der Fibonacci-Zahlenfolge ist bereits seit Anfang des 13. Jahrhunderts bekannt. Der italienische Rechenmeister Leonardo Fibonacci beschrieb damit das numerische Wachstum einer Kaninchenpopulation. Tatsächlich folgen auch noch zahlreiche andere Wachstumsvorgänge in der Natur der Fibonacci-Folge.

Die mathematische Lösung kann man mit dem in Listing 1 angegebenen C-Quellcode darstellen. Kurz ausgedrückt, besagt er: Wenn das Funktionsargument kleiner oder gleich 1 ist, dann gib 1 zurück; ansonsten liefere mir die Summe aus den beiden Vorgängerzahlen. Das kann man mathematisch als fn**= fn-1+fn-2 ausdrücken, wie sich auf Wikipedia nachlesen lässt [7]. Entsprechend dieser Vorschrift bildet sich die Zahlenfolge 0, 1, 1, 2, 3, 5, 8, 13, 21 und so fort.

Die C-Funktion aus Listing 1 weist aber noch auf eine weitere wichtige Fähigkeit von C hin, die Rekursion.

Listing 1

Fibonacci-Funktion

int fibonacci(int i) {
 if (i <= 1) {
  return 1;
 }
 return fibonacci(i - 1) + fibonacci(i - 2);
}

Objekt und Methode

Als Objekt für die Fibonacci-Zahlenfolge dient eine einfache »struct«-Definition. Sie stellt noch keine Instanz im objektorientierten Sinn dar, wie wir später noch sehen werden. Die beiden enthaltenen Variablen sind dennoch alles, was wir brauchen; komplizierter muss man es nicht machen.

Listing 2

Fibonacci-Objekt

struct object_fibonacci {
 int arg;
 int sum;
};

Um eine Zahlenfolge beschreiben zu können, bedarf es, wie die Mathematiker sich auszudrücken pflegen, einer Bildungsvorschrift wie jener aus der oben zitierten Formel für fn.

Mit der Funktion nach Listing 1 lässt sich die Folge auch rekursiv berechnen. Die Methode, die ich beschreiben und auch anwenden will, sehen Sie in Listing 3. Die Funktionsdefinition erscheint zwar etwas komplizierter als die aus Listing 1. Dennoch hat diese Darstellung mehrere Vorteile bezüglich des Rückgabewerts, des Funktionskopfs, des Funktionsarguments sowie der Funktionsdefinition.

Listing 3

Fibonacci-Methode

int method_fibonacci(void *args)
{
 int success=1;
 struct object_fibonacci *p = (struct object_fibonacci *)args;
 struct object_fibonacci r1,r2;
 if(p != NULL) {
  if(p->arg <= 1) {
    p->sum=1;
  }
  else {
   r1.arg=p->arg - 1;
   success=method_fibonacci(&r1);
   r2.arg=p->arg - 2;
   success=method_fibonacci(&r2);
   p->sum=r1.sum + r2.sum;
  }
 }
 else {
  fprintf(stderr, "Object %s is not defined.\n", "p");
  return EXIT_FAILURE;
 }
 return EXIT_SUCCESS;
} /* method_fibonacci */

Der Rückgabewert ist vom Typ »int« und zeigt lediglich an, ob die Methode von Erfolg gekrönt war oder nicht. Der in der ersten Zeile deklarierte Funktionskopf eignet sich dazu, eine allgemeine Typdefinition vorzunehmen (siehe Listing 4). Das kommt einem Prototyping gleich, weil man damit viele Funktionen auf dieselbe Weise schreiben kann.

Listing 4

Operator-Prototypen

typedef int OPERATOR(void *args);
typedef int (*p_operator)(void * args);

Das heißt, dass alle Funktionen, die man entsprechend deklariert beziehungsweise definiert, automatisch der Typdefinition »OPERATOR« entsprechen. Darüber hinaus lassen sich mittels des Typs »p_operator« auch Zeigervariablen des Typs »OPERATOR« definieren. Diese Variablen kann man anschließend sehr abstrakt nutzen, um Funktionszeiger von Funktionen aufzunehmen, die inhaltlich noch gar nicht bekannt sind. Mehr über diese Vorgehensweise lesen Sie bei Interesse im Klassiker “Programmieren in C” [8] im Unterkapitel 5.11. “Zeiger auf Funktionen” nach. Das Konzept ist also schon lange bekannt und wird hier nur noch etwas weiter abstrahiert.

Es gibt nur ein Funktionsargument, einen typenlosen Zeiger (»void*«). Das hat den Vorteil, dass es sich innerhalb der Funktionsdefinition (Listing 3, Zeilen 4 und 5) per Typecasting in einen Zeiger beliebiger Art umwandeln lässt, also auch in einen Pointer auf eine Instanz der »struct«-Definition nach Listing 2. Der Zeiger selbst wird ja innerhalb der Funktionsdefinition nicht modifiziert. Dennoch stehen alle Variablen der entsprechenden Struktur en bloc mithilfe des Pfeiloperators »->« zum Auslesen oder Modifizieren zur Verfügung.

Das ist genau das, was ich unter Objektorientierung verstehe: Es gibt ein Objekt (einen Operanden), den man einer Methode (Operator, Funktion) übergibt, die den Inhalt des Objekts bearbeitet. Damit ist auch gleichzeitig das Thema Datenkapselung erledigt [9]: Der Operator selbst soll lediglich mitteilen, ob er erfolgreich war oder nicht.

Innerhalb der Funktionsdefinition fallen für den rekursiven Aufruf noch zwei lokale Instanzen der Objektdefinition nach Listing 2 an. Das sieht zwar etwas umständlicher aus als in Listing 1, eine Ähnlichkeit ist aber unverkennbar: Der »arg«-Wert aus der übergeordneten, per Zeiger »p« übergebenen Struktur wird der einen Instanz (»r1«) als Argument (Zeile 12) übergeben, gefolgt von einem rekursiven Funktionsaufruf (Zeile 13). Anschließend erfolgt dasselbe für die zweite Instanz »r2« (Zeile 14 und 15). Am Ende übergibt der Code beide Werte summarisch an »p->sum« (Zeile 16), und die Arbeit ist getan.

Mehr braucht es nicht. Freilich könnte man die beiden Instanzen »r1« und »r2« auch als typbezogene Zeiger vereinbaren. Allerdings bedarf es dann auch bei jedem einzelnen Aufruf der rekursiven Funktion »method_fibonacci« einer Speicherallokation und entsprechenden Wiederfreigabe. Wenn man diese Wohlgeformtheit als Programmierprinzip nicht beherzigt, dann wird in jedem Fall Valgrind so etwas anmosern.

Die Anwendung

Letztlich bedarf es noch eines Tests (siehe Listing 5), ob das Konstrukt denn tatsächlich funktioniert. Zwei Präprozessor-»include«-Anweisungen für die Standard-Header-Dateien »stdio.h« und »stdlib.h« sowie der Code für die »main«-Funktion aus Listing 5 vervollständigen den Quelltext.

Listing 5

main-Funktion

int main()
{
 int success = 1;
 struct object_fibonacci *pfibonacci = NULL;
 /* allocate one object */
 pfibonacci = (struct object_fibonacci *) calloc(1, sizeof(struct object_fibonacci));
 /* initialize the object, calloc allows neglecting this */
 pfibonacci->arg = 0;
 pfibonacci->sum = 0;
 /* apply the object */
 for(int i = 0; i < 11; i++) {
  pfibonacci->arg = i;
  success = method_fibonacci(pfibonacci);
  fprintf(stdout, "Fibonacci of %d is %d.\n", pfibonacci->arg, pfibonacci->sum);
 }
 /* free the object */
 free(pfibonacci);
 pfibonacci = NULL;
}

Das Inkludieren der Header-Dateien und das Anflanschen des Codes aus den Listings 2, 3 und 5 ergibt letztlich einen Quelltext, nennen wir ihn »fibonacci.c«, den Sie wie gewohnt mit GCC übersetzen. Die Ausgabe des Programms dürfte wie in Listing 6 aussehen. Falls es Ihnen ein Bedürfnis ist, können Sie auch noch ein Profiling mit Gprof und einen Speichertest mit Valgrind vornehmen.

Listing 6

Ausgabe

Fibonacci of 0 is 1.
Fibonacci of 1 is 1.
Fibonacci of 2 is 2.
Fibonacci of 3 is 3.
Fibonacci of 4 is 5.
Fibonacci of 5 is 8.
Fibonacci of 6 is 13.
Fibonacci of 7 is 21.
Fibonacci of 8 is 34.
Fibonacci of 9 is 55.

Um sich mit der dargestellten Art der Objektorientierung weiter vertraut zu machen, empfehle ich, den Grundgedanken dieser Art der Berechnung (auch der rekursiven) auf eine weitere bekannte Funktion anzuwenden: die Fakultät. Bei mir funktioniert das tadellos, und das nächste Thema, das ich auf diese Weise angehen möchte, ist der große Komplex der numerischen Integration. Für weitere Anwendungen dieser Art gibt es genügend Anlass, und ich lade Sie herzlich dazu ein, sich mit mir darüber auszutauschen.

Fazit

Die Listings zeigen, dass die Sprache C alle Mittel an Bord hat, um objektorientiert zu programmieren. Deswegen hat diese Art der Objektorientierung auch Eingang in meinen allgemeinen Programmierfundus gefunden.

Das große Gebiet der numerischen Integration mit all seinen Facetten als gewöhnliche oder partielle Differentialgleichungen beziehungsweise entsprechende Systeme, die vollständige adaptive Integration [10] oder auch Anfangs- und Anfangsrandwertaufgaben einschließlich einer Thread- beziehungsweise MPI-Parallelisierung warten auf die Implementierung und Anwendung. Letztere könnte beispielsweise ein Programm sein, das quantenchemische Ab-initio-Berechnungen anstellen kann, das Ganze selbstverständlich mit C unter der GPL, damit jeder es lesen und verstehen kann. (jlu)

Der Autor

Dr. Peter Friedel arbeitet am Leibniz-Institut für Polymerforschung Dresden e.V. als theoretischer Chemiker und Strukturmodellierer. Er administriert zudem zwei Linux-Rechencluster am Institut für Theorie der Polymere. Darüber hinaus interessiert ihn sowohl aus naturwissenschaftlicher wie auch philosophischer Sicht die Frage, wie das Leben auf der Erde entstand.

Infos

  1. “Linux kernel coding style”: https://www.kernel.org/doc/html/v4.10/process/coding-style.html
  2. Objektorientierung: https://de.wikipedia.org/wiki/Objektorientierte_Programmierung
  3. Ben Klemens, “C im 21. Jahrhundert”: https://oreilly.de/produkt/c-im-21-jahrhundert/
  4. James Gosling über Java: https://news.ycombinator.com/item?id=20029739
  5. GNU Scientific Library: https://www.gnu.org/software/gsl/
  6. Michael W. Schmidt et. al., “General Atomic and Molecular Electronic Structure System”: https://onlinelibrary.wiley.com/doi/abs/10.1002/jcc.540141112
  7. Fibonacci-Folge: https://de.wikipedia.org/wiki/Fibonacci-Folge
  8. Brian Kernighan, Dennis Ritchie, “Programmieren in C”: https://www.hanser-kundencenter.de/fachbuch/artikel/9783446154971
  9. Datenkapselung: https://de.wikipedia.org/wiki/Datenkapselung_(Programmierung)
  10. Peter Friedel et. al., “A hierarchical MPI Communication Model for the Parallelized Solution of Multiple Integrals”: https://link.springer.com/chapter/10.1007/3-540-48228-8_48
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
1 Kommentar
Älteste
Neuste Beste Bewertung
Chris Boesch
2 Jahre her

In Summe ist der Ansatz für C gut. Moderne Sprachen wie Go oder Zig greifen das ebenfalls auf und verfeinern den Ansatz der structs dahingehend, dass direkte Funktionsaufrufe innerhalb der structs möglich sind. Dadurch hat man noch mehr Objekt-Charakter, weil bspw. ein Datei-Objekt dann über Funktionen zum Öffnen und Schließen etc. verfügt. Gepaart mit der Möglichkeit Fehler zusätzlich zu Ergebnissen zurückliefern zu können. Auf etwas möchte ich aber noch aufmerksam machen, was mir in dem Artikel nicht so gut gefallen hat: Rekursion. Ist ganz nett sollte aber tunlichst vermieden werden, weil einerseits schnell sehr teuer und andererseits kann undefiniertes Verhalten… Mehr »

Nach oben