Angepinnt Das C++-Tutorial für Einsteiger

  • C++

Es gibt 12 Antworten in diesem Thema. Der letzte Beitrag () ist von Elanda.

    Das C++-Tutorial für Einsteiger



    Vorwort


    Hallo an all die lieben Menschen aus dem VB-Paradise Forum. ^^
    Es ist nun soweit, ich habe es endlich geschafft den ersten Schritt in mein aller erstes Tutorial zu setzen; bin gespannt wie Autobatterie
    und freue mich bereits mega auf eure Resonanz.

    Ich hoffe natürlich das ich dem ein oder anderen helfen kann, das würde mich sehr freuen. :)
    Scheut euch nicht vor Feedback oder Verbesserungsvorschlägen!

    Kurz dazu wie das hier funktioniert:
    Jene die @Nofear23m 's WPF Tutorial bereits kennen, wissen schon wie der Hase läuft.
    Ich werde nicht alle Tutorials gleich raus-hauen, sondern nach und nach.
    Das hat zwar den Nachteil das nicht alle Informationen sofort zur Verfügung stehen, aber den Vorteil das ich die Kapitel je nach Gebrauch
    verändern kann.

    Stichwort "verändern", die folgende Übersicht bietet eine konzeptuelle Liste von Themen die ich versuchen werde im Laufe des Tutorials
    durchzunehmen, es können Themen dazu kommen oder, je nach Verlauf, reduziert werden.

    Übersicht


    1. Einleitung
      1. Motivation
      2. Kurzer Disclaimer
      3. Was ist C++?
      4. Wie funktioniert C++ im Vergleich zu anderen Sprachen?
        1. Compiler
        2. Linker
        3. Memory-Management
        4. Versioning
    2. Die Basics
      1. Umgebung und Setup (IDE, Toolchain, Build-System)
        1. Setup
        2. Build-System
        3. Toolchain
      2. Das obligatorische “Hello World”
      3. Header, Sources und TUs
      4. Variablen
      5. Typenmodell und Datentypen
      6. Keywords und Operatoren
      7. Statements und Expressions
    3. Elemente der Sprache
      1. Funktionen
      2. Flow-Control (if, for, switch…)
      3. Zeiger und Referenzen
      4. Preprocessor
      5. Namespaces
      6. Enumerationstypen (enum)
      7. Häufige Elemente (arrays, strings…)
      8. Union-Typen
    4. Objektorientiertes Programmieren
      1. Benutzerdefinierte Typen (Klassen, Strukturen)
      2. Typen-Struktur (Konstruktor, Funktionen, Destruktor…)
      3. Vererbung (Inheritance)
      4. Polymorphismus (abstrakte Klassen, interfaces…)
      5. Virtuelle Funktionen
    5. Templates und Meta-Programming
      1. Einführung
      2. Generische Typen
      3. Generische Funktionen
      4. Variadische Funktionen
      5. Typen-Abhängigkeiten
      6. Partielle Spezialisationen
    6. Konzepte der Sprache
      1. Container (array, vector, list…)
      2. Lambdas (Closure types)
      3. Copy-Move Semantik
      4. Forwarding-Referenzen
      5. ADL (Argument-dependent lookup)
      6. Wertkategorien (value categories)
      7. Allgemeine Konventionen


    Kontakt


    Bei Fragen zum Tutorial gehts hier zum:
    C++-Tutorial Diskussions Thread

    Oder auch persönlich auf dem:
    VB-Paradise Discord-Server
    (Bitte beachtet jedoch das alle Fragen auch für jeden einfach zugänglich sein sollten, daher erbitte ich euch essentielle Dinge über VB-Paradise zu klären)



    Na dann wünsche ich euch noch viel Spaß :) :P
    Elanda <3
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    Dieser Beitrag wurde bereits 20 mal editiert, zuletzt von „Elanda“ ()

    1. Einführung

    1. Einleitung


    Bevor ich mit diesem Tutorial beginne möchte ich mich zuerst ein wenig vorstellen. (zumindest Partiell)
    Ich denke das es gute Schule ist dies zu tun.

    Ich bin Elanda, 24 Jahre (aber natürlich nicht für immer, nur zum Zeitpunkt der Erstellung dieses Beitrages) und Entwickle nun seit 10 Jahren
    im Bereich der Computerwissenschaften. Ich habe weder studiert noch wurde in irgendeiner Weise fakultativ in diesem Segment unterstützt.
    Alles was ich hier in diesem Tutorial mit euch Teile, habe ich in der Vergangenheit von verschiedensten Artikeln gelernt.
    Das sollte euch zumindest Mut machen und zeigen das man für diese Dinge nicht unbedingt einen Fachabschluss braucht. :)

    Damals fing ich mit VB.Net an und war begeistert von den Möglichkeiten die mir Visual Studio gezeigt hat. Diese neue Welt… und auch diese neuen
    Freiheiten meine eigene Welt zu gestalten, absolut wonderbra.
    Später irgendwann war ich hin und weg von Minecraft modding und fand meinen Weg zu Java, welches, trotz aller härte es gegenüber (teilweise auch berechtigt),
    immer noch Platz 2 in meinem Herzen belegt. Später irgendwann kam dann C# hinzu und wieder war ich begeistert, welches mich auch
    irgendwann zu WPF führte aber nur kurzweilig.

    Letztendlich, vor ungefähr 4 Jahren (Anfang 2018), bin ich dann schlussendlich mit C++ in Berührung gekommen.
    Ursprünglich bin ich auf den Zug aufgesprungen da ich gerne Plugins für Musikprogramme entwickeln wollte. (Sogenannte VSTs, welches auch nur ein Format vieler anderer ist)
    Jedoch… war Audio-Engineering doch etwas komplexer als zuerst angenommen. Hätte ich damals mehr Zeit investiert, wäre es mir sicher möglich gewesen,
    aber ich bin eben eine Person die sehr schnell das Interesse verliert.
    Bei C++ blieb ich aber, da ich noch nie zuvor so eine Verbundenheit gespürt hatte.

    Mehr als 4 Jahre Erfahrung kann ich euch natürlich nicht vorweisen und verstehe auch wenn das ein paar von euch skeptisch werden lässt;
    da C++ bekannt dafür ist es nie gänzlich ausgelernt haben zu können und es viele gibt welche mehrere Dekaden an Erfahrung mit sich bringen
    und selbst immer noch nicht alles wissen.
    Allerdings muss man auch dem technischen Fortschritt zugute halten, dass man mittlerweile alles mit nur ein paar wenigen Klicks aufrufen kann;
    somit ist es nicht mehr all-zu schwer gewisse Informationen in knackigem Format zu konsumieren.

    Gut… nicht so tolle Überleitung - aber egal.
    Ich denke das ist genug zu mir und meinen allgemeinen philosophischen Eindrücken dieser Welt.
    Kommen wir zum Hintergrund für dieses Tutorial.

    1.1 Motivation


    Grundsätzlich möchte ich hier nicht mit einer Lüge beginnen. Zu 50% mache ich das Tutorial hier für mich, weil ich das Gefühl haben möchte das ich etwas zustande bekommen
    habe das einem anderen geholfen hat. Das ist sowohl zur hälfte als egoistisch als auch als altruistisch zu betrachten.
    Dennoch sollte das kein Maßstab sein an dem man anfängt zu messen ob diese Reihe, im großen und ganzen, auch wirklich berechtigt ist da zu stehen wo sie ist.

    Denn der andere genau so nicht zu verachtende Teil betrifft den Fakt das ich immer wieder von Leuten zu hören bekomme,
    dass sie mit C++ nichts zu tun haben wollen (oder Angst davor haben) weil sie irgendwann von XY gehört haben, dass es eine zu komplizierte Sprache sei.
    Es stimmt schon, es kann an manchen stellen recht komplex sein und auch stimmt, dass Sie einen oft in die Knie zwingen kann.
    Dennoch frage ich mich an dieser Stelle: "Ist das nicht auch in anderen Sprachen der Fall?"

    Obgleich das relativieren verschiedener Sprachen gerechtfertigt ist sei mal dahingestellt. Man sollte sich aber darüber bewusst werden, dass jede einzelne dieser Sprachen
    ihre eigenen Macken und Probleme aufweisen.
    Man sollte nicht immer vom schlimmsten ausgehen und es einfach mal probieren. (obwohl, esoterische Sprachen sind da dann wieder ein ganz anderes Kapitel)

    Auch möchte ich noch ein wenig die Sinnhaftigkeit dieser Reihe ansprechen.
    Mir ist klar das ich hier möglicherweise das Rad neu erfinde (die Phrase hat auch noch keiner jemals verwendet) wenn man all die Tutorials im Netz betrachtet.
    Tonnenweise wird man damit überschwemmt. Die einen gut, die anderen Besser, aber immer noch als Referenz sehr sehr empfehlenswert.
    Was hier aber zu unterscheiden ist - diese Tutorials sind oft sehr professionell gestaltet.

    Ich war nie wirklich richtig Fan von diesem übermäßig sachbezogenen Tonus wie es vor den 2000ern noch der Fall war; doch die Welt ist ständig im Wandel.
    Ich liebe Artikel die etwas Persönlichkeit mit sich ziehen; weil es einfach (besonders Beginnern) zeigt, dass nicht alles immer strikt nach Konventionen
    gehen muss da auch der Wert der Individualität eine große Rolle spielt und etwas sehr wunderschönes sein kann.

    Und bevor wir weiter gehen, zum nächsten Teil, möchte ich euch noch schnell den Leitsatz dieser Reihe übermitteln.
    Ich hoffe ihr sprecht diesen auch laut aus damit ihr ihn euch auch wirklich einprägen könnt:
    KEINE ANGST VOR C++

    Ich lasse euch da mal rein-interpretieren was ihr möchtet...

    1.2 Kurzer Disclaimer


    Und bevor wir mit dem Tutorial endlich beginnen, möchte ich Vorweg noch ein paar Dinge festhalten.
    Ich versuche hier nicht Menschen zu konvertieren. Heißt, wenn du bei VB oder C# oder was auch immer bleiben möchtest,
    dann bitte tu das auch. Lediglich das näher bringen ist hier meine Mission.

    Es gibt bestimmt einige unter euch die mal Interesse an C++ hatten oder aber auch immer noch haben, daher musste ich das nochmal erwähnen.
    Sollten irgendwelche Fragen auftauchen, könnt ihr das gerne im Zweit-Post tun. (Idee einfach mal Hardcore gestohlen (Danke NoFear23m, du bist eine Koryphäe <3))

    Und nun!
    Smartphones nieder legen und Kamera ab! (ok ich glaube ich bin hier im falschen Film, sorry das musste noch sein :p)

    1.3 Was ist C++?



    Die vorherigen Punkte nun hinter uns gelassen, möchte ich euch den Superstar dieses Beitrages selbst mal etwas vorstellen.
    (Ich werde mich gegebenenfalls hier und da ein wenig von Wikipedia unterrichten lassen, denn alles weiß ich nun auch nicht auswendig (vor allem Jahreszahlen, igitt): wikipedia.org/wiki/C%2B%2B)

    Zuerst auf die Bildfläche getreten ist C++ im Jahre 1985 - entwickelt von Bjarne Stroustrup als Erweiterung von C,
    was das "++" hinter dem Namen signalisieren soll. Das ist der sogenannte post-increment operator und bedeutet kurz: "Den Wert um eins erhöhen".
    Vergleichbar mit VB6 und VB.Net ist auch C++ der Nachfolger von C, anders jedoch wird C auch heute noch weitestgehend verwendet.
    Ich würde sogar behaupten C nicht als veraltet zu betiteln, da es immer noch legitime Anwendungsbereiche wie zum Beispiel Embedded-Development gibt.
    Nein, die Ähnlichkeit zwischen C/C++ und VB6/VB.Net hier liegt eher darin das sich der Entwicklungs-Stil stark gewandelt hat.

    Während C eine eher Imperative/Prozedurale Sprache ist, steht C++ dem in nichts nach und fügt auch noch ein paar andere hinzu,
    unter anderem ist dies die wohl allseits bekannte Objekt-Orientierte Programmierung.
    Das bedeutet aber nicht das man mit C++ strikt OOP entwickeln muss, nein, als Teil des Nachfolger-seins kann in C++ normal mit C entwickelt werden,
    das erinnert ein wenig an JSON kompatibilität in YAML.
    Doch, zu beachten ist, dass C-Programme die in C++ geschrieben wurden nicht automatisch auch C Programme sind; das hängt vom Compiler/Linker (dazu später mehr) ab.
    Das liegt daran, dass C++ code, dadurch das es einige Änderungen gibt anderen Output für gewisse Dinge generiert.

    Als Beispiel:
    C-Funktionen können nicht überladen werden, sie werden (je nach Implementation) mit ihrem Namen auch in die Ausführbare Datei so abgelegt. (mit möglichen Verzierungen, je nach Compiler)
    Das heißt, eine Funktion void myFunction(int) könnte ungefähr so übersetzt werden:

    Quellcode

    1. myFunction


    Während in C++ Parameter, Rückgabe-Type ect. Teil des Namens werden, was das überladen ermöglicht.
    Das nennt sich "name mangling". (keine Sorge, das passiert alles im Hintergrund und ist selten als Wichtig zu betrachten)
    Auch darauf wird in einem späteren Update noch näher eingegangen.
    Dieselbe Funktion von oben könnte nun so aussehen:

    Quellcode

    1. ?myFunction@@YAXH@Z

    Das wäre mal nur ein Beispiel von vielen, wenn man dies schon mal verstanden hat ist die halbe Miete dieser Sektion schon gewonnen.

    Aber kommen wir zurück zum Haupthema, wir sollten uns noch mal klar darüber werden für was C++ heutzutage überhaupt eingesetzt wird.
    Man könnte fast behaupten das C++ für so-ziemlich alles anwendbar ist: Spiele, Applikationen, Server, auch Embedded-Development
    und sogar im Web gibt es Anwendungsgebiete. (habe ich mir sagen lassen)
    Per-se lässt sich natürlich nicht sagen "eine Sprache ist effizienter als eine andere", es kommt immer darauf an wo etwas läuft; wie gut entwickelt wurde;
    was entwickelt wurde; aber dessen außer Acht gelassen ist es nicht sehr leicht an C++ heran zu treten. (über Java reden wir mal gar nicht)

    Mal als ein paar Beispiele von Anwendungen/Programmen die in C++ ganz oder teilweise entwickelt wurden:
    • Photoshop
    • Spotify Back-End
    • MySQL
    • Firefox
    • Windows
    • Unreal Engine
    Und noch massig andere Dinge, worunter natürlich auch viele Videospiele zählen.
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „Elanda“ ()

    1. Einführung

    1.4 Wie funktioniert C++ im Vergleich zu anderen Sprachen?


    Um euch auch wirklich bestens erklären zu können wie hier was funktioniert, werde ich eure Lieblingssprachen (wo kommt den hier der Plural her?) aus Demonstrationszwecken demontieren um auf diese Weise
    analytisch besser aufschlussreiche Vergleiche zu C++ herstellen zu können. Dafür nehme ich C# her, da ich erstens, nicht mehr wirklich viel von VB weiß und zweitens, C#'s Syntax auf der von C basiert. (daher kommt auch das C in C#)
    Aber keine Sorge für euch nicht-C# Entwickler (ihr kleinen Schlingel :p), ich werde den C++ part so einfach versuchen zu erläutern wie es mir möglich ist. Somit auch ohne die Vergleiche alles gut verständlich ist.
    Der Vergleich soll es nur ein wenig einfacher machen. (außerdem teilen C# und VB, soweit ich weiß, alles behind-the-code)

    Nun habe ich mir natürlich selbst eine Falle gestellt.
    Einerseits habe ich lange mit C# gearbeitet, andererseits immer nur sehr oberflächlich. Heißt, anders als mit C++ habe ich mir mit C# damals nicht die Mühe gemacht mehr über die Sprache die ich verwende zu lernen.
    Daher wird das hier etwas ulkig werden, aber here goes nothing. (Für euch ist das nicht weiter schlimm, wenn dieses Tutorial hoch geht bin ich ja mit dem schlau machen eh schon fertig)

    1.4.1 Compiler



    Fangen wir mit dem C#-Compiler an:
    Wenn man auf den schönen grünen Knopf drückt, welcher das Programm startet, wird dein Code durch den Compiler gefüttert.
    Glücklicherweise für die meisten von euch geschieht dies alles im Hintergrund.
    Visual Studio, grundsätzlich, führt den Compiler für euch aus, mit all den nötigen Argumenten.

    Ihr könnt es ja auch mal für euch selbst testen ohne Visual Studio zu öffnen.
    Unten angehängt habe ich für euch eine C# Datei mit der typischen "main" routine, lädt die runter, öffnet CLI und führt folgenden Befehl aus:

    Quellcode

    1. <Pfad zum Compiler> pfad/zur/datei/Test.cs

    <Pfad zum Compiler> denotiert hier, natürlich, den Pfad zum Compiler.
    Laut Docs ist das irgendwie was mit: C:\WINDOWS\Microsoft.NET\Framework\<Version>\csc.exe

    Hat die Kompilation geklappt sollte neben eurer Test.cs Datei eine neue Test.exe erscheinen, die könnt ihr in der CLI nun ausführen und solltet dort nun "Hello World" lesen können.
    Das heißt es hat geklappt!

    Was ist nun in dieser sogenannten exe?
    Nun, ich werde nicht zu weit ins Detail gehen, aber grob gesprochen:
    Csc.exe ist ein Compiler, das bedeutet, der Code wird in eine andere Sprache übersetzt und zusammengetragen. Im Falle von C# ist diese Sprache die sogenannte CIL. (Common Intermediate Language)
    Das ist sozusagen eine Zwischensprache die Assembly ähnelt, aber nicht genau Assembly ist. (man kann es eher mit Java bytecode vergleichen)
    Die CIL binary wird dann, während sie läuft, durch die CLR (Common Language Runtime) just-in-time (also on-demand) in Maschinencode übersetzt. Dies hat sowohl Vorteile als auch Nachteile.

    Ein Vorteil wäre, dass dadurch das der Code während der Laufzeit übersetzt wird, mehr Möglichkeiten für Optimisationen bereitstehen.
    Ein Nachteil ist natürlich das der Code während der Laufzeit übersetzt wird, was ein wenig an der Effizienz nagt da der Code ja erst übersetzt werden muss bevor die Instruktionen an die CPU überreicht werden können;
    ganz anders als bei Applikationen die ja schon im richtigen Format sind.

    Und wie sieht das für C++ aus?
    Nun, anders als der C# Compiler, kompiliert ein C++ Compiler den Code nicht in eine Zwischensprache, nein, ein C++ Compiler kann deinen Code in alles mögliche Kompilieren.
    Hier sind dem absolut keine Grenzen gesetzt - natürlich hängt das vom verwendeten Compiler ab.

    Nehmen wir mal G++ als konkretes Beispiel:
    G++, ein C++ Compiler der GNU Compiler Collection (kurz GCC), wird zuerst alles in Assembly umwandeln. (den Preprocessor ignoriere ich hier jetzt mal bewusst)
    Das ist eigentlich alles was der Compiler macht, jedoch gehört dazu auch noch, im Falle von G++, das es als Teil der Kompilation auch einen externen Assembler ausführt welcher die von G++ generierten Assembly Dateien in Maschinencode übersetzt. (in sogenannte Objektdateien)
    MSVC ist da anders, MSVC ist der standard Windows C++ Compiler. (theoretisch kann man G++ auch auf Windows verwenden (dafür gibt es MinGW), aber grundsätzlich ist dieser eher ein Linux-Ding)
    MSVC ist etwas intransparent in sachen Kompilation, aber, im Gegensatz zu G++, Kompiliert es deinen Code beinahe-direkt in Maschinensprache.
    (wer weiß was die kleinen Männchen-Inside alles tun; wenn es jemand genauer weiß, eine kurze Aufklärung von euch könnte dem Tutorial sicher gut tun)

    Achtung: Ich bitte hier zu unterscheiden zwischen C++ und C++/CLI, letzteres ist eine eigene Implementation von Microsoft (natürlich muss MS wieder mal sein eigenes Ding drehen (dieser Satz hat keinen Bezug zum 20. April)) die auch zu CIL übersetzt wird, wie C#.
    Diese Art von C++ werde ich aber in diesem Tutorial nicht durchnehmen, erstens, da sie unterschiedlich zu Standard-C++ ist, und zweitens, da ich niemals zuvor damit gearbeitet habe.

    Natürlich haben die meisten Compiler verschiedene Optionen zur Verfügung um verschiedene Dateien auszugeben. G++ hat zum Beispiel "-S", welche den Compiler durchläuft aber vor dem Assembler halt macht um die Assembly Dateien auszugeben.
    Für MSVC wäre das "/FA". Es gibt etliche Möglichkeiten und auch genau so viele Compiler. Es gibt auch C++ JIT Compiler, aber die brauchen wir hier nicht, wir fokussieren uns hier ausschließlich auf statische Compiler.
    Die drei wichtigsten hier wären MSVC, G++ und Clang, wobei wir im weiteren Verlauf wahrscheinlich nur auf MSVC eingehen werden. (wer weiß, ich habe ja nur bis hierher geschrieben, keiner weiß was da noch kommt)

    Nachdem wir nun unseren Code zu Objektdateien kompiliert and assembled haben, ist natürlich klar dass ja auch noch die exe-Datei irgendwo herkommen muss.
    Das ist aber nicht mehr die Aufgabe des Compilers sondern hier kommt der Linker ins Spiel.

    1.4.2 Linker



    Kurzer Disclaimer: C#-mäßig etwas aufgeschmissen hier, nach Einstündiger Suche bin ich zu dem Schluss gekommen, dass ich keine eindeutige Antwort erwarten konnte.
    Manche schreiben C# habe so etwas wie einen Linker nicht, andere schreiben doch da gibt es so etwas… naja.
    Daher tut es mir leid euch diesbezüglich enttäuschen zu müssen da ich hier keine Parallelen ziehen kann. (warum muss Microsoft immer alles so schwammig machen)
    Ich werde aber darauf achten extra Vorsichtig für C++ zu argumentieren.

    Der C++ Linker ist DER Teil der Kompilation, der dafür verantwortlich ist alle vom Compiler und Assembler generierten Objektdateien zu verknüpfen und in eine Ausführbare Datei zu packen.
    (eigentlich ist der Linker nicht Teil der Kompilation, da es aber im Zuge des ausführens des Compilers passiert, werde ich mal ein Auge zudrücken ;))

    Warum ist das Notwendig?
    Nun, C++ besteht in den meisten Fällen ja nicht nur aus einer Datei. Es gibt die sogenannten .cpp Dateien welche jeweils, unter normalen Umständen, ihre eigene Einheit erzeugen. (welche jeweils zu Objektdateien umgewandelt werden)
    Diese Dateien beinhalten deinen code, genau so wie es .cs Dateien für C# tun. Es gibt auch Header dateien die mit .hpp oder .h enden, aber dafür habe ich, für den späteren Verlauf dieses Tutorials eine eigene Sparte geplant.
    Jedenfalls, diese Source-Dateien haben möglicherweise Abhängigkeiten zueinander.
    (Source-Datei ist technisch gesehen etwas schwammig, da auch header Dateien Source-Dateien sind. Jedoch werden .cpp Dateien konventionell Source-Dateien genannt… ein ewiges Streitthema. When in doubt, einfach cpp-Datei sagen)

    Eine dieser Dateien definiert eine Funktion, die andere verweist auf jene. Dafür ist der Linker da um diese Abhängigkeiten zusammenzuführen.
    Der Linker kann auch eigene Optimisationen durchführen, wie das inlinen von Funktionen/Variablen oder sogar code-generierung um geschriebenen code noch weiter aufzuwerten.
    Wie all das aber genau funktioniert würde hier sowohl den Rahmen sprengen als auch mein Verständnis von Linkern überschreiten.
    Es ist auch kein notwendiges Wissen, es sei denn du schreibst deinen eigenen Compiler.

    Nachdem der Linker nun alle diese Abhängigkeiten aufgelöst hat, wird er alles in eine Kompakte ausführbare Datei zusammenführen.

    1.4.3 Memory-Management



    Ich glaube wir sind nun da angekommen was allgemein als der "gefürchtete Onkel" oder die "abschreckende Schwiegermama" bekannt ist.
    Das Memory-Management, oder zu deutsch Speicherverwaltung, ist das was viele nicht-C++ Entwickler nachts unter ihrem Bett erwarten wenn sie von C++ hören.

    Aber eigentlich gibt es dafür gar keinen Grund!!1!
    Das Hauptproblem hier ist das die meisten einfach so sehr davor bangt ohne es überhaupt je zuvor versucht zu haben, und wenn sie es dann doch tun, nicht mehr Wissen wieso sie eigentlich diese Ängste hatten.
    Aber zu eurem Glück können wir hier nun wieder Parallelen zu C# ziehen. (Auch hier keine Sorge für nicht-C# Entwickler, der C++-Teil ist auch ohne den Vergleich einfach Verständlich)

    In C#, genau wie auch in C++, wird der Speicher in Stack und Heap unterteilt. (Stapel und Haufen, es gibt auch noch den Statischen Speicher, aber hier nicht relevant)
    Für beide gilt, der Stack ist ein LIFO Konstrukt (Last In goes First Out), bedeutet, es werden Daten oben draufgelegt und auch von oben wieder entfernt - in genau dieser Reihenfolge.

    Der Heap ist mehr ein… ja, ein Haufen, im wahrsten Sinne des Wortes.
    Der Stack im allgemeinen hat den Vorteil schneller zu sein, da die Adressierung der Daten nicht mehr als nur eine Addition/Subtraktion ist.

    Man kann sich RAM als eine Art von Stadt vorstellen und ihre Bewohner als Daten wobei die Stadt ein Register besitzt welches alle Adressen und dessen Eigentüme als Liste aufführt.
    Wenn nun einer diese Eigentümer aus irgendwelchen Gründen erreichbar sein muss, kann man im Register nachsehen, die Adresse rausfinden und somit den Einwohner konfrontieren.
    So ähnlich ist das auch mit dem RAM, wenn man gewisse Daten im Speicher abrufen möchte, so tut man dies mit einer numerischen Adresse. (virtuelle Adressen im Normalfall)

    Und da hat der Stack einen Vorteil, die CPU besitzt in ihren Registern einen sogenannten Zahlenwert welcher die Adresse auf das oberste Paket im Stack repräsentiert. (für die die es interessiert, nennt sich “Stack Pointer” welcher abgekürzt auch als das x86-Register “SP” bekannt ist)
    Wenn nun Daten auf/vom Stack geschrieben/genommen werden, dann braucht diese Zahl nur addiert/subtrahiert werden und man hat die richtige Adresse.
    Dies funktioniert so lange wie das Programm weiß welche Größe Daten haben sollten. Da die größe bereits im Maschinencode der Ausführbaren Datei enthalten ist.

    Jedoch rasseln wir da in ein Problem: Was wenn es Daten gibt die wir noch nicht zu dem Zeitpunkt festmachen können an dem wir das Programm noch nicht ausgeführt haben?
    Oder aber was wenn diese Daten zu groß für den Stack sind oder diese Daten müssen zwischen Threads geteilt werden? (der Stack hat durschnittlich so um die 1 bis 4 mebibytes)
    Nun, das ist der Punkt an dem wir uns über den Heap unterhalten müssen.

    Der Heap hat nicht wirklich dieselbe Ordnung wie der Stack. (obwohl er schon in Speicherblöcken unterteilt werden kann)
    Wenn man etwas auf den Heap schreiben möchte, dann ist das leider nicht so einfach als nur eine Zahl zu verändern.
    Es muss ja auch zu allem Überfluss noch gesucht werden wo genug Platz für die angefragten Daten sind und auch noch weitere Sicherheitsmaßnahmen müssen durchgeführt werden.
    Hier kommt dann der Garbage Collector ins Spiel wie zum Beispiel .Net und Java ihn besitzen. (also, der JIT-Compiler und nicht die Sprache)

    Dieser berechnet wann und wie Daten auf den/vom Heap gelegt/gelöscht werden müssen. Wie dies geschieht ist von GC zu GC unterschiedlich, C# macht das mit sogenanntem “Reference Counting”,
    heißt, wenn niemand mehr Zugriff auf ein Datenpaket hat, wird für die “Löschung” markiert.
    Das heißt, der Entwickler braucht sich darum keine Sorgen mehr zu machen, da ja sowieso alles im Hintergrund geschieht.
    Jedoch sind beide Sprachen unterschiedlich in dem was auf jeweils dem Stack und den Heap landet. Da kommt auch der Effizienz-Unterschied ins Spiel.

    Nun, im folgenden Vergleich werde ich nur auf die Objekte eingehen, nicht auf andere Dinge wie Funktionsaufrufe oder Instruktion-Speicher und den ganzen anderen Quatsch.
    C#, grob gesagt, legt alles auf den Stack was eines der folgenden Typen hat (Werttypen):
    bool, byte, char, decimal, double, enum, float, int, long, sbyte, short, struct, uint, ulong, ushort

    und Verweise auf Verweistypen.

    Der Heap bekommt (Verweistypen):
    class, interface, delegate, object, string

    Offensichtlicherweise, wenn Werttypen teil eines Verweistypen sind, werden die natürlich auch auf den Heap gleich mit-gelegt, also lässt sich das natürlich nicht immer so pauschal sagen.

    C++ hingegen gibt dir die Wahl selbst was auf dem Heap und dem Stack landet und das ist was vielen Entwicklern anfänglich aufstößt.
    “Hallo?? Was redest du da… ich soll mich um den Heap/Stack kümmern? ...........Deine Mudda!” (Nachgestellte Szene)

    Während man es wahrscheinlich gewohnt ist Objekte mit "new" zu erzeugen, wird in C++ das Objekt ohne eben jenes erzeugt.- diese Daten landen dann auf dem Stack.
    Es gibt "new" aber auch in C++, damit werden schlussendlich dann dynamische Objekte erzeugt und im Heap untergebracht.

    Warum man das eine über das andere stellt?
    Nun, der Stack ist dem Heap, wenn möglich, immer vor-zu-ziehen, eben wegen den oben besprochenen Vorteilen.
    Der Heap wird für Dinge verwendet, welche Stack-Objekte leider nicht hinbekommen, und eines dieser Dinge ist die Speicherdauer.
    In C++ werden Stack-Objekte als "automatisch" bezeichnet, das liegt daran, dass diese Daten automatisch "gelöscht" werden wenn sie ihren Stack-Frame verlassen. (z.B. den Geltungsbereich einer Funktion)
    Dynamische Objekte müssen jedoch manuell gelöscht werden, dafür gibt es das keyword "delete" und daher können diese Objekte weit über ihren Stack-Frame zurückgegeben werden.
    Dazu kommt aber noch später mehr im Beitrag: "Pointer, Referenzen und dynamische Objekte". (langsam bekomme ich das Gefühl ich hätte das ganze etwas besser strukturieren sollen -_-)

    1.4.4 Versioning



    Auch noch ein wichtiger Punkt ist wie das Versioning in C++ funktioniert.
    Als C++ das erste mal rauskam gab es sowas wie einen "Standard" noch nicht, es war einfach nur ein "einfaches" Projekt von Bjarne Stroustrup um seiner Nachfrage selbst nach zu kommen.
    Jedoch, 1998 wurde die Sprache dann endgültig Standardisiert und wurde ein Teilnehmer des ISO-Standards.
    Mit der ersten Version unter dem Namen "C++98" fing der lang fortwährende Weg von C++ an zu leuchten.

    Seitdem ist alles was man in C++ findet, teil eines offiziellen Standards, das heißt, alles wurde klar definiert und kann auch nachgelesen werden:
    Standard: eel.is/c++draft/
    Dokumentation: de.cppreference.com/w/

    Der Beschluss über was und wie C++ verfügen soll wird vom C++-Committee durchgeführt, was man sich wie ein Gipfeltreffen voller Rocker vorstellen kann. (nur das diese Rocker eben Entwickler sind) ^^
    Die nächste Version, C++03 fügte dann einige neue Features und möglichkeiten hinzu, so wie es auch dann 8 Jahre später C++11 tat und alle folgenden Standards es bis heute tun.
    Ab der Version C++11 wurde dann ein regelmäßiger Zyklus eingeführt, das heißt alle 3 Jahre beschließen die Götter des C++-Olymps was neues kommt.
    Wenn wir diesen Zyklus verfolgen kommen wir auf eine Versions-Historie von: C++11, C++14, C++17 und C++20, gefolgt vom nächsten Standard C++23.

    Diese Information ist wichtig, da die neuen Features die eingebaut werden nicht Rückwärts-Kompatibel sind.
    Und um diese Sparte nun abzuschließen, werde ich euch auch noch verraten auf welche Version wir uns in diesem Tutorial fokussieren werden:
    C++17
    Dateien
    • Test.cs

      (121 Byte, 108 mal heruntergeladen, zuletzt: )
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Elanda“ ()

    2. Die Basics

    2 Die Basics


    Natürlich fangen wir nicht willkürlich einfach irgendwo an.
    Das wichtigste sind immer die Grundlagen, ohne Ausnahme. Daher werde ich euch in diesem Kapitel die Bausteine der Sprache vorlegen und es liegt ganz an euch diese zu verbauen.

    2.1 Umgebung und Setup


    Um unser Abenteuer überhaupt beginnen zu können, brauchen wir zuerst natürlich das passende Toolset.
    In C++, dies setzt sich normalerweise aus der IDE, dem Build-System und der Toolchain zusammen. (und in manchen fällen ein Projekt-Generator wie CMake)

    Es gibt massenweise Auswahl an IDEs die für C++ das bare-minimum bieten. Wir wollen aber das bare-maximum und glücklicherweise wird Windows-Benutzern da massiv unter die Arme gegriffen.
    Ihr dürft nämlich Visual Studio verwenden. (falls es wen juckt, ich verwende CLion, kostet aber was)
    Für die die es nicht haben oder kennen, die Community-Edition ist kostenlos.

    Visual Studio vereint die drei oben genannten Dinge alle in einem, somit habt ihr nicht mehr viel zu tun.

    2.1.1 Setup



    Hier ist die Schritt für Schritt Anleitung, von Installation bis zum ersten Projekt (für die die Visual Studio schon installiert haben, ihr könnt direkt zu Punkt 3 springen):
    1. Zuerst ist es wichtig Visual Studio zu downloaden, das könnt ihr hier tun.
      Das ist die Community Edition und frei verfügbar für Zwecke die ein gewisses Einkommen nicht überschreiten: Hier Downloaden
    2. Als nächsten Schritt installieren wir den Visual Studio Installer,
      dazu startet ihr das Setup und folgt der Anleitung bis der Installer installiert ist.
    3. Als nächstes startet ihr den Installer den ihr in eurem Startmenü finden könnt und klickt auf "Ändern" dann geht auf den Tab "Workloads".
      (ich bin mir nicht sicher ob auch auf deutsch “Workloads” da steht, da ich alles auf Englisch habe, es sollte aber im Zweifelsfall der erste Tab sein)


    4. Dort wählt ihr aus "Desktop development with C++" (oder "Desktopentwicklung mit C++") und das wichtigste ist dann auch schon ausgewählt,
      dann installiert ihr das und wartet bis die Installation fertig ist. (Das kann, je nach eurer Anbindung, etwas dauern)
    5. Ist die Installation fertig und habt Visual Studio nun geöffnet, geht auf "Neues Projekt erstellen" und sucht nach "Konsolen-App" mit zwei "+" im oberen rechten Eck.


    6. Nun gebt ihr eurem Projekt noch einen Namen und klickt auf "Erstellen".
    Das wars fürs erste, nun solltet ihr von einer Datei "ProjektName.cpp" begrüßt werden.

    "Aber was is nun mit dem Build-System und der Toolchain Elanda?"
    Sachte sachte, dazu komme ich ja jetzt. :)

    2.1.2 Build-System



    Ein Build-System ist das, was uns davor schützt selbst den Compiler ausführen zu müssen.
    Und ich sage hier "schützen" mit vollem Ernst und Selbstvertrauen, ihr wollt den Compiler wirklich nicht selbst aufrufen müssen, ich kann das gar nicht genug betonen, ihr wollt das einfach nicht - absolut nicht - weniger als gar nicht.
    Stellt euch "wenig" vor und jetzt multipliziert das mit 420 - so wenig und nicht anders… naja gut, außer es sind nur wenige Dateien, dann kann das schon eher Vorteilhaft sein als ein ganzes Build-System einzubinden
    Aber… wollt ihr mal ein Beispiel sehen wie ein Befehl aussehen könnte in einer Visual Studio standard Konsolen-Anwendung?

    Quellcode

    1. C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.31.31103\bin\HostX64\x64\CL.exe /c /ZI /JMC /nologo /W3 /WX- /diagnostics:column /sdl /Od /D _DEBUG /D _CONSOLE /D _UNICODE /D UNICODE /Gm- /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /permissive- /Fo"x64\Debug\\" /Fd"x64\Debug\vc143.pdb" /external:W3 /Gd /TP /FC /errorReport:prompt TestAnwendung.cpp


    Und im falle von MSVC für den Linker auch noch:

    Quellcode

    1. C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.31.31103\bin\HostX64\x64\link.exe /ERRORREPORT:PROMPT /OUT:"C:\Users\User\source\repos\TestAnwendung\x64\Debug\TestAnwendung.exe" /INCREMENTAL /ILK:"x64\Debug\TestAnwendung.ilk" /NOLOGO kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:"C:\Users\User\source\repos\TestAnwendung\x64\Debug\TestAnwendung.pdb" /SUBSYSTEM:CONSOLE /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"C:\Users\User\source\repos\TestAnwendung\x64\Debug\TestAnwendung.lib" /MACHINE:X64 /pdbthreads:2 x64\Debug\TestAnwendung.obj


    Tja haha, da guckt ihr was?
    Genau aus diesem Grund haben wir Build-Systeme, um zu abstrahieren und jeden Build deterministisch zu gestalten.
    Denn abgesehen von dem vorher genannten Punkt, sind diese Systeme auch dazu da, jeden Build gleich auszuführen.
    Das kann auf anderen Maschinen oder auch anderen Plattformen sehr wichtig sein. Vor allem Cross-Platform Build-Systeme händeln das native selbst und der Entwickler hat meistens nichts mehr damit zu tun.
    Lediglich ein Build-Skript ist vonnöten, welches dann vom Build-System gelesen wird und dann in die wichtigsten Teile eines builds aufgeteilt wird.
    Das Build-System übernimmt hier für dich das formen und ausführen des Compilers. (oder eher Toolchain, werdet ihr gleich verstehen)

    Für den Fall Visual Studio haben wir das Build-System MSBuild.
    Ihr solltet MSBuild-Skripte bereits kennen, sie sitzen tief in euren Herze… ach quatsch, die findet man in jedem eurer Projekte.
    Schon jemals gewundert was die "*.csproj", "*.vbproj", oder im Falle unseres neu erstellten Projektes, "*.vcxproj" Dateien repräsentieren?
    Das sind einfach nur MSBuild-Skripte, die beinhalten alles Wichtige wie die Dateien des Projektes, alle Abhängigkeiten oder auch Compiler-Dinge - die Liste ist endlos.
    Sagen wir einfach, dort findet man alles was man braucht um von eurem Code zu einer exe zu kommen.

    Wir haben doch gerade ein neues Projekt erstellt, nicht?
    Öffnet doch mal die "ProjektName.vcxproj" Datei, die sollte in etwa so aussehen:
    Spoiler anzeigen

    XML-Quellcode

    1. <?xml version="1.0" encoding="utf-8"?>
    2. <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    3. <ItemGroup Label="ProjectConfigurations">
    4. <ProjectConfiguration Include="Debug|Win32">
    5. <Configuration>Debug</Configuration>
    6. <Platform>Win32</Platform>
    7. </ProjectConfiguration>
    8. <ProjectConfiguration Include="Release|Win32">
    9. <Configuration>Release</Configuration>
    10. <Platform>Win32</Platform>
    11. </ProjectConfiguration>
    12. <ProjectConfiguration Include="Debug|x64">
    13. <Configuration>Debug</Configuration>
    14. <Platform>x64</Platform>
    15. </ProjectConfiguration>
    16. <ProjectConfiguration Include="Release|x64">
    17. <Configuration>Release</Configuration>
    18. <Platform>x64</Platform>
    19. </ProjectConfiguration>
    20. </ItemGroup>
    21. <PropertyGroup Label="Globals">
    22. <VCProjectVersion>16.0</VCProjectVersion>
    23. <Keyword>Win32Proj</Keyword>
    24. <ProjectGuid>{baaaed56-4c51-477f-a293-7a005c705ede}</ProjectGuid>
    25. <RootNamespace>TestAnwendung</RootNamespace>
    26. <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
    27. </PropertyGroup>
    28. <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
    29. <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
    30. <ConfigurationType>Application</ConfigurationType>
    31. <UseDebugLibraries>true</UseDebugLibraries>
    32. <PlatformToolset>v143</PlatformToolset>
    33. <CharacterSet>Unicode</CharacterSet>
    34. </PropertyGroup>
    35. <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
    36. <ConfigurationType>Application</ConfigurationType>
    37. <UseDebugLibraries>false</UseDebugLibraries>
    38. <PlatformToolset>v143</PlatformToolset>
    39. <WholeProgramOptimization>true</WholeProgramOptimization>
    40. <CharacterSet>Unicode</CharacterSet>
    41. </PropertyGroup>
    42. <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
    43. <ConfigurationType>Application</ConfigurationType>
    44. <UseDebugLibraries>true</UseDebugLibraries>
    45. <PlatformToolset>v143</PlatformToolset>
    46. <CharacterSet>Unicode</CharacterSet>
    47. </PropertyGroup>
    48. <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
    49. <ConfigurationType>Application</ConfigurationType>
    50. <UseDebugLibraries>false</UseDebugLibraries>
    51. <PlatformToolset>v143</PlatformToolset>
    52. <WholeProgramOptimization>true</WholeProgramOptimization>
    53. <CharacterSet>Unicode</CharacterSet>
    54. </PropertyGroup>
    55. <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
    56. <ImportGroup Label="ExtensionSettings">
    57. </ImportGroup>
    58. <ImportGroup Label="Shared">
    59. </ImportGroup>
    60. <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    61. <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
    62. </ImportGroup>
    63. <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    64. <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
    65. </ImportGroup>
    66. <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    67. <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
    68. </ImportGroup>
    69. <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    70. <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
    71. </ImportGroup>
    72. <PropertyGroup Label="UserMacros" />
    73. <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    74. <LinkIncremental>true</LinkIncremental>
    75. </PropertyGroup>
    76. <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    77. <LinkIncremental>false</LinkIncremental>
    78. </PropertyGroup>
    79. <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    80. <LinkIncremental>true</LinkIncremental>
    81. </PropertyGroup>
    82. <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    83. <LinkIncremental>false</LinkIncremental>
    84. </PropertyGroup>
    85. <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    86. <ClCompile>
    87. <WarningLevel>Level3</WarningLevel>
    88. <SDLCheck>true</SDLCheck>
    89. <PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
    90. <ConformanceMode>true</ConformanceMode>
    91. </ClCompile>
    92. <Link>
    93. <SubSystem>Console</SubSystem>
    94. <GenerateDebugInformation>true</GenerateDebugInformation>
    95. </Link>
    96. </ItemDefinitionGroup>
    97. <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    98. <ClCompile>
    99. <WarningLevel>Level3</WarningLevel>
    100. <FunctionLevelLinking>true</FunctionLevelLinking>
    101. <IntrinsicFunctions>true</IntrinsicFunctions>
    102. <SDLCheck>true</SDLCheck>
    103. <PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
    104. <ConformanceMode>true</ConformanceMode>
    105. </ClCompile>
    106. <Link>
    107. <SubSystem>Console</SubSystem>
    108. <EnableCOMDATFolding>true</EnableCOMDATFolding>
    109. <OptimizeReferences>true</OptimizeReferences>
    110. <GenerateDebugInformation>true</GenerateDebugInformation>
    111. </Link>
    112. </ItemDefinitionGroup>
    113. <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    114. <ClCompile>
    115. <WarningLevel>Level3</WarningLevel>
    116. <SDLCheck>true</SDLCheck>
    117. <PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
    118. <ConformanceMode>true</ConformanceMode>
    119. </ClCompile>
    120. <Link>
    121. <SubSystem>Console</SubSystem>
    122. <GenerateDebugInformation>true</GenerateDebugInformation>
    123. </Link>
    124. </ItemDefinitionGroup>
    125. <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    126. <ClCompile>
    127. <WarningLevel>Level3</WarningLevel>
    128. <FunctionLevelLinking>true</FunctionLevelLinking>
    129. <IntrinsicFunctions>true</IntrinsicFunctions>
    130. <SDLCheck>true</SDLCheck>
    131. <PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
    132. <ConformanceMode>true</ConformanceMode>
    133. </ClCompile>
    134. <Link>
    135. <SubSystem>Console</SubSystem>
    136. <EnableCOMDATFolding>true</EnableCOMDATFolding>
    137. <OptimizeReferences>true</OptimizeReferences>
    138. <GenerateDebugInformation>true</GenerateDebugInformation>
    139. </Link>
    140. </ItemDefinitionGroup>
    141. <ItemGroup>
    142. <ClCompile Include="TestAnwendung.cpp" />
    143. </ItemGroup>
    144. <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
    145. <ImportGroup Label="ExtensionTargets">
    146. </ImportGroup>
    147. </Project>

    (Na? Auch wenn diese Datei mehr beinhaltet, was ist für euch leserlicher: Die zwei Befehle oben oder das Build-Script?)

    Darin befinden sich alle elementaren Informationen über einen Build und dann gibt es auch noch die Targets.
    Ein Target ist sozusagen ein Cluster von Aktionen, sei es das löschen von Dateien, aufsetzen gewisser Dinge oder eben das Kompilieren des Programmes.
    Wie ihr oben im Project tag seht, steht da DefaultTargets.
    Das bedeutet, wenn MSBuild nicht gezwitschert wird welches Target wir ausführen wollen, dann wird das default-target ausgeführt. In diesem Fall ist dies Build, was ein von Microsoft integriertes Standard-Target ist das für den Build-Prozess verantwortlich ist.
    Dieses Target wird durch einen Import importiert:

    Quellcode

    1. <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />


    Dieses Target wird dann die gesamte Toolchain mit euren Dateien und Daten füttern und schlussendlich eine Ausführbare Datei ausspucken.

    Ihr könnt das ganze ja auch mal ohne Visual Studio testen - so wie beim Unterkapitel Compiler von oben.
    Geht in euren Ordner in welcher sich die “*.vcxproj” befindet und öffnet die CLI dort.
    Auch hier wieder einfach mit Befehlen auszuführen:
    ”C:\Program Files\Microsoft Visual Studio\<VS Version>\Community\Msbuild\Current\Bin\MSBuild.exe” .\NameDesProjektes.vcxproj

    Dann werdet ihr einige Meldungen bekommen und schlussendlich sollte in diesem Ordner ein neuer Ordner erscheinen mit dem Namen “Debug”, dort drin befindet sich nun die erzeugte exe.
    Visual Studio macht das nicht anders, im Grunde ist es nichts anderes als ein Texteditor mit Befehlsausgabe.

    Ihr fragt euch bestimmt:
    “Warum müssen wir das eigentlich Wissen? Wir lernen ja C++ und C# benützt doch dasselbe Build-System, wieso also jetzt?”

    Nun, es ist wichtig zu bedenken das C++ eine Sprache ist die mehr als nur in Visual Studio and somit auch auf Windows verwendet wird.
    Ihr habt vielleicht Glück mit Windows, aber wenn ihr diese Utensilien nicht zur Verfügung habt, werdet ihr aufgeschmissen sein.
    Daher dachte ich, dass ich euch das Konzept eines Build-Systems mal etwas näher bringe. Ich habe hier ja nur explizit über MSBuild geredet, es gibt tonnenweise verschiedene dieser Systeme.
    Für Linux, sehr bekannt ist GNU make, was eine etwas andere Herangehensweise erfordert.

    “Und was ist nun mit der Toolchain?”
    Ja ja, kommt ja schon… kommt schon…

    2.1.3 Toolchain



    Die Toolchain ist nun das was ich in diesem Tutorial schon öfters angeteasert habe; die Kollektion von Tools die deinen Code “wirklich” in die exe umwandelt.

    Wir haben ja letztes mal von “Compiler”, “Linker” und “Assembler” gesprochen und ich beschrieb es so als wäre es alles Teil des Compilers.
    Nun, das ist nicht ganz die Wahrheit. Wie auch schon gesagt, der Compiler tut wirklich nur das nach was er benannt wurde, er übersetzt deinen Code in eine weiter-verarbeitbare Form an Informationen; das kann Assembly sein oder auch nicht.
    Wenn wir also sagen: “Übersetze, Assemble und Linke den Code” dann ist das die Aufgabe der Toolchain.
    Also nichts anderes als eine Kette an Werkzeugen die zusammen arbeiten um das Produkt fertig zu stellen.
    Die Toolchain kann eine Handvoll Werkzeugen sein, am wichtigsten für uns sind aber die drei oben benannten und der Debugger.

    "Was ist ein Debugger?"
    Ein Debugger ist ein Werkzeug das uns erlaubt unser Programm zu betrachten während es läuft.
    Das bedeutet, man kann den Callstack sehen, den Speicher-Verlauf und vieles mehr.
    Debugger sind wichtig, da sie uns erlauben unsere Anwendungen in einem anderen Blickwinkel zu betrachten - behind the scenes sozusagen.
    Es erleichtert das lösen von Bugs ungemein. (daher auch de-bug-ger)

    Um dieses Kapitel auch schon wieder abzuschließen, sehen wir uns doch noch schnell einmal an aus was die MSVC-Toolchain besteht:
    • cl.exe: Der MSVC Compiler
    • link.exe: Der MSVC Linker
    • cdb.exe: Der MSVC Debugger (nur wenn man auch wirklich im Debug-Modus startet)
    • Etwaige Zusatz-Tools, zum Beispiel zum Resourcen einbetten, oder den Symbol-Export von Bibliotheken ect.
    • Die ganzen Windows und Runtime-Header und Bibliotheken
    Hier zur veranschaulichung:

    (Bildquelle: cjwind.idv.tw/Toolchain-on-windows-and-linux/)
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Elanda“ ()

    2. Die Basics

    2.2 Das obligatorische "Hello World"


    Es ist soweit, nun kommen wir endlich zu unserem ersten Code-Beispiel und damit zum Eintritt in die Welt von C++!

    Wir verfolgen nun das Beispiel das Visual Studio für uns generiert hat, das ist sozusagen die Eintritts-Datei.
    Wir können es mal kompilieren und sollten erwarten im Output “Hello World!” sehen zu dürfen.

    So sollte das hier ungefähr aussehen (für euch wahrscheinlich wieder auf Deutsch, sry dafür):

    C-Quellcode

    1. // TestAnwendung.cpp : This file contains the 'main' function. Program execution begins and ends there. [0]
    2. // [0]
    3. #include <iostream> // [1]
    4. int main() // [2]
    5. {
    6. std::cout << "Hello World!\n"; // [3]
    7. }
    8. // Run program: Ctrl + F5 or Debug > Start Without Debugging menu [0]
    9. // Debug program: F5 or Debug > Start Debugging menu [0]
    10. // Tips for Getting Started: [0]
    11. // 1. Use the Solution Explorer window to add/manage files [0]
    12. // 2. Use the Team Explorer window to connect to source control [0]
    13. // 3. Use the Output window to see build output and other messages [0]
    14. // 4. Use the Error List window to view errors [0]
    15. // 5. Go to Project > Add New Item to create new code files, or Project > Add Existing Item to add existing code files to the project [0]
    16. // 6. In the future, to open this project again, go to File > Open > Project and select the .sln file [0]

    (Die Kästchen mit den Zahlen im Code gehören nicht dazu, die habe natürlich ich dazu geschrieben und beschreibe damit eine Sektion hier darunter)

    Hier gibt es erstmal viel zu verdauen, aber ich versichere es euch erneut: Es ist nicht so kompliziert wie es aussehen möge.

    Als erstes gehen wir die Datei durch, der Name sollte so etwas wie "ProjektName.cpp" sein.
    cpp zeigt der IDE das es sich um eine CPP-Datei handelt, diese Dateien werden für den hauptsächlichen Code verwendet.
    Das sind die Dateien welche die Toolchain dann jeweils als Objektdateien ausgibt.
    Zum Beispiel für MSVC: ProjektName.cpp -> ProjektName.obj (wobei die ".obj" Datei die fertige Objektdatei ist)
    Diese Dateien stehen alle individuell für sich und müssen dann eben vom Linker gelinkt werden um die exe zu erzeugen.
    Wohingegen Header-Dateien (.h, .hpp) nicht kompiliert werden, sie werden nur als Teil einer .cpp Datei kompiliert, indem sie darin importiert werden.
    Dazu hilft euch [1] etwas weiter.

    Gehen wir jetzt erst einmal unseren Code von oben nach unten durch:
    [0]
    Die meißten vermögen dies zu wissen, aber alle Zeilen die hier mit “//” anfangen sind sogenannte Kommentare. Kommentare sind nicht Teil deines Codes und werden vollständig entfernt bevor der Präprozessor und Compiler in Aktion treten.
    Sie werden also nicht mit in die Anwendung geschrieben und dienen nur der Dokumentation deines Codes.

    Es gibt zwei Arten von Kommentaren, ein-zeilige sehen so aus:

    C-Quellcode

    1. // Ich bin ein ein-zeiliger Kommentar der nur für diese Zeile gilt
    2. Ich bin eine ungültige Code-Zeile und zerstöre gerade deinen Compiler


    Und mehr-zeilige Kommentare (auch Block-Kommentare genannt):

    C-Quellcode

    1. /*
    2. Ich bin ein Block-Kommentar.
    3. Jede Linie ist ein Teil von mir solange bis ich wieder ein sternchen und ein slash zu sehen
    4. bekomme.
    5. Ich bin im Übrigen auch poetisch:
    6. Rosen sind Rot, Veilchen sind Blau,
    7. du verstehst C++, das weis ich genau! ;)
    8. */

    Kommentare werden auch dazu verwendet um eine Dokumentation für dein Programm zu exportieren.
    Ein typisches Hilfswerkzeug hier ist Doxygen, welches dein gesamtes Projekt durchnimmt und anhand der geschriebenen Kommentare Dokumente erstellt, die dein Programm beschreiben.

    [1]
    Sehen wir uns nun diese Zeile einmal genauer an, hier gibt es bereits sehr viel zu verwerten:

    C-Quellcode

    1. #include <iostream>


    Dies wird eine “Präprozessor-Direktive” genannt, diese zeichnen sich durch das beginnende “#” Zeichen aus.
    Wichtig zu verstehen ist das all diese Direktiven vor dem Kompilieren ersetzt werden, das heißt,
    sobald der Compiler deinen Code sieht, gibt es diese nicht mehr - da sie mit Code ausgetauscht werden.

    Der Präprozessor läuft vor dem Compiler und erzeugt somit eine TU (Translation Unit) aus deiner Source-Datei, welche nun bereit dazu ist, endgültig übersetzt zu werden.
    Die include direktive ist hier somit ein Import von einer anderen Datei.

    Das bedeutet, sieht der Präprozessor eine solche Direktive, wird er die Datei suchen und den GESAMTEN Code dieser Datei mit der “#include” Direktive ersetzen.
    Stell euch das mal kurz vor was für gewaltige Dateien dabei entstehen könnten:
    “Datei 1 inkludiert Datei 2 inkludiert Datei 3”, und so weiter. Der gesamte Inhalt dieser Dateien landet somit in der letzten Datei die all diese direkt und indirekt inkludiert.
    Das kann schon eine Menge sein!

    Da aber nun der Code anderer Dateien in einer Datei verfügbar ist, kann man die, in den anderen Dateien definierten Objekte; Variablen; Funktionen ect. verwenden.
    In diesem Fall ist das der iostream Header.

    Achtung, wichtig zu wissen, normalerweise haben Header die Dateiendung “.h” oder “.hpp”. Aber bei C++-Standard Headern ist dem nicht so,
    das liegt daran das C-Header mit “.h” enden und man somit unterscheiden kann von welcher Sprache man die Header inkludiert.
    Was diese Datei nun aber inkludiert, sehen wir gleich weiter unten.

    Noch ein kleiner Hinweis, es gibt zwei verschiedeneWege includes zu schreiben:
    Da wäre einmal #include <Pfad/Zum/Header> und auch noch #include "Pfad/Zum/Header".
    Die erste Version wird hauptsächlich für Standard-Header verwendet und zeigt dem Compiler das er zuerst an den Pfaden suchen soll die dem Compiler übermittelt worden.
    Die zweite Version zeigt dem Compiler das er im selben Ordner wie die inkludierende Datei anfängt zu suchen.

    [2]
    Das hier

    C-Quellcode

    1. int main()

    ist eine Funktionssignatur.

    Was die einzelnen Teile bedeuten erfahrt ihr Später im Kapitel “Funktionen”.
    Was aber dennoch wichtig zu wissen ist, dass die Funktion mit dem Namen main die eine Funktion ist, die wenn das Programm gestartet wird der erste Aufruf ist.
    Das heißt, du startest ein Programm und main ist der Eintrittspunkt, alles was außerhalb von main geschieht
    und nicht in main aufgerufen wird ist nicht zwingend Teil des Programmablaufes.

    Hier gibt es verschiedene Flavours:

    C-Quellcode

    1. int main()

    und

    C-Quellcode

    1. int main(int argc, char *argv[])


    Ersteres gibt keine Argumente wieder, die im Kommandozeilen-Befehl des startens der Anwendung in der Kommandozeile übergeben wurden.
    Bedeutet, man hat keinen Zugriff darauf, die zweite Version tut das aber schon.

    Des weiteren ist main dazu verpflichtet int zurückzugeben.
    Zurückgeben bedeutet, sobald die Funktion zu Ende ist, gibt dir die Funktion einen Wert.

    int ist ein Datentyp welcher einen Ganzzahlwert repräsentiert der in diesem Fall an die ausführende Instanz zurückgegeben wird.
    Hier ist auch wieder wichtig zu beachten, ein Wert von 0 bedeutet das Programm ist ohne jegliche Fehler zu Ende gelaufen,
    jeder andere Wert bedeutet die Anwendung ist “abnormal” geendet. (es gibt hier keinen Standard welcher Wert was bedeutet, lediglich Konventionen)

    Übrigens, main ist die einzige Funktion welcher es erlaubt ist das return wegzulassen, in diesem Fall wird 0 zurückgegeben.
    Es schadet aber nicht explizit zu sein, daher würde ich empfehlen es trotzdem mit-reinzunehmen.

    [3]
    Nun kommen wir auch schon zum komplexesten Teil der Anwendung (dies wird durch “iostream” oben inkludiert):

    C-Quellcode

    1. std::cout << "Hello World!\n";


    Dies ist das Pendant zu C#’s Console.WriteLine("Hello World!"), mit diesem Statement sendet man etwas an die Konsole das dann ausgegeben wird.

    Nehmen wir es mal individuell durch, der Syntax möge etwas konfus sein.
    Zuerst haben wir hier std, dies ist der namespace der Standard-Bibliothek, bedeutet, alle zuvor importierten Funktionen; Typen ect. welche sich im standard namespace befinden, können so aufgegriffen werden.
    Der doppelte Doppelpunkt ist in C++ das, was in C# der punkt ist.
    Aber nur im Falle von Namespaces und Statischen-Member-Zugriffen, wenn man auf Klassen-Instanz-Objekte zugreifen will wird das auch mit einem Punkt gelöst.

    Das << ist der Left-Shift oder auch der Streaming-Operator.
    C++ erlaubt es Operatoren zu überladen, das heißt, für bestimmte Typen kann man die Funktion von Operatoren verändern.
    In diesem Fall wurde der << Operator für std::cout überladen, um auf diesem Weg Objekte an std::cout weiterzugeben.

    Auf diesem Wege kann man auch Nachrichten formattieren, z.B.:

    C-Quellcode

    1. std::cout << “Das hier ist eine Zahl:<< 666 << “\n”;

    Dies wird dann in der Konsole, bzw. im Output-Fenster als Das hier ist eine Zahl: 666 ausgegeben.

    Dann haben wir noch:

    C-Quellcode

    1. “Hello World!\n”

    Das ist ein sogenanntes String-Literal.
    String-Literale sind unveränderbare Zeichenketten, das heißt dieser String bleibt für immer so wie er ist.
    Das liegt daran das dieser String direkt in der exe-Datei landet, man könnte rein-theoretisch die exe Datei durchsuchen und irgendwo müsste es in Binärformat zu finden sein.
    Natürlich kann man String-Literale auch für dynamische Strings verwenden, aber das ist für uns jetzt noch nicht wichtig.
    Wichtig ist nur das alles zwischen “ als ein String-Literal interpretiert wird.

    Wir können natürlich diese String-Literale auch in einer Variable speichern:

    C-Quellcode

    1. const char *mein_string = “Hallo, bin ‘n String!!1!”;


    Wie ihr hier schon const sehen könnt, bedeutet das, dass der String nicht mehr verändert werden kann.
    const bedeutet so viel wie: “Diese Variable ist nicht veränderbar”
    Wenn ihr das const vergesst, wird euch der Compiler Warnen das dies Illegal ist, da String-Literale ja nicht veränderbar sind und dass kann,
    sollte euch der Compiler das überhaupt erlauben, zu schwer verfolgbaren Bugs führen.

    Das char ist ein Datentyp und steht für "character", ein Character ist also auf gut deutsch ein Buchstabe oder jegliches andere "Text-Zeichen".
    (Hier kann es etwas tricky werde, da "char" nur ASCII repräsentieren kann)
    Dieser Datentyp repräsentiert sowohl C#'s char als auch byte, da char nichts anderes als ein Ganzzahlenwert ist der maximal 1 Byte groß ist.
    Das Sternchen dahinter ist ein "Pointer" oder auf deutsch "Zeiger".
    Wir vertiefen es jetzt nicht, aber im Grunde bedeutet das hier das diese Variable auf ein Array zeigt.
    (für die Verwirrung die jetzt warscheinlich aufkommen wird, nicht jeder Zeiger ist ein Array, nur in diesem und ein paar anderen Fällen)

    Zum Schluss, für die VBler unter euch, ist da dann noch ;.
    Dies ist ein sogenanntes Semikolon, dieses beendet ein Statement.
    Während in VB eine neue Zeile das Statement beendet, tut das hier das Semikolon, daher dürft ihr das niemals vergessen - WICHTIG!
    Dies erlaubt es uns mehrere Statements in einer Zeile auszuführen, aber keine Sorge, normalerweise würde das sowieso niemand tun oder nur in seltenen Umständen.
    Einfach da es den Code nur schwerer zu lesen macht und Lesbarkeit die oberste Regel in einem jeden C++ Programm ist.

    Das ist auch wieder eines dieser ewigen Streitthemen, aber tatsächlich 50/50. Die eine Hälfte sagt Effizienz ist wichtiger, die andere sagt die Lesbarkeit deines Codes und ich gehöre zu letzteres.
    Es gibt hier keine definitive Antwort und jeder hat ein Recht zu seiner Meinung, daher haben beide Gruppen recht.

    Nun das ist nun mal alles für diese Kapitel, ich hoffe es hat euch ein wenig weiter geholfen und werden uns dann beim nächsten Kapitel “Variablen” wiedersehen!

    Viel Spaß euch noch <3
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „Elanda“ ()

    2. Die Basics

    2.3 Header, Sources und TUs


    Ich habe beschlossen, dass ich diesen Part vielleicht doch besser jetzt schon durchnehmen möchte, da ich die nächsten Teile teilweise an die Bindung zwischen Source-Dateien
    und Header anlehnen werde und es gibt auch Verbindungen die ich mit diesem Vorwissen besser erklären kann.

    Es gibt seit C++20 auch "Modules", die von dem, was ich gesehen habe, den JS Modules stark ähneln, jedoch habe ich bis jetzt noch nicht mit jenen gearbeitet,
    da nicht alle Compiler Standardmäßig mit diesen Kompatibel sind.
    Solche Features können manchmal Jahre brauchen damit jeder Compiler mit ihnen zurechtkommt, trotz der Tatsache das sie schon offiziell released wurden.
    Dies ist auch der Grund wieso wir hier C++17 und nicht C++20 durchnehmen, da an manchen stellen die Kompatibilität noch etwas Müll sein könnte.
    (obwohl ich gelesen habe das MSVC bereits alles implementiert haben soll… naja)
    Wenn euch Modules dennoch interessieren, dann könnt ihr nach diesem Beitrag ja mal hier vorbeischauen:
    docs.microsoft.com/de-de/cpp/c…modules-cpp?view=msvc-170
    (ich rate euch aber das wirklich nach diesem Kapitel erst zu tun, da ihr möglicherweise etwas Verwirrung davon tragen könntet)

    Nun, stellenweise haben wir Header und Source-Dateien eigentlich schon durchgemacht, daher wird dies im Großen und ganzen ein Recap.
    Aber es kommen auch neue Informationen dazu, daher macht euch deswegen keine Sorgen, es ist nicht vollkommen umsonst.

    Sources
    Sources sind eure Hauptcode Dateien, alles was darin steht wird in Objektdateien übersetzt.
    Nehmen wir mal unser Hello-World Beispiel, der Weg ist folgender:
    Zuerst werden alle Kommentare gelöscht, nur noch der Code bleibt hinter.
    Als nächstes wird der Präprozessor alle Direktiven mit Code für den Kompiliervorgang ersetzen, für unser Beispiel ist das nur das #include <iostream>.
    Nach diesem Vorgang ist unsere Source eine sogenannte TU.
    Eine TU ist wie unsere Source, nur dass es keine Präprozessor Direktiven mehr gibt - was, da sie nun bereit zum übersetzen ist, ihr den Namen (TU) Translation-Unit gibt,
    der deutsche Name erklärt das Prinzip vielleicht besser: Übersetzungseinheit.
    Diese wird nun durch einen Teil der Toolchain geschleudert und den Rest kennt ihr ja, *ratter* *ratter* BAMM -> Objektdatei.

    Doch ein Programm besteht oftmals nicht nur aus Sources.
    Vor Allem, wenn diese Abhängigkeiten zueinander haben, wie als, zwei Source Dateien brauchen zugriff auf die gleiche Funktion oder sogar eine Klasse oder eine Variable,
    dann brauchen wir einen Weg um diese zu Teilen.
    Man kann theoretisch, Funktionen und Variablen über zwei Source-Dateien teilen, da der Linker, wenn richtig gemacht,
    diese ja eh zusammenführt - allerdings ist das mühselig und bricht unter Umständen, wenn man nicht aufpasst, eine Elementare Regel der Sprache: ODR
    ODR ist kurz für “One Definition Rule” und bedeutet das jede Entität nur einmal definiert werden darf, was logisch ist,
    denn woher soll der Linker wissen welche Funktion er nehmen soll wenn sie zweimal in verschiedenen Sources definiert wurde?
    Sollte dies der Fall sein, kann alles mögliche passieren, es ist nicht definiert und endet in UB.
    (Undefined Behaviour - undefiniertes Verhalten das nicht im Standard festgelegt wurde und UB sollte immer vermieden werden)

    Wir wissen ja mittlerweile das manche Dinge zwischen Sources geteilt werden wenn sie nicht explizit davon abgehalten werden.
    Und genau aus diesem Grund gibt es dann die:

    Header
    Header Dateien sind die Dateien die wir mittels #include “pfad/zum/header” in eine Source oder auch in andere Header inkludieren können.
    Wie auch schon erwähnt, bedeutet das, dass der gesamte Code des Headers direkt in die Source kopiert wird mit allen anderen Header includes die der Header selbst inkludiert.

    Doch nun stellt sich die Frage wofür wir diese Header denn überhaupt brauchen?
    Na ganz einfach, oben haben wir es schon angesprochen, um Dinge zwischen Sources zu teilen.
    Wenn eine Source einen Header inkludiert der zum Beispiel die Funktion “addition” deklariert, dann weiß unsere Source-Datei das in irgendeiner anderen Source Datei, diese Funktion definiert wurde.
    Der Header hier aber definiert die Funktion nicht, sondern gibt der Source nur den Anhaltspunkt DAS die Funktion irgendwo anders existiert, das trifft genauso auf Variablen zu.

    Es gibt auch hier wie für alles wieder Ausnahmen, da man Entitäten auch im Header definieren kann, da gibt es dann wieder extra Regeln die man beachten muss, standardmäßig aber werden diese Dinge aufgeteilt.

    Jetzt kommt aber noch etwas anderes wichtiges das wir beachten sollten. Denn es kann passieren, dass wenn wir Header zwei-mal inkludieren, der Code zwei-mal eingesetzt wird.
    Es gibt da keinen automatischen Schutz der uns davor bewahrt.
    Doch wir haben zwei Möglichkeiten dies zu verhindern!

    Möglichkeit 1: Include Guards
    Include Guards kommen noch aus der C Zeit, wahrscheinlich wird euch das Konstrukt jetzt noch nichts bedeuten da wir noch nicht über Makros gesprochen haben, aber ich zeige es euch trotzdem mal.
    Wir haben hier einen Header “Beispiel.h”:

    C-Quellcode

    1. #ifndef H_BEISPIEL
    2. #define H_BEISPIEL
    3. // Der gesamte Code des Headers
    4. #endif

    Was hier passiert ist, das wenn das Makro H_BEISPIEL nicht existiert, wird es mit #define H_BEISPIEL definiert und alles andere zwischen #ifndef und endif wird auch mit reingenommen.
    Das heißt, sollten wir diesen Header versehentlich ein zweites mal inkludieren, wird der Präprozessor sehen das H_BEISPIEL bereits definiert wurde und überspringt das Ganze beim zweiten mal.
    Eine Regel für die Benennung gibt es zwar nicht, allerdings gibt es Konventionen.

    Meine Konvention der Wahl wäre hier die von Google:
    <PROJEKT NAME>_<PFAD>_<DATEINAME>_H_


    Möglichkeit 2: #pragma once
    #pragma once ist ein anderer Weg doppelte Inklusionen zu vermeiden. Ich bevorzuge diesen Weg da es nicht diese klobigen C-Include-Guards benötigt.
    Jedoch ist wichtig zu wissen das dies NICHT Standard-C++ ist - was aber nicht bedeutet das man es nicht verwenden sollte,
    denn trotz seines fehlens im Standard sollte es heutzutage jeder für uns wichtige Compiler unterstützen.
    Es ist eher selten einen Compiler anzutreffen der es nicht unterstützt, daher kann ich dies wärmstens empfehlen.

    Wenn ihr aber die include-guards bevorzugt, dann ist das halt so, seid aber gewarnt dass ihr aufpassen müsst das jeder Header seinen Include-Guard anders bezeichnet.
    Hier haben wir H_BEISPIEL, würde ich einem anderen Header denselben Namen geben,
    würde er möglicherweise nicht mehr inkludiert werden können sollte der andere Header mit demselben Include-Guard zuvor inkludiert worden sein.
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    2. Die Basics

    2.4 Variablen


    Jedes Programm mit mehr als nur nichts besitzt Variablen.
    Ohne diese wären arbeitende Anwendungen nicht lauffähig, so auch keine C++-Applikation.
    Ich gehe mal davon aus, dass ihr wisst was Variablen tun, aber ich werde es dennoch kurz erläutern, als kleines Refreshment:
    Variablen sind veränderbare Daten! Perfekt.

    Erklärung
    In C++, Variablen sind Objekte.
    Die Definition eines Objektes ist alles das eine Größe, eine Lebenszeit, einen Typen, eine Speicherausrichtung (Alignment) und einen Wert besitzt - optional dazu einen Namen.
    Gehen wir diese Dinge mal durch:
    • Größe:
      Dies ist die Größe eines Objektes in Byte, als Beispiel, eine Variable mit Typ char hat einen Byte und das ist Fakt.
      Bedeutet, auf Maschinen auf welchen ein Byte 8 Bit hat, kann ein char 255 verschiedene Werte annehmen. (plus den Wert 0, also 256)
    • Lebenszeit:
      Dies hängt von der sogenannten Speicherdauer ab (storage duration), die selbst in vier Unterkategorien unterteilt werden kann:
      • Automatisch: Das Objekt wird erzeugt wenn es definiert wird und zerstört wenn es seinen Stack-Frame verlässt (Geltungsbereich (Scope) einer Funktion,
        ein Stack Frame beginnt mit "{" und endet mit "}" einer Funktion, man kann die Geltungsbereiche auch einengen, dazu gleich mehr)
      • Statisch: Das Objekt wird erzeugt wenn das Programm startet und zerstört wenn das Programm endet
      • Dynamisch: Objekte werden mit new erzeugt und mit delete gelöscht, dynamische Objekte landen auf dem Heap-Speicher
      • Thread-Lokal: Diese Variablen werden erzeugt wenn der jeweilige Thread startet und zerstört wenn dieser endet
    • Typ:
      Die Art der Daten des Objektes, z.B. int speichert Ganzzahlwerte, double speichert Fließkommazahlen oder,
      auch Klassen die jegliche Daten speichern oder auch keine.
    • Speicherausrichtung:
      Etwas kompliziert und nicht sehr wichtig für uns hier, aber dennoch kurz erklärt - Jedes Objekt ist ausgerichtet im RAM,
      das heißt, ein Objekt mit bestimmter Größe kann nur an einer Adresse gespeichert werden die ein Vielfaches der Größe des Objektes ist,
      beispielsweise: char mit einer Größe von 1-Byte kann an jeder Addresse gespeichert werden;
      ein Objekt mit Größe 8-Byte kann nur an Adressen gespeichert werden die durch 8 geradeaus Teilbar sind (0x00, 0x08, 0x0F ect.).
      Für benutzerdefinierte Typen (z.B. Klassen) ist das etwas komplizierter.
    • Wert:
      Sollte selbsterklärend sein, der Inhalt des Objektes
    • Name:
      Der Name des Objektes, nicht jedes Objekt braucht einen Namen - Variablen müssen diese aber besitzen.
      Groß-/Kleinschreibung ist zu beachten, "var" ist nicht dasselbe Objekt wie "Var".

    Definition

    Die Definition einer Variable ist recht einfach:

    C-Quellcode

    1. Typ name = [Wert];


    Jedoch ist das auch nur die Basis und je nachdem wo diese aufzufinden ist, wird mehr nötig sein als nur das.
    Variablendefinitionen können wir überall finden, aber es ist unterschiedlich zu betrachten wie diese definiert werden müssen,
    da viele Faktoren wie Lebenszeit; Speicherdauer; Qualifikation und Verlinkung eine große Rolle spielen.

    Sehen wir uns zuerst lokale Variablen an - oder anders gesagt, Variablen in Funktionen.

    Lokale Variablen
    Variablen in Funktionen sind, semantisch gesehen, Deklaration und Definition zugleich.
    Nehmen wir unsere main Funktion von unseren "Hello World" einmal her und verändern sie dementsprechen:

    C-Quellcode

    1. int main()
    2. {
    3. int ganzzahlVariable = 0;
    4. }

    Mit diesem Beispiel haben wir nun eine automatische Variable mit dem Namen ganzzahlVariable definiert. (nochmals, automatisch bedeutet sie landet auf dem Stack)
    Diese Variable kann Ganzzahlen-Werte beinhalten die den Maximal- und Minimalwert des Typen int nicht überschreiten.
    Die Variable wird erzeugt in der Zeile in welcher sie definiert wird und zerstört sobald sie ihren Geltungsbereich verlässt, welcher endet sobald “}” erreicht wurde.
    Danach existiert die Variable nicht mehr und von außerhalb kann nicht mehr auf sie zugegriffen werden.

    Wir könnten innerhalb dieser Funktion noch einen eigenen Geltungsbereich erzeugen, indem wir das hier machen:

    C-Quellcode

    1. int main()
    2. {
    3. {
    4. int ganzzahlVariable = 0;
    5. }
    6. }

    Damit ist die Lebensdauer der Variable nochmals verkürzt auf das innerste Klammern-Paar.
    Somit können wir uns notieren, automatische Variablen gelten immer nur für den innersten Bereich von zwei Klammern in dem sie definiert wurden.

    Also anders formuliert:

    C-Quellcode

    1. int main()
    2. { // Beginn des Bereiches [1]
    3. { // Beginn des Bereiches [2]
    4. int ganzzahlVariable = 0;
    5. {// Beginn des Bereiches [3]
    6. } // Ende des Bereiches [3]
    7. } // Ende des Bereiches [2]
    8. } // Ende des Bereiches [1]

    Die Variable existiert hier also nur für die Bereiche [2] und [3], und wird zerstört wenn der Bereich [2] sein Ende findet, Bereich [1] weis nichts mehr von unserer Variable.
    Das sind Lokale Variablen, sie gelten also immer nur für ihr Bereich welcher mit “{}” gekennzeichnet wird.

    Um auf diese Variable zugreifen zu können, müssen wir nur ihren Namen verwenden, wir können nun also folgendes tun:

    C-Quellcode

    1. int main()
    2. {
    3. int ganzzahlVariable = 69;
    4. std::cout << “Meine erste Lokale Variable:<< ganzzahlVariable << “\n”;
    5. }

    Sobald das Programm nun läuft sollten wir folgendes lesen können:
    Meine erste Lokale Variable: 69

    So werden Variablen verwendet.

    Dann gibt es aber noch globale Variablen, hier wirds jetzt etwas heftig, also seid achtsam und notiert fleißig mit.

    Globale Variablen
    Eine globale Variable ist dann global, wenn sie außerhalb von Funktionen und Klassen definiert wird. Das nennt sich auch namespace-scope.
    Globale Variablen haben ihren eigenen Speicher, der sogenannte statische Speicher.

    Wird eine Variable global gemacht, existiert sie vom Start bis zum Ende des Programms.
    Jeder der diese Variable sehen kann, hat auch auf sie Zugriff!

    Lasst uns doch einfach einmal eine globale Variable definieren:

    C-Quellcode

    1. int globaleGanzzahlVariable = 420;
    2. int main()
    3. {
    4. std::cout << “Meine erste Lokale Variable:<< globaleGanzzahlVariable << “\n”;
    5. }

    Der Unterschied hier ist das diese Variable nicht nur während main existiert, sondern solange bis das Programm geendet hat.
    Nun laufen wir aber in ein Problem: Sollte eine andere Source dieselbe Variable globaleGanzzahlVariable definiert haben, bekommen wir einen ODR verstoß.
    Es gilt dasselbe Prinzip: Woher soll der Linker wissen welche definition jetzt genommen wird, da beide Sources im Linking-Stadium zusammengetragen werden?

    Das Problem was hier besteht ist das diese globale Variable “extern” verknüpft wurde.
    Es gibt zwei verschieden Arten von Verknüpfungen (Linkage):
    • External:
      Diese Entitäten werden zwischen allen Sources geteilt die dein Programm besitzt.
      Hast du 100 Sources, hast du 100 Sources die die gleiche Variable besitzen. (wenn sie in allen 100 Sources inkludiert bzw. definiert werden)
      Definieren zwei oder mehr Sources diese Variable, weiß der Linker nicht welche nun “die Eine” ist.
    • Internal:
      Diese Entitäten werden nicht zwischen Sources geteilt, jede Source hat ihre eigene Kopie.
      Zum Beispiel, beide Sources haben eine interne Variable globaleGanzzahlVariable, aber jede ihre eigene.
      Eine Änderung in einer Source verändert nicht die intern verknüpfte globale Variable der anderen Source.
    Doch wie kontrollieren wir wie globale Variablen, oder auch Funktionen, intern/extern verknüpft werden?
    Dazu haben wir zum einen das Keyword static.
    Anders als in anderen Sprachen hat dieses Keyword mehrdeutige Verwendungen.
    (wir betrachten mal jetzt die Bedeutung außerhalb von Klassen und Funktionen)

    static bedeutet, dass jene Entität “intern” verknüpft wird.
    Also nur für die Source in welcher sie definiert wurde, denn ohne dieses Keyword ist alles automatisch “extern” verknüpft.

    Haben wir nun folgendes:

    C-Quellcode

    1. static int globaleGanzzahlVariable = 420;

    Kann eine andere Source nicht mehr mitreden, da diese Variable nun “intern” Verknüpft ist.

    Warum dafür static gewählt wurde und nicht beispielsweise ein extra Keyword intern (welches nicht existiert), weis ich nicht,
    aber es ist nunmal so und bringt häufig Verwirrung für Personen die dieses Keyword aus anderen Sprachen kennen.
    Somit ist eine der mehrdeutigen Bedeutung von static, dass es die Art wie etwas Verknüpft ist steuert.

    Aber Achtung,
    wird eine globale Variable in einem Header definiert, ohne static, dann bedeutet dies das jede Source die diesen Header inkludiert alle diese Variable definieren und so kommen wir wieder zu dem Problem ODR.
    Ist das Keyword im Header higegen Präsent, so bekommt jede Source die diesen Header inkludiert ihre eigene Kopie dieser Variable.
    Das ist aber oft nicht wünschenswert da wir meistens nur eine globale Variable über mehrere Sourcen brauchen, heißt, sie sollte extern verknüpft werden.
    Da wissen wir aber schon, dass wir das ODR Problem haben, wie lösen wir das also?

    Dafür gibt es nun zwei elementare Keywords, einmal extern und einmal inline. (bitte beachten, vor C++17 hatte inline eine andere Bedeutung)
    Ersteres zeigt C++ das wir eine Variablendeklaration haben die extern verknüpft werden soll.
    Da globale Variablen ja automatisch extern verknüpft werden brauchen wir es hier also nur dafür um zu zeigen das es eine Deklaration und keine Definition ist.

    Der unterschied zwischen den beiden ist das die Definition die Variable erzeugt und die Deklaration nur zeigt das sie irgendwo existiert.
    Es ist also in etwa wie ein einfacher Indikator für die Definition.

    Eine Deklaration besitzt nur den Typen, den Namen und etwaigen Zusatz, nicht aber einen Wert. (oder im Fall von Funktionen noch die Parameter aber ohne Funktionskörper)
    Das Problem bei Variablen aber ist, dass in manchen Fällen eine Definition auch ohne Wert definiert werden kann, das trifft auf Variablen zu die default-instanziierbar sind.
    Wird kein Wert angegeben, bekommt die Variable also den default-Wert.
    Im falle von Integral und Floating-Point typen ist das z.B. 0. Bei Klassen ist es der default-Konstruktor. (achtung, Lokale Variablen die als Typ nicht eine Klasse haben werden nicht default-instanziert, wird kein Wert an der Definition angegeben sind sie uninitialisiert und können allen möglichen Müll beinhalten)
    Daher müssen wir mittels extern, obwohl sie schon extern ist, zeigen das es sich um eine Deklaration handelt.
    Versucht man ihr einen Wert zuzuweisen, bekommt man entweder eine Warnung oder einen Error das man einer Deklaration keinen Wert zuweisen sollte.

    Das sieht ungefähr so aus:

    C-Quellcode

    1. extern int globaleGanzzahlVariable; // Hier keine Wertzuweisung!

    Somit weis der Linker nun: “Aha, das ist ein Tipp um mir zu zeigen diese Variable existiert wo anders.”
    Aber natürlich existiert diese Variable noch nirgendwo, wir haben nur eine Information erzeugt nicht aber die Variable.
    Um dieser Deklaration nun einen Wert zu geben, müssen wir diese Variable nun irgendwo definieren. (natürlich ohne extern, sonst wäre es nur wieder eine Deklaration)
    Aber nur in einer Source, sonst das typische ODR-Problem.

    Nun haben wir diese Deklaration in einem Header, jede source die diesen Header nun inkludiert weis, dass diese Variable in einer anderen Source definiert wurde.
    Somit können alle Sources die diesen Header inkludieren nun auf diese Variable in einer anderen Source zugreifen!

    Dann gibt es noch das inline Keyword welches seit C++17 es uns erlaubt Deklaration und Definition in einem zu erzeugen. (daher inline - kombiniert)
    Das heißt wenn wir in einem Header folgendes haben:

    C-Quellcode

    1. inline int globaleGanzzahlVariable = 420;

    Dann brauchen wir nicht in irgendeiner Source eine Definition dafür zu erstellen.

    Jede source die diesen Header nun inkludiert wird nach dem Präprozessor diese Zeile besitzen, aber der Linker weiß:
    “Aha, dies ist eine inline-Variable, das heißt obwohl sie in jedem Source enthalten ist, gibt es nur eine Definition, somit kann ich all diese Verweise zusammenführen!”

    Wichtiger hinweis jedoch, sollte eine Source inline int globaleGanzzahlVariable = 420; und eine andere zum Beispiel inline int globaleGanzzahlVariable = 29929; haben,
    dann ist der Wert den die Variable hat unbekannt, es könnte entweder ersteres oder letzteres sein, daher beachtet dies und verwendet inline-Variablen bestenfalls nur in Headern und nur einmal.

    Wir wissen nun wie wir mit lokalen und globalen Variablen umzugehen haben, doch was können wir mit diesen nun anstellen?
    Grundsätzlich bedeutet eine Variable nichts anderes, als ein Datenpaket im RAM das nach belieben verändert werden kann. (dies hängt natürlich mit dem verwendeten Typ zusammen)
    Wir können Variablen also dazu verwenden, daten zu speichern und abzurufen.
    Wir können Variablen kopieren, bewegen und weitergeben.
    Mehr sind sie eigentlich nicht.

    Um den Wert einer Variable zu bekommen können wir das tun indem wir ihren Namen aufrufen.
    Das hier zum Beispiel kopiert den Wert in eine weitere Variable:

    C-Quellcode

    1. int andereVariable = ganzzahlVariable;

    Somit haben wir nun zwei Variablen mit dem selben Wert, da die erste Kopiert wurde.

    Wir können Variablen aber auch nach belieben verändern:

    C-Quellcode

    1. ganzzahlVariable = 384;

    Somit hat diese Variable nun einen neuen Wert von 384 abgespeichert.

    Natürlich können wir mit Ausdrücken auch Kalkulationen vornehmen:

    C-Quellcode

    1. ganzzahlVariable = 2 * 4;

    Der Inhalt wird nun 8 beinhalten, da “2 * 4” bekanntermaßen 8 ist. (ich hoffe ich habe mich nicht verrechnet, wenn es tatsächlich 9 sein sollte ist dies ein Fall für das FBI)

    Dann gibt es noch eine besondere Form von Variablen die eigentlich keine Variablen sind da “Variabel” bedeutet das sich etwas ändern kann:

    Konstanten
    Eine Konstante ist ein Datenpaket, das es erlaubt nicht veränderbaren Werten einen Namen zu geben.
    Sie werden aber auch verwendet um Variablen, lokal, den Änderbarkeitsstatus abzusprechen.
    Es gibt Situationen in dem man sicherstellen möchte, dass eine Variable nicht veränderbar sein soll.

    Und hier ein Tipp von mir:
    Ihr solltet alle Variablen die nicht veränderbar sind Konstant machen, außer wenn sie von einer Funktion zurückgegeben werden, aber das ist ein Thema für einen anderen Tag.

    Eine Konstante können wir so definieren:

    C-Quellcode

    1. const int konstante = 12;

    Dieser Wert ist lesbar aber nicht mehr schreibbar.
    In C# ist das mit dem readonly Keyword vergleichbar. (glaube ich?)

    Man könnte es auch so schreiben:

    C-Quellcode

    1. int const konstante = 12;

    Aber das ist Geschmackssache, ersteres nennt sich “west-const”, letzteres “east-const”.
    Technisch gesehen ist zweiteres semantisch korrekt, aber für mich persönlich ist ersteres lesbarer, da ich gerne jegliches zusätzliches Keyword für eine Variable vor dem Typen habe.
    Ihr solltet aber für euch selbst entscheiden was ihr bevorzugt, auf das Programm haben beide denselben Einfluss.

    Warum sollte man nun Konstanten verwenden?
    Es gibt etliche Begründungen dafür:
    • Dokumentation:
      Eine Variable die const ist zeigt dem Leser das es sich um eine Konstante handelt, somit weis man im Vorhinein schon um was es sich hier handelt und man kann keine Änderungen im Verlauf des Codes erwarten.
    • Optimisation:
      Selbiges gilt für den Compiler/Linker. Wenn diese schon im Vorhinein Wissen das eine Variable nicht verändert wird, können diese jeweils Optimisationen für eine Variable bereitstellen die das Programm, manchmal sogar um vieles, schneller macht. Eine dieser Optimisationen kann sein die Konstante zu entfernen und sie direkt dort einzusetzen wo sie verwendet wird, das kann die Anfrage an den RAM ersparen. (wenn sie nicht sowieso schon im CPU-Cache oder einem CPU-Register aufzufinden war)
    • Fehlerprävention:
      Bei einer Variable, bei der man weiß das sie nicht mehr verändert werden soll und aber das const vergessen hat, kann es passieren das wenn man viel Code hat, möglicherweise unbewusst doch jene verändert. Wenn man nun versucht aber eine Konstante zu verändern, bekommt man eine Fehlermeldung vom Compiler das dies nicht möglich sei und somit sofort weis: “Upps, da wollte ich doch Glatt eine Konstante ändern”
    • Magic Numbers:
      Eine Magic Number ist eine Zahl die im Code verwendet wird, bei welcher aber nicht ersichtlich ist was sie eigentlich repräsentiert.
      Um dies zu verhindern kann man Konstanten erstellen die solchen Zahlen einen Namen geben damit man sofort weis: “Aha, das ist also diese Zahl”

      Als Beispiel, was findet ihr schöner:

      C-Quellcode

      1. int m = 0; // Irgend ein Wert für Masse
      2. int E = m * 300000 * 300000;

      oder eher doch:

      C-Quellcode

      1. const int c = 300000; // Lichtgeschwindigkeit
      2. int m = 0; // Irgend ein Wert für Masse
      3. int E = m * c * c;


    Nun, und weil es gerade nicht schon kompliziert genug war, globale Konstanten haben, anders als globale Variablen, automatisch interne Verknüpfung.
    Das heißt, eine Konstante in einem Header; jede Source die diesen Header inkludiert wird eine eigene Kopie dieser Konstante erhalten. (wenn sie nicht sowieso schon weg-optimiert wurde)

    Keyword: auto
    Des weiteren, für die Leute die gerne nicht explizit mit Typen umgehen, kann man auch ein "relativ" neues Keyword anstelle des Typen verwenden.
    Es nennt sich auto und nein, das bedeutet nicht, dass deine Daten gleich mit Saus und Braus davon-düsen, sondern, es handelt sich um einen automatischen Typ-Erkenner. (nennt sich "type-deduction")
    Eine Variable, also:

    C-Quellcode

    1. auto variable = 1;

    Wird automatisch den Typen int erhalten, da standardmäßig Ganzzahlen, ohne jegliches Suffix, integer sind.
    Wie ihr seht also erlaubt dieses Keyword uns ohne explizite Typen Nennung, nur anhand des Wertes, fest zu machen was sie ist.
    Das bedeutet aber auch das ihr die Variable wirklich sofort initialisieren müsst.
    Auch wichtig, C++ ist eine stark-typisierte Sprache, daher wird der Typ immer feststehen, es erlaubt dir nicht später einen Wert eines anderen Typs zuzuweisen. (wenn der Typ nicht implizit oder explizit konvertierbar ist)

    Persönliche Meinung folgt wieder:
    Selbst die offizielle Dokumentation von C++ rät dazu fast immer auto zu verwenden wenn möglich.
    Ich kann verstehen wo das herkommt, allerdings bin ich nicht wirklich Fan davon wenn man nicht Explizit den Typ erkennen kann.
    Klar, es gibt viele Momente in dem dies sogar Bugs verhindern kann, dennoch, persönlich bevorzuge ich es explizit zu sein.
    Aber, und bitte beachtet dies, das ist wirklich nur meine Ansicht und ich rate euch dazu eure Meinung selbst zu bilden.
    Man kann damit auf jeden Fall nichts falsch machen, also egal wie ihr euch entscheidet, ihr müsst damit umgehen können.

    Initialisation
    Bevor wir das hier abschließen möchte ich noch etwas zur Initialisierung von Variablen und Konstanten sagen, denn die ist oft, um es milde auszudrücken, ein schwieriger Fall.
    Aber nicht aus Laufzeit-Gründen, sondern aus syntaktischen.
    “Alle Wege führen nach Rom”, den Spruch kennt man ja, das kann man auch hier anwenden nur vielleicht etwas abgeändert: “Tausend Wege führen zur initialisierung einer Variable”.
    Es gibt nicht nur einen Weg eine Variable zu initialisieren… OHOH nein, es gibt VIELE und während in manchen Situationen alle anwendbar sind, sind in anderen einige sogar vonnöten.

    Default-Initialisation/Statische-Initialisation:

    C-Quellcode

    1. Typ variable;

    Dies bedeutet für fundamentale Datentypen, das
    • wenn Lokal, lasse die Variable un-initialisiert, der Wert kann irgendwas sein und ist nicht festgesetzt.
      Das kommt noch aus den C Zeiten, denn wenn man keinen Startwert braucht, trägt man nicht die Kosten einer Zuweisung
    • wenn Global, Statische-Initialisation passiert, bedeutet, sie wird mit einem default-Wert initialisiert, für numerische Typen ist das “0”, für bool “false” und so weiter.
    Für Klassentypen bedeutet das, dass der default-Konstruktor aufgerufen wird. (wenn einer Verfügbar ist, sonst ist es ein Error)

    Wert-Initialisation (value-initialisation):

    C-Quellcode

    1. Typ variable();
    2. Typ variable{};

    Das gleiche wie für Default-Initialisation, nur das lokale Variablen auch default-initialisiert werden da es nun explizit gefordert wurde.

    Direkte Initialisierung (direct-initialisation):

    C-Quellcode

    1. Typ variable(<Wert oder Konstruktor-Argumente>);

    Das bedeutet das die Variable mit einem Argument initialisert wird.
    Bei fundamentalen Typen ist das wie mit “= Wert”, aber anders dargestellt.
    Bei Klassentypen sind es die Konstruktor Argumente.

    Kopier-Initialisation (copy-initialisation):

    C-Quellcode

    1. Typ variable = Typ(variable);

    Für diese Variante gilt dasselbe wie für die “Direkte Initialisierung”, nur das die Variable erzeugt wird, dann kopiert und schlussendlich zugewiesen.
    Im Normalfall wird das wegoptimiert (Copy-Elision) und dann endet es so wie “Direkte Initialisierung”, aber wenn nicht, hat die CPU die zusätzliche Arbeit des kopierens der Daten.
    Kleiner Nachtrag, weil es mir gerade auffällt: Seit C++20 ist Copy-Elision nicht mehr eine Optimisation sondern fester Bestandteil des kompilierens!

    Listen-Initialisation (list-initialisation):

    C-Quellcode

    1. Typ variable { Element1, Element2, Element3… }; // Listen-Direkt-Initialisation
    2. Typ variable = { Element1, Element2, Element3… }; // Listen-Kopier-Initialisation

    Für Typen die mehrere Elemente beherbergen, ist diese Form der initialisation der Weg um die Elemente in die Variable zu bekommen. Zum Beispiel für Array’s.
    Hier auch wieder zwischen direkter Initialisierung und Kopier-Initialisation zu unterscheiden.

    Aggregat-Initialisation:

    C-Quellcode

    1. Typ variable { <Wert> };
    2. Typ variable { <Parameter1/Klassenfeld1>, <Parameter2/Klassenfeld2> };

    Für fundamentale Typen passiert hier dasselbe wie bei Typ v = Wert; oder Typ v(Wert);
    Für Klassentypen, wenn ein Konstruktor mit den Argumenten bereit steht, wird dieser aufgerufen.
    Für Klassentypen welche keinen Konstruktor haben und die Felder öffentlich sind, werden die Felder direkt initialisiert.


    Na gut… puuuh, das war mal ein langer Beitrag.
    Wir haben heute EINIGES gelernt, von Definition zu Deklaration bis hin zu Tausend Wege um in die Initialisation zu beißen.
    Aber freut euch schon mal auf den nächsten Beitrag, denn dort werden wir die Typen von C++ durchnehmen, und auch das ist anfänglich "etwas" schwer zu verdauen.

    Wie auch immer, ich hoffe es hat euch bis hier gefallen und hoffentlich konntet ihr auch etwas lernen. Wenn Fragen aufkommen wisst ihr ja wo ihr die stellen könnt.

    Dann noch viel Spaß und bis zum nächsten mal euch allen <3
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „Elanda“ ()

    2. Die Basics

    2.5 Typenmodell und Datentypen


    Nun gut, wir haben bisher ja schon des öfteren Wörter wie "Typ", "int" oder "char" gelesen, aber was ist das überhaupt und wieso muss man das wissen?

    Na… müssen tut man erst mal gar nichts.
    Es ist aber schon von Vorteil ein gewisses Grundkenntnis, über den Boden auf dem man baut, zu haben.
    Noch dazu wird man eher weniger weit kommen, sollte man diese Elemente nicht bis zu einem etwaigen Grad beherrschen, daher kommt man MIT doch wohl eher in den Garten Eden als OHNE.

    Was ist also nun das Typenmodell?
    Im falle von C++, haben wir ein statisches Typensystem.
    Das bedeutet, dass Typen schon vor dem Kompilieren festgelegt sein müssen - ganz anders als mit JavaScript wo dies nicht der Fall ist und wo Objekte ihren Typ ändern können.
    Wir können also Variablen, Parameter, Konstanten etc. nur mit jeweils den Werten behandeln über dessen der Typ es dafür zulässt.

    So ziemlich alles in C++ hat einen Typ, eine Variable, eine Funktion ja sogar Ausdrücke und Operationen (wie z.B. "1 + 1") bekommen implizit Typen zugewiesen.
    Das ist notwendig, da der Computer wissen muss als was er eine Entität betrachten und wie er sie weiterverarbeiten soll.
    Im Grunde genommen sind Typen im Ende nichts mehr als Größeninformation, ein Hinweis darauf welche Instruktionen verwendet werden sollen und wie sie in Bits zu repräsentieren sind.
    Denn, das C++ Typenmodell beruht nicht darauf das alle Typen auf einen Typ basieren wie C# oder Java es mit Object haben, nein, alle Typen stehen für sich selbst und haben keinen gemeinsamen wirklichen Nenner.
    Das macht es manchmal natürlich schwierig in generalisierten Kontexten mit Typen gut umzugehen, aber es gibt immer einen Weg, auch C++ hat darauf eine Antwort.
    Speziell mit Skalaren Typen kann man gut beobachten wie Typen im Ende definiert werden.
    Was sind Skalare Typen?
    Skalare Typen sind die-welche, die nur aus einem Wert bestehen, also sozusagen eine Zahl, ein Buchstabe oder ein Zeiger.
    Damit sind Arrays und Klassen schon mal nicht Skalar, da sie mehr als nur einen Wert besitzen. (können)

    Wenn der Compiler nun also eine Variable vom Typ int sieht, welches kein Klassentyp sondern ein fundamentaler Typ ist, weiß er:
    "AHA, das ist ein Ganzzahlenwert, nun weiß ich wie viel Speicher ich bereit mache (also wie viele bytes "erzeugt" werden müssen), wie der Typ im Nullen- und Einser-Format aussieht und welche Instruktionen ich zu verwenden habe/kann."

    Das ist ja mal alles schön und gut, was wir aber nicht Wissen ist, wie sehen eigentlich die Regeln aus wenn wir Typen modifizieren beziehungsweise weitergeben?
    Wir können doch nicht einfach einer Variable des Typs (nehmen wir an wir haben Benutzerdefinierte Typen) "Thanos" einen Wert des Typs "HundertProzent" zuweisen, oder?
    Weil der Typ immer festsitzt? (und jeder weiß, Thanos ist kein Freund von 100%)

    Teilweise stimm das, doch man KÖNNTE sehr Wohl, aber der Typ der Variable wäre für den Verlauf dennoch immer derselbe.
    Das Objekt eines Typs muss also die Zuweisung eines anderen Typs erst unterstützen um dies möglich zu machen.
    Das nennt sich Typen-Konversion.
    Einmal die explizite und, schlussfolgernd, die implizite Konversion. (und noch etwas drittes, wir werden diesem Geheimnis bald auf den Grund gehen ???)

    Implizite Konversion
    Die implizite Variante ist sowohl Segen als auch eine Traufe.
    Mal arbeitet sie mit dir und mal gegen dich; daher ist es essentiell zu verstehen wo und wann man von impliziter Konversion spricht, da somit der Faktor des unbekannten und damit schwerwiegende Bugs vorgebeugt werden können.
    Im Grunde ist die Definition folgende:
    Implizite Konversion passiert immer dann wenn ein Typ mit einem anderen Typen zusammenkommt welcher nicht derselbe ist, jedoch ist es für diese Kombination erlaubt, Typ1 in Typ2 umzuwandeln. (Nicht zwangsläufig auch umgekehrt)

    Als Beispiel, wir haben hier eine Variable int wert, wie wir schon wissen, bedeutet dies, dass die Variable nur Werte die aus Ganzzahlen bestehen annehmen kann, heißt: 1, 420, 3000 oder -1, -395038 und so weiter.
    Nun, es gibt auch Fließkommazahlen wie zum Beispiel: 0.34, 0.0, -737.083880288 etc.
    Wenn wir nun versuchen einen Fließkommazahlenwert an eine Ganzzahlen-Variable zuzuweisen, werden wir Zeuge einer impliziten Konversion, das ist, sie wird getrimmt - die Dezimalstellen fallen weg und nur der Ganzzahlwert bleibt über.
    (Sollte der Typ groß genug für den Wert sein)
    Auch hier ein Besipiel: 392.492920303 wird zu 392.

    Grundsätzlich gilt, arithmetische Typen sind zu allen anderen arithmetischen Typen Konvertierbar. (Arithmetisch: alle Ganzzahlen- und Fließkommazahlen-Typen)
    Mit einem Haken, dem resultierenden Output.
    Dies stellt kein Problem dar sollte der Wert von Typ1 in ein Objekt von Typ2 passen, da Typ2 ja genug Platz bietet.
    Ist der Wert von Objekt Typ1 aber größer als Typ2 repräsentieren kann, gibt es verschiedene Ausgänge.

    Nehmen wir Ganzzahl-Typen her:
    charund int. (Für die die jetzt aufschreien, char ist unter x86 meistens signed, für die die nicht verstehen was das bedeutet, keine Sorge ich komme später noch dazu)
    Für unser Beispiel, char hat einen Maximalwert von +127 und einen Minimalwert von -128.
    Jetzt stellt euch vor wir haben eine Variable int mit einem Wert von 200, was passiert wohl wenn wir dies an eine Variable vom Typ char zuweisen?

    Hier offenbart sich uns eine Eigenheit von C++, "Undefined Behaviour" oder kurz UB.
    Das bedeutet einfach nur, das wenn man in UB gerät, so ziemlich alles unerwünschte das nur irgendwie möglich ist auch passieren kann.
    Ich erinnere hier nur kurz an Murphy's Law, ich würde euch also raten es nicht auf die Probe zu stellen.
    Natürlich aber nur aus Sicht des Entwicklers, der Computer handelt natürlich deterministisch basierend auf Annahmen, Kontext und Implementation.

    Wenn wir nun aber einen int an einen Fließkommazahlen-Typen zuweisen, sieht die Sache schon anders aus.
    Fließkommazahlen sind schon etwas komplizierter und müssten eigentlich schon ein eigenes Kapitel bekommen. (Es gibt ganze Bücher nur über Fließkommazahlen da diese so komplex sind)
    Hier gilt, wenn der Ganzzahlenwert in der Fließkommazahl repräsentierbar ist, wird sie auch so konvertiert.
    Ist die Zahl jedoch zu groß oder zu klein, landen wir wieder im UB Bereich.

    Es gibt aber noch eine Eigenheit: Selbst wenn die Zahl nicht zu groß oder zu klein ist, kann es sein das die Zahl nicht repräsentierbar ist, aber auf andere Weise.
    Das liegt einfach daran das Fließkommazahlen, dank ihrer eigenen Repräsentation im Bit-Format, nicht jeden Wert besitzen können.
    Zum Beispiel, 20000 könnte existieren, 20001 nicht aber 20002 doch wieder. (Das sind keine geprüften Zahlen, nur willkürlich gewählte Beispiele)
    Das sind sogenannte Lücken die je nach Höhe der Zahl immer größer werden - das gilt aber tatsächlich nicht nur für C++ sondern für so ziemlich alle Sprachen.
    Und sollte hier ein Wert, 20001, zugewiesen werden - wird das an einen repräsentierbaren Wert angepasst, also 20001 könnte zu 20000 oder auch 20002 werden. (Im Normalfall ist es irgendein Dezimalwert und kein Ganzzahlenwert, kommt auf die Höhe darauf an)

    Und das ist unsere nächste Eigenheit von C++.
    "Implementation-Defined Behaviour", anders als UB ist dieses Verhalten jedoch klar definiert.
    Das heißt, man muss möglicherweise zuerst Tausend Handbücher durchgehen, aber man wird zu einem Ziel kommen und auch der Compiler wird nichts unüberlegtes tun.
    Es bedeutet einfach nur das der Ausgang der Situation von der Implementation abhängt.

    "Was ist Implementation?"
    Gute Frage Timmy und Chantal: Implementation ist die Kombination aus Compiler und Maschine.
    Das heißt, wenn eines von beidem unterschiedlich ist, kann der Ausgang auch anders sein, aber wie gesagt, klar definiert.
    Es rentiert sich aber meistens nicht wenn man Portablen Code schreiben möchte, da man dann für alle Implementationen möglicherweise verschiedenen Code schreiben müsste.
    Das betrifft hier in unserem Beispiel das Format von Fließkommazahlen, glücklicherweise folgen x86 Prozessoren dem IEEE 754 Floating Point Standard.
    Dieser definiert die Repräsentation und das Verhalten von Fließkommazahlen.
    Für unser Beispiel also, der Wert wird zum nächstmöglichen Wert angepasst.

    Nun gibt es einen Ausnahmetypen: bool.
    Dieser Typ ist auch ein Ganzzahltyp welcher aber nur zwei Werte annehmen kann, und zwar 0 und 1.
    Boolesche Werte werden dazu verwendet um Situationen wie "stimmt" oder "stimmt nicht" zu repräsentieren.
    Dafür gibt es die Keywords true welches 1 bedeutet und false welches 0 bedeutet.
    Jeder, egal welcher, andere Arithmetische Typ welcher zu bool konvertiert wird, folgt folgender Regel: Jeder Wert der 0 ist wird zu false und alle anderen Werte, egal ob 1, 1000 oder -7392984, werden zu true.
    Umgekehrt gilt selbiges, nur das true immer zu den für den Zieltypen als 1 bekannten Wert wird.
    Das ist auch als Boolean-Conversion bekannt.

    Dann gibt es noch implizite Konversion für Klassentypen, aber dazu kommen wir im OOP Kapitel des Tutorials.

    Was vielleicht noch interessant ist, in C++ ist implizite Konversion nicht vermeidbar, wenn man nicht aufpasst dann passiert es einfach.
    So etwas wie Option Strict On gibt es nicht, und in manchen Situationen ist das gar nicht so übel wenn es kontrolliert vonstatten geht.
    Aber manche Compiler und zusätzliche Analyse-Tools Warnen zumindest davor wenn man eine "narrowing-conversion" vornimmt (heißt, größerer Typ zu kleineren Typ),
    somit hat der Programmierer die Chance nochmals darauf zurück zu kommen und gegebenenfalls, wenn es gewollt war, die Konversion Explizit zu machen um die Warnung zu entfernen.
    Und das ist das wozu wir jetzt kommen.

    Explizite Konversion
    Anders als implizite Konversion, ist explizite Konversion etwas mehr… naja… Sichtbar.
    Während implizit automatisch vonstatten geht, wird bei expliziter bewusst vom Entwickler dazu aufgerufen die Konversion vorzunehmen.
    Die Gründe dafür sind vielfältig, zum einen, wie oben benannt, um Warnungen des Compilers über implizite Konversionen zu entfernen zum anderen aber auch wenn eine implizite Konversion nicht möglich ist aber eine explizite sehr wohl.

    Für viele wäre wohl eher der häufigere Terminus "casting" geläufig.
    Wie auch in anderen Sprachen, sieht casting hier nicht anders aus:

    C-Quellcode

    1. Typ1 var = (Typ1) whatever;

    Hier wird whatever mit dem (angenommen) Type2zu Typ1 konvertiert, nun legen wir die Füße hoch, essen was schönes vor der Flimmerkiste und… *zapzzz* "Mist, da stimmt was nicht, wo kommt dieser Bug plötzlich her?"

    C++ wäre nicht C++ wenn ich euch nicht hin und wieder mal etwas enttäuschen müsste.
    Es stimmt zwar das man auf diese Weise casten kann, allerdings nennt sich dieser Syntax "C-Style Cast" und ist absolut nicht mehr, zu C++ Zeiten, zu empfehlen.
    Es ist ein Relikt aus der Vergangenheit.

    Fakt ist, sicheres casting in C++ ist nicht wirklich schön anzusehen, es ist klobig und braucht Platz, dafür aber gibt es dir Sicherheiten mit welchen ihr Bugs und Fehlern vorbeugen könnt.
    Niemals, unter keinen Umständen, sollte man in C++ C-Style Casting verwenden sondern immer die C++ Varianten, egal wie sicher man sich fühlt, auch wenn es nur dazu dient Warnungen von impliziten Konversionen zu entfernen,
    denn es könnte passieren das man später mal was ändert und der C-Style Cast etwas zulässt das eigentlich nicht möglich sein sollte.

    "Huch… Varianten? Plural? Es gibt mehrere Arten von Casts?"
    Leider ja, aber dafür bin ich ja hier, gehen wir sie mal durch:

    static_cast<Typ1>(whatever)
    Dies ist der wohl am häufigsten aufzufindende Cast und somit auch der sicherste von allen.
    Man sollte immer, IMMER, zuerst diesen Cast probieren, denn wenn dieser nicht funktioniert weil die Typen inkompatibel sind, bekommt man so einen Hinweis das man möglicherweise etwas an seinem Code-Design ändern sollte.
    Aber es gibt auch Situationen in denen man Bewusst etwas konvertieren will obwohl es eigentlich "nicht so vorgesehen" ist, dafür ist dieser Cast nicht geeignet, er stoppt dich nur davor etwas dummes zu tun.

    "Was ist erlaubt?"
    Dieser Cast erlaubt es dir nur Casts vorzunehmen welche implizit möglich sind oder welche vom Benutzer definiert worden. (OOP)

    reinterpret_cast<Typ1>(whatever)
    Das ist der gefährliche Bruder des static_cast. Dieser Cast erlaubt dir Dinge vorzunehmen die mit ersterem nicht möglich waren - somit ist dieser weniger sicher.
    Die grundsätzliche Funktion dieses Casts ist es einfach nur die "Bits" eines Objektes dem Compiler in "Bits" eines anderen Typen zu uminterpretieren. (Daher "reinterpret")

    const_cast<Typ1>(whatever)
    Es tut mir fast schon im Herzen weh diesen Cast vorstellen zu müssen aber der Vollständigkeit halber tu ich es trotzdem.
    Dieser Cast ist für mich, neben dem C-Style Cast, das wohl unerwünschteste Familienmitglied der gesamten Bande.
    Er erlaubt es einer Variable bzw. einer Konstante das const abzusprechen, somit ist dieser Cast nicht nur absolut unschön sondern durchaus auch immens gefährlich.
    Denn, C++, kann Konstante Variablen optimieren so das sie so gar nicht mehr existieren, somit also ist es gut möglich eine Variable zu modifizeren die gar nicht mehr da ist, was ultimativ in UB enden kann.
    Daher meine Empfehlung, verwendet niemals, unter keinen Umständen diesen Cast, wenn ihr denkt es gibt einen guten Grund dies zu tun, dann solltet ihr euren Code neu gestalten. (meine Meinung)

    dynamic_cast<Typ1&>(whatever)
    Und dann haben wir noch den einen Cast der, mehr oder weniger, sich von den anderen abhebt.
    Denn dieser Cast, anders als die anderen die alle während der Kompilieren in Effekt treten, tritt dieser Cast zur Laufzeit auf.
    Ich werde nicht viel weiter darauf eingehen, aber dieser cast ist sozusagen der "Vererbungs" Cast.
    Mit diesem kann man Polymorphe Umwandlungen vornehmen. (geht auch mit static_cast, aber dynamic_cast überprüft ob die Konversion wirklich rechtens ist, anders als static_cast, und das geht nur zur Laufzeit)

    Alle diese Casts, bis auf dynamic_cast, sind im C-Style Cast enthalten.
    Das heißt (Typ1) wird all die oben benannten Casts durchlaufen bis es einen trifft der passt. (Wenn einer passt - alle Konversionen sind sowieso nicht möglich)
    Klingt ganz schön gefährlich, huh?
    Daher aufpassen, und standardmäßig static_cast oder, je nachdem, dynamic_cast verwenden.

    Nun werden einige kommen und sagen: "Aber Elanda, es ist doch in Ordnung C-Style Casts zu verwenden wenn ich das Resultat ja sowieso kenne, außerdem verkleistert es dann den Code ja nicht so."
    Tja, möge ja sein das es in Situation 1 oder Situation 2 gut geklappt hat, aber Situation 3 könnte Endstation sein.
    Lesbarkeit, obwohl Regel Nummer 1, sollte hier ausnahmsweise keine Ausrede dafür sein nicht die oben genannten Versionen zu wählen.
    In diesem Fall: Sicherheit geht vor!
    Aber wie auch immer zählt hier das eigene Ermessen, ich gebe euch hier nur meine Gefühle wieder.
    Wenn ihr es unbedingt so machen müsst dann tut das, ich rate euch nur guten Gewissens davon ab.

    Nun… es gibt tatsächlich noch eine dritte Art der Konversion, diese ist schleichend und liebt es sich vor euch zu verstecken.
    Wenn ihr nicht darüber lesen würdet, währt ihr darüber möglicherweise gar nicht in Kenntnis.
    Diese Art der Konversion ist die:

    Promotion
    Dies betrifft die arithmetischen Typen.
    Man könnte C++ oft wie ein kleines Kind betrachten, man bietet etwas an, aber es will mehr.
    Arithmetische Operationen, wie var + var2 oder var * var2 zum Beispiel akzeptieren keinen Typ der kleiner als int ist, daher, wenn möglich, werden Typen kleiner als int entweder zu int oder einen anderen größeren oder gleich großen Typen konvertiert.
    Das geschieht unter dem Radar.
    Selbiges kann unter umständen auch mit dem Typen float zu double geschehen.

    Das liegt daran, dass üblicherweise (es gibt immer Ausnahmen) int an die natürliche Verarbeitungsgröße der Maschine angepasst ist.
    Heißt, auf einem 32x System, sind 32x Werte am schnellsten verarbeitbar, weil das die Weite ist die der Prozessor in einem Cycle durchführen kann.
    Nun, es hilft natürlich nicht das auf 64x Systemen ints immer noch 32x haben, das ist aber der Kompatibilität geschuldet.
    int wird immer noch empfohlen als Standard Ganzzahlen Typ bewertet zu werden - egal ob es nie das Maximum erreichen würde, selbiges gilt für double.
    Aber es ist nicht pauschal definierbar, denn auch Speicherverbrauch spielt eine Rolle in der Leistung und auch ob Daten in den Cache passen - es ist einfach nur ein weit-akzeptierter Standard.

    Aber um wieder auf Promotion zurückzukommen, der Compiler wird diese Ausdrücke als den best-passendsten Typen interpretieren um davon profitieren zu können.
    Natürlich, wenn der Typ im vorhinein schon Größer ist muss auch nicht promoted werden.

    Typen
    Kommen wir nun schlussendlich mal zu den Typen die wir in C++ haben.
    Grundsätzlich gibt es zwei Arten von Typen in C++: Fundamentale und Compound-Typen.
    Zuerst nehmen wir uns die fundamentalen Typen durch, welche weiter und weiter und weiter unterkategorisiert werden können.

    Arithmetische Typen > Ganzzahltypen
    Typ
    Größe
    Werte
    bool8 Bit oder Größer0 oder 1, bzw., false oder true

    Trotzdessen das bool nur 2 Werte annehmen kann, ist es dennoch mindestens einen Byte groß, das liegt daran, dass dies die kleinste Einheit ist die gespeichert/verarbeitet werden kann.
    Es gibt auch Implementationen wo diese sogar größer als 1 Byte sind. (mir persönlich bekannt ist jedoch keine)



    Typ
    Größe
    Werte
    short int
    16 Bit oder größerWenn 16 Bit: -32.768 bis 32.767
    int16 Bit oder größer (meistens 32 Bit)
    long int32 Bit oder größerWenn 32 Bit: -2.147.483.648 bis 2.147.483.647
    long long int64 Bit oder größerWenn 64 Bit: −9.223.372.036.854.775.808 bis 9.223.372.036.854.775.807

    Hier gibt es ein paar dinge zu beachten.
    Zuerst syntaktisch:
    Hier das int als Zusatz ist nicht notwendig, short ist dasselbe wie short int es ist einfach nur die komplette schreibweise als Hinweis das diese Keywords nur Längenspezifizierer für int sind.
    Man kann int aber weglassen. (Außer für int natürlich)

    Des weiteren, die Größe der Datentypen kann variieren, sie ist nicht festgelegt, maximal die mindestgröße - dies ist auch abhängig von der Implementation.
    Als Beispiel, auf Windows ist long immer 32 bit groß, also immer gleich wie int, egal ob die CPU 64x oder 32x bit ist.
    Auf Linux ist das anders, ein 32x Prozessor möge diesen Umstand zwar teilen aber auf einem 64x Prozessor haben long zum Beispiel 64 bit.
    Also man sieht, man kann sich nicht wirklich auf die Größe von Typen verlassen.
    Selten braucht man das aber auch und wenn, dann gibt es auch dafür Wege um sicher zu gehen.



    Typ
    Größe
    Werte
    char1 Byte-128 bis 127 oder 0 bis 255 bzw. alle ASCII Werte, z.B.: 'a' für 8-Bit Byte-Maschinen
    wchar_tWeit genug um jedes Zeichen repräsentieren zu könnenUnicode Code-Points

    Charakter-Typen gehören auch zu den Ganzzahltypen, werden jedoch oft als eigene Kategorie angelegt, da gewisse Funktionen diese anders verarbeiten als die oben genannten.
    Zum Beispiel, wie in unserem "Hello World" Beispiel std::cout.
    Sowohl int als auch char beinhalten Ganzzahlenwerte, aber std::cout interpretiert char so das es nicht als Zahl sondern als Zeichen ausgegeben wird.

    Hinzuzufügen ist, dass alle der oben genannten, bis auf bool und wchar_t eine alternative Form annehmen können, dafür gibt es die Keywords signed und unsigned.
    Alle Typen sind implizit signed.

    Was bedeutet das?
    Ein "signed" Typ ist ein Typ, der einen Bit wegnimmt der für das Zeichen "+/-" steht.
    Das heißt zwar, dass nur die Hälfte der positiven Werte zur Verfügung stehen, dafür die andere Hälfte aber für negative Werte bereitsteht.
    Anders als bei unsigned Typen, diese haben den vollen positiven Umfang, können dafür aber keine negativen Werte darstellen, da das "sign" bit nicht mehr für das Zeichen sondern für weitere Werte ist.

    Also, wenn signed nicht angehängt wurde, ist es so als hätte der Typ signed.

    Aber Achtung, für char gibt es da eine Ausnahme.
    Dieser Typ ist weder automatisch "signed" noch "unsigned", klar der Typ hat natürlich in der Repräsentation eines der beiden, aber dieser Typ ist anders zu betrachten als mit einem der beiden.
    Das liegt daran das char nicht klar definiert ist welche der beiden auf ihn zutrifft, das ist auch wieder von Implementation zu Implementation verschieden, aber auf x86 sollte es "signed" sein.

    Noch kurz etwas zur Bit-Representation von "signed" und "unsigned" Typen.
    Die "unsigned" Typen haben eine klare Form: 0000 0001 bedeutet eins und ect.

    Für die "signed" Variante, ist auch das wieder Implementation-Defined.
    Jedoch auch hier wieder, auf x86 sollte es Two's-Complement sein.
    Bedeutet, wenn ihr Bit-Operationen macht, solltet ihr das mit "unsigned" Typen tun, denn das "sign" bit könnte ein wenig problematisch werden. (und je nach Operation sogar in UB enden)

    Um die Größenangaben für Typen nun abzuschließen, gibt es auch noch die typische Line die man öfters findet:
    1 Byte == char <= short <= int <= long <= long long

    Es ist also theoretisch möglich, das char 64 bit hat, denn ein "byte" muss nicht zwingend 8 bit haben. (Obwohl ich denke, dass größere/kleinere eher die Seltenheit sind)
    Somit könnten, rein-theoretisch, alle Typen dieselbe Größe haben.



    Arithmetische Typen > Fließkommazahlen
    Typ
    Größe
    float32 Bit
    double64 Bit
    long double64 Bit oder größer

    Diese Werte gehen davon aus, dass die CPU IEEE 754 unterstützt. (Also ja, einfach nur ja)
    Für long double, die Spezifikation ist etwas Konfus, aber die drei Hauptgrößen sind 64, 80 und 128 bit.
    Ich muss zugeben, dass ich da nie wirklich durchgeblickt habe wer-wann-wie hier welche Größe definiert, aber ich würde behaupten dass dieser Typ selten, wenn überhaupt jemals, in Verwendung tritt.
    Fließkommazahlen haben im übrigen keine "signed" oder "unsigned" Varianten, sie haben alle negative Wertbereiche.



    Restliche fundamentale Typen
    void
    Dieser Typ kann keine Werte annehmen, er bezeichnet Umstände, in denen man keinen Wert weitergeben möchte wie zum Beispiel bei der Rückgabe einer Funktion ohne Wertrückgabe.
    Die Ausnahme bildet void*, welches so ziemlich jeden Wert annehmen kann, aber dazu kommen wir im Kapitel "Pointer".

    std::nullptr_t
    Dies ist der Typ des Keywords nullptr.
    Aber auch hier wieder, Kapitel "Pointer"!



    Compound-Typen

    Kommen wir nun zu den Compound-Typen.
    Compound Typen gehen schon etwas eher in die späteren Sparten, aber ich liste sie hier auf damit man später mal darauf zurückkommen kann.
    Ich werde hier auch nicht viel Beschreibung dazugeben, weil es wahrscheinlich nur verwirren würde.
    Aber jedes einzelne davon wird noch durchgenommen, daher ist es nicht ganz so doof sie zumindest aufzuzeigen.
    Macht euch nur keine Sorgen, wie immer sieht es komplexer aus als es eigentlich ist.

    Bezeichnung
    Definition
    Arrays
    Typ array[N]
    Funktionstypen
    Rückgabetyp(Parameter, …)
    Referenztypen:
    • lvalue
      1. Objekt
      2. Funktion
    • rvalue
      1. Objekt
      2. Funktion
    • lvalue
      1. Typ&
      2. Rückgabetyp(&)(Parameter, …)
    • rvalue
      1. Typ&&
      2. Rückgabetyp(&&)(Parameter, …)
    Zeigertypen
    1. Objekt
    2. Funktion
    1. Typ*
    2. Rückgabetyp(*)(Parameter, …)
    Zeiger-zu-Angehörige
    1. Objektzeiger
    2. Funktionszeiger
    1. Klasse::Typ*
    2. Rückgabetyp(Klasse::*)(Parameter, …)
    Enumerationstypen (enum)
    1. Uneingeschränkt
    2. Eingeschränkt
    1. enum Name {}
    2. enum class Name {}
    Klassentypen
    1. Union
    2. Struct/Class
    1. union Name {}
    2. class Name {} und struct Name {}

    Ich weis, es sieht etwas lieblos aus, aber vertraut mir, es wird noch Sinn machen.
    Wenn man einmal alles verstanden hat, kann man immer noch mal zurückkommen um nachzusehen wie etwas definiert werden muss.



    Kompensations-Typen
    Das ist kein offizieller Begriff, ich verwende ihn nur gerne da diese Typen die Tatsache kompensieren, dass man sich nicht sicher sein kann welche Größe fundamentale Typen im Endeffekt haben.
    Das ist speziell wichtig wenn man cross-platform entwickelt, aber auch wenn man Bit-Operation durchführt und die Größe Einfluss auf diese Entscheidung hat, anderfalls sollte man einfach mit int gehen.
    Der offizielle Begriff hier wäre: Ganzzahlentypen mit fester Größe (klingt auf deutsch etwas sperrig - "Fixed width integer types")

    Name
    Beschreibung
    • std::int8_t
    • std::int16_t
    • std::int32_t
    • std::int64_t
    Dies sind die "signed" Ganzzahltypen, die Zahl bestimmt welche exakte Größe sie repräsentieren.
    • std::uint8_t
    • std::uint16_t
    • std::uint32_t
    • std::uint64_t
    Selbiges wie oben, nur das dies die "unsigned" Varianten sind.
    • std::int_fast8_t
    • std::int_fast16_t
    • std::int_fast32_t
    • std::int_fast64_t
    Diese Typen definieren jeweils Typen mit einer "mindestens" Garantie.
    Das heißt der Typ kann Größer sein als die Zahl aber nicht weniger.
    Der Grund dafür ist das, wie oben mit int bereits beschrieben, man möchte den schnellsten Typen haben aber zumindest eine Garantie für eine Mindestgröße.
    • std::uint_fast8_t
    • std::uint_fast16_t
    • std::uint_fast32_t
    • std::uint_fast64_t
    Selbiges wie oben, nur das dies die "unsigned" Varianten sind.
    • std::int_least8_t
    • std::int_least16_t
    • std::int_least32_t
    • std::int_least64_t
    Dies Typen definieren jeweils Typen mit einer "mindestens" Garantie.
    Anders als die "fast" Varianten aber, wird hier versucht den kleinst-möglichen Typ zu ergattern.
    Hier geht es nicht um geschwindigkeit, sondern darum den kleinsten Typen zu bekommen, welcher größer ausfallen könnte in fällen wo diese Größe villeicht nicht existiert.
    • std::int_least8_t
    • std::int_least16_t
    • std::int_least32_t
    • std::int_least64_t
    Selbiges wie oben, nur das dies die "unsigned" Varianten sind.
    std::intmax_t
    Der größte Ganzzahlenwert Typ den die Platform erübrigen kann.
    std::uintmax_t
    Selbiges wie oben, nur das dies die "unsigned" Variante ist.
    std::intptr_t
    Spezifiziert die "unsigned" Variante eines Types, die groß genug ist eine Zeigeradresse beinzuhalten.
    std::uintptr_t
    Selbiges wie oben, nur das dies die "unsigned" Variante ist.

    Ihr seht schon, es gibt die ein oder anderen Typen für alle Fälle.
    Man beachte das diese Typen nicht mit den Typen von oben zu vergleichen sind, denn dies sind sozusagen nur "Aliase" zu Typen die oben definiert wurden.
    Heißt, std::uint_fast8_t könnte ein int sein.

    Um auf diese Typen Zugriff zu bekommen, muss man nur den Header <cstdint> inkludieren und man hat Zugriff. :thumbup:

    Wo die herkommen gibt es natürlich noch einige mehr, aber ich glaube wir haben hier jetzt einen Punkt erreicht an dem ich endlich mal schluss machen sollte.
    Ich danke euch wieder für's Einschalten und hoffe das ich auch dieses mal wieder eine Hilfe sein konnte.
    Wie immer könnt ihr natürlich im Diskussions-Thread Fragen stellen und diskutieren, Verbesserungsvorschläge und so weiter, freue mich schon drauf:
    Das C++-Tutorial für Einsteiger: Diskussions-Thread

    Dann noch viel Spaß euch allen <3
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    2. Die Basics

    2.6 Keywords und Operatoren


    Keywords
    Ein vitaler Punkt einer jeden Sprache sind natürlich die Keywords.
    Sie alle haben ihren eigenen Nutzen und jedes verfolgt seinen eigenen Sinn.

    Einige Keywords haben wir schon gesehen und viele wurden im vorherigen Kapitel schon angesprochen, die fundamentalen Datentypen z.B. zählen auch zu den Keywords daher werde ich dinge wie int oder unsigned hier nicht nochmals mit auflisten.
    Wir werden uns dafür gut um die anderen kümmern.

    Wir gehen das jetzt mal alphabetisch durch:
    Keyword
    Beschreibung
    Beispiel
    alignas
    Wir haben ja bereits in einem früheren Beitrag über die "Datenausrichtung" gesprochen.
    Dieses Keyword kontrolliert diese Anforderung an einem Klassentyp oder einem Objekt.
    Ihr werdet dieses Keyword nicht häufig brauchen.
    Häufige Anwendungsgebiete sind z.B. Cache/Multithreading, SIMD Operationen oder wenn man auf einer Plattform Entwickelt die da sehr streng mit sind.
    In einer üblichen Sitzung wird man damit aber eher kaum in Berührung kommen.

    C-Quellcode

    1. alignas(8) int var;

    (8 bedeutet das var an 8-Byte-Begrenzungungen ausgerichtet werden soll. (also nur an Adressen speichern die ein Vielfaches von 8 sind))
    alignof
    Das Pendant zu dem alignas-Operator.

    Hiermit kann man die Speicherausrichtungsvorgabe eines Typen abfragen, welches als std::size_t zurückgegeben wird. (Ist auch ein Alias für einen Ganzzahlwert)

    C-Quellcode

    1. std::size_t ausrichtung = alignof(int);

    asm
    Das inline-Assembly Keyword.

    Dieses Keyword erlaubt uns direkt in unserem Code Assemblerinstruktionen zu schreiben.

    Jedoch ist dieses Keyword nur bedingt verfügbar und Implementation-Defined, nicht jeder Compiler unterstützt es und/oder implementiert es unterschiedlich.

    C-Quellcode

    1. asm("Assembly code");

    auto
    [1]
    Leitet automatisch den Typ eines Initialisers/Ausdruckes/Rückgabewertes.

    [2]
    Man kann auto dazu verwenden um nachfolgende Rückgabetypen zu definieren, wenn der Rückgabetyp entweder von den Parametern abhängig ist oder der Rückgabetyp so kompliziert ist, dass er die Lesbarkeit beeinträchtigen könnte.

    [3]
    Seit C++17 gibt es auch noch "Structuctured Bindings", welche es erlauben das Interface eines Objektes bzw. eines Tuples zu extrahieren oder ein Array flach auszulegen.
    Beispiel 1:

    C-Quellcode

    1. // Variable
    2. auto x = 0.4; // double
    3. // Rückgabewert
    4. auto funktion() { return 2 + 2; } // int


    Beispiel 2:

    C-Quellcode

    1. auto funktion() -> SehrLangerKomplizierterTyp
    2. {
    3. return {};
    4. }


    Beispiel 3:

    C-Quellcode

    1. struct Pod
    2. {
    3. int x;
    4. int y;
    5. int z;
    6. };
    7. void entpacken(Pod pod)
    8. {
    9. auto [x, y, z] = pod;
    10. // bla bla
    11. }

    case
    Ein Fall eines switch-statements welcher nur eintritt, wenn die Kondition erfüllt wurde oder das vorherige case durchfällt. (nicht ausbricht)

    C-Quellcode

    1. int x = 0;
    2. switch(x)
    3. {
    4. case 0: break;
    5. case 1:;
    6. case 420:;
    7. default:;
    8. }

    catch
    Fängt einen try-Block ab wenn eine Exception geworfen wurde.

    C-Quellcode

    1. try
    2. {
    3. // Potentiell werfender Code
    4. }
    5. catch (std::exception &ex)
    6. {
    7. // Wenn die Exception auftritt, mach was damit
    8. }

    class, struct
    [1]
    Definiert eine Klasse/eine Struktur oder eine eingeschränkte Enumeration.

    [2]
    Als Template-Indikator, zeigt class das ein Parameter ein Typ ist.
    Beispiel 1:

    C-Quellcode

    1. class MeineKlasse {};
    2. enum class MeinEnum {};


    Beispiel 2:

    C-Quellcode

    1. template<class Typ>

    const
    [1]
    Deklariert eine Konstante.

    [2]
    Qualifiziert eine nicht-statische Klassenfunktion als aufrufbar wenn teil eines nicht-Konstanten oder eines Konstanten Objektes.
    Beispiel 1:

    C-Quellcode

    1. // Konstante
    2. const int var = 0;


    Beispiel 2:

    C-Quellcode

    1. // const qualifizierte Funktion
    2. class Foo
    3. {
    4. void bar() const {}
    5. };

    constexpr
    [1]
    Erlaubt es einer Variable/Funktion in "Constant Expressions" verwendet zu werden.
    Außerdem, impliziert const für Variablen.

    [2]
    Seit C++17, constexpr if statements, welche es erzwingen eine Kondition zur Kompilierzeit zu evaluieren und basierend auf dem Ergebnis zu entfernen.
    Beispiel 1:

    C-Quellcode

    1. constexpr int var = 0;


    Beispiel 2:

    C-Quellcode

    1. if constexpr (1 != 2) {}

    continue
    Bricht den derzeitigen Durchlauf einer Schleife ab und springt zur nächsten Iteration bzw. zum nächsten Durchlauf.

    decltype
    Nimmt den übergebenen Ausdruck und wird zum Typ der durch den Ausdruck entstehen würde. Der Ausdruck wird nicht evaluiert.

    C-Quellcode

    1. decltype(0) foo = 20; // wird zu int
    2. decltype(funktion()) bar = …; // wird zum Rückgabetyp der Funktion

    default
    [1]
    Als letzter Fall in einem switch-statement wenn keiner der cases erfolgreich war.

    [2]
    Definiert eine von C++'s Prädefinierten Funktionen so als wäre sie vom Compiler selbst definiert worden. (wenn Verfügbar)
    Beispiel 1: Siehe case

    Beispiel 2:

    C-Quellcode

    1. class Klasse
    2. {
    3. Klasse() = default; // In Fällen in denen der default-Konstruktor gelöscht wird
    4. }

    delete
    [1]
    Zerstört und löscht dynamisch erzeugte Daten.

    [2]
    Löscht eine Funktion und verhindert somit das aufrufen dieser.
    Beispiel 1:

    C-Quellcode

    1. int *var = new int(12);
    2. delete var;


    Beispiel 2:

    C-Quellcode

    1. class Klasse
    2. {
    3. Klasse() = delete; // Kein default-Konstruktor mehr
    4. }



    do
    Deklariert eine do-while-Schleife.

    C-Quellcode

    1. do
    2. {
    3. // Mach etwas
    4. }
    5. while (Kondition);

    else
    Teil eines if-statements, als alternative Abzweigung sollte die vorangehende Bedingung nicht zutreffen.

    C-Quellcode

    1. if (false)
    2. {
    3. // bla bla
    4. }
    5. else // true
    6. {
    7. // bloo bloo
    8. }

    enum
    Deklariert einen Enumerationstypen, entweder eingeschränkt oder uneingeschränkt.

    explicit
    [1]
    Wenn ein Konstruktor mit explicit spezifiziert wird, kann eine Klasse nicht durch implizite Konversion erzeugt werden.

    [2]
    Wenn eine Konversionsfunktion mit explicit spezifiziert wird, kann eine Klasse nicht implizit zu einem anderen Typen konvertiert werden.
    Beispiel 1:

    C-Quellcode

    1. class Foo
    2. {
    3. explicit Foo(int) {}
    4. }
    5. Foo foo = 1; // Error, da explizit, ansonsten erlaubt
    6. // Nur explizit ist erlaubt
    7. Foo foo(1);


    Beispiel 2:

    C-Quellcode

    1. class Foo
    2. {
    3. explicit operator int()
    4. { return …; }
    5. }
    6. Foo foo;
    7. int bar = foo; // Error, da explizit, ansonsten erlaubt
    8. // Nur explizit ist erlaubt
    9. int bar = static_cast<int>(foo);


    export
    Dieses Keyword hatte ursprünglich eine andere Bedeutung und ist seit C++11 reserviert. (ohne Verwendung)

    Seit C++20 Teil des neuen Modul-Systems.

    extern
    [1]
    Verknüpft globale Variablen extern und erlaubt es sie als Deklaration zu deklarieren. (auch für Funktionen, ist aber optional und somit redundant)

    [2]
    Erlaubt Kompatibilität mit C-Funktionen, da diese anders verlinkt und aufgerufen werden.

    "C" und "C++" sind die einzig garantierten Sprachen, andere könnten aber werden nicht garantiert.
    Beispiel 1:

    C-Quellcode

    1. // Variable
    2. extern int foo;
    3. // Funktion (redundant)
    4. extern void bar();


    Beispiel 2:

    C-Quellcode

    1. extern "C"
    2. {
    3. // C Funktion
    4. void c_foo();
    5. }
    6. // Aufrufen
    7. c_foo();

    false, true
    Boolesche Werte die jeweils einen Ganzzahlwert repräsentieren.

    false: 0, true: 1

    for
    Deklariert entweder eine normale oder eine "range-based" for-Schleife.
    Beispiel:

    C-Quellcode

    1. // Normal
    2. for (int i = 0; i < 20; ++i)
    3. {
    4. // Mach etwas
    5. }
    6. // Range-based
    7. int array[20] {};
    8. for (auto &element : array)
    9. {
    10. // Mach noch was
    11. }

    friend
    Erlaubt es einer Funktion/Klasse Foo die nicht Teil einer Klasse Bar ist, zugriff auf die privaten Member von Klasse Bar.

    goto
    Erlaubt es an eine andere Stelle zu springen die von einem Label gekennzeichnet wurde.

    Es wird schwerstens davon abgeraten dies heutzutage jemals zu verwenden, da es das Programm undurchschaubar macht.

    C-Quellcode

    1. void foo()
    2. {
    3. // Springt hierher
    4. start:
    5. // Irgendein code
    6. goto start;
    7. // Irgendein code
    8. }



    if
    Deklariert ein if-statement.

    Führt code aus wenn die Kondition zu true, bzw. 1, evaluiert, sonst, überspringt den Code.
    Siehe else
    inline
    [1]
    Für Funktionen/Variablen, erlaubt es Funktionen in mehreren TUs definiert zu werden, sie müssen aber identisch sein.

    Diese Funktionen/Variablen sind extern verknüpft.

    [2]
    Erlaubt es Benutzerdefinierten Namespaces behandelt zu werden, als wären sie Teil des übergeordneten Namespace.
    Beispiel 2:

    C-Quellcode

    1. namespace foo
    2. {
    3. inline namespace bar
    4. {
    5. // Alles hier drin kann verwendet werden als würde Namespace "bar" nicht existieren
    6. }
    7. }

    mutable
    Erlaubt einem Unterobjekt modifiziert zu werden auch wenn es Teil eines Konstanten Typs ist.

    C-Quellcode

    1. class Foo
    2. {
    3. mutable int x;
    4. int y;
    5. // const qualifizierte Funktion
    6. void bar() const
    7. {
    8. x = 2; // erlaubt, ist mutable
    9. y = 0; // compiler error, nicht mutable
    10. }
    11. }

    namespace
    Erlaubt es einen Namespace zu deklarieren.

    new
    Erzeugt ein neues dynamisches Objekt am Heap.
    Siehe delete
    noexcept
    Funktionen, spezifiziert mit noexcept, garantieren, dass sie keine Exceptions werfen.

    Eine Funktion die dennoch eine Exception wirft, trotz des Spezifizierers, schließt die Anwendung abnormal.

    nullptr
    Repräsentiert einen Zeiger der auf keine Addresse zeigt.

    Vergleichbar mit null aus anderen Sprachen.

    operator
    Erlaubt es einen Operator zu überladen.

    C-Quellcode

    1. class Foo
    2. {
    3. int operator+(int x)
    4. {
    5. return x + 1;
    6. }
    7. };
    8. Foo foo;
    9. int bar = foo + 1; // 2

    private, protected, public
    Deklariert Klassen-Member als entweder Öffentlich, Privat oder Geschützt.

    C-Quellcode

    1. class Foo
    2. {
    3. public:
    4. // Öffentliche Member
    5. protected:
    6. // Geschützte Member
    7. private:
    8. // Private member
    9. };

    register
    Ursprünglich als ein Speicherklassen spezifizierer, dafür um ein Objekt direkt in ein CPU-Register zu speichern.

    Heutzutage reserviert und unbenutzt.

    return
    Innerhalb einer Funktion, bricht die Funktion ab und gibt einen Wert zurück wenn nicht void.

    sizeof
    [1]
    Gibt die Größe eines Typs als std::size_t zurück.

    [2]
    In variadischen Templates, gibt die Länge an Argumenten zurück.
    Beispiel 1:

    C-Quellcode

    1. std::size_t größe = sizeof(int);


    Beispiel 2:

    C-Quellcode

    1. template<class …Typen>
    2. void foo()
    3. {
    4. std::size_t länge = sizeof…(Typen);
    5. }



    static
    [1]
    Spezifiziert eine globale Variable/Funktion als intern verknüpft.

    [2]
    Definiert/deklariert statische Funktionen/Variable innerhalb einer Klasse die aber nicht Teil einer Instanz sind.

    [3]
    Definiert eine statische Variable innerhalb einer Funktion die nicht lokal zur Funktion sondern global über alle Aufrufe der Funktion existiert.

    static_assert
    Deklariert eine zur Kompilierzeit evaluierte Kondition.

    Wenn diese Kondition nicht zu true evaluiert, ist es ein Compiler-Error und der Compiler wird gestoppt.

    switch
    Deklariert ein switch-statement.
    Siehe case
    template
    Deklariert ein Klassen/Funktions/Variablen-Template.

    this
    Der this-Zeiger, zeigt immer auf die derzeitige Instanz einer Klasse.

    thread_local
    Deklariert eine Variable die nur für den Bereich eines Threads gilt und unabhängig von der gleichen Variable in einem anderen Thread steht.

    throw
    Wirft eine Exception.

    C-Quellcode

    1. throw std::exception();
    2. throw std::runtime_error("bla bla bla");
    3. // Ect.

    try
    Deklariert einen try-catch Block der mögliche Exceptions abfängt, sollten sie auftreten.
    Siehe catch
    typedef
    Erzeugt einen Typ-Alias.

    C-Quellcode

    1. typedef int Foo;
    2. Foo foo = 1;

    typeid
    Frägt Information über einen Typen ab und gibt ihn als std::type_info zurück.

    C-Quellcode

    1. std::type_info info = typeid(Typ);

    typename
    [1]
    Als alternative für class in einer Template-Deklaration.

    [2]
    Für Untertypen eines Template-Typen welcher noch nicht instanziert wurde.
    Beispiel 1: Siehe class
    union
    Deklariert einen union-Typen, wessen Member alle denselben Speicher teilen.

    using
    [1]
    Als bevorzugte alternative für typedef, um einen Typen-Alias zu erzeugen.

    [2]
    Als Import-Statement um eine Entität aus einem Namespace oder auch um einen gesamten Namespace zu importieren.

    [3]
    Als Import statement um Vererbte Klassenfunktionen zu importieren.
    Beispiel 1:

    C-Quellcode

    1. using Foo = int;


    Beispiel 2:

    C-Quellcode

    1. using std::cout; // nun ohne std
    2. using namespace std; // alles aus std importiert

    virtual
    [1]
    Um eine virtuelle Funktion zu deklarieren die von einer erbenden Klasse überschrieben werden kann.

    [2]
    Um eine Klasse als virtuell zu vererben um mit dem "Diamant-Problem" klarzukommen.

    volatile
    [1]
    Deklariert eine Variable volatile, kann sie nicht wegoptimiert werden, außerdem, jede Operation die einen Effekt auf diese Variable hat kann nicht vor oder nach einer Operation mit dieser Variable umgeordnet werden.

    Anders als in anderen Sprachen, ist volatile in C++ kein Multi-threading Synchronisations-Keyword.

    [2]
    Ähnlich wie const, ist volatile auch ein Qualifizierer für Klassenmethoden.

    while
    Deklariert eine while-Schleife.

    C-Quellcode

    1. while (Bedingung)
    2. {
    3. // Mach etwas
    4. }




    Identifiers with special meaning
    Neben den Keywords gibt es noch diese Dinger (^) welche als Namen von Variablen/Funktionen verwendet werden können, aber an gewissen Stellen wie Keywords fungieren.
    Keyword
    Beschreibung

    final
    Eine Klasse mit diesem Identifier kann nicht vererbt werden.
    Eine virtuelle Funktion mit diesem Identifier kann nicht weiter überschrieben werden.

    override
    Als Teil einer Funktions-Deklaration/Definition, zeigt dieser Identifier dem Compiler das eine Funktion überschrieben werden soll.
    Dies ist ein optionales Ding, auch ohne kann eine Funktion überschrieben werden, allerdings hilft es als Dokumentation und verhindert das versuchte überschreiben von Funktionen die gar nicht überschreibbar sind.




    Operatoren
    Und natürlich, wie in so manch anderen Sprachen haben wir auch noch die Operatoren - ohne die läuft nix!
    Die brauchen wir natürlich dazu um Ausdrücken eine Richtung zu geben, zum Beispiel durch "+" oder "-".

    Ich gehe hier mal die typische Ordnung durch so wie es auch die meisten zu unterteilen vermögen.
    Des weiteren, werde ich noch eine Zusatzspalte hinzufügen welche Aufschluss darüber gibt ob ein Operator überladbar ist oder nicht.
    C++ erlaubt nämlich das überladen von Operatoren.

    Arithmetische Operatoren
    Name
    Beschreibung
    Überladbar
    Beispiel
    Addition
    Summiert zwei Operanden miteinander.
    Ja
    2 + 2 = 4
    Subtraktion
    Subtrahiert zwei Operanden miteinander.
    Ja
    4 - 1 = 3 (quick maths)
    Multiplikation
    Multiplziert zwei Operanden miteinander.
    Ja
    4 * 4 = 16
    Division
    Dividiert zwei Operanden miteinander.
    Ja
    4 / 4 = 1
    Unäres Plus
    Gibt den Wert des Operanden zurück.
    Ja
    +(4) = 4
    Unäres Minus
    Gibt den invers des Operanden zurück.
    Ja
    -(4) = -4
    Modulo
    Dividiert zwei Operanden miteinander und gibt den Rest zurück.
    Ja
    4 % 5 = 4
    Inkrement
    - Prefix
    - Postfix
    Inkrementiert den Operand.
    - Gibt den neuen Wert zurück
    - Gibt den alten Wert zurück
    Ja
    - ++4 = 5
    - 4++ = 4
    Dekrement
    - Prefix
    - Postfix
    Dekrementiert den Operand.
    - Gibt den neuen Wert zurück
    - Gibt den alten Wert zurück
    Ja
    - --4 = 3
    - 4-- = 4


    Vergleichsoperatoren
    Name
    Beschreibung
    Überladbar
    Beispiel
    Gleich
    Vergleicht zwei Operanden und gibt zurück ob sie dem gleichen Wert entsprechen.
    Ja
    0 == 2 = false
    Ungleich
    Vergleicht zwei Operanden und gibt zurück ob sie NICHT dem gleichen Wert entsprechen.
    Ja
    0 != 1 = true
    Größer als
    Vergleicht zwei Operanden und gibt zurück ob der linke Operand größer als der rechte ist.
    Ja
    1 > 0 = true
    Kleiner als
    Vergleicht zwei Operanden und gibt zurück ob der linke Operand kleiner als der rechte ist.
    Ja
    1 < 0 = false
    Größer oder gleich
    Vergleicht zwei Operanden und gibt zurück ob der linke Operand größer oder gleich dem rechten Operand ist.
    Ja
    1 >= 1 = true
    Kleiner oder gleich
    Vergleicht zwei Operanden und gibt zurück ob der linke Operand kleiner oder gleich dem rechten Operand ist.
    Ja
    1 <= 1 = true
    C++20 hat auch den Spaceship-Operator (oder Standardgerecht "three-way-operator") hinzugefügt. (<=>)
    Dieser vergleicht zwei Operanden und gibt eine Wert zurück der angibt ob diese Operanden gleich, kleiner oder größer sind.

    Logische Operatoren
    Name
    Beschreibung
    Überladbar
    Beispiel
    Logisches "Nicht"
    Konvertiert (wenn nicht bereits) den Operand zu einen bool und negiert den Wert.
    Ja
    !true = false
    !0 = true
    !289 = false
    Logisches "Und"
    Vergleicht zwei Operanden und gibt true zurück wenn beide zu true evaluieren.
    Ja
    true && true = true
    false && false = false
    true && false = false
    Logisches "Oder"
    Vergleicht zwei Operanden und gibt true zurück wenn mindestens einer der beiden zu true evaluiert.
    Ja
    true || true = true
    true || false = true
    false || false = false


    Bitweise Operatoren
    Name
    Beschreibung
    Überladbar
    Beispiel
    Bitweises "Nicht" oder Komplement-Operator
    Invertiert alle Bits eine Wertes. 0 werden zu 1, 1 werden zu 0.
    Ja
    ~5 (0101) = 10 (1010)
    Bitweises "Und"
    Vergleicht die Bits zweier Operanden und gibt einen Wert zurück in welchem die Bits auf beiden Seiten 1 waren.
    Ja
    5 & 1 (0101 & 0001) = 1 (0001)
    Bitweises "Oder"
    Vergleicht die Bits zweier Operanden und gibt einen Wert zurück in welchem die Bits auf mindestens einer der beiden Seiten 1 waren.
    Ja
    5 | 3 (0101 & 0011) = 7 (0111)
    Bitweises "Exklusives Oder"
    Vergleicht die Bits zweier Operanden und gibt einen Wert zurück in welchem die Bits auf nur einer der beiden Seiten 1 waren.
    Ja
    5 ^ 3 (0101 ^ 0011) = 6 (0110)
    Bitweises verschieben nach links
    Schiebt die Bits des linken Operanden um so viele Stellen nach links, wie der rechte Operand angibt.
    Ja
    5 << 3 (0101 << 3) = 40 (0010 1000)
    Bitweises verschieben nach rechts
    Schiebt die Bits des linken Operanden um so viele Stellen nach rechts, wie der rechte Operand angibt.
    Ja
    5 >> 2 (0101 >> 2) = 1 (0001)


    Zuweisungsoperatoren
    Name
    Beschreibung
    Überladbar
    Beispiel
    Zuweisung (=)
    Weißt den Wert des rechten Operanden dem linken Operanden zu.
    Ja
    var = 3
    Modifikation und Zuweisung (+=; -=; *=; /=; %=; &=; |=; ^=; <<=; >>=)
    Modifiziert den Wert des linken Operanden, in dem er die Operation vor dem Zuweisungsoperators mit dem rechten Operand durchführt.
    Ja

    C-Quellcode

    1. var = 6;
    2. var /= 3;
    3. (var ist 2)



    Zugriffs- und Zeigeroperatoren
    Name
    Beschreibung
    Überladbar
    Beispiel
    Index (Subscript)
    Greift auf einen indizierten Wert eines Arrays oder Objektes zu.
    Ja
    array[3]
    Dereferenzieren (Indirection)
    Greift auf das Objekt zu, auf welches die Adresse des Zeigers zeigt.
    Ja
    *var
    Addressierung (address-of)
    Erhält die Adresse eine Objektes, und gibt einen Zeiger mit dieser Addresse zurück.
    Ja
    &var
    Strukturdereferenzierung (structure dereference)
    Dereferenziert einen Zeiger und gibt sofortigen Zugriff auf die Member des Objektes.
    Ja
    instanz->member
    (alternative für "(*instanz).member")
    Member-zugriff (structure reference)
    Erlaubt Zugriff auf Member eines Objektes.
    Nein
    instanz.member
    Zeiger zu Member eines Objektzeigers (pointer-to-member)
    Erlaubt zugriff auf ein Member, eines Zeigers zu einem Objekt, welches als Zeiger erhalten wurde.
    Ja
    instanz->*member
    Zeiger zu Member eines Objektes
    Erlaubt zugriff auf ein Member, eines Objektes, welches als Zeiger erhalten wurde.
    Nein
    instanz.*member


    Sonstige Operatoren
    Name
    Beschreibung
    Überladbar
    Beispiel
    Funktionsaufruf
    Ruft eine vorher definierte Funktion auf.
    Ja
    function(parameter1, parameter2)
    Komma
    Evaluiert den linken Operand und wirft das Resultat weg. Das Resultat ist die Ausgabe des rechten Operands.
    Ja
    expr1, expr2
    Ternäre-Bedingung
    Ein abgekürztes if-statement, erlaubt das evaluieren einer Bedingung und gibt das Resultat zurück.
    Nein
    Bedingung ? <wenn true> : <wenn false>
    Bereichsauflösung (scope resolution)
    Erlaubt den Zugriff auf verschachtelte Enitäten, wie statische Member einer Klasse oder Entitäten eines Namespaces.
    Nein
    Namespace::Klasse::Unterklasse::statischeFunktion
    Benutzerdefinierte Literale
    Erlaubt es Benutzern eigene String-Literal-Anhängsel zu definieren und verwenden.
    Ja
    "Irgendein String"_meinLiteral
    sizeof, sizeof..., alignof, typeid
    Schon oben als Keywords angeschnitten, zählen auch zu den Operatoren.
    Nein
    sizeof(int)
    sizeof...(Types)
    alignof(int)
    typeid(int)
    Casting
    Konvertiert einen Typen zu einen anderen.
    Ja¹
    (int) var (C-Style Casting)
    int(var) (Function-Style Casting, equivalent zum C-Style cast)
    static_cast, reinterpret_cast, const_cast, dynamic_cast
    Konvertiert einen Typen zu einen anderen. Dem C-Style cast vorzuziehen.
    Ja¹
    static_cast<int>(var)
    New (dynamische Allokation)
    Erzeugt ein Objekt auf dem Heap anstelle von auf dem Stack.
    Ja
    new int() (Objekt-Erzeugung)
    new int[N] (Array-Erezugung)
    Delete (dynamische Deallokation)
    Löscht zuvor mit "new" erzeugte Datenpakete.
    Ja
    delete var(Objekt-Löschung)
    delete[] arrayVar (Array-Löschung)
    noexcept
    Markiert eine Funktion als noexcept. Funktion geht einen Vertrag ein das sie keine Exceptions werfen wird.
    Nein
    void meineFunktion() noexcept {}
    ¹ Diese Operatoren können nicht direkt überladen werden, jedoch verwenden diese Operatoren die überladenen Konversionsfunktionen des Objektes wenn welche definiert wurden.

    Das ist mal die grundlegende Auflistung der Operatoren. (Ich hoffe ich habe jetzt nichts vergessen, oof)
    Ein weiterer wichtiger Punkt ist die Priorität der Operatoren, im Englischen auch als Precedence bekannt.
    Wenn nämlich mehrere Operatoren in einem Ausdruck verwendet werden, geschieht die evaluierung nicht zwangsläufig von links nach rechts.
    Verschiedene Operatoren haben ein unterschiedliches Vorrangslevel, man kann sich das wie "Punkt vor Strich" vorstellen. (welches im Übrigen auch hier gilt)

    Des weiteren gibt es noch eine Eigenheit von Operatoren, die Assoziativität.
    Diese bezeichnet die Seite von welcher verschiedene Operatoren, wenn mehrere mit gleicher Priorität auf einmal verwendet werden, evaluiert werden.
    Heißt, zwei Operatorenausdrücke nebeneinander mit der selben Priorität werden mit ihrer genannten Richtung evaluiert.

    Ich liste mal die Prioritäten und Assoziativität nach Level auf, von höchster zu niedrigster. (hohe Priorität bedeutet das dieser Operator zuerst evaluiert wird)
    Prioritätslevel
    Operatoren
    Assoziativität
    1
    ::
    Links-zu-Rechts
    2
    var++, var--, func(), array[], ., ->, typeid, type(var),static_cast, reinterpret_cast, const_cast, dynamic_cast
    Links-zu-Rechts
    3
    ++var, --var, +var, -var, !var, ~var, (type)var, *zeiger,
    &var, sizeof, new, new[], delete, delete[]
    Rechts-zu-Links
    4
    .*, ->*
    Links-zu-Rechts
    5
    a * b, a / b, a % b
    Links-zu-Rechts
    6
    a + b, a - b
    Links-zu-Rechts
    7
    <<, >>
    Links-zu-Rechts
    8
    <=> (C++20)
    Links-zu-Rechts
    9
    <, >, <=, >=
    Links-zu-Rechts
    10
    ==, !=
    Links-zu-Rechts
    11
    a & b
    Links-zu-Rechts
    12
    a ^ b
    Links-zu-Rechts
    13
    a | b
    Links-zu-Rechts
    14
    a && b
    Links-zu-Rechts
    15
    a || b
    Links-zu-Rechts
    16
    a ? b : c, =, +=, -=, *=, /=, %=, <<=, >>=, &=, |=, ^=
    Rechts-zu-Links
    17
    ,
    Links-zu-Rechts

    Gut das wars dann mal wieder von mir für dieses mal.
    Wenn Fragen, Feedback oder sonstige Dinge aufkommen, wisst ihr ja wo ihr die festmachen könnt:
    Das C++-Tutorial für Einsteiger: Diskussions-Thread

    Ich wünsche euch, wie immer natürlich, noch viel Spaß und hoffentlich konnte ich wieder mal etwas hilfreich sein.
    Gut, dieses Kapitel war ja natürlich nicht sehr Diskussionsreich sondern eher nur eine Auflistung von Dingen, ist aber trotzdem wichtig dies einmal durchzunehmen.

    Man sieht sich!
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    Dieser Beitrag wurde bereits 11 mal editiert, zuletzt von „Elanda“ ()

    2. Die Basics

    2.7 Statements und Expressions


    Nun kommen wir zum atomaren Bestandteil eurer Programme. Wenn Statements die Atome sind, sind Expressions die Partikel, aus welchen diese bestehen.
    Deswegen passt es auch wie Arsch auf Eimer, wenn ich sage, dass beide elementare Konzepte sind. Sie dienen der Erschaffung einer gewissen Leitlinie, die ein Programm abarbeitet.

    Was sind denn nun Statements?
    Nun, Statements sind Anweisungen, die deiner Maschine eine Richtung geben.
    Sie werden, auch wenn nur scheinbar, nacheinander verarbeitet.

    Warum scheinbar?
    Weil dein Code einer sogenannten Regel zugrunde liegt.
    Statements können um-geordnet werden (oder präziser noch, Instruktionen können um-geordnet werden, da aber ein Statement aus mehreren Instruktionen bestehen kann, lass ich das mal so stehen),
    je nachdem wie der Compiler oder Prozessor bewerten kann wo noch Effizienz herausgeschlagen werden könnte.
    Aber man muss sich darum, solange man nicht im Multithreading bereich arbeitet, keine Sorgen machen, denn eine andere Regel lautet: Das um-ordnen von Instruktionen ist soweit erlaubt,
    wie es die Erwartung der Ausführung eines einzelnen Threads nicht beeinträchtigt. Heißt, wir machen uns hier noch keine Sorgen darum, je nachdem ob ich mich entscheide dieses Thema vielleicht später noch in Angriff zu nehmen.

    Nun wir wissen jetzt, dass Statements Befehlsangaben sind und wir wissen, wenn auch unnötig für diesen Kontext, dass Teile davon um-geordnet werden können.
    Was wir aber nicht wissen, ist, wie diese aussehen.

    Grob gesagt sind Statements die Zeilen aus Code, die in einem Funktionskörper aufgelistet werden.
    "Grob" weil es erlaubt ist mehrere Statements in einer Zeile zu schreiben, davon rate ich aber wärmstens ab, da das die Lesbarkeit eures Codes ungemein verschlechtert.

    In VB zum Beispiel geht das nicht von vornherein, man kann dies zwar mit dem ":" Charakter erreichen, es wäre meiner Kenntnis nach aber eher ungewöhnlich.
    (Ich Spreche jetzt von Code den ich hier größtenteils aufgeschnappt habe, meine Kenntnis über die Sprache ist beängstigend gering)
    In VB.Net ist es normalerweise nämlich ein Zeilenumbruch, welcher ein Statement beendet.
    In C++ aber, genauso wie in allen anderen Sprachen die auf C basieren (wie Java, C# ect.), beendet das Semikolon ein Statement ";".
    Das ist nicht optional, sondern notwendig. Somit weiß der Compiler, dass ein Befehl beendet ist, bevor er zum nächsten springt.

    Mal als Beispiel, dies

    C-Quellcode

    1. int var = 0;

    ist ein Statement (wenn innerhalb einer Funktion), dass eure Maschine Platz für eine Variable des Typs int machen soll und sie dann mit 0 initialisiert.
    Das sind dann schon mal mehrere Instruktionen, aber ein Statement.
    Somit sind Statements nichts anderes als ein Befehl, etwas so zu tun, wie wir das möchten.

    Es gibt verschiedene Arten von Statements: Expression-, Null-, Selection-Statements und andere, jedoch ist diese Unterscheidung nicht vital für dieses Tutorial.
    Die Unterscheidung, wenn nicht für theoretische Zwecke, ist kaum notwendig. Interessant ist für uns nur, dass die meisten Statements, Expression-Statements sind.

    Und was sind Expressions?
    Kurz gesagt, Expressions sind eine Aneinanderkettung von ein oder mehreren Operatoren und deren Operanden.
    Auch Literale zählen als Expressions, obwohl keine Operatoren vorhanden sind - somit müsste es also eher "keine oder mehrere Operatoren" heißen.
    Jede Expression hat einen Typen, welcher durch implizite Konversion und Promotion nach den oben genannten Regeln im Beitrag über Typen-Konversion resultiert.
    Es gibt viele Arten, darunter Integral Expressions; welche nur aus Ganzzahlen-Operationen bestehen oder Floating Point Expressions; selbes Spiel nur mit Fließkommazahlen oder auch sogenannte Compound Expressions;
    welche mehrere Sub-Expressions verschiedener Arten in einer vollen Expression miteinschließen.


    Tja das wars dann mal wieder.
    Zum Abschluss möchte ich mich entschuldigen, dass es etwas länger gedauert hat, es war in letzter Zeit viel los und die Motivation war auch eher auf andere Projekte fixiert.
    Aber zum Glück bin ich jetzt wieder zurück für die nächsten paar Kapitel!

    Ich weiß das dieses Kapitel jetzt nicht sehr lange war, aber es sollte ja auch nur der Grundaufklärung dienen, das nächste mal wird es wieder monströß, ist ja auch ein Monster-Thema. FUNKTIONEN YAYYYY!
    Für etwaige sonstige Fragen, Feedback oder Diskussionen könnt ihr dann wieder den Diskussions-Thread ansteueren, der sich hier zu meiner Rechten befindet: Das C++-Tutorial für Einsteiger: Diskussions-Thread

    Aber, freut euch schon mal auf den 27.11, denn dann kommt der nächste Teil, rechtzeitig zum ersten Advent!
    Ich wünsche euch noch einen schönen Abend Leute (oder Tag), tschüdelü <3
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Elanda“ ()

    3. Elemente der Sprache

    3.1 Funktionen


    Einstieg
    Eine Funktion ist etwas, das wir hauptsächlich dazu verwenden, Code ab-zu-spalten und auf andere Zugriffspunkte auf-zu-teilen.
    Unser erster Eindruck wäre somit, dass wir so unseren Code etwas schöner gestalten können.
    Als Beispiel, vergleicht mal das hier:

    C-Quellcode

    1. int main()
    2. {
    3. std::cout << "Gib Zahlen von 0 bis N aus\n";
    4. int n = 10;
    5. for (int i = 0; i <= n; ++i)
    6. {
    7. std::cout << "Zahl: " << i << "\n";
    8. }
    9. std::cout << "Zahlen ausgegeben!\n";
    10. }

    Dieses Beispiel erhöht eine Zahl bis 10 und gibt jeden dieser Schritte als Zahl in der Konsole aus. Jedoch haben wir die Möglichkeit, dies etwas schöner zu gestalten,
    als auch etwas verwertbarer und zwar mit Funktionen:

    C-Quellcode

    1. void berechneUndGibsMir(int n)
    2. {
    3. for (int i = 0; i <= n; ++i)
    4. {
    5. std::cout << "Zahl: " << i << "\n";
    6. }
    7. }
    8. int main()
    9. {
    10. std::cout << "Gib Zahlen von 0 bis N aus\n";
    11. berechneUndGibsMir(10);
    12. std::cout << "Zahlen ausgegeben!\n";
    13. }

    Na, sieht doch schon mal schöner aus, nicht wahr?
    Dieses Beispiel ist wieder mal etwas einfacher gehalten, aber wenn man mehr in die Entwicklung eintaucht, werden Funktionen immer größer und das Aufteilen macht dann umso mehr Sinn.
    Nicht zu vergessen, wir haben unserem Algorithmus nun auch einen klaren und deutlichen Namen gegeben: Günther!
    Ne spaß: berechneUndGibsMir, somit wissen wir auch was jene für eine Aufgabe hat.

    Im Grunde genommen könnte man abschließend sagen, dass Funktionen also einfach nur ein wertvolles Spielzeug dafür sind, Recht und Ordnung bei-zu-behalten.
    Jedoch würde das dann doch nicht so ganz zutreffen. Denn, auch wenn man mit Funktionen seinen Aufräum-Fetisch zwar größtenteils befriedigen möge,
    wird einem schnell klar, dass das wahrlich nicht die Grund-Existenzberechtigung sein kann.

    Zum ersten steht und fällt der Code mit einer Funktion, wir kennen sie nun bereits als die "main" Funktion. Dies ist der Eintrittspunkt in und der Austrittspunkt aus dem Programm. Alles,
    was in einem Programm passiert, beginnt in der "main" Funktion.
    Bereits 1000 Funktionsaufrufe tief im Wirrwarr?
    Der Beginn kam in der "main" Funktion.
    Ihr seht also, alles was in einem Programm abläuft, passiert zwangsläufig innerhalb von Funktionen.

    Zweitens, überlegt mal: Was, wenn dir einfällt, dass du es doch noch mal berechnen und es dir geben lassen möchtest? (betreffend unseres Beispiels oben)
    Die äußerst unartigen Konnotationen mal außen vor gelassen; überlegen wir uns in welcher Situation es von absoluter Wertschätzigkeit (bitte was?) sein könnte, abgesehen des eigenen Ordnungs-Dranges zuliebe,
    diesen einen Schnipsel in einen delegierten Bereich zu überstellen.
    Na, schon drauf gekommen?
    Es geht um die Wiederverwendbarkeit.

    Richtig… aber warte - was bedeutet das?
    Na denk doch mal nach, wenn dir jetzt einfällt: “Ah hoppla, ich brauch das ja noch einmal”, dann wäre es sehr wünschenswert, die Möglichkeit zu besitzen, denselben Code wiederverwerten zu können.
    Man könnte nun hergehen und einfach den Block kopieren und erneut an der Stelle einfügen, an dem er abermals zur Verwendung kommt. Könn’t ja klappen, sieht aber scheiße aus und riecht zudem auch noch so;
    eigens dafür gibt es eine Konventionelle Phrase die sich “Code Smell” nennt, wenn etwas nicht mit rechten Dingen zu geht, dann stinkt dein Code!
    Hier gibt’s keinen Grund, dies auf irgendeine Weiße zu beschönigen.

    Man bedenke, sollte sich der Code irgendwann ändern, müsste man diesen eben auch an jeder eingefügten Stelle ändern, damit er wieder “erwartete Resultate” ausspuckt. Das ist nervig,
    kratzt am Hintern und ist zu allem Übel auch noch ein Nährboden für Bugs. Aus genau diesem Grund ist es erstrebenswert, diesen Bereich an einem einzelnen Ort festzulegen, an dem es dann nur einer Änderung bedarf.
    Dieses Prinzip wird auch “DRY” genannt, kurz für “don’t repeat yourself” oder auf gut germanisch, “wiederhole dich nicht, oder ich wiederhole es dir zu zeigen, wie sehr ich dir den Tag versauen kann”, simpel.
    Logischerweise ist dies kein inhärentes C++ Prinzip, wird aber sehr gerne und oft darauf angewendet.

    Des Weiteren ermöglichen Funktionen den Aufruf an sich selbst, was “rekursiver Aufruf” genannt wird. Das ist ein Thema für ein anderes mal und geht schon sehr Tief in das Thema fortgeschrittene Algorithmik,
    für den schnellen Imbiss jedoch kurz erklärt:
    Man kann die Funktion sich selbst aufrufen lassen, das bedeutet, über sich selbst verwaltet die Funktion den Ablauf eines sehr speziell gestalteten Ablaufs von Instruktionen, um am Ende ein Ergebnis zu erhalten,
    welches direkt durch den vorangehenden Ablauf der Funktion beeinflusst wird.
    TLDR: Die Funktion füttert sich selbst WIEDERHOLT mit Daten, um ein gewisses Ergebnis zu erlangen.
    Rekursive Funktionen sollten jedoch selten gehalten werden, und es gibt in fast allen Fällen einen alternativen Weg, dasselbe Resultat zu bekommen. (in vielen Fällen reicht dazu eine Schleife
    vor allem in C++ können Schleifen besser optimiert werden als dies für rekursive Aufrufe, dank ihrer Komplexität, der Fall ist)

    Struktur
    Gut, ich glaube, das war jetzt genug Basiswissen; gehen wir jetzt an die Gewürzgurken! (enttäuschender Witz Elanda… einfach nur enttäuschend)

    Wie es auch mit Variablen der Fall ist, wird es auch in C++ für Funktionen anfänglich echt anstrengend.
    Denn hier gilt dasselbe Prinzip: Funktions-Definition und -Deklaration.

    Bevor wir uns aber an dieses Rabbit-Hole herantasten, begutachten wir den Aufbau einer Funktion mal etwas genauer.
    Achtung: Hier kommt hauptsächlich Theorie, ihr könnt diesen Teil gerne überspringen wenn ihr direkt zur Sache kommen wollt, allerdings werfen Compiler mit dieser Terminologie gerne um sich,
    daher ist es ganz sicher von Vorteil, sich damit etwas auseinanderzusetzen.

    Eine Funktion besteht aus zwei Hauptteilen: dem Funktionskopf und dem Funktionskörper.
    Der Funktionskörper ist der Teil einer Funktion, welcher den auszuführenden Code umschließt, dies ist an den Operatoren { und } zu erkennen.
    (wir haben oben bereits gelernt, dass eine Funktion dieser Operatoren die Bereichs-deklarierung ist)
    Dies betrifft aber wirklich nur den äußersten Bereich der Funktion, wird ein weiter Bereich innerhalb des Funktionskörpers mit diesen Operatoren definiert, ist das nicht eine eigene Funktion,
    sondern immer noch Teil der umschließenden Funktion; es ist zwar ein eigener Bereich, aber keine eigene Funktion, es fehlt ja auch der Funktionskopf.
    TLDR: Der Funktionskörper umschließt den Code, der ausgeführt werden soll, wenn diese aufgerufen wurde, nichts was folgt und auch nichts was davor kommt; nur den einen Bereich.

    Der Funktionskopf ist da etwas komplizierter zu verstehen, dieser ist sozusagen eine Personalie für den Funktionskörper. Also wenn der Körper das ist, was sie tut, ist der Kopf das, was sie ist.
    Wir haben hier im Grunde also eine 5W-Situation “Wer bist du” und “Was machst du”.
    Das Wie braucht uns nicht zu interessieren, das überlassen wir dem Compiler und auf das Wo kommen wir etwas später zurück.
    Das Wann betrifft unsere Eigeninitiative, ab welchem Punkt wir sie zum Einsatz kommen lassen.

    Wie dem auch sei, mit Hilfe des Funktionskopfes haben wir also einen Weg, um zu identifizieren, welche der möglicherweise 1000 Funktionen wir ansteuern wollen. Zu allem Überfluss,
    kann der Funktionskopf auch in noch drei andere Teile gegliedert werden:
    • Rückgabetyp
    • Signatur
    • Spezifizierer (noexcept, constexpr, storage class ect.)

    Als Beispiel eines Funktionskopfes nehmen wir unser vorheriges Beispiel in Augenschein:

    C-Quellcode

    1. void berechneUndGibsMir(int n)


    void ist unser Rückgabetyp, dieser erzählt dem Compiler, auf was er unsere Maschine vorbereiten muss, wenn wir die Funktion verlassen.
    Da wir aber void haben, was bedeutet, dass die Funktion am Ende ihres Ablaufs nichts zurückgibt, kann uns das egal sein.
    "void” steht für “die Leere”, daher auch: “nichts” zurückgeben!

    Auf der gegenüberliegenden Seite haben wir die Signatur berechneUndGibsMir(int n). Die Signatur ist dafür da, der Funktion die eigentliche Identität zu geben.
    Offensichtlich brauchen wir diese Identität, wir wollen ja Kontrolle über den Aufruf einer Funktion haben, oder um es genauer zu betiteln, Kontrolle über WELCHE Funktion wir aufrufen.
    Durch eben diese Funktionssignatur ist es uns möglich, verschiedene Funktionen zu definieren. Der kleinste Unterschied in der Signatur hebt eine Funktion von einer anderen ab und da der Rückgabewert NICHT Teil der Signatur ist,
    werden zwei Funktionen mit derselben Signatur, aber anderer Rückgabetyp, nicht unterschiedlich behandelt. (Dies betrifft nur normale Funktionen, bei Funktionen-Templates ist dies etwas anders)
    Es ist also nicht möglich, zwei Funktionen mit derselben Signatur, aber einem anderen Rückgabetyp, zu deklarieren.

    Die Faktoren, die eine normale Funktion von anderen also abheben, sind also nur in der Signatur anzutreffen:
    • Funktionsname
    • Funktionsbereich (Namespace, Klasse)
    • Parameter (die Namen der Parameter spielen keine Rolle für die Signatur)
    • Qualifizierer (diese werden wir dann in 4. Objektorientiertes Programmieren kennenlernen)

    Der Compiler wird all diese Teile aufschnappen und daraus einen Namen generieren, der die Funktion von anderen unterscheidet. Dieser Name nennt sich “mangled name”.
    Funktionen, die sich in der Signatur unterscheiden, sind gänzlich andere Funktionen und haben keine Relation zueinander. Funktionen, die sich durch Parameter unterscheiden, aber sehr wohl.
    Sie sind zwar auch als verschiedene Funktionen anzusehen, aber mit einer etwas anderen Perspektive. Man nennt diese “überladene Funktionen”, das heißt, sie besitzen denselben Namen,
    unterscheiden sich aber an den Parametern.

    Sehen wir uns mal ein paar Funktionsköpfe an, die sehr wohl als unterschiedliche Funktionen betrachtet werden, an (Notiz, wir werden uns hier nur die vom Compiler generierten Funktionsnamen für den MSVC Compiler ansehen,
    jeder Compiler macht es unterschiedlich, sollte uns aber nicht kratzen):

    C-Quellcode

    1. void berechneUndGibsMir(int n); // MSVC generiert: ?berechneUndGibsMir@@YAXH@Z
    2. void berechneUndGibsMir(); // MSVC generiert: ?berechneUndGibsMir@@YAXXZ
    3. void berechneUndGibsMir(int n, double f); // MSVC generiert: ?berechneUndGibsMir@@YAXHN@Z
    4. void berechneUndGibsDir(int n); // MSVC generiert: ?berechneUndGibsDir@@YAXH@Z

    Alle diese Varianten werden als eigene Funktion betrachtet, und müssen dementsprechend auch anders aufgerufen werden. (damit auch der letzte Compiler weiß auf welche Funktion wir anspielen möchten)

    Für den nötigen Kontrast, hier auch ein paar Beispiele die nicht als eigenständig betrachtet werden: (auch hier wieder, Fokus liegt auf MSVC)

    C-Quellcode

    1. void berechneUndGibsMir(int n); // MSVC generiert: ?berechneUndGibsMir@@YAXH@Z
    2. void berechneUndGibsMir(int f); // MSVC generiert: ?berechneUndGibsMir@@YAXH@Z
    3. int berechneUndGibsMir(int n); // MSVC generiert: ?berechneUndGibsMir@@YAHH@Z

    Oha, was ist denn das?
    Der letzte Teil lässt einen vermuten, dass der Rückgabetyp ja doch eine Rolle in der Signatur spielt. Ja, es stimmt schon, MSVC zieht auch den Rückgabetyp mit in den gemangelten Namen.
    Aber auch meines Wissens nach nur MSVC (gibt bestimmt auch andere), GCC und Clang machen das zum Beispiel nicht für “normale” Funktionen.
    Es ist auch deshalb nicht Teil der Signatur, nur des generierten Namens. Der Grund, weshalb MSVC sich entschied, den Rückgabetyp mit in diesen Namen zu nehmen, ist mir unbekannt, es lässt sich vermuten,
    dass es den Umgang mit Sprach-Features erleichtert (für die Compiler-Entwickler) und/oder aus Rückwärts Kompatibilitätsgründen.
    Wenn es jemand besser weiß, im Diskussionsthread kann mich ja jemand, der das Wissen besitzt, darüber aufklären und ich füge es dann an dieser Stelle hinzu.

    Nur zur veranschaulichung wie derselbe Vorgang mit GCC oder Clang aussieht:

    C-Quellcode

    1. void berechneUndGibsMir(int n); // _Z18berechneUndGibsMiri
    2. int berechneUndGibsMir(int n); // _Z18berechneUndGibsMiri

    Hier ist also kein Unterschied zu erkennen. (Immer diese Miri, ts ts ts)

    Wie dem auch sei, wenn es euch interessiert wie MSVC diese Namen anhand einer Funktionssignatur generiert, könnt ihr ja gerne mal diesen Artikel hier durchstöbern.
    Ich möchte nur daran erinnern, dass es keine notwendige Information für die Entwicklung mit C++ ist, wenn ihr nicht auch unbedingt tief in die Materie eintauchen wollt.
    Ich kenne es auch nicht auswendig und habe trotzdem die Möglichkeit, dieses dürftige Tutorial zusammenzustellen.

    Die Signatur ist wichtig für uns, der gemangelte Name nicht unbedingt, da der ja sowieso von Compiler zu Compiler unterschiedlich sein wird. (allerdings werden diese "mangled names" in vielen Fehlermeldungen mit einbezogen, vorallem für den Linker,
    also vollkommen Sinnlos ist das Wissen auch wieder nicht; wenn man es denn hat)

    Somit wissen wir jetzt mal bis zu dieser Stelle:
    • Der Funktionskopf erklärt uns wer diese Funktion ist, anhand dieser können wir sie identifizieren und beschreiben
    • Der Funktionskörper wiederum zeigt uns auf was diese tut, was wird passieren wenn wir diese Funktion aufrufen

    Definition/Deklaration
    Nun interessiert uns aber auch noch das Wo.

    Hier gehen wir wieder ein paar Schritte zurück, auf ein älteres Kapitel 2.4 Variablen.
    “Weißt du noch Vitali, damals im Kapitel über Variablen? Hmm… Als wir über external/internal Linkage gesprochen haben? Oder als wir gelernt haben, dass es Definition und Deklaration gibt? Weißt du das noch?”
    Ja, dieses Thema werden wir so schnell nicht los. Wie es auch für Variablen der Fall war, stimmen Funktionen mit, mehr oder weniger, denselben Regeln überein. Wir haben auch hier wieder verschiedene Regeln hinsichtlich der Sichtbarkeit und der Linkage.

    Wer sich erinnern kann, wir haben damals eine Variable über unserer Main-Routine definiert und haben gelernt, dass diese nicht nur für diese TU sichtbar war, sondern auch für alle anderen, da sie automatisch extern verlinkt werden,
    wenn wir nicht explizit dazu auffordern, es intern verlinken zu lassen; so ist das grundsätzlich auch hier. Nun, der Hauptunterschied zu Variablen ist, dass es so etwas wie lokale Funktionen nicht gibt.
    Theoretisch kann man so etwas mit sogenannten “Lambdas” simulieren, aber das würde ich jetzt nicht als herkömmliche Funktion bezeichnen.
    Wir haben also nur Funktionen, die es im Namespace-Scope gibt. (und innerhalb von Klassen natürlich auch)
    Somit ist die Wo Frage für jedwede Funktion ein wichtiger Punkt.

    Übernehmt mal das erste Beispiel oben, mit der "main" und unserer Günther berechneUndGibsMir Funktion, in unsere ProjektName.cpp.
    Thema Header/Source sind wir ja schon früher mal durchgegangen, jetzt kommt es mal zum Einsatz!

    Da wir die Funktion berechneUndGibsMir in unserer Source deklariert und definiert haben, wird diese auch in anderen Source-Dateien (.cpp) verfügbar sein, da sie ja automatisch extern verlinkt wurde.
    Wir dürfen die Funktion in verschiedenen Sourcen deklarieren, aber in nur einer definieren. Weil, noch mal, alle Sourcen am Ende zusammengetragen werden und dann sowieso alle im gleichen Bereich enden.

    Wir können das ja mal für uns testen:
    1. In Visual Studio, fügt eine neue .cpp Datei hinzu und benennt sie nach belieben.
    2. Innerhalb dieser neuen .cpp Datei, ich nenne die jetzt einfach mal Definition.cpp,
      aus der ProjektName.cpp schneiden wir jetzt die ganze Funktion berechneUndGibsMir aus und fügen sie in unsere neue Source-Datei ein.
    3. Wir gehen nun zurück in unsere Haupt-Source-Datei, und ihr werdet sehen, dass VS sich darüber beschwert, das unserer Funktion berechneUndGibsMir nicht mehr existiert.

    Das ist wichtig und richtig!
    Wir haben zwar die Funktion für unser gesamtes Programm nun definiert, aber unsere Haupt-Source-Datei kennt die Identität der Funktion nicht, da sie sich woanders aufhält.
    Um die Definition nun in unserer Haupt-Source-Datei einzuspeisen, müssen wir diese Funktion nun deklarieren. Das geht ganz einfach, wir müssen nur den Funktionskopf,
    vor dem Punkt an dem sie in Verwendung tritt (ganz wichtig), deklarieren, das geht so:

    C-Quellcode

    1. void berechneUndGibsMir(int n); // Dies hier ist die Deklaration
    2. int main()
    3. {
    4. std::cout << "Gib Zahlen von 0 bis N aus\n";
    5. berechneUndGibsMir(10);
    6. std::cout << "Zahlen ausgegeben!\n";
    7. }

    Im Grunde genommen also einfach nur eine normale Funktion, aber halt ohne ihren Körper. Damit teilen wir den Compiler mit, dass irgendwo eine Funktion existiert, die aber nicht hier definiert wurde.
    Anders als bei Variablen müssen wir hier nicht das extern keyword anhängen. Man kann wenn man will, muss man aber nicht, es sollte keinen Unterschied machen. Nun startet ihr das Programm
    und ihr solltet ein erwartetes Ergebnis erhalten.

    Die Frage, wofür wir nun unseren Header brauchen ist berechtigt, denn, der Header ist genau das was wir hier gemacht haben nur etwas “cleaner”.
    Im Grunde genommen ist unser Header nichts anderes als eine Source-Datei, mit der Ausnahme, dass dieser nicht kompiliert wird. Die Header sind dafür da, diese Deklarationen zu beherbergen, damit wir diesen dann inkludieren können.
    Wir können unsere Deklaration in unseren ProjektName.h Header einfügen und am Kopfe unserer ProjektMain.cpp ein #include “ProjektName.h” einfügen. Somit ist unsere Deklaration nun für mehrere Source Dateien verfügbar,
    wenn diese denn den Header inkludieren. Das macht natürlich nur Sinn, wenn wir die Funktion auch in mehreren Dateien haben möchten, weniger Sinn würde es machen wenn die Funktion nur für eine Source-Datei erreichbar sein muss
    dann kann man die Funktion auch wirklich nur auf diese begrenzen.

    Wichtig ist, eine Funktion (wie auch Variablen) können nur verwendet werden, wenn sie für den Compiler sichtbar sind, bevor sie verwendet wurden.
    Als Beispiel:

    C-Quellcode

    1. int main()
    2. {
    3. std::cout << "Gib Zahlen von 0 bis N aus\n";
    4. berechneUndGibsMir(10);
    5. std::cout << "Zahlen ausgegeben!\n";
    6. }
    7. void berechneUndGibsMir(int n)
    8. {
    9. for (int i = 0; i <= n; ++i)
    10. {
    11. std::cout << "Zahl: " << i << "\n";
    12. }
    13. }

    Das wird nicht klappen, da unsere Funktion erst nach der Verwendung definiert wurde.
    Wir können allerdings die Funktion vorher deklarieren, damit der Compiler weiß, dass sie existiert:

    C-Quellcode

    1. void berechneUndGibsMir(int);
    2. int main()
    3. {
    4. std::cout << "Gib Zahlen von 0 bis N aus\n";
    5. berechneUndGibsMir(10);
    6. std::cout << "Zahlen ausgegeben!\n";
    7. }
    8. void berechneUndGibsMir(int n)
    9. {
    10. for (int i = 0; i <= n; ++i)
    11. {
    12. std::cout << "Zahl: " << i << "\n";
    13. }
    14. }

    Der Compiler weiß somit, dass die Funktion, welche in main verwendet wird, irgendwo existiert, aber nicht genau wo. Wenn wir die Definition entfernen würden, würde der Linker einen Aufschrei machen,
    da unsere Deklaration keine Definition hat.
    Also ist es immer wichtig, unsere Entitäten, die wir zuvor deklarieren, auch irgendwo erreichbar zu definieren ABER NIEMALS MEHR ALS EINMAL! (wenn sie extern verlinkt ist)

    Parameter und Rückgabetypen
    Ein weiterer wichtiger Punkt sind Parameter und Rückgabetypen.

    Parameter sind unsere Eingaben. Wir geben der Funktion Daten, mit welchen sie arbeiten kann, um anhand dieser Daten ein gewünschtes Ergebnis zu erzeugen.
    Eine Funktion kann keine bis unendlich viele Parameter besitzen. (theoretisch unendlich, es ist sehr warscheinlich das es gewisse Grenzen hier gibt welche von der Plattform und dem Compiler abhängen)

    Als zweites Stück dazu gibt es noch einen Rückgabetyp.
    Dieser Rückgabetyp bestimmt, ob am Ende des Durchlaufes der Funktion, ein Wert an der Stelle, an dem wir die Funktion aufgerufen haben, wieder “ausfließt”. Bedeutet, die Funktion gibt einen Wert zurück.
    Der Rückgabetyp kann sein, was auch immer man will, der bekannteste wird aber wohl void sein. Wie schon genannt, bedeutet dies, dass die Funktion nichts zurückgibt und alles wertvolle nur innerhalb der Funktion abläuft.

    Machen wir uns nun eine neue Funktion, welche alle Zahlen von 0 bis n summiert und zurückgibt:

    C-Quellcode

    1. int summiereUndGibsMir(int n) // [0]
    2. {
    3. int ergebnis = 0; // [1]
    4. for (int i = 0; i <= n; ++i) [2]
    5. {
    6. ergebnis += i; // [3]
    7. }
    8. return ergebnis; // [4]
    9. }


    [0]
    Hier haben wir wieder unseren Funktionskopf, wenn wir diesen aber genauer betrachten, fallen uns zwei Dinge auf:
    Der Rückgabetyp ist int, wir geben dem Compiler somit die Information, dass unsere Funktion einen Ganzzahlwert zurückgibt, sobald wir die Funktion verlassen.
    Wir haben einen Ganzzahl-Parameter mit dem Namen “n” (nochmals, der Name spielt keine Rolle für die Signatur, das ist nur eine Benennung, damit wir wissen wie wir innerhalb des Funktionskörpers darauf zugreifen)

    [1]
    Hier erstellen wir eine lokale Variable, das ist sozusagen unser Anker, welchen wir benötigen, um den Wert an dem wir herumhantieren zwischenzuspeichern.
    Wir initialisieren diesen auf 0, da, wenn wir das nicht machen, nicht sicher sein können, welchen Wert diese lokale Variable beinhalten könnte.
    Somit sind wir sicher, dass es 0 sein wird!

    [2]
    Ignorieren wir mal den Fakt, dass wir noch nichts über Schleifen gelernt haben und fokussieren uns stattdessen darauf, dass wir hier auf unseren Parameter n zugreifen.
    Kurz gesagt, hier wird einfach nur getestet, dass die Schleife nur so lange durchläuft wie i kleiner oder gleich der Wert n ist, den wir am Funktionsaufruf als 10 übergeben haben.
    Es wird also 10 mal mit “ergebnis” summiert.

    [3]
    Hier summieren wir den derzeitigen Wert mit unserer lokalen ergebnis Variable.
    Das machen wir mit dem Additionszuweisungs-Operator (+=), was nichts anderes bedeutet als den derzeitigen Wert aus ergebnis auszulesen, i hinzuaddieren und es dann wieder an ergebnis zuzuweisen.
    Mühsamer sieht das so aus: ergebnis = ergebnis + i;, es ist also nur etwas weniger wortreich.

    [4]
    Das ist der Punkt, an dem wir die Funktion verlassen und den Wert, der durch den Rückgabetyp definiert wurde, zurückgeben.
    Ein return bedeutet immer die Funktion ab diesem Punkt zu verlassen.
    Bedeutet, hätten wir return 0; als erste Zeile vor unserer lokalen Variable geschrieben, würde die Funktion ab diesem Punkt beendet und alles folgende würde ignoriert werden und die Funktion würde den Wert 0 zurückgeben.

    Wenn wir diese Funktion nun aufrufen, können wir das Ergebnis auch weiter verwenden:

    C-Quellcode

    1. int summiereUndGibsMir(int n)
    2. {
    3. int ergebnis = 0;
    4. for (int i = 0; i <= n; ++i)
    5. {
    6. ergebnis += i;
    7. }
    8. return ergebnis;
    9. }
    10. int main()
    11. {
    12. int ergebnis = summiereUndGibsMir(10);
    13. std::cout << “Das Ergebnis ist:<< ergebnis << “\n”;
    14. }

    int ergebnis = summiereUndGibsMir(10);, hier geschieht die ganze Magie.
    Wir rufen die Funktion auf, übergeben 10 als den “n” Parameterwert, die Funktion wird anhand dieses Wertes durchlaufen und mit Hilfe unseres “return” den errechneten Wert zurückgeben.
    Auf der linken Seite des “=” sehen wir, dass wir die Funktion in eine neue lokale Variable abspeichern, das ist wie wir an den zurückgegebenen Wert rankommen. Wäre der Rückgabetyp void,
    würde VS wieder mal rumeiern, da die Funktion ja nichts zurückgibt und schon gar keinen int kompatiblen Wert.
    Übrigens, nein, die neue Variable muss nicht zwingend auch “ergebnis” heißen, dass habe ich jetzt nur hier für die Benennung so genommen weil ich zu dumm bin mir etwas besseres einfallen zu lassen. (OMG :o)

    Wie gesagt, man kann so viele Parameter deklarieren, wie man möchte, man sollte aber beachten, dass man wirklich nur so viele deklariert, wie man auch benötigt; sonst wirds hier wie in der Folterkammer.
    Wir können auch hier ein neues Beispiel erstellen.
    Dieselbe Funktion wie oben, aber wir wollen einen anderen Startwert als 0 haben:

    C-Quellcode

    1. int summiereUndGibsMir(int start, int n)
    2. {
    3. int ergebnis = start;
    4. for (int i = 1; i <= n; ++i)
    5. {
    6. ergebnis += i;
    7. }
    8. return ergebnis;
    9. }


    Und zum aufrufen wieder:

    C-Quellcode

    1. int main()
    2. {
    3. int ergebnis = summiereUndGibsMir(20, 10);
    4. std::cout << “Das Ergebnis ist:<< ergebnis << “\n”;
    5. }

    Hier starten wir mit dem Startwert 20 und addieren zu den bereits gesetzten 20 einfach dazu, easy peasy oder?
    Man beachte wieder, start ist wieder einer unserer Parameter auf den wir zugreifen und wird zu ergebnis zugewiesen.

    Was hier nun interessant ist, ist, dass wir hier zwei Funktionen mit demselben Namen haben.
    Der einzige Unterschied ist die Parameterliste. Diese zwei Funktionen werden als zwei unterschiedliche Funktionen betrachtet, da wir ja gelernt haben, dass die Parameter, als Teil der Funktionssignatur
    bestimmen, wer die Funktion ist.

    Funktionen, die sich nur anhand ihrer Parameter unterscheiden, nennen sich “überladene” Funktionen.
    Wenn man also eine bestimmte dieser aufrufen möchte, muss man acht geben, welche Parameter man übergibt.
    summiereUndGibsMir(10) ruft eine andere Funktion auf als das es summiereUndGibsMir(20, 10) das tut.
    Das gilt nicht nur für die Anzahl der Parameter, sondern auch für den Typen. Haben wir Beispielsweise zwei Funktionen mit nur einem Parameter, wobei eine int nimmt und die andere double, kommt es darauf an,
    was wir übergeben, um zu selektieren, welche Funktion gewählt wird.

    Dieses gesamte System nennt sich “Overload Resolution” und ist der Begriff dafür, den der C++ Compiler verwendet, um die best passendste Funktion auszuwählen, die am besten an die übergebenen Parameter passt.
    Dies geschieht, wenn die Funktion nicht anhand ihres Namen differenziert werden kann und anschließend dessen, mehrere Funktionen mit dem gleichen Namen gefunden wurden. Somit analysiert der Compiler die Signatur und die Daten,
    die übergeben werden und wenn es am besten passt, wird diese Funktion gewählt.
    Das bedeutet auch, dass wenn für einen bestimmten Typen keine passende Überladung existiert, der Compiler eine Variante sucht, die aber dennoch passend sein könnte, durch beispielsweise Konversion.
    Wenn also, zum Bleistift, kein int overload existiert, jedoch eine double variante, selektiert der C++ Compiler auch die double Variante für int Werte
    und konvertiert den Ganzzahlwert in einen Fließkommawert und übergibt diesen dann.

    Auch noch sehr sehr wichtig zu erwähnen ist, dass die Evaluierung der Parameter sowohl als auch die Übergabe der Daten an die Funktion, nicht festgesetzt ist.
    Es kann sein das zuerst der erste Parameter übergeben wird, dann der nächste, also von links nach rechts, oder aber auch das zuerst der letzte übergeben wird, also von rechts nach links.
    Daher sollte man nicht davon ausgehen, dass Parameter, die zusammenhängend übergeben werden, auch in einer bestimmten Reihenfolge evaluiert und übergeben werden.
    Deshalb ist es immer am besten, jegliche Operationen vor dem Aufruf der Funktion zu erfüllen, die sich gegenseitig beeinflussen.

    Als Beispiel:

    C-Quellcode

    1. int wert = 420;
    2. int ergebnis = summiereUndGibsMir((wert += 246), (wert += 300));

    Man möge davon ausgehen, dass der erste Parameter ganz sicher 666 übergeben wird, da aber die Reihenfolge der Evaluierung nicht fix ist, könnte dieser Wert dann bereits schon 720 haben und da werden die 246 dann noch rauf gepackt.
    Also… Obacht!
    Die Reihenfolge für beide Vorgänge hängt wieder mal stark von der Plattform, dem Compiler und der sogenannten “Calling Convention” ab. Auch wieder ein Wort, auf das ich hier nicht näher eingehen möchte, nur kurz anteasern für die
    die etwas Interesse daran haben könnten. (I am so evil ;))

    "Main" erklärt
    Zum Abschluss möchte ich euch noch einmal durch die "main" Funktion begleiten.
    Im "Hello World" Kapitel haben wir die schon angeteasert, aber ich denke jetzt wäre der richtige Zeitpunkt, die mal knackig durchzunehmen.

    Anders als… ja… so ziemlich alle anderen Funktionen, gibt es für die "main" Funktion bestimmte Vorgaben. Einmal die Argumentlose und einmal die Argumentstarke Variante.
    Sehen wir uns erst einmal die einfachere Variante an:

    C-Quellcode

    1. int main()
    2. {
    3. // Programm code
    4. return 0;
    5. }

    Das ist die trivialste Variante.

    Die, die wir am häufigsten, im restlichen Verlaufe dieses Tutorials, sehen werden; da wir nicht wirklich jedes Mal mit Argumenten arbeiten werden.
    Das return 0; hier ist etwas speziell für die "main" Funktion.

    Grundsätzlich, wie auch schon oben erwähnt, ist "main" die einzige Methode, die nicht zwingend ein "return" braucht, trotz des nicht-"void" Rückgabetyps. In diesen Fällen gibt sie 0 zurück, aber nochmals,
    der Verständlichkeit halber würde ich es trotzdem hinzufügen.

    Dann gibt es noch die Argument Version:

    C-Quellcode

    1. int main(int argc, char *argv[]) // oder auch "char **argv"
    2. {
    3. // Programm code
    4. return 0;
    5. }

    Wofür steht "Argument"?
    Argumente sind Wörter, die dem ausführbaren Pfad im Konsolenfenster folgen.
    Beachtet das "Wort" sich hier auf alles bezieht, das durch Leerzeichen getrennt wird, also auch wenn die Bestandteile reine Zahlen sind.
    Das ist jetzt keine offizielle Bezeichnung, aber für den Sinn dieser Erklärung, hab ich das nun so festgesetzt. (fight me)

    Wenn eine Anwendung über die Konsole gestartet wird, sieht die Ausführzeile in etwa so aus: c:\pfad\zum\Programm.exe wort1 wort2 wort3
    Wie man hier sieht, werden die jeweiligen "wortX" an die Parameter von "main" übergeben, wobei "argc" für die Anzahl steht und "argv" für ein zweidimensionales Array, welches die Wörter beinhält.
    Das Asterisk (*) ist ein Zeiger, welcher auch für Arrays verwendet werden kann, aber dazu im Thema über Zeiger und Referenzen, später mehr.

    Na da ist's wohl wieder etwas länger geworden als erwartet, aber was soll man machen, es ist ein komplexes Thema und ein komplexes Thema benötigt eben Platz.
    Wir haben uns jetzt mal endlich um ein sehr zentrales Thema gekümmert, Funktionen sind ein sehr elementarer Bestandteil eines jeden Programms und um die kommt man nicht herum; aber warum auch?
    Ich würde euch vorschlagen, euch mal selbst in eine Experimentier-Situation zu versetzen und mal ein wenig zu werkeln.

    Ich weiß ich könnte hier jetzt ein paar Hausaufgaben geben und Beispiele bringen, ich muss euch aber leider dahingehend enttäuschend.
    Ich bin, was Lernbeispiele angeht, sehr doof, villeicht möge jemand anders ein paar aufbringen?

    Wie auch immer... ich hoffe der Artikel hat euch Spaß gemacht und hoffentlich
    auch das eine oder andere beigebracht.
    Wenn nicht tut mir das Leid, ich gebe mein Bestes (doch manchmal ist das leider nicht gut genug)
    Hier könnt ihr wieder Feedback, Wünsche und/oder Anregungen an mich bringen: Das C++-Tutorial für Einsteiger: Diskussions-Thread

    Ich wünsche euch noch einen schönen ersten Advent und habt Spaß! <3
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „Elanda“ ()

    3. Elemente der Sprache

    3.2 Flow-Control


    Was ist "Flow"
    Bevor wir mit "Flow-Control" beginnen, sollten wir zuerst den Terminus "Flow" etwas näher begutachten.
    Oder besser gesagt: Flow of Control (Kontrollfluss; der Fluss der Kontrolle)

    In einem C++-Programm liegt diese Kontrolle im geistigen Zeigefinger des Entwicklers, oder anders ausgedrückt, ist es deine Fähigkeit, aufzuzeigen, wie du dir den Fluss eines Programms vorstellst.
    Dieses Bild in deinem Kopf versuchst du dann im Zuge der Verwirklichung in einem "Fluss" von Befehlen widerzuspiegeln, die der Computer verstehen kann.
    Um es deutlicher auszudrücken: Der Fluss ist nichts anderes als die chronologische Abfolge eines Stapels an Kommandos, die du einem Rechner anvertraust.

    Der "Fluss" ist wichtig, da wir zum erfolgreichen Ausführen eines Programmes eine gewisse Ordnung brauchen, andernfalls hätten wir Mayhem.
    Es ist wichtig zu berücksichtigen, dass wir hier nur vom oberflächlichen "Flow" sprechen, daher auch der spirituelle Trip im 2ten Absatz.
    Sobald ein Programm kompiliert und ausgeführt wird, liegt es an der Implementation, dem OS und dem Prozessor, diese Befehle zu verwerten, und das entspricht dann meistens einem eigens definierten "Fluss", da separat davon noch Memory-Reordering und Optimisationen existieren und unser initial definierter Fluss für die Katz ist.
    Das Programm entspricht im Großen und Ganzen immer noch unserer Vorstellung, aber einige Dinge werden umsortiert oder ersetzt, was speziell im Falle von Multi-Threading ärgerlich werden kann.

    Also noch einmal:
    Der Kontrollfluss entspricht unserer oberflächlichen Vorstellung einer gewissen Ordnung an Befehlen an den Computer, welche eingehalten werden sollten, um ein erfolgreiches Resultat nach Wunsch zu erhalten.

    Gut, wir hätten nun einmal eine gewisse Sichtweise für den Kontrollfluss, wie aber beeinflussen wir diesen?
    Nun, im Grunde tust du dies mit jedem Statement, das du schreibst - dies ist der sogenannte "Lineare Kontrollfluss" oder "Sequentieller Kontrollfluss".
    Linear, da es einer gewissen Linie folgt, die wir mit jedem Befehl erweitern, jeder Befehl geht in den nächsten über:


    Nun aber können wir diese "Linearität" aushebeln oder besser gesagt, etwas ausbauen.
    Denn wir haben hier etwas, das sich "Flow-Control" Statements nennt, womit wir explizit auf diesen Zug aufspringen können.
    Mit diesen "Flow-Control" Statements manipulieren wir den Fluss auf eine Weise, die es uns erlaubt, nicht-Linear zu denken und zu beurteilen.

    Bevor wir nun zu den Kandidaten kommen, möchte ich nur kurz anmerken, dass es nicht ungemein wichtig ist, über den "Fluss" explizit nachzudenken.
    Klar, wir haben eine gewisse Ordnung, die wir unbedingt einhalten müssen, damit wir das erhalten, was wir uns vorstellen, aber wenn man schon länger im Prozess ist, geht das automatisch.
    Was ich damit sagen will, ist, dass du "Flow-Control" Statements nicht dazu verwenden solltest, um explizit den Fluss zu beeinflussen, sondern weil du sie benötigst, um ein gewisses Ziel zu erreichen.
    Wir kommen zwar erst noch zu den Schleifen, aber nehmen wir die kurz als Beispiel: Verwende Schleifen nicht, weil du keine Linearität wünscht, sondern weil du sie für etwas Bestimmtes brauchst.
    Es ist aber dennoch interessant (wie ich finde), über diese Dinge nachzudenken, daher habe ich es auch angeschnitten.

    Gut, nun sollten wir uns langsam aufmachen endlich das eigentliche Thema anzuschneiden.
    Die "Flow-Control" Statements.

    C++ bietet uns einige dieser an, welche du vielleicht aber auch schon aus anderen Sprachen kennen dürftest.
    Im groben wird stark unter-kategorisiert:
    • Selection Statements (if, if-else, switch)
    • Iteration Statements (for, while, do-while)
    • Jump Statements (break, continue, return, goto)
    Na dann, shall we?

    if, if-else
    Die wohl geläufigsten wären die if und if-else Statements. Diese erlauben es uns, einen Block von Befehlen nur dann auszuführen, wenn eine gewisse Voraussetzung erfüllt ist.
    Diese Voraussetzung, wie auch der Befehlsblock, liegt in unseren Händen.

    Diese Statements werden auch "Branching Statements" genannt und die einzelnen Blöcke "Branches". (Zweige)
    Ein Zweig ist ein potenziell ausführbarer Bereich für den Fall, dass die Kondition zutrifft.

    Nehmen wir ein Beispiel:

    C-Quellcode

    1. if (var == 0)
    2. {
    3. std::cout << "Es war 0\n";
    4. }

    Hier haben wir ein if Statement, welches den Wert von var mit dem Nummernliteral 0 abgleicht.

    Wenn nun var (irgendein Objekt, das wir vorher als int deklariert haben) 0 beinhaltet, wird alles innerhalb des folgenden Klammerpaares ausgeführt werden.
    Aber auch nur dann, wenn es auch wirklich zutrifft, ansonsten wird alles darin übersprungen und das Programm geht unterhalb weiter.

    Bitte merke dir, dass Vergleiche immer mit dem doppelten == Operator ausgeführt werden, DENN, in C++ ist es möglich innerhalb von if Statements Zuweisungen durchzuführen.
    In unserem Fall würden wir also auf 0 zuweisen und dann würde var Implizit zu bool konvertiert werden, und da 0 false entspricht, würde dieser Zweig fälschlicherweise nicht ausgeführt werden.

    Wir haben aber auch die Möglichkeit, eine Alternative anzubieten, das heißt, etwas, das nur dann ausgeführt werden soll, sollte die Kondition NICHT zutreffen.
    Dafür haben wir das else Keyword:

    C-Quellcode

    1. if (var == 0)
    2. {
    3. std::cout << "Es war 0\n";
    4. }
    5. else
    6. {
    7. std::cout << "Es war nicht 0, sondern: " << var << "\n";
    8. }

    In diesem Fall, wenn var nicht 0 ist, wird der Block nach else ausgeführt und aber auch nur dann.

    Um dies noch etwas mehr zu verkomplizieren, gibt es auch noch if-else if Statements.
    Denn, wie wir bis jetzt beobachten konnten, gab es immer nur ein bis zwei Zweige, woher soll denn der dritte Zweig kommen, es gibt hier ja nur ein Entweder-oder?
    Das erlaubt uns der nächste Kandidat:

    C-Quellcode

    1. if (var == 0)
    2. {
    3. std::cout << "Es war 0\n";
    4. }
    5. else if (var == 1)
    6. {
    7. std::cout << "Es war 1\n";
    8. }
    9. else
    10. {
    11. std::cout << "Es war nicht 0 oder 1, sondern: " << var << "\n";
    12. }

    Mit diesen, kann man ein if und ein else kombinieren, vergleichbar wäre es mit:

    C-Quellcode

    1. if (var == 0)
    2. {
    3. std::cout << "Es war 0\n";
    4. }
    5. else // war nicht 0
    6. {
    7. if (var == 1) // neuer check, ist es villeicht doch eher 1?
    8. {
    9. std::cout << "Es war 1\n";
    10. }
    11. else
    12. {
    13. std::cout << "Es war nicht 0 oder 1, sondern: " << var << "\n";
    14. }
    15. }

    Man beachte, dass dies nur in diesem Beispiel vergleichbar ist, beide Versionen erlauben unterschiedliche Dinge, aber für den Vergleich passt es auf Auge wie Goethes Faust.

    Nun kann man dies auch etwas komplizierter gestalten, mit sogenannten logischen und relationalen Operatoren, dafür hätten wir:
    • &&: Logischer UND Operator, kombiniert zwei Aussagen und wird nur dann Wahr, wenn beide Aussagen Wahr sind
    • ||: Logischer ODER Operator, kombiniert zwei Aussagen und wird nur dann Wahr, wenn mindestens eine der beiden Aussagen Wahr ist
    • ==, !=: Vergleichsoperatoren, ersteres ergibt wahr, wenn der linke und der rechte Operand gleich ist, zweiteres, wenn beide ungleich sind
    • <, >, <=, >=: Vergleichen die Operanden nach ihrer Größe, die ersten Beiden werden Wahr, wenn links kleiner/größer ist als der rechte, wie auch die letzten Beiden aber Vergleichen zusätzlich auch noch ob sie optional auch gleich sind
    Damit haben wir jetzt die Möglichkeit, kompliziertere Konditionen zu gestalten:

    C-Quellcode

    1. if ((var > 0 && var != 1) || var == -1)
    2. {
    3. std::cout << "Sieht so aus als wären wir nun hier\n";
    4. }

    Das wird jetzt etwas komplizierter, aber um es kurz vorzustellen: Wir testen, ob var entweder größer als 0 ist und nicht 1 (also alles nach 1) oder ob var -1 entspricht sollte der erste Check nicht stimmen.
    Der Sinn dieses Beispiels sei mal dahingestellt, aber uns werden hier neue Möglichkeiten eröffnet. Theoretisch könnte man (var > 0 && var != 1) mit var > 1 ersetzen, aber ich wollte die Operatoren vorstellen.
    Beachtet auch die Klammern, in diesem Beispiel machen diese zwar keinen Unterschied, da && Vorrang zu || hat, aber in anderen Fällen ist dies nicht so einfach, mit Klammern kannst du deinem Computer erklären, was Vorrang hat, also welches Segment zuerst evaluiert werden sollte.

    Nun gut, es gibt aber ein weiteres interessantes Merkmal von if-else Statements:
    Die Evaluation dieser Branching statements.
    Denn unter Umständen (unter erdenklich schlechten Umständen) muss jeder der Zweige evaluiert werden, da die anderen "fehlschlugen". "Fehlschlag" heißt, die Kondition eines Zweiges war falsch und der nächste Zweig musste untersucht werden, bis einer gefunden wurde, welcher sich als Wahr herausstellt und dann ausgeführt werden kann.
    Aus diesem Grund und man möge es jetzt "Vorzeitige Optimierung" nennen, sollte man darauf achten, wenn die Möglichkeit denn besteht, dass man die Zweige so anordnet, dass der wahrscheinlichste bis zum am wenigsten wahrscheinlichen Zweig darüber geordnet wird, heißt, von oben nach unten: Wahrscheinlichster Zweig -> folgend weniger wahrscheinlicher Zweig.

    Der Grund, warum dies "vorzeitige Optimierung" ist (ich nenne das harmlose "vorzeitige Optimierung", da es nicht wirklich die Lesbarkeit beeinflusst), ist der, dass ein Prozessor heutzutage etwas besitzt, das sich "Branch Prediction" nennt, heißt, der Prozessor findet heraus, welcher Zweig bis zum nächsten Zeitpunkt am wahrscheinlichsten ausgeführt wird, da die vergangenen Evaluationen protokolliert und untersucht werden.
    Der Grund, warum man dennoch sortieren kann, ist die erste Gegenüberstellung, wenn noch keine Geschichte angelegt wurde.
    Aus diesem Grund ist dies wohl ein weniger wertvoller Tipp, da es in den meisten Fällen keinen Unterschied machen wird, aber wenn es die Lesbarkeit nicht degradiert und man nicht durch Abhängigkeiten anderer Zweige dazu gezwungen ist, die Statements in einer gewissen Reihenfolge zu sortieren, kann man durchaus darüber nachdenken.

    Als Letztes möchte ich auch noch eine Variante des ifs vorstellen, welche gerne dazu verwendet wird, kurze Anweisungen in kürzerer Form darzustellen, nehmen wir dazu ein kleines Beispiel:

    C-Quellcode

    1. int var = 0;
    2. bool res;
    3. if (var == 0)
    4. {
    5. res = false;
    6. }
    7. else
    8. {
    9. res = true;
    10. }

    Was wir hier tun, ist, nachzuschauen, ob var 0 ist, wenn ja, dann wird res zu false, andernfalls true.
    Natürlich ist dies absoluter Quatsch, da int implizit zu bool konvertierbar ist, aber für unser Beispiel reicht es allemal.
    Denn was wir hier eindeutig sehen können, ist, dass wir hier sehr viel Code für ein so einfaches Ergebnis schreiben müssen, aber geht das kürzer?

    JA! Tut es.
    Mit dem sogenannten "Ternary" Operator, das ist nichts anderes als ein einfaches if-else Statement in einer einzigen Zeile, welches folgendes Format hat: <Kondition> ? <Wahr> : <Falsch>
    Als tun wir folgendes:

    C-Quellcode

    1. int var = 0;
    2. bool res = (var == 0 ? true : false); // klar, wir könnten auch einfach nur "bool res = (var != 0)" machen

    Die Klammern hier sind unnötig da ?; Vorrang zu = hat (selbe Vorrangsgruppe aber Rechts-zu-links Assoziation), aber ich verwende die immer zum Abgrenzen, das ist meiner Meinung nach schöner.
    Wie dem auch sei, dies erlaubt es uns ein einfaches wenn-oder Verhältnis zu schaffen, das wir in dieselbe Zeile schreiben können.
    Man kann diese übrigens auch verschachteln, wozu ich aber Klammern unbedingt empfehle, da manche Operatoren anderen Vorrang haben und du somit möglicherweise einen Teil eines Ausdruckes, welcher noch dazu gehören sollte, verlierst.

    switch
    Ein weiteres Selektionsmittel ist das switch Statement, mit diesem kann man anhand eines übergebenen Wertes und einer nachfolgenden Tabelle an Möglichkeiten entscheiden, welche dieser Möglichkeiten selektiert werden sollen.
    Man kann jedes switch Statement mit einem if-else if Block auswechseln, nicht aber umgekehrt.

    Nehmen wir mal ein Beispiel, wieder mit var:

    C-Quellcode

    1. switch(var)
    2. {
    3. case 0:
    4. std::cout << "Es war 0\n";
    5. break;
    6. case 1:
    7. std::cout << "Es war 1\n";
    8. break;
    9. case 2:
    10. std::cout << "Es war 2\n";
    11. case 3:
    12. std::cout << "Es war 3\n";
    13. break;
    14. default:
    15. std::cout << "Es war nix von alldem\n";
    16. }


    Kommen wir zur Erklärung:
    Mit switch(var), eröffnen wir ein solches Statement und der Teil in den Klammern spezifiziert welcher Wert getestet werden soll.
    Merke dir: Der übergebene Wert muss ein integraler Typ oder implizit konvertierbar zu einem integralen Typ sein, also ein Typ, welcher Ganzzahlen repräsentiert, andernfalls ist es ein Error.
    Dem folgend sind die cases, diese definieren die Tabelle an Werten, die ausgeführt werden sollen, wenn var einen gewissen Wert besitzt.
    Zum Bleistift: case 0: ist an der Reihe, sollte var 0 sein, dann bekommen wir den Output: Es war 0

    Fall-Werte können btw. nur integrale "constant-expressions" sein.
    Eine "constant-expression" (Konstantausdruck) ist ein Ausdruck, welcher zur Kompilierzeit bereits bekannt ist, das heißt, ein Ausdruck, welcher nicht erst zur Laufzeit bestimmt werden kann.
    Das wäre für unseren Fall also ein konstantes Objekt wie: const int var = 2; - bitte sei gewarnt, dass dies keine "constant-expression" mehr ist sollte der rechte Wert erst zur Laufzeit bekannt sein.

    Nun fragst du dich bestimmt, was break bedeutet, nun, wir werden später noch tiefer darauf eingehen aber um für dieses Segment abschließen zu können, müssen wir noch zwei Dinge über switch Statements Wissen:
    1. Jeder switch-case leidet unter Durchfall, das bedeutet aber nicht, dass es sich um einen Fall für einen Arzt handelt, sondern, dass jeder Fall, wenn wir am Ende des Blocks kein break einfügen, der Code zu dem nächsten Fall durchfällt.
      Das sieht man im Falle von Fall 2 und 3, hier wird der Code beider Fälle ausgeführt, wenn var 2 beinhaltet, also niemals vergessen ein break zu setzen. In manchen Szenarien ist dies aber erwünscht, in solch einem Moment wird der Compiler dich darauf hinweisen mit einer Warnung, du kannst dann dem Compiler mittels des Attributs [[fallthrough]] mitteilen, dass dies erwünscht sei.

      C-Quellcode

      1. case 2:
      2. std::cout << "Es war 2\n";
      3. [[fallthrough]];

    2. Jedes switch Statement kann einen einzigen default Fall definieren, dieser wird nur dann ausgeführt, wenn entweder keiner der anderen Fälle zutrifft oder wenn ein anderer Fall wiedermal Durchfall hat und in default rein-leakt.
      Das default kann überall erscheinen und muss nicht am Ende gesetzt werden, jedoch hat dies nur den Vorteil, wenn man den Durchfall-Fall im default Fall benötigt, sonst wird es eigentlich konventionell immer als letztes gesetzt.
    Bevor wir switch-cases abschließen, ist es noch wichtig zu erwähnen, dass alles, was in einem switch Statement definiert wird, für alle Fälle definiert wird, das bedeutet, ein Objekt in Fall 2 wird auch in Fall 3 verfügbar sein, daher wird man in diesem Fall einen "Redefinition" Error erhalten.
    Und aus diesem Grund empfehle ich immer Fälle mit einem folgenden Klammerpaar einzuzäunen:

    C-Quellcode

    1. switch(var)
    2. {
    3. case 1:
    4. {
    5. std::cout << "Es war 1\n";
    6. } break;
    7. case 2:
    8. {
    9. std::cout << "Es war 2\n";
    10. }
    11. }

    Wie wir ja gelernt haben, definiert ein Klammerpaar immer einen neuen Geltungsbereich, somit grenzen wir den Inhalt eines Falles auf diesen einen Fall ein.

    Und weil wir gerade dabei sind und das if Statement auch schon durchgemacht hatten, so könnte unser Beispiel als eben jenes aussehen:

    C-Quellcode

    1. if (var == 0)
    2. {
    3. std::cout << "Es war 0\n";
    4. }
    5. else if (var == 1)
    6. {
    7. std::cout << "Es war 1\n";
    8. }
    9. else if (var == 2 || var == 3) // Erinnere dich, in 2 gab es einen Durchfall
    10. {
    11. if (var == 2)
    12. {
    13. std::cout << "Es war 2\n";
    14. }
    15. std::cout << "Es war 3\n";
    16. }
    17. else // default
    18. {
    19. std::cout << "Es war nix von alldem\n";
    20. }

    Nicht sehr schön, oder?

    Es gibt jedoch Fälle, in denen man sich einfach nicht entscheiden kann zwischen einem if-else if und einem switch, woher weiß ich also, was ich wann verwende?
    Nun, du wirst wahrscheinlich schon oft davon gehört haben, dass einige Entwickler Dinge gesagt haben wie: "Aber ifs sind schneller" oder "Bullshit! Switches sind schneller" - und dann werden dir die unterschiedlichsten Messwertergebnise dargeleget.
    Woran manche aber nicht denken ist wie immer, dass diese Dinge Situationsabhängig sind: Was wird verglichen? Wie viel wird verglichen?
    Aus diesem Grund: Lesbarkeit geht vor, denn ein Compiler ist smart und kann, wenn er die Möglichkeit und den Vorteil darin sieht, ein switch in eine if-else if Kette verwandeln.
    Aber nicht nur das, es gibt etliche Möglichkeiten wie einen Jump Table oder auch einfach nur simple Bit-Operationen um den richtigen Fall zu bekommen.

    Deshalb empfehle ich persönlich switch dann zu verwenden, wenn es sichtbare Vorteile hat, wie im Falle von index-basierten Fällen beispielsweise mit einem enum, oder auch wenn du eine Tabellare Übersicht über die möglichen Werte einer Variable haben möchtest - bei einer if-else if Kette wäre da zu viel dazwischen und du könntest die Werte nicht mehr so einfach erkennen.
    Aber wie ich immer wieder in diesem Tutorial beteuere: Es liegt an dir.

    Ein weiter Vorteil von switch Statements, ist, dass wenn man mit einem enum arbeitet und keinen default Fall besitzt, der Compiler dich warnen wird, sollte eine der Konstanten des enums nicht im switch definiert worden sein. (natürlich, nur wenn die Warnung aktiviert ist)
    Somit weiß man, dass man eine Konstante vergessen hat, das geht mit if-else if Ketten nicht.

    Schleifen (for, while, do-while)
    Jetzt kommen wir zu dem wahrscheinlich zweit-häufigsten Konstrukt in der Software-Entwicklung: Schleifen.
    Schleifen sind dafür da, einen Code-Block mehrmals zu wiederholen ohne den Code selbst wiederholen zu müssen - was anfällig für Fehler wäre, mühselig zu warten und im Fall von mehreren tausend bis zu Millionen Wiederholungen möglicherweise auch einfach nur eine Sysiphosarbeit wäre, außerdem erlauben Schleifen das Vektorisieren von Aufgaben, was händisch nicht schön aussehe und eine gute Planung voraussetzen würde. (und zudem wäre es auch noch vorzeitige Optimierung)

    Schleifen kommen in 2½ Geschmacksrichtungen, auf der einen Seite haben wir die for Schleife, auf der anderen die while und do-while Schleifen, beginnen wir mit dem häufigsten Flavour.
    Eine for Schleife gibt es wiederum in zwei Varianten, die ursprüngliche und die "range-based" for Schleife:

    C-Quellcode

    1. // Normal
    2. for (int i = 0; i < 1000; ++i)
    3. {
    4. std::cout << "Derzeit: " << i << "\n";
    5. }
    6. // Range-based (for-each)
    7. for (int wert : array)
    8. {
    9. std::cout << "Derzeit: " << wert << "\n";
    10. }

    Was die erste Schleife macht, ist i zu definieren und mit 0 zu initialisieren, dann läuft die Schleife so lange, bis i 1000 ist und wiederholt den Code innerhalb der Klammern bis dies vorüber ist.

    Eine normale for Schleife hat drei Segmente, das Initialisierungsegment (also hier i), das Konditionssegment und einen Ausdruck, der nach jedem Durchlauf der Schleife ausgeführt wird.
    Interessanterweise sind all diese Segmente optional, das heißt eine for Schleife for (;;) ist gültig und wird sogar oft dazu verwendet, eine unendliche Schleife zu erzeugen, dazu gibt es noch das Pendant while (true) wozu wir später noch kommen, jedoch sieht man das eher selten, beide haben denselben Effekt, also Unterschiede gibt es da keinen - bis auf die Semantik.

    Im Initialisierungssegment können wir ein- bis unendliche Objekte vom selben Typen definieren und initialisieren (Komma-separiert), was aber natürlich kein muss ist, im Falle das wir ein Objekt außerhalb der Schleife besitzen, also folgendes wäre auch möglich:

    C-Quellcode

    1. int i = 0;
    2. for (; i < 1000; ++i)
    3. {}


    Das Konditionssegment funktioniert ähnlich wie ein if Statement, es überprüft, ob der Ausdruck Wahr ist, und wenn dies der Fall ist, führt es den Code innerhalb der Klammern aus, ist es unwahr, wird die Schleife abgebrochen.
    Das letzte Segment, der Ausdruck, ist irgendwas, das gegen Ende hin jedes Durchlaufes ausgeführt werden soll, im Normalfall ist dies i welches mit 1 addiert wird, das heißt im nächsten Durchlauf ist i 1 mehr, man kann dort aber jeden Ausdruck einfügen, den man haben möchte.

    Die zweite Variante ist die "range-based" Schleife, die ist etwas komplizierter, wenn man hinter die Kulissen starrt. Hier gibt es oberflächlich betrachtet zwei Segmente, die "range-declaration" und die "range-expression".
    Zweiteres deklariert ein Objekt, oder gibt es an, welches Unterobjekte beinhaltet, wie etwa ein Array oder einen C++ Vektor, ersteres deklariert den Typ und den Namen der Objekte innerhalb dessen.
    Wo es kompliziert wird, ist, welche Objekte valide sind, um als Range-Expression herhalten zu können, unter anderem wäre das wie gesagt ein fundamentales Array oder jedes andere Objekt, welches eine begin() und end() Methode definiert. (Methode = informell eine Funktion Teil einer Klasseninstanz = "member-function")

    Auch wäre eine sogenannte "initialiser-list" ein möglicher Kandidat, was im Grunde nur syntaktischer Zucker ist:

    C-Quellcode

    1. for (int i : { 0, 1, 2, 3, 4, 5, 6, 7 })
    2. {}

    Dieses Beispiel würde dank C++'s automatischer Typenerkennung eine std::initialiser_list<int> werden, also eine Instanz dieser Klasse mit dem Typen int.

    Gut, aber wofür sind jetzt while Schleifen gut?
    Nun, eine while Schleife ist nichts anderes als eine for Schleife mit dem einzigen Unterschied, dass wir hier nur das Konditionssegment besitzen, würden wir also unser obiges Beispiel in eine dieser Schleifen umschreiben, sähe das folgendermaßen aus:

    C-Quellcode

    1. int i = 0;
    2. while (i < 1000) // könnte auch "for (; i < 1000;)" sein, aber hier sparen wir uns die Semikolons
    3. {
    4. // irgendein Code
    5. ++i;
    6. }


    Und auch mit der do-while Schleife:

    C-Quellcode

    1. do
    2. {
    3. // irgendein code
    4. ++i;
    5. }
    6. while (i < 1000);

    Der Unterschied zwischen diesen zwei Schleifen ist, dass ersteres, sollte i bereits 1000 sein, komplett übersprungen wird, während das do-while den Code mindestens einmal ausführt und erst danach abbricht, sollte der Wert 1000 sein.

    Aber wozu sind die denn nun gut?
    Nun, nehmen wir mal zwei Beispiele durch:

    C-Quellcode

    1. int sum = 0;
    2. for (int i = 1; i < 100; ++i)
    3. {
    4. sum += i; // dasselbe wie "sum = sum + i"
    5. }

    Hier summieren wir alle Zahlen von 1 bis 99 in ein Objekt, da der Code 99-mal ausgeführt wird. (beachte "i < 100"- solange i kleiner als tausend ist, für die Zahl 100 wird die Schleife nicht mehr ausgeführt)
    Die schlechtere Alternative hier wäre es selbst, und zwar 99-mal mit der jeweiligen Zahl zu addieren, das wäre nicht sehr schön und fehleranfällig.

    Beispiel 2:

    C-Quellcode

    1. std::array<int, 10> array {
    2. 23, 412, 414, 114, 525, 135, 5634, 1353, 52, 1
    3. };
    4. int sum = 0;
    5. for (int wert : array) // auch möglich: for (int wert : { 23, 412, 414, 114, 525, 135, 5634, 1353, 52, 1 })
    6. {
    7. sum += wert;
    8. }

    Auch hier addieren wir wieder alle Werte in ein Objekt, aber diesmal mit den Werten aus einem Array.

    Nun gibt es für Schleifen noch ein weiteres interessantes Konzept: jump Statements.
    Dazu zählen: break, continue und unter anderem natürlich auch return.

    Mit diesen kann man die Ausführung einer Schleife hart steuern, ein break Statement wird dazu verwendet, eine Schleife innerhalb des ausführbaren Blocks abzubrechen, das heißt, man wartet nicht bis auf den letzten Durchlauf, sondern kann diese frühzeitig beenden.
    Sehr ähnlich ist hier return, wobei dieses den gesamten Funktionsraum zurückgibt, das heißt, hier wird nicht nur aus der Schleife ausgebrochen, sondern aus dem gesamten Funktionskörper, welcher die Schleife beinhaltet.
    continue auf der anderen Seite bricht nur den restlichen Schleifen-Körper ab, an dem Punkt, an dem es aufgerufen wurde und startet die Schleife bei der nächsten Iteration neu.

    C-Quellcode

    1. int sum = 0;
    2. for (int i = 1; i < 100; ++i)
    3. {
    4. if (i == 10)
    5. {
    6. break;
    7. }
    8. sum += i;
    9. }

    Dies bedeutet, dass diese Schleife niemals 99 erreichen kann, weil wir sie abbrechen alsbald i den Wert 10 erreicht, das heißt sum wird nur die Summe bis einschließlich 9 beinhalten.

    C-Quellcode

    1. int sum = 0;
    2. for (int i = 1; i < 100; ++i)
    3. {
    4. if (i == 10)
    5. {
    6. continue;
    7. }
    8. sum += i;
    9. }

    Anders sieht es hier aus, hier werden alle Werte von 1 bis 99 addiert, abgesehen von 10, weil wir ab diesem Wert die Schleife weiterschicken zum nächsten Wert, bevor die Summierung mit 10 überhaupt vonstattengeht.

    In einer Endlosschleife (Schleifen, die niemals stoppen) sind break und/oder return von elementarer Bedeutung, denn bei einer Endlosschleife, wie der Name schon sagt, handelt es sich um eine Schleife, die niemals aufhört, da wir keine Kondition anbieten, welche jemals als unwahr evaluiert werden kann.
    Für diesen Fall benötigen wir dann eines der beiden, um anhand einer Routine innerhalb des Körpers der Schleife zu bestimmen, wann wir ausbrechen wollen.

    goto
    goto ist eines dieser Dinge, die von der Gesellschaft der Programmierwelt gerne verachtet und ausgestoßen wird, aus guten Gründen.
    Denn gotos erlauben es dir innerhalb des Codes an einen anderen, völlig willkürlichen Ort zu springen, was in vielen Fällen absolut schwer nachzuvollziehen ist, und aus diesen unvorhersehbaren Gründen wird goto meist vermieden.

    Ein goto besteht aus einem Label und einer Sprunganweisung; die Anweisung erklärt, zu welchem Label gesprungen werden soll, als Beispiel:

    C-Quellcode

    1. einLabel:
    2. int var = 0;
    3. if (var == 0)
    4. {
    5. std::cout << "Wert ist: " << var << "\n";
    6. ++var;
    7. }
    8. goto einLabel;

    Dies ist eine endlose Wiederholung, var wird mit 0 initialisiert, abgefragt, ob es 0 enthält und der Wert an die Konsole ausgegeben und anschließend um 1 erweitert.
    Jedoch wird mit dem darauf folgenden goto auf das zuvor definierte Label einLabel zurückgesprungen und der Kram geschieht von neuem.
    Diese Labels sind aber nicht auf den derzeitigen Geltungsbereich getrimmt, sondern können überall erscheinen, was sie so tricky macht.

    Ich persönlich rate von diesen prinzipiell ab, es gibt viele, die sagen, für verschachtelte Schleifen sei dies in Ordnung, um dort auszubrechen, weil man sonst mehrere Checks und break Statements einbauen muss, aber dafür verwende ich dann Funktionen mit return Statements, da die auch aus mehreren Schleifen ausbrechen können.
    Es liegt in diesem Fall an euch.



    Ich wünsche euch noch viel Glück und wenn eventuelle Kommentare noch offen stehen, bin ich wie immer hier zur Diskussion verfügbar:
    Das C++-Tutorial für Einsteiger: Diskussions-Thread

    Viel Spaß <3
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------

    3. Elemente der Sprache

    3.3 Zeiger und Referenzen


    Hust hust... Entschuldigung, mir ist nur der Zeiger im Hals stecken geblieben.
    Also, Zeiger und Referenzen, wundervolle Zeiten kommen nun auf uns zu - unser nächstes Thema ist ein essenzieller Schritt dort hin, wo C++ für uns anfängt C++ zu werden. Nichts schreit so sehr "Memory Management" wie es ein Zeiger/Referenz tut.

    Bevor wir uns da aber reinkrümeln, sehen wir uns mal an, was RAM ist und wie er funktioniert.
    Der RAM (auch Random Access Memory) ist ein sogenannter Zwischenspeicher, welcher Daten, naja... zwischenspeichert. (ey ich bin so ein 5head manchmal)
    Die Daten, die sich dort befinden, sind "flüchtig" (im Englischen, wichtig: volatile), da es Daten sind, die nur zur Laufzeit von Bedeutung sind und nicht für die Dauer vonnöten; jedoch können Daten vom RAM auf die Festplatte übertragen werden, wenn man sie braucht auch nach dem wir das Programm geschlossen oder den Computer heruntergefahren haben, das ist das, was passiert, wenn man in vielen Anwendungen auf das Speicher-Icon klickt - Daten werden zuerst serialisiert und dann vom RAM auf die Festplatte überführt.
    Natürlich befindet sich nicht immer alles im RAM, vor allem Objekte, an denen gerade im Prozessor gearbeitet wird, werden häufig im CPU-Cache des jeweiligen Kerns behalten (L1, L2 und heutzutage auch L3); da dies dann aber meistens zurück in den RAM geschrieben wird, belassen wir es dabei, aufgrund der Komplexität für dieses Tutorial, nicht darüber zu sprechen.

    Um auf den Aufbau zu kommen, kann man sich RAM wie eine Tabelle vorstellen, in welcher jeder Bit eine Zelle repräsentiert, die mit einer Spalte und einer Zeile definiert wird. Die Spalte repräsentiert den Bit im Datenpaket und die Zeile die Adresse im RAM - eine Zeile und alle Spalten in dieser Zeile repräsentieren dann ein Byte an Daten:
    4000 (0xFA0)00111001
    4001 (0xFA1)00101111
    4002 (0xFA2)10011000
    4003 (0xFA3)01100010
    4004 (0xFA4)00100011
    4005 (0xFA5)00110110


    Ganz links sehen wir die Adresse, gefolgt von einer Reihe an Bits. (wir gehen von einer Breite von 8 Bits aus, da das heutzutage der Standard ist)
    Neben der dezimalen Adresse (Base 10) sehen wir, in Klammern, eine hexadezimale Ansicht (Base 16) der Adresse, dies wirst du häufiger in Verbindung mit Adressen sehen, wenn du in C++ arbeitest.
    Warum ist das so?
    Erstens sind hexadezimale Zahlen kürzer, zweitens werden Dinge wie Bytes immer in zweier-Potenzen gelesen und das ist mit hexadezimal einfacher als mit dem Dezimalsystem. (obwohl für den Ottonormalverbraucher eher irrelevant)
    In anderen Sprachen müssen wir uns damit selten auseinandersetzen und auch in C++ wirst du nicht direkt mit Adressen arbeiten müssen, da das intern passiert - jedoch ist es kein Fehler für dieses Thema, dies anzusprechen, da man durchaus während dem Debuggen oder bei Fehlermeldungen damit in Kontakt kommt.

    Wenn wir aber Daten haben, welche größer sind als ein Byte, dann werden diese über mehrere Adressen aufgeteilt.
    Nun, nehmen wir an, wir sind auf einer Plattform, auf welcher int 32-Bit oder 4-Byte hat und dass wir es mit der Adresse 3892 zu tun haben. (ignorieren wir Endianess)
    Wir haben den Wert 443940289, das wäre in Binär 0001 1010 0111 0101 1111 1101 1100 0001, also könnte dies so aussehen:
    0x0F34 (3892)
    0x0F35 (3893)
    0x0F36 (3894)
    0x0F37 (3895)
    1100 0001
    1111 1101
    0111 0101
    0001 1010


    Wichtig für den Prozessor und Memory-Bus sind hier nur die Start-Adresse und die Größe des Datenpakets, klar, weil "Objekt = Start-Adresse bis (Start-Adresse + Größe)".
    So funktionieren im Übrigen auch Arrays, weshalb es nicht falsch ist zu behaupten, dass jedes Objekt ein undercover Array aus Bytes ist. (dies alles gilt natürlich nicht nur für C++)

    Warum und wofür eigentlich?
    Nun, der wahrscheinlich wichtigste Grund, weshalb wir diese brauchen, ist, dass in C++ alles Pass-By-Value ist, also, jedes Objekt, welches weitergereicht wird, wird kopiert; das kann sehr zeitaufwändig sein, vor allem für sehr große Objekte. Wenn man aber nun Zeiger oder Referenzen weitergibt, wird das Objekt nicht kopiert, sondern es wird nur darauf gezeigt, was den Aufwand einer Kopie erspart.
    Im Großen und Ganzen sage ich immer: "Wenn möglich, dann als Referenz weitergeben, wenn nicht, dann als Zeiger weitergeben, wenn auch DAS nicht, dann als Kopie, es sei denn, es handelt sich um fundamentale Typen, die klein genug sind, sodass eine Kopie keinen signifikanten Wert für die Laufzeit hat. Natürlich, wenn man eine Kopie benötigt, weil man zum Beispiel das Objekt verändert, ohne das Original zu verändern oder es weitergeben möchte, dann steht dies natürlich außer Frage, in diesem Fall macht es mehr Sinn, das Objekt By-Value weiterzugeben.

    Damit man weiß, was By-Value bedeutet, zwei Beispiele:

    C-Quellcode

    1. // Hier wird "objekt" By-Value and "funktion" übergeben, das heißt, objekt ist ein eigenes Objekt und nicht dasselbe wie jenes welches wir an die Funktion übergeben haben
    2. void funktion(int objekt);
    3. //innerhalb einer Funktion
    4. int objekt = 1;
    5. int anders_objekt = objekt; // auch hier übergeben wir "objekt" an "anderes Objekt" By-Value, also wird an dieses Objekt kopiert


    Ok, picture this: Deine Mutter, dein Vater, ein Freund oder auch dein Finanzberater möchten von dir wissen, wo du denn die halb-leere Tetra-Pack-Milch hinstelltest, nachdem du genüsslich die letzten Krumen deiner Kellog's Frostys aus der Schüssel schlürfst. Bevor diese Fragen stellende Person auf diese Packung Milch Zugriff bekommen kann, muss sie wissen, wo diese sich befindet, also erhebst du deinen Zeigefinger und zeigst auf deinen Kopf, denn du hattest gestern einen Traum davon, Rekordweltmeister im Balancieren zu werden, wenn du dies denn während der Ausführung anderer Aktivitäten übst. Nun weiß diese andere Person: "Ohh, hier befindet sie sich also", und kann damit dann schlussendlich doch noch den Nachbarn waterboarden.

    Was bedeutet dies nun im Rahmen eines C++ Programms?

    Zeiger
    Nun, ein Zeiger ist genau das mit dem einzigen Unterschied, dass er nicht auf Milch, sondern auf Daten (meistens) im RAM zeigt; deshalb nennen sie sich ja auch passenderweise "Zeiger". (oder "Pointer" im Englischen)
    Ein Zeiger ist wie jede andere Variable (bspw. eine int var; Variable) ein Objekt; ein Objekt, welches eine numerische Adresse im RAM aufzeigt (wie oben in der Tabelle), im Grunde also nur ein integraler Typ mit zusätzlichen Features.
    Beinahe jeder Typ kann als Zeiger herhalten, allerdings muss man dies explizit in die Deklaration mit-einfließen lassen, sehen wir uns mal ein Beispiel einer integer Variable an:

    C-Quellcode

    1. int muesli_flocken = 420;
    2. int* muesli_flocken_ptr = &muesli_flocken;
    3. std::cout << "So viele Flocken: " << muesli_flocken << "\n";
    4. std::cout << "So viele Flocken (Zeiger): " << *muesli_flocken_ptr << "\n";
    5. *muesli_flocken_ptr /= 10;
    6. std::cout << "So viele Flocken noch übrig: " << muesli_flocken << "\n";
    7. std::cout << "So viele Flocken noch übrig (Zeiger): " << *muesli_flocken_ptr << "\n";

    (Achtung: In Wahrheit ist es falsch, anzunehmen, dass ein Zeiger nur ein integraler Typ ist, da der Standard von C++ nicht angibt, wie ein Zeiger eigentlich repräsentiert wird, das bleibt dem Compiler und der Plattform vorbehalten. Aber ich denke, für den eigenen Kopf ist dies viel leichter zu verkraften, als, wenn ich sagen würde: "Yo, auf dieser Plattform sieht das so und so aus, und hier ist es wiederum anders als nochmals hier auf dieser Plattform." - Ich hoffe ihr versteht das, vor allem weil es für uns nicht wichtig ist)

    Wir erzeugen hier ein neues Objekt muesli_flocken mit Typ int und dem Initialwert 420. Dieses Objekt (wir gehen zwecks Einfachheit mal davon aus) wird nun an irgendeine freie Adresse im RAM zugewiesen und die Daten dort abgelegt, beziehungsweise, startend an dieser Adresse, da int mehr als ein Byte hat, wird es über mehrere Adressen aufgeteilt, man braucht aber nur die Start-Adresse, da der Compiler weiß, wie groß int ist und somit von der Start-Adresse hochzählen kann.
    In der nächsten Zeile wollen wir einen Verweis auf dieses Objekt, wir wollen keine Kopie, sondern wirklich dieses erste Objekt betreuen, dort nehmen wir also die Adresse.
    Ein Zeigertyp wird immer mit einem Asterisk definiert Typ*, "Typ" ist nun ein Zeiger zu einem "Typ" und kein "Typ" selbst; mithilfe von & können wir nun die Adresse von muesli_flocken anfragen und an muesli_flocken_ptr übergeben. Also merke, & vor einem Namen fragt die Adresse eines benannten Objektes an.

    Wenn wir rein hypothetisch annehmen, dass muesli_flocken nun an der Adresse "0xff0032" im RAM startet, dann erhalten wir diesen Wert über den Adressen-Operator und weißen in an das Zeiger-Objekt zu - du als Programmierer musst aber glücklicherweise nichts über die spezifische Adresse wissen, nur, dass es sich um einen Zeiger handelt welcher diese Beinhaltet.
    Wenn wir in den nächsten Zeilen beide Objekte anfragen, ergeben sie beide dieselben Werte, weil beide dasselbe Paket im RAM ausgeben.
    Im Übrigen, wie wir sehen, müssen wir, um an den Inhalt eines Zeigers zu kommen, zuerst diesen "dereferenzieren", das bedeutet, wir müssen diesen Compiler Bescheid geben, dass wir nicht den Zeiger wollen, sondern das, was sich dort an dieser Adresse befindet.
    Das geht, wie wir sehen, mit dem "Derefenzierungs-Operator" (wieder ein Asterisk): *muesli_flocken_ptr.

    Wir können Zeiger aber nicht nur zum Lesen verwenden, sondern auch zum Schreiben, wie wir an unserem Beispiel sehen: *muesli_flocken_ptr /= 10
    Hier "dereferenzieren" wir zuerst, also nochmal, fragen das eigentliche Objekt an dieser Adresse an und nicht die Adresse selbst und dividieren die Daten mit 10 und weisen sie dem Originalobjekt wieder zu.
    Wenn wir nun wieder beide Objekte ausgeben, erhalten wir die Antwort auf alles.

    Aber warum ist nun muesli_flocken auch 42? Wir haben doch nur das Objekt an der Adresse von *muesli_flocken_ptr geändert?
    Ja genau, du hast das Objekt an dieser Adresse durch den Zeiger geändert, rate doch mal wo muesli_flocken sich befindet, genau, an dieser Adresse. Dies nennt sich auch "Aliasing", weil "Alias" = anderer Name aber gleiches Objekt.

    Nun gibt es eine Eigenheit für Zeiger, die du womöglich schon aus anderen Sprachen kennen dürftest: nullptr. (null in anderen Sprachen)
    Nun, es ist so, in C++ kann kein nicht-Zeiger null sein, das geht nicht, du kannst also keine Dinge tun wie in etwa:

    C-Quellcode

    1. int variable = nullptr;

    Das wird nicht gehen, da variable kein Zeiger ist, wobei nullptr jedoch eine Adresse ist, undzwar eine ungültige Adresse. Wenn ein Zeiger nun nullptr besitzt, heißt das, er zeigt auf keine gültige Adresse.
    Um dem Ganzen noch die Krone aufzusetzen, sollten wir jemals in Versuchung kommen einen nullptr zu derefenzieren, dann gnade uns Gott, denn nur Gott weiß, was mit UB passiert - in C++ wirst du nicht wirklich Hinweise darauf bekommen, dass du einen nullptr dereferenziert hast, in anderen Sprachen wie z.B. Java, bekommst du eine NullPointerException.

    Kleine Randnotiz: C++ besitzt auch ein sogenanntes Makro NULL welches zu 0 wird, da es erlaubt ist 0 einem Zeiger zu-zuweißen anstelle von nullptr.
    Jedoch ist dies noch aus der C-Zeit, und wir wissen ja, dass C++ darunter leidet, kompatibel mit C zu sein. Also bitte verwendet dieses Makro nicht, es kann der Overload-Resolution schaden und auch Templates (wohin wir noch irgendwann kommen), und Konsistenz ist wichtig... meiner Meinung nach.

    Aber wieso haben wir denn nun nullptr? Wieso brauche ich einen Zeiger, der nirgends hinzeigt?
    Gute Frage Thomas, und ich kann es dir auch gleich beantworten:
    Es gibt dutzende Möglichkeiten, die einem nullptr bietet, einer davon wäre Typenerkennung, da nullptr in C++ ein eigener Typ ist und nicht nur eine Adresse. (std::nullptr_t, "_t" steht immer für "Type")
    Die aber wohl häufigste Anwendungsmöglichkeit ist die Alternative-Behandlung, denn ein Zeiger, der auf nichts zeigt, kann abgefragt werden, ob er denn auf nichts zeigt; das erlaubt es dir, beispielsweise eine Funktion abzuschalten, wenn man keinen Zeiger benötigt.

    C-Quellcode

    1. int entwederWertOderAlternative(int wert, int *alternative)
    2. {
    3. if (!alternative) // ein Zeiger ist implizit konvertierbar zu boolean, und wenn ein Zeiger "nullptr" ist, wird es zu "false"
    4. return wert;
    5. return *alternative;
    6. }
    7. int main()
    8. {
    9. int erst_wert = 203;
    10. int alternative = 430;
    11. int resultat_mit_null = entwederWertOderAlternative(erst_wert, nullptr);
    12. std::cout << resultat_mit_null << "\n";
    13. int resultat_mit_zeiger = entwederWertOderAlternative(erst_wert, &alternative);
    14. std::cout << resultat_mit_zeiger << "\n";
    15. }

    Wir testen also, ob der Parameter alternative nullptr ist, und wenn ja, gib wert aus ansonsten alternative.

    Ein weiteres wichtiges Thema von Zeigern, was viele oft übersehen, ist, dass Zeiger nicht die Lebensdauer eines Objektes erweitern.
    Das heißt, wir haben eine Funktion, in welcher wir ein Objekt erzeugen und die Adresse dieses Objektes zurückgeben, ist dieses Objekt nach dem Zurückgeben nicht mehr da, das heißt, wir geben einen sogenannten "dangling" Zeiger zurück.
    Wie wir wissen, werden automatische Objekte nach dem Ablauf ihres Geltungsbereichs automatisch bereinigt.

    C-Quellcode

    1. int* gibMirZeigerBaby()
    2. {
    3. int wert = 8943;
    4. return &wert;
    5. }
    6. int main()
    7. {
    8. std::cout << *gibMirZeigerBaby() << "\n";
    9. }

    Dies ist äußerst "gefährlich", da wert nach dem Funktionsablauf von gibMirZeigerBaby zerstört wird, wir erhalten hier also einen Zeiger, der eine Adresse aufzeigt, die das OS schon abgeschrieben hat.
    Die Adresse existiert zwar noch, keine Frage, aber das OS weiß nicht, dass wir diesen Bereich noch in Anspruch nehmen wollen und da wir sie nicht in Verwendung haben, kann sowohl das OS diesen Bereich jederzeit überschreiben, als auch der Compiler kann nötige Optimierungen durchführen, die diese Adresse betreffen, da er davon ausgeht, der Entwickler ist sich darüber bewusst, dass dieses Objekt nicht mehr existiert.
    Dies ist ein UB-Fall, und das bedeutet, niemand weiß, was passieren wird, sollten wir diese Adresse dereferenzieren; möglicherweise hast du Glück und der Wert wurde noch nicht überschrieben oder aber du bekommst irgendwas anderes raus, das Programm crashed oder wer weiß sonst noch was, also immer darauf achten, worauf die Zeiger zeigen; das ist einer der Gründe, wieso Entwickler oft Angst vor Memory Management haben: Das Bookkeeping.

    Ein weiterer Punkt, den man sich oft nicht vorstellen kann, ist der, dass ein Zeiger wie jedes andere Objekt auch nur ein Objekt ist. Das heißt, der einzige Unterschied zwischen einem Objekt von int* und int, sind nur die typen-spezifischen Spezifikationen (Alignment, Größe, Interpretation ect.), aber beides ist nur ein Objekt im Speicher.
    Das erlaubt es dir also, einen Zeiger zu einem anderen Zeiger zu erzeugen:

    C-Quellcode

    1. int muesli_flocken = 420;
    2. int* muesli_flocken_ptr = &muesli_flocken;
    3. int** muesli_flocken_ptr_ptr = &muesli_flocken_ptr;
    4. std::cout << "So viele Flocken: " << muesli_flocken << "\n";
    5. std::cout << "So viele Flocken (Zeiger): " << *muesli_flocken_ptr << "\n";
    6. *muesli_flocken_ptr /= 10;
    7. std::cout << "So viele Flocken noch übrig: " << muesli_flocken << "\n";
    8. std::cout << "So viele Flocken noch übrig (Zeiger): " << *muesli_flocken_ptr << "\n";
    9. **muesli_flocken_ptr_ptr /= 2;
    10. std::cout << "So viele Flocken noch übrig: " << muesli_flocken << "\n";
    11. std::cout << "So viele Flocken noch übrig (Zeiger): " << **muesli_flocken_ptr_ptr << "\n";

    Hier haben wir wiederum einen Zeiger zu einem Zeiger, der auf ein int zeigt. Wie wir sehen, müssen wir diesen Zeiger zweimal dereferenzieren, da wir zuerst die Adresse des ersten Zeigers erhalten wollen und dann die Daten an der Adresse des ersten Zeigers.
    Diese Dinge werden sehr schnell kompliziert, weshalb es auch nicht oft gesehen wird; daher gibt dir C++ mögliche Alternativen für Fälle, in denen sich dies abspielen würde.
    Ein Beispiel eines doppelten Zeigers wäre die main Funktion, der erste Parameter argv ist ein Array aus char: char* argv[], das gleiche wie char** argv.
    Wobei der erste Zeiger auf die Start-Adresse des Arrays zeigt, und alle jeweiligen Zeiger in diesem Array auf die Start-Adresse der jeweiligen 0-terminierten char Arrays oder auch Strings.
    Denn ein Array ist nichts anderes als eine Reihe aufeinanderfolgender Objekte im RAM.

    Des Weiteren ist die Constness von Zeigern zu beachten. Wie wir wissen, hilft uns das Keyword const ein Objekt immutabel zu machen. (nicht weiter veränderbar)
    Wir wissen, dass dies so aussieht: const int immutable_objekt (damit ist dieses Objekt nicht mehr veränderbar)
    Was aber kompliziert wird, ist im Falle von Zeigern zu beurteilen, was hier eigentlich nun konstant ist.
    Wenn man denn nun zum Beispiel dies hier tut: const int* irgendein_zeiger, dann ist nicht der Zeiger konstant, sondern das Objekt, zu dem es zeigt; also man kann das Objekt nicht mehr bearbeiten, man kann aber den Zeiger immer noch zuweisen oder ändern.
    Dies würde dieses Problem lösen: int *const irgendein_zeiger
    Hier machen wir einen konstanten Zeiger zu einem nicht konstanten Objekt, das heißt, das Objekt, auf das gezeigt wird, ist veränderbar, jedoch nicht der Zeiger selbst.
    Wenn du also einen konstanten Zeiger zu einem konstanten Objekt willst, musst du folgendes tun: const int *const irgendein_zeiger; und wenn du multiple Zeiger hast, so: const int *const *const irgendein_zeiger
    Beachte, dass const immer auf das zutrifft, was dem Keyword vorangeht, außer für den Typen, auf den die Zeiger zeigen, hier kann man für sich selbst entscheiden, ob vor oder nach dem Typ. (const int* var oder int const* var)
    Um in Fällen multipler Zeiger zu wissen, welcher Zeiger nun konstant sein sollte, merke dir: Die Stufen von Zeigern gehen von rechts nach links, also, der derzeitige Zeiger ist der, den du am rechtesten stehen hast.

    Heap-Speicher
    Für dieses Szenario gibt es aber eine Lösung und das ist auch einer der Gründe, wieso wir dynamischen Speicher brauchen. (neben vielen anderen)
    Dynamischer Speicher (der Heap), ist in C++ ein Weg, Objekte zu erzeugen und zu verwenden; der dynamische Speicher ist das explizite Pendant zu unseren automatisch-erzeugten Objekten. Während in unserem oberen Beispiel wert nach der Funktion abgelaufen ist, können wir das verhindern, indem wir das Objekt dynamisch verwalten:

    C-Quellcode

    1. int* gibMirZeigerBaby()
    2. {
    3. int* wert = new int(8943);
    4. return wert;
    5. }

    In diesem Fall erzeugen wir wert explizit auf dem "Heap", und erhalten die Adresse - Merke, dass "Heap" Objekte immer Zeiger sind, da wir die Adresse selbst verwalten müssen, heißt, selbst löschen, wenn wir sie nicht mehr benötigen.
    Und da wir es selbst löschen müssen, wird nach der Rückgabe der Funktion das Objekt auch nicht automatisch entfernt.
    Wenn wir das Objekt später dann nicht mehr brauchen:

    C-Quellcode

    1. int main()
    2. {
    3. int* resultat = gibMirZeigerBaby();
    4. std::cout << *resultat << "\n";
    5. // nun löschen wir das Objekt wieder, da wir es nicht mehr brauchen
    6. delete resultat;
    7. // optional setzen wir es auf nullptr, damit, wenn wir den Zeiger später noch brauchen, wissen, dass das Objekt nicht mehr da ist
    8. resultat = nullptr;
    9. }


    Die Frage, die sich jetzt stellt, ist, wieso machen wir denn nicht einfach alle Objekte dynamisch, in C#, Java sind doch auch fast alle Objekte auf dem "Heap"?
    Nun, das Schöne in C++ ist, dass wir die Möglichkeit haben, den "Heap" zu vermeiden, wenn wir ihn nicht benötigen, und das kann durchaus Vorteilhaft sein, aus mehreren Gründen:
    1. Es gibt etwas, das sich Memoryleak nennt, dies passiert, wenn man vergisst, ein "Heap" Objekt zu löschen. Resultierend daraus gibt es eine Region im Speicher, welche niemals mehr (solange das Programm läuft) freigegeben wird, das passiert, wenn wir vergessen, ein Objekt zu löschen und den Zeiger (oder eher die Adresse dazu) verlieren, weil wir sie z.B. an niemanden weitergeben, die ist dann futsch. (oops)
    2. "Heap" Speicher ist meistens langsamer als Stack-Speicher, wir haben das schon angeschnitten, aber "Heap" benötigt zuerst eine freie Adresse und muss dann noch System-Aufrufe machen, um die geeignete Stelle zuweisen, auch das Löschen des Objektes ist ein schwerwiegender Eingriff in die Laufzeit, das alles resultiert in unnötigen Zeitaufwand, welcher für andere Dinge verwendet werden hätte können.
    3. Man muss nicht mit Zeigern arbeiten (man wird immer wieder mal mit Zeigern arbeiten, also wie auch immer), häufig wird man den Tipp bekommen, Zeiger zu vermeiden, wenn es nicht dringend notwendig ist, und C++ bietet auch viele Alternativen an, wie z.B std::optional, was es dir erlaubt nullptr Funktionen zu bekommen ohne Zeiger, was mit zusätzlichen Daten funktionert, die angeben, ob ein Objekt vorhanden ist oder nicht.
    Daher rate ich auch oft, wenn es nicht dringend notwendig ist, dynamischen Speicher zu verwenden, dies auch zu vermeiden.
    In unserem oben (schlecht) gewählten Beispiel zum Bleistift ist es nicht einmal notwendig, einen Zeiger zurückzugeben, wir könnten es auch einfach By-Value zurückgeben.

    Nun gibt es etwas, basierend auf Punkt 1, das wir tun können, um dieses Problem zu minimieren; sollten wir gezwungen sein, dynamischen Speicher zu verwenden.
    C++ bietet uns einen Wrapper an, welcher dynamischen Speicher automatisch managed, und die nennen sich std::unique_ptr und std::shared_ptr.
    Das sind Klassen, welche innerhalb ihrer Konstruktoren und Destruktoren, new und delete automatisch ausführen. Und da diese Wrapper auf dem Stack landen (nicht der Speicher, den sie verwalten, nur die Wrapper selbst), werden sie automatisch auf new und delete zurückgreifen, wenn sie ihren Geltungsbereich verlassen.
    Es ist in 99% der Fälle zu bevorzugen, diese Klassen zu verwenden, sollte man dynamischen Speicher brauchen, denn es gibt eine Konvention, die sich "never new" nennt, die die Regel aufstellt, diese zwei Keywords nie im eigenen Code zu verwenden, aus dem Grund, dass es manchmal zu schwierig ist, den Verlauf dieser zu verfolgen. Auch wenn man denkt, "ich weiß genau, was ich tue", ist es leider oft der Fall, dass dem nicht so ist, auch ein Guru kann an diesen Dingen scheitern.
    Einer der wahrscheinlich unvorhersehbarsten Dinge sind "Exceptions" und das daraus resultierende "Stack-Unwinding", stelle dir vor, ein Objekt wirft eine Exception, nachdem du das Objekt generiert hast aber noch bevor du es löscht.
    In diesem Fall wirst du zeuge von "Stack-Unwinding", was so viel bedeutet wie das alles, was bis zu dieser Exception automatisch erzeugt wurde, wieder automatisch zerstört wird und die Funktion beendet.
    Daraus resultierend hast du das delete niemals erreicht, und somit hast du ein Memory-Leak.
    Während du mit std::unique_ptr aber die Garantie hast, sollte "Stack-Unwinding" vonstattengehen, dessen Destruktor aber dennoch aufgerufen wird und somit das Objekt trotzdem noch gelöscht wird.
    Beachte nur, dass diese Wrapper nur den Zeiger beinhalten, nicht die Daten selbst, du kannst durchaus den Zeiger weitergeben, aber wenn der Wrapper zerstört wurde, ist der Zeiger auch hinfällig.

    Referenzen
    Referenzen sind eine Neuerung, die es erst seit C++ gibt und die nicht von C übernommen worden sind, wie Zeiger. Referenzen fungieren oberflächlich teilweise ähnlich wie Zeiger, sind jedoch sehr stark von diesen zu unterscheiden.
    Eine Referenz ist dazu da, das Objekt selbst weiterzugeben und nicht, um es zu kopieren, das heißt, ähnlich wie bei Zeigern, wird nicht das Objekt selbst weitergegeben, sondern eben in diesem Fall nur eine Referenz dazu. Das klingt jetzt erst mal kompliziert, also sehen wir uns einmal ein Beispiel dazu an:

    C-Quellcode

    1. void funcMitReferenz(int &ref)
    2. {
    3. ref = 5;
    4. }
    5. void funcOhneReferenz(int nichtRef)
    6. {
    7. nichtRef = 5;
    8. }
    9. int main()
    10. {
    11. int test = 0;
    12. std::cout << test << "\n"; // Gibt 0 aus
    13. funcOhneReferenz(test);
    14. std::cout << test << "\n"; // Gibt auch 0 aus
    15. funcMitReferenz(test);
    16. std::cout << test << "\n"; // Huh? Gibt plötzlich 5 aus
    17. }

    Man sieht, ähnlich wie bei einem Zeiger, wird aber diesmal anstelle eines "*" ein "&" verwendet.

    In der Version ohne Referenz wird das Objekt kopiert, bevor es an die Funktion weitergegeben wird, und somit hat nur die Funktion auf dieses Objekt Zugriff - sobald die Funktion verlassen wird, wird es entfernt.
    In der zweiten Version jedoch geben wir das Objekt als Referenz weiter, das heißt, wir kopieren nicht das ursprüngliche Objekt, sondern geben das Original weiter.
    Da wir ja in der nicht-Referenz Version ein neues Objekt zuweisen, wird diese Zuweisung für das Original niemals sichtbar werden, da eben bei der Übergabe das Original kopiert wurde und nun ein eigenständiges Objekt ist.
    Die Referenz-Version aber ändert das Originalobjekt, und somit geben wir den neuen Wert aus, dies wird auch manchmal als "Out-Parameter" betitelt, da der Parameter, neben dem Rückgabetyp, ein weiterer Weg ist, etwas von einer Funktion zurückzugeben.
    Für die C# Entwickler unter euch, dies ist vergleichbar mit ref und out Parameter, wobei Referenzen hier beides bedienen.

    Was Referenzen aber nun von Zeigern unterscheidet, ist hauptsächlich die Zuweisung.
    Eine Referenz kann keinen nullptr berherbergen (außer es ist eine Referenz zu einem Zeiger: int&* referenz_zu_zeiger)
    Des Weiteren kann eine Referenz kein zweites Mal zugewiesen werden, das heißt, weist man etwas auf eine Referenz zu, so wird die Referenz NICHT die Referenz zu einem anderen Objekt ändern, sondern das Objekt selbst, welches sie bereits referenziert.
    Und Schlussendlich muss eine Referenz immer initialisert werden, uninitialiserte Referenzen sind ein Error:

    C-Quellcode

    1. int test;
    2. int &referenz = test; // das geht
    3. // aber das nicht
    4. int &referenz; // Error: nicht initialisert


    Ein weiteres Stilmittel sind konstante Referenzen, welche als Parameter an Funktionen übergeben werden:

    C-Quellcode

    1. void func(const int &ref);

    Dies bedeutet, man übergibt ein Objekt an die Funktion weiter, ohne es zu kopieren, aber es erlaubt der Funktion nicht, diese zu verändern. Dies wird dazu verwendet, ein Objekt an eine Funktion weiterzugeben, ohne dass dieses Objekt kopiert wird, da eine Kopie unter Umständen extra Laufzeit in Anspruch nimmt.
    Für die fundamentalen Typen ist dies jedoch nutzlos (int, long, double, bool ect.), da das Kopieren dieser kein Aufwand ist, aber für Objekte, die sehr groß werden, kann dies durchaus einen Unterschied machen - vor allem, wenn es nur darum geht, das Objekt auszulesen und weiterzuverwerten.

    Als Teil von Klassen zum Beispiel, werden Referenzen auch als Rückgabewert einer Methode verwendet, ein Klassenunterobjekt zurückzugeben.
    Somit gibt man eine Referenz zu einem Objekt zurück, ohne es kopieren zu müssen, dies geht aber meist nur in Klassen (oder auch in einer freien Funktion, sollte die Referenz länger existieren als der Funktionsaufruf) weil diese Objekte länger existieren als die Funktion die sie zurückgeben.

    Im Übrigen kann man keine Referenz zu einer Referenz machen, also das geht nicht: int&& var.
    Es ist aber etwas komplizierter, denn dies ist kein Fehler, es ist aber keine Referenz zu einer Referenz. Dies nennt sich "rvalue-Referenz" (doppeltes "&", ein einzelnes "&" nennt sich auch "lvalue-Referenz"), und ist ein ganz eigenes Monster.
    Was dies bedeutet, ist, dass Objekte, die an eine dieser Referenzen übergeben werden, sogenannte rvalues sind, das sind Werte, die keine Speicheradresse besitzen, wohingegen "lvalues" sehr wohl.
    Wir werden zu diesen noch im Thema "Move-Semantics" kommen, also geduldet euch noch ein wenig.



    Aber freut mich, dass ihr auch heute wieder eingeschalten habt, ich würde mich sehr über eure Gedanken freuen, die ihr natürlich wieder hier zum Besten geben könnt: Das C++-Tutorial für Einsteiger: Diskussions-Thread


    Wie dem auch sei, ich wünsche euch noch viel Spaß, peace out
    Elanda <3
    ----------------------------------------------------------------------------------------------------------------------

    Hier könnte meine Signatur stehen, aber die ist mir abfußen gekommen.

    ----------------------------------------------------------------------------------------------------------------------