C++ Erklärung der Grundkonzepte

  • Hallo,
    da viele hier im Forum irgendwann anfangen C++ zu lernen, aber ein Großteil der Bücher und Tutorials veraltet oder falsch ist, erkläre ich hier mal die
    Grundprinzipien für sauberes C++.

    • Objektorientierte Programmierung (OOP):
      OOP ist das grundlegende Feature von C++ und heißt, dass Daten in Objekten gekapselt sind.
      Eine Klasse ist der Datentyp eines Objekts.
      Das ist ähnlich wie ein DllStruct, nur kann man neben ein paar anderen Features zusätzlich Methoden definieren, das sind Funktionen, die mit dem Objekt
      verbunden sind. Wenn man daran gewöhnt ist, ist $variable.length() einfacher zu lesen, als StringLength($variable), außerdem kann man die internen Daten
      eines Objekts verstecken, sodass man sie von außen nicht ändern kann, Methoden haben aber immernoch volle Zugriffsrechte darauf.
      Man kann Operatoren (+, &, +=, etc) für eigene Objekte selber definieren und so übersichtlich mit ihnen programmieren, indem man z.B. eine Vektorklasse hat und die normalen
      mathematischen Schreibweisen zulässt.


    • Vererbung:

      Vererbung heißt, man leitet eine Klasse von einer anderen ab. Die abgeleitete übernimmt alle Eigenschaften der Basisklasse, kann aber Methoden überschreiben
      und zusätzliche definieren. Dadurch kann man eine allgemeine Basis haben und die immer weiter spezialisieren. Zum Beispiel kann die Basisklasse Tier nehmen
      und Verhaltensweisen wie Essen und schlafen definieren. Dann leitet man die spezialisierte Klasse Hund ab und fügt Bellen hinzu und leitet als zweites Vogel ab
      und fügt Fliegen hinzu, überschreibt aber das normale Essen mit Körnerpicken.


    • Konstruktor:

      Der Konstruktor ist einfach eine Funktion, die ein Objekt initialisiert.

      [autoit]

      Func createVector($x, $y)
      Local $struct = DllStructCreate("int x; int y;")
      DllStructSetData($struct, "x", $x)
      DllStructSetData($struct, "y", $y)
      return $struct
      EndFunc

      [/autoit]


      wäre quasi ein Konstruktor, aber richtige Konstruktoren sind übersichtlicher und sicherer, weil sie implizit aufgerufen werden:
      Bei

      Code
      string text = "Hello, world!";

      wird automatisch ein Konstruktor aufgerufen, der den String "Hello, world!" in das Objekt kopiert.
      Würde man altes C verwenden, müsste man erst selbst den Speicher reservieren mit

      Code
      char* string = malloc(strlen(anderer_string));


      und dann füllen mit

      Code
      strcpy(string, anderer_string);


    • Destruktor:

      Er ist quasi das Gegenteil des Konstruktors. Wenn das Objekt gelöscht wird, sorgt er dafür, dass alles ausgeräumt wird. Bei einem string sorgt er zum Beispiel dafür, dass der Speicherplatz wieder freigegenben wird. Bei dem anderen Beispiel von C muss man dafür selber sorgen mit
      Code
      free(string);

      und wenn man das irgendwo vergisst, hat man Memory leaks.
      Mehr dazu schreibe ich später.


    • Exceptions:

      Exceptions sind Ausnahmen, die irgendwo bei einem Fehler ausgelöst werden. Sie übernehmen die Aufgabe von @error, aber @error muss nach jedem Schritt überprüft werden, gegenenenfalls die Funktion beendet und @error neu gesetzt werden muss
      [autoit]Func Foo()
      Bar()
      if @error Then Return SetError(@error)
      ; blablabla
      EndFunc[/autoit]
      und das wird von einer Funktion in die nächste weitergegeben, bis es eine ignoriert oder verarbeitet.

      Dagegen beenden Exceptions automatisch alle anderen Funktionen und springen zu einer Stelle, die vorher mit try und catch definiert wurde

      Code
      try
      {
      // ganz gang viele, komplizierte Funktionen
      }
      catch (exception e)
      {
      cout<<e.what();	// das heißt, die Feherbeschreibung wird ausgegeben
      }


      Der Vorteil ist, dass ein Fehler an ganz anderer Stelle bearbeitet werden kann, wenn z.B. beim Einlesen von Dateien ein Fehler auftritt, der Fehler vom Manager aufgefangen und an GUI oder Konsole weitergegeben werden. Dadurch erreicht man eine striktere Trennung von verschiedenen Bereichen und mehr Übersichtlichkeit.


    • Namespaces:

      Namespaces sind wirklich kein großartiges Feature.
      Es erlaubt nur Namensräume, quasi Codeabschnitte mit Namen zu definieren und so zu verhindern, dass sich Namen überschneiden. Zum Beispiel gibt es in der Standard-library (= standard-UDFs) die Klasse string, man hat vielleicht aber auch eine andere Klasse, die genauso heißt. Deswegen schreibt man std::string und bestimmt dadurch, das man die Klasse/Funktion aus std (Standard library) haben will. Bei AutoIt macht man das ähnlich, indem man _WinAPI_... schreibt, aber die namespaces sind da noch etwas mächtiger und übersichtlicher. Wenn man selber eine library schreibt, sollte man namespaces nutzen und wenn man eine nutzt, sollte man sich sehr gut überlegen, ob man den Namespace mittels
      Code
      using namespace

      in den globalen integriert. Leider ist es in Tutorials sehr verbreitet, denn es erspart Tipparbeit, wenn man nur string statt std::string schreibt, aber umso größer und komplexer das Pojekt, umso wahrscheinlicher wird es auch, dass sich Namen überschneiden, Fehler und Probleme bei der Zuordnung auftreten.
      Leider lernen die meisten Anfänger das falsch und gewöhnen sich schnell daran.


    • Templates:

      Templates sind eine Art zusätzlicher Parameter, der beim Compilen bestimmt wird. Im Gegensatz zu anderen Parameter können sie auch Typen enthalten. Sie sind sehr praktisch, wenn man eine allgemeine Funktion oder allgemeines Objekt für verschiedene Datentypen haben will. Eine template-classe ist beispielsweise list, eine verkettete Liste aus der Standard library Mit
      Code
      list<int>

      erzeigt man eine Liste aus integern, mit

      Code
      list<string>

      eine aus Strings und mit

      Code
      list<MeineEigeneKlasse>

      eine für die selbst definierte Klasse. In C musste man noch Funktionen für alle drei Dateintypen selbst definieren und dadurch mehrmals fast den gleichen Code schreiben. Fast alles aus der Standard-library enthält templates, damit man es so allgemein wie möglich halten kann, deswegen spricht man auch von der standard template library, kurz STL.
      Templates selber können noch viel mehr, als nur Typen dynamisch zu machen und für solche Zwecke zwar sehr einfach, aber wenn man den vollen Funktionsumfang nutzen will, sind eines der kompliziertesten Dinge aus C++. Sie haben quasi den Umfang einer kompletten Programmiersprache, die aber während des Compilens umgesetzt wird und so maximale Übersichtlichkeit und Sicherheit bei keinem Geschwindigkeitsverlust bieten.


    • RAII:

      Dieses ist wohl der wichtigste Punkt, denn dieses Konzept ist das grundlegende für sauberes C++, vermeidet fast alle Flüchtigkeitsfehler, die zu Memory Leaks, fehlerhafter Threadsteuerung oder anderen fehlern bei Resourcen führen. Es wurde früher schon eingeführt aber enthielt immer noch Lücken, in C++11 (der neue Standard von 2011) wird es aber konsequent und fast vollständig umgesetzt und mit C++14 noch weiter ausgebaut. Es steht für Resource Acquisition Is Initialization und bedeutet dass alle Daten in Objekte gekapselt werden, die bei der Objekterzeugung zugewiesen und bei Zerstörung der Objekte vom Destruktor automatisch freigegeben wird.
      Zum Beispiel muss man in AutoIt Dateien mit FileOpen öffnen und mit FileClose wieder schließen. Dabei können Fehler auftreten, indem FileClose nicht aufgerufen wird.
      [autoit]Func foo()
      Local $file = FileOpen("Datei.txt")
      If $file=-1 Then Return SetError(1) ; beende mit exit code 1

      Local $string = FileRead($file, 5)
      If StringLen($string)<>5 Then Return SetError(2) ; beende mit exit code 2

      ; anderer code

      FileClose($file)
      EndFunc
      Hat die Datei weniger als 5 Buchstaben, wird frühzeitig mit Return abgebrochen, die Datei bleibt geöffnet und kann von anderen Programmen nicht mehr verwendet werden. Man kann die Datei aber auch nachträglich nicht mehr schließen, weil $file nur lokal ist und deswegen nach Ende der Funktion nicht mehr existiert. Man muss also an jedem möglichen frühzeitigen Ende alle Resourcen aufräumen.
      Bei AutoIt ist es aber harmlos, weil nur durch Return, ExitLoop, ContinueLoop, if-Abfragen, Switch und Select ein FileClose oder ähnliche Funktion ausgelassen wird, wobei nur Return wirklich relevant ist, weil man meistens am Ende der Funktion aufräumt.
      In C++ hingegen gibt es alle diese Fälle auch, aber zusätzlich auch noch goto, longjump (ähnlich wie goto, aber über Funktionsgrenzen hinweg), auf diese beiden sollte man aber meistens verzichten, und Exceptions, die an jeder Stelle des Codes ausgelöst werden können, auch innerhalb von aufgerufenen Funktionen und teilweise unabsehbar. Es kann auch passieren, dass beim Aufruf einer Funktion ein Parameter eine exception auslöst und so die Resource des andere Parameter nicht freigegeben werden kann. In C+ hat (und braucht) man auch deutlich direkteren Zugriff auf den Arbeitsspeicher und kann deswegen sehr leicht memory-leaks auslösen.
      C++ garantiert aber, dass alle Objekte beim Verlassen des Scopes gelöscht und die Destruktoren aufgerufen werden. Deswegen kapselt man die Resourcen in ihnen und egal was passiert (Außnahme Absturz), es ist gesichert, dass alle Resourcen freigegeben werden.
      Für Dateien gibt es fstream, das RAII und andere Vorteile in sich vereint und man sollte sie deswegen nutzen und nicht fopen und fclose. Will man einen container (array, liste, map, hashmap u.v.m.) haben, sollte man sich keine wie in C selbst basteln und mit Funktionen alles selbst verwalten und freigeben, sondern die fertigen nutzen, die neben RAII (auch die Destruktoren des Inhalts werden aufgerufen) auch generisch und damit übersichtlicher sind.
      Wenn ein Objekt länger als der Scope leben soll, dann kann man es dynamisch mit der Funktion new erzeugen und einen Zeiger darauf übergeben, aber dann muss man es auch wieder selbst freigeben mit delete (delete ruft auch den Konstruktor auf), aber ein Zeiger ist nicht RAII, denn wenn er gelöscht wird, bleibt der Speicher erhalten. Deswegen sollte man hierfür smart-pointer nutzen, denn diese existieren als Objekt, das den Speicher im Destruktor unter der Voraussetzung freigibt, dass es nicht vorher kopiert wurde, dann wird nämlich der Zeiger übergeben und das andere Objekt lässt ihn leben.

      [/autoit]

    Ich denke jetzt mal, dass ihr vielleicht nicht viel oder zumindest nicht alles verstanden habt, aber denkt daran, wenn ihr mal C++ lernt, euch hiermit und anderen Artikeln über modernes C++ auseinanderzusetzen, denn ein recht großer Teil der C++-Programmierer hat die Sprache nie wirklich verstanden oder verwendet noch Programmierweisen aus den 80ern, ohne zu beachten, dass sich C++ weiterentwickelt und besser wird. Leider ist ein Teil dieser Programmierer auch noch der Meinung Tutorials oder Bücher schreiben zu müssen und leider gibt es inzwischen sehr viele Bücher/Tutorials, die falsches beibringen.

    Mfg Marthog

  • Zitat von Marthog


    oder verwendet noch Programmierweisen aus den 80ern

    *hust*

    Gut, das kommt mir bekannt vor. Grundsätzlich kann ich zwar C++, allerdings neige ich zu 90% dazu, C zu programmieren, soll heißen: Kein std::vector, sondern C-Arrays, wenn möglich auch Array of Char, und kein std::string... Und so weiter. ^^

    Aber Danke für die kleine Einführung. Ich werde mir wohl mal angewöhnen müssen, richtiges C++ zu programmieren.

    lg

  • Über den Vektoren Teil kann ich nichts sagen, habe ich mir nie richtig angeschaut. Ich würde vielleicht den Polymorphismus noch hinzufügen, speziell das überladen von Funktionen und vielleicht die Funktionen „virtual“ sowie die Erklärung von „private“ und „public“.

    Ansonsten finde ich es nicht schlecht, wobei ich meiner Seits auch sagen muss, dass ich Bücher die von fast allen verabscheut werden bevorzuge. Folgend wäre mir das hier zu wenig Text :P Mein Schulbuch ist eig. auch ein Ranzbuch wie man im Nachhinein erfahren hat, aber es war für mich sehr viel einfacher mir das Buch zu nehmen und von vorne bis hinten durchzuarbeiten, als mir einen eigenen Pfad des Vorgehens und der Bereiche schaffen und die passenden Informationen dazu suchen zu müssen. Es zeigt auf jeden Fall die Basics simpel auf. Ich hab es grad nicht griffbereit, aber sonst könnte man unten ja noch einen Bereich mit Links: hinzufügen (sofern du mast natürlich) :)

    Lob an dich für den Aufwand :)

    Grüße Yaerox

    Grüne Hölle

  • Super gemacht, aber dennoch bin ich an dieser Stelle anderer Meinung, auch wenn ich nicht allwissend bin ^^.
    1.

    Zitat

    string text = "Hello, world!";
    wird automatisch ein Konstruktor aufgerufen, der den String "Hello, world!" in das Objekt kopiert.


    Falsch.. Der Konstruktur wird zuerst aufgerufen und dann wird die Methode die mit dem =operator verknüpft ist aufgerufen. Diese speichert erst den Inhalt in sich ab.

    2.

    Zitat

    Der Konstruktor ist einfach eine Funktion, die ein Objekt initialisiert.


    und zwar "sich selber". Die Methode initialisiert alles nötige, damit das Objekt, in dem es ist, funktioniert.
    Daher ist das Beispiel auch kein Konstruktor, da es erst ein Object (eine Struct) erzeugt und dann von außen Daten drin speichert.


    Ansonsten bin ich vollkommen einverstanden und dankbar, dass ich einigen Leuten nurnoch diesen Thread zeigen muss ^^ ( auch wenn sie es sich eh nicht durchlesen :S )

  • Über den Vektoren Teil kann ich nichts sagen, habe ich mir nie richtig angeschaut. Ich

    würde vielleicht den Polymorphismus noch hinzufügen, speziell das überladen von Funktionen und vielleicht die Funktionen „virtual“ sowie die Erklärung von

    „private“ und „public“.

    Ich wollte nicht so speziell darauf eingehen, weil dann viele einfach nur noch Bahnhof verstanden hätten. virtual ist z.B. mit dem Überschreiben von Funktionen

    gemeint. Über die Implementierungsdetails wollte ich nicht erzählen, sondern nur allgemein, damit ein Grundverständnis zu den Begriffen da ist.



    1.


    Falsch. Der Konstruktur wird zuerst aufgerufen und dann wird die Methode die mit dem =operator verknüpft ist aufgerufen. Diese speichert erst den Inhalt in sich

    ab.

    Nein, beim Erstellen und gleichzeitigen Zuweisen wird der Konstruktor mit einem passenden Parameter rausgesucht und verwendet. Es kann sein, dass das als
    string text = string(text) compiled wird und erst ein Objekt erzeugt und das dann kopiert wird, sodass man zwei Konstruktoraufrufe auf unterschiedliche Objekte

    hat (1 parameter + copy).
    Der Zuweisungsoperator wird aber nur angewandt, wenn das Objekt bereits besteht und dann zugewiesen wird, wobei manche Implementierungen im

    Konstruktor einfach den Zuweisungsoperator aufrufen, weil man sich so doppelten Code spart.



    2.


    und zwar "sich selber". Die Methode initialisiert alles nötige, damit das Objekt, in dem es ist, funktioniert.
    Daher ist das Beispiel auch kein Konstruktor, da es erst ein Object (eine Struct) erzeugt und dann von außen Daten drin speichert.


    Klar, aber so ist ungefähr verständlich, was ein Konstruktor überhaupt macht, denn fast immer läuft Speicherplatzreservierung und Konstruktoraufruf zusammen ab.