Menu
Coddy logo textTech

Semantyka przenoszenia i r-wartości

Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 83 z 104.

W C++ każde wyrażenie jest albo l-wartością (ma trwałą tożsamość, można pobrać jej adres), albo r-wartością (jest tymczasowe i zaraz zostanie zniszczone). Zrozumienie tej różnicy pozwala korzystać z semantyki przenoszenia — wydajnej optymalizacji, która pozwala uniknąć niepotrzebnego kopiowania.

Referencja r-wartości, deklarowana za pomocą &&, wiąże się konkretnie z obiektami tymczasowymi. Dzięki temu możesz „przejmować” zasoby z obiektów, które i tak zaraz znikną:

#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";
    }
    
    // Konstruktor przenoszący – przejmuje zasoby
    Buffer(Buffer&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr;  // Pozostaw obiekt źródłowy w poprawnym stanie
        other.size = 0;
        std::cout << "Moved\n";
    }
    
    ~Buffer() { delete[] data; }
};

int main() {
    Buffer b1(1000);
    Buffer b2(std::move(b1));  // Wywołuje konstruktor przenoszący
}

Funkcja std::move tak naprawdę niczego nie przenosi — po prostu rzutuje l-wartość na referencję do r-wartości, sygnalizując, że chcesz zrezygnować z zasobów obiektu. Faktyczne przenoszenie odbywa się w konstruktorze przenoszącym lub operatorze przypisania przenoszącego.

Semantyka przenoszenia znacznie poprawia wydajność podczas pracy z obiektami wymagającymi dużych zasobów, takimi jak kontenery czy ciągi znaków. Zamiast głęboko kopiować megabajty danych, po prostu przenosisz własność wskaźnika — operację o stałym czasie, niezależnie od rozmiaru.

challenge icon

Wyzwanie

Łatwy

Stwórzmy klasę DataBuffer zarządzającą zasobami, która pokaże semantykę przenoszenia w działaniu. Zobaczysz, jak przenoszenie zasobów zamiast ich kopiowania może znacznie zwiększyć wydajność podczas przekazywania własności pamięci przydzielonej dynamicznie.

Rozmieścisz swój kod w trzech plikach:

  • DataBuffer.h: Zdefiniuj klasę DataBuffer, która zarządza dynamicznie przydzielaną tablicą liczb całkowitych.

    Klasa powinna mieć prywatne składowe przechowujące wskaźnik na dane (int*), rozmiar (size_t) oraz nazwę (std::string), aby ułatwić rozpoznawanie poszczególnych buforów podczas operacji.

    Zadeklaruj:

    • Konstruktor, który przyjmuje nazwę typu std::string i rozmiar typu size_t, przydziela tablicę i wyświetla: [name] constructed with size [size]
    • Konstruktor przenoszący, który przyjmuje referencję do wartości r-wartościowej, przejmuje zasoby i wyświetla: [name] moved from [source_name] (bufor, do którego przenoszone są zasoby, przejmuje nazwę źródła)
    • Destruktor, który wyświetla [name] destroyed (lub empty destroyed, jeśli bufor został przeniesiony)
    • Metodę getSize(), która zwraca bieżący rozmiar
    • Metodę getName(), która zwraca nazwę bufora

    Pamiętaj, aby oznaczyć konstruktor przenoszący jako noexcept i pozostawić obiekt źródłowy w prawidłowym, pustym stanie (nullptr, rozmiar 0, nazwa "empty").

  • DataBuffer.cpp: Zaimplementuj wszystkie metody zadeklarowane w pliku nagłówkowym. Gdy uruchomi się destruktor, usuwaj dane tylko wtedy, gdy wskaźnik nie jest pusty. Dołącz <iostream>, aby móc wyświetlać dane.
  • main.cpp: Wczytaj dwa dane wejściowe:
    1. Nazwę bufora (ciąg znaków)
    2. Rozmiar bufora (liczba całkowita)

    Utwórz obiekt DataBuffer o podanej nazwie i rozmiarze. Następnie utwórz drugi bufor, przenosząc zasoby z pierwszego za pomocą std::move(). Po przeniesieniu wyświetl stan obu buforów:

    • Original: [name] size=[size]
    • New: [name] size=[size]

    Dołącz <utility>, aby użyć std::move.

Na przykład dla danych wejściowych Alpha i 100:

Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyed

Dla danych wejściowych Buffer i 50:

Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyed

Zwróć uwagę, że konstruktor przenoszący przekazuje własność przydzielonej pamięci bez kopiowania danych. Oryginalny bufor pozostaje pusty, ale prawidłowy, a gdy oba bufory zostaną zniszczone pod koniec programu, pamięć usuwa tylko ten, który nadal jest jej właścicielem.

Spróbuj swoich sił

#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"

int main() {
    std::string name;
    int size;
    
    std::cin >> name;
    std::cin >> size;
    
    // TODO: Utwórz DataBuffer o podanej nazwie i rozmiarze
    
    // TODO: Utwórz drugi bufor, przenosząc dane z pierwszego za pomocą std::move()
    
    // TODO: Wypisz stan obu buforów:
    // Oryginalny: [name] size=[size]
    // Nowy: [name] size=[size]
    
    return 0;
}
quiz iconSprawdź się

Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.

Wszystkie lekcje w sekcji Programowanie obiektowe

Poćwicz samodzielnie: Kompilator C++ online