previous next Title Contents Index

16. Dynaaminen muistinkäyttö


Mitä tässä luvussa käsitellään?

* Dynaaminen muistinhallinta

* Dynaamiset taulukot

* Hajottaja (destructor)

Syntaksi:

	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();    

Yhdessä käytettävät funktiot tai operaattorit:

	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).

16.1 Muistin käyttö

Karkeasti ottaen tavallisen ohjelman muistinkäyttö näyttäisi ajan funktiona seuraavalta:

            ^
muistin     |     
kaytto      |    +-------------------------------+ 
            |    |                               |
            |    |                               |
            |    |                               |
            |    |                               |
            |    |                               |
            |    |                               |
            |    |                               |
            |    |                               |
            |    |                               |
            +------------------------------------------->
                ohjelman                        ohjelman
                alku                            loppu
Edellinen 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
                                           free
Nä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.

16.2 Dynaamisen muistin käyttö C- kielessä

Vaikka seuraavassa käsitelläänkin asioita C:n ja C++:n ominaisuuksia rinnakkain, pitää muistaa käyttää samaan muuttujaan kohdistuen aina "saman sarjan" operaatioita (malloc-free tai new-delete, muttei esimerkiksi: malloc- delete).

16.2.1 malloc, C

Tilaa voidaan varata malloc- funktiolla. Funktiolle viedään parametrina haluttu koko tavuina ja funktio palauttaa muistista löytyneen alueen alkuosoitteen.

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:

runko.1\kerho.cpp - jäsentaulukon luominen

	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!

16.2.2 calloc, C

calloc toimii lähes kuten malloc, mutta se on tarkoitettu pääasiassa taulukoiden varaamiseen. callocille ilmoitetaan taulukon alkioiden määrä ja koko (huomaa 2 parametria, mallocissa vain 1). calloc alustaa kunkin taulukon alkion nollia täytteen. Ei kuitenkaan ole syytä uskoa, että osoitintaulukko tulisi täyteen NULL osoittimia tai reaalilukutaulukko täytteen 0.0 lukuja. Toisaalta merkkijonoista tulee tyhjiä ja kokonaislukutaulukoista täynnä 0:ia olevia taulukoita.

Esimerkiksi jäsentaulukko voitaisiin luoda myös kutsulla

	jasenet = (cJasen **)calloc( koko , (sizeof(cJasen *)) );

16.2.3 free, C

Varattu muistitila pitää aina vapauttaa, kun sitä ei enää tarvita. Ohjelman lopuksi tietenkin kaikki ohjelman aikana varattu muistitila vapautuu. Kuitenkin usein aliohjelmien pitää varata itselleen työtilaa ja aliohjelman lopuksi tämä työtila pitää vapauttaa. Tällöin aliohjelmasta ei voida poistua return - lauseella, vaan pitää tehdä goto vapauta tai vastaava hyppy aliohjelman loppuun.

Kun jäsenistöstä poistetaan jäsen, voidaan tämä tehdä vaikkapa seuraavasti:

runko.1\kerho.cpp - jäsenistön poistaminen

	void cKerho::poista_taulukko()
	{
	  if ( max_jasenia > 0 ) free(jasenet);
	  max_jasenia = 0;
	}

16.2.4 realloc, c

Funktioilla malloc tai calloc varattua muistitilaa voidaan tarvittaessa muuttaa realloc - funktiolla. Funktiolle viedään parametrina muistitilan osoite ja haluttu uusi koko. Käytännössä realloc voi toimia siten, että se ensin varaa kokonaan uuden muistitilan ja tämän jälkeen kopioi vanhan muistitilan vastaavan osan (kaikki tai osan jos pienennetään) uuteen paikkaan. Funktio palauttaa uuden paikan osoitteen tai NULL, mikäli koon muuttaminen ei onnistu.

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):

talletus.2\jasenet.cpp - taulukon koon kasvattaminen

	//----------------------------------------------------------------------------
	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;

16.2.5 Ole varovainen reallocin kanssa

reallocin kanssa on kuitenkin oltava erittäin huolellinen, koska kaikki allokoitavassa osassa olevat kentät saattavat siirtyä uuteen paikkaan. Mikäli meillä on osoittimia allokoitavan alueen sisälle, on niillä reallocin jälkeen täysin väärät arvot!

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.

Tehtävä 16.149 Jäsenistön realloc?

Onko kerhon jäsenistön kanssa em. realloc ongelmia?

16.3 Dynaamisen muistin käyttö C++:ssa

16.3.1 new, C++

C++:ssa tilan varaamisen kannattaa ilman muuta käyttää new- operaattoria. Tässä on se suuri etu, että luonnin yhteydessä kutsutaan kunkin luotavan olion muodostajaa ja näin mikään syntyvistä olioista ei jää ilman alkuarvoa. Ainoastaan luotaessa perustietotyyppien mukaisia muuttujataulukoita, voidaan puolustella mallocin käyttöä, tällöinkin vain jos taulukon kokoa pitää jälkeenpäin muuttaa.

Esimerkiksi uusi jäsen lisättäisiin seuraavalla aliohjelmalla:

runko.1\kerho.cpp - jäsenen lisääminen

	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.

16.3.2 delete, C++

Jos olio on luotu new- operaattorilla, pitää se poistaa delete- operaattorilla:
	void cKerho::poista_jasenet()
	{
	  for (int i=0; i<jasenia; i++)
	    delete jasenet[i];
	  jasenia = 0;
	}

16.3.3 Taulukon luominen new [] ja tuhoaminen delete []

Jos new- operaattorilla on luotu taulukko (luonnissa käytettiin hakasulkeita), pitää vastaavasti tuhoaminen suorittaa delete- operaattorin taulukkoversiolla
	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.

16.4 Dynaamiset taulukot

Kerhon jäsenrekisterissä käytettiin osoitintaulukkoa dynaamisesti. Vastaavan rakenteen tarve tulee usein ohjelmoinnissa. Tällöin tulee aina vastaan ongelma: montako alkiota taulukossa on nyt ja montako sinne mahtuu? Jäsenrekisterissä tämä oli ratkaistu cJasenet- luokassa tekemällä sinne kentät, joista nämä rajat selviävät.

Tavallinen taulukkokin voidaan usein esitellä vastaavasti

dyna\taul_d.c - esimerkki dynaamisesta taulukosta

	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:

dyna\taul_s.c - staattinen taulukko

	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  6 
Staattisessa 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?

16.5 Dynaamiset taulukot C++:ssa

C++:ssa edällä mainittu dynaaminen taulukko voidaan toteuttaa käyttäjän kannalta todella joustavaksi:

dyna\taul_d.cpp -esimerkki dynaamisesta taulukosta

	#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:

16.6 Hajottaja (destructor)

Muodostaja ja hajottaja (destructor) ovat eräs olio- ohjelmoinnin kulmakiviä. C++:n lisäetuna (?) on vielä automaattisesti kutsuttavat muodostajat ja hajottimet. Eli esimerkiksi edellisessä esimerkissä (taul_d.cpp) kutsutaan automaattisesti olion luvut hajottajaa silloin, kun olion vaikutusalue lakkaa, eli poistutaan tässä tapauksessa pääohjelmasta.

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.

16.6.1 Jäsenistön poisto

Esimerkiksi kerhossa on jäsenistö poistettava kun kerho lakkaa olemasta:

runko.1\kerho.cpp - hajottaja

	class cKerho {
	...
	  void  poista_kaikki() { poista_jasenet(); poista_taulukko();            }
	...
	  ~cKerho() { 
	    if ( muutettu ) talleta(); 
	    poista_kaikki();     
	  }
	};
	...
	

16.7 Tietovirta parametrina ja oletusparametri

Metodi tulosta on esitelty
	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öön
mutta tästä seuraa enemmän kirjoittamista.


previous next Title Contents Index