Institute of Computer Science
  1. Courses
  2. 2023/24 spring
  3. Programming in C++ (LTAT.03.025)
ET
Log in

Programming in C++ 2023/24 spring

  • Pealeht
  • 1. Muutujad ja andmetüübid
  • 2. Keele põhikonstruktsioonid I
  • 3. Keele põhikonstruktsioonid II
  • 4. Klass, struktuur, mallid
  • 5. Dünaamiline mäluhaldus I
  • 6. Dünaamiline mäluhaldus II
6 Dünaamiline mäluhaldus II
6.1 Kodutöö
6.2 Harjutused
6.3 Videolingid
  • 7. Kontrolltöö 1

Seitsmendal nädalal toimub 1. kontrolltöö

1. kontrolltöö näidis on Moodles

  • 8. Dünaamiline mäluhaldus III
  • 9. STL andmestruktuurid I
  • 10. STL andmestruktuurid II
  • 11. OOP I Klassid
  • 12. OOP II Pärilus ja polümorfism
  • 13. Erindite töötlemine
  • 14. Täiendavad teemad
  • 15. Kontrolltöö 2

Viieteistkümnendal nädalal toimub 2. kontrolltöö

  • 16. Projekti esitlus
  • Mõned viited - vajalikud kaaslased
  • Vanad materjalid
  • Juhendid
  • Viited

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 ja free
  • oskab kasutada operaatoreid new ja delete muutujate, objektide ja massiivide jaoks
  • teab, millal võivad tekkida mälulekked ja mis on rippuv viit (dangling pointer)

Sisukord

1. Viit (pointer) klassi isendile9. Mälu hõivamine objekti jaoks
2. Objekti edastamine funktsioonile10. Mälu hõivamine objektide massiivi jaoks
3. Dünaamiline mälujaotus11. Mälu hõivamine objektide vektori jaoks
4. Magasinmälu (stack memory)12. Rippuv viit (dangling pointer)
5. Operaatorid new ja delete13. Tüüpviga mälu hõivamisel/vabastamisel
6. Mälu hõivamine ja vabastamine ühe täisarvu jaoks koos erindi püüdmisega14. Mälulekked
7. Mälu hõivamine funktsioonis15. Vaba mälu fragmenteerimine
8. Mälu hõivamine massiivi jaoks16. Enesetestid

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++ operaatori new 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, sest new arvutab mälu vajaduse tüübi järgi ise
  • ei ole vaja tüübiteisendust, sest new tagastab nõutud tüüpi viida, mitte void*
  • 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 indal 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;)

<< Näita enesetesti >>

<< Näita enesetesti >>

<< Näita enesetesti >>

<< Näita enesetesti >>

<< Näita enesetesti >>

  • Institute of Computer Science
  • Faculty of Science and Technology
  • University of Tartu
In case of technical problems or questions write to:

Contact the course organizers with the organizational and course content questions.
The proprietary copyrights of educational materials belong to the University of Tartu. The use of educational materials is permitted for the purposes and under the conditions provided for in the copyright law for the free use of a work. When using educational materials, the user is obligated to give credit to the author of the educational materials.
The use of educational materials for other purposes is allowed only with the prior written consent of the University of Tartu.
Terms of use for the Courses environment