* Dynaamiset taulukot
* Hajottaja (destructor)
Dyn.muut.luonti C: pMuuttuja = malloc(koko); pTaulukko = malloc(alkioiden_lkm * alkion_koko); // ei alus pTaulukko = calloc(alkioiden_lkm,alkion_koko); // alust. 0 C++: pOlio = new cLuokka; // oletusmuodostaja pOlio = new cLuokka(muodostajan_param_lista); pOlioTaul = new cLuokka[alkioiden_lkm]; Koon vaihto C: pTaulukko = realloc(pVanha,uusi_koko_tavuina); Hävittäminen C: free(pMuuttuja); free(pTaulukko); C++: delete pOlio; delete [] pTaulukko; Hajottaja ~cLuokka();
C: malloc - calloc - realloc - free C++: new - delete new [] - delete []
Olemme oppineet varaamaan muuttujia esittelyn yhteydessä. Muuttujia voidaan luoda (= varata muistitilaa) myös ohjelman ajon aikana. Tämä on tarpeellista erityisesti silloin, kun ohjelman kirjoittamisen aikana ei tiedetä muuttujien määrää tai ehkei jopa edes kokoa (taulukot).
^ muistin | kaytto | +-------------------------------+ | | | | | | | | | | | | | | | | | | | | | | | | | | | +-------------------------------------------> ohjelman ohjelman alku loppuEdellinen kuva on hieman yksinkertaistettu, koska "oikeasti" aliohjelmien lokaalit muuttujat (automaattiset muuttujat) syntyvät aliohjelmaan tultaessa ja häviävät aliohjelmasta poistuttaessa. Näin ollen käytetyn muistin yläraja vaihtelee sen mukaan mitä aliohjelmia on kesken suorituksen.
Dynaamisia muuttujia voidaan tarvittaessa luoda ja kun muistitilaa ei enää tarvita, voidaan vastaavat muuttujat vapauttaa:
^ muistin | kaytto | +--+ | +----+ | | +---+ | | | | | +-+ | | +--+ +----+ +---+ +-----+ | | | | | | +-------+----+----+--+---+-+---+------------> ohjelman malloc calloc malloc ohjelman alku free free realloc loppu freeNäin muistin maksimimäärä saattaa pysyä huomattavasti pienempänä kuin ilman dynaamisia muuttujia. Idea on siis siinä, että muistia varataan aina vain sen verran, kuin sillä hetkellä tarvitaan. Kun muistia ei enää tarvita, vapautetaan muisti.
Ajonaikana luotaviin muuttujiin tarvitaan osoitteet. Nämä osoitteet pitää sitten tallettaa johonkin. Talletus voitaisiin tehdä esimerkiksi taulukkoon tai sitten alkioista pitää muodostaa linkitetty lista.
Mikäli tilaa ei saada allokoitua, palautetaan NULL. Tämä pitää muistaa AINA tarkistaa!
Apuna alkion koon laskemisessa käytetään usein käännösaikaista operaattoria sizeof (ks. sizeof), joka palauttaa parametrinsa koon.
pKokonaislukuTaulu = malloc(20*sizeof(int));Esimerkiksi kerhon jäsenten osoitintaulukko voitaisiin luoda seuraavasti:
const char *cKerho::luo_taulukko(int koko) { jasenet = (cJasen **)malloc( koko * (sizeof(cJasen *)) ); jasenia = 0; max_jasenia = 0; if ( jasenet == NULL ) return EI_VOI_LUODA; max_jasenia = koko; return NULL; }Huomautus! malloc ei alusta varattua muistia mitenkään!
Esimerkiksi jäsentaulukko voitaisiin luoda myös kutsulla
jasenet = (cJasen **)calloc( koko , (sizeof(cJasen *)) );
Kun jäsenistöstä poistetaan jäsen, voidaan tämä tehdä vaikkapa seuraavasti:
void cKerho::poista_taulukko() { if ( max_jasenia > 0 ) free(jasenet); max_jasenia = 0; }
Jäsenen lisäyksessä voisimme tehdä myös seuraavasti, eli kasvatettaisiin jäsenistön kokoa 50%, mikäli jäsenistö tulee täyteen (cKerho jaettiin kahteen osaan: cKerho ja cJasenet):
//---------------------------------------------------------------------------- const char *cJasenet::kasvata_kokoa() /* ** Yritetään allokoida uutta tilaa 50% maksimimäärään nähden lisää. ** Paitsi jos vanha koko on 1, niin kasvatetaan 2:ksi. ** Jos vanha tila on 0, niin tehdään uusi tila. ----------------------------------------------------------------------------*/ { if ( max_lkm <= 0 ) return luo_taulukko(2); int uusi_koko = 3*(max_lkm)/2; if ( uusi_koko <= 1 ) uusi_koko=2; cJasen **uusi_tila = (cJasen **)realloc(alkiot,uusi_koko*sizeof(cJasen *)); if ( uusi_tila == NULL ) return LIIKAA_ALKIOITA; alkiot = uusi_tila; max_lkm = uusi_koko; return NULL; } ... if ( jasenia >= max_jasenia ) virhe = kasvata_kokoa(); if ( virhe ) return virhe;
Siis alkuvaiheessa realloc kannattaa ehkä unohtaa, mutta periaatteessa sillä voitaisiin korjata väärää kokomäärittelyä. Ja voidaanhan se aina lisätä jälkeenpäin kuten edellisessä esimerkissä! C++:ssa ei ole vastaavaa funktiota. Tämä olisikin osaltaan hankala toteuttaa, koska luomisessa pitää kutsua olion (olioiden) muodostajaa ja vastaavasti poistamisessa hajottajaa. realloc- tilanteessa voidaan koko muistialue joutua siirtämään toiseen kohti muistia ja kloonaamaan sitten sisällöt sinne. Olioiden tapauksessa tämä kloonaaminen ja sitten vanhojen poisto ei kuitenkaan ole aina yksikäsitteistä ja näin kielen tekijät ovat päätyneet siihen, että reallocia vastaavan toiminnon tekeminen jätetään ohjelmoijalle itselleen.
Esimerkiksi uusi jäsen lisättäisiin seuraavalla aliohjelmalla:
const char *cKerho::lisaa(const cJasen &jasen) { cJasen *uusi_jasen; if ( jasenia >= max_jasenia ) return LIIKAA_JASENIA; uusi_jasen = new cJasen(jasen); // uudelle jäsenelle jäsenen tiedot if ( uusi_jasen == NULL ) return EI_SAA_JASENTA; jasenet[jasenia] = uusi_jasen; jasenia++; return NULL; }Itse asiassa edellä on kutsuttu Jäsen- luokan kopiointimuodostajaa (copy constructor), joka luo uuden olion ja tekee siitä samalla sisällöltään samanlaisen kuin muodostajan parametrina viety olio. Kopiointimuodostajan parametrilistassa on tasan yksi parametri ja sen tyyppi on sama kuin luokan tyyppi.
void cKerho::poista_jasenet() { for (int i=0; i<jasenia; i++) delete jasenet[i]; jasenia = 0; }
pMonta = new cLuokka[20]; ... delete [] pMonta;Käytettäessä delete- operaattoria, kutsutaan jokaisen tuhottavan olion hajottajaa (ks. vähän myöhemmin). Tällä on se etu, että olion poistuessa muistista se voi samalla siivota mahdolliset muut jäljet oliosta. Esimerkiksi graafinen olio voi pyyhkiä itsensä pois näytöltä samalla kun se hävitetään.
Tavallinen taulukkokin voidaan usein esitellä vastaavasti
typedef struct { int max_koko; int lkm; int *alkiot; } Taulukko_tyyppi; ... Taulukko_tyyppi luvut; luvut.max_koko = 7; luvut.alkiot = calloc(7,sizeof(luvut.alkiot[0])); luvut.alkiot[0] = 0; luvut.alkiot[1] = 2; luvut.lkm = 2; Taulukko_tyyppi luvut
+-----+ max_koko | 7 | 0 1 2 3 4 5 6 lkm | 2 | +--------------------+ *alkiot | o--+------>| 0| 2| ?| ?| ?| ?| ?| +-----+ +--------------------+Tällaisen taulukon kuljettaminen parametrina on helppoa, kun se voidaan välittää vain yhtenä parametrina.
Viittausten puolesta samanalainen vastaava staattinen esittely olisi:
typedef struct { int max_koko; int lkm; int alkiot[7]; } Taulukko_tyyppiS; ... Taulukko_tyyppiS luvut = { 7, 2, {0,2} }; Taulukko_tyyppiS luvut
+-----+ max_koko | 7 | lkm | 2 | +-----+--------------+ alkiot | 0| 2| ?| ?| ?| ?| ?| +--------------------+ 0 1 2 3 4 5 6Staattisessa esittelyssä taulukko tulisi tietueen osaksi. Kuitenkin kummassakin tapauksessa viitataan taulukon yksittäiseen alkioon muodossa luvut.alkiot[3]=5;.
Staattisen esittelyn "hyvä" puoli olisi siinä, että tällainen taulukko voitaisiin sijoittaa toiseen taulukon yhdellä käskyllä:
Taulukko_tyyppiS apu,luvut = { 7, 2, {0,2} }; apu = luvut;Tietysti sijoitus onnistuu dynaamisessakin tapauksessa, mutta tulos saattaa olla muuta kuin on haluttu! Miksi?
Viimeksi esiteltyä staattistakin esittelyä voidaan käsitellä dynaamisesti luomalla muuttujat luvut dynaamisesti. Koska C- kielessä ei useinkaan ole indeksitarkistusta, niin luomisessa malloc - funktiolle voidaan "valehdella" alkion koko:
Taulukko_tyyppiS *luvut; ... luvut = malloc( sizeof(Taulukko_tyyppiS) + 4*sizeof(int) ); luvut- >max_koko = 7 + 4; ...Tosin koko taulukon sijoittaminen yhdellä käskyllä ei enää onnistu tämän tempun jälkeen! Miksi?
#include <iostream.h> #include <iomanip.h> class cTaulukko { int max_koko; int lkm; int *alkiot; public: cTaulukko(int akoko) { max_koko = 0; alkiot = new int[akoko]; if ( alkiot ) max_koko = akoko; lkm = 0; } ~cTaulukko() { if ( max_koko ) delete [] alkiot; max_koko = 0; } int lisaa(int luku) { if ( lkm >= max_koko ) return 1; alkiot[lkm] = luku; lkm++; return 0; } void tulosta(ostream &os=cout) const; }; void cTaulukko::tulosta(ostream &os) const { int i; for (i=0; i < lkm; i++) os << setw(5) << alkiot[i]; os << endl; } int main(void) { cTaulukko luvut(7); luvut.lisaa(0); luvut.lisaa(2); // Ilo on täällä!!! luvut.tulosta(); return 0; }Edellä olemme käyttäneet muutamia C++:n aikaisemmin käsittelemättömiä ominaisuuksia:
Hajottaja on parametriton ja tyypitön metodi, jonka nimi on ~Luokan_nimi. Vaikka muodostajia saattoi olla useita, on hajottajia aina VAIN YKSI luokkaa kohti.
Jos luokkaa on mahdollista periä, pitää hajottaja esitellä virtuaaliseksi.
class cKerho { ... void poista_kaikki() { poista_jasenet(); poista_taulukko(); } ... ~cKerho() { if ( muutettu ) talleta(); poista_kaikki(); } }; ...
void tulosta(ostream &os=cout) const;Näin voidaan tulostusvaiheessa valita mille laitteelle tulostetaan. Koska oletuksena on cout, tulostetaan näytölle jos kutsu on muodossa:
luvut.tulosta();Tiedostoon tulostettaisiin esimerkiksi:
ofstream fo("luvut.dat"); ... luvut.tulosta(fo);Oletusparametri tarkoittaa sitä, että mikäli kutsussa ei anneta jollekin parametrille arvoa, käytetään sille oletusarvoa. Ominaisuutta käytetään erittäin usein muodostajan yhteydessä.
Tietysti sama asia voidaan hoitaa funktioiden ja metodien kuormituksen avulla:
void tulosta(ostream &os) const; // tulostaa tiedostoon os void tulosta() const; // tulostaa näyttöönmutta tästä seuraa enemmän kirjoittamista.