Skip to content

Objektorientierung in ANSI C

Die Wahl ist vorbei, jeder kann sich aufregen oder freuen – wir haben nun für die nächsten 4 Jahre scheinbar eine neue Regierung und egal wie es ausgegangen wäre, es hätten sich immer welche beschwert. Nun kann ich mich wieder ein wenig besser um den eigentlichen Sinn des Blogs kümmern und aus dem Leben eines Softwarearchitekten berichten. Heute gibt es zum ersten Mal etwas Praxis aus der Entwicklung. Das Thema wird wohl jeden interessieren, der schonmal eine objektorientiert ausgelegte Sprache wie Java genutzt hat und nun mal mit Ansi C entwickeln möchte.

[ad#co-1]

Wie versprochen, wenn auch etwas verspätet, fange ich nun an nach und nach ein paar Codezeilen zu veröffentlichen.
Damit ich in meinem ersten Beitrag diesbezüglich nicht klecker sondern klotze, gibt es diesmal auch ein komplettes Projekt als Beispiel.
Jeder der schonmal halbwegs professionell eine Software geschrieben hat wird wissen wie wichtig es ist den Code übersichtlich, wartbar und wiederverwendbar zu halten.
Das Zauberwort um dies zu erreichen heißt Objektorientierung.
Gerade in einer Sprache wie JAVA oder mittlererweile auch PHP bietet es sich einfach an in Klassen zu arbeiten, diese sauber zu strukturieren und oft wieder zu verwenden. Natürlich kann man auch ohne Objektorientierung gute Programme entwickeln, jedoch glaube ich, dass mir niemand wiedersprechen wird wenn ich sage, dass ein objektorientierter Quelltext besser zu lesen und zu verstehen ist als ein prozedural oder sturkturiert geschriebener Quelltext, wie es normalerweise in C der Fall ist. Oftmals verlaufen sich die Programmierer bei der prozeduralen Programmierung in endlosen Schleifen die niemand nachvollziehen kann.
Um ein Programm auch in Ansi C halbwegs lesbar zu halten bietet es sich an zu überlegen, ob man es vielleicht schaffen könnte mit den Hausmitteln von C so etwas wie objektorientiertes Programmieren hinzubekommen.
Als Erstes skizziere ich dazu was die Objektorientierung ausmacht :

  • Vererbung
  • Polymorphie
  • Kapselung

Die Vererbung und Polymorphie sind in Ansi C nicht bekannt.
Für die Kapselung gibt es etwas Vergleichbares. Mit Hilfe des struct kann man eine Kapselung schon vom Hause aus erreichen. Durch das struct ist es im begrenzten Rahmen auch möglich Vererbung und Polymorphie zu nutzen.
Da ja bekanntlich ein Code mehr als 1000 Worte sagt, veröffentliche ich als erstes die Quelltextdateien.
Das Projekt ist denkbar Simple, wird haben eine Hauptklasse, in unserem Fall „animal“, diese hat die Funktion ein Tierlautauszugeben, desweiteren haben wir zwei Klassen „dog“ und „cat“ die jeweils von der „animal“ Klasse erben und ihren eigenen Tierlaut setzen.

Die Header und Sourcedateien der Klasse animal sehen wie folgt aus :
animal.h

  1.  
  2. #include <stdio.h>
  3. #include <string.h>
  4.  
  5. #ifndef ANIMAL_H_
  6. #define ANIMAL_H_
  7.  
  8. typedef struct
  9. {
  10.         char* animalsound;
  11.         void (*sound)(void* this);
  12. } class_animal;
  13.  
  14. void FreeClassAnimal(class_animal this);
  15. class_animal NewClassAnimal();
  16.  
  17. #endif
  18.  

animal.c

  1.  
  2. #include "animal.h"
  3.  
  4. static void sound(void* this)
  5. {
  6.         class_animal animal = (*(class_animal*)this);
  7.        
  8.         printf("%s", animal.animalsound);
  9. };
  10.  
  11. class_animal NewClassAnimal()
  12. {
  13.         class_animal ret;
  14.        
  15.         ret.animalsound = "Nothing";
  16.         ret.constructor = (void*)constructor;
  17.         ret.destructor = (void*)destructor;
  18.         ret.sound = (void*)sound;
  19.        
  20.         return ret;
  21. };
  22.  
  23. void FreeClassAnimal(class_animal this)
  24. {
  25. };
  26.  

In der Headerdatei wird das struct aufgebaut. Dies beinhaltet die Klassenvariablen und Pointer auf die eigentlichen Funktionen. Etwas unschön ist es, dass man nicht mit this oder ähnlichem auf das struct Zugreifen kann und aus diesem Grund das Objekt immer übergeben muss (siehe Funktion sound()). Es gibt einen Konstruktor (NewClassAnimal()) und einen Destruktor (FreeClassAnimal()) die zum Initialisieren und reservieren des benötigten Speichers bzw. deren Freigabe nach der Verwendung genutzt werden können. Theoretisch wäre es auch noch möglich den construtor und destructor als Teil des struct zu setzen, jedoch empfinde ich diese Nutzung als Intuitiver und weniger Kompliziert.

Die ANSI C Version der cat Klasse :

cat.h

  1.  
  2. #include "animal.h"
  3.  
  4. #ifndef CAT_H_
  5. #define CAT_H_
  6.  
  7. typedef struct
  8. {
  9.         class_animal super;
  10.         void (*sound)(void* this);
  11. } class_cat;
  12.  
  13. void FreeClassCat(class_cat this);
  14. class_cat NewClassCat();
  15.  
  16. #endif
  17.  

cat.c

  1.  
  2. #include "cat.h"
  3.  
  4. static void sound(void* this)
  5. {
  6.         class_cat cat = (*(class_cat*)this);
  7.        
  8.         printf("Cat : ");
  9.        
  10.         cat.super.sound(&cat.super);
  11.        
  12.         printf("\n");
  13. };
  14.  
  15. class_cat NewClassCat()
  16. {
  17.         class_cat ret;
  18.         ret.super = NewClassAnimal();
  19.  
  20.         ret.super.animalsound = "MiauMiauMiau";
  21.        
  22.         ret.sound = (void*)sound;
  23.        
  24.        
  25.         return ret;
  26. };
  27.  
  28. void FreeClassCat(class_cat this)
  29. {
  30. };
  31.  

In der cat.c findet zum ersten Mal eine Art Vererbung statt. Das struct class_cat kann auf die Funktionen der class_animal zugreifen und diese gegebenenfalls überschreiben, wie in der Implementation der sound() zu erkennen ist.

Und die objektorientierte Version der dog Klasse

dog.h

  1.  
  2. #include "animal.h"
  3.  
  4. #ifndef DOG_H_
  5. #define DOG_H_
  6.  
  7. typedef struct
  8. {
  9.         class_animal super;
  10.         void (*sound)(void* this);
  11. } class_dog;
  12.  
  13. void FreeClassDog(class_dog this);
  14. class_dog NewClassDog();
  15.  
  16. #endif
  17.  

dog.c

  1.  
  2. #include "dog.h"
  3.  
  4. static void sound(void* this)
  5. {
  6.         class_dog dog = (*(class_dog*)this);
  7.        
  8.         printf("Dog : ");
  9.        
  10.         dog.super.sound(&dog.super);
  11.        
  12.         printf("\n");
  13. };
  14.  
  15. class_dog NewClassDog()
  16. {
  17.         class_dog ret;
  18.         ret.super = NewClassAnimal();
  19.         ret.super.animalsound = "Wuff";
  20.        
  21.         ret.sound = (void*)sound;
  22.        
  23.         return ret;
  24. };
  25.  
  26. void FreeClassDog(class_dog this)
  27. {
  28. };
  29.  

Das Programm kann einfach mit der folgenden Main.c getestet werden

Main.c

  1.  
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include "animal.h"
  5. #include "dog.h"
  6. #include "cat.h"
  7.  
  8. int main(int argc, char** argv)
  9. {
  10.         printf("we are testing OOP in C with animal hierarchy : \n\n");
  11.        
  12.         class_animal animal;
  13.         animal = NewClassAnimal();
  14.         animal.sound(&animal);
  15.         FreeClassAnimal(animal);
  16.         printf("\n");
  17.  
  18.         class_dog dog;
  19.         dog = NewClassDog();
  20.         dog.sound(&dog);
  21.         FreeClassDog(dog);
  22.        
  23.         class_cat cat;
  24.         cat = NewClassCat();
  25.         cat.sound(&cat);
  26.         FreeClassCat(cat);
  27.        
  28.         printf("\nfinished!\n");
  29.        
  30.         return EXIT_SUCCESS;
  31. }
  32.  

Die Ausgabe des Programms sieht wie folge aus :

we are testing OOP in C with animal hierarchy : 

Nothing
Dog : Wuff
Cat : MiauMiauMiau

finished!

Ich hoffe der Beitrag konnte einigen Helfen.
Über Kritik und Anregungen würde ich mich freuen.

Interessante Artikel

There are no related posts on this entry.

Kommentare

6 Kommentare

  1. Rolf Kemper Dezember 28, 2013

    Hallo Herr Altmann,

    ich habe versucht den Ansatz mal mit visual studio 2010 zu kompellieren.
    Folgendes ist aufgefallen:
    1) this wird angemeckert , habe ich durch self ersetzt
    2) Fehlende deklaration für constructor , destructor habe ich durch auskommentieren in aniaml.c gelöst

    Dann den test auf animal reduziert.
    (Siehe Zeilen unter dem Text )

    Schade das ich das nicht hinbekomme.
    Mit welcher Umgebung haben Sie gearbeitet ?

    MfG
    R. Kemper

    ******************** output log vom VS *****************************************
    1>—— Erstellen gestartet: Projekt: Animals, Konfiguration: Debug Win32 ——
    1> Test.c
    1>c:\users\rolf\documents\visual studio 2010\projects\animals\animals\test.c(11): error C2275: ‚class_animal‘: Ungültige Verwendung dieses Typs als Ausdruck
    1> c:\users\rolf\documents\visual studio 2010\projects\animals\animals\animal.h(11): Siehe Deklaration von ‚class_animal‘
    1>c:\users\rolf\documents\visual studio 2010\projects\animals\animals\test.c(11): error C2146: Syntaxfehler: Fehlendes ‚;‘ vor Bezeichner ‚animal‘
    1>c:\users\rolf\documents\visual studio 2010\projects\animals\animals\test.c(11): error C2065: ‚animal‘: nichtdeklarierter Bezeichner
    ………

    Hier die simplen modifizierten dateien:

    ************** anaimal.h ***************************************
    #include
    #include

    #ifndef ANIMAL_H_
    #define ANIMAL_H_

    typedef struct
    {
    char* animalsound;
    void (*sound)(void* self);
    } class_animal;

    void FreeClassAnimal(class_animal self);
    class_animal NewClassAnimal();

    #endif

    ***************** animal.c *****************************
    #include „animal.h“

    static void sound(void* self)
    {
    class_animal animal = (*(class_animal*)self);

    printf(„%s“, animal.animalsound);
    };

    class_animal NewClassAnimal()
    {
    class_animal ret;

    ret.animalsound = „Nothing“;
    // ret.constructor = (void*)constructor;
    // ret.destructor = (void*)destructor;
    ret.sound = (void*)sound;

    return ret;
    };

    void FreeClassAnimal(class_animal self)
    {
    };
    ********************* Test.c **********************************
    #include
    #include
    #include „animal.h“
    //#include „dog.h“
    //#include „cat.h“

    int main(int argc, char** argv)
    {
    printf(„we are testing OOP in C with animal hierarchy : \n\n“);

    class_animal animal;
    animal = NewClassAnimal();
    animal.sound(&animal);
    FreeClassAnimal(animal);
    printf(„\n“);
    /*
    class_dog dog;
    dog = NewClassDog();
    dog.sound(&dog);
    FreeClassDog(dog);

    class_cat cat;
    cat = NewClassCat();
    cat.sound(&cat);
    FreeClassCat(cat);
    */
    printf(„\nfinished!\n“);

    return EXIT_SUCCESS;
    }
    *****************************************************************************

  2. Mr. Spex März 16, 2014

    wird das „this“ evtl. angemeckert weil der visual studio compiler den für ein reserviertes schlüsselwort hält? das dürfte bei einem c-compiler normalerweise kein problem sein.
    ich würde bei der c-programmierung nicht auf visual studio setzen – allgemein nicht auf windows.
    ich würde den gcc zur kompilierung empfehlen.

  3. No Name Juni 18, 2015

    Ich bin auf der Suche nach einer Implementierung für Polymorphie in dem Sinn, dass ich ein array von Basisklassenobjekten deklarieren, dann aber Instanzen abgeleiteter Klassen darin speichere.

    Wenn eine abgeleitete Klasse nun zusätzliche Felder definiert, würde der hier vorgestellte Ansatz nicht mehr funktionieren, da die Objekte der abgeleiteten Klasse mehr Speicher benötigen als die Objekte der Basisklasse. Ist das richtig?

  4. Jens Altmann Juni 18, 2015

    Es kommt darauf an. Wenn du einen eigenen destrcutor für jede Klasse schreibst und diesen auch sauber jedes mal aufrufst, sollte die zusätzliche Speicherreservierung kein Problem sein. Du musst nur dafür sorgen, dass du immer von der niedrigsten Klasse anfängst zu bereinigen. Zusätzlich Speicher kannst du darin jederzeit erzeugen.

  5. Stefan Lang Juni 19, 2015

    Hallo.
    Über eine Frage auf CodePorject..com bin ich auf diese Seite gestossen. Da ich dort bereits entsprechende Kritiken widergegeben habe, fand ich es nur fair, diese auch hier anzumerken:

    1. Die Klassenobjekte werden mehrfach unnötig kopiert, da sie entweder nicht als Referenz übergeben werden, oder (in den sound() Funktionen) unnötig eine lokale Kopie erstellt wird. Dies hat potentiell oder definitiv verschiedene negative Effekte:
    a) es kann bei komplexen Objekten Probleme im Bereich Performance und Speicher hervorrufen.
    b) der Programmcode, der auf diese Kopien angewendet wird, kann niemals das originale Objekt verändern!
    c) in Erweiterung zu b) kann natürlich dieser Code somit auch keine Veränderung hervorrufen, die einen Vererbungseffekt duplizieren könnte, da natürlich auch das übergeordnete Objekt kopiert wird.

    2. Das Testprogramm ist nicht geeignet um Vererbung vorzuführen: es ruft die sound Funktion immer nur auf dem Klassen-Interface auf, das dem tatsächlichen Typ des Objekts entspricht. Vererbung bedeutet, dass das korrekte Verhalten ausgelöst wird, auch wenn man die Funktionen über das Interface der Basisklasse aufruft.
    Beispiel:

    class_dog dog = NewClassDog();
    class_animal* my_pet = &dog;
    my_pet->sound();

    3. Kopien, Kopien, Kopien. Ich habe es zwar in Punkt 1 schon angesprochen, aber muss es nochmals tun, denn im Code werden andauernd Kopien erstellt, nicht nur von den Objekten selbst, auch von den enthaltenen super Objekten, und den animalsound Pointern. In diesn Beispielen funktioniert das vermutlich noch ohne grosse Probleme, aber in typischen objekt-orientierten Anwendungen führt das unweigerlich zu grossen problemen, da einige Funktionen mit teilweise modifizierten Objekten arbeiten, während andere noch mit (älteren) unmodifizierten Versionen arbeiten. Das _kann_ so nicht funkionieren. Es gibt nur einen Weg, hier sauber zu arbeiten, nämlich dass immer nur ein Objekt existiert, und dann auch immer nur Referenzen darauf weiter gegeben und kopiert werden, niemals das Objekt selbst – ausser natürlich du willst genau das erreichen.

    4. Abschliessender Tipp: Dass objektorientierte Programmierung in C möglich ist, haben die frühen C++-Compiler bewiesen – die haben nämlich genau dies getan, indem sie den C++-Code in C-Code „übersetzt“ haben. Es gibt mit Sicherheit Artikel darüber, aus denen man zu diesem Thema Einiges lernen kann…

  6. Jens Altmann Juni 19, 2015

    Hallo Stefan,

    danke für deine Anmerkungen und Hinweise. Du hast natürlich recht, dass der Speicher sinnvoller genutzt werden kann. Auch bei dem Beispielprogramm hast du Recht.
    Der Artikel selbst ist auch von 2009. Heue würde ich die „Klassen“ auch anders schreiben und formulieren. Jedoch ist dieses Beispiel ein gutes Beispiel wie man sowas wie Klassen, Methoden sowie Klassenvariablen in C nutzen kann.

Kommentiere den Artikel

Required

Required

Optional