Menu
CoddyTech

Zasada trzech / pięciu / zera

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

Gdy twoja klasa zarządza zasobami, takimi jak pamięć dynamiczna, wiesz już, że potrzebujesz własnego destruktora, konstruktora kopiującego i konstruktora przenoszącego. Istnieje jednak zasada, która pomaga zdecydować, które specjalne funkcje składowe zaimplementować: reguła trzech, pięciu i zera.

Reguła trzech mówi: jeśli zdefiniujesz którąkolwiek z tych trzech rzeczy, powinieneś zdefiniować wszystkie trzy:

  • Destruktor
  • Konstruktor kopiujący
  • Operator przypisania kopiującego

Reguła pięciu rozszerza to w nowoczesnym C++, dodając operacje przenoszenia:

  • Destruktor
  • Konstruktor kopiujący
  • Operator przypisania kopiującego
  • Konstruktor przenoszący
  • Operator przypisania przenoszącego
class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}
    
    ~Buffer() { delete[] data; }                          // 1. Destruktor
    Buffer(const Buffer& other);                          // 2. Konstruktor kopiujący
    Buffer& operator=(const Buffer& other);               // 3. Przypisanie kopiujące
    Buffer(Buffer&& other) noexcept;                      // 4. Konstruktor przenoszący
    Buffer& operator=(Buffer&& other) noexcept;           // 5. Przypisanie przenoszące
};

Oba operatory przypisania działają według tego samego schematu: zabezpieczają przed przypisaniem do samego siebie, zwalniają stary zasób, przejmują nowy i zwracają *this:

Buffer& Buffer::operator=(const Buffer& other) {   // przypisanie kopiujące
    if (this != &other) {
        delete[] data;
        size = other.size;
        data = new int[size];
        std::copy(other.data, other.data + size, data);
    }
    return *this;
}

Buffer& Buffer::operator=(Buffer&& other) noexcept {   // przypisanie przenoszące
    if (this != &other) {
        delete[] data;
        data = other.data;
        size = other.size;
        other.data = nullptr;
        other.size = 0;
    }
    return *this;
}

Reguła zera to najprostsze podejście: jeśli Twoja klasa nie zarządza bezpośrednio zasobami, nie definiuj żadnej z tych funkcji. Pozwól kompilatorowi je wygenerować albo użyj inteligentnych wskaźników i standardowych kontenerów, które zajmą się zasobami za Ciebie.

class Player {
    std::string name;              // std::string samodzielnie zarządza swoją pamięcią
    std::vector<int> scores;       // std::vector samodzielnie zarządza swoimi zasobami
public:
    Player(std::string n) : name(n) {}
    // Nie są potrzebne funkcje destruktora, kopiowania ani przenoszenia!
};

Przestrzeganie tych zasad zapobiega błędom, takim jak podwójne usuwanie, wycieki pamięci i wiszące wskaźniki, które występują, gdy niektóre operacje są zdefiniowane, a innych brakuje.

challenge icon

Wyzwanie

Łatwy

Zbudujmy klasę TextBuffer, która stosuje regułę pięciu: implementuje wszystkie pięć specjalnych funkcji składowych, aby prawidłowo zarządzać dynamicznie przydzielanymi danymi znakowymi. Pokaże to, jak operacje kopiowania i przenoszenia współdziałają ze sobą, tworząc solidną klasę zarządzającą zasobami.

Utwórz dwa pliki, aby uporządkować swój kod:

  • TextBuffer.h: Zdefiniuj klasę TextBuffer, która przechowuje tekst w dynamicznie przydzielanej tablicy znaków. Twoja klasa powinna zawierać:
    • Prywatne składowe: wskaźnik char* o nazwie data, przechowujący tekst, oraz size_t length, określający długość ciągu znaków (bez znaku terminującego null)
    • Konstruktor parametryzowany, który przyjmuje ciąg znaków w stylu C (const char*), przydziela pamięć, kopiuje zawartość i wypisuje "TextBuffer created: <text>"
    • Destruktor, który zwalnia pamięć (jeśli wskaźnik nie jest równy null) i wypisuje "TextBuffer destroyed"
    • Konstruktor kopiujący, który wykonuje głęboką kopię i wypisuje "TextBuffer copied"
    • Operator przypisania kopiującego, który obsługuje przypisanie do samego siebie, zwalnia istniejące dane, wykonuje głęboką kopię i wypisuje "TextBuffer copy-assigned". Zwróć *this
    • Konstruktor przenoszący (oznaczony noexcept), który przenosi własność i wypisuje "TextBuffer moved". Pozostaw obiekt źródłowy w prawidłowym, pustym stanie
    • Operator przypisania przenoszącego (oznaczony noexcept), który obsługuje przypisanie do samego siebie, zwalnia istniejące dane, przenosi własność i wypisuje "TextBuffer move-assigned". Zwróć *this
    • Metodę getText(), która zwraca przechowywany tekst (zwróć pusty ciąg znaków "", jeśli data ma wartość null)
    • Metodę getLength(), która zwraca długość
  • main.cpp: Zademonstruj działanie wszystkich pięciu specjalnych funkcji składowych. Wczytaj ciąg tekstowy ze standardowego wejścia, a następnie:
    • Utwórz obiekt TextBuffer o nazwie original z wczytanym tekstem
    • Utwórz obiekt copied za pomocą konstruktora kopiującego, kopiując z original
    • Utwórz obiekt another z tekstem "Temporary"
    • Użyj przypisania kopiującego: another = original
    • Utwórz obiekt moved przez skonstruowanie go z przeniesieniem z original za pomocą std::move()
    • Utwórz obiekt target z tekstem "Target"
    • Użyj przypisania przenoszącego: target = std::move(copied)
    • Wypisz "--- Final State ---"
    • Dla każdego bufora — original, copied, moved, another, target — wypisz "original: <text> (length: <len>)"

Po przeniesieniu obiekty źródłowe (original i copied) powinny zawierać pusty tekst o długości 0, a obiekty docelowe — przeniesione dane. To pokazuje regułę pięciu w praktyce: wszystkie pięć funkcji współdziała ze sobą, zapewniając bezpieczne zarządzanie zasobami.

Dołącz <cstring>, aby korzystać z funkcji do obsługi ciągów znaków, takich jak strlen i strcpy, oraz <utility> dla std::move().

Spróbuj swoich sił

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

using namespace std;

int main() {
    string input;
    getline(cin, input);

    // TODO: Utwórz obiekt TextBuffer o nazwie 'original' z tekstem wejściowym

    // TODO: Utwórz 'copied' za pomocą konstruktora kopiującego, używając 'original'

    // TODO: Utwórz 'another' z tekstem "Temporary"

    // TODO: Użyj przypisania kopiującego: another = original

    // TODO: Utwórz 'moved' przez przeniesienie z 'original' za pomocą std::move()

    // TODO: Utwórz 'target' z tekstem "Target"

    // TODO: Użyj przypisania przenoszącego: target = std::move(copied)

    // TODO: Wypisz "--- Final State ---"

    // TODO: Wypisz stan każdego bufora w następującym formacie:
    // "original: <text> (length: <len>)"
    // Wypisz dla: original, copied, moved, another, target

    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