Kirjautuminen

Haku

Tehtävät

Keskustelu: Ohjelmointikysymykset: C++: malloc

Sivun loppuun

punppis [19.07.2009 23:50:35]

#

Tässä kun on nyt tullut opiskeluta C++:aa, niin muutamassa esimerkkiohjelmassa on varattu muistia malloc-funktiolla. Miksi näin tehdään? Eikös muistia varata tarvittava määrä jo muuttujan määrittelyn yhteydessä? Ja vapautetaan sitten tietysti kun muuttuja tuhotaan lohkon tai ohjelman lopussa.

C++-ohjelmissa pitäisi vissiin varata tuota muistia new-operaattorilla ja vapauttaa delete-operaattorilla. En siltikään vielä ymmärrä, että miksi tämä pitäisi tehdä erikseen? Toinen juttu mitä en tajua on noi stackit ja globaali muistialue. Mitä näille voi koodari tehdä? Mulle muisti on muistia, ilman mitään rekistereitä ja stackeja. Joku osaa varmaan vähän selventää :P

Heikki [20.07.2009 01:31:30]

#

Muisti toki varataan automaattisesti kun muuttuja esitellään. Esim. int foo; varaa kokonaislukumuuttujan tarvitseman muistin.

Osoittimia käytettäessä muistia ei varata automaattisesti. Kun osoitin luodaan, se ei aluksi osoita mihinkään. Se voidaan sen jälkeen asettaa osoittamaan jo varattuun muistiin (esimerkiksi tavallisesti alustettuun muuttujaan) tai sille voidaan varata erikseen muistia. Tämä mahdollistaa esimerkiksi sellaisen tempun, että taulukon koko riippuu käyttäjän syötteestä:

int n;
cout << "Montako lukua tallennetaan?\n";
cin >> n;

// Luodaan osoitin joka asetetaan myöhemmin osoittamaan siihen kohtaan muistia, jonne luvut tallennetaan
int *luvut;
// Varataan sitten muistia n luvulle. Muista, että taulukko on käytännössä vain osoitin
luvut = new int[n];

// Nyt luvut-osoitinta voidaan käsitellä normaalina taulukkona

// Lopuksi varataan vapautettu muisti: käsin varattua muistia kun ei vapauteta automaattisesti!
delete[] luvut;

Metabolix [20.07.2009 09:04:36]

#

Pino (stack) on se muistialue, jolle funktioiden sisällä olevat muuttujat varataan, jossa välitetään funktioiden parametreja ja johon funktiokutsussa tallennetaan tieto, mistä kohti ohjelmaa suoritus taas jatkuu funktion jälkeen. Pinon kokoa on joissain käyttöjärjestelmissä rajoitettu, ja voit todeta tämän vaikka lisäämällä funktioon suuren taulukon, jolloin ohjelma saattaa kaatua funktiokutsuun:

void f() {
  int taulu[1234][12345];
}

Muu olennainen muisti on taas ohjelmoijan kannalta suunnilleen samaa. Staattisilla (globaaleilla) muuttujilla ja malloc-funktiolla (new-operaattorilla) kokoa rajoittavat käyttöjärjestelmän toteutus ja koneen muistin määrä. Raja voi olla esimerkiksi kaksi gigatavua, näin muistelisin ainakin Windowsista.

Käytännössä muistia ei kannata varata tuolla tavalla käsin. C++:n standardikirjastossa on kätevä luokka vector, jota voi käyttää taulukon tavoin ja jolle voi määrätä uuden koon. Se varaa muistia automaattisesti, ja muisti myös vapautuu normaalien sääntöjen mukaan eli silloin, kun vector-olio tuhoutuu lohkon lopussa.

Aiheesta on käyty täällä jo lukuisia pitkiä keskusteluja, joista yksi on tässä.

punppis [23.07.2009 20:10:16]

#

Onko siis käytännön hyöty tästä muistinvarauksesta vain taulukoiden kanssa (koska sitä alkioiden määrää ei voi jälkeenpäin muuttaa normaalisti)?

os [23.07.2009 20:46:37]

#

EDIT: Nyt ymmärsin mitä kysyit.

Esimerkiksi std::vector (kuten muutkin standardikirjaston tietorakenteet - ainakin jos et muuta Allocator-templaattiparametriä) käytännössä varaa muistin myös dynaamisesti (new:llä), joten kyse ei ole pelkästään taulukoiden muistinvarauksesta, vaan stack-heap-jaottelu koskee kaikkea muistivarausta. Kaikki suuret tai käännösaikana tuntemattoman kokoiset varaukset tehdään (pitäisi tehdä) käytännössä dynaamisesti eli heapista. C99 tosin mahdollistaa dynaamisten kokoisten pätkien varaamisen myös stackista.

Metabolix [24.07.2009 09:10:16]

#

On muitakin tietorakenteita kuin taulukoita, esimerkiksi puita, linkitettyjä listoja jne. Mutta kyllä, yleensä käytännön hyötyä on vain tapauksissa, joissa datan määrä voi vaihdella. Ainahan voi varata etukäteen niin suuren taulukon, että se varmasti riittää, mutta tämä taas on resurssien tuhlausta, jos sitä taulukkoa ei lopulta tarvitakaan kokonaan. Voi myös olla, että ohjelman eri osat ovat aktiivisia eri aikaan, jolloin voi kannattaa muistin säästämiseksi varata muistia vain sille osalle, jota kulloinkin käytetään. Esimerkiksi selaimet kuluttavat muistia sen mukaan, paljonko kuvia sivulla on ja kuinka monta sivua käyttäjällä on kerralla auki.

zokier [26.07.2009 03:56:05]

#

Imho dynaaminen muistin varaus on keskeinen asia jos käyttää pointtereita. new:illä saatu pointteri kelpaa missä päin prosessia tahansa, kunnes sen osoittama muisti vapautetaan deletellä.

Toinen etu new:in ja pointtereiden käyttämisessä on se että niiden avulla pystytään välttämään ne ongelmatilanteet joissa kaksi luokkaa sisältävät toisensa.

Vertaile esim seuraavaa kahta:

struct A {
	// Ei käänny koska B ei ole määritelty
	// Kääntäjän pitäisi tietää kuinka suuri b on
	// jotta voisi päätellä kuinka suuri A:n instanssi olisi
	// oliot ovat aina vakiokokoisia
	B b;
};

struct B {
	A a;
};
struct A;
struct B;

struct A {
	A() {
		b = new B();
	}
	~A() {
		delete b;
	}

	// b:n koko on nyt tunnettu, yhden pointterin verran
	// yleensä 4 tai 8 tavua
	// joten A:n instanssien koko on myös tunnettu
	// new:illä varattua muistia ei lasketa olion muistiksi
	B *b;
};

struct B {
	B() {
		a = new A();
	}

	~B() {
		delete a;
	}

	A *a;
};

Metabolix [26.07.2009 09:43:24]

#

zokier: Esimerkkisi on kohtalaisen epäonnistunut. Kummankin luokan sisällyttäminen toiseen nimittäin tarkoittaisi, että luokka sisältäisi epäsuorasti myös itsensä. Tilankäyttö olisi siis ääretön, koska A sisältäisi A:n, joka sisältäisi A:n, joka sisältäisi A:n... Aivan sama ongelma tulee new-operaattorin kanssa, paitsi että nyt ohjelma kääntyy ja kaatuukin vasta ensimmäiseen olion konstruktoriin (muisti loppuu, heitetään std::bad_alloc-poikkeus). Muutenkin tilanteessa, jota ehkä yritit tarkoittaa, oikeampi ratkaisu olisi käyttää viittauksia:

struct B;
struct A {
  B &b;
  A(B &_b): b(_b) {}
};

struct B {
  A a;
  B(): a(*this) {}
};

Tietysti jos jäsen A::b pitää pystyä vaihtamaan toiseen B-olioon jälkikäteen, osoitin on tarpeen. Tämä ei ilmeisesti kuitenkaan ollut esimerkkisi sisältö. Esimerkilläsi ei myöskään ole mitään tekemistä sinänsä dynaamisen muistinvarauksen kanssa: osoittimia olioihin voisi aivan hyvin ottaa staattisistakin yksilöistä, tai muistin voisi varata vector-luokan tai vastaavan avulla (tosin tämä olisi hassua, jos tarvittaisiin vain yhtä oliota).

koo [26.07.2009 13:18:08]

#

C++:ssa muistia varataan ja vapautetaan dynaamisesti new- ja delete-operaattoreilla. C:ssä sama tehdään malloc- ja free-funktioilla.

C++:ssa käytetään oikeastaan new- ja delete-lauseita, mutta ei sen nyt niin väliä. Juoni on kuitenkin se, että new varaa tarvittavan muistin JA alustaa luotavan olion. Vastaavasti delete kutsuu tuhottavan olion destruktoria ja vapauttaa muistin. Tämän takia C++:ssa EI kannata käyttää mallocia ja freetä.

Olioiden luomista ja tuhoamista tällä tavoin kutsutaan dynaamiseksi allokaatioksi, tai dynaamiseksi muistinhallinnaksi (johon C++:ssa siis liittyy myös konstruktorit ja destruktorit).

Dynaamista allokointia käytetään, kun tarvittavien olioiden lukumäärä tiedetään vasta ajon aikana. Tällöin ei ole pakko käyttää newiä ja deleteä itse, vaan on paljon turvallisempaa käyttää valmiita container-luokkia (deque, list, vector jne.).

Tärkeämpi syy käyttää dynaamista allokointia on, kun tarvittavien olioiden tyyppi tiedetään vasta ajon aikana. Tämä kytkeytyy vahvasti luokkien periytymiseen.

C++:ssa kolmas syy dynaamisen allokoinnin käyttöön on, kun olion elinkaarta halutaan hallita itse. Tavallisestihan oliot syntyvät ja kuolevat ohjelmalohkoissa tai funktioissa, jolloin ainoa tapa jatkaa niiden elämää on kopioida niitä funktioiden paluuarvoina. Kopiointi ei aina tule kysymykseen, jolloin dynaaminen allokointi ja osoittimien käyttö ratkaisee ongelman.

Ajoympäristön pinon kokorajoitus ei ole oikea syy. Dynaaminen allokointi voi jossain ympäristössä auttaa kiertämään jonkun rajoituksen (stack). Yhtä hyvin ajoympäristössä voisi olla kokorajoitus dynaamisen muistin määrälle (heap).

Pinolla (stack) tarkoitetaan C++:ssa sitä, kun muuttujat syntyvät ja tuhoutuvat automaattisesti funktioiden ja lohkojen sisällä. On sitten ihan erikseen se, miten tämä käyttäytyminen on ajoympäristössä toteutettu.

Dynaamisen allokaation yhteydessä muistivarausten sanotaan tapahtuvan keosta (heap). Kekokin on parasta ymmärtää käsitteellisenä mallina eikä minään tiettynä ajoympäristön toteutuksena.

Muuttujien, jotka on määritelty funktioiden ja luokkien ulkopuolella tai static-määreillä, sanotaan olevan globaalilla muistialueella. Määrittelyistä jä esittelyistä riippuen niihin voidaan viitata suoraan nimellä useammasta luokasta tai ohjelmalohkosta. Tämäkin on sitten mieluummin käsite- kuin ympäristön toteutusmalli.

Koodarin ei yleensä tarvitse "tehdä" mitään erityistä näille "muistialueille". Tarvittava toiminnallisuus ensisijaisesti ratkaisee, millaisia olioita kannattaa käyttää.


Sivun alkuun

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta