Prof. Dr. Malte Schilling
Autonomous Intelligent Systems Group
Eine IDE ist eine Software-Anwendung, die Software-Entwicklern eine umfassende Reihe von Tools zur Verfügung stellt, um die Softwareentwicklung zu erleichtern.
Sie integriert normalerweise einen Quellcode-Editor, Build-Automatisierungstools und einen Debugger.
Folien zu IDEs wurden durch ChatGPT zusammengestellt (und nachher angepasst), Kommunikation mit ChatGPT (4.0) am 20.5.2023.
&
.NULL
– „Zeiger ins Nichts“.*
.T
ist T*
.ptr[i] == *(ptr+i)
T[]
als Parameter an eine Funktion, „verfällt“ der Typ zum entsprechenden Pointer T*
(Array Decaying).T
ist T&
.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 |
Es gibt keine automatische Garbage Collection!
Zwei wichtige Begriffe bei der statischen und dynamischen Speicherverwaltung sind Stack und Heap (hiermit sind nicht die so benannten Datenstrukturen gemeint).
Die statische Speicherverwaltung verwaltet den Stack.
Mit der dynamischen Speicherverwaltung verwaltet der Programmierer den Heap.
Basis for Comparison | Stack | Heap |
---|---|---|
Basic | Memory is allocated LIFO | Allocated in random order. |
Allocation and Deallocation | Automatic | Manual |
Cost | Less | More |
Invoking | O(1) | O(N) |
Issue | Shortage of memory | Memory fragmentation |
Flexibility | Fixed size and is not flexible | Resizing is possible |
Access time | Faster | Slower |
Daher: Dynamische Speicherverwaltung → Programm kann zur Laufzeit Speicher reservieren und freigeben. Dies wird durch C++ unterstützt mit eingebauten Operatoren.
malloc
C bietet die Bibliotheksfunktion malloc
(stdlib.h
) zur Allokation von Speicher auf dem Heap:
void*
auf reservierten Speicherbereich (NULL
, falls kein Speicher reserviert werden konnte).malloc
2Erinnerung: Der Operator sizeof
liefert die Größe eines Datentyps bzw. des Datentyps eines Ausdrucks:
Im Vergleich dazu: In Java wird mit new
Speicher auf dem Heap alloziert.
Zu jedem malloc
gehört ein free
(im Beispiel oben ein free(ip);
) – siehe folgende Folien.
malloc
3malloc
und free
Wichtig: C bietet keine automatische Speicherverwaltung.
free
verwendet:malloc
gehört auch immer ein free
.free
entstehen Speicherlecks.new
und delete
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;
Zu jedem new
sollte ein delete
genutzt werden – siehe folgende Folien.
new
new T
(T
ist ein Typ) reserviert Speicher, damit ein Objekt vom Typ T
hineinpasstIm Gegensatz zu der malloc
-Variante, ruft new
immer auch einen Konstrukor auf. Entweder explizit speziellen Konstruktor oder implizit den Standard Konstruktor.
Der Zugriff auf die erstellten Objekte erfolgt mithilfe des Dereferenzierungsoperators *
(genau, wie bei Zeigern, auf Elemente von Arrays):
Zu jedem new
sollte ein delete
genutzt werden – siehe folgende Folien.
new
und delete
für Arraysnew[]
.delete[]
freigegeben werden: Widget* widgets = new Widget[10]; ... delete[] widgets;
Das Vermischen von new
und delete[]
sowie new[]
und delete
ist verboten!
new []
ruft immer für alle Elemente den Standard Konstruktor auf.new[]
und KonstruktorenBei Standarddatentypen (wie z.B. int
und char
) macht der Standardkonstruktor nichts!
new []
initialisierten Speicher mit 0
zu initialisieren:
Aufgabe zum expliziten erzeugen und nutzen der dynamischen Speicherverwaltung.
Im jupyter-book sind verschiedene Aufgaben angegeben:
Hierüber kann dann direkt auf dem Hub eine Umgebung gestartet werden, in der C++ interpretiert wird (wird die ersten Termine genutzt):
Dies hat mehrere Gründe:
new
muss genau ein entsprechendes delete
gehören.new
und delete
stehen aber im Quelltext oft weit außeinander. delete
aufgerufen werden muss. Wem gehört eigentlich die Ressource?delete
, wird Speicher nicht freigegeben. Man spricht von einem Speicherleck.delete
mehrmals für den selben Pointer aufgerufen, führt dies zu einem Laufzeitfehler.Das folgende Beispiel illustriert einige Probleme mit der manuellen Speicherverwaltung. Frage: Wird wp
korrekt freigegeben?
Nein! Es gibt Verzweigungen in dieser Funktion, welche die Funktion beenden, ohne delete
aufzurufen. Zusätzlich könnte bar()
eine Exception auslösen und die Funktion so in Zeile 7 verlassen werden.
template
kündigt in C++ ein Template an, hier eine Template-Funktion.typename
. Während die Funktionsargumente stellvertretend für Werte stehen, stehen die Template-Argumente für Datentypen (z. B. int
oder float
).T
wie ein normaler Datentyp verwendet werden.In der folgenden Aufgabe (später zu bearbeiten): Implementieren und testen Sie eine Template-Funktion printMaxReturnMin
welche zwei Argumente erwartet und das größere auf der Konsole ausgibt und das kleinere als Rückgabewert zurück gibt.
Ein Objekt, das uns erlaubt andere Objekte zu sammeln und mit ihnen auf spezielle Art und Weise zu interagieren.
Beispiele: Vektoren, Stapel oder Warteschlangen!
Was ist der Zweck von Containertypen für Programmierungsprachen?
Normalerweise bieten Container einige grundlegende Standard-Funktionen an.
Ein array
ist die primitive Form eines vector
s: Feste Größe in einer strengen Reihenfolge
Eine deque
ist eine double ended queue
Eine list
ist eine doppelt verkettete Liste = lann in beide Richtungen durchlaufen werden.
Wie funktioniert vector
eigentlich?
Auf einer hohen Ebene ist ein Vektor eine geordnete Sammlung von Elementen des gleichen Typs, die wachsen und schrumpfen kann.
_size
= Anzahl der Elemente des Vektors_capacity
= Platz, der für die Elemente reserviert wurdeAlle Container können prinzipiell alle Arten von Informationen enthalten! Wie wählen wir aus, welche wir sinnvollerweise verwenden sollten?
Im jupyter-book sind verschiedene Aufgaben angegeben – den letzten Teil müssen sie jedoch im Terminal ausführen.
Hierüber kann dann direkt auf dem Hub eine Umgebung gestartet werden – dies lädt das gitlab und sie können hier ein Terminal starten, in dem sie den Code kompilieren und ihr Programm dann ausführen können.
Aufgabe | std::vector |
std::deque |
std::list |
---|---|---|---|
Einfügen/entfernen Element vorne | langsam | schnell | schnell |
Einfügen/entfernen Element hinten | super schnell | sehr schnell | schnell |
Indizierter Zugriff | super schnell | schnell | nicht möglich |
Einfügen/entfernen Element in der Mitte | langsam | schnell | sehr schnell |
Speichernutzung | gering | hoch | hoch |
Verbinden (splicing, joining) | langsam | sehr langsam | schnell |
Stabilität (iterators, concurrency) | schlecht | sehr schlecht | gut |
std::vector
std::vector
ist der (mit Abstand) am meisten verwendete Container.std::vector
modelliert ein dynamisches Array von Werten desselben Typs, der durch einen Template-Parameter angegeben wird (z. B.: std::vector<int>
).std::vector<float> vf( 1024 ); //1024 float Werte
std::vector<float> vf{ 1.1f, 2.2f, 3.3f, 4.4f, 5.5f }; //5 float Werte
int
Wert initialisiert werden können: std::vector<int> vi1{ 1, 2, 3, 4, 5 }; //5 int Werte
std::vector<int> vi2{ 1024 }; // 1 int Wert (1024)
std::vector<int> vi3( 1024 ); //1024 int Werte
std::vector
Jede Funktion hat eine fest definierte Komplexität, z. B.:
[]
oder at
: vi[42] = 23; vi.at( 23 )= 42;
vi.push_back( 42 );
vi.insert( vi.begin(), 23 );
vi.resize( 23 );
std::vector
Reicht für Funktionen wie push_back
der bisher reservierte Speicher nicht aus, so rufen diese resize
auf und die Komplexität wird
resize
beim Vergrößern alle Zeiger und Referenzen, die auf Elemente des Vektors verweisen.push_back
vermeiden oder per reserve
oder resize
vorsorgen.std::vector
Vorsicht mit Zeigern und Referenzen auf Elemente eines Vektors!
#include <vector>
#include <iostream>
struct A{ int i = 24; };
int main ()
{
std::vector <A> v{};
v.push_back( A{} );
v[0].i = 24;
A* ap = &(v[0]); // ap ist ein A-Zeiger auf das erste Element in v
std::cout << ap->i << std::endl; // 24
for( int i = 0; i < 100; ++i )
v.push_back( A{} );
std::cout << ap->i << std::endl; // 17005616
}
Seit C++11 gibt es eine weitere Möglichkeit einen Container vollständig zu durchlaufen:
for
-Schleife nennt man range-based for loop.begin
und end
Funktionen implementiert, die Iteratoren zurückgeben.auto
zu verwenden: for( auto i : vi ){ std::cout << i << ""; }
Achtung: Die linke Schleife ist wirkungslos, da die Zuweisung an eine Kopie und nicht das Original geschieht. Stattdessen Referenzen (auto&
) verwenden!
std::vector<int> vi{1,2,3}; auto iterator = vi.begin(); int one = *iterator;
auto
ist hier besonders praktisch. Der Typ des Iterators aus dem letzten Beispiel ist std::vector<int>::iterator
.++
bewegen sich Iteratoren durch einen Container: iterator++; int two = *iterator; int three = *(++iterator);
container.begin()
verweist immer auf das erste Element;container.end()
verweist hinter das letzte Element – das Dereferenzieren von container.end() ist nicht erlaubt!std::vector
für fast alles verwendenstd::deque
nutzenstd::list
verwendenAlle Container können prinzipiell alle Arten von Informationen enthalten! Wie wählen wir aus, welche wir sinnvollerweise verwenden sollten?
map
Karten werden als pair
implementiert: std::pair<const key, value>
const
! Schlüssel müssen unveränderlich sein.myMap[key]
) durchsucht die zugrunde liegende Sammlung von Paaren nach dem ersten Auftreten des Schlüsselattributs und gibt dessen zweites Attribut zurück.std::map
std::map
speichert Werte anhand eines Schlüssels ab: std::map<std::string, std::string> m; m["key"] = "value";
std::map
mit zwei Template-Parametern spezifiziert, dem Datentyp des Schlüssels (Key) und des Wertes (Value): std::map<KeyDatatype, ValueDatatype> m;
std::map
std::map<std::string, unsigned int> m; m["test"] = 4;
[]
-Operators können sowohl Werte aus der Map gelesen als auch in die Map geschrieben werden: unsigned int length_of_test = m["test"]; m["c++"] = 3;
Ist zu einem Schlüssel kein Wert vorhanden, so wird ein neuer Schlüssel angelegt!
`unsigned int length_of_unknown = m[“unknown”]; //return 0
std::map
– Zugriff auf Schlüsselfind
verwendet: auto pos = m.find("not_in_map"); if(pos == m.end()){...}
erase
können Schlüssel-Wert-Paare aus der Map entfernt werden: m.erase("test");
std::map
std::vector
hat jede Funktion eine definierte Komplexität, z. B. :
m["key"] = "value"; auto v = m["key"];
auto pos = m.find("needle");
m.erase(m.begin()); /*O(1)*/ m.erase("to_be_erased")/*O(log n)*/
<
überladen.Auch eine Map kann vollständig durchlaufen werden:
p.first
greift man auf den Schlüssel zu.p.second
greift man auf den Wert zu.first
und second
sind keine Funktionen, sondern einfache public
Attribute.map
s/ set
sSowohl Maps als auch Sets in der STL haben eine ungeordnete Version!
Es gibt viele Ähnlichkeiten zwischen Maps/Sets. Allgemeine Tipps zur Wahl:
Category | Container | After insertion, are… | After erasure, are… | Conditionally | ||
---|---|---|---|---|---|---|
iterators valid? | references valid? | iterators valid? | references valid? | |||
Sequence containers | array | N/A | N/A | |||
vector | No | N/A | Insertion changed capacity | |||
Yes | Yes |
Before modified element(s) (for insertion only if capacity didn’t change) |
||||
No | No | At or after modified element(s) | ||||
deque | No | Yes | Yes, except erased element(s) | Modified first or last element | ||
No | No | Modified middle only | ||||
list | Yes | Yes, except erased element(s) | ||||
forward_list | Yes | Yes, except erased element(s) | ||||
Associative containers |
set multiset map multimap |
Yes | Yes, except erased element(s) | |||
Unordered associative containers |
unordered_set unordered_multiset unordered_map unordered_multimap |
No | Yes | N/A | Insertion caused rehash | |
Yes | Yes, except erased element(s) | No rehash |
nach (Kölling und Rosenberg 2001), siehe (Vahrenhold 2022)
aus Definition 8.2 (Vahrenhold 2022), basierend auf (Echtle und Goedicke 2000):
“Ein (korrekt modelliertes) Objekt modelliert ein gedanklich abgegrenztes Gebilde mit allen seinen Eigenschaften und Verhaltensweisen.”
class
eingeleitet.this
ist ein Pointer auf das instanziierte Objekt und in jeder Klasse vorhanden..h
oder .hpp
) und die Definitionen in einer Source-Datei (.cpp
).Bei größeren Projekten trennt man meist Deklarationen und Definitionen in - Header-Dateien und - Implementierungs-Dateien
→ Erlaubt separate Kompilierung/Verwendung von Prebuilt-Libraries.
Beim Kompilieren müssen alle Implementierungsdateien angegeben werden: gcc -Wall -o foo max.c foo.c
Die Aufeilung in Header und Implementierung ist nicht immer einfach. Generell gilt:
public
, private
oder protected
deklariert werden.public
Member zugegriffen werden.private
Member können nur Objekte der Klasse zugreifen.protected
Member können auch Objekte abgeleiteter Klassen zugreifen.private
.class
auch struct
verwendet werden. Dann ist die Standardsichtbarkeit public.const
const
kann bei Member-Funktionen angegeben werden.const
sind (z. B. die Referenz wr
), können nur konstante Member-Funktionen ausführen.delete
auf dem Objekt, Objekt ist statisch und Programm terminiert).Initialisiere alle member objects in der member initialization list.
public
-Vererbung vs. private
-Vererbung.public
-Vererbung in C++.Bei einfacher Vererbung besitzt eine Klasse genau eine Oberklasse.
Derived
wird mit : public Base
die Vererbung angegeben.Derived
kann explizit ein Konstruktor von Base
aufgerufen werden (ansonsten automatisch der Standardkonstruktor von Base
).protected
- und public
-Attribute einer Basisklasse behalten ihre Sichtbarkeit in der abgeleiteten Klasse.customer.cpp
an.customer.cpp
gemäß dem UML-Diagramm.deposit
, withdraw
und transfer
.customer.cpp
eine main
-Funktion und testen Sie die Klasse Account.Hinweis: Kompilieren Sie Ihr Programm mit g++ -std=c++20 -Wall -Wextra -o customer customer.cpp
Person
mit einem Namen und einem Alter.Customer
, die von Person
erbt und dazu einen Account
und eine ID (z. B. unsigned int
) besitzt.print
hinzu, die den Zustand des Objekts (Werte aller Attribute) ausgibt.virtual
!int main() {
std::cout << std::boolalpha;
Account a{ 25 };
a.deposit( 75 );
a.deposit( 50 );
std::cout << (a.balance() == 150) << std::endl; // true
std::cout << a.withdraw( 25 ) << std::endl; // 25
std::cout << (a.balance() == 125) << std::endl; // true
std::cout << a.withdraw( 150 ) << std::endl; // 0
Account b{ a };
std::cout << a.transfer( 50, a) << std::endl; // false
std::cout << a.transfer( 126, b ) << std::endl; // false
std::cout << a.transfer( 125, b ) << std::endl; // true
std::cout << (a.balance() == 0) << std::endl; // true
std::cout << (b.balance() == 250) << std::endl; // true
}
Aufgabe zum Anlegen einer Klasse und von Beispielinstanzen
Im jupyter-book ist die Aufgaben angegeben:
Hierüber kann dann direkt auf dem Hub eine Umgebung gestartet werden, in der C++ interpretiert wird (wird die ersten Termine genutzt):