Oop In Cee
StartSeite | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern
Kann man in C objektorientiert Programmieren?
- Ja, aber es ist mehr Arbeit und alle Aspekte von OO Sprachen kann man nicht nachbilden.
Warum sollte es dann jemand tun wollen?
- Weil man die Vorteile von C (Systemnähe, Effizienz, ...) nicht aufgeben, aber möglichst vielen Vorteilen der OO Arbeitsweise und Denkweise haben möchte. Außerdem gibt es zusätzliche Vorteile, die OO Sprachen nicht haben.
siehe auch:
Vorteile von OopInCee:
- Ein sehr fluides System für die Wiederverwendung von Software sowohl auf der Ebene von Modulen als auch von Funktionen.
- Ballastfreies OO Programmierung ist möglich. D. h.
- Klassen können ohne Vererbung nahtlos (aus Sicht von OopInCee) erweitert werden.
- Das geht auch in einigen echten OO-Sprachen, z.B. in der SpracheRuby (richtig?)
- bei entsprechender Granularität der Bibliotheken enthält das ausführbare Programm nur benötigte Funktionen
- bei entsprechender Granularität der Klassen gilt das für jede OO-Sprache, oder?
- Durch die Arbeit mit nachgebildeten OO Funktionalitäten entsteht auch ein Verständnis für die innere Arbeitsweise von OO Sprachen. Damit kann man diese auch effizienter Nutzen.
- Es steht im Bedarfsfall immer ein Plus an Systemnähe und Effizienz zur Verfügung.
- ...
Nachteile von OopInCee:
- Die umfangreichen "goodies" von OO Sprachen, besonders die umfangreichen Standardbibliotheken sind nicht so ohne weiteres wettzumachen.
- Es bleibt gegenüber Sprachen mit GarbageCollection immer ein Mehraufwand in der Speicherverwaltung und ein MemoryLeak-Restrisiko.
- Der Bedarf an Systemnähe und Effizienz ist vielleicht gar nicht so groß, wie man erwarten mag (siehe WardsWiki:IllusionOfControl, zweiter Abschnitt)
- Der Effizienz-Vorteil ist vielleicht gar nicht so groß, wie man erwarten mag. (Die HotspotEngine? kann durch DynamischeOptimierung? in Teilbereichen sogar Performance-Vorteile gegenüber statisch optimiertem C-Code erreichen.)
- Der Bedarf an effizienter Entwicklung und gut wartbarem/erweiterbarem Design kann sicher mit einer 'echten' OO-Sprache besser befriedigt werden
- Die innere Arbeitsweise von modernen OO Sprachen ist möglicherweise so komplex, dass ich durch 'naive' Nachbildung in der SpracheCee gar nicht so viele wertvolle Einblicke bekomme und besser bedient wäre, mich mit der 'äußeren' Arbeitsweise von OO Sprachen zu beschäftigen.
- ...
Es kann natürlich verschiedene Ausprägungen von OO Programmierung in C geben, und ich beschreibe hier nur eine, relativ einfache Form. -- HelmutLeitner
Siehe auch einzelne Punkte von ProgrammierStilAlpha
C hat keine Objekte, also muss man die Verbindung von Datenstruktur und Funktionen nachbilden. Dazu vereinheitlicht man die Schnittstellen:
- Objekte werden immer nur als Pointer zur zugrunde liegenden Struktur übergeben
- Jeder Funktionsname beginnt mit dem Objektnamen.
Zusätzlich sammelt man alle Funktionen zu einem Objekt in einem Sourcemodul und alle Prototypen in einem korrespondierenden Header (es kann Gründe geben, von dieser Organisation abzuweichen).
Die typischen Basisfunktionen für ein Objekt sind:
| object=ObjectCreate(...);
ObjectSetProperty(object,property);
ObjectGetProperty(object,&property);
property=ObjectRetProperty(object);
error=ObjectAction(object);
error=ObjectActionAnotherobject(object,anotherobject);
...
ObjectFree(&object); |
|
|
Das Vokabular ist natürlich frei wählbar, man könnte statt ObjectCreate auch object_create, ObjectNew, object_new oder object_alloc verwenden, aber es muss konsistent sein.
Die Speicherverwaltung erfolgt manuell, d. h. der Programmierer hat die Verantwortung für die Freigabe der erzeugten Objekte. Dabei gibt es im wesentlich 3 Möglichkeiten (2 davon sind einfach):
- Objekte sind lokal zu einer Funktion. Sie werden am Anfang der Funktion erzeugt und am Ende der Funktion freigegeben (einfach).
- Objekte sind global. Sie werden irgendwann erzeugt und dann bis zum Ende des Programms benutzt. Eine explizite Freigabe ist nicht erforderlich (einfach).
- Verwaltete Objekte. Sie entstehen auf Grund von Eingaben, haben eine ungewisse Lebensdauer und werden auf Grund von Eingaben wieder freigegeben. Der Programmierer muss immer genau wissen, wer der Eigentümer eines Objektes ist, auch wenn mehrere Zeiger darauf existieren sollten. Diese Objekte müssen mit Hilfe irgendwelcher Verwaltungsysteme (-objekte) verwaltet werden. Das ist der komplizierte Fall.
Das Standardbeispiel sieht so aus:
| object=NULL;
...
object=ObjectCreate(...);
if(object==NULL) {
...Fehlerbehandlung...
}
...
ObjectFree(&object); |
|
|
Alle Freigabefunktionen müssen tolerant sein, das heißt: ObjectFree muss damit rechnen, dass ihm ein NULL-Object übergeben wird (und dann nichts tun). Weiter muss ObjectFree den Objektzeiger auf NULL setzen. D. h. es ist das Ziel dass alle Zeigervariablen korrekt sind, d. h. entweder den Wert NULL haben oder auf einen gültigen Speicherblock zeigen.
Auf diese Weise sind die Elemente voneinander ziemlich unabhängig und folgendes:
| object=NULL;
...
for(;;) {
switch(RandomRetIntRange(0,100)) {
case 0:
goto do_finalize;
case 1: case 14: case 77:
ObjectFree(&object);
break;
default:
ObjectFree(&object);
object=ObjectCreate(...);
break;
}
}
...
do_finalize:
ObjectFree(&object); |
|
|
wäre völlig problemlos in Bezug auf die korrekte Speicherverwaltung.
Da es in C keinen Vererbungsmechanismus gibt, muss Vererbung - wenn sie gewünscht ist - simuliert werden. Im Bereich der Datenstrukturen ist die Integration einer "Parent"-Struktur relativ einfach:
| typedef struct Parent {
...
} Parent;
typedef struct Child {
Parent *parent;
....
} Child; |
|
|
Bei den Methoden kann die Vererbung durch "Delegation" simuliert werden:
| ... ChildAction(Child *child) {
return ParentAction(child->parent);
} |
|
|
Vererbung ist auf diese Art relativ arbeitsintensiv. Daher wird man sie auf des absolut notwendige Minimum beschränken. Also isolierte, mächtige Objekte gegenüber komplexen Klassenhierarchien bevorzugen.
Virtuelle Funktionen können auch mit Hilfe von Funktionspointern realisiert werden.
Polymorphie kann, wenn sie benötigt wird (selten), mit verschiedenartigen Mitteln realisiert werden:
- mit Funktionspointern in der Parent-Struktur
- mit Funktionspointern in klasseneigenen "virtuellen" Funktionspointer-Tabellen (a la C++)
- mit Funktionspointern in klassen- oder objekteigenen Hash-Tabellen (a la Perl)
Eine Kapselung kann im strengen Sinn nicht realisiert werden, denn in C kann man mit einem Zeiger im Prinzip überall hineinarbeiten.
Eine Kapselung könnte in gewissen Ausmaß realisiert werden, indem die Übergabe der Objekte mittels neutralisierter Pointer (z. B. void Pointer) erfolgt, die nicht die eigentliche Strukturinformation haben.
Auf eine Kapselung wird verzichtet, weil sie für den C-Programmierer keine sinnvolle Kosten-Nutzen-Relation hat. Er muss in so vielen Bereichen diszipliniert sein, dass dies im Bereich der Objekte überhaupt kein zusätzliches Problem erzeugt.
Gültigkeit von Objekt-Zeigervariablen
Ein wichtiger Punkt ist die Gültigkeit von Objekt-Zeigervariablen. Die Funktion, die ein Objekt erzeugt seit auch für die Fehlerkontrolle (Zeiger = NULL) zuständig. Außer der Freigabefunktion müssen sich alle anderen Funktionen darauf verlassen können, dass sie gültige Objekte übergeben bekommen.
Anmerkung: es wäre nicht effizient, würden dutzende Funktionen in einem Modul (einer "Klasse") redundant immer wieder die Korrektheit des gleichen Objektzeigers prüfen, wenn dies an einer Stelle, dort wo das Objekt erzeugt wird, zentral einmal geprüft und - im Fehlerfall - optimal rückgemeldet werden kann.
Fehlerbehandlung
Die Art der Fehlerbehandlung ist nicht essentiell für OopInCee, aber rundet die Sache ab. Die Rückmeldung erfolgt nur auf 3 Arten:
- als eigener Fehlerstatus (OK=0, Fehler=negative Werte)
- als Integer-Rückgabewert <0 (wenn der normale Rückgabewert>=0) z. B. der Index in einer Tabelle oder die Position in einem File.
- als Zeiger-Rückgabewert =NULL (wenn der normale Rückgabewert ein Zeiger!=NULL ist)
Die Fehlerreaktion erfolgt mittels goto so, dass flache Funktionen entstehen:
| ...
error=ObjectAction(object);
if(error) {
...Fehlerbehandlung...
goto do_finalize;
}
...
error=AnobjectAction(anobject);
if(error) {
...Fehlerbehandlung...
goto do_finalize;
}
...
do_finalize:
ObjectFree(&object)
AnobjectFree(&anobject)
... Finalisierung weiterer Objekte |
|
|
Auf diese Weise bleiben die eigentlichen Aktivitäten einer Funktion auf der Hauptlinie und die Verschiebbarkeit von Aktivitäten wird nicht durch Einrückungsebenen behindert.
siehe OopInCeeBeispiel
Speicherverwaltung
Eine Funktion wie ObjectCreate(...) erzeugt die Objektdaten immer auf dem Heap. Damit man Objekte auch auf dem Stack ablegen kann, wäre eine Initialize-Funktion vielleicht besser:
- Heap
| Object* pObject=NULL;
...
pObject=malloc(sizeof(Object));
if(pObject==NULL) {
...Fehlerbehandlung...
}
ObjectInitialize(pObject, ...);
...
ObjectCleanup(pObject);
free(pObject);
pObject=NULL; /* Kann man in Makro zusammenfassen */ |
|
|
- Stack
| Object object;
...
ObjectInitialize(&object, ...);
...
ObjectCleanup(&object); |
|
|
-- MichaelButscher
Vorteile:
- Man spart sich für die oberste Ebene des Objektes die dynamische Speicherverwaltung.
- Einfache Objekte kommen damit ohne dynamische Speicherverwaltung aus.
- Es ist also ein Performancevorteil.
Nachteile:
- Das System enthält nun 2 Arten von Objekten.
- HeapObjekte, deren Lebensdauer varierbar ist und die über einen Referenzpointer verwaltet werden können.
- StackObjekte, die die Lebensdauer der Funktion haben, in der sie definiert sind. Diese Objekte können nicht über einen Pointer verwaltet werden.
- Komplexere Stack-Objekte (mit dynamischen Bestandteilen) sind merkwürdige Hybride. Der Rumpf am Stack, innere dynamische Bestandteile am Heap.
Im Grunde ist das der Unterschied zwischen C++ und Java. C++ kann beide Arten von Objekten. Java kann nur Heapobjekte und ist dadurch einfacher.
In OopInCee schlage ich vor, den einfacheren Weg zu gehen und auf Stack-Objekte zu verzichten. Performance-Vorteile ergeben sich bei einfachen Objekten, die sich auch als Strukturen ohne dynamischer Speicherverwaltung verstehen lassen.
-- HelmutLeitner
Aus OopInCee#Polymorphie:
- Polymorphie kann, wenn sie benötigt wird (selten)...
Polymorphie ist meiner Meinung nach eine der wichtigsten Eigenschaften von OOP überhaupt! Gerade dadurch, dass ich nur wissen muss, dass ein Objekt eine bestimmte Nachricht versteht, und dieses selber dafür verantwortlich ist, angemessen darauf zu reagieren, wird ein OO-Design doch so flexibel und leicht erweiterbar! -- IljaPreuß
Na ja. OopInCee ist eben nur eine unvollständige Nachbildung. Polymorphie ist möglich, muss aber mit einem Mehr an Komplexität und Arbeitsaufwand, u.U. sogar mit Performanceverlusten bezahlt werden. Bei der Polymorphie stellt sich dann noch die Frage nach der Ausprägung der Polymorphie. Das was du beschreibst (Ein Objekt braucht nur eine bestimmte Nachricht zu verstehen) ist z.B. weder in Java noch C++ erfüllt (Bist du Smalltalker?). In C könnte ich mir zur Implementierung nur etwas Perl-ähnliches mit einer Hash-Tabelle pro Klasse mit den Methoden vorstellen. Oder vielleicht die versteckte, explizite Übergabe eines Interface-Methoden-Vektors mit dem Objektpointer. --hl
- Wenn man mit "Nachricht" nur den Aufruf eines bestimmten Eintrags in der virtuellen Methodentabelle meint, dann kann C++ auch Nachrichten empfangen.
- Das scheint mir sogar sicherer, da es keine zufällige Namensgleichheit wie in SpracheSmalltalk geben kann.
- Java andererseits kann auch Methoden nach Namen aufrufen mittels Reflection. Dürfte allerdings nicht sehr effizient sein. --MichaelButscher
Eigentlich bin ich in der SpracheJava zuhause, lerne aber gerade die SpracheSmalltalk. Ich muss zugeben, dass mir die StatischeTypisierung? von Java bereits das eine oder andere Mal in die Quere gekommen ist. Nichtsdestotrotz erachte ich die CodeRefactorings TypenschlüsselDurchUnterklasseErsetzen? und TypenschlüsselDurchZustandOderStrategieErsetzen? als elementar für das Verständnis von OoDesign? - und Grundvoraussetzung dafür ist eben das Vorhandensein von Polymorphie.
Unter diesem Aspekt möchte ich bezweifeln, dass mit OopInCee eine 'ballastfreie OO Programmierung möglich ist', dass es sich um ein 'sehr fluides System...' handelt oder dass man wirklich ein zutreffendes 'Verständnis für die innere Arbeitsweise von OO Sprachen' erhält. -- ip
In dieser Konzentration scheinen mir meine eigenen Formulierungen etwas dick aufgetragen. Trotzdem möchte ich meine Standpunkte verteidigen. Wir müssten nur aufpassen, nicht in irgendeine Form von PissingContest abzugleiten. Wenn ich sage OopInCee gibt einen Sinn, dann meine ich ausdrücklich nicht, dass C in irgendeinem generellen Sinn "besser" ist als OO Sprachen. -- hl
Ist eigentlich garantiert, daß in C eine struct, die mit den gleichen Elementen beginnt wie eine andere "kompatibel" ist? Also z. B.:
| #include <stdio.h>
struct Base {
int a;
double b;
}
struct Derived {
int a;
double b;
char* c;
}
int main(void) {
struct Derived deri;
struct Base* pBase;
pBase=(struct Base*)&deri;
deri.b=0.5;
deri.a=2;
printf("%i %f", pBase->a, pBase->b);
return 0;
} |
|
|
Das sollte etwa "2 0.5" ausgeben. --mb
Ja, das ist garantiert. Der Standard garantiert, dass die Reihenfolge der Strukturelemente nicht verändert wird. Das Alignment kann bei gleicher Feldreihenfolge nicht variieren (solange du das StrukturAlignment? nicht bewußt beeinflußt, indem du die Strukturen in verschiedene Compilationseinheiten gibst und allfällig vorhandene implemetierungsspezifische Alignment-Parameter unterschiedlich setzt). -- hl
Ich störe mich etwas an der formulierung ganz zu Beginn dieser Seite: [In C OO Programmieren] ist mehr Arbeit und alle Aspekte von OO Sprachen kann man nicht nachbilden. Dies stimmt natürlich nicht. Man kann alle Aspekte nachbilden. Dies sieht man schon daher das C TuringÄquivalent? ist und die ersten Compiler für C++ die Sprache nicht in Assembler sondern in C übersetzt haben. Wieso steht das denn so da? -- th
Theoretisch gesehen hast du vielleicht recht, aber praktisch funktionieren nur Dinge mit vertretbarem Arbeitsaufwand. Versuche z.B. in C Exceptions so zu implementieren, dass es sowohl von der Syntax her als auch von der Funktionalität her (Resourcenfreigabe der zwischen throw und catch liegenden Funktionen) äquivalent ist. Das ist kaum realisierbar. Wenn ich von OopInCee rede, dann möchte ich damit etwas Praktisches beschreiben, das mehr Nutzen bringt als es Arbeitsaufwand kostet. -- hl
KategorieC KategorieCee KategorieOop
StartSeite | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern
Text dieser Seite ändern (zuletzt geändert: 29. November 2007 8:44 (diff))