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órykolwiek z tych trzech elementów, powinieneś zdefiniować wszystkie trzy:
- Destruktor
- Konstruktor kopiujący
- Operator przypisania kopiującego
Reguła pięciu rozszerza to podejście we współczesnym 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
};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 potrzeba destruktora ani funkcji kopiujących czy przenoszących!
};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.
Wyzwanie
ŁatwyStwórzmy klasę TextBuffer zgodną z Regułą Pięciu: zaimplementujemy wszystkie pięć specjalnych funkcji składowych, aby prawidłowo zarządzać dynamicznie przydzielanymi danymi znakowymi. Pokaże to, jak operacje kopiowania i przenoszenia współpracują ze sobą, tworząc solidną klasę zarządzającą zasobami.
Utworzysz 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 nazwiedataprzechowujący tekst orazsize_t lengthprzechowujący długość ciągu znaków (bez znaku terminującego null) - Konstruktor parametryzowany, który przyjmuje ciąg znaków C (
const char*), przydziela pamięć, kopiuje zawartość i wypisuje"TextBuffer created: <text>" - Destruktor, który zwalnia pamięć (jeśli nie jest null) i wypisuje
"TextBuffer destroyed" - Konstruktor kopiujący, który wykonuje kopię głęboką i wypisuje
"TextBuffer copied" - Operator przypisania kopiującego, który obsługuje przypisanie do samego siebie, zwalnia istniejące dane, wykonuje kopię głęboką i wypisuje
"TextBuffer copy-assigned". Zwróć*this - Konstruktor przenoszący (oznaczony jako
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 jako
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"", jeśli data jest null) - Metodę
getLength(), która zwraca długość
- Prywatne składowe: wskaźnik
main.cpp: Pokaż działanie wszystkich pięciu specjalnych funkcji składowych. Odczytaj ciąg tekstowy z wejścia, a następnie:- Utwórz obiekt
TextBuffero nazwieoriginalz tekstem wejściowym - Utwórz obiekt
copiedza pomocą konstruktora kopiującego, kopiując obiektoriginal - Utwórz obiekt
anotherz tekstem"Temporary" - Użyj przypisania kopiującego:
another = original - Utwórz obiekt
moved, używając konstruktora przenoszącego do przeniesienia obiektuoriginalza pomocąstd::move() - Utwórz obiekt
targetz tekstem"Target" - Użyj przypisania przenoszącego:
target = std::move(copied) - Wypisz
"--- Final State ---" - Dla każdego bufora wypisz
"original: <text> (length: <len>)": original, copied, moved, another, target
- Utwórz obiekt
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 działaniu: wszystkie pięć funkcji współpracuje, aby zapewnić 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>, aby korzystać z 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' za pomocą konstruktora przenoszącego, używając 'original' i 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 tym formacie:
// "original: <text> (length: <len>)"
// Wypisz dla: original, copied, moved, another, target
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generycznyPoćwicz samodzielnie: Kompilator C++ online