Klasy - zarządzanie zasobami¶
Konstruktor klasy jest odpowiedzialny za konstrukcję obiektu w prawidłowym stanie. Czasami aby utworzyć poprawnie działający obiekt wymagane jest pozyskanie zasobów (np. pamięć ze sterty, otwarcie pliku, otwarcie socketu, itp.).
class Vector
{
private:
size_t size_;
double* items_;
public:
Vector(size_t size, double value);
};
Vector::Vector(size_t size, double value) : size_{size}, items_{ new double[size_] }
{}
Destruktor klasy¶
Jeśli obiekt klasy pozyskuje zasób, to po wykorzystaniu go zasób ten powinien zostać zwolniony. W rezultacie niektóre klasy potrzebują funkcji, która będzie wywołana w momencie niszczenia obiektu i będzie odpowiedzialna za zwolnienie wykorzystywanych zasobów. Taką rolę pełni destruktor klasy.
Destruktory są dla obiektów niejawnie w momencie gdy obiekt ma zostać zniszczony:
dla zmiennych automatycznych gdy opuszczany jest zakres, w którym zostały zadeklarowane
dla zmiennych dynamicznych, gdy zwalniana jest pamięć za pomocą operatorów
delete
.
Destruktor może zostać również wywołany jawnie.
Nazwa destruktora – nazwa klasy poprzedzona tyldą: np. ~Vector()
.
class Vector
{
private:
size_t size_;
double* items_;
public:
Vector(size_t size, double value);
~Vector();
};
Vector::Vector(size_t size, double value) : size_{size}, items_{ new double[size_] }
{}
Vector::~Vector()
{
delete[] items_;
}
Technika, która wiąże się z pozyskaniem zasobu w konstruktorze i zwolnieniem go w destruktorze jest nazywana RAII - Resource Acquisition Is Initialization.
Kopiowanie¶
Obiekty są domyślnie kopiowalne. Domyślna operacja kopiowania jest definiowana przez kompilator i jest wartościowo realizowana pole po polu.
class BankAccount
{
public:
BankAccount(double balance, long number, const std::string& owner);
private:
double balance_;
long number_;
string owner_;
};
BankAccount::BankAccount(double balance, long number, const std::string& owner)
: balance{balance}, number_{n}, owner_{o}
{
}
// ...
int main()
{
BankAccount account1{1, 1000.0, "Jan Kowalski"};
BankAccount copy_of_acc = account1; // kopiowanie (konstruktor kopiujący)
BankAccount other_copy_of_acc(account1); // kopiowanie (konstruktor kopiujący)
BankAccount another_copy_of_acc{account1}; // kopiowanie (konstruktor kopiujący)
BankAccount account2{2, 3000.0, "Adam Nowak"};
account1 = account2; // kopiowanie (przypisanie kopiujace);
}
Jeśli klasa pozyskuje zasób i odwołuje się do niego przy pomocy wskaźnika (resource handle) operacja kopiowania wartościowego doprowadzi do współdzielenia zasobu. Wywołanie destruktorów obiektów współdzielących zasób doprowadzi do próby wielokrotnego zwolnienia zasobu (undefined behaviour). Kopiowanie takich obiektów powinno zostać zaimplementowane przez twórcę klasy za pomocą specjalnych operacji kopiujących:
konstruktora kopiującego -
T(const T&)
kopiującego operatora przypisania -
T& operator=(const T&)
class Vector
{
private:
size_t size_;
double* items_;
public:
Vector(size_t size, double value);
~Vector();
Vector(const Vector& source); // konstruktor kopiujący
Vector& operator=(const Vector& source); // kopiujący operator przypisania
//...
};
Konstruktor kopiujący¶
Poprawna implementacja konstruktora kopiującego dla klasy Vector
powinna zaalokować odpowiednio duży blok pamięci, a następnie skopiować wszystkie elementy tablicy z obiektu źródłowego:
Vector::Vector(const Vector& source)
: size_{source.size_}, items_{new double[source.size_]}
{
for(size_t i = 0; i < size_; ++i)
items_[i] = source.items_[i];
}
Kopiujący operator przypisania¶
Równolegle z implementacją konstruktora powinien zostać zaimplementowany kopiujący operator przypisania:
Vector& Vector::operator=(const Vector& source)
{
if (&source != this)
{
double* temp = new double[source.size_];
for(size_t i = 0; i != source.size_; ++i)
items_[i] = source.items_[i];
delete[] items_; // zwolnienie pamięci po starej tablicy
items_ = temp;
size_ = source.size_;
}
return *this;
}
Przenoszenie¶
Kopiowanie może być kosztowne zwłaszcza w kontekście tworzenia obiektów tymczasowych lub zwracania obiektów w funkcji.
Vector operator+(const Vector& a, const Vector& b)
{
if (a.size() != b.size())
throw VectorSizeMismatch();
Vector result(a.size());
for(size_t i = 0; i != a.size(); ++i)
result[i] = a[i] + b[i];
return result;
}
// ...
void f(const Vector& x, const Vector& y, const Vector& z)
{
Vector result;
// ...
result = x + y + z;
// ...
}
W przypadku tworzenia obiektów tymczasowych nie chcemy ich kopiowania. Zamiast tego możemy dokonać transferu ich stanu wewnętrznego do obiektu docelowego. Jest możliwe z wykorzystaniem specjalnych operacji przenoszących (move operations):
konstruktora przenoszącego -
T(T&&)
przenoszącego operatora przypisania -
T& operator=(T&&)
Obie funkcje specjalne przyjmują jako argument r-value referenję (&&
). Ten typ referencji może być wiązany tylko z obiektami r-value. W uproszczeniu obiekt r-value to obiekt tymczasowy, z którego bezpiecznie możemy pobrać zawartość pozostawiając go w stanie nieokreślonym, ale bezpiecznym do zniszczania.
class Vector
{
// ...
Vector(const Vector& source); // konstruktor kopiujący
Vector& operator=(const Vector& source); // kopiujący operator przypisania
Vector(Vector&& source); // konstruktor przenoszący
Vector& operator=(Vector&& source); // przenoszący operator przypisania
//...
};
Konstruktor przenoszący¶
Vector::Vector(Vector&& source)
: size_{source.size_}, items_{source.items_}
{
source.size_ = 0;
source.items_ = nullptr;
}
Przenoszący operator przypisania¶
Vector& Vector::operator=(Vector&& source)
{
if (&source != this)
{
size_ = source.size_;
items_ = source.items_;
source.size_ = 0;
source.items_ = nullptr;
}
return *this;
}
Specjalne funkcje składowe klas¶
Specjalne funkcje składowe klas w C++11:
Konstruktor domyślny
Destruktor
Operacje kopiowania - konstruktor kopiujący i kopiujący
operator=
Operacje przenoszenia - konstruktor przenoszący i przenoszący
operator=
Wszystkie specjalne funkcje składowe są generowane przez kompilator i posiadają następujące cechy:
są publiczne
są inline
są non-explicit
C++11 daje możliwość jawnego zadeklarowania funkcji specjalnych jako domyślnych lub usunięcia ich z interfejsu klasy.
Domyślne specjalne funkcje składowe - default
¶
Deklaracja default
- wymusza na kompilatorze generację domyślnej implementacji dla deklaracji
specyfikowanej przez użytkownika (np. generacja domyślnego konsktruktora w przypadku, gdy istnieją inne konstruktory przyjmujące parametry)
class Gadget
{
public:
Gadget(const Gadget&); // copy constructor will prevent
// generating implicitly declared
// default ctoe and move operations
Gadget() = default;
Gadget(Gadget&&) noexcept = default;
};
Operacje zadeklarowane jako default
są traktowane jako user-declared. W efekcie klasa:
class Any // default copy semantics enabled
{
};
nie jest taka sama jak klasa zaimplementowana w poniższy sposób:
class Any // default copy semantics deprecated in C++14 (and later probably disabled)
{
~Any = default;
};
Usunięte funkcje składowe - delete
¶
Deklaracja delete
- usuwa wskazaną funkcję lub funkcję składową z interfejsu klasy. Nie jest generowany kod takiej funkcji, a wywołanie jej, pobranie adresu lub użycie w wyrażeniu z sizeof
jest błędem kompilacji.
// prevents object from making copies and from move operations
class NoCopyable
{
protected:
NoCopyable() = default;
public:
NoCopyable(const NoCopyable&) = delete;
NoCopyable& operator=(const NoCopyable&) = delete;
};
// The same efect as NoCopyable
class NoMoveable
{
NoMoveable(NoMoveable&&) = delete;
NoMoveable& operator=(NoMoveable&&) = delete;
};
Usunięcie funkcji umożliwia uniknięcie niejawnej konwersji argumentów wywołania funkcji:
void integral_only(int a)
{
cout << "integral_only: " << a << endl;
}
void integral_only(double d) = delete;
// ...
integral_only(10); // OK
short s = 3;
integral_only(s); // OK - implicit conversion to short
integral_only(3.0); // error - use of deleted function