programowanieProgramista C++

Czym jest konstruktor kopiujący i operator przypisania? Jak unikać błędów podczas kopiowania obiektów z dynamiczną pamięcią?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

Konstruktor kopiujący oraz operator przypisania kopiującego są niezbędne do pracy z obiektami zarządzającymi zasobami (np. dynamiczną pamięcią). Domyślnie kompilator wygeneruje przypisanie kopiujące pole po polu, co jest niebezpieczne dla surowego wskaźnika:

class Buffer { public: Buffer(size_t size) { data = new int[size]; this->size = size; } ~Buffer() { delete[] data; } // Poprawna implementacja konstruktora kopiującego Buffer(const Buffer& other) : size(other.size) { data = new int[size]; std::copy(other.data, other.data + size, data); } // Poprawny operator przypisania Buffer& operator=(const Buffer& other) { if (this != &other) { delete[] data; size = other.size; data = new int[size]; std::copy(other.data, other.data + size, data); } return *this; } private: int* data; size_t size; };

W przeciwnym razie podczas kopiowania dwóch obiektów jeden zwalnia pamięć, a drugi pozostaje z wieszającym wskaźnikiem (double free lub use after free).

Pytanie z pułapką

Co się stanie, jeśli jawnie zadeklarujemy tylko konstruktor kopiujący, ale nie operator przypisania? Kiedy jest on potrzebny, a kiedy — nie?

Odpowiedź: Jeśli zadeklarowany jest tylko konstruktor kopiujący, ale nie zadeklarowany operator przypisania, kompilator wygeneruje operator przypisania domyślnie (kopiowanie bitowe), co jest niebezpieczne, jeśli obiekt zawiera dynamiczne zasoby, tzn. będzie niszczyć ten sam wskaźnik dwukrotnie.

W przypadku zarządzania pamięcią należy zaimplementować oba: zarówno konstruktor kopiujący, jak i operator przypisania, aby uniknąć błędów kopiowania/wycieków pamięci.

Przykłady rzeczywistych błędów z powodu nieznajomości szczegółów tematu


Historia

W serwerze multimedialnym przy kopiowaniu obiektu bufora użyto konstruktora kopiującego, ale zapomniano o operatorze przypisania. Kiedy jeden bufor przypisywano do drugiego, następowało podwójne zwolnienie pamięci podczas niszczenia obiektów. Błąd ujawnił się podczas testów obciążeniowych.


Historia

W projekcie logistyki transportowej operator przypisania domyślnie kopiował strukturę z wskaźnikiem na tablicę współrzędnych. Po usunięciu jednego obiektu drugi odwoływał się do zwolnionej pamięci, powodując błędy segmentacji.


Historia

W jednym z projektów z zasobami graficznymi zapomniano zaimplementować konstruktor kopiujący przy przekazywaniu obiektów między wątkami. Przekazywanie przez wartość prowadziło do shallow copy, a po zmianie obiektu w innym wątku dochodziło do uszkodzenia danych i zawieszenia programu.