Move-Semantik & Rvalues
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy. Lektion 83 von 104.
In C++ ist jeder Ausdruck entweder ein lvalue (hat eine dauerhafte Identität, kann adressiert werden) oder ein rvalue (temporär und kurz davor, zerstört zu werden). Das Verständnis dieses Unterschieds ermöglicht Move-Semantik – eine leistungsstarke Optimierung, die unnötiges Kopieren vermeidet.
Eine rvalue-Referenz, deklariert mit &&, bindet sich speziell an temporäre Objekte. Dadurch kannst du Ressourcen von Objekten „stehlen“, die ohnehin gleich verschwinden:
#include <iostream>
#include <utility>
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : data(new int[s]), size(s) {
std::cout << "Constructed\n";
}
// Move-Konstruktor - stiehlt Ressourcen
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Hinterlässt die Quelle in einem gültigen Zustand
other.size = 0;
std::cout << "Moved\n";
}
~Buffer() { delete[] data; }
};
int main() {
Buffer b1(1000);
Buffer b2(std::move(b1)); // Löst den Move-Konstruktor aus
}Die Funktion std::move verschiebt tatsächlich nichts – sie wandelt lediglich einen Lvalue in eine Rvalue-Referenz um und signalisiert damit, dass du bereit bist, die Ressourcen des Objekts aufzugeben. Das tatsächliche Verschieben erfolgt im Move-Konstruktor oder Move-Zuweisungsoperator.
Move-Semantik verbessert die Leistung bei der Arbeit mit ressourcenintensiven Objekten wie Containern oder Zeichenketten erheblich. Anstatt Megabytes an Daten tief zu kopieren, überträgst du einfach den Besitz des Zeigers – eine Operation mit konstanter Laufzeit unabhängig von der Größe.
Aufgabe
EinfachErstellen wir eine ressourcenverwaltende DataBuffer-Klasse, die Move-Semantik in der Praxis demonstriert. Du wirst sehen, wie das Verschieben von Ressourcen statt ihrer Kopie die Effizienz beim Übertragen des Besitzes dynamisch reservierten Speichers erheblich verbessern kann.
Du organisierst deinen Code auf drei Dateien:
DataBuffer.h: Definiere deineDataBuffer-Klasse, die ein dynamisch reserviertes Array von Ganzzahlen verwaltet.Deine Klasse sollte private Member für den Datenzeiger (
int*), die Größe (size_t) und einen Namen (std::string) besitzen, damit während der Operationen nachvollzogen werden kann, welcher Puffer welcher ist.Deklariere Folgendes:
- Einen Konstruktor, der einen
std::string-Namen und einesize_t-Größe entgegennimmt, das Array reserviert und Folgendes ausgibt:[name] constructed with size [size] - Einen Move-Konstruktor, der eine Rvalue-Referenz entgegennimmt, die Ressourcen übernimmt und Folgendes ausgibt:
[name] moved from [source_name](wobei der Zielpuffer den Namen der Quelle übernimmt) - Einen Destruktor, der
[name] destroyedausgibt (oderempty destroyed, wenn der Puffer verschoben wurde) - Eine
getSize()-Methode, die die aktuelle Größe zurückgibt - Eine
getName()-Methode, die den Namen des Puffers zurückgibt
Denke daran, deinen Move-Konstruktor mit
noexceptzu kennzeichnen und das Quellobjekt in einem gültigen leeren Zustand zu belassen (nullptr, Größe 0, Name "empty").- Einen Konstruktor, der einen
DataBuffer.cpp: Implementiere alle in deinem Header deklarierten Methoden. Wenn der Destruktor ausgeführt wird, darf er die Daten nur löschen, wenn der Zeiger nicht null ist. Binde<iostream>für die Ausgabe ein.main.cpp: Lies zwei Eingaben ein:- Einen Namen für deinen Puffer (String)
- Eine Größe für deinen Puffer (Ganzzahl)
Erstelle einen
DataBuffermit dem angegebenen Namen und der angegebenen Größe. Erstelle anschließend einen zweiten Puffer, indem du den ersten mitstd::move()verschiebst. Gib nach dem Verschieben den Zustand beider Puffer aus:Original: [name] size=[size]New: [name] size=[size]
Binde
<utility>fürstd::moveein.
Zum Beispiel mit den Eingaben Alpha und 100:
Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyedMit den Eingaben Buffer und 50:
Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyedBeachte, wie der Move-Konstruktor den Besitz des reservierten Speichers überträgt, ohne Daten zu kopieren. Der ursprüngliche Puffer bleibt in einem leeren, aber gültigen Zustand, und wenn beide Puffer am Ende des Programms zerstört werden, löscht nur derjenige, der den Speicher noch besitzt, diesen tatsächlich.
Probier es selbst
#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"
int main() {
std::string name;
int size;
std::cin >> name;
std::cin >> size;
// TODO: Erstellen Sie einen DataBuffer mit dem angegebenen Namen und der Größe
// TODO: Erstellen Sie einen zweiten Puffer, indem Sie den ersten mit std::move() verschieben
// TODO: Geben Sie den Zustand beider Puffer aus:
// Original: [name] Größe=[size]
// Neu: [name] Größe=[size]
return 0;
}Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamespaces & GültigkeitsbereichEinführung in OOP mit C++Klassen vs. ObjekteDer „this“-ZeigerMethoden (Member-Funktionen)Attribute (Datenmember)Grundlagen zu Ctors & DtorsRückblick – Einfacher Taschenrechner4Klassenattribute
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen und -KlassenZusammenfassung – Bankkontoverwaltung7Vererbung
Grundlagen der VererbungZugriffsebenen bei der VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick – Mitarbeiterhierarchie2Speicherverwaltung
Stack- vs. Heap-SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Intelligente Zeiger in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsbezeichner in C++Zugriffsbezeichner im DetailInformationsverbergungStruct vs. KlasseVerschachtelte und innere KlassenWiederholung – Studierendenverwaltungssystem8Polymorphie
Polymorphie zur Compilezeit vs. LaufzeitFunktionsüberladungVirtuelle Funktionen erneut betrachtetRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIRückblick – Shape-Calculator3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorMove-KonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktor im DetailDrei-/Fünf-/Null-RegelZusammenfassung – String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladung der Operatoren [] und ()TypkonvertierungsoperatorenWiederholung – Matrix-Klasse9Templates
FunktionstemplatesKlassen-TemplatesTemplate-SpezialisierungVariadische TemplatesGrundlagen zu SFINAE und Type TraitsRückblick – Generischer Container12Moderne C++-Funktionen
Move-Semantik & RvaluesPerfektes ForwardingLambda-Ausdrücke im Detailstd::function & std::bindconstexpr und constevalStrukturierte Bindungenoptional, variant, anyÜbe selbstständig: Online-C++-Compiler