na obsah klávesové zkratky na hlavní stránku na menu

Dynamická alokace v C++

S dynamickou alokací jsme se setkali již při výkladu jazyka C. Jazyk C++ přidává další možnosti, jak spravovat dynamicky alokovanou paměť. V tomto článku se podíváme na operátory new, delete a jejich varianty pro pole.

Operátor new a delete

V jazyce C jsme měli k dispozici několik funkcí, které umožňovaly získat spojitý blok paměti a zase ho uvolnit. V jazyce C++ je dynamická alokace výsadou čtyř operátorů. Odtud též plyne jiná syntaxe. Výhodou C++ operátorů je jejich přizpůsobení objektům. Když alokujeme objektový typ pomocí operátoru new, zavolá za nás operátor konstruktor objektu. Stejně tak operátor delete zavolá automaticky destruktor. Nyní si ukážeme, jak lze alokovat a uvolnit paměť pomocí výše zmíněných operátorů.

int *a = new int;

double *d = new double;

// vytváříme dynamický objekt
Vozidlo *v1 = new Vozidlo;
// zde voláme konstruktor s parametrem
Vozidlo *v2 = new Vozidlo (4, 100.0);

// ...
delete a;
delete d;
delete v1;
delete v2;

Operátory jsou součástí globálního jmenného prostoru a pro jejich použití není nutné vkládat speciální hlavičku. Použití je vcelku přímočaré. Za klíčovým slovem new následuje typ, který chceme alokovat. Pokud jde o objektový typ, můžeme za něj ještě do kulatých závorek umístit hodnoty parametrů konstruktoru (jako v případě proměnné v2). Ať už budeme s pamětí provádět cokoliv, musíme ji na konci práce s ní uvolnit. Pro uvolnění alokované paměti stačí zavolat operátor delete a jako parametr mu předat ukazatel získaný voláním operátoru new. Všimněme si, že jsme neuvedli příklad na alokaci pole. K tomu totiž slouží další dva operátory.

Operátor new[] a delete[]

Operátory new[] a delete[] jsou obdobou prvních dvou uvedených operátorů s tou výjimkou, že slouží pro práci s poli. Důvodem, proč existují samostatné varianty pro práci s polem, je, že alokujeme-li pole objektů, je nutné zavolat konstruktory a destruktory všech prvků pole. Ukažme si tedy, jak vytvoříme pole.

int *a = new int[10];

double *d = new double[5];

// je volán konstruktor bez parametru, musí být přítomný
Vozidlo *v = new Vozidlo[15];

// ...
delete[] a;
delete[] d;
delete[] v;

Jediným rozdílem proti předchozímu příkladu je přidání hranatých závorek. Do nich se při alokaci zapisuje počet prvků vytvářeného pole. Tento údaj je při uvolňování paměti již nepotřebný, a proto se operátor delete[] zapisuje s prázdnými závorkami, aby se odlišil od prosté varianty. Je třeba důsledně dodržovat párování odpovídajících dvojic operátorů. Kdybychom alokovali pole objektů, ale uvolňovali ho pomocí prosté varianty operátoru delete, pak by se mohlo stát (záleží na implementaci překladače), že by se správně neuvolnila paměť a nedošlo by k volání všech destruktorů. Program Valgrind při takové chybě též vypíše hlášení o nesprávném spárování operátorů.

==20796== Mismatched free() / delete / delete []

Signalizace chyby alokace

V jazyce C vracela funkce malloc() NULL, pokud nebylo možné alokovat žádané množství paměti. Obě varianty operátoru new se takto normálně nechovají, ale vyhazují v případě chyby tzv. výjimku, se kterými se blíže seznámíme v článku o výjimkách. Operátor vyhazuje výjimku std::bad_alloc. Pokud bychom chtěli, aby se operátor choval jako funkce z jazyka C, a tedy vracel NULL, pak mu musíme předat speciální konstantu nothrow, která je deklarovaná v hlavičkovém souboru new a umístěná do jmenného prostoru std. Ukažme si, jak přetvoříme operátor tak, aby se choval jako funkce malloc().

#include <new>

// tato verze new bude vracet NULL
// namísto výjimky v případě chyby 
double *d = new(std::nothrow) double;

Technicky jde o jiný operátor new. Je to další praktická ukázka přetěžování funkcí. Operátorů new může existovat libovolné množství a mohou mít libovolné množství parametrů, které se pak uvádějí v kulatých závorkách. Přetížit lze i standardní verze operátorů new a delete.

Přetěžování operátorů new a delete

Jelikož jsou new a delete operátory, můžeme je přetížit a implementovat tak vlastní alokační logiku. Toho se používá ve specifických situacích, kdy je potřeba např. umístit alokovaný blok na přesně danou adresu, pro účely ladění nebo pro implementaci garbage collectoru.

Operátory new a delete lze přetížit dvěma způsoby: jako statické členy tříd, nebo globálně. V prvním případě bude změna alokace svázána jen se třídou, která operátory přetěžuje. Ve druhém případě se změna dotkne všech použití operátorů. Platí základní pravidlo, že když se rozhodneme přetížit některý z operátorů, je potřeba přetížit celý pár newdelete.

Přetížení operátorů jako členů tříd

Máme-li v programu třídu, která vyžaduje specifický přístup k alokaci instancí, můžeme pro ni přetížit alokační operátory. Nejjednodušeji to provedeme takto:

#include <new>
#include <iostream>
#include <cstdlib>

class ChybaAlokace
{
};

class SpecialniBuffer
{
  public:
  static void* operator new (size_t velikost)
  {
    void *p = malloc(velikost);
    if (p == NULL)
    {
      throw ChybaAlokace();
    }
    std::cout << "Volan new s velikosti " << velikost << " a adresou " <<
    p << std::endl;
    return p;
  }
  
  static void operator delete (void *adresa)
  {
    free(adresa);
    std::cout << "Volan delete s adresou " << adresa << std::endl;
  }
  
  static void* operator new[] (size_t velikost)
  {
    void *p = malloc(velikost);
    if (p == NULL)
    {
      throw ChybaAlokace();
    }
    std::cout << "Volan new[] s velikosti " << velikost << " a adresou " <<
    p << std::endl;
    return p;
  }
  
  static void operator delete[] (void *adresa)
  {
    free(adresa);
    std::cout << "Volan delete[] s adresou " << adresa << std::endl;
  }  
  SpecialniBuffer()
  {
    
  }
};

int main (int argc, char **argv)
{
  // zavolá se přetížený operátor new
  SpecialniBuffer* b = new SpecialniBuffer();
  // zavolá se přetížený operátor delete
  delete b;
  
  // zavolá se přetížený operátor new[]
  SpecialniBuffer* c = new SpecialniBuffer[10];
  // zavolá se přetížený operátor delete[]
  delete[] c;
  return 0;
}

V ukázce výše jsme vložili hlavičku new pro zpřístupnění typu size_t a dalších typů souvisejících s přetěžovanými operátory. Operátory jsou vždy implicitně statické a klíčové slovo static tak lze vynechat. Při přetěžování operátoru new je nutné kontrolovat, zda alokace proběhla v pořádku a v případě chyby vhodně zareagovat. V příkladu vyhazujeme vlastní výjimku. Samozřejmě není problém přetížit i operátory pro alokaci a dealokaci polí. Jejich deklarace je až na hranaté závorky shodná s předchozími operátory. Zajímavý je kód funkce main(), kde využíváme naše nové operátory. Má-li třída přetížené alokační operátory, jsou tyto zavolány automaticky tehdy, když požadujeme dynamickou alokaci instancí.

Operátor new může mít též další parametry (viz případ se std::nothrow). Ukažme si, jak přetížit operátor s dodatečným parametrem.

// ...

class SpecialniBuffer
{
  public:
  static void* operator new (size_t velikost, int parametr)
  {
    // ...
  }
  
};

int main (int argc, char **argv)
{
  SpecialniBuffer* b = new(42) SpecialniBuffer();
  // ...
}

Při volání operátoru se hodnoty parametrů zapisují do kulatých závorek za slovo new. V deklaraci jsou dodatečné parametry uvedeny za povinnou velikostí. Deklarace takového operátoru je pak poněkud odlišná od vlastního použití, což může být lehce matoucí. Parametry budou však předány správně.

Přetížení globálních operátorů

Druhou možností je zastínit globální operátory new a delete. Tato akce s sebou nese riziko v podobě nemožnosti vrátit se k původním vestavěným operátorům. Proto by se při přetěžování globálních operátorů mělo postupovat velmi obezřetně a zcela jistě by se nemělo provádět v dynamických knihovnách. Takto přetížené operátory nelze umístit do vlastního prostoru jmen a nemohou být statické, čili jsou viditelné ve všech modulech. Bezpečnější metodou pro globální přetížení operátorů je vytvořit objekt na vrcholu stromu dědičnosti a umístit do něj nové verze operátorů. Ostatní objekty budou potomci tohoto objektu a zdědí tak tyto operátory.

Přetížení provedeme takto.

#include <new>

void* operator new (size_t velikost)
{
  std::cout << velikost << std::endl;
  // ...
}

void operator delete (void *adresa)
{
  std::cout << adresa << std::endl;
  // ...
}

Od statických členů tříd se deklarace nijak neliší, musí být ovšem umístěné v globálním prostoru jmen a nesmějí být statické. Přetížit lze samozřejmě i varianty operátorů pro pole a operátor new s parametrem. Stále platí, že přetěžování by se mělo provádět v párech newdelete.