Haladó memóriakezelés
Czirkos Zoltán · 2022.06.21.
Placement new, osztályok saját operator new függvényei, allokátorok.
Ez a labor néhány haladó memóriakezelési technikát mutat be.
Feltöltés
A laborhoz kiírt beadandó a szokásos helyen van, az admin portálon. Ide óra végén töltsd fel a forráskódokat (*.cpp, *.h)! A feladatokat ezért külön projektben oldd majd meg, ne írd felül a megoldásokat.
Labor otthoni munkában
A labor teljesítéséhez mind a három feladatot meg kell oldani.
Emlékezz vissza az előadásra a globális operator new
függvénnyel
kapcsolatban! A C++ szabványos könyvtár biztosít két függvényt a dinamikus memória
foglalásához (allocation) és felszabadításához (deallocation):
#include <new>
void * operator new(size_t);
void operator delete(void *) noexcept;
Ezek a C-s malloc()
és free()
függvénnyel teljesen analóg módon használhatóak. Az operator
new
megkapja bájtokban, mekkora memóriaterületet kell foglalnia, és a foglalt területre mutató pointerrel tér vissza. Az
operator delete
az így kapott pointer kapja, és felszabadítja a memóriaterületet. Objektumok konstruktorát és
destruktorát ezek nem futtatják, hanem csak a memória kezeléséért felelnek.
Ezek a függvények a felhasználó által definiálhatóak, és így a rendszer által adott dinamikus memóriakezelő lecserélhető.
- Próbáld ezt ki! Definiáld a fenti függvényeket úgy, hogy a C-s könyvtári
malloc()
ésfree()
függvényeket használják! - Írj mindegyik függvénybe egy kiírást a működés megfigyeléséhez, pl.
„allocated 45 bytes at 0x01203567” és „freed memory at 0x01203567”. A
void *
pointerekhez vanoperator<<
overload. - Foglalj egy
int
-et:new int
. Szabadítsd fel: mit látsz? - Hozz létre egy
std::string
objektumot:std::string h = "helló világ alma körte barack";
. (Fontos, hogy hosszú szöveg legyen benne, mert egyesstd::string
implementációk a rövid szövegeknél el tudják kerülni a dinamikus memóriakezelést.) Mit látsz? És akkor, hastd::string *h = new std::string("helló világ alma körte barack")
módon foglalod? Kipróbálhatsz más osztályt is, pl.std::set{1, 2, 3, 4, 5}
. - Valósíts meg egy egyszerű dinamikus memóriakezelést ellenőrző keretrendszert
a függvények által! Tartsd nyilván egy közös (globális)
int
változóban, hány darab memóriaterület van épp lefoglalva! Acstdlib
fejlécfájlatexit()
függvényének átadhatsz egy paraméter és visszatérési érték nélküli,void()
függvényt, amelyet a programból kilépéskor, azaz amain()
-ből visszatérés után meghív. Adj ennek egy olyan függvényt, amely kiírja, hány darab felszabadítatlan memóriaterület maradt! Teszteld, hogy mutatja-e a felszabadítatlan területeket! - A
new
operátornakstd::bad_alloc
típusú kivételt kell dobnia, ha a foglalás nem sikerült. Ellenőrizd ezért amalloc()
által adott pointert, hogy nem null értékű-e! Dobj hibát, ha ez null, vagy ha 1 megabájtnál nagyobb területet próbál foglalni a hívó!
Megoldás
#include <iostream>
#include <cstdlib>
#include <string>
#include <set>
#include <stdexcept>
#include <new>
class MAllocator {
private:
static unsigned allocated_count;
public:
static void * alloc(size_t size) {
if (size > 1<<20) {
std::cerr << "Can't allocate " << size << " bytes" << std::endl;
throw std::bad_alloc{};
}
++allocated_count;
void *mem = malloc(size);
if (mem == nullptr) {
std::cerr << "malloc() returned nullptr for " << size << " bytes" << std::endl;
throw std::bad_alloc{};
}
std::cerr << "Allocated " << size << " bytes at " << mem << std::endl;
return mem;
}
static void dealloc(void *mem) noexcept {
--allocated_count;
std::cerr << "Freed mem at " << mem << std::endl;
free(mem);
}
static void allocated_report_atexit() {
if (allocated_count != 0) {
std::cerr << "Memory leak: " << allocated_count << " chunk(s) still allocated" << std::endl;
}
}
static void init() {
atexit(allocated_report_atexit);
}
};
unsigned MAllocator::allocated_count = 0;
void * operator new(size_t size) {
return MAllocator::alloc(size);
}
void operator delete(void *mem) noexcept {
return MAllocator::dealloc(mem);
}
int main() {
MAllocator::init();
delete new int;
std::cout << "=====\n";
{
std::string s = "hello vilag alma korte barack szilva dinnye";
}
std::cout << "=====\n";
delete new std::string("hello vilag alma korte barack szilva dinnye");
std::cout << "=====\n";
delete new std::set<int>{1, 2, 3, 4, 5, 6, 7};
std::cout << "=====\n";
try {
new int[123456789];
} catch (std::bad_alloc &b) {
std::cout << "Caught std::bad_alloc" << std::endl;
}
std::cout << "=====\n";
new int;
std::cout << "=====\n";
}
Implementációs részletek
Előfordulhat, hogy a fordító figyelmeztetést ad, miszerint saját operator delete(void*)
implementáció esetén operator delete(void*, size_t)
függvényt is kell írni.
Az operator delete(void *, size_t)
változatról előadáson külön nem esett szó. Ezt használhatja a fordító
akkor, ha tudja valahonnan felszabadításkor a terület méretét is (ami nyilvánvalóan megegyezik a foglalt mérettel). Lényeg, hogy
a kettőt együtt kell megvalósítani. Ha szeretnél később elmélyedni a témában, az operator delete(void*, size_t) függvényről szóló írásban olvashatsz erről.
Hasonlóképp, létezik külön operator new[]
és operator delete[]
is. Ha tömbök foglalásakor
és felszabadításakor nem látod, hogy lenne kimenete a programodnak, írd meg ezeket is – akár a nem tömbös változatokra
visszavezetve.
Emlékezz vissza az előadáson bemutatott „placement new” szintaxisra! Ez arra jó, hogy egy már lefoglalt, de még memóriaszemetet tartalmazó memóriaterületre „ráhívjunk” egy konstruktort, hogy ott egy objektum keletkezzen. Így lehet objektumot létrehozni memóriafoglalás nélkül, de természetesen a helyet nekünk kell biztosítanunk. Az így létrehozott objektum destruktorának hívásáért is a programozó felel, de az működik szokványos tagfüggvényként is:
class Foo { /* ... */ };
unsigned char mem[sizeof(Foo)];
Foo *f = new (mem) Foo{}; // ctor
f->do_something();
f->~Foo(); // dtor
A feladat egy olyan verem (stack) osztályt írni, amely:
push()
tagfüggvénye megjegyzi a paraméterként kapott értéket,pop()
tagfüggvénye visszaadja a legutóbb beszúrt értéket (törölve azt a veremből),- a konstruktora átveszi a verem maximális méretét,
- az elemeket pedig belül egy olyan tömbben tárolja, amelyet a
malloc()
függvénnyel foglalt.
A verem osztály másoló konstruktorát írd meg! Az értékadó operátorával ne töltsd az időt, azt csak tiltsd le. A példányosító
típusnak ne használd az alapértelmezett konstruktorát vagy értékadó operátorát, sőt tételezd fel, hogy nincs is neki egyik sem! A
teszteléshez használhatod az alábbi Noisy
osztályt.
(Figyelem: ennek a feladatnak semmi köze az előzőhöz! Nem az operator new
memóriafoglaló függvény
átdefiniálásával kell megoldani, sőt azzal nem is lehet.)
A Noisy
osztály kódja
class Noisy {
public:
explicit Noisy(int i) : i_{i} {
std::cout << "Noisy{" << i << "} ctor\n"; count++;
}
Noisy(Noisy const &o) : i_{o.i_} {
std::cout << "Noisy copy ctor " << i_ << "\n"; count++;
}
Noisy& operator=(Noisy const &o) = delete;
~Noisy() {
std::cout << "Noisy dtor " << i_ << "\n"; count--;
i_ = rand(); /* ! */
}
static void report() {
std::cout << count << " instance(s).\n";
}
private:
int i_;
static int count;
};
int Noisy::count = 0;
A destruktor jelölt sorával kapcsolatban egy megjegyzés. Ezt az értékadást a fordító kioptimalizálhatja; minek is adnánk értéket egy tagváltozónak a destruktorban, ha ez az objektum meg fog szűnni mindjárt. A memóriaterületét senki nem kellene olvassa már az objektum élettartamának vége után. Ha mégis, akkor az egy hibás kódrészlet (definiálatlan működésű); azt pedig nem kell biztosítania a fordítónak, hogy a hibás kódra determinisztikus viselkedésű programot generáljon.
Megoldás
#include <iostream>
#include <cstdlib>
#include <new>
#include <stdexcept>
#define DEBUG 1
template <typename T>
class Stack {
public:
Stack(size_t max_size);
Stack(Stack const &orig);
Stack & operator=(Stack const &orig) = delete;
~Stack();
void push(T const &what);
T pop();
bool empty() const;
private:
size_t size_;
size_t max_size_;
T *pData_;
};
template <typename T>
Stack<T>::Stack(size_t max_size) {
size_ = 0;
max_size_ = max_size;
/* itt nem szabad new T[]-t irni, mert az lefuttatna max_size_ darab T
* objektum default konstruktorat is. csak memoria kell, objektumok nincsenek. */
pData_ = (T*) malloc(sizeof(T) * max_size_);
}
template <typename T>
Stack<T>::Stack(Stack<T> const &orig) {
size_ = orig.size_;
max_size_ = orig.max_size_;
/* ez sem new T[], hanem csak malloc, hogy csak memoria legyen. egyelore. */
pData_ = (T*) malloc(sizeof(T) * max_size_);
/* itt masolodnak az objektumok is. ez nem lehet pdata[i] = orig.pdata[i], mert
* pdata[i] meg memoriaszemet, es kulonben annak az operator=-jet hivnank! */
for (size_t i = 0; i != size_; ++i)
new (&pData_[i]) T{orig.pData_[i]};
}
template <typename T>
Stack<T>::~Stack() {
/* destruktorok futtatasa */
for (size_t i = 0; i != size_; ++i)
pData_[i].~T();
/* memoria felszabaditasa. ez nem lehet delete[]. */
free(pData_);
}
/* Push element onto stack. */
template <typename T>
void Stack<T>::push(T const &what) {
if (size_ + 1 > max_size_)
throw std::length_error("stack tele");
/* a memoria mar megvan, csak egyelore memoriaszemet van a tarolt elem helyen.
* ezert nem operator=-t hivunk, hanem konstruktort! */
new (&pData_[size_]) T{what};
size_++;
}
/* Pop element from top of the stack. */
template <typename T>
T Stack<T>::pop() {
if (size_ == 0)
throw std::length_error("stack ures");
T saved{pData_[size_-1]};
/* itt lefut a destruktor, de a memoriaterulet marad! az kulon lett malloc()-olva. */
pData_[size_-1].~T();
size_--;
return saved;
}
template <typename T>
bool Stack<T>::empty() const {
return size_ == 0;
}
int main() {
Stack<char> s{100};
char c;
while (std::cin.get(c))
s.push(c);
while (!s.empty())
std::cout << s.pop();
}
Írd át a fenti vermes feladat osztályát úgy, hogy ne kelljen megadni maximális méretet! Ha kell, a verem méretezze át a tároláshoz használt tömböt az alábbi módon:
- Ha teli verembe kell beszúrni, megduplázza a méretét,
- ha kiszedés után negyedére zsugorodott a használt terület a foglalthoz képest, akkor pedig felezze meg a méretét.
Megoldás
#include <iostream>
#include <cstdlib>
#include <new>
#include <stdexcept>
#define DEBUG 1
template <typename T>
class Stack {
public:
Stack();
Stack(Stack const &orig);
Stack & operator=(Stack const &orig) = delete;
~Stack();
void push(T const &what);
T pop();
bool empty() const;
private:
size_t size_;
size_t allocated_;
T *pData_;
void resize(size_t new_allocated);
};
template <typename T>
Stack<T>::Stack() {
size_ = 0;
allocated_ = 1;
pData_ = (T*) malloc(sizeof(T) * allocated_);
}
template <typename T>
Stack<T>::Stack(Stack<T> const &orig) {
size_ = orig.size_;
allocated_ = orig.allocated_;
pData_ = (T*) malloc(sizeof(T) * allocated_);
for (size_t i = 0; i != size_; ++i)
new (&pData_[i]) T{orig.pData_[i]};
}
template <typename T>
Stack<T>::~Stack() {
for (size_t i = 0; i != size_; ++i)
pData_[i].~T();
free(pData_);
}
/* Reallocate stack's objects, move data to new memory area. */
template <typename T>
void Stack<T>::resize(size_t new_allocated) {
if (allocated_ == new_allocated || new_allocated < size_)
return;
#if DEBUG
std::cerr << "Resizing from " << allocated_ << " to " << new_allocated << std::endl;
#endif
T *newPData = (T*) malloc(sizeof(T) * new_allocated);
for (size_t i = 0; i != size_; ++i) {
new (&newPData[i]) T{pData_[i]};
pData_[i].~T();
}
free(pData_);
allocated_ = new_allocated;
pData_ = newPData;
}
/* Push element onto stack. */
template <typename T>
void Stack<T>::push(T const &what) {
if (size_ + 1 > allocated_)
resize(allocated_ * 2);
new (&pData_[size_]) T{what};
size_++;
}
/* Pop element from top of the stack. */
template <typename T>
T Stack<T>::pop() {
if (size_ == 0)
throw std::length_error("ures stack");
T saved{pData_[size_-1]};
pData_[size_-1].~T();
size_--;
if (size_ <= allocated_ / 4)
resize(allocated_ / 2);
return saved;
}
template <typename T>
bool Stack<T>::empty() const {
return size_ == 0;
}
int main() {
Stack<char> s;
char c;
while (std::cin.get(c))
s.push(c);
while (!s.empty())
std::cout << s.pop();
}