Dünaamiline mäluhaldus II
Pärast selle praktikumi läbimist üliõpilane
- oskab kasutada viita (pointer) objektidele viitamisel
- oskab objekti viita ja viidet edastada funktsioonile
- teab, mis on magasinmälu (stack memory) ja kuidas seda kasutatakse
- teab C keele funktsioone
malloc
,calloc
,realloc
jafree
- oskab kasutada operaatoreid
new
jadelete
muutujate, objektide ja massiivide jaoks - teab, millal võivad tekkida mälulekked ja mis on rippuv viit (dangling pointer)
Sisukord
Viit (pointer) klassi isendile (objektile)
Klassi isendeid saab omistada ka viidamuutujale. Vaatame näidet.
#include <iostream> using namespace std; class Minuklass { private: int m_i{}; public: Minuklass(int i) : m_i{i} { cout << "Minuklass konstruktoris\n"; } void set_i(int i) { m_i = i; } int get_i() { return m_i; } }; int main() { Minuklass o{1}; // objekt o o.set_i(2); // seame objektis o i = 2 cout << "&o = " << &o << " i = " << o.get_i() << '\n'; Minuklass* p_o{&o}; // p_o hakkab viitama objektile o p_o->set_i(3); // seame i = 3 cout << "p_o = " << p_o << " i = " << p_o->get_i() << '\n'; cout << "i = " << (*p_o).get_i() << '\n'; return 0; } | Minuklass konstruktoris &o = 0xd33ffffe54 i = 2 p_o = 0xd33ffffe54 i = 3 i = 3 |
Käsuga Minuklass o{1};
luuakse Minuklass
objekt, millele viitab muutuja o
. Väljatrükis on vastav konstruktori teade. Käsuga Minuklass* p_o{&o};
luuakse viidamuutuja p_o
, mis hakkab viitama samale objektile.
Kuna viidamuutuja abil saab viidatavatele andmetele ligi operaatori *
abil, siis näiteks isendimuutuja i
kättesaamiseks tuleks kirjutada (*p_o).get_i()
. See on pikk ja kohmakas, selle asemel on lihtsam võimalus kasutada nooleoperaatorit ->
:
Avaldis
viidamuutuja -> klassi-liige
on samaväärne avaldisega
(*viidamuutuja).klassi-liige
Näiteks (*p_o).get_i()
ja p_o->get_i()
on samaväärsed.
Objekti edastamine funktsioonile
Funktsioonile võib edastada klassitüüpi objekte samamoodi nagu kõiki teisi tüüpe. Objekti saab edastada funktsioonile kas kopeerimise teel või viida (viite) abil. Lisame eelmisele näitele kaks üledefineeritud funktsiooni void muuda_objekti(Minuklass o, int arv)
ja void muuda_objekti(Minuklass* o, int arv)
. Esimeses funktsioonis edastatakse funktsioonile objekt kopeerimise teel, st edastatavast objektist tehakse koopia. Teises funktsioonis edastatakse funktsioonile viit objektile.
void muuda_objekti(Minuklass o, int arv) { o.set_i(arv); cout << "&o funktsioonis muuda_objekti(Minuklass o, int arv): " << &o << '\n'; cout << "i = " << o.get_i() << "\n"; } void muuda_objekti(Minuklass* p_o, int arv) { p_o->set_i(arv); // p_o on viit objektile cout << "p_o funktsioonis muuda_objekti(Minuklass* p_o, int arv): " << p_o << '\n'; cout << "i = " << p_o->get_i() << "\n"; }
Pöördumisel funktsioonis main
esimese funktsiooni poole
Minuklass o{1}; muuda_objekti(o, 2); cout << "&o = "<< &o << " i = " << o.get_i() << '\n'; | Minuklass konstruktoris &o funktsioonis muuda_objekti(Minuklass o, int arv): 0x7bf07ffba0 i = 2 &o = 0x7bf07ffbcc i = 1 |
tehakse objektist Minuklass o{1}
koopia (kasutatakse koopiakonstruktorit, millest hiljem) ja uue objekti isendimuutujale i
omistatakse 2
. Objekti aadressid funktsioonides muuda_objekti
ja main
on erinevad. Seetõttu funktsioonis muuda_objekti
isendivälja i
muutmine ei mõjuta main
funktsioonis olevat objekti o
, selles on isendivälja i
väärtus endiselt 1
.
Teise funktsiooni muuda_objekti(Minuklass* p_o, int arv)
poole pöördumisel edastatakse funktsioonile viit objektile (objekti aadress) ja funktsioonis tehtavad muudatused mõjutavad objekti. Objekti aadressid funktsioonides muuda_objekti
ja main
on samad.
Minuklass o{1}; muuda_objekti(&o, 2); // funktsioonile edastatakse objekti aadress cout << "&o = "<< &o << " i = " << o.get_i() << '\n'; | Minuklass konstruktoris p_o funktsioonis muuda_objekti(Minuklass* p_o, int arv): 0xcbffbff8ac i = 2 &o = 0xcbffbff8ac i = 2 |
Kasutades viidet (reference), võib teise funktsiooni vormistada järgmiselt.
void muuda_objekti(Minuklass& o, int arv) { o.set_i(arv); cout << "&o funktsioonis muuda_objekti(Minuklass& o, int arv): " << &o << '\n'; cout << "i = " << o.get_i() << "\n"; }
Kuna funktsioonile edastatakse viide objektile, siis objekti isendivälju ja funktsioone saab kätte .
operaatori abil. Funktsiooni poole pöördumisel (main
funktsioonis) antakse argumendiks lihtsalt objekt o
.
Minuklass o{1}; muuda_objekti(o, 2); cout << "&o = "<< &o << " i = " << o.get_i() << '\n'; | Minuklass konstruktoris &o funktsioonis muuda_objekti(Minuklass& o, int arv): 0x11b73ff93c i = 2 &o = 0x11b73ff93c i = 2 |
Siin objekti aadressid on samad ja funktsioonis toimetatu mõjutab objekti ennast.
Märgime siin, et ei saa korraga üle defineerida funktsioone muuda_objekti(Minuklass& o, int arv)
ja muuda_objekti(Minuklass o, int arv)
, sest mõlema funktsiooni poole pöördumine on sama kujuga, nt muuda_objekti(o, 2)
ja kompilaator ei tea, kumba funktsiooni välja kutsuda.
Dünaamiline mälujaotus
C++ programmide poolt kasutatav mälu jaguneb neljaks suuremaks osaks
- mälu, kus paikneb programmitekst (ja kood)
- mälu, kus paiknevad staatilised ja globaalsed muutujad
- magasinmälu (stack memory), kuhu pannakse funktsioonide väljakutsetega jms seotud info (lokaalsed muutujad, parameetrid, jne)
- kuhjamälu (heap memory), kust saab mälu küsida C funktsioonide
malloc
,calloc
,realloc
abil, samuti C++ operaatorinew
abil
Viimast kahte võib nimetada dünaamiliseks mäluks, kuna nende kasutus muutub programmi täitmise jooksul.
Magasinmälu (stack memory)
Olgu meil programm, milles on peale main
funktsiooni veel kaks funktsiooni arvuta
ja kuup
#include <iostream> using namespace std; int kuup(int x){ return x*x*x; } int arvuta(int a1, int a2){ int sum = kuup(a1 + a2); return sum; } int main() { int a{3}; int b{5}; int c{arvuta(a, b)}; cout << "(" << a << " + " << b << ")^3 = " << c << '\n'; return 0; }
Vaatame selle programmi täitmist sammhaaval.
1. Programmi töö täitmine algab main
funktsioonist. Kogu vajalik info main
funktsiooni kohta (lokaalsed muutujad, ...) kopeeritakse magasinmällu. Vastavat mäluosa nimetatakse ka magasinraamiks (stack frame).
2. Funktsioonist main
toimub pöördumine funktsiooni arvuta
poole. Magasinmällu kopeeritakse kogu vajalik info arvuta
funktsiooni jaoks (parameetrid, lokaalsed muutujad, ...).
3. Funktsioonist arvuta
toimub pöördumine funktsiooni kuup
poole. Magasinmällu kopeeritakse kogu vajalik info kuup
funktsiooni jaoks (parameetrid, lokaalsed muutujad, ...).
4. Funktsioonis kuup
tehakse arvutused ja toimub tagasipöördumine väljakutsuja, st funktsiooni arvuta
poole. Kuna funktsiooni kuup
töö on lõppenud, eemaldatakse magasinmälust vastav informatsioon ja funktsiooni kuup
lokaalsed muutujad (kui neid on), kaotavad kehtivuse.
5. Funktsioonis arvuta
väärtustatakse muutuja sum
ja toimub tagasipöördumine väljakutsuja, st funktsiooni main
poole. Funktsiooni arvuta
info kustutatakse magasinmälust ja tema lokaalsed muutujad ei ole enam kättesaadavad.
6. Funktsioonis main
kuvatakse info ekraanile, tagastatakse süsteemile 0 ja main
lõpetab töö. Magasinmälust kustutatakse vastav info, vt ka joonis
Tegelikult iga kord, kui sisenetakse lokaalsesse skoopi (lisaks funktsiooni väljakutsele võib selleks olla nt lause, kus tegevused {
ja }
vahel, ... ) võib sellest mõelda, kui skoobi lisamisest magasini. Kui programmi täitmine väljub skoobist, siis skoop eemaldatakse magasinmälust.
Seega, funktsioonist või mõnest muust skoobist lokaalse muutuja aadressi tagastamine viidana või viitena võib lõppeda programmi jaoks ettearvamatu veaga, sest peale skoobist väljumist vabastatakse skoobi lokaalsete muutujate alt mälu ja seda võidakse kasutada süsteemi poolt teistel eesmärkidel.
Operaatorid new
ja delete
Programmeerimiskeeles C on mälu hõivamiseks ja vabastamiseks funktsioonid malloc
, calloc
, realloc
, free
. Kuidas need funktsioonid täpselt töötavad, uuri Moodles viidatud ingliskeelsest videost (alates 2:29:14) või https://en.cppreference.com/w/c/memory.
Dünaamilise mälu hõivamine toimub nn kuhjamälust (heap memory). Kuna arvuti mälu on piiratud ressurss, siis mälu hõivamisel võib tekkida erind bad_alloc
, mida tuleb rohkem mälu nõudvate ülesannete juures kindlasti arvestada.
Keeles C++ on dünaamilise mälu haldamiseks kaks operaatorit - new
ja delete
. Kuigi need operaatorid töötavad analoogiliselt C funktsioonidega, on operaatoritel new
ja delete
siiski mõned eelised:
- ei ole vaja kasutada
sizeof
operaatorit, sestnew
arvutab mälu vajaduse tüübi järgi ise - ei ole vaja tüübiteisendust, sest
new
tagastab nõutud tüüpi viida, mittevoid*
- mõlemat operaatorit on võimalik üle defineerida
Vaatame näidete varal operaatorite new
ja delete
kasutamist.
Mälu hõivamine ja vabastamine ühe täisarvu jaoks koos erindi püüdmisega
Erindid on tuttavad juba keelest Python
, C++
erindeid käsitletakse põhjalikumalt hiljem.
#include <iostream> using namespace std; int main() { int* ptr{}; // ptr algväärtuseks nullptr try { ptr = new int; // mälu hõivamine int jaoks } catch (bad_alloc b) { // erindi püüdmine cout << "Mälu hõivamise viga!\n"; return 1; // lõpetame veaga } *ptr = 200; // väärtuse omistamine cout << "Aadressil " << ptr << " "; // aadress cout << "on väärtus " << *ptr << '\n'; // väärtus aadressil ptr delete ptr; // mälu vabastamine ptr = nullptr; return 0; } | Aadressil 0x1d76add18f0 on väärtus 200 |
Käsuga int* ptr{};
defineeritakse viidamuutuja ptr
ja initsialiseeritakse väärtusega nullptr
. Väärtus nullptr
ei viita kuhugi ja seda saab tingimuslauses kontrollida. Käsuga ptr = new int;
hõivatakse kuhjamälus ruum int
tüübi jaoks ja selle aadress salvestatakse muutujasse ptr
. Kui mälu hõivamine ebaõnnestus, siis täidetakse catch
plokis olev osa, st kuvatakse teade veast ja lõpetatakse töö.
Kui mälu hõivamine õnnestus, siis käsuga *ptr = 200;
salvestatakse hõivatud mäluossa väärtus 200
.
Kui käsuga new
hõivatud mälu ei ole enam vaja, siis tuleb see vabastada. Mälu vabastatakse siin käsuga delete ptr;
. Selle käsuga antakse hõivatud mälu vabaks, kuid ei muudeta muutujat ptr
, mis sisaldab endiselt aadressi. Seda viita võib endiselt kasutada, kuid tulemus võib olla ettearvamatu, sest süsteem võib vabastatud mälu kasutada teisel otstarbel. Seetõttu on mõistlik segaduste vältimiseks anda talle väärtus nullptr
.
Kui mälu ei vabastata ja viidale omistatakse uus aadress, siis hõivatud mälu jääb programmi käsutusse programmi töö lõpuni.
Järgmistes näidetes ei kasutata mälu hõivamisel erindi püüdmist, et hoida programm lühemana.
Mälu hõivamine funktsioonis
Järgmises näites hõivatakse mälu funktsioonis, mis tagastab viida ja mälu vabastatakse pöörduja poolt: (vt ka joonis)
#include <iostream> using namespace std; int* fun(){ int* ptr{}; ptr = new int(200); // mälu hõivamine int jaoks koos väärtustamisega cout << "Aadress fun-is: " << ptr << '\n'; return ptr; // viida tagastamine } int main() { int* p = fun(); // funktsioon tagastab viida cout << "Aadress main-is: " << p << '\n'; cout << "Väärtus main-is: " << *p << '\n'; delete p; // mälu vabastamine cout << "Väärtus main-is peale vabastamist: " << *p << '\n'; p = nullptr; return 0; } | Aadress fun-is: 0x153b7a218f0 Aadress main-is: 0x153b7a218f0 Väärtus main-is: 200 Väärtus main-is peale vabastamist: -1214113168 |
Peale funktsiooni fun
töö lõppu tagastatakse täisarvuline viit main
funktsiooni muutujale p
, mis viitab samale kohale kuhjamälus. Peale info kuvamist ekraanile mälu vabastatakse. Ekraanile kuvatakse viidatud aadressil olev täisarv ka peale mälu vabastamist ja tulemuseks ei ole enam oodatud 200.
NB! Mõnikord võib info ka säilida peale mälu vabastamist, mis on eriti eksitav.
Mälu hõivamine massiivi jaoks
Mälu hõivamine massiivi jaoks toimub samuti käsu new
abil nt int* p1 = new int[5]
.
#include <iostream> using namespace std; int main() { int* p, i; // NB! viit on ainult muutuja p; i on tavaline int muutuja p = new int [10]; // mälu hõivamine 10 täisarvu jaoks for (i = 0; i < 10; ++i) { p[i] = i; } for(i = 0; i < 10; i++) { cout << *(p + i) << " "; } delete [] p; // mälu vabastamine massiivi alt p = nullptr; return 0; } | 0 1 2 3 4 5 6 7 8 9 |
Esimeses for
- tsüklis on kasutatud indekseid, teises aga viitade (pointer) aritmeetikat.
NB! Pöörake tähelepanu mälu vabastamise lausele, kus sulud []
on peale võtmesõna delete
ja enne muutujat.
Mälu hõivamine objekti jaoks
Mälu saab hõivata (ja vabaks lasta) ka objekti jaoks, nt
Minuklass* o = new Minuklass{1}; // Mälu hõivamine objekti jaoks ja objekti loomine delete o; // mälu vabastamine objekti alt
Mälu hõivamisel käivitatakse vastav konstruktor ja mälu vabastamisel destruktor (destructor). Destruktor on klassis spetsiaalne funktsioon, mis kutsutakse automaatselt välja objekti eluea lõpul. Destruktor kannab klassi nime, mille ees on märk ~
ja tal ei ole tagastustüüpi ega ka parameetreid. Täpsemalt tuleb destruktoritest juttu objektorienteeritud programmeerimise peatükis.
Kui muutuja on viit klassile, siis klassi liikmeid saab välja kutsuda .
asemel ->
notatsiooniga, nt o->set_i(5)
.
#include <iostream> using namespace std; class Minuklass { public: Minuklass() = default; // vaikekonstruktor Minuklass(int i) : m_i{i} { // ühe parameetriga konstruktor cout << "Minuklass konstruktoris m_i = " << m_i << '\n'; } ~Minuklass() { // destruktor cout << "Minuklass destruktoris m_i = " << m_i << '\n'; } void set_i(int i) { // isendimuutuja seadmine m_i = i; } int get_i() { // isendimuutuja võtmine return m_i; } friend ostream& operator<<(ostream& os, const Minuklass& m); private: int m_i{}; }; ostream& operator<<(ostream& os, Minuklass* m){ os << "Minuklass: " << m->get_i() << '\n'; return os; } // objekti muutmine void muuda_objekti(Minuklass* o, int i) { o->set_i(i); } int main() { // mälu hõivamine objekti jaoks ja isendi loomine Minuklass* o = new Minuklass{1}; muuda_objekti(o, 5); // objekti muutmine cout << o; // objekti kuvamine ekraanile delete o; // mälu vabastamine cout << o; // NB! objekti alt on mälu vabastatud, tulemus ettearvamatu return 0; } | Minuklass konstruktoris m_i = 1 Minuklass: 5 Minuklass destruktoris m_i = 5 Minuklass: 1125936624 |
Objekti loomisel koos mälu hõivamisega käivitatakse konstruktor ja mälu vabastamisel destruktor. Objektimuutuja ise on kättesaadav skoobi lõpuni, st kuni main
lõpuni. Seetõttu peale delete
käsku on käsk cout << o
legaalne, kuid tulemus ei ole see, mis lootsime.
Mälu hõivamine objektide massiivi jaoks
Mälu saab hõivata ka objektide massiivi jaoks, kuid ainult siis, kui klassil on olemas vaikekonstruktor. See tuleneb asjaolust, et massiivi jaoks ruumi hõivamisel ei saa kasutada initsialiseerijat. Loome massiivi main
funktsioonis (klassil Minuklass
on vaikekonstruktor olemas):
int main() { Minuklass *p; // mälu hõivamine massiivi jaoks, kus on 5 objekti. p = new Minuklass[5]; // Objektid luuakse vaikekonstruktoriga // massiivi objektide muutmine, argumendiks tuleb anda // viit objektile ja m_i uus väärtus for (int i{}; i < 5; ++i) { muuda_objekti(p + i, i); } // objektide kuvamine ekraanile for (int i{}; i < 5; ++i) { cout << p + i; } // mälu vabastamine, massiivi iga liikme jaoks // kutsutakse välja destruktor delete[] p; return 0; } | Minuklass: 0 Minuklass: 1 Minuklass: 2 Minuklass: 3 Minuklass: 4 Minuklass destruktoris m_i = 4 Minuklass destruktoris m_i = 3 Minuklass destruktoris m_i = 2 Minuklass destruktoris m_i = 1 Minuklass destruktoris m_i = 0 |
Mälu hõivamine objektide vektori jaoks
Sageli on vaja luua andmekogumeid objektidest, kus objekti loomiseks ei piisa vaikekonstruktorist, vaid on vaja kasutada parameetritega konstruktoreid. Sellisel juhul saab kasutada objektide hoidmiseks vektorit.
Loome vektori vector<Minuklass*>* p
, mille elementideks on viidad (pointer) Minuklass
objektidele (lisada ka lause #include <vector>
).
Lisame klassi Minuklass
funktsiooni vabasta(Minuklass* m)
, mis vabastab mälu parameetris viidatud Minuklass
objekti alt.
void vabasta(Minuklass* m){ if (m){ // kas viit on kehtiv delete m; // vabastame viidatud mälu m = nullptr; } }
Funktsioonis main
loome vektori ja lisame sinna viis objekti (õigemini viis viita (pointer)) Minuklass
objektidele. Edasi muudame neid objekte, kuvame objektide info ekraanile ja lõpuks vabastame viidatud mäluosad.
Objektide muutmisel käsu p->at(i)->set_i(i*2)
esimese osaga p->at(i)
saame vektori i
ndal kohal oleva elemendi, milleks on viit Minuklass
objektile ja edasi rakendame sellele objektile funktsiooni set_i
.
int main() { // vektor, mille elementideks on viidad Minuklass objektidele vector<Minuklass*>* p = new vector<Minuklass*>; int objekte{5}; for (int i{}; i < objekte; ++i) { p->push_back(new Minuklass(i)); // lisame vektorisse uue objekti } // muudame objekte for (int i{}; i < p->size(); ++i) { p->at(i)->set_i(i*2); } // Kuvame objektid ekraanile for (int i{}; i < p->size(); ++i) { cout << p->at(i); } // vabastame mälu objektide alt for (int i{}; i < p->size(); ++i) { vabasta(p->at(i)); } delete p; //vabastame viidatud mälu return 0; } | Minuklass konstruktoris m_i = 0 Minuklass konstruktoris m_i = 1 Minuklass konstruktoris m_i = 2 Minuklass konstruktoris m_i = 3 Minuklass konstruktoris m_i = 4 Minuklass: 0 Minuklass: 2 Minuklass: 4 Minuklass: 6 Minuklass: 8 Minuklass destruktoris m_i = 0 Minuklass destruktoris m_i = 2 Minuklass destruktoris m_i = 4 Minuklass destruktoris m_i = 6 Minuklass destruktoris m_i = 8 |
Dünaamilise mäluhalduse ohud ja riskid
Dünaamilisel mäluhaldusel Võib tekkida erinevaid probleeme, neist olulisemateks on rippuv viit (dangling pointer), mälulekked (memory leaks) ja mälu fragmenteerimine (memory fragmentation).
Rippuv viit (dangling pointer)
Rippuv viit (dangling pointer) on viidamuutuja, mis sisaldab mäluaadressi, mis on juba vabastatud kas delete
või delete []
poolt. Selle viida osundamine (dereference) tähendab sellisest mälust lugemist (või halvemal juhul sellisesse mäluossa kirjutamist), mida on juba kasutatud teiste programmiosade poolt. Tulemus on ennustamatu ja kindlasti ka soovimatu. Mitmekordne vabastamine, st kui vabastada juba vabastatud (seega rippuvat) viita teist korda, kasutades kas delete
või delete []
, on teine ohu allikas.
Üks võimalus viimast vältida, on omistada viidale peale vabastamist nullviit nullptr
.
Tüüpviga mälu hõivamisel/vabastamisel
Mälu hõivamine massiivi või ühe väärtuse jaoks on küllalt sarnane. Näiteks,
int* üks_int{new int{55}}; // viit ühele täisarvule, mille väärtus on 55 int* massiiv_int{new int[55]}; // viit 55st elemendist koosnevale argväärtustamata täisarvude massiivile
Peale seda ei ole kompilaatoril võimalust neid kahte viita eristada, eriti kui neid edastatakse erinevatele programmiosadele. See tähendab, et järgmised kaks käsku kompileeruvad isegi ilma hoiatusteta:
delete [] üks_int; // vale! delete massiiv_int; // vale!
Mis sellisel juhul juhtub, sõltub kompilaatori implementatsioonist, kuid kindlasti on see vigade allikaks.
Mälulekked
Mäluleke juhtub siis, kui hõivata mälu käsuga new
või new[]
hiljem vabastamata. Kui hõivatud vaba mäluosa aadress läheb kaotsi, nt aadressi ülekirjutamise teel, siis toimub samuti mäluleke. See võib juhtuda tsüklis, kus programm tarbib järk-järgult rohkem mälu ja võib aeglustada programmi tööd.
Kui vaadelda skoopi, siis viit on samasugune muutuja nagu iga teine. Viida eluiga algab hetkest, kui ta plokis defineeritakse ja lõpeb plokki lõpetava suluga. Peale seda pole viidamuutujat enam olemas ja pole enam võimalik mälu vabastada. Sellist olukorda on väga lihtne tekitada, näiteks return
käsuga mälu hõivamise ja vabastamise vahel. Eriti raske on mäluleket tuvastada juhtudel, kui mälu hõivatakse ühes programmiosas, aga vabastatakse hoopis mujal.
Vaba mälu fragmenteerimine
Mälu fragmenteerimine võib tekkida sellistes programmides, mis hõivavad ja vabastavad mälu sageli. Iga kord new
käsku täites hõivatakse uus sidus baitide plokk. Kui programm hõivab ja vabastab palju erineva suurusega plokke, siis võib juhtuda, et mälus on palju väikese suurusega vabu plokke ja ükski neist ei sobi, kui programmil on vaja hõivata suurt plokki. Sellisel juhul uue mälu hõivamine lõpeb veaga. Niisugune olukord juhtub tänapäeva arvutitel siiski harva, sest kasutatav mälu on suur. Kindlasti suur defragmenteerimise aste aeglustab programmi tööd ja sellega peab arvestama, eriti kui on tegemist programmiga, mille kasutamisel töö kiirus on kriitilise tähtsusega.
Siinkohal toome tsitaadi raamatust: Ivor Horton, Peter Van Veert, Beginning C++20 From Novice to Professional, Sixth Edition, Apress, 2020. Lk. 220
"Never use the operators new, new[], delete, and delete[] directly in day-to-day coding. These operators have no place in modern C++ code. Always use either the std::vector<> container (to replace dynamic arrays) or a smart pointer (to dynamically allocate individual objects and manage their lifetimes). These high-level alternatives are much, much safer than the low-level memory management primitives and will help you tremendously by eradicating dangling pointers, multiple deallocations, allocation/deallocation mismatches, and memory leaks from your programs."
Enesetestid
NB! Enesetestides eeldame, et on kasutatud standardnimeruumi (using namespace std;
)