Kapitel 12 - Einführung in C++

Peter Ulbrich

🚀 by Decker

Rückblick – Aktueller Stand

  • Weiterer Ablauf der Vorlesung:
    1. Einführung in C++ (nächstes Kapitel)
      • Wichtigste Unterschiede und Stolpersteine
    2. Objektorientierte Programmierung (OOP)
      • Bündelung von Funktionen, Structs und konsistenter Initialisierung
    3. Ausnahmenbehandlung (Exceptions)
    4. Generische Programmierung (Templates)
    5. Kurzer Ausflug in die Standard Template Library (STL)
fig/roadmap-intermezzo.svg

Dann legen wir mal los 😀

Unterschiede – C und C++

  • Zur Erinnerung: C++ ist ein Superset von C

  • Der Funktionsumfang von C ist (größtenteils) in C++ abgebildet

    Kein Zwang, andere Funktionen/Konzepte als bisher zu verwenden

  • Wichtig: Der C++-Standard ist riesig

    • ISO-Spezifikation von C++23 hat 2104 Seiten
    • Viele Funktionalitäten wegen Mängeln mehrfach implementiert
    • Alte Komponenten wurden wegen Abwärtskompatibilität behalten
    • Unmöglich, alles davon zu verwenden bzw. in einer Vorlesung zu behandeln
  • Pragmatische Lösung: Verwendung der benötigten/bevorzugten Komponenten

    → Rest ignorieren 😀

Wir schauen uns jetzt einmal das Notwendigste an

Back to the Roots – „Hello World“ in C++

#include <iostream>

int main() {
    std::cout << "Hello World" << std::endl;
}
cpp
  • Unmittelbar auffällig: Anderer Header + Ausgabefunktion sieht anders aus
  • Header in C++ verzichten auf das Suffix .h
  • Anstatt printf() jetzt std::cout (über Outputstreams)
  • Im Folgenden:
    • Wofür steht das std::?
    • Was zeichnet Outputstreams aus?

Einführung von Namensräumen

  • Worin besteht hier das Problem in C?
void print(double d);
void print(int i);
  • Zur Erinnerung: In C alle Funktionen und Variablen auf globaler Ebene angesiedelt
    • Zum einen: Sehr unübersichtlich
    • Zum anderen: Namensvergebung ist schwierig, Doppelungen leicht möglich
  • Die Lösung: Namensräume
  • Namensräume (Namespaces) kapseln Funktionalität
// Besser: Namespaces in C++
namespace Double {
    void print(double d) {
        std::cout << d << std::endl;
    }
}

namespace Integer {
    int i = 1;
    void print(int i) {
        std::cout << i << std::endl;
    }
}

Namensräume

  • Zugriff auf Komponenten des Namespaces mit dem Scope Resolution Operator::

  • Daher auch Präfix std → Zugriff auf Namensraum std

    std entspricht C++-Standardbibliothek

  • Anmerkung: Es gibt auch den globalen Namespace

    • Leerer Name, Zugriff mit :: als Präfix (→ ::x)
    • Verdeutlicht Verwendung globaler Variablebesseres Verständnis
    • Umgehung von lokalem Shadowing einer Variablen

Beispiel - Namensräume

#include <iostream>

int k = 4711;

namespace A {
    double d = 3.5;
    void print(double d) {
        std::cout << d << std::endl;
    }
}
namespace B {
    int i = 1;
    void print(int i) {
        std::cout << i << std::endl;
    }
}

int main() {
    A::print(A::d);
    B::print(B::i);
    B::print(::k);
    return 0;
}
cpp

Verschachtelung von Namensräumen

  • Namespaces können beliebig geschachtelt werden:
#include <iostream>

namespace A {
    int i = 0;
    namespace B {
        int add(int num1, int num2) {
            return num1 + num2;
        }
    }
    namespace C {
        int j = 2;
    }
}

int main () {
    std::cout << A::B::add(A::i, A::C::j) << std::endl;
    return 0;
}
cpp

(Namenräume können über mehrere Dateien verteilt sein → siehe std)

Verwendung von Namensräumen

  • Problem: Viel Tipparbeit bei Verwendung von Namensräumen
  • Lösung: Das Schlüsselwort using!
  • Erlaubt selektives Einbinden von benötigten Funktionen/Variablen
  • Einbinden von ganzen Namespaces ist ebenfalls möglich:

Beispiel: using namespace std

#include <iostream>
using std::cout, std::endl;

int main() {
    cout << "Hello World!" << endl;

}

 

#include <iostream>
using namespace std;

int main() {
    cout << "Hello World" << endl;
}

Anmerkungen zum Einsatz von using namespace std

  • Einerseits: Weniger Tipparbeit als vorher
  • Aber: Globaler Namesraum wieder genauso gefüllt wie in C
  • Vorteil der Schachtelung zunichte gemacht
  • Außerdem: Häufig werden eigene Implementierungen geschrieben
    • Bessere Performance für den jeweiligen Anwendungsfall
    • Namensraum hilft bei Unterscheidbarkeit
#include <iostream>
using namespace std;

int main() {
    cout << "Hello" << endl;
}
#include <cmath>

namespace FooMath {
    float sin(float num) {
        return num;
    }
}

int main() {
    // Jetzt gut unterscheidbar
    std::sin(3.5f); // -0.3507
    FooMath::sin(3.5f); // 3.5
}

Achtung: Viele (schlechte) Tutorials verwenden leider using namespace std! Nehmt euch die Zeit, das auszuschreiben. Es erspart auf lange Sicht Qualen.

Outputstreams

  • Parallel zu printf() gibt es in C++ einen neuen Mechanismus: Outputstreams
  • Name ist Programm: Sind als beliebig langer Zeichenstrom konzipiert
#include <iostream>
using std::cout, std::endl;

int main() {
    cout << "Hello" << endl;
}
  • Konsolenausgabe über std::cout
  • Verkettung der Zeichen durch Operator <<
  • Neue Zeile mit std::endl
  • Besonderheit: Setzen von Manipulator-Flags
    • „Einfärbung“ des Stroms, bis Flag wieder geändert wird
    • Beispiel: std::hex, um alle folgenden Zahlen hexadezimal darzustellen

Outputstreams

#include <iostream>

int main() {
    bool b = true;
    int i = 42;

    std::cout << "1: \t" << b << std::endl;
    std::cout << std::boolalpha;                // Uminterpretierung von bool
    std::cout << "true: \t" << b << std::endl;
    std::cout << std::noboolalpha;              // Revertierung
    std::cout << "1: \t" << b << std::endl;

    std::cout << "Dec: \t" << i << std::endl;   // Default ist std::dec
    std::cout << std::hex;
    std::cout << "Hex: \t" << i << std::endl;
    std::cout << std::oct;
    std::cout << "Oct: \t" << i << std::endl;
}
cpp

(Eine Liste aller Manipulator-Flags ist hier verlinkt)

Inputstreams

  • Analog zu Outputstreams gibt es Inputstreams
  • Funktionsweise wie bei Outputstreams, aber in umgekehrter Richtung
  • Einlesen von der Konsole: std::cin
  • Einlesen eines Zeichenstroms und Abspeichern in Variable
  • Achtung: Der Operator zum Verketten ist umgedreht → >>
#include <iostream>
using std::cout, std::endl;
using std::cin;

int main() {
    char buffer[24];
    cout << "Name: ";
    cin >> buffer;
    cout << buffer << endl;
    return 0;
}

Alternative: std::print

  • Outputstreams sind mächtiges Werkzeug
  • Aber: Für einfache Ausgabe oft zu mächtig und zu viel Schreibaufwand
  • Seit C++23: std::print()
  • Verbindet Einfachheit von printf() mit Nützlichkeit von Outputstreams
  • Erkennt Parameter über Position in Parameterliste und konvertiert diese
#include <print>

int main() {
    // Achtung: Nullbasiertes Indizes
    std::print("B: {1}, I: {0}, S: {2}", 1, true, "Hello");
    return 0;
}

Zeiger++

  • In C:     Zeiger
  • In C++: Zeiger + Referenzen
  • Funktional: Beide verweisen auf einen Speicherbereich
void swap (int *a, int *b); // Alt
void swap (int &a, int &b); // Neu

int a = 1;
int &a_ref = a;     // Alias von a
  • Notation: Referenzoperator & ersetzt Zeigerstern *
  • Hauptunterschied
    • Zeiger sind eigenständige Typen (mit eigenem Speicher, z.B. auf dem Stack)
    • Wert des Zeigers ist der verwiesene Speicherbereich
    • Wert kann verändert werden (→ Tor zur Hölle 😈)
  • Referenzen sind nur ein Alias der Variable, keine Dereferenzierung möglich!

Beispiel - Referenzen

#include <iostream>

void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int a = 1, b = 2;
    std::cout << "A: " << a << ", B: " << b << std::endl;
    swap(a, b);
    std::cout << "A: " << a << ", B: " << b << std::endl;

}
cpp

Funktionen++

  • C++ ermöglicht das sogenannte „Überladen“ von Funktionen (Overloading)
  • Gleichen Funktionsnamen mit verschiedenen Parametern zu überladen
  • Geht also auch ohne Namensräume
#include <iostream>

using std::cout, std::endl;

void print_num(double d);
void print_num(int i);

int main() {
    cout << print(3.5) << endl;
    cout << print(1) << endl;
}
  • Achtung: Gilt nur für die Parameter
  • Rückgabewert wird nicht berücksichtigt

 

void print_num(int i);
int  print_num(int i); // Error

Default-Werte für Funktionsparameter

  • Weitere Neuerung: Parameter können Default-Werte haben
  • Beginnend bei letztem Parameter können für Parameter Rückfallwerte verwendet werden
  • Derartige Parameter müssen nicht explizit im Aufruf genannt werden
int add1(int i, int j, int k = 2);           // Ok
int add2(int i, int j = 1, int k = 2);       // Ok
int add3(int i = 0, int j = 1, int k = 2);   // Ok
int add4(int i = 0, int j, int k = 2);       // Error, falsche Reihenfolge

add3();         // Rückgabe: 3
add3(1);        // Rückgabe: 4
add3(1,4);      // Rückgabe: 7
add3(1,2,3);    // Rückgabe: 6

Überladung von Funktionen hinter den Kulissen

// Dieser C-Quellcode...
int add(int i, int j) {
    return i + j;
}

int sub(int i, int j) {
    return i - j;
}

int main() {
    add(1,2);
    sub(2,1);
}
// ... erzeugt folgende Symbole
add
sub
main
// Dieser C++-Quellcode...
int add(int i, int j) {
    return i + j;
}

int sub(int i, int j) {
    return i - j;
}

int main() {
    add(1,2);
    sub(2,1);
}
// ... erzeugt folgende Symbole
_Z3addii
_Z3subii
main

In C++ codiert der Symbolname u. a. Parameter 💡

Name Mangling

  • Codiert u. a. Namensraum, Rückgabewert, Funktionsnamen, Parameter und deren Typen in Symbolname

    C++ Name Mangling

  • Achtung: Die Kodierung ist nicht standardisiert
    • Unter anderem abhängig von Compiler und Plattform
    • GCC und Clang verwenden unter Linux/Unix die Itanium C++ ABI für Name Mangling (Link)
    • Microsoft hingegen definiert seine eigene ABI für Windows
// Dieser C++-Quellcode...
int add(int i, int j) {
    return i + j;
}

int sub(int i, int j) {
    return i - j;
}

int main() {
    add(1,2);
    sub(2,1);
}
// ... erzeugt folgende Symbole
_Z3addii
_Z3subii
main

Name Mangling meets Reality

  • Situation: Existierendes C-Projekt mit C++-Code kombinieren
  • Problem
    • C++ nutzt Name Mangling, C hingegen nicht
    • Linker kann hier die Namen der Symbole nicht richtig auflösen
  • Lösung: Rückfall auf C-Namensschema mithilfe von extern "C"
  • Definierte Bereiche verwenden C Linkage
// Ohne Name Mangling...
int add(int i, int j);
int sub(int i, int j);
int main();
// ... werden C-Symbole erzeugt
add
sub
main

 

(Das C-Namensschema ist allgemein sehr einfach und wird deswegen für die Interoperabilität zwischen verschiedenen Programmiersprachen sehr oft verwendet.)

Abschalten des Name Manglings

Markieren einzelner Funktionen/Blöcke

// Markierung einer Funktion
extern "C" void print(int i);

// Alternativ: Blockweise Markierung
extern "C" {
    void foo1(double d);
    void foo2(char c);
}

Entfernen des Name Manglings für Header

// Kein #pragma once -> Komplett kompatibel
// Gut für Projekte mit C und C++
#ifndef HEADERNAME_H
#define HEADERNAME_H

#ifdef __cplusplus  // Nur in C++ definiert
extern "C" {        // Block-Beginn
#endif

/* Header-Code */

#ifdef __cplusplus
}                   // Block-Ende
#endif
#endif // HEADERNAME_H
// Dateiende

Strings

  • In C++ eine Abstraktion für Strings → std::string
std::string str = "Hello";
std::cout << str;
  • Kapselt C-String char * bzw. char [] in Klasse
  • Zugriff auf C-String mit c_str()
  • Neuer Komfort: size() → Gibt Größe aus
  • std::string bietet einige Vorteile
    • Einfache String-Konkatenation: str += " World";
  • Für vollständige Liste der Funktionen in Referenz nachschauen

Range-based for-loops

  • In C++ gibt es eine Abstraktion der bereits bekannten for-Schleifen:

    Range-based for-loops

  • Gedacht als Tipperleichterung für komplexere Daten-Container wie std::vector (kommt gleich)

  • Vorteil: Es muss nicht mehr auf die Indizes geachtet werden

  • Erreicht dies unter der Haube über sogenannte Iteratoren (hier nicht weiter Thema)

#include <string>
#include <iostream>
using std::cout, std::endl;

int main() {
    std::string str = "Hello World";
    for (char c: str) {
        // Print every character
        cout << c;
    }
    cout << endl;
}
cpp

Range-based for-loops

  • Achtung: Range-based for-loops haben eine kleine Tücke
  • Die Schleifenvariable wird standardmäßig bei jedem Durchlauf neu initialisiert (By Value!)
    • Schlecht bei großen Datenstrukturen (Structs/Klassen)
  • Besser: Variable als Referenz deklarieren
  • Und falls Daten nicht verändert werden sollen: Zusätzliches const
// Okay für primitive Datentypen:
// By-Value
for (char c: str) {
    cout << c;
}
cout << endl;
// Besser für komplexe Datentypen:
// Read-Only und By-Reference
for (const char & c: str) {
    cout << c;
}
cout << endl;

Moderne Felder

  • Klasse std::vector kapselt Eigenschaften eines Feldes
  • Zugriff mittels [], kann beliebige Datentypen aufnehmen und dynamische Größe 🤯
    • Alternativ: Zugriff mit Funktion at(), da [] bei falschem Index undefiniert ist
#include <iostream>
#include <vector>

int main() {
    std::vector<int> nums;

    nums.push_back(3);
    nums.push_back(42);
    nums.push_back(-1);

    nums[2] += 10;

    for (const int & i : nums) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    for (size_t i = 0; i < nums.size(); ++i) {
        std::cout << nums.at(i) << " ";
    }
    std::cout << std::endl;

    return 0;
}
cpp

Zusammenfassung

  • Übergang von zu C zu C++ 🥳
  • „Hello World“-Beispiel in C++
  • Namensräume
  • Überladen von Funktionen
  • Default-Parameter
  • Range-based for loops
  • std::string und std::vector
Logo Sys
Kapitel 12 - Einführung in C++