Hallo und herzlich willkommen. Wir sind am Ende der Beitragsreihe “C++: Die Basics” angelangt. Mit Stolz darfst du dir selbst auf die Schulter klopfen. Nicht viele haben die Ausdauer und Motivation, bis hierher zu gehen. Das liegt auch ein wenig an der Natur der Sache.

Die Softwareprogrammierung in C++ ist ein riesiges Themengebiet mit vielen, teils sehr abstrakten und komplizierten Inhalten. Kann im ersten Moment überwältigend sein und zugegeben manchmal etwas trocken. Doch der Grundstein ist gelegt.

Lass uns abschließend noch einmal alle Grundlagen zusammentragen.

Nachdem wir uns einen kleinen Überblick über C++ und seine Geschichte verschafft haben, sind wir direkt los und haben auf ganz klassischer Art mit einem Hello World gestartet.

// listing1: hello world

#include <iostream>

int main()
{
    std::cout << "Hello World!" << std::endl;
    return 0;
}

Ohne viel von dem geschriebenen Code zu verstehen, fokussierten wir uns erst auf das Kompilieren und Ausführen der winzigen Applikation.

# listing2: compile and run

$ g++ -o hello hello.cpp
$ ./hello
# Output

Hello World!

Nun waren wir in der Lage zu sehen, was unser Programm bewirkt, und wollten anschließend verstehen, wie es aufgebaut ist.

Da trafen wir zum ersten Mal auf Präprozessor-Anweisungen und die main Funktion, dem Rumpf deines Programms. Dazu kamen der return Wert der Funktion und der namespace der C++ Standard Library.

Anschließend fingen wir an mit primitiven Datatypen, eigenen Enums und auch Strukturen das Verhalten von Variablen und Konstanten zu erkunden. Bauten daraus erste kleine Kommandozeilenprogramme mit Ein- und Ausgabe.

// listing3: input and ouput

#include <iostream>

int main()
{
  struct {
    char* initials{new char[3]};
    float floatNumber;
    int intNumber;
  } inputStorage;

  std::cout << "Hi, what are your initials?" << std::endl;
  std::cin >> inputStorage.initials;

  std::cout << "And now, please tell me a number between 2.0 and 3.0" << std::endl;
  std::cin >> inputStorage.floatNumber;

  inputStorage.intNumber = inputStorage.floatNumber;

  std::cout << "Hey " << inputStorage.initials
            << ", be careful with the data types, otherwise your " 
            << inputStorage.floatNumber << " will quickly turn into a"
            << inputStorage.intNumber << "!" << std::endl;

  return 0;
}
# Output

Hi, what are your initials?
ec
And now, please tell me a number between 2.0 and 3.0
2.4
Hey ec, be careful with the data types, otherwise your 2.4 will quickly turn into a 2!

Doch das reichte noch nicht und wir nahmen uns komplexeren Datentypen, basierend auf Arrays an. Aus der C++ Standard Library folgten Vektor und Strings hinzu.

Ausgestattet mit einer guten Auswahl an Datentypen haben wir begonnen, Variablen mit Ausdrücken und Anweisungen miteinander in Beziehung zu setzen. Dazu haben wir eine große Anzahl an arithmetischen, relationalen, logischen, Bit und zusammengesetzten Operatoren verwendet.

Operator Table

Abbildung 1: Operatoren Übersicht

Von da an waren wir in der Lage, uns so auszudrücken, Variablen zu gestalten und miteinader zu verbinden, wie es uns gefällt. Oder der Anwendungsfall verlangt.

Doch es fehlte noch die Kontrolle über den Programmablauf. Bisher wurden alle unsere Anweisungen sequentiell von oben nach unten nacheinander ausgeführt. Das ist allerdings nicht in jedem Fall gewünscht.

Abhängig von bestimmten Gegebenheiten soll das Programm auch einen anderen Weg einschlagen können. Dazu haben wir Auswahl- und Verzweigungsanweisungen kennengelernt. Mit Konditionalen Ausdrücken und if ... else oder switch() Anweisungen haben wir das Programm entscheiden lassen, welcher Teil des Codes als nächstes ausgeführt wird.

// listing4: conditional execution with if ... else

#include <iostream>
#include <stdlib.h>

int main()
{
    bool condition = rand() % 2;
    std::cout << "condition is " << condition << std::endl;

    if(condition)
    {
        std::cout << "condition is true" << std::endl;
    }
    else
    {
        std::cout << "condition is false" << std::endl;
    }

    return 0;
}
# Output

condition is 1
condition is true

Zudem haben wir mit Schleifen Programmcodeabschnitte für eine bestimmte Anzahl wiederholen lassen.

// listing5: for loop

#include <iostream>
#include <stdlib.h>

int main()
{
    int guessable {rand() % 10};
    int guess {0};
    int numberOfGuesses {3};
    
    std::cout << "Guess a number between 0 and 10 " << std::endl;

    for(int i = 0; i < numberOfGuesses; i++)                // for loop
    {
        std::cout << numberOfGuesses - i << " guess left" << std::endl;
        std::cout << "Your guess: ";
        std::cin >> guess;

        if(guessable > guess)
        {
            std::cout << "I'm afraid you missed! Your number is too small. " << std::endl;
        }
        else if(guessable < guess)
        {
            std::cout << "I'm afraid you missed! Your number is too large. " << std::endl;
        }
        else
        {
            std::cout << "Great! "<< guess << " is right." << std::endl;
            break;
        }
    }

    std::cout << "See you!" << std::endl;

    return 0;
}
# Output

Guess a number between 0 and 10 
3 guess left
Your guess: 1
I'm afraid you missed! Your number is too small. 
2 guess left
Your guess: 2
I'm afraid you missed! Your number is too small. 
1 guess left
Your guess: 3
Great! 3 is right.
See you!

Da wir nun den Ablauf unter Kontrolle hatten, erweiterten wir unsere Fähigkeiten mit dem Definieren von Aufgaben als gekapselte Funktionen. Dieses mächtige Werkzeug bietet uns die Möglichkeit, unseren Code sinnhaft in kleine Pakete zu strukturieren, die wir jederzeit wiederverwenden können.

// listing6: functions

#include <iostream>

const double Pi = 3.14159265;

// functions
double diameter(double radius)
{
    return 2 * radius;
}

double area(double radius)
{
    return Pi * radius * radius;
}

double circumference(double radius)
{
    return 2 * Pi * radius;
};

// main function
int main()
{
    double radius = 0.0;
    
    std::cout << "Enter radius: ";
    std::cin >> radius;
    
    // call function "diameter"
    std::cout << "diameter is: " << diameter(radius) << std::endl;
    
    // call function "area"
    std::cout << "area is: " << area(radius) << std::endl;
    
    // call function "circumference"
    std::cout << "circumference is: " << circumference(radius) << std::endl;
    
    return 0;
}
# Output

Enter radius: 3
diameter is: 6
area is: 28.2743
circumference is: 18.8496

Wir erstellten Prototypen, Definitionen und Funktionsaufrufe. So wurde die Lesbarkeit und Übersicht unseres Codes erhöht.

Daraufhin sind wir tiefer in die Computerwissenschaften eingestiegen und haben uns etwas den Systemspeicher angeschaut. Das half uns, Zeiger und Referenzen besser verstehen zu können.

Mit den beiden Helfern ist es uns möglich gewesen, die Speichermenge von Variablen und den Zugriff auf bestimmte Variablen im Speicher selbst zu kontrollieren.

// listing7: pointers and references

#include <iostream>

int main()
{
    int anInt{15};          // variable initialization
    int& refToInt = anInt;  // reference intialization
    int* pointToInt;        // pointer declaration
    pointToInt = &anInt;    // pointer assignment

    std::cout << "Value of variable = " << anInt << std::endl;
    std::cout << "Address of variable = " << &anInt << std::endl;

    std::cout << "Value of reference = " << refToInt << std::endl;
    std::cout << "Address of reference = " << &refToInt << std::endl;
    
    std::cout << "Value of pointer = " << *pointToInt << std::endl;
    std::cout << "Address of pointer = " << pointToInt << std::endl;
}
# Output

Value of variable = 15
Address of variable = 0x7ffccb621f34
Value of reference = 15
Address of reference = 0x7ffccb621f34
Value of pointer = 15
Address of pointer = 0x7ffccb621f34

Ab da wurde es sehr wichtig, den Gültigkeitsbereich von Variablen zu kennen. Von wo sind Werte zu erreichen? Sind es die echten Werte oder nur Kopien? Wie verhält sich die Sichtbarkeit? Das wissen wir nun.

Ausblick

Nochmal auf alles Gelernte zurückzuschauen und bewusst die eigene Entwicklung zu sehen, fühlt sich sehr gut an. Damit hast du eine gute Basis und kannst dich selbstbewusst auf fortgeschrittene Themen in der Programmiersprache C++ stürzen. Und da gibt es einiges zu entdecken.

Objektorientierte Programmierung (OOP)

Hast du schon von Programmierparadigmen gehört? Diese beschreiben einen grundlegenden Programmierstil, auf den eine Programmiersprache ausgelegt ist. Auch wenn C++ mehrere Paradigmen unterstützt, ist sie meist als objektorientiert bekannt. Bei der objektorientierten Programmierung ist das Design auf Daten und Softwareobjekte fokussiert und weniger auf Funktionen oder Logik. Die Konzentration auf Objekte eignet sich sehr gut für große und komplexe Programme, die ständig aktualisiert oder weiterentwickelt werden. Es entstehen aufgabenspezifische gekapselte Softwareeinheiten, die sich wunderbar wiederverwenden lassen. Außerdem bringt es den Vorteilen, wenn mehrere Entwickler an einem Projekt arbeiten. Denn eine Änderung an einem Objekt verändert nicht den gesamten Code und es entstehen weniger Konflikte.

Clean Code

Beim kollaborativen Arbeiten hilft es deinen Teammitgeliedern, wenn du beim Design und der Implementierung deines Codes auf Sauberkeit, Lesbarkeit sowie Struktur achtest. Es gibt ein sehr bekanntes Buch [4] über Prinzipien, um intuitiv verständlich und leicht zu ändernden Clean Code zu schreiben. Da lernst du eine fantastische Anzahl an Akronymen kennen, wie KISS - Keep It Simple and Stupid, DRY - Don’t Repeat Yourself, YAGNI - You aren’t gonna need it, SOLID - Single-Responsibility; Open-Closed; Liskovsche Substitution; Interface-Segregation; Dependency-Inversion. So machst du es prinzipiell jedem Programmierer einfacher deinen Code zu verstehen und damit zu arbeiten.

Design Patterns

Du kannst dir auch Lösungsschablonen für immer wiederkehrende Entwurfsprobleme anschauen. Die sogenannten Design Patterns sind in einem Buch [5], das bis heute nicht an Bedeutung verloren hat, aufgeführt und beschrieben. Die Muster lassen sich auch für deine Softwarearchitektur kombinieren. So kannst du schnell bewährte Ansätze für deinen Code nutzen. Es handelt sich dabei wirklich nur um Muster, die du für deinen Anwendungsfall entsprechend anpassen musst. Also vermute keine Lösungen von der Stange, die einfach nur zu kopieren sind.

Forgeschrittenes C++

Wenn du dich nicht nur mit neuen Konzepten, Mustern oder Prinzipien weiterentwickeln möchtest, gibt es auch genügend technische Themen zu lernen. Wir haben uns zwar die C++ Standard Library schon angeschaut, doch bisher nur einen sehr kleinen Teil. Da kannst du noch viel tiefer einsteigen. Zudem gibt es in modernem C++ neue Funktionalität, wie beispielsweise Smart Pointer, Lambdas oder Templates. Du kannst deinen Code auch stabiler und sicherer gestalten, indem du dich mit Exception Handling auseinandersetzt. Oder mit Multithreading die Leistungsfähigkeit erhöhen.

Das bleibt hängen

Wie du siehst, hat unsere Reise in die Welt der Programmiersprache C++ gerade erst begonnen. Die ersten Steigungen haben wir geschafft, doch es wartet noch einiges für uns zu entdecken. Zum Schluss bleibt mir nur noch zu sagen:

Ich wünsche dir maximalen Erfolg!


Quellen

  • [1] B. Stroustrup, A Tour of C++. Pearson Education, 2. Auflage, 29. Juni 2018.
  • [2] B. Stroustrup, Programming: Principles and Practice Using C++. Addison Wesley, 2. Auflage, 15. Mai 2014.
  • [3] U. Breymann, Der C++ Programmierer. C++ lernen – professionell anwenden – Lösungen nutzen. Aktuell zu C++17. München: Carl Hanser Verlag GmbH & Co. KG, 5. Auflage, 6. November 2017
  • [4] R. C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall, 1. Auflage, 1. August 2008
  • [5] E. Gamma, R. Helm, R. Johnson, J. Vlissides, Design Patterns. Elements of Reusable Object-Oriented Software. Prentice Hall, 1. Auflage, 1. Juli 1997