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.
Wyzwanie
ŁatwyZbudujmy 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 nazwiedata, przechowujący tekst, orazsize_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ślidatama wartość null) - Metodę
getLength(), która zwraca długość
- Prywatne składowe: wskaźnik
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
TextBuffero nazwieoriginalz wczytanym tekstem - Utwórz obiekt
copiedza pomocą konstruktora kopiującego, kopiując zoriginal - Utwórz obiekt
anotherz tekstem"Temporary" - Użyj przypisania kopiującego:
another = original - Utwórz obiekt
movedprzez skonstruowanie go z przeniesieniem zoriginalza 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 —
original,copied,moved,another,target— wypisz"original: <text> (length: <len>)"
- 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 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;
}
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