Copy Konstruktor Beispiel Essay

Ein Kopierkonstruktor, oft Copy-Konstruktor genannt, ist in der Objektorientierten Programmierung ein spezieller Konstruktor, der eine Referenz auf ein Objekt desselben Typs als Parameter entgegennimmt und die Aufgabe hat, eine Kopie des Objektes zu erstellen.

Beispiel[Bearbeiten | Quelltext bearbeiten]

Als Beispiel dient eine Klasse, die eine Zeichenkette oder eine Klasse selben Typs über ihren Konstruktor verarbeitet. Das folgende Beispiel in C++ zeigt zum Vergleich einen gewöhnlichen Konstruktor und einen Kopierkonstruktor:

classMitCopyKonstruktor{private:char*cString;public:// gewöhnlicher KonstruktorMitCopyKonstruktor(constchar*value){cString=newchar[strlen(value)+1];// Speicher der richtigen Länge reservierenstrcpy(cString,value);// Den String aus value in den reservierten Speicher kopieren}// Kopierkonstruktor:// hat in C++ immer die Signatur "Klassenname(const Klassenname&)"MitCopyKonstruktor(constMitCopyKonstruktor&rhs)// Üblicherweise rhs: "Right Hand Side"{cString=newchar[strlen(rhs.cString)+1];strcpy(cString,rhs.cString);}};

Aufruf[Bearbeiten | Quelltext bearbeiten]

Der Kopierkonstruktor wird bei der Initialisierung eines Objektes mittels eines anderen Objekts desselben Typs aufgerufen. In C++ wird dieses andere Objekt als einziger Parameter dem Konstruktor übergeben. Es erfolgt in der Deklaration des Objektes die Zuweisung des anderen Objektes oder das Objekt wird als Wertparameter an eine Funktion oder Methode übergeben.

Beispiel in C++ (Fortsetzung):

intmain(){MitCopyKonstruktormitCC("Dulz");// Erstellt eine Zeichenkette MitCopyKonstruktormitCC2=mitCC;// Kopierkonstruktor, ZuweisungssyntaxMitCopyKonstruktormitCC3(mitCC);// Kopierkonstruktor, Aufrufsyntax}

Verwendung[Bearbeiten | Quelltext bearbeiten]

Dieser Artikel oder Abschnitt bedarf einer Überarbeitung. Näheres ist auf der Diskussionsseite angegeben. Hilf mit, ihn zu verbessern, und entferne anschließend diese Markierung.

Einige Programmiersprachen, wie beispielsweise C++, stellen einen vordefinierten Kopierkonstruktor zur Verfügung, der einfach die Elementvariablen des zu kopierenden Objektes in die des zu initialisierenden Objektes kopiert. (In anderen Programmiersprachen, z. B. Java, muss der Kopierkonstruktor explizit programmiert werden.) Dies kann allerdings zu Problemen führen. Sind unter den Elementvariablen nämlich Handles auf Ressourcen und gibt das bereits existente Objekt die Ressourcen frei, so ist das Handle in dem per Standard-Kopierkonstruktor erstellten Objekt ungültig und seine Verwendung kann dann zu Programmabstürzen führen. Pointer auf Speicherbereiche werden so ebenfalls kopiert, so dass die Kopie des Ursprungsobjekts nun Pointer auf bereits genutzte Speicherbereiche besitzt. Werden nun diese Speicherbereiche geändert, z. B. durch eine Änderung des Ursprungs oder des kopierten Objekts, so hat das Auswirkungen auf alle Objekte, die Pointer auf den gleichen Speicherbereich verwenden.

Im Beispiel enthält jede Instanz von Zeichenkette ihren eigenen Speicher, der beim Aufruf des Kopierkonstruktors reserviert wird. Wenn jede Kopie eines Objektes exklusiven Zugriff auf ihre Ressourcen hat, d. h., sie nicht mit anderen Objekten teilen muss, spricht man von einer tiefen Kopie (engl. deep copy). Andernfalls spricht man von einer flachen Kopie (engl. shallow copy). Eine flache Kopie produziert der Compiler mit dem vordefinierten Kopierkonstruktor automatisch. Ist in der Klasse Zeichenkette kein Kopierkonstruktor definiert, der eine tiefe Kopie erstellt, würden nach einer Kopie zwei Objekte einen Zeiger auf denselben Speicherblock haben, da die Adresse einfach kopiert werden würde. Ein Objekt weiß dann aber nicht, ob das andere bereits delete auf dem Speicherblock aufgerufen hat. Sowohl ein Zugriff auf den Speicher als auch ein erneutes delete würden dann zu einem Absturz des Programmes führen. Folgendes Beispiel illustriert dies.

Beispiel in C++ (gekürzt):

classZeichenketteF{public:/* * Konstruktor mit Parameter. * In der Initialisierungsliste wird der Zeiger m_memory so * initialisiert, dass er auf den neu reservierten Speicher auf dem * Heap zeigt. */explicitZeichenketteF(constchar*value):m_memory(newchar[strlen(value)+1]){// Kopiert den String aus value in den reservierten Speicherstrcpy(m_memory,value);}/* * Destruktor. */~ZeichenketteF(){// Gibt den im Konstruktor reservierten Speicher wieder freideletem_memory;}/* * Kopierkonstruktor. * In der Initialisierungsliste wird der Zeiger z.m_memory kopiert, * aber nicht der Speicherbereich, auf den er zeigt (!). * Es gibt anschließend zwei Objekte von ZeichenketteF, deren Zeiger * m_memory auf denselben Speicherbereich zeigen. */ZeichenketteF(constZeichenketteF&z):m_memory(z.m_memory){}private:/* * Zuweisungsoperator. * * Die Deklaration als "private" und die fehlende Definition sorgen * dafür, dass der Compiler eine Zuweisung mittels "=" nicht * zulässt und auch keinen Zuweisungsoperator implizit erzeugt. */ZeichenketteF&operator=(constZeichenketteF&z);char*m_memory;};voidscheitere(){ZeichenketteFname("Wolfgang");ZeichenketteFkopie(name);/* Nun wird eine so genannte flache Kopie erstellt. * Sowohl name.m_memory als auch kopie.m_memory zeigen nun auf * denselben Speicher! * * Sobald die Funktion scheitere() endet, wird für beide Objekte der * Destruktor aufgerufen. Der erste gibt den Speicherbereich frei, * auf den m_memory zeigt; der zweite versucht, denselben Speicher * nochmals freizugeben, was zu undefiniertem Verhalten führt. * Das kann z.B. ein Programmabsturz sein. */}

Kosten tiefer Kopien[Bearbeiten | Quelltext bearbeiten]

Wie am Beispiel unter Aufruf sichtbar, finden tiefe Kopien statt, daraus folgt eine gewisse Last. Zur Vermeidung unnötiger Last empfehlen sich zwei Varianten der oben dargestellten Kopier-Strategie.

  • Ressourcen mittels Referenzzählung in verschiedenen Instanzen gemeinsam zu nutzen; viele Implementierungen der Klasse String machen hiervon Gebrauch.
  • konstante Referenzen als Parameter in Funktionen und Methoden zu übernehmen, in all den Fällen, in denen auf Parameter nur lesend zugegriffen wird.

Der Kopierkonstruktor selbst zeigt in seinem Prototyp wie man unnötige tiefe Kopien von Objekten vermeidet, auf die man nur lesend zugreifen muss: Er übernimmt eine konstante Referenz, denn sonst müsste er ja (implizit) aufgerufen werden, bevor er aufgerufen wird! Die Signatur "Klassenname(const Klassenname&)" ist auch deshalb typisch.

Siehe auch[Bearbeiten | Quelltext bearbeiten]

Ich boykottiere die neue Rechtschreibung!
(... bis auf Ausnahmen)

C++ Programmierrichtlinien
plus einige Tips & Tricks

C++ Programming (Style) Guidelines
plus some Tips & Tricks

Original location of this document:
www.in.tu-clausthal.de/~zach/progr/guidelines.html

Gabriel Zachmann
TU Clausthal, Institut fuer Informatik
Julius-Albert-Str. 4
D-38678 Clausthal

Version 2.3, Nov 2004


Inhalt

Teil 1 - Kurzfassung (für Experten)

Teil 2 - für Noch-Nicht-Experten


Intro
Programmier-Stil
    Die n Gebote für Programmierer
    The Python Way
Namenskonventionen
    Intro
    Meta-Naming (C++)
    C und C++
    C
    C++
Kommentare
Strukturierung und Layout der Files
    Anordnung innerhalb einer Klasse
    Einrückung
Wartbarkeit
    Leserlichkeit
Gute und schlechte Programmier-Praxis
    C++
    Round-off errors
    Fest-verdrahtete Pfade
    Arrays
    Magic numbers
    Makros
    Annahmen
    Eingabe-Parameter
    "Can't happen"-Fälle
    Misc
    Anfänger-Bugs
    Kleinere Angelegenheiten
    Optimierungen
Object-oriented Design
    Allgemeine Richtlinien
    Liskov's Substitution Principle
    Open/Closed Principle
    Klasse oder Algorithmus?
Robustheit
Arbeitsmethoden
    "Später kommt nie"
    Wie klaut man Code?
    Wie sucht man Bugs?
    RTFM
    Tools
    Bibliographie, weitere Guidelines

Teil 1 - Kurzfassung (für Experten)

Dieser Teil ist für erfahrene Programmierer gedacht.

Namenskonventionen

An dieser Stelle nur Beispiele, die unsere Konventionen verdeutlichen sollen. Ergänzende Details gibt es weiter unten. (Zum Meta-Naming.)
Natürlich ist das wirklich Wichtige von Namen deren Aussageb) für den Benutzer (wirklich gute Programmierer erkennt man an ihren guten Objekt- und Methodennamen).


 
b) "Nomen sit omen".
 

Kommentare

Verwende die Templates in template-comments.cpp.
Mehr zum Kommentieren.

Strukturierung und Layout der Files

Verwende die Templates und . (Details dazu.)
Für C-Files verwende . C++-Files haben das Suffix , Header-Files haben das Suffix , die Implementierung von Templates haben das Suffix .

Jeder File im Projekt muß einen eindeutigen Namen haben.

Nur wenige Klassen pro File. Falls mehrere Klassen in einem File stehen, sollen diese unmittelbar miteinander zu tun haben bzw. zu einer grözeren Einheit zusammen gehören. (Bsp: kleine Helper-Klassen, die man sonst als Klasse-in-Klasse implementieren würde.)

Klammerung wird vertikal ausgerichtet. Beispiel:

int myFunction( .. ) { if ( .. ) { for ( .. ) { .. } } else { .. } } Die Einrücktiefe pro Stufe sind 4 Spaces!
K&R-Style ist verboten!
Strukturiere eine Zeile sinnvoll durch Spaces.
Mehr zum Thema Einrückung und Spaces.

Teile größere Sinnzusammenhänge des Codes (oder auch des Headers) durch eine Leerzeile ab (quasi ein "Absatz").

Do's and Dont's

Lies Scott Meyers' "Effective C++" und "More effective C++".

Initialisiere im Konstruktor immer. Verwende Initialisierung statt Zuweisung im Konstruktor (wann immer es geht). Grund: Performanz.

Rufe im Konstruktor immer den Konstruktor der Basisklasse auf (natürlich in der Initialisierungsliste).

Deklariere immer einen Copy-Konstruktor und einen Zuweisungs-Operator. Falls die Klasse diese nicht braucht, mache sie (Grund). Implementiere den Copy-Konstruktor / die Konvertierungskonstruktoren immer so:

A::A( const A/B &source ) { *this = source; }

Konstruktoren mit nur einem Parameter (heißen Conversion-Constructor) müssen gemacht werden (Grund).
Außer dem Copy-Constructor! (Grund: sonst geht return-by-value und pass-by-value nicht mehr; der Compiler darf zwar die Kopie weg-optimieren, tut das i.A. auch, aber der Standard schreibt es so vor -- vermutlich, damit das Programm auch auf Compilern übersetzbar ist,die diese Optimierung nicht beherrschen.I)
Dasselbe gilt für Konstruktoren, bei denen alle Parameter bis auf einen Default-Argumente haben.

Verwende bzw. einfache Funktionen statt .

Bevorzuge anstelle von (bzgl. Performanz siehe alloca).

Keine Instanz- oder Klassenvariablen. (Außer, sie sind )

Falls die Klasse A nur per Pointer oder Referenz im Header-File der Klasse B benutzt wird, dann verwende eine Vorwärts-Deklaration; includiere nicht den Header-File der Klasse B. Beispiel:

class A; class B { private: class A *a; }

Verändere nicht die "Bedeutung" eines Operators und beachte immer auch das semantische Gegenstück. (Beispiele)

Implementiere binäre Operatoren global (nicht als Methoden). Evtl. müssen sie sein. (Grund: siehe "Effective C++", Kapitel 19).

Reference-Parameter werden immer mit deklariert, ansonsten als Pointer (d.h., sie können verändert werden) (Grund).

Benutze Namespaces.
Schreibe niemals blub in einem Header-File!

Wenn Methoden überladen werden, müssen sie sein. (ARVIKA 15.1 -- weiß jemand wieso?)
Virtuelle Methoden müssen auch in Unterklassen als deklariert werden (wenn sie überladen werden) (Grund).

Überschreibe niemals einen geerbten Default-Parameter.

Methoden dürfen in einer Unterklasse nicht gemacht werden.

Vermeide Casts. Wenn doch, dann verwende die neuen C++-Casts (Grund und Beispiele).

Vorsicht bei der Definition von Cast-Operatoren! Auch hier können unbemerkt ungewollte Dinge geschehen. (Beispiel)

Mache Downcasts nur mit (Grund).

Mache kein "händisches" Inlining. (Compiler-Optionen, Konstruktoren)

Header-Files mit Template-Klassen sollen keinen weiteren Code enthalten. Bei Templates steht der "Code" in einem extra File mit Suffix .

Verwende die C++-Features RTTI ( und ) und Exceptions.

Vermeide geschachtelte Klassen.

Vermeide Mehrfachvererbung.

Eine Variable, die innerhalb eines -Konstruktes deklariert wird, gilt nur innerhalb der Schleife:

for (int i=0; i<10; i ++ ) { // i ist gültig } // i ist nicht mehr gültig Gib beim Compilieren auf SGI die Option an.
Gib beim Compilieren mit Intel's Compiler die Option an.

Schalte die Warnings des Compilers an und schreibe Warning-freien Code.

Keine temporären Objekte in Funktionsaufrufen (Grund).

Instanzvariablen, für die es keine -Methode gibt, dürfen gemacht werden.

Tue keine "richtige" Arbeit in Konstruktoren oder verwende Exceptions (Grund).

Dokumentiere Null-Statements im Source (Grund).

Schreibe -korrekten Code. (Das macht am Anfang Mühe ) Achte von Anfang an auf const-correctness! (Im Nachhinein ist es praktisch unmöglich.) Vergiss auch das auf der "rechten" Seite von Funktionen nicht.

Verwende das -Makro) freizügig. (Setze unbedingt im Release!)

Verwende nicht direkt, sondern den -Wrapper aus .

Verwende wirklich , wenn Du den negativen Wertebereich n bei gcc).

icht brauchst. Das ist meistens in Schleifen der Fall. Überlege Dir das auch bei jedem Prototypen, der bekommt.
Schalte die entsprechende Warning an ( bei gcc).

Verlasse Dich niemals auf die Reihenfolge, in der globale oder Variablen initialisiert werden!
(Der Standard definiert diese Reihenfolge zwar eindeutig für eine Translation-Unit, aber wenn man braucht später im Code nur die Reihenfolge zweier Definitionen ändern, und schon knallt es!)

Eine Funktion, eine Aufgabe!
Böse Beispiele: und z.B. .
(Grund: übersichtlicher, leichter exception-safe zu machen.)

Code-Beispiel

Für Eilige ist hier ein Beispiel mit annotiertem Code, der einige der Regeln dieser Guidelines enthält.

Compiler-Optionen

Folgende Optionen sollen immer gesetzt sein:

Für g++ :

Für SGI :

Für Intel unter Windoofs :

Teamarbeit

Wir wollen möglichst viel Code-Reuse machen.
Darum sollten alle folgendes tun:
Frage!
Bevor Du anfängst zu programmieren, frage auf der Mailing-List, ob es nicht schon dieses oder ein ähnliches Stück Code im System gibt.

Siehe auch how to steal code.

Erzähle!
Wenn Du etwas implementiert hast, eine neue Funktion, eine neues Feature, eine neue Aktion/Event, etc., schreibe eine kurze Mail an die Mailing-List.
So können andere vielleicht einmal davon profitieren und Deinen Code / Dein Feature benutzen.

Teil 2 - für Noch-Nicht-Experten

Intro

Diese Guidelines und Tips sind für alle diejenigen gedacht, die relativ neu in der Abteilung sind, und die noch keine jahrzehntelange Programmiererfahrung in C/C++ haben (also z.B. HiWis und Diplomanden).

Diese Guidelines sollen Euch helfen, einen guten Programmierstil zu entwickeln. Außerdem habe ich versucht, ein paar grundlegende Tips zum Programmieren zusammenstellen, die Euch (hoffentlich) helfen, das Programm schneller fertig zu bekommen, weniger Bugs zu produzieren, und Bugs schneller zu finden.

Nun höre ich einige von Euch stöhnen: "Jetzt darf ich noch nicht mal so programmieren wie ich will!", und: "Muß ich mir das alles wirklich durchlesen?". Das habe ich auch gedacht, als ich Guidelines zum ersten Mal in die Hand gedrückt bekam. Aus meiner langjährigen Programmier-Erfahrung kann ich Euch aber versichern: ja, es muß sein, wenn man in einem Team arbeitet. Und selbst wenn man nicht in einem Team arbeitet, sind einige Grundregeln und ein guter Programmierstil sinnvoll, weil sie Euch helfen. Auf jeden Fall macht man sich mit einem schlechten Stil bei den Kollegen unbeliebt!

Insgesamt habe ich versucht, in dem Guideline-Teil so wenig Vorschriften und Einschränkungen wie möglich zu machen, und nur so viel wie nötig: es ist klar, daß es mehrere gute Programmierstile gibt1), und jeder soll und muß seinen eigenen Stil entwickeln. Es ist aber auch klar, daß es viel mehr schlechte als gute Stile gibt ...

 
1) Mathematisch gesagt: auf der Menge der Programmierstile gibt es nur eine Halbordnung, keine totale
 

Generell gilt: Diese Guidelines dürfen gebrochen werden, wenn (und nur wenn) der Source-Code dadurch besser lesbar, oder robuster, oder besser zu warten wird.

Übrigens ist es selbstverständlich, daß man nur durch's Durchlesen dieser Guidelines nicht sofort den perfekten Code schreibt. Es ist noch kein Meister vom Himmel gefallen. Wie bei Man-Pages muß man auch in Guidelines immer wieder mal reinschauen. Auch ich arbeite immer noch an meinem Stil .

Diese Guidelines können nur die gröbsten Tips geben; die Feinheiten sind zu viele und zu individuell, als daß man sie in Guidelines auflisten könnte. Besser ist es, wenn in Eurem Kopf einfach ständig eine Art "Style-Daemon" läuft, während Ihr programmiert, und der ständig seine Datenbank erweitert .

In der zweiten Hälfte enthalten diese Guidelines einige Tips und Tricks zu Unix, C, und sonstigem, was man als Programmierer im täglichen Leben braucht oder gebrauchen kann.

Good points, bad points

Dieser Abschnitt ist aus "C++ Coding Standard", aber weil er recht gut zusammenfaßt, wozu Guidelines gut sind, möchte ich ihn hier einfach kopieren:

Good Points

When a project tries to adhere to common standards a few good things happen:
  • programmers can go into any code and figure out what's going on
  • new people can get up to speed quickly
  • people new to C++ are spared the need to develop a personal style and defend it to the death
  • people new to C++ are spared making the same mistakes over and over again
  • people make fewer mistakes in consistent environments
  • programmers have a common enemy :-)

Bad Points

Now the bad:
  • the standard is usually stupid because it was made by someone who doesn't understand C++
  • the standard is usually stupid because it's not what I do
  • standards reduce creativity
  • standards are unnecessary as long as people are consistent
  • standards enforce too much structure
  • people ignore standards anyway

Programmier-Stil

Dazu gehören syntaktische als auch "semantische" Gepflogenheiten. Ich behaupte, daß es ohne einen guten Programmierstil nicht möglich ist, guten Code zu schreiben (im Sinne von Robustheit, Wartbarkeit, Effizienz, Eleganz). Ich behaupte auch, daß man nur mit einem guten Programmierstil langfristig effizient programmieren kann! (Denn: schlechter Stil -> mehr Bugs oder schlechtes Design -> längere Bugsuche bzw. mehr Redesigns -> mehr Zeitaufwand in der Summe.)

Die n Gebote für Programmierer

Rahmt Euch die ein und hängt sie neben den Badezimmerspiegel!
Ich will niemals hören: "Ich weiß, daß man X noch machen müßte, aber das mache ich später, wenn alles läuft"
Glaube mir: Du wirst es später nicht machen.
Nur eleganter Code ist guter Code.
Kommentiere! (Es steht zwar im Prinzip im Code, aber keiner hat Lust auf Reverse-Engineering!)
Wenn Du die Regeln verletzen mußt, kommentiere warum (und nicht daß Du sie verletzt hast).
Wähle die Namen Deiner Funktionen, Variablen und Methoden sorgfältig!
Verwende bezeichnende Namen ("labeling names") und eine einheitliche Namenskonvention.
Achte auf übersichtliches Indenting und Spacing!
Frage Dich beim Schreiben immer "was ist wenn ..."! (vollständige Fallunterscheidung)
Schreibe nie Code mit Nebeneffekten!
Wenn es doch sein muß, kommentiere diese ausführlich und unübersehbar!
Lerne Deine Tools vollständig zu beherrschen.

The Python Way

Hier ist noch eine kleine Liste von Programmierregeln, die ich aus dem Netz aufgeschnappt habe. Ich lasse sie in Englisch.
  1. Beautiful is better than ugly.
  2. Explicit is better than implicit.
  3. Simple is better than complex.
    (Aus Big Ball of Mud: "A complex architecture may be an accurate reflection of our immature understanding of a complex system or problem.")
  4. Complex is better than complicated.
  5. Flat is better than nested.
  6. Sparse is better than dense.
  7. Readability counts.
  8. Special cases aren't special enough to break the rules.
  9. Although practicality beats purity.
  10. Errors should never pass silently.
  11. Unless explicitly silenced.
  12. In the face of ambiguity, refuse the temptation to guess.
  13. There should be one -- and preferably only one -- obvious way to do it.
  14. Now is better than never.
  15. Although never is often better than right now.
  16. If the implementation is hard to explain, it's a bad idea.
  17. If the implementation is easy to explain, it may be a good idea.
  18. Namespaces are one honking great idea --- let's do more of those!

Namenskonventionen

Intro

Es kommt einigen von Euch vielleicht lächerlich oder nervig vor, daß wir auf Namen so großen Wert legena). Tatsächlich ist aber eine gute Benennung von Variablen, Funktionen, Methoden und Klassen das allerwichtigste Kriterium für einen guten Programmierer (und ein gutes Design)! Besonders beim objekt-orientierten Programmieren ist das fast noch wichtiger als bei reinem C. Eine schlechte Benennung kann eine Library fast unbrauchbar machen.
 
a) Und dabei sagte doch Goethe: "Name ist Schall und Rauch". (Marthens Garten)
 

Überlege Dir bei der Wahl eines Namens für eine Klasse, ein Objekt, eine Variable, oder einen Typ, was ein anderer Programmierer aus dem Namen erkennen kann, wenn er Deinen Code zum ersten Mal sieht und nichts darüber weiß. Er sollte am besten die Bedeutung aus dem Namen ersehen können. Längere Namen sind meistens besser zum Verstehen als kurze (zu lange sind für die Anwender Deines Codes natürlich auch lästig ). Zum Beispiel ist viel besser verständlich als .

Ein sehr gutes Kriterium dafür, daß ein objekt-orientiertes Design Fehler hat, sind Namen: wenn sie zu lang werden, wenn sie keinen Sinn mehr machen von einem globalen Blickpunkt aus, oder wenn alle Funktionen , und heißen, dann ist es höchste Zeit, das Design zu überprüfen! Wenn Klassennamen aus mehr als 3 Wörtern bestehen, dann ist das ein Indiz dafür, daß Du verschiedene Entities Deines System durcheinander bringst.

Meta-Naming (C++)

Die von Stroustrup eingeführten Begriffe member function etc. sind eine Unsitte! Die Dinger heißen "Klassenmethoden" (static member functions) und "Instanzenmethoden" (non-static member functions). Analog für Variablen.

C und C++

Funktionen/Methoden tun meistens etwas, deswegen sollten ihre Namen zusammengesetzt sein aus verb + Substantiv (inCaps-Notation). Hier ein Beispiel für unsere Konvention: . (Andere Konventionen wären: , oder . Ich persönlich finde die Underscore-Schreibweise nicht so schön.)

Wenn eine Funktion eine Eigenschaft zurückliefert, dann soll man den Namen besser aus "is" oder "has" + Adjektiv zusammensetzen; z.B. oder .

Manchmal sind Suffixes hilfreich, z.B. , , , , , etc.

Verwende die üblichen Konventionen für "temporäre" Variablennamen, also etc., für Integers (insbes. Schleifenvariablen und Indizes), für String-Variablen, für Characters, etc.

Wenn mehrere Funktionen/Methoden im selben Modul/Klasse ähnliche Parameter mit ähnlicher Bedeutung haben, so sollen diese Parameter auch dieselben (oder wenigstens ähnliche) Namen haben. Das gilt natürlich ganz besonders für überladene Methoden.

Namen, die für conditional compilation verwendet werden, sollen "all caps" sein (z.B. ).

Die Namen von -Typen sollen erkennen lassen, daß es sich um einen solchen handelt. Deswegen sollen diese mit einem enden, z.B.: . Die Namen der "Members" eines Enums werden wie Defines gebildet:

typedef enum // Kommentar zu meinem tollen Enum Typ { XYZ_RESULT_MIN, // ungültiger Wert (zum Parameter-Check) XYZ_RESULT_SENSIBLE, // blub blub XYZ_RESULT_SILLY, // bla bla XYZ_RESULT_STONED, // lall XYZ_RESULT_MAX // ungültiger Wert (zum Parameter-Check) } xyzResultE; Bei Enums innerhalb eines Klassen-Scopes sind Präfixe nicht notwendig. Die Namen der Members sollen erkennen lassen, zu welchem Enum sie gehören.
Siehe das "Enum-Problem in C++".

Bei -, - oder -Typen ist eine Kennzeichnung nicht notwendig, da der Typ aus dem Kontext hervorgeht. Wer will kann trotzdem sich Suffixes analog zum -Konvention überlegen. Möglichkeiten sind z.B.: , oder für -Types; für Pointer. Andere Konventionen sind denkbar; ich finde die Konvention "Cap-Suffix" am schönsten (und am schnellsten zu tippen ).

Weiterhin fände ich es toll, wenn Ihr Euch Konventionen überlegt, die semantische Bedeutung einer/s Variable/Objektes im Namen zu kennzeichnen; also z.B. alle Vektoren mit dem Buchstaben beginnen lassen, alle Matrizen-Namen mit beenden, alle Exception-Objekte mit beginnen lassen, etc.

C

Es gilt dasselbe wie oben für C++; allerdings müssen Funktionen zusätzlich ein Präfix (2-4 Buchstaben) haben, welches für das Modul steht, in dem sie definiert sind: Präfix + Verb + Substantiv. Z.B.: , , oder . Dasselbe gilt für Funktionen, die eine Eigenschaft liefern: Präfix + "Is" + Adjektiv; z.B. .

C++

Man verwendet für Klassen dieselben Namenskonventionen wie für Funktionen, außer daß Klassennamen mit einem Großbuchstaben anfangen. Es ist nicht nötig, Klassennamen mit einem großen C als extra Präfix oder Suffix zu versehen (redundant). Klassenvariablen/-methoden und Instanzvariablen/-methoden werden nach der selben Namenskonventionen gebildet.

Methoden, die verwendet werden um Instanzvariablen zu setzen, sollen mit beginnen. Methoden, die den Wert einer Instanzvariablen liefern, sollen wie die Instanzvariable heißen (oder mit beginnen). Die Variable selbst beginnt dann mit Underscore.

Wenn mehrere Klassen zusammen eine Library ergeben, dann kann es manchmal ganz sinnvoll sein, wenn die Klassennamen wiederum einen Präfix haben (z.B. für Performer). Es ist nicht nötig, die Methoden- oder Variablennamen dieser Klassen mit Präfix zu schreiben. Die Files einer Klasse werden wie die Klasse selbst genannt (z.B. steht in File die Klasse ).

Alle Methoden fangen mit einem Kleinbuchstaben an. Wenn es keine zu große Umstellung für Dich ist, dann verwende die inCaps Notation (also oder ).

Vermeide Redundanzen beim Naming. In folgender Zeile:

myWindow->setWindowVisibility( libWindow::WINDOW_VISIBLE); mußte man 4× "window" tippen und 2× "visible". Genauso gut und verständlich ist: myWindow->setVisibility( true ); (Aufgrund des Namens der Methode weiß jeder, daß man ihr nur Boole'sche Werte übergeben kann, deswegen ist als Parameter ok hier.)
Das Enum-Problem in C++
In C war es einfach, Enum's dort zu verwenden, wo mehrere "Optionen" verodert als Parameter übergeben werden sollten. Z.B.: typedef enum // renderer options { renWithWindow, // create window renStereo, // stereo window renWindowDecorations // windows has decorations } renOptionsE; void renderInit( renOptionsE options ); was man dann so aufrufen konnte: renderInit( renWithWindow | renStereo ); In C++ geht das so nicht mehr. Ich schlage daher folgenden "Umweg" vor (Alex' Idee): typedef enum { ... }; typedef int myEnumE; void foo( myEnumE options ); Leider kann man den üblichen automatischen Dokumentationsextraktionstools nicht beibringen, daß sie den unbenannten dokumentieren sollen aber unter dem anderen Namen!
(Das einzige Tools, das ich kenne, und das es schaffen könnte, ist Perceps.)
Auch alle anderen mir bekannten Varianten zur Lösung des Enum-Problems können nicht "transparent" von den Dokumentations-Tools verarbeitet werden.

Kommentare

Generell gilt: So wenig Kommentar wie möglich, so viel Kommentar wie nötig.

Einerseits helfen Kommentare, Eure Gedanken besser zu ordnen (und damit sauberer zu programmieren); andererseits hilft es Euch, wenn Ihr in einem Jahr etwas an dem Code ändern müßt (oder gar andere) --- sagt nicht, daß Ihr Euch das merken könnt, oder daß alles selbsterklärend ist! .

Es gibt vier Arten von Kommentaren:

  1. Am Anfang einer Klasse ein Überblick über das "große Bild", die Funktionen des Files (der Klasse), verwendete "Compile-Flags" (conditional compilation per ).
  2. Vor jeder Funktion eine Beschreibung für diese, Ein-/ Ausgabe-Parameter, pre- und post-conditions, Seiteneffekte, Caveats, Bugs, etc. (Zu pre- und post-conditions siehe auch das -Makro).
  3. Im Code selbst für größere Blöcke von Zeilen.
  4. Für Variablen (alle globalen bzw. Klassenvariablen und teilweise lokale), Members von s und ähnlichem.
Kommentare sollten in einer einheitlichen Form im ganzen Modul gemacht werden (siehe das Kommentar-Template). Kommentare für Funktionen sollen mit dem im Template gezeigten Block gemacht werden, damit sie durch ein automatisches Tool extrahiert werden können. Ihr könnt Kommentare in Englisch oder in Deutsch machen, je nach dem, wo Ihr Euch wohler fühlt.

Der Kommentar muß auf jeden Fall klar machen, welche Bedeutung die Parameter haben, welche Klassenvariablen (oder Variablen) verwendet werden (möglichst wenige), was zurückgeliefert wird, welche Bedingungen eingehalten werden müssen durch den Caller. Selbstverständlich gehört eine Beschreibung der Funktion dazu.

Wenn einige Parameter Rückgabe-Parameter sind, so muß das eindeutig gemacht werden! (Im Bsp. .)

Hier ein Beispiel für den Kommentar einer Funktion:

/** Do something * * @param param1 blubber (in) * @param param2 bla (out) * * @return * -1 falls fehlgeschlagen, 0 wenn alles ok. * * Diese Funktion berechnet ... * Kann jederzeit aufgerufen werden. * * @throw Exception * XCoffee, falls kein Kaffee mehr da. * * @warning * Erwartet dass die Funktion init() schon aufgerufen wurde. * * @pre * Param1 wurde von der Funktion blub() berechnet. * * @sideeffects * @arg The global variable @c M_Interest * Nebenwirkungen, globale Variablen, die veraendert werden, .. * * @todo * Schneller machen. * * @bug * Produziert einen core dump, wenn @a param1 = 0.0 ist. * * @internal * Basiert auf dem Algorithmus von ... * * @see * eineAndereFunktion() * **/ Nach meiner Erfahrung geht es am schnellsten, wenn man den Kommentar zu einer Funktion dann schreibt, wenn sie "halb" fertig ist, weil dann noch alles frisch ist. Wenn sie vollends fertig ist, sollte man noch einmal drüber sehen, ob der Kommentar noch korrekt ist.

Manchmal hilft es auch, wenn man den Kommentar teilweise schreibt bevor man mit Codieren anfängt! Z.B. ein paar Zeilen, was genau die Funktion tun soll, und einige Parameter auflisten kann schon viel zur Ordnung der eigenen Gedanken helfen.

Wer erst ein ganzes Modul ohne Kommentar schreibt --- "den Kommentar schreib' ich am Ende wenn alles läuft" ---, der schreibt mit ziemlicher Sicherheit überhaupt keinen Kommentar mehr. (Weil es einfach zu viel auf einmal ist, und weil die Feinheiten wie z.B. Caveats nicht mehr im Gedächtnis sind.)

Wichtig ist, daß man durch Überfliegen der Kommentar-Zeilen im Funktions-Body einen Überblick über die Funktion und wie sie "funktioniert" gewinnt. Der in-line Kommentar sollte nur beschreiben was im entsprechenden Code-Block passiert, nicht wie es passiert (Bsp.: "berechne Mittelwert" ist besser als "summiere und teile durch n").

Wenn es wichtige Bedingungen gibt, die eingehalten werden müssen, oder Schleifeninvarianten, so ist es sinnvoll, diese in einem Kommentar zu vermerken, damit diese nicht aus Versehen später verletzt werden, wenn man (evtl. jemand anders!) den Code modifiziert. Ein Beispiel steht oben, ein weiteres ist:

// do the following *after* ... !

Hier ist ein Beispiel eines schlechten Kommentars:

a = malloc( 100 * sizeof(int) ); // gimme more memory x = glob( ... ); // do file completion // now sort the elements qsort( e, n, sizeof(elemT), compfunc ); Kommentiere nicht neu entdeckte Library-Funktion: jeder kann in den Man-Pages selbst nachsehen.

Kommentare von Variablen und Typen könnten ungefähr so formatiert werden:

#define MAX_REC_DEPTH 1000 // max depth of a boxtree static int RecursionDepth = 0; // used in bxtConstructGraph typedef struct // Kommentar für den struct allg. { vmmPointP x, y; // Kommentar der einzelnen Members int a, // Kommentar .... b; // .. der einzelnen Members } MyStructS;

Strukturierung und Layout der Files

Ein Template für C bzw. C++ Files findet man im CVS in . (Ersetze durch den Namen der Klasse, oder, besser noch, laß das den Editor beim ersten Erzeugen des Files machen.)

Die -Files enthalten keine CVS-Keywords außer einem Id-Keyword. Dieses ist für die Produktversion vorgesehen und wird nur für diese Version expandiert (zur Identifizierung der einzelnen Versionen, aus denen das System zusammen gesetzt ist).

Das bedeutet, daß alle in ihr folgende Zeilen eintragen müssen:

status -v update -P -ko add -ko checkout -P -ko diff -ko -b -B -d cvs -z 9 edit -a none tag -c Damit werden unnötige "diffs" vermieden, die nur aufgrund verschiedener Expandierungen der CVS-Keywords entstehen. (Für die Produktversion muß dann mit der Option aufgerufen werden.)
(Abgesehen davon sollten alle diesen File in ihrem Home stehen haben.)

Verwende 4-er Einrückungen!
Source-Zeilen sollten möglichst nicht länger als 80 Zeichen sein. (Es gibt Ausnahmen.)

Pro Zeile soll nur ein Statement stehen2) (es gibt berechtigte Ausnahmen).

 
2) Eine psychologische Untersuchung hat gezeigt, daß Programmierer in Zeilen denken, d.h., daß die kleinsten Einheiten, mit denen Programmierer Code erfassen und verstehen, einzelne Zeilen sind.
 

Jeder C-File included und (falls das nicht schon in einem "globalen" gemacht wird).

Achte auf eine "schöne" Formatierung der Funktionsprototypen, des Deklarationsblockes von lokalen Variablen, etc. Deine Kollegen werden die Nase rümpfen, wenn es "saumäßig" aussieht.

Hier findet man ein graphisches, annotiertes Beispiel, wie Source-Code aussehen soll.

Anordnung innerhalb einer Klasse

Klassen haben mehrere Abschnitte, analog zu C-Files, die folgendermaßen geordnet sein sollen:
  1. Konstanten, Typen
  2. Variablen (falls zugelassen)
  3. Methoden
  4. Zeugs (selbe Reihenfolge)
  5. "parts" (selbe Reihenfolge)

Header-Files

Der Aufbau von Header-Files ist ähnlich wie der von C-Files.

Der "Inhalt" von Header-Files muß mit gegen Mehrfach-Including geschützt werden, wie im schon gemacht. (Die -Zeile erledigt dasselbe wie die -Klammer und ist effizienter beim Compilieren, ist aber nicht auf allen Plattformen verfügbar.)

Der Name eines Header-Files ist gleich wie der dazugehörige C-File (also zu ). Man sollte Namen vermeiden, die schon in für Standard-Header-Files vergeben sind, z.B. oder ).

Reines C
In einen Header-File gehört nur das, was ein Anwender der Lib oder des Object-Files wirklich wissen muß --- alles andere gehört in die entsprechenden C-Files oder in "interne" Header-Files, die nicht im "Anwender-Header-File" included werden. (Das kann bei großen Projekten nicht-trivial werden! ) Zu normalen Applikationen gibt es i.A. keine extra Header-Files!
Bei C++ geht das nicht so einfach/elegant, da man leider auch die -Methoden im Header-File deklarieren muß.

Mache C-Header-Files kompatibel mit C und C++. Das bedeutet, daß C-Header-Files in durch ein geklammert werden müssen:

#ifdef __cplusplus extern "C" { #endif .... #ifdef __cplusplus } #endif So können sie sowohl in C-Files als auch C++-Files included werden.

Einrückung und Spaces

Wir verwenden folgende Konvention: for ( ... ) { if ( .. ) { .. } else { ... } } So können die schließenden Klammern am leichtesten zugeordnet werden.

Der K&R-Style ist verboten, da schlecht lesbar (Ziel dieses Styles ist, den Code so "dicht" wie möglich zu machen):

for ( ... ) { if ( .. ) { .. } else { ... } } else { ...

Es ist kein Muß, aber es ist schöner, wenn Variablen und Kommentare so tabuliert werden, daß sie in der selben Spalte anfangen:

int x, y; // dominant coord planes of polygon int xturns, yturns; // # turns of xslope, yslope objPolyhedronP p1, p2; int i; ist viel schöner als int x, y; // dominant coord planes of polygon int xturns, yturns; // # turns of xslope, yslope objPolyhedronP p1, p2; int i; Wenigstens die Kommentare sollten gleich ausgerichtet sein (es sei denn, sie passen sonst nicht in die Zeile).

Spaces innerhalb einer Zeile sind genauso wichtig:

for(i=obj->begin();i<obj->l()&&obj->M(i)!=-1;i++){ obj->M(i)=-1; } istvielschlechterzulesenals for( i = obj->begin(); i < obj->l() && obj->f(i) != -1; i++ ) { obj->f(i) = -1; }

Wartbarkeit

Maintainance besteht i.A. aus leichtem Modifizieren des Sources. Diejenigen, die diese Maintainance machen, sind fast nie diejenigen, die den Source ursprünglich erstellt haben (aus verschiedenen Gründen). Selbst wenn derjenige, der den Code modifiziert, der ursprüngliche Autor ist: wenn man sich ein Jahr lang nicht mehr ständig mit diesem Code beschäftigt hat, dann sind die Details und manchmal auch das "große Bild" vergessen!

Das gilt sowohl für Modifikationen des Codes durch den Autor selbst wenige Monate nachdem der Code entstanden ist (best case), als auch für Modifikationen 1-2 Jahre später durch jemand, der keine Ahnung vom "großen Bild" hat (worst case).

Man kann nicht viele konkrete Regeln aufstellen, die Code gut wartbar machen --- man muß sich durch Erfahrung ein Gefühl dafür schaffen, welche Konstrukte im Code später schlecht wartbar sind. Man kann aber doch folgende allgemeine Tips beherzigen:

  1. Falls eine Funktion ausschließlich über das Public-Interface einer Klasse implementiert werden kann, dann soll diese Funktion keine Member-Funktion sein! Das erhöht die Kapselung. (Siehe [12])
  2. Mit Kommentaren kann man für andere Hinweise geben.
    • Im Kommentar vor der Funktion: "Assumptions" und "Caution". Diese Hinweise werden während des Schreibens der Funktion/Methode ausgefüllt! Schon nach 1 Woche weiß sonst auch der Autor nicht mehr, was es alles zu beachten gibt, wenn man die Funktion aufrufen oder gar verändern will!
    • In-line-Kommentar im Body der Funktion für Bedingungen, z.B.: /* now nelems = 2 */:
      oder /* MUST be done before ... because ... */
      Dann weiß man auch noch in einem Jahr, wenn man die Funktion verändern muß, daß diese Bedingung im Rest der Funktion gültig sein muß. Außerdem dient es zur eigenen Kontrolle, daß man sich tatsächlich überlegt hat, daß diese Bedingung existiert für den Rest der Funktion! (Wer erinnert sich noch an wp- oder Hoare-Kalkül?)
    • Falls ein bestimmter Code-Abschnitt sehr sensibel gegenüber Änderungen ist, kommentiere das.
      Falls Änderungen in einem Stück Code Änderungen in einem anderen Stück Code notwendig macht, kommentiere das (in ersterem!).
  3. Faktorisieren: Wenn man zwei Funktionen hat foo( a, b ) { .... } bar( x, y, z ) { ,,, // zusaetzlicher code ... // selber code wie in foo ,,, // zusaetzlicher code } dann muß man umformen in: bar( a, b, c ) { ... foo( a, b ) ... } Auch, wenn man eine solche Möglichkeit zur Faktorisierung erst später entdeckt!

    Kandidaten, bei denen fast immer Faktorisierung angewendet werden muß, sind mehrere Konstruktoren einer Klasse, Increment-/Decrement-Operatoren, zwischen dem Copy-Konstruktor und dem Zuweisungsoperator, der Operator und , etc.

    Noch ein Beispiel: Ganz schlecht ist

    if ( ... ) { blabla ... } else { gleicher blabla wie oben anderer code ... }

    Das läßt sich ganz schlecht überblicken. Und wenn man in einem Jahr mal den Code ändern muß, passiert es sehr leicht, daß man einen der beiden Zweige vergißt! Und dann sucht man stundenlang nach einem Bug, falls er überhaupt gleich auftaucht und nicht erst 2 Monate später, wenn man schon längst vergessen hat, daß man da überhaupt was geändert hat ...

  4. Vermeide Code, den man später "mißverstehen" kann; z.B. Zuweisungen in Bedingungen: if ( a = b ) wird garantiert später von jemand, der einen Bug in dieser Funktion sucht, "repariert" zu if ( a == b )

    Oder: Ist in dem Code

    for ( c = s; c < ...; ... ) c = f(...); tatsächlich eine -Schleife ohne Body gemeint (dann wurde das Semikolon vergessen), oder ist es nur schlecht eingerückt? Schreibt man dagegen for ( c = s; c < ...; ... ) {}; c = f(...) oder (je nachdem was gemeint war) for ( c = s; c < ...; ... ) c = f(...); dann ist es eindeutig.

Leserlichkeit

Oberstes Ziel in diesem Zusammenhang ist die leichte Lesbarkeit des Source-Codes für Andere! (Insbesondere für mich.) Wer nach dem Motto programmiert, "es war für mich hart zu programmieren, dann soll es für die anderen wenigstens schwer zu lesen sein", der verhält sich einfach nur unkollegial.

Zu guter Lesbarkeit gehört auch eine gute Strukturierung des ganzen Files (siehe Strukturierung und Layout), sinnvolle Modularisierung, Strukturierung der einzelnen Zeilen, als auch Kommentare (siehe Kommentare)

Gute und schlechte Programmier-Praxis

Oder: "It's not a feature - it's a bug."

Alle hier aufgeführten Bugs sind tatsächlich vorgekommen! Die meisten haben etliche Stunden gekostet, um sie zu finden.

Fazit: wenn Du nach einem Programmier-Abschnitt nochmal 2 Minuten darüber nachdenkst, ob Du wirklich alle Fälle bedacht hast, dann kannst Du später locker einige Stunden frustrierende Bug-Suche sparen! "Was passiert, wenn jener Zeiger NULL ist?", "Was passiert, wenn der String doch länger als 100 Zeichen ist, weil z.B. ein Pfad-Name ziemlich lang ist?", "Was passiert, wenn diese Anzahl von ... 0 ist?", "Was passiert, wenn das graphische Objekt sich verändert? wenn es sich bewegt? wenn es seine Form ändert? oder seine Farbe?", "Was passiert, wenn das Objekt nicht direkt unter der Wurzel des Szenengraphen hängt?".

Ich kenne sogar Fälle, wo extrem schlechter Code (Bugs, schlechte Modularisierung, miserable Modifizierbarkeit, etc.) hinterher 3 Leute jeweils(!) 1 Mann-Woche (verteilt auf 1 Jahr) gekostet hat, um ihn zu warten, anzupassen, und debuggen. Der Code wurde (leider) in nur 1 Woche gehackt/zusammenkopiert --- hätte man noch 1 Woche investiert, ihn sauber zu schreiben und zu testen, hätte man in der Summe 2 Mann-Wochen gespart.

C++

Vererbung

Bevor Du eine Klasse B als Unterklasse von A deklarierst, frage Dich, ob zwischen den beiden Klassen wirklich die Beziehung "B ist ein A" besteht, oder ob nicht eher die Beziehung
  • "B benutzt A" oder "A ist Teil von B" besteht.
    In diesem Fall enthält einfach die eine Klasse einen Pointer auf eine Instanz der anderen. Es gibt keine Vererbung zwischen beiden Klassen.
  • "B ist wie ein A" besteht.
    In diesem Fall sind beide Klassen Unterklasse einer gemeinsamen Oberklasse. Das kann bedeuten, daß man erst einmal eine neue Oberklasse anlegen muß und eine Menge Code von Klasse A "nach oben" schaffen muß.
Insbesondere wird oft fälschlicherweise Mehrfach-Vererbung verwendet, wenn ein Objekt eigentlich Pointer auf mehrere andere Objekte enthält (mehrfache "benutzt"-Beziehung)!

Konstruktoren und Destruktoren

Schreibe immer einen virtual Destruktor, auch wenn die Klasse keinen braucht! (Problem: auf Zeiger auf Basisklasse.) Die einzige Ausnahme sind sehr kleine Klassen (speichermäßig), und wenn der Extra-Speicher für die nicht akzeptabel ist. In solch einem Fall muß das unbedingt im Klassenkommentar vermerkt werden!

Wenn eine Klasse keinen Konstruktor braucht/hat, deklariere einen Default-Konstruktor als ohne Implementierung im C-File (Mit Kommentar not implemented). Das verhindert, daß der Compiler einen erzeugt, der evtl. falsch ist. Deklariere immer einen Copy-Konstruktor und einen Zuweisungsoperator. Wenn die Klasse diese nicht braucht, mache sie ohne Implementierung.
Das Problem bei C++ ist nämlich, daß man dem Code nicht ansieht, wann der Copy-Konstruktor und der Zuweisungsoperator aufgerufen werden! Siehe dieses Beispiel.

Konstruktoren können keinen Error-Code liefern. Dazu gibt es zwei Lösungen:

  1. In Konstruktoren darf nur Code stehen, der garantiert nicht fehlschlagen kann.
    Verwende statt dessen immer eine -Funktion, um die "wirkliche" Initialisierung eines Objektes zu erledigen (die auch mißlingen kann): Class *o = new Class(); if ( o->init() < 0 ) { error ... } Problem: es können trotzdem Excpetions entstehen (z.B. kann fehlschlagen).
  2. Verwende Exceptions.
Initialisiere immer alle Instanzvariablen im Konstruktor. Verwende keine globalen Variablen im Konstruktor.

Rufe keine virtuellen Methoden in Konstruktoren auf.

Verwende, wenn es geht, Initialisierung anstatt Zuweisung. Bei der Verwendung von Zuweisungen im Konstruktor werden evtl. viele temporäre Instanzen erzeugt - was eine schlechte Performance ergibt. Basistypen (, , etc.) können im Konstruktor per Zuweisung initialisiert werden.
Hier ein teures Beispiel mit Zuweisung:

class String { public: String(void); // make 0-length string String( const char *s); // copy constructor String& operator=( const String &s ); private: ... } class Name { public: Name( const char *t ) { s = t; } private: String s; } void main( void ) { // how expensive is the following ?? Name neighbor = "Joe"; } Folgendes passiert:
  1. wird aufgerufen mit Parameter "Joe"
  2. wird durch den Default-Konstruktor erzeugt. Das erzeugt einen 1-Byte großen Speicherblock für das Zeichen '\0'.
  3. Ein temporärer String "Joe" wird erzeugt als Kopie des Parameters mit Hilfe des Copy-Konstruktors (noch ein ).
  4. Die Zuweisung wird durchgeführt (mittels des -Operators).
    Dazu wird der alte String in d, ein neuer erzeugt mit und dann ein gemacht.
  5. Der temporäre String wird gelöscht ().
Insgesamt: 3 s, 2 s und 2 s.

Und hier die bessere Alternative mit Initialisierung im Konstruktor. Einziger Unterschied zu obigem Code-Beispiel ist der Konstruktor von :

Name::Name( const char *t ) : s(t) {}
  1. wird aufgerufen mit Parameter "Joe"
  2. wird initialisiert von mittels
  3. macht ein und einen .
Insgesamt: 1 , 1 . (keine temporären Objekte, !)

Wenn man Konstruktoren mit genau einem Parameter (conversion constructors) nicht macht, dann kann es passieren, daß diese an Stellen verwendet werden, wo man es nicht "sieht", z.B.:

class A { A(float); } void foo(A a) { .. } foo( 1.0 ); // hier wandelt der Compiler 1.0 automatisch in ein A um! Manchmal kann das erwünscht sein, aber i.A. ist es schwer, in solchem Code Performance-Probleme zu finden (was besonders bei Computer-Graphik wichtig ist).

Casts

Verwende die neuen Casts:
  • um ein wegzucasten (oder hinzucasten);
  • statt dem old-style C-Cast ; Grund: solch ein Cast läßt sich leichter mit (und durch "Draufsehen") finden; außerdem wird die Constness nicht verändert.
  • kann dazu verwendet werden, einen Zeiger auf ein Objekt der Basisklasse in einen Zeiger auf ein Objekt der Unterklasse zu verwandeln. Tatsächlich ist der Ausdruck dynamic_cast<type>( expression ) eine Funktion, die liefert, falls expression nicht vom Typ type ist, sonst aber einen Zeiger des gewünschten Typs liefert.
    Dieser Cast funktioniert natürlich nur, wenn RTTI unterstützt wird, und nur, wenn die Basisklasse eine vtable (d.h., eine oder mehrere virtuelle Metoden) hat!.

Selbstdefinierte Cast-Operatoren können sehr undurchsichtige Effekte haben (wie 1-Parameter-Konstruktoren).
Beispiel:

class A { public: A() { .. }; explicit A( char* ) { .. }; ~A () {}; }; class B { public: B() { .. }; ~B () {}; operator char *() { .. }; }; void foo(void) { B b; A a(b); // geht, da b nach char* gecastet werden kann! } Also: sparsam verwenden.

Exceptions

Achte auf "exception-safety": wenn eine Exception geworfen wird, muß das Objekt immer noch konsistent sein, und keine Resourcen (z.B. Speicher) lecken, und das Programm insgesamt in einem "vernünftigen" Zustand sein, so daß die Ausführung fortgesetzt werden kann.
Das bedeutet, daß der Programmierer bei jede Zeile bedenken muß, daß eine Exception geworfen werden könnte.

Wirf keine Exceptions in Destruktoren!!

Leite alle Exception-Klassen von ab (). Eventuell macht es Sinn, eine der Standard-Unterklassen von zu verwenden oder davon abzuleiten (, , , , , , , , , , ).

Catch by reference (), never catch by value (). (Grund: die Exception, die ankommt, könnte eine Unterklasse sein.) Oder einfach nur .

Mache die -Blöcke groß, wenn es geht.

C-style Callbacks (z.B. Callbacks für C-Libraries) sollen immer als "no-throw" deklariert werden:

void myCallback( ) throw ()

Befolge das Idiom "Resource Allocation is Initialization". Vermeide im Konstruktor, oder schachtele es in (denn der Destruktor wird nicht aufgerufen im Falle einer Exception). Verwende evtl. aus der (sie liefern einen einfachen Mechanismus, wie man Speicher automatisch wieder freigeben kann). Verwende evtl. "strong pointers" (siehe die Official Resource Management Page).

Always perform unmanaged resource acquisition in the constructor body, never in initializer lists. In other words, either use "resource acquisition is initialization" (thereby avoiding unmanaged resources entirely) or else perform the resource acquisition in the constructor body.
For example, say was and was a plain old that was 'd in the initializer-list; then in the handler there would be no way to it. The fix would be to instead either wrap the dynamically allocated memory resource (e.g., change to string) or it in the constructor body where it can be safely cleaned up using a local try-block or otherwise.

Verwende Exceptions nicht, wenn es den Code komplizierter macht. Dann ist vermutlich ein normales Return-Code-Schema besser.

Deklariere keine Exception-Spezifikation (); statt dessen, dokumentiere die möglichen Exceptions im Kommentar zu der Funktion.
Grund:

  • Kurz gesagt: auch Experten tun es nicht.
    Etwas länger gesagt:
  • Manche Compiler erzeugen sehr langsamen Code, wenn sie solche Exception-Spezifikationen sehen;
  • Wenn dann doch eine Exception kommt, die nicht in der Spezifikation steht, wird aufgerufen -- das ist oft nicht das, was man will;
  • Für Methoden in Templates kann man sowieso keine sinnvolle Exception-Spezifikation schreiben, da man nichts über den zukünftigen Basistyp weiß, mit dem das Template instanziiert wird.
Siehe auch Exception-specification rationale der Boost-Library.

Methoden, Funktionen und Operatoren

Wenn eine Methode irgendwo in der Vererbungshierarchie deklariert ist, dann soll sie überall in der ganzen Hierarchie deklariert werden.
Damit ist besser dokumentiert, daß eine Methode überladen werden kann, bzw. daß die entsprechende Methode in der Oberklasse tatsächlich ist. Der Standard sagt zwar " once virtual, always virtual", aber "explizit ist besser als implizit".

Inline-Methoden können sein: Zugriffs- (auf Instanzvariablen) und Forwarding-Methoden (die nichts tun außer eine andere Methode aufrufen). Die -Deklaration ist heutzutage aber kaum noch nötig, da der Compiler bei eingeschalteter Optimierung das von alleine macht (s.a. Optimierungen).
Achtung: folgende Funktionen sollen nie inline sein!

  • Konstruktoren und Destruktoren
  • virtuelle Funktionen
    (macht auch keinen Sinn, da diese Funktionen erst zur Laufzeit gebunden werden.)
  • Funktionen mit variabler Anzahl Parameter (

Eine Methode, die per Design die Instanz nicht verändern soll, soll man mit deklarieren. Das verhindert, daß später aus Versehen doch Code eingefügt wird, der etwas verändert. Außerdem können nur solche Methoden für -Instanzen aufgerufen werden.

Achtung: der default Assignment-Operator macht nur eine "shallow copy"!

Der Assignment-Operator soll zurückgeben. (Grund: dann kann etwas wie nie passieren.)

Verwende Operator-Overloading selten und einheitlich. Ein Operator soll immer dasselbe "bedeuten". (Dasselbe gilt für Funktionen-Overloading.) Jeder Anwender erwartet, daß z.B. der -Operator irgend einen internen Zustand "erhöht", und daß der -Operator irgend eine arithmetische Multiplikation ist.
Implementiere immer auch das semantische Gegenstück eines Operators. Wenn es den Operator gibt, dann erwartet jeder, daß es auch gibt, und wenn es gibt, dann sollte es auch geben (manchmal ist es natürlich nicht möglich, z.B. bei einem Iterator durch eine einfach-verkettete Liste).
Wenn es den Operator gibt, sollte es auch , und geben. Wenn es gibt, sollte es auch , und geben.
Diese "balancierten" Operatoren sind sehr gute Kandidaten für Faktorisierung!

Liefere nie einen Zeiger auf eine Instanzvariable zurück! Wenn es unbedingt sein muß, dann nur als Zeiger oder Reference!

Vermeide call-by-value-Übergabe von Objekten als Argumente für eine Funktion.

Pointer oder Reference?

Das Problem: Man kann Funktionsparametern, die als Reference deklariert sind, nicht ansehen, daß sie eben nicht call-by-value sind5)! Wenn Du gerne References als formale Parameter in Funktionen verwenden möchtest, dann nur als !
Grund: Wenn man die Konvention vereinbart, daß ein Pointer-Parameter verändert werden darf und ein Referenz-Parameter nicht, dann kann man den Referenz-Parameter auch gleich mit deklarieren, da damit auch der Compiler gewissermaßen über diese Konvention "informiert" wird (und damit besser optimieren kann).
 
5) Meiner bescheidenen Meinung nach sind References keine gelungene "Verbesserung" in C++!
 

Noch ein Grund, warum Referenzen immer mit deklariert sein sollten, der auch Pragmatiker überzeugen dürfte: temporäre Objekte sind prinzipiell . Wenn also ein formaler Parameter eine nicht- Referenz ist, dann kann man so etwas nicht schreiben:

foo( A() );

Misc

Instanz- oder Klassenvariablen sollen nie sein. Verwende statt dessen - und -Funktionen. (Sonst wird "data hiding" verletzt.)
(Ausnahme: Variablen. Diese können nur in der Initialisierungsliste der Konstruktoren gesetzt werden.)

Offset Pointer to Members müssen sehr gut begründet werden können! Normalerweise sind sie ein Zeichen dafür, daß im Design etwas nicht stimmt (z.B. falsche Identifizierung, welches die dem Problem am besten angepaßten Objekte sind, oder falsche Verteilung der Funktionalität).

Verwende keine temporären Objekte in Funktionsaufrufen. Es sei denn, Du weißt genau, wann diese wieder gelöscht werden (weißt Du's?).

// Haesslich!! setColor( &(Color(black)) ); // So ist's schoen Color color(black); setColor( &color ); Initialisierung von Instanzen per Zuweisung ist verboten!
Statt A a = A(); // verboten schreibe A a; a = A(); // ok (wenn auch unnötig) wenn es denn sein muß.
Grund: die erste Variante liefert verschiedenes Verhalten mit verschiedenen Compilern, und kann dazu führen, daß der Destruktor einmal mehr aufgerufen wird als der Konstruktor (SGI's Compiler-Bug).

Floating-Point Arithmetik und Round-Off Errors

In den Beispielen hier sind alle Variablen s.

falsch:

ca = Dotprod(v1, v2) / (Len(v1) * Len(v2)); sa = sqrtf( 1 - ca*ca ); richtig: h = vmmLen(v1) * vmmLen(v2); if ( h < epsilon ) /* fallback stuff */ else { ca = Dotprod(v1, v2) / h; if ( ca >= 1.0 ) ca = 1.0; if ( ca <= -1.0 ) ca = -1.0; sa = sqrtf( 1 - ca*ca ); }

total falsch:

if ( x == a ) ... immer noch falsch: if ( x < a ) ... else if ( x > a ) ... else ... besser: if ( x > a-epsilon && x < a+epsilon ) ... richtig: #include <math.h> if ( fabs(x - a) <= epsilon * fabs(a) )

Fest-verdrahtete Pfade

Ganz miserabel: file = fopen("/igd/a4/home/mies/bla", "r"); // hart codierter File-Name! fscanf(file, ...); // kann Core-Dump geben! nur etwas besser: #define BlaFile "/igd/a4/home/mies/bla" file = fopen(BlaFile, "r"); // immer noch hart kodiert! if ( ! file ) { fprintf(stderr, "couldn't open ..."); exit(1); // exit ist immer schlecht! // es soll immer - moeglichst // sinnvoll - weitergehen

ein bißchen besser:

file = fopen( getenv("BLAFILE"), "r" ); // kann schon wieder Core-Dumpen! if ( ! file ) { fprintf(stderr, "couldn't open ..."); ...

am besten:

blafileenv = getenv("BLAFILE"); if ( ! blafileenv ) { fprintf(stderr, "env.var BLAFILE not set - using default %s\n", BLAFILEDEFAULT ); blafileenv = BLAFILEDEFAULT; } file = fopen( blafileenv, "r" ); if ( ! file ) { perror("open"); fprintf(stderr, "couldn't open %s!\n", blafileenv ); ...... // hier moeglichst irgendwelche return; // sinnvollen Default-Werte setzen } fscanf(file, ...);

Bei : Wie schon erwähnt gibt es immer Ausnahmen. Der call ist eine solche --- hier müssen sogar feste Pfade benutzt werden! Das Problem: man macht sich sonst von der Umgebung () des Users abhängig.

Beispiel: man möchte per eine remote shell starten. Falsch ist:

system( "rsh machine ..." ); denn das Kommando ist vielleicht gar nicht im des Users enthalten, und wenn, dann ist vielleicht zuerst die restricted-shell im , und nicht die remote shell!

Deswegen: bei das Kommando immer mit absolutem Pfad angeben (mit am Programmanfang deklarieren!). Am besten testet man vorher mit noch, ob es den Befehl auch wirklich gibt. Also im Beispiel:

#define RSH_PROG "/usr/bsd/rsh" ... err = stat( RSH_PROG, &statbuf ); // auf manchen Unices ist rsh if ( err ) // nicht da wo man sie vermutet! ... err = system( RSH_PROG " machine ..." );

Arrays

Feste Array-Größen
Bedenke: Durch "feste" Arraygrößen sind schon viele Security-Holes in Unix entstanden (das ist die Klasse der "buffer overflow security leaks")! (z.B. mit 100k Argument-String, oder >1000 telnet connections pro sec.)
Array-Indizierung
In C werden Arrays grundsätzlich mit 0 beginnend indiziert! Niemals mit 1 beginnend (obwohl es in Fortran so gemacht wird.) Wer es doch so macht verwirrt alle anderen und produziert direkt oder indirekt garantiert einen off-by-one Bug.

Magic numbers

Numerische Konstanten heißen oft auch "magic numbers". Sie machen den Code mindestens unwartbar und unverständlich, und sorgen auch für den einen oder anderen Bug.

Ganz falsch:

void foo( int bla ) { if ( bla == 1 ) .. else if ( bla == 2 ) .. Problem: Du weißt nie, wer alles aufruft! Was passiert, wenn man die Bedeutung von mal ändern muß?

Besser:

typedef enum { Fall1, Fall2, ... } FooFaelleE; void foo( FooFaelleE bla ) { ... }

Falls man mehrere Fälle "verodern" möchte, dann muß man verwenden (jedenfalls in C++).

Makros

Durch automatisches Inlining moderner Compiler sind Makros meistens überflüssig geworden. Außerdem ist das Debugging von Makros extrem mühsam, das Inlining von Funktionen hingegen kann man ausschalten. Verwende Makros nur, wenn es mit einer Funktion nicht geht.

Bei Makros muß man aufpassen, sowohl wenn man sie verwendet als auch, wenn man sie definiert! Denn: Makros und deren Parameter können Nebeneffekte haben! Generell soll man Makros so schreiben, daß das Prinzip der geringsten Überraschung gilt. Aus diesem Grund haben wir eine Namenskonvention (all-caps) für Makros, die sie als solche deutlich kenntlich macht.

Mehrfache Auswertung von Argumenten: Wenn ein Makro ist, sollte man nie Argumente übergeben, die Nebeneffekte haben, z.B.

foo( i++ ) ist streng verboten!
Wenn das Makro nämlich expandiert wird zu: if ( arg < Max ) x = arg; ?!

Noch viel Schlimmeres kann in so einem Fall passieren, wenn das Argument eine Funktion ist:

foo( bar(x) ) Wie oft wird aufgerufen?! Was ist, wenn das Makro in der rekursiven Funktion selbst vorkommt?!

Und was noch viel schlimmer ist: selbst wenn kein Bug entsteht, so wird das ganze Programm trotzdem seehhr laaangsam, weil viel zu oft aufgerufen wird --- und das kann man praktisch überhaupt nicht mehr herausfinden!!

Variablen in Makros müssen auf jeden Fall so gewählt werden, daß sie nie genau so lauten können wie tatsächliche Variablen. Auch solch ein Bug ist praktisch nicht zu finden! (I.A. wird der Compiler noch nicht einmal eine Warning ausgeben!) Deswegen verwende immer Großbuchstaben für Makro-Variablen; am besten verdoppelte Buchstaben, oder ähnliches.

Bei der Definition von Makros muß man immer alle möglichen Fälle und Kontexte in Betracht ziehen, wie das Makro verwendet werden könnte. Zwei typische Fehler sind:

  • Nicht terminierte s; z.B.: #define Bla( X ) if ( X < 0 ) X = 0; Wenn dieses Makro in einem weiteren verwendet wird, ist schon ein Bug produziert, für den der Anwender noch nicht mal etwas kann: if ( ... ) Bla( x ) else ... Problem: der Compiler wird das auf das innere beziehen! Abhilfe: den ganzen -Ausdruck im Makro mit klammern.
  • Code-Blöcke muß man klammern! Wenn folgendes Makro #define blub( X ) \ z = .... ; \ y = .... ; nicht geklammert wird (mit ), dann entsteht bei der Verwendung in folgendem Statement if ( ... ) blub( X ); ein Bug, der sehr schwer zu finden ist!

    Wenn ein Makro mehr als 7 Zeilen (= Anweisungen) lang ist, sollte man sowieso eine Funktion daraus machen -- der Compiler kann solche "kleinen" Funktionen inline-en.

  • Nicht geklammerte Ausdrücke; z.B.: #define Bla( X, Y ) X-Y führt zu einem schwer zu findenden Bug, wenn man das Makro in einem weiteren Ausdruck verwendet, z.B. z = Bla(a,b) * c; Das Resultat ist nämlich , was sicher nicht die Intention des Programmierers war! Abhilfe: .

    Ein weiteres Beispiel, wieso Klammerung nötig ist:

    #define vmmPrintVec( V ) printf( "%f %f %f", V[0], V[1], V[2] ) liefert vollkommenen Blödsinn (bis zu Core-Dump!), wenn man es so verwendet: vmmPrintVec( *v ) (Prioritäten der Operatoren und !) In diesem Beispiel muß man das Makro also so schreiben: #define vmmPrintVec( V ) printf( "%f %f %f", (V)[0], (V)[1], (V)[2] )

Genauso sollte man versuchen, Makros so zu definieren, daß Argumente nur einmal verwendet werden (was natürlich nicht immer geht). Z.B. kann man statt

#define blub( X ) \ bla = X; \ blub = malloc( X * ... ); besser schreiben #define blub( X ) \ bla = X; \ blub = malloc( bla * ... );

Annahmen

Verlasse Dich niemals darauf, daß Funktionen sich so verhalten, wie Du denkst, wenn es nicht explizit in der Man-Page steht! Einige Beispiele:
  • IDs von Visuals können verschieden sein, auch wenn Du dasselbe Visual angefordert hast!
  • kann den Speicherblock verschieben, auch wenn Du den Block verkleinerst.

Eingabe-Parameter

Viele Bugs entstehen dadurch, daß Parameter nicht auf Gültigkeit und Plausibilität gecheckt werden7)

7) Ein Zitat aus dem Netz: An ounce of prevention is worth a ton of code. (Anonymus).

Funktionen, die nicht mehr als ca. 100× pro Frame aufgerufen werden, sollen immer die Parameter checken auf gültigen Wertebereich! Das kann den Code dieser Funktionen locker auf das doppelte anwachsen lassen --- aber: das ist es wert!

  • Checke Zeiger auf . Außer, die Funktion wird mehr als 100× pro Frame aufgerufen. (siehe auch Pointer oder Reference)
  • Mit einem einzigen Buchstaben kann man manchmal schon einen Core-Dump verhindern. Z.B.: void foo( char *param ) { char blub[MaxBlubLen]; strcpy( blub, param ); \\ was passiert, wenn param laenger als MaxBlubLen ist ?!! } Besser ist: strncpy( blub, param, MaxBlubLen ); blub[MaxBlubLen-1] = 0; Noch besser ist natürlich eine zusätzliche Warning.
  • Wenn ein Parameter ein -Typ ist, dann soll dieser Typ mit Min-/Max-Werten deklariert werden: typedef enum { XYZ_MIN, XYZ_VALUE_1, // kommentar XYZ_VALUE_2, // kommentar XYZ_MAX, } xyzTypeE; Damit kann man in einer Funktion den gültigen Wertebereich mit if ( e <= XYZ_MIN || e >= XYZ_MAX ) Fehlermeldung checken. Dieser Check bleibt auch gültig, wenn man nachträglich Werte zum -Typ hinzufügt.

    Dasselbe gilt in C++, wenn der "" mit Hilfe mehrerer s deklariert wird.

  • Checke, daß nicht aus Versehen ein Verzeichnis gelesen wird, wo eigentlich ein File gelesen werden sollte. (Siehe .)

"Can't happen"-Fälle

Eigene Funktionen. Wenn Du Deine eigenen Funktionen verwendest, wird es viele Stellen geben, wo gewisse Parameter-Kombinationen oder Variablen-Belegungen zwar laut Code vorkommen könnten, wo Du aber weißt, daß das nicht passieren kann, weil Du die Funktion nur mit bestimmten Parametern aufrufst.

Glaube aber einem erfahrenen (und leid-geprüften) Programmierer: es wird vorkommen! (Vorausgesetzt, Dein Code überschreitet eine gewisse "kritische Größe", das sind ungefähr 5,000 Zeilen.)

Deswegen: in jeden und in die meisten 's gehört ein bzw. für den "can't happen"-Fall! Der muß wenigstens dafür sorgen, daß das Programm eine auffällige Fehlermeldung liefert und ohne Core-Dump weiterläuft.

System calls. Auch system calls (z.B. oder oder ) können schief gehen! Sogar dann, wenn es gar nicht passieren kann. (Z.B. kann nämlich immer passieren, daß der Speicher oder die i-node table voll ist.)

Deswegen sieht ein immer so aus:

f = open("bla", "r") if ( f < 0 ) { perror("open"); fprintf(stderr, "module: Failed to open file ..."); do something sensible instead } und jeder so: m = malloc( n * sizeof(type) ); if ( ! m ) { fprintf(stderr, "module: malloc failed!\n"); ... // do something sensible instead {

Man könnte sich dafür natürlich Wrapper-Makros schreiben. Meine Erfahrung allerdings ist, daß diese dann oft umständlich im Code aussehen, und man spart eigentlich nur ein bißchen Tiparbeit, welche man mit einem vernünftigen Editor sowieso reduzieren kann.

Misc

Verwende nie denselben Filenamen mehrfach in einem Software-System! Weder bei Header-Files noch bei C-Files.

Das -Makro (siehe ) kann helfen, die Wartbarkeit zu erhöhen, und hilft gleichzeitig, Bugs schneller zu erkennen (auch wenn man an der betreffenden Stelle gar keinen gesucht hat).
Außerdem werden durch das -Makro explizit Bedingungen im Code sichtbar gemacht, z.B. Schleifeninvarianten, oder Vor- und Nachbedingungen.
Achtung: achte darauf, daß dieses Makro in der Produktversion nicht aktiviert ist! ()

Verwende keine Pfade beim Includen (z.B. )! Verwende statt dessen die -Option des Compilers (dann kann man später wesentlich leichter die Libraries re-organisieren, ohne daß alle Source-Files geändert werden müssen).
Verwende für Standard-Header-Files (normalerweise in ) und für alle anderen (kleiner Speedup beim Compilieren).

Der Header-File sollte immer auch in dem C-File included werden, in dem die entsprechenden Funktionen oder Variablen tatsächlich definiert werden. Dann kann der Compiler checken, daß die Deklaration immer noch mit der Definition übereinstimmt.

Verwende bevor Du eines der anderen -Makros verwendest. Z.B.

if ( isascii(*c) && isdigit(*c) )

kann aufhören bevor es alle Parameter gescant hat. Return-Wert checken!

Verwende einen -Wrapper, der Form

#define xmalloc( PTR, SIZE, ACTION ) \ { \ PTR = malloc( SIZE ); \ if ( ! PTR ) \ ACTION; \ } Das zwingt einen dazu, tatsächlich sich Gedanken zu machen zu dem Fall, daß kein Speicher mehr frei ist.

Falls das fall-through feature eines -Statements verwendet wird, so muß das kommentiert werden. Das -Statement eines muß immer vorhanden sein. Vermeide eingebettete Statements. Auch und zählen.
Nur manchmal kann es den Code leserlicher machen, wie z.B.

while ( (c = getchar()) != EOF ) { process the character }

Anfänger-Bugs

Jeder Anfänger macht folgende Bugs --- mach Dir also nichts daraus, wenn sie Dir auch passieren (auch mir sind sie passiert):
  • Vergleich auf String-Gleichheit mit if ( strcmp(s,t) )
  • mit called-by-value Parametern (ergibt i.A. einen Core-Dump an dieser Stelle). mit falschem Format-String ergibt meistens zufälligen Output oder einen Core-Dump. Der SGI-Compiler würde aber eine Warning ausgeben.
  • Die Funktion (oder Makro) auf s oder s anwenden gibt Müll -- dafür muß man oder nehmen!
  • Präzedenz der Operatoren und falsch gemerkt 6) Merkhilfe: kopiert Strings. (Dafür gibt es natürlich .)
     
    6) Mache Dir eine Kopie der Tabelle der Präzedenzen aller Operatoren, z.B. aus dem Insight-Book C Language Reference Manual, Chapter 7, Table 7.1 .
     
  • Der Wert von wird direkt verwendet, und nicht auf getestet! (Was ist, wenn die Environment-Variable nicht definiert ist? Was ist, wenn die Variable zwar definiert ist, aber eine leerer String ist?)
  • statt in s. Ein beliebter Fehler, der jedem mal passiert, ist if ( ch = '\r' ) was immer liefert! (Gemeint war natürlich .)
    Man kann den Vergleich umgekehrt schreiben: if ( '\r' = cr ) weil dann schon der Compiler meckert! Das klappt natürlich nur, wenn man auf der einen Seite eine Konstante hat. Ich persönlich finde diese Schreibweise auch nicht so hübsch ;-)

Kleinere Angelgenheiten

Wenn man s "vorne" und "hinten" en muß, so soll man denselben Namen wählen: typedef struct blubT { ... } blubT;

Vermeide exzessive ""-itis! Es macht keinen Sinn, einen Typ einzuführen, oder , oder , oder !

Unäre Operatoren werden i.A. ohne Space geschrieben, binäre Operatoren (außer "." und "") haben links und rechts ein Space. Bei komplexen Ausdrücken muß man von Fall zu Fall neu entscheiden.

Wenn ein -Loop lange Sections enthält, schreibe jede Section auf eine eigene Zeile, z.B.:

for ( i = 0; i < plhGetNFaces(o)*2 + plhGetNPoints(o); i += n/2 + (empty ? 1 : 2) )

Verwendung von und innerhalb derselben Schleife sollte vermieden werden.

Schreibe ANSI-C! (komplette Prototypen)

RTTI ist erlaubt (kostet inzwischen keine Performance mehr). Aber verwende es nie anstelle von virtuellen Methoden.

Optimierungen

Generell gilt: optimiere zuerst den Algorithmus, und nicht die Implementierung durch vereinzelte "Tricks"! Der Compiler kennt die CPU viel besser als Du.

Bevor Du die Implementierung "tune-st" (optimierst), frage Dich, ob die Implementierung wirklich schon so weit fortgeschritten ist, daß das Sinn macht!9)

 
9) "Premature Optimization is the Root of All Evil" -- Donald E. Knuth.
 

Wenn optimiert werden soll, dann nur nach einem Profiling! Du wirst staunen, wo die Zeit wirklich verloren geht.

Zuerst läßt man den Compiler optimieren. Dies geschieht mit folgenden Compile-/Link-Optionen:

  1. cc -n32 -O ...
  2. Für C++-Code kann Inlining ein bißchen Geschwindigkeit bringen, wenn man viele kleine - und -Funktionen hat. Dazu muß man keinen Source im Header-File schreiben! Das geht mit der richtigen Compiler-Option: cc -n32 O -INLINE:=ON schaltet Inlining für einzelne Files an, d.h., Funktionen werden innerhalb dieses Files inlined.

    Für C-Code bringt es nur etwas, wenn man weiß, daß man kleine(!) Funktionen hat, die ein paar 1000 Mal aufgerufen werden.

    Inlining über mehrere Files hinweg geht mit

    cc -O -IPA:inline=ON muß auf der Compile-Zeile als auch auf der Link-Zeile angegeben werden.

    Wenn man wissen will, was da eigentlich abgeht, macht man

    cc -O -INLINE:=ON:list=ON Dann wird auf ausgegeben, was inlined wird.

    Generell ist meine Erfahrung: der Compiler weiß sehr gut, wann es sich lohnt! Wenn man trotzdem unbedingt möchte, daß eine bestimmte Funktion inlined wird, macht man

    cc -O -INLINE:=ON:must=foo,bar (Fuer C++ müssen natuerlich die "mangled names" angegeben werden.)

    Für Inlining aus Libraries kann man verwenden, wenn es eine -Lib ist (nicht ) und wenn diese Library auch mit ) erzeugt wurde. Ansonsten muß man nehmen. Man sollte außerdem setzen, sonst wird der Code zu groß (behauptete jemand in der Newsgroup).

  3. Die ganz heftigen Compiler-Optionen sind: cc -n32 -O3 -OPT:alias=typed -OPT:fast_sqrt=ON:fast_exp=ON:IEEE_arithmetic=3 -OPT:ptr_opt=ON:Olimit=3000 -OPT:unroll_times_max=6 -LNO:opt=1:gather_scatter=2 -IPA:alias=ON:addressing=ON:aggr_cprop=ON -IPA:inline=ON -INLINE:must=foo,bar
Wer mehr zu Inlining und anderen Compiler-Optionen für die Optimierung wissen will, macht , oder schaut im Insight-Book "MIPSpro Compiling and Performance Tuning Guide" nach.

Beispiele von Pseudo-Optimierungen

while ( *i++ = *j++ ) ; ist nicht schneller (sogar eher langsamer) als while ( *j ) *i = *j , i ++ , j ++; (Noch besser in diesem Fall ist oder )
Denn: mit Nebeneffekten () nimmt man dem Compiler sogar Möglichkeiten zur Optimierung! (Z.B. durch Vertauschen von Assembler-Zeilen.) Außerdem ist sehr sorgfältig in Assembler codiert.

Mit anstatt einfach nur zwingst Du den Compiler höchstens, seine optimierte Register-Allozierung fallenzulassen, um Deiner Anweisung nachzukommen! (falls er es überhaupt beachtet.)

Inlining einer Funktion bringt wirklich nur dann etwas, wenn diese aus 1-2 Zeilen besteht! (In allen anderen Fällen explodiert nur die Code-Größe.)

Ganz analog ist es mit dem Faktorisieren von Funktionen: wer alles in eine Funktion packt, oder aus jeder Funktion ein Makro10) macht, der soll mal ganz schnell in CPU benchmarks nachschauen! (Da kann man nachsehen, wie teuer ein Funktionsaufruf wirklich ist.)

 
10) OK, ich gebe zu, das haben wir im Y leider auch gemacht --- zu unserer Entschuldigung kann man sagen, daß damals (1994) die Compiler noch nicht sehr gut optimieren konnten (kein inlining), und daß wir damals einfach noch nicht wußten, wie schnell ein Funktionsaufruf wirklich ist!
 

Eigene Pointer-Arithmetik lohnt sich meistens nicht:

for ( p = array + n - 1; p >= array; p -- ) { p->item = ... oder *p = ... } ist genauso effizient wie for ( i = n-1; i >= 0; i -- ) p[i] = ... Die zweite Variante ist um den Faktor 10 schneller (Weil der Compiler mehr Freiheit zum Optimieren hat)!

Es kann extrem peinlich werden, wenn ein Informatiker die Oberstufen-Mathematik nicht beherrscht. Es ist schon vorgekommen, daß Leute den Ausdruck 1 + q + q2 + ... + qn mit einer Schleife berechnet haben! (Geometrische Reihe)

Ungeschicktes Codieren

Vermeide FPEs (floating-point exceptions). Zwar werden sie i.a. ignoriert, kosten aber doch Zeit, da die Exception trotzdem erzeugt und bearbeitet wird. FPEs können u.a. durch Rechnen mit uninitialisierten Variablen oder NaN'sentstehen.

Durch ungeschicktes Codieren kann der effizienteste Algorithmus zunichte werden. Ein Beispiel:
Ein Algorithmus verarbeitet einen String der Länge N und hat Komplexität O(N*log(N)). Ein Zwischenschritt ist das Konkatenieren von k Teilstrings der Gesamtlänge N. Geschickte Implementierung:

char *teilstring[k]; char gesamtstring364; char *gesamtende = gesamtstring; char *charptr; for ( i = 0; i < k; i ++ ) { charptr = teilstring[i]; while ( *gesamtende++ = *charptr++ ); } hier ist der Aufwand genau a*N. Weniger gut: for ( i = 0; i < k; i ++ ) { strcpy( gesamtende, teilstring[i] ); gesamtende += strlen( teilstring[k] ); } hier ist der Aufwand genau a*2N. (Weil jeder genau 2× durchlaufen wird.)
Miserabel: for ( i = 0; i < k; i ++ ) strcat( gesamtstring, teilstring[i] ); hier ist der Aufwand genau a*N2!

Noch ein "schlechtes" Beispiel:

length = sqrt( pow( point1[0] - point2[0], 2) + pow( point1[1] - point2[1], 2) + pow( point1[2] - point2[2], 2) ); Wenn dieser Code häufig ausgeführt wird, ist die Performance im Eimer! Abgesehen davon ist es einfach extrem häßlich, das Quadrat einer Zahl mit statt mit zu berechnen. Außerdem zeugt so etwas davon, daß der Programmierer das System nicht kennt, von dem sein Code ein Teil werden soll --- denn jedes graphische System stellt garantiert schon eine ganze Menge von Funktionen für die allfällige Vektor-Matrix-Arithmetik zur Verfügung.

Verwende , wenn Du temporär Speicher brauchst, der nach dem Ende der Funktion nicht mehr benötigt wird. Das geht schneller, die Gefahr von memory leaks ist kleiner, und es vermeidet Speicherfragmentierung. Verwende nicht, falls Du evtl. viel Speicher brauchst, denn falls auf dem Stack nicht mehr genügend Speicher vorhanden ist, wird das Programm von Unix gekillt.

Niemand programmiert mehr einen String-Copy, Quicksort, Hashtables, Listen, dynamische Arrays, etc.! Dafür gibt es gute, effiziente, bewährte Standard-Libraries! (siehe RTFM) --- selber programmieren dauert viel zu lange, gibt mehr Möglichkeiten für Bugs, und ist nie schneller als die Standard-Funktionen, da diese sorgfältig in Assembler geschrieben wurden und getunet sind.

Object-oriented Design

In diesem Abschnitt werden ein paar grundlegendste Richtlinien von objekt-orientiertem (oo) Design (OOD) beschrieben. Die meisten sind unabhängig von der Sprache (man kann ja ein OOD sogar in Assembler implementieren).

Echte Optimierungen

Richte Arrays, deren Größe in der selben Größenordnung wie eine Cache-Zeile ist, an entsprechenen Memory-Boundaries aus. (Bsp.: eine Cache-Zeile ist 64 Bytes lang (Pentium 4), also richte Arrays von ungefähr dieser Größe auch an 64-Byte-Boundaries aus.)

Verwende Pre-Increment, statt Post-Increment.
Grund: bei Post-Increment mu� der Compiler zuerst eine Kopie des Objektes erzeugen (Copy-Ctor!), dann die Methode des Objektes aufrufen, und schlie�lich die Kopie wieder verwerfen. Dabei hat es der Compiler wesentlich schwerer zu erkennen, da� der erste Aufruf des Copy-Ctors eingespart werden kann.
Beim Pre-Increment f�llt dies wesnetlich leichter.

Allgemeine Richtlinien

Hier einige grundlegende Richtlinien eines jeden Moduls oder Library:
  1. Einfachheit.
    Sowohl das Interface als auch die Implementierung muß einfach sein Die Einfachheit des Interfaces hat Priorität gegenüber der Einfachheit der Implementierung -- trotzdem ist eine einfache Implementierung wichtig, besonders für die Wartbarkeit.
  2. Korrektheit.
  3. Konsistenz.
    Dieses Kriterium ist vielleicht am schlechtesten objektiv meßbar. Nichtsdestotrotz ist Korrektheit genauso wichtig wie Einfachheit. Um Konsistenz zu erreichen kann man, wenn unbedingt nötig, ein wenig von der Einfachheit opfern -- aber nie umgekehrt!
  4. Vollständigkeit.
    Das Design / die Library / das Modul muß alle möglichen Situationen abdecken, und ein paar mehr, da es sehr schwer ist, alle Fälle vorauszusehen.
    Falls die Einfachheit extrem leiden würde, ist es besser auf die Vollständigkeit zu verzichten.
Wichtig ist auch, die richtige Beziehung ("is-a", "uses", "is-like") zwischen Klassen zu finden. Es ist falsch, als erstes an Vererbung zu denken.

In "Worse is better" ist ein interessanter Gedanke zum Thema Vollständigkeit, Einfachheit, und Konsistenz: manchmal kann es besser sein, etwas von der Konsistenz- oder Vollständigkeitserhaltung dem Aufrufer aufzubürden, nämlich dann, wenn es der Aufrufer viel leichter erreichen kann als die Implementierung in der Library.
Das einzige Problem ist eigentlich "nur", das richtige Maß zu treffen!

Liskov's Substitution Principle

0 Replies to “Copy Konstruktor Beispiel Essay”

Lascia un Commento

L'indirizzo email non verrà pubblicato. I campi obbligatori sono contrassegnati *