Przeciążanie operatora przypisania
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 44 z 104.
Operator przypisania kopiującego (=) jest wywoływany, gdy przypisujesz jeden istniejący obiekt do drugiego. W przeciwieństwie do konstruktora kopiującego (który tworzy nowy obiekt) operator przypisania działa na obiekcie, który już istnieje i może mieć zasoby wymagające wcześniejszego zwolnienia.
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s), data(new int[s]) {}
~Buffer() { delete[] data; }
Buffer& operator=(const Buffer& other) {
if (this != &other) { // 1. Sprawdzenie przypisania do samego siebie
delete[] data; // 2. Zwolnienie istniejącego zasobu
size = other.size; // 3. Skopiowanie danych
data = new int[size];
for (size_t i = 0; i < size; ++i)
data[i] = other.data[i];
}
return *this; // 4. Zwrócenie *this
}
};Sprawdzenie samoprzypisania (this != &other) ma kluczowe znaczenie. Bez niego b = b; usunęłoby dane przed próbą ich skopiowania, powodując niezdefiniowane zachowanie.
Operator zwraca *this przez referencję, aby umożliwić łączenie operacji, takie jak a = b = c;. Jest to zgodne z zachowaniem typów wbudowanych.
W przypadku klas zarządzających zasobami często implementuje się zarówno operatory przypisania przez kopiowanie, jak i przenoszenie:
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data; // Zwolnij bieżący zasób
data = other.data; // Przejmij zasób
size = other.size;
other.data = nullptr; // Pozostaw obiekt źródłowy w prawidłowym stanie
other.size = 0;
}
return *this;
}Operator przypisania przenoszącego przekazuje własność zamiast kopiować, dzięki czemu operacje takie jak vec[0] = createBuffer(); są znacznie wydajniejsze.
Wyzwanie
ŁatwyStwórzmy klasę DynamicString, która zarządza dynamicznie alokowaną tablicą znaków i prawidłowo obsługuje przypisywanie między istniejącymi obiektami. To praktyczny przykład sytuacji, w której trzeba bezpiecznie przenieść dane tekstowe z jednego obiektu do drugiego, nie powodując wycieków pamięci ani wiszących wskaźników.
Utworzysz dwa pliki, aby uporządkować swój kod:
DynamicString.h: Zdefiniuj klasęDynamicString, która przechowuje ciąg znaków w stylu C przy użyciu pamięci dynamicznej. Klasa powinna zarządzać:- Prywatnym wskaźnikiem
char*na dane tekstowe - Prywatną zmienną
size_tprzechowującą długość - Konstruktorem, który przyjmuje
const char*i tworzy głęboką kopię - Destruktorem, który prawidłowo zwalnia zaalokowaną pamięć
- Operatorem przypisania kopiującego, który wykonuje głęboką kopię z zabezpieczeniem przed samoprzypisaniem
- Operatorem przypisania przenoszącego, który sprawnie przenosi własność
- Metodą
c_str(), która zwraca wewnętrzny ciąg znaków (const) - Metodą
length(), która zwraca długość ciągu znaków (const)
Operator przypisania kopiującego musi wykonywać cztery podstawowe kroki: sprawdzić, czy nie zachodzi samoprzypisanie, zwolnić istniejące zasoby, skopiować nowe dane i zwrócić
*this. Operator przypisania przenoszącego powinien przejąć wskaźnik źródłowy i pozostawić obiekt źródłowy w prawidłowym, pustym stanie (nullptr, długość 0).- Prywatnym wskaźnikiem
main.cpp: Wczytaj trzy ciągi znaków ze standardowego wejścia (każdy w osobnym wierszu). Utwórz trzy obiektyDynamicString:str1z pierwszym wczytanym ciągiem,str2z drugim istr3z trzecim.Zademonstruj działanie operatorów przypisania:
- Wyświetl początkowy stan wszystkich trzech ciągów znaków
- Użyj przypisania kopiującego:
str1 = str2; - Wyświetl str1 i str2 po przypisaniu kopiującym (oba powinny mieć tę samą zawartość)
- Użyj przypisania przenoszącego:
str2 = std::move(str3); - Wyświetl str2 po przypisaniu przenoszącym oraz długość str3 (po przeniesieniu powinna wynosić 0)
Format wyjścia:
Initial: str1: <value> str2: <value> str3: <value> After copy (str1 = str2): str1: <value> str2: <value> After move (str2 = std::move(str3)): str2: <value> str3 length: 0
Użyj <cstring> dla funkcji strlen i strcpy. Oznacz operator przypisania przenoszącego jako noexcept. Dołącz <utility> w pliku main.cpp, aby użyć std::move. Nie zapomnij o strażnikach nagłówka w pliku nagłówkowym.
Spróbuj swoich sił
#include <iostream>
#include <string>
#include <utility>
#include "DynamicString.h"
using namespace std;
int main() {
// Wczytaj trzy ciągi znaków z wejścia
string input1, input2, input3;
getline(cin, input1);
getline(cin, input2);
getline(cin, input3);
// Utwórz trzy obiekty DynamicString
DynamicString str1(input1.c_str());
DynamicString str2(input2.c_str());
DynamicString str3(input3.c_str());
// TODO: Wypisz początkowy stan wszystkich trzech ciągów znaków
// Format:
// Initial:
// str1: <value>
// str2: <value>
// str3: <value>
// TODO: Użyj przypisania kopiującego: str1 = str2;
// TODO: Wypisz str1 i str2 po przypisaniu kopiującym
// Format:
// After copy (str1 = str2):
// str1: <value>
// str2: <value>
// TODO: Użyj przypisania przenoszącego: str2 = std::move(str3);
// TODO: Wypisz str2 po przeniesieniu oraz długość str3
// Format:
// After move (str2 = std::move(str3)):
// str2: <value>
// str3 length: 0
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