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.
mallocC 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 freeWichtig: C bietet keine automatische Speicherverwaltung.
free verwendet:malloc gehört auch immer ein free.free entstehen Speicherlecks.new und deletenew 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.
newnew 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. \(\Leftrightarrow\) delete kann leicht vergessen werden.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 vectors: 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::vectorstd::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 Wertestd::vector<float> vf{ 1.1f, 2.2f, 3.3f, 4.4f, 5.5f }; //5 float Werteint 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 Wertestd::vectorJede Funktion hat eine fest definierte Komplexität, z. B.:
[] oder at: \(O(1)\), vi[42] = 23; vi.at( 23 )= 42;vi.push_back( 42 );vi.insert( vi.begin(), 23 );vi.resize( 23 );std::vectorReicht für Funktionen wie push_back der bisher reservierte Speicher nicht aus, so rufen diese resize auf und die Komplexität wird \(O(n)\)!
resize beim Vergrößern alle Zeiger und Referenzen, die auf Elemente des Vektors verweisen.push_back vermeiden oder per reserve oder resize vorsorgen.std::vectorVorsicht 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?
mapKarten 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::mapstd::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::mapstd::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::mapstd::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.maps/ setsSowohl 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.constconst 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):