Kirjautuminen

Haku

Tehtävät

Koodit: Dynaaminen muistinvaraus; 2d-taulu, rakenteet (C)

Kirjoittaja: Metabolix

Kirjoitettu: 17.01.2008 – 17.01.2008

Tagit: koodi näytille, vinkki

Muistin varaaminen ajon aikana on tarpeen lähes ohjelmassa kuin ohjelmassa. C++ tarjoaa monia valmiita tietorakenteita, jotka auttavat hallitsemaan virheitä, mutta C-ohjelmoija on aivan oman taitonsa varassa. Ainoat välineet ovat malloc-johdannaiset ja free, ja näillä pitäisi saada varattua ja vapautettua erilaisia mutkikkaitakin tietorakenteita.

Jos tietorakenne sisältää vaikkapa kolme erillistä, eri mittaista taulukkoa, yleinen ratkaisu on varata niistä kukin erikseen malloc-funktiolla ja vapauttaa samoin. Kun rakennetta myöhemmin muuttaa niin, että taulukkoja onkin enemmän, on suuri riski unohtaa muuttaa jokin lukuisista kohdista, joissa muistia varataan tai vapautetaan. Helpoiten unohtuu muistin vapauttaminen, kun se ei edes estä ohjelman toimintaa — ainakaan heti. Eniten vaivaa aiheuttavat rakenteen luonnin aikana sattuneiden virheiden käsittelyt: mitkä osat rakenteesta onkaan tuossa tai tuossa vaiheessa jo varattu, mitä pitää vapauttaa?

Hyvä suunnittelu voi helpottaa asioita paljon. Kun muistin varaamiseen hieman panostaa, monet rakenteet on mahdollista varata yhdellä malloc-kutsulla, jolloin myös vapauttaminen onnistuu kerralla. Tällöin rakenteen muuttuessa tarvitsee muuttaa vain varauskoodia, mikä pienentää unohduksen mahdollisuutta huomattavasti. Toinen tätä tapaa puoltava seikka on todellisen varattavan muistin määrä: Jokainen malloc-kutsu kuluttaa muutaman tavun muistia kirjanpitoon. Tällöin esimerkiksi megatavun varaaminen kerralla kuluttaa muistia megatavun, mutta sama muistimäärä tavu kerrallaan varattuna viekin ehkä yllättäen jopa 16 megatavua muistia!

Seuraavista koodiesimerkeistä ensimmäinen sisältää funktion 2-ulotteisen taulun varaamiseen. Vapauttaminen onnistuu tavallisella free-funktiolla, mutta usein on selkeämpää kuitenkin käyttää erillistä funktiota tähänkin siltä varalta, että joskus päättääkin muuttaa toteutusta. Jälkimmäinen esimerkki esittelee, kuinka vastaavalla tavalla varataan tietue ja siihen liittyvät taulukot. Lopuksi vielä näytetään, että tämä kaikki todella toimii aivan odotusten mukaan.

Esimerkeissä käytetään calloc-funktiota, joka malloc-funktiosta poiketen myös nollaa muistin. Usein virheitä etsiessä on mukavampaa huomata taulun olevan täynnä nollia kuin huomata, että se sisältää ties mitä ihmeellisiä arvoja, koska jälkimmäisessä tapauksessa on epävarmempaa, mistä arvot ovat peräisin.

Kopeekan pyynnöstä lisäsin vinkkiin vielä muistin asettelun rakenteen koon mukaisesti, koska esimerkiksi SPARC-arkkitehtuurilla voi aiheutua ongelmia, jos double-tyyppinen muuttuja ei ala kahdeksalla jaollisesta muistipaikasta.

varaus_2d.c

#include <stdlib.h>

/**
* Varausfunktio; calloc nollaa muistin, siksi tämäkin on calloc_2d
**/
void **calloc_2d(size_t alkion_koko, size_t d1, size_t d2)
{
	void **ret;
	char *muisti;
	int i;
	size_t alin_bitti;

	/* Lasketaan muuttujien nimiä vastaavat tiedot. */
	size_t osoitinten_koko = d1 * sizeof(void*);
	size_t rivin_koko      = d2 * alkion_koko;
	size_t datan_koko      = d1 * rivin_koko;

	/* Etsitään alkion koon alin merkitsevä bitti, jotta data voi alkaa
	** sopivalta rajalta. Esim. SPARC-arkkitehtuuri ei salli double-muuttujaa
	** muualla kuin kahdeksalla jaollisesta muistipaikasta alkaen.
	** Suurennetaan osoitinalueen kokoa niin, että dataosio alkaa sopivasti. */
	alin_bitti = 1;
	while ((alkion_koko & alin_bitti) == 0) {
		alin_bitti <<= 1;
	}
	if (osoitinten_koko % alin_bitti != 0) {
		osoitinten_koko += alin_bitti - osoitinten_koko % alin_bitti;
	}

	/* Varataan koko muisti yhtenä palikkana, calloc myös nollaa muistin. */
	muisti = calloc(1, datan_koko + osoitinten_koko);

	/* Osoitin tauluun on osoitin tähän muistiin */
	ret = (void **) muisti;

	/* Dynaaminen 2D-taulu on taulukollinen osoittimia, jotka osoittavat
	** datariveihin. Varsinainen data alkaa osoitinten jälkeen. */
	muisti = muisti + osoitinten_koko;

	/* Osoittimet voidaan asettaa nyt helposti rivi kerrallaan niin,
	** että ne osoittavat rivin koon päähän toisistaan. */
	for (i = 0; i < d1; ++i) {
		ret[i] = muisti + i * rivin_koko;
	}

	return ret;
}

/**
* Vapautusfunktio
**/
void free_2d(void *t)
{
	if (t == NULL) {
		/* Ei taulua, ei vapautusta. */
		return;
	}

	/* Koska varaus tehtiin yhtenä palasena, myös vapautus pitää tehdä niin.
	** Tämä funktio on siis oikeastaan turha, kun normaalikin free toimii.
	** Koodissa on kuitenkin usein selkeämpää käyttää omille erikoisuuksille
	** omia funktioita. */
	free(t);
}

varaus_malli.c

#include <stdlib.h>

/**
* Rakenteet verteksille (kärkipisteelle), kolmiolle ja mallille
**/
struct verteksi {
	double x, y, z;
};

struct kolmio {
	int a, b, c;
};

struct malli {
	size_t vertekseja, kolmioita;
	struct verteksi *verteksit;
	struct kolmio *kolmiot;
};

/**
* Varausfunktio; calloc on hieman harhaanjohtava, kun palautettava
* rakenne ei ole täysin nollilla. Ajatus kuitenkin pätee.
**/
struct malli *calloc_malli(size_t vertekseja, size_t kolmioita)
{
	struct malli *ret;
	char *muisti;
	size_t alin_k, alin_v;

	/* Lasketaan valmiiksi osioiden koot: struct malli, verteksit, kolmiot. */
	size_t koko_m = sizeof(struct malli);
	size_t koko_v = sizeof(struct verteksi) * vertekseja;
	size_t koko_k = sizeof(struct kolmio) * kolmioita;

	/* Alimmat bitit, sama syy kuin taulukon kanssa, eli säädetään alueiden
	** kokoja, jotta ne alkavat sopivista muistipaikoista. */
	for (alin_v = 1; (sizeof(struct verteksi) & alin_v) == 0; alin_v <<= 1);
	if (koko_m % alin_v != 0) {
		koko_m += alin_v - koko_m % alin_v;
	}
	for (alin_k = 1; (sizeof(struct kolmio) & alin_k) == 0; alin_k <<= 1);
	if (koko_v % alin_k != 0) {
		koko_v += alin_k - koko_v % alin_k;
	}

	/* Varataan koko muisti yhtenä palikkana, calloc myös nollaa muistin. */
	muisti = (char *) calloc(1, koko_m + koko_v + koko_k);
	if (!muisti) {
		/* Virhe varauksessa! */
		return 0;
	}

	/* Itse rakenne alkaa varatun muistin alusta.
	** Verteksit alkavat siitä, mihin struct malli loppuu.
	** Kolmiot alkavat siitä, mihin verteksit loppuvat. */
	ret            = (struct malli *)    (muisti);
	ret->verteksit = (struct verteksi *) (muisti + koko_m);
	ret->kolmiot   = (struct kolmio *)   (muisti + koko_m + koko_v);

	/* Tarpeelliset tiedot, kun nämä kerran jo tiedetään */
	ret->vertekseja = vertekseja;
	ret->kolmioita = kolmioita;

	return ret;
}

/**
* Vapautusfunktio
**/
void free_malli(struct malli *malli)
{
	if (malli == NULL) {
		return;
	}
	free(malli);
}

varaus_esimerkki.c

#include <stdio.h>
#include <stdlib.h>

/* Tähän väliin tarvitaan yo. koodit tai sopivat otsikkotiedostot niistä. */
/* Tällainen include on ehdottomasti väärä ratkaisu! ;) */
#include "varaus_2d.c"
#include "varaus_malli.c"

/**
* Ylimääräinen rakenne testausta varten
**/
struct kulmio {
	double a, b;
};

int main(void)
{
	const int leveys = 5, korkeus = 7;
	int vertekseja = 4, kolmioita = 4;
	int i, j;
	struct kulmio **kulmiot;
	struct malli *malli;

	/* Varataan, käytetään normaalisti ja vapautetaan */
	kulmiot = (struct kulmio **) calloc_2d(sizeof(struct kulmio), leveys, korkeus);
	for (i = 0; i < leveys; ++i) {
		for (j = 0; j < korkeus; ++j) {
			kulmiot[i][j].a = i;
			kulmiot[i][j].b = j;
			printf("%6.1f ", kulmiot[i][j].a * kulmiot[i][j].b);
		}
		printf("\n");
	}
	free_2d(kulmiot);

	/* Varataan malli, käytetään normaalisti ja vapautetaan */
	/* Verteksien ja kolmioiden määrät voisi lukea vaikkapa tiedostosta */
	malli = calloc_malli(vertekseja, kolmioita);

	/* Kaikki on nollattu valmiiksi varausfunktiossa, asetetaan pari ykköstä vain */
	malli->verteksit[1].x = 1;
	malli->verteksit[2].y = 1;
	malli->verteksit[3].z = 1;

	/* Kolmiot; tetraedri */
	malli->kolmiot[0].a = 0; malli->kolmiot[0].b = 2; malli->kolmiot[0].c = 1;
	malli->kolmiot[1].a = 1; malli->kolmiot[1].b = 2; malli->kolmiot[1].c = 3;
	malli->kolmiot[2].a = 3; malli->kolmiot[2].b = 1; malli->kolmiot[2].c = 0;
	malli->kolmiot[3].a = 3; malli->kolmiot[3].b = 0; malli->kolmiot[3].c = 2;

#if 0
	/* Mallin voisi OpenGL:llä piirtää näin: */
	glBegin(GL_TRIANGLES);
	for (i = 0; i < malli->kolmioita; ++i) {
		glVertex3dv(&malli->verteksit[malli->kolmiot[i].a].x);
		glVertex3dv(&malli->verteksit[malli->kolmiot[i].b].x);
		glVertex3dv(&malli->verteksit[malli->kolmiot[i].c].x);
	}
	glEnd();
#endif

	/* Lopuksi vapautus. Tavallinen free toimii, mutta free_malli on selkeämpi */
	/* free(malli); */
	free_malli(malli);

	return 0;
}

Kommentit

hunajavohveli [17.01.2008 10:10:11]

#

Tämähän on varsin kätevää, että pannaan pointterit ja varsinainen data yhteen pötköön. Itse olen oman malloc_2d:ni toteuttanut niin, että varataan ensin taulukko pointtereille ja sitten haluttu määrä saraketaulukoita, kukin omalla malloc-kutsullaan, jolloin myös vapautusfunktiolle on aina pakko kertoa, montako taulukoita on. Tuokin on hyvä tietää, että mallocin käyttö varaa enemmänkin tavuja kuin varsinainen muistialue pitää sisällään. Käyttämäni datamäärät tosin ovat olleet sen verran suuria, että ylimääräiset n*15 tavua eivät olet juuri tuntuneet.

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta