Einführung in C++

02 - Repräsentationen und Pointer

Prof. Dr. Malte Schilling

Autonomous Intelligent Systems Group

🚀 by Decker

Überblick heutige Termin

  • Wiederholung zum Kontrollfluss,
  • Funktionen (Deklaration und Definition), Overloading
  • Einführung von strukturierten Datentypen
  • Pointer und Referenzen (sowie Arrays) und einführend etwas zum Handling des Memory in C++ und C

Rückblick: Grundlegende C++-Typen

  • C++ übernimmt aus C die bekannten Basisdatentypen:
    • Für ganze Zahlen (Integer-Typen): char, short, int, long, long long, Bsp.: int i = 42;
    • Für Gleitkommazahlen: float, double, long double, Beispiel: double d = 42.23;
    • Für Buchstaben: char, Beispiel: char c = ’a’;
    • Wahrheiswerte (in C++ Teil der Sprache): bool, Beispiel: bool b = true;
  • Achtung: Auch in C++ ist die Größe der Basisdatentypen systemabhängig!
  • C++ bietet zusätzlich eine Klasse für Strings im Header <string> an.

Variablen und Datentypen in C++

  • Variablen können entweder deklariert (int i;) oder direkt mit einem Wert initialisiert werden (int i = 42; float f = randomFloat();).
  • Werden Variablen direkt initialisiert, kann in C++ anstelle des Datentyps das Schlüsselwort auto verwendet werden:
    • auto i = 42; auto f = randomFloat();
  • Der Compiler ermittelt den Datentyp dann automatisch (Type Inference).

Datentyp (Definition 1.8)

Ein Datentyp ist eine Menge von Werten zusammen mit einer Menge von Operationen, die auf diesen Werten definiert sind.

C++ ist eine statisch typisierte Sprache

Statisch typisiert

Alles mit einem Namen (Variablen, Funktionen, etc.) erhält einen Typ vor der Laufzeit.

Dynamisch typisiert

Allem, was einen Namen hat (Variablen, Funktionen, etc.), wird zur Laufzeit einen Typ zugewiesen basierend auf dem aktuellen Zustand und Wert.

Kompiliert vs. Interpretiert

Quellcode: Der ursprüngliche Code, der normalerweise von einem Menschen in einen Computer eingegeben wird.

Übersetzung: Die Umwandlung von Quellcode in etwas, das ein Computer verstehen kann (d.h. Maschinencode).

Hauptunterschied: Wann wird der Quellcode übersetzt?

Dynamisch typisiert, interpretiert

  • Die Typen werden während der Ausführung überprüft, Zeile für Zeile
  • Beispiel: Python

Statisch typisiert, kompiliert

  • Typisierung vor Programmstart
  • Überprüfung bei Kompilierung
  • Beispiel: C++, C

Laufzeit

Zeitraum, in dem das Programm Befehle ausführt (gegebenenfalls nach Kompilierung).

Kompiler-Sequenz

Beim Übersetzen in ein ausführbares Programm werden folgende Schritte durchgeführt:

  1. Der Präprozessor ersetzt Makros (z. B. #include).
  2. Der Compiler übersetzt den Quellcode in ein Assemblerprogramm.
  3. Der Assembler erzeugt Maschinencode.
  4. Der Linker verbindet die vers. Dateien zu einer ausführbaren Datei.
code-9c78fbe6.tex.svg
code-b2cb7882.tex.svg
code-22fc2d15.tex.svg
code-472b1d24.tex.svg
code-472b1d24.tex.svg

Dynamische vs. statische Typisierung: Python vs. C++

Python

val = 3
bVal = true
str = "hi"
code-ad13f09a.dot.svg

C++

int val = 3 ;
bool bVal = true;
string str = "hi";
code-4b3230c7.dot.svg

Dynamische vs. statische Typisierung: Python vs. C++

Python

val = 3
bVal = true
str = "hi"
val = "hi"
str = 100
code-ebf6d04f.dot.svg

C++

int val = 3 ;
bool bVal = true;
string str = "hi";
val = "hi"; // ERROR!
str = 100;
code-9d755399.dot.svg

Dynamische vs. statische Typisierung: Python vs. C++

Python

def div_3(x):
	return x / 3

div_3("hallo")

Crash während der Laufzeit – kann eine Zeichenkette nicht dividieren.

C++

int div_3 (int x){
	return x / 3 ;
}
div_3("hallo")

Kompilierfehler: Dieser Code wird nie ausgeführt.

Dynamische vs. statische Typisierung: Python vs. C++

Python

def mul_3(x):
	return x * 3

mul_3("10")

returns "101010"

C++

int mul_3 (int x){
	return x * 3 ;
}
mul_3("10")

Kompilierfehler: “10” ist ein String! Dieser Code würde nicht laufen können.

Dynamische vs. statische Typisierung: Python vs. C++

Python

def add_3(x):
	return x + 3

add_3("10")

returns "103"

C++

int add_3 (int x){
	return x + 3 ;
}
add_3("10")

Kompilierfehler: “10” ist ein String!


Statische Typisierung

… hilft uns dabei Fehler zu verhindern, bevor unsere Code läuft.

Typen in Sprachen

Vorteile ungetypter Sprachen

  • Alle Werte sind von gleicher Art, keine “logischen” Fehler.
  • Möglichkeit, z. B. Funktionen als Daten zu betrachten (analog: Daten wie Funktionen auszuwerten).

Vorteile getypter Sprachen

  • Vereinfachtes Eliminieren von (nicht-abgefangenen) Fehlern.
  • Effizienz bei der Ausführung (Optimierung).
  • Effizienz bei der Entwicklung “im Kleinen” (Fehlersuche).
  • Effizienz bei der Entwicklung “im Großen” (Modularisierung).

Typkonvertierung (cast)

  • Achtung: C und C++ führen automatisch Typkonvertierungen zwischen den Basisdatentypen durch.
  • Dabei können Informationen verloren gehen, z. B.: int i = 5.6;
  • Bei der Parameterübergabe an Funktionen findet ebenfalls eine automatische Konvertierung statt.
  • Man kann eine Typkonvertierung manuell erzwingen:

In C (und C++)

int i = (int) 5.6; 
float f = (float) i;

In C++

int i = static_cast<int>(5.6); 
float f = static_cast<float>(i);

Typkonvertierung (cast)

In C++ gibt es verschiedene Cast-Operatoren mit abgestufter Wirkungsweise:

  • static_cast = Operator erlaubt Konvertierungen zwischen Basisdatentypen sowie zwischen Klassen, die durch öffentliche, nicht-virtuelle Ableitung auseinander hervorgehen.
  • dynamic_cast = Hiermit werden Zeiger oder Referenzen auf miteinander verwandte polymorphe Klassen – und nur solche – konvertiert.
  • const_cast = Dient ausschließlich dazu, die Konstantheit eines Typs zu entfernen. Dies ist ein schwerwiegender und potentiell fehlerträchtiger Vorgang.
  • reinterpret_cast = Dieser Operator konvertiert beliebige Zeigertypen ineinander, unabhängig vom Verwandtschaftsgrad, und konvertiert alle Zeiger in ganzzahlige Typen und umgekehrt.

Beispiele Konvertierung

const_cast

#include <iostream>

void changetNumber(const int& num) {
    // Versuch, die Konstanz des num-Referenzparameters aufzuheben
    int& numRef = const_cast<int&>(num);
    numRef = 99; // Aenderung Wert
}

int main() {
    int num = 42;
    const int& numRef = num; 
    std::cout << "Vorher: " << num << std::endl;
    changeNumber(numRef);
    std::cout << "Nachher: " << num << std::endl;
    return 0;
}

reinterpret_cast

int num = 42;

// Ein Zeiger auf eine int-Variable
int* pNum = &num;

// Ein Zeiger auf eine char-Variable
char* pChar = reinterpret_cast<char*>(pNum);

Typen-Deduktion über auto

auto

Schlüsselwort verwendet anstelle eines Typen, wenn eine Variable deklariert wird. Dies teilt dem Compiler mit, dass er den Typ selbst bestimmen muss.

// Welche Typen liegen hier vor?
auto a = 3 ;
auto b = 4.3;
auto c = "X";
auto d = "Hallo";
auto e = std::make_pair( 3 , "Hallo");
// Welche Typen liegen hier vor?
auto a = 3 ; // int
auto b = 4.3; // double
auto c = "X"; // char
auto d = "Hallo"; // char* (ein C-String)
auto e = std::make_pair( 3 , "Hallo"); // std::pair<int, char*>

auto bedeutet natürlich nicht, dass die Variable keinen Typ hat. Es drückt nur aus, dass der Typ vom Compiler hergeleitet wird.

Faustregel: AAA-Style (Almost Always Auto) nutzen.

Kontrollfluss

Rückblick: Kontrollfluss

Kontrollstrukturen erlauben …

Bedingte Ausführung

  • if-else
  • switch-case

Schleifen

  • for
  • while
  • do-while

Sprünge

  • goto und Spungmarken

if-else

Syntax if-else

if (Bedingunung)
    Anweisung1
else
    Anweisung2

  • Bedingung ist ein Ausdruck, der nach bool konvertierbar ist
  • Anweisungen können Ausdrücke, Blöcke oder andere Kontrollstrukturen sein

Beispiel

bool x = true;
if (x == true){ // aequivalent: if (x)
    std::cout<<"x is true"<<std::endl;
} else {
    std::cout<<"x is false"<<std::endl;
}

Negativ-Beispiel für das Weglassen von Blöcken

bool x = true;
bool y = true;
if (x == true)
    if (y == false)
        std::cout<<"x true and y false"<<std::endl;
else
    std::cout<<"x false"<<std::endl;

Programm gibt x false aus. Zu welchem if gehört das else?

Besser: Explizite Blöcke benutzen!

while-Schleifen

Anweisung wird wiederholt ausgeführt, solange Bedingung zutrifft (also zu true ausgewertet wird).

Syntax while

while (Bedingung) {
    Anweisung
}

Beispiel für while

int x = 0;
while (x<100) {
    std::cout << x++ << std::endl;
}

do-while-Schleifen

  • Anweisung wird wiederholt ausgeführt, solange Bedingung true ist
  • Anders als bei while wird hier Anweisung mindestens einmal ausgeführt

Syntax do while

do {
    Anweisung
} while (Bedingung);

Beispiel für do-while

int x = 0;
do {
    std::cout << x++ << std::endl;
} while (x<100);

for-Schleifen

Der große Bruder der while-Schleife …

Syntax for

for (Initialisierung; Bedingung; Schritt) {
    Anweisung
}
  • Initialisierung, Bedingung, Schritt sind Ausdrücke
  • Bedingung muss nach bool konvertierbar sein

Beispiele für for-Schleifen

char text[] ="hello world";
for(int i=0; i<strlen(text); ++i) {
    if (text[i] ==' ') text[i] ='\n';
}

oder besser …

for (int i=0, end=strlen(text); i<end; ++i){
    if(text[i] ==' ') text[i] ='\n';
}

Die break-Anweisung

Die Anweisung break verursacht, dass der aktuelle (innerste) for, while oder case-Block verlassen wird.

#include <string>

int main(){
    std::string str;
    std::cin >> str;
    for(int i=0; true; ++i){
        if (!str[i]) 
        	break;
        str[i] = toupper(str[i]);
    }
}

Die continue-Anweisung

  • Verursacht einen Sprung an das Ende der aktuellen Schleife
  • Nur innerhalb des Rumpfes von for, while und do-while-Schleifen
  • Ermöglicht flachere und übersichtlichere Klammerstruktur
#include <string>
#include <iostream>

int main() {
    std::string str; std::cin >> str;
    for(int i=0; true; i++) {
        if (!str[i]) break;
        if (str[i]>=’A’ && str[i]<=’Z’) continue;
        if (str[i]>=’a’ && str[i]<=’z’) str[i] += (’A’−’a’);
    }
    std::cout << str << std::endl;
}

switch-case

  • Für Fallunterscheidungen

Syntax: switch-case

switch (Ausdruck) {
    case C1: Anweisungen
    case C2: Anweisungen
    ...
    default: Anweisungen
}
  • Tatsächlich kann der Block auch nur eine einzelne Anweisung sein
  • Ausdruck muss ein Ganzzahl-Typ – ein sog. Integral-Typ – sein(bool, char, wchar_t,(un)signed int,short)
  • C1, C2,… müssen konstante Ganzzahl-Ausdrücke sein.

Anmerkungen zu switch-case

  • Wichtig: Die einzelnen Anweisungsteile müssen explizit mit einem break-Statement beendet werden.
  • Ansonsten werden alle folgenden Anweisungsteile mitausgeführt
  • Sollten in der Anweisungsliste einer case Marke Variablen deklariert werden, so muss die Anweisungsliste in einem Block zusammengefasst werden (Grund: nicht klar definierbare Lebenszeit des Variablenbezeichners)

switch-case Beispiel

void handle_event(const MouseEvent &evt){
    switch(evt.getType()) {
        case PRESS_EVENT:
            global_mouse_down = true;
            break;
        case RELEASE_EVENT:
            global_mouse_down = false;
            break;
        case MOVE_EVENT: {
            int x = evt.getX(), y = evt.getY();
            global_mouse_pos = Point(x,y);
            break;
        }
        default:
            return;
    }
    update_visualization();
}

switch-case-Anmerkungen

  • Manchmal möchte man in Fallunterscheidung lokale Variablen anlegen
  • Da break optional ist kann es hierzu zu Überspringen von Variablendeklarationen kommen!
switch(evt.getType()) {
    case PRESS_EVENT:
        int x = evt.x(); int y = evt.y();
        // ...
        break;
    case MOVE_EVENT:
        int x = evt.x(); int y = evt.y(); // geht nicht
        x = evt.x(); y = evt.y(); // geht auch nicht
        // ...
        break;
}

switch-case-Anmerkung

Lösung: lokale Blöcke benutzen

switch(evt.getType()) {
    case PRESS_EVENT:
    {
        int x = evt.x(); int y = evt.y();
        // ...
    }
    break;
    case MOVE_EVENT:
    {
        int x = evt.x(); int y = evt.y(); // geht!!
        // ...
    }
    break;
}

Die goto-Anweisung

  • C-Relikt
  • wird in C++ sehr selten (und ungern) gesehen
int main() {
    int i = 0;
    for (; i<100; ++i){
        if (i == 10){
            goto MARKE;
        }
    }
    MARKE:
    std::cout << i << std::endl;
    return 0;
}
  • Besser: Verwendung von Exceptions (später)

Anmerkungen zu goto

  • MARKE und goto MARKE müssen in der gleichen Funktion stehen
  • Andere Sprünge mittels setjmp und longjmp (verfügbar durch den Header <csetjmp>)
  • Diese werden z.B. zur Fehlerbehandlung in C-Bibliotheken verwendet
  • Sind aber gefährlicher
  • In C++: Leichtere Ausnahmenbehandlung mittels try-catch-Blöcken und dem throw-Operator (später)

Funktionsdeklaration und -definition

Funktionen

  • Funktionen sind das Hauptelement zur Strukturierung eines Programms in C (und auch in C++ ein wichtiges Strukturierungselement).
  • Funktionsdefinition ähnlich wie in Java:
int max( int a, int b )
{
	return a>b? a : b;
}
  • Soll eine Funktion keinen Wert zurückgeben, muss der Typ void vereinbart werden (return darf dann entfallen).

  • Beim Aufrufen einer Funktion werden die Werte der Parameter als Kopie übergeben (call by value).

  • Ausnahme: Arrays (und damit auch „Strings“) – mehr dazu später.

Funktionsdeklaration

  • Eine Funktion kann zunächst deklariert und später definiert werden.
  • Beispiel: int max( int , int ); – hier dürfen die Namen der Parameter entfallen.

Warum gibt es die Unterscheidung zwischen Definition und Deklaration?

  • Die Deklaration enthält Informationen wie die Funktion zu benutzen ist.
  • Die Definition legt fest, was die Funktion tut.

Funktionsdeklaration

  • Beim Übersetzten überprüft der Compiler bei jedem Funktionsaufruf:
    • Gibt es eine Funktion mit dem Namen?
    • Passen die angegeben Argumente mit den Typen der Parametern zusammen?
    • Passt der Rückgabetyp?
  • Dazu reichen dem Compiler die Informationen aus der Deklaration aus.
  • Die Definition kann in einer anderen Datei stehen und wird dann erst vom Linker mit dem Funktionsaufruf verbunden (mehr dazu später).

void als Parameterliste

Eine Besonderheit in C im Vergleich zu anderen C-ähnlichen Sprachen:

  • Soll eine Funktion keine Parameter entgegennehmen ist es korrekter void als Parameterliste anzugeben.
  • Das Fehlen einer Parameterliste in C bedeutet, dass eine beliebige Anzahl von Parametern mit beliebigen Typen übergeben werden kann (dies gilt nicht in C++):
int add_no_void () { int x = 5; int y = 2; return x + y; }

int add_with_void( void ) { int x = 5; int y = 2; return x + y; }

int main( void ) {
	int res1 = add_no_void (5, ’c’ ); // No error
	int res2 = add_with_void (5, ’c’ );// Error: too many arguments
}

Standardparameter

  • C++ bietet die Möglichkeit, für Funktionsparameter Standardausdrücke vorzugeben.
  • Diese werden automatisch eingesetzt, wenn der entsprechende Parameter beim Aufruf fehlt.
int foo(int x, int y = 1000); 
int test = foo(42);
  • Standardparameter müssen am Ende der Parameterliste stehen.
  • Gibt es eine Funktionsdeklaration, so werden die Standardausdrücke dort angegeben und nicht in der Funktionsdefinition.

Call by Value

Funktionsparameter werden in C und C++ immer by value übergeben, d. h.:

  • Die Parameter werden kopiert.
  • Die Funktion arbeitet auf der Kopie eines Parameters.
  • Das Original kann von der Funktion nicht verändert werden.
int foo(int i, int j) {
  i = i * 2;
  j++; // j = j + 1
  return i + j;
}

int main () {
  int i = 5; int j = 6; 
  int k = foo(i, j);
}

Überladen von Funktionen

Was ist, wenn wir zwei Versionen einer Funktion für zwei verschiedene Typen haben wollen?

Beispiel: int-Division und double-Division

Überladen

Overloading

Definiere zwei Funktionen mit demselben Namen, aber unterschiedlichen Typen.

int half( int x ) {
	std::cout << "1" << endl; // (1)
	return x / 2 ;
}

double half( double x ) {
	std::cout << "2" << endl; // (2)
	return x / 2 ;
}

Aufruf:

half( 3 ) // verwendet Version **(1)** , gibt ? zurück 
half(3.0) // verwendet Version **(2)** , gibt ? zurück

Aufruf:

half( 3 ) // verwendet Version **(1)** , gibt 1 zurück 
half(3.0) // verwendet Version **(2)** , gibt 1.5 zurück

Überladen

int half( int x, divisor=2 ) {
	std::cout << "1" << endl; // (1)
	return x / divisor ;
}

double half( double x) {
	std::cout << "2" << endl; // (2)
	return x / 2 ;
}

Aufruf:

half( 4 ) // verwendet Version **(1)** , gibt ? zurück 
half( 3, 3 ) // verwendet Version **(1)** , gibt ? zurück 

half(3.0) // verwendet Version **(2)** , gibt ? zurück

Aufruf:

half( 4 ) // verwendet Version **(1)** , gibt 2 zurück 
half( 3, 3 ) // verwendet Version **(1)** , gibt 1 zurück 

half(3.0) // verwendet Version **(2)** , gibt 1.5 zurück

Hinweis: Wiederholungsaufgabe 2_1

Im jupyter-book sind verschiedene Aufgaben angegeben:

Jupyter-Book Link

Hierüber kann dann direkt auf dem Hub eine Umgebung gestartet werden, in der C++ interpretiert wird (wird die ersten Termine genutzt):

JupyterHub Link

Struct – Repräsentationen in C und C++

Computation

Vorgehensweise:

  1. Analyse der realen Situation. Bestimmung für die Anwendung wesentlicher Aspekte.
  2. Abstraktion von der so analysierten Situation. Ersetzen der (komplexen) realen Situation durch einfacheres Modell, in dessen Rahmen die Anwendung beschrieben werden kann.
  3. Darstellung der abstrakten Situation. Aufstellen einer Repräsentation mit festgelegten Interpretationsregeln.

Ziel: Formales Modell, das von einer Maschine bearbeitbar ist.

Ziel: Maschinelle Verarbeitung von Informationen. Verarbeitung nur möglich auf Basis von Repräsentationen (Schwerpunkt Woche 2).

Modellbildung

Ziel der Modellbildung: Lösung eines Problems.

Festlegen (durch Spezifikation), welches Ziel gelöst werden soll.

Präzisierung des Modells:

  • Ablaufmodellierung: – Beschreibung der Verarbeitung der Datenobjekte. – Darstellungsformen: imperative/funktionale/prädikative/objektorientierte Form.
  • Datenmodellierung: – Angabe der Datenobjekte, mit denen die Realität nachgebildet wird. – Notwendig hierzu:Konstruktion von Datentypen und-objekten.

Methoden zur Modellbildung:

Logische Strukturen, Graphen, Grammatiken, Automatenmodelle, Petri-Netze, UML-Diagramme …

Erstellung von Programmen

In fünf Schritten

  1. Datenanalyse und -definition.
  2. Signatur, Zweck und Funktionskopf angeben.
  3. Beispiele erstellen.
  4. Funktionsrumpf erstellen.
  5. Funktionsweise überprüfen.

Durchführen von 1. Datenanalyse und Datendefinition: Überprüfen, ob die Aufgabenstellung die Verwendung von Strukturen nahe legt.

Structs

  • C hat keine Klassen oder Objekte, aber Structs die als Ersatz verwendet werden können.
  • In einem struct können mehrere Datentypen zu einem neuen Datentypen zusammengefasst werden.
  • Ein Struct entspricht einer Klasse ohne Methoden mit ausschließlicher public-Sichtbarkeit.

Beispiel

struct person {
	char name [16];// Deklariere Member name
	char lastname [16];
	unsigned int age;
}; // <-- Achtung: ; nicht vergessen!

// Initialisierung
struct person p = { "Vorname" , "Nachname" , 24};

// Zugriff auf Elemente wie in Java:
p.age = 42;
printf( "Ich bin %s %s und %u Jahre alt .\n" , p.name , p.lastname , p.age);

Structs

struct gehört immer zum Namen der Struktur!

  • Mit typedef kann direkt bei der Deklaration einer Struktur ein „einfacherer“ Name vereinbart werden:
typedef struct {
	char name [16];
	char lastname [16];
	unsigned int age;
} person_t;

person_t p = { "Vorname" , "Nachname" , 42};

Structs können innerhalb von Structs verwendet werden (wie Klassen in z.B. Java).

Größe von Structs

Die Größe eines Structs kann wie für primitive Datentypen abgefragt werden:

sizeof (struct person)
sizeof (person_t)

Aber: sizeof ( struct ) \(\neq\) Summe der Größe der einzelnen Bestandteile/ Member:

struct values {
	char c1;
	int i;
	char c2;
}

// Summ der Größe aller Member
// Output für 2 * sizeof ( char ) + sizeof ( int ) = 6
// Größe von struct values
// Output für sizeof ( struct values) = 12

Hinweis Aufgabe 2_2

Im jupyter-book sind verschiedene Aufgaben angegeben:

Jupyter-Book Link

Hierüber kann dann direkt auf dem Hub eine Umgebung gestartet werden, in der C++ interpretiert wird (wird die ersten Termine genutzt):

JupyterHub Link

Pointer und Referenzen

Pointer

Erinnerung: Eine Variable ist ein Name für eine Stelle im Speicher, an der ein Datum abgespeichert wird.

  • Zu jeder Variable existiert eine Adresse im Speicher.
  • Diese Adresse kann man mit dem Adressoperator & erfahren:
int i = 5;
printf( _"%p"_ , &i ); // Ausgabe in C
// Mögliche Ausgabe: 0 x7ffcea219ff

Pointer

Eine Variable, die die Adresse einer anderen Variable speichert (auf sie zeigt), nennt man einen Zeiger (engl. Pointer).

Pointer

  • Ein Pointer auf eine int-Variable hat den Typ int*.
int i = 5; float f = 4.2;
int *ip = &i; float *fp = &f;

Allgemein: Ein Pointer auf eine Variable vom Typ T hat den Typ T*.

  • Mithilfe des Dereferenzierungsoperators * kann auf das gespeicherte Datum zugegriffen werden.
int a = 42;
int* b = &a; // Adressoperator angewendet auf a

*b = 12; // Dereferenzierungsoperator angewendet auf b; a == 12

../data/01/pointer.svg

Visualisierung Pointer

code-32ad05cf.tex.svg
code-7b590b4c.tex.svg
code-53e300e0.tex.svg
code-df106a01.tex.svg
code-b45419f5.tex.svg
code-3368d916.tex.svg
code-b20ec228.tex.svg

Überblick: Pointer

  • Ein Zeiger (engl. Pointer) enthält die Adresse einer Variable im Speicher.
  • Die Speicheradresse einer Variable liefert der Adressoperator &.
  • Spezielle Adresse: NULL – „Zeiger ins Nichts“.
  • Die Variable zu einem Pointer liefert der Dereferenzierungsoperator *.
  • Der Typ eines Pointers auf eine Variable vom Typ T ist T*.
int a = 42;
int* b = &a; // Adressoperator angewendet auf a
*b = 12; // Dereferenzierungsoperator angewendet auf b; a == 12

Array-Variable und Pointer

  • Eine Array-Variable verhält sich wie ein Pointer, der auf das erste Element des Arrays zeigt.
  • Auf einen Pointer kann – wie auf ein Array – mit dem []-Operator zugegriffen werden. Dabei gilt: ptr[i] == *(ptr+i)
  • Übergibt man einen Array-Typ T[] als Parameter an eine Funktion, „verfällt“ der Typ zum entsprechenden Pointer T* (Array Decaying).

Referenzen

  • Der Umgang mit Pointern ist besonders am Anfang schwierig.
  • C++ führt mit Referenzen ein ähnliches Sprachmittel ein.
  • Wie Pointer sind Referenzen Variablen, die Verweise auf andere Variablen speichern.
  • Der Typ einer Referenz auf eine Variable vom Typ T ist T&.

Unterschied zu Pointern

  • Zur Initialisierung wird nicht der Adressoperator verwendet!
  • Zum Zugriff auf das gespeicherte Datum wird nicht der Dereferenzierungsoperator verwendet werden!
int b = 58;
int& c = b;
std::cout << "b: " << b << " / c: " << c << std::endl; // gibt 58 fuer beide aus
b = 42; // c == 42

Beispiel Referenzen

code-cd871d23.tex.svg
code-ee5b7b38.tex.svg
code-6ec1d950.tex.svg
code-c523fb08.tex.svg
code-981c9b42.tex.svg
code-a6b74c46.tex.svg
code-2342280b.tex.svg

Hinweis Aufgabe 2_3

Im jupyter-book sind verschiedene Aufgaben angegeben:

Jupyter-Book Link

Hierüber kann dann direkt auf dem Hub eine Umgebung gestartet werden, in der C++ interpretiert wird (wird die ersten Termine genutzt):

JupyterHub Link

Pointer

  • Ein Pointer kann (nacheinander) auf unterschiedliche Variablen zeigen:
int i1 = 5;
int i2 = 6;
int* ip = &i1;// ip zeigt auf i
// ...
ip = &i2;// ip zeigt auf i

NULL-Pointer

Der spezielle Wert NULL kann einem Pointer zugewiesen werden, wenn dieser ins „Nichts“ zeigen soll:

In C++

nullptr ist ein spezieller Zeigerwert, der seit C++11 eingeführt wurde und explizit auf keine gültige Speicheradresse zeigt (vorher einfach 0 verwendet).

int* ptr = nullptr; // NULL-Zeiger in C++
if (ptr == nullptr) {
    cout << "Zeiger zeigt auf nullptr." << endl;
}

In C

NULL ist definiert in stdio.h).

int* ip = NULL;
int i = 5;
ip = &i;

Pointer

Zu beachten:

  • In Variablendeklarationen muss * vor jedem definierten Pointer stehen.

Beispiel

int* ip = NULL; // ip ist int -Pointer
int a, *ap, b, *bp;// a, b sind int -Variablen ; ap , bp sind int-Pointer
int* x, y, z; // x ist int -Pointer; y, z sind int - Variablen

Pointer: const

  • Bei Pointern hat das Schlüsselwort const mehrere Bedeutungen:
    • Der Wert, auf den der Pointer verweist, soll nicht verändert werden: int const* ip;oder const int* ip;
    • Der Pointer soll nicht verändert werden, d. h., der Pointer kann nicht neu zugewiesen werden: int* const ip;
    • Wert und Pointer sollen nicht verändert werden: const int* const ip;

Probleme mit Pointern

Der falsche Umgang mit Pointern ist eine der Hauptfehlerquellen in C!

Task: Was ist hier falsch?

int a = 0;
int* b = a;
*b = 1;

Problem

Pointer erlauben unkontrollierten Zugriff auf beliebige Speicherstellen.

  • Führt zu Programmabbruch durch das Betriebssystem bei Zugriff auf Speicher außerhalb des Prozesses (gut!).
  • Prozess-lokale Daten können überschrieben werden (schlecht!).
  • Fehler durch falsche Pointer sind oft sehr schwer zu finden.

Pointer Arithmetik

Addition einer Konstanten erhöht einen Pointer um ein Vielfaches der Größe des Basisdatentyps (Pointerarithmetik).

Betrachte:

char c;
int i;
printf( "&c = %p, &i = %p\n" , &c , &i );
printf( "&c + 1 = %p, &i + 1 = %p\n" , &c + 1, &i + 1 );

Wie lässt sich diese mögliche Ausgabe erklären?

&c = 0x7fff55f1cb2b , &i = 0x7fff55f1cb24
&c + 1 = 0x7fff55f1cb2c , &i + 1 = 0x7fff55f1cb28

→ Pointerarithmetik: Auf diesem System hat char die Größe 1 und int die Größe 4.

Referenzen

  • Der Umgang mit Pointern ist besonders am Anfang schwierig.
  • C++ führt mit Referenzen ein ähnliches Sprachmittel ein.
  • Wie Pointer sind Referenzen Variablen, die Verweise auf andere Variablen speichern.
  • Der Typ einer Referenz auf eine Variable vom Typ T ist T&.

Unterschied zu Pointern

  • Zur Initialisierung wird nicht der Adressoperator verwendet!
  • Zum Zugriff auf das gespeicherte Datum wird nicht der Dereferenzierungsoperator verwendet werden!
int b = 58;
int& c = b;
std::cout << "b: " << b << " / c: " << c << std::endl; // gibt 58 fuer beide aus
b = 42; // c == 42

Beispiel Referenzen

code-cd871d23.tex.svg
code-ee5b7b38.tex.svg
code-6ec1d950.tex.svg
code-c523fb08.tex.svg
code-981c9b42.tex.svg
code-a6b74c46.tex.svg
code-2342280b.tex.svg

Referenzen

  • Referenzen müssen immer sofort initialisiert werden:
  • Eine Referenz kann nicht neu zugewiesen werden!
  • Bei einer Zuweisung wird der referenzierte Wert überschrieben!
int i1 = 5;
int i2 = 6;
int& ir = i1; // ir zeigt auf i1
ir = i2; // ir zeigt immer noch auf i1 // i1 == i2

Unterschiede zwischen Pointern & Referenzen

Pointer Referenzen
Initialisierung int* ip = &i; int& ir = i;
Zugriff lesend *ip ir
Zugriff schreibend *ip = 5; ir = 5;
Kann ins „Nichts“ zeigen Ja Nein
Kann neu zugewiesen werden Ja Nein

Call by Reference

  • Faustregel: In C++ sollten (wenn möglich) Referenzen statt Pointer verwendet werden.
  • Falls doch Pointer benötigt werden, sollte man auf Smart Pointer (siehe spätere Vorlesung) zurückgreifen.

Call by Reference

  • Erinnerung: Die Parameterübergabe an Funktionen erfolgt in C (und C++) immer by value.
  • Mit Pointern können wir jedoch call by reference simulieren.

Beispiel:

void swap( int* x, int* y) {
	int h = *x;
	*x = *y;
	*y = h;
}

int main( void ) {
	int i = 5;
	int j = 42;
	swap(&i, &j);// Achtung: Adressen auf i und j übergeben!
	printf( _"i = %i, j = %i\n"_ , i, j);
}

Return by Reference

Task: Ist folgendes Programm korrekt?

int* foo( void )
{
	int result;
	result = 42;
	return &result;
}

  • Syntaktisch ja, aber: Niemals Pointer auf lokale Variablen zurückgeben!
    • Lokale Variablen werden auf dem Stack angelegt.
    • Mit dem Ende der Funktion verlieren sie ihre Gültigkeit.
  • Der Speicher für result müsste stattdessen „von Hand“ bereitgestellt werden.

Arrays und Pointer

  • Pointer und Arrays sind in C++ eng verwandt.
  • Entscheidend: Werte von Arrays liegen garantiert zusammenhängend im Speicher.

Es gibt dabei zwei wesentliche Dinge zu beachten:

  • Eine Array-Variable verhält sich wie ein Pointer auf das erste Element des Arrays.
  • Auf einen Pointer kann – wie auf ein Array – mit dem []-Operator zugegriffen werden.
int array [5];
int* ptr = array; // ptr == &( array[0])
int i0 = ptr [0]; // i0 == array[0] == *ptr
int i3 = ptr [3]; // i3 == array[3] == *(ptr+3)
  • Allgemein gilt: ptr[i] == *(ptr+i)
  • Zuweisungen an Array-Variablen sind nicht erlaubt!

Beispiel Arrays und Pointer

Was macht die Funktion foo?

unsigned int foo( const char* s)
{
	const char* p = s;
	while ( *s ) ++s;
	return s - p;
}

int main( void )
{
	char test[] = "Hallo!";
	printf( "%i\n" , foo( test ));// gibt 6 aus
}

Arrays und Pointer

  • Arrays können in C++ nicht by value an Funktionen übergeben werden.
  • Stattdessen wird der Pointer auf das erste Element übergeben:
void foo( int* ip) {
	*ip = 23;
}

int main( void ) {
	int array[42];
	array[0] = 32;
	foo(array);
	printf( "%i\n" , array[0]); // gibt 23 aus
}
  • Dabei ist es unerheblich, welche Syntax verwendet wird:
void foo( int* ip); // ip ist ein Pointer
void foo( int ip[]); // ip ist auch hier ein Pointer
void foo( int ip[42]);// ip ist auch hier ein Pointer

Hinweis Aufgabe 2_3

Im jupyter-book sind verschiedene Aufgaben angegeben:

Jupyter-Book Link

Hierüber kann dann direkt auf dem Hub eine Umgebung gestartet werden, in der C++ interpretiert wird (wird die ersten Termine genutzt):

JupyterHub Link

Wiederholung: Pointer

  • Ein Zeiger (engl. Pointer) enthält die Adresse einer Variable im Speicher.
  • Die Speicheradresse einer Variable liefert der Adressoperator &.
  • Spezielle Adresse: NULL – „Zeiger ins Nichts“.
  • Die Variable zu einem Pointer liefert der Dereferenzierungsoperator *.
  • Der Typ eines Pointers auf eine Variable vom Typ T ist T*.
int a = 42;
int* b = &a; // Adressoperator angewendet auf a
*b = 12; // Dereferenzierungsoperator angewendet auf b; a == 12

Array-Variable und Pointer

  • Eine Array-Variable verhält sich wie ein Pointer, der auf das erste Element des Arrays zeigt.
  • Auf einen Pointer kann – wie auf ein Array – mit dem []-Operator zugegriffen werden. Dabei gilt: ptr[i] == *(ptr+i)
  • Übergibt man einen Array-Typ T[] als Parameter an eine Funktion, „verfällt“ der Typ zum entsprechenden Pointer T* (Array Decaying).

Referenzen

  • Der Umgang mit Pointern ist besonders am Anfang schwierig.
  • C++ führt mit Referenzen ein ähnliches Sprachmittel ein.
  • Wie Pointer sind Referenzen Variablen, die Verweise auf andere Variablen speichern.
  • Der Typ einer Referenz auf eine Variable vom Typ T ist T&.

Unterschied zu Pointern

  • Zur Initialisierung wird nicht der Adressoperator verwendet!
  • Zum Zugriff auf das gespeicherte Datum wird nicht der Dereferenzierungsoperator verwendet werden!
int a = 42;
int& b = a;
std::cout << b << std::endl; // gibt 42 aus
b = 5; // a == 5

Referenzen

  • Referenzen müssen immer sofort initialisiert werden:
  • Eine Referenz kann nicht neu zugewiesen werden!
  • Bei einer Zuweisung wird der referenzierte Wert überschrieben!
int i1 = 5;
int i2 = 6;
int& ir = i1; // ir zeigt auf i1
ir = i2; // ir zeigt immer noch auf i1 // i1 == i2

Unterschiede zwischen Pointern & Referenzen

Pointer Referenzen
Initialisierung int* ip = &i; int& ir = i;
Zugriff lesend *ip ir
Zugriff schreibend *ip = 5; ir = 5;
Kann ins „Nichts“ zeigen Ja Nein
Kann neu zugewiesen werden Ja Nein

Call by Reference

  • Faustregel: In C++ sollten (wenn möglich) Referenzen statt Pointer verwendet werden.
  • Falls doch Pointer benötigt werden, sollte man auf Smart Pointer (siehe spätere Vorlesung) zurückgreifen.

const bei Pointern und Referenzen

  • Das Schlüsselwort const sollte bei Pointern und Referenzen verwendet werden, wenn auf ein Argument nur lesend zugegriffen wird:
void copy(const int& source, int& destination) { 
  destination = source;
}

int main() {
  int i = 5; int j; copy(i, j);
}

Erinnerung: Bei Pointern hat das Schlüsselwort const mehrere Bedeutungen:

  • Der Wert, auf den der Pointer verweist, soll nicht verändert werden: int const* ip; oder const int* ip;
  • Der Pointer soll nicht verändert werden, d. h., der Pointer kann nicht neu zugewiesen werden: int* const ip;
  • Wert und Pointer sollen nicht verändert werden: const int* const ip;

C++ – new und delete

  • In C++ existieren die Operatoren new und delete.
  • new erwartet einen Typ sowie (optional) Argumente für den Konstruktor und gibt einen Pointer des angegebenen Typs zurück: Widget* w = new Widget{args};
  • delete ruft den Destruktor des angegeben Pointers auf und gibt den entsprechenden Speicherbereich frei: delete w;
  • Um in C++ Speicher für ein Array anzufordern, gibt es den Operator new[].
  • Entsprechend muss der Speicherbereich mit dem Operator delete[] freigegeben werden: Widget* widgets = new Widget[10]; ... delete[] widgets;

Achtung

Das Vermischen von new und delete[] sowie new[] und delete ist verboten!

References

Vahrenhold, Jan. 2022a. „Informatik I: Grundlagen der Programmierung, 1 Einführung“. Lecture Notes, University of Münster.
———. 2022b. „Informatik I: Grundlagen der Programmierung, 4 Zusammengesetzte Daten“. Lecture Notes, University of Münster.