Kirjautuminen

Haku

Tehtävät

Keskustelu: Ohjelmointikysymykset: C++: Osoittimien käyttö C++

Sivun loppuun

alottelijaa [26.02.2009 13:40:09]

#

Olen tässä miettinyt mikä ohjelmassani menee vikaan, kun välillä se kaatuu muistia vapauttaessa. Laitan tähän koodin jossa olen samalla lailla varannut/käyttänyt/vapauttanut muistia kuin ohjelmassani.

class esimerkki{
int a, b;
T* osoitin;
 public:
  esimerkki();
  ~esimerkki();
};

esimerkki::esimerkki()
{
  a = 0;
  b = 1;
  osoitin = new T; //jos laitan tähän vaikka T[b] varautuuko muistia sitä mukaa kun b kasvaa?
}

esimerkki::~esimerkki()
{
  osoitin = 0; //ohjelma kaatuu ilman tätä, mutta nyt muisti jää vapauttamatta?
  delete [] osoitin;
}

Käyttö:
int arvo = 4; //oletetaan että T on int
osoitin[++a] = arvo;

E: selkeyden vuoksi en laittanut kaikkea mallien tarvitsemaa koodia

petrinm [26.02.2009 14:44:29]

#

alottelijaa kirjoitti:

//jos laitan tähän vaikka T[b] varautuuko muistia sitä mukaa kun b kasvaa?

Ei, vaan muistia varataan sen verran kuin b:n arvo on kun varaaminen tehdään. Taulukon kokoa on mahdollista muuttaa käskyllä realloc tai käyttämällä C++:n std::vectoria tai vastaavaa. Näistä vectorien käyttö on helpompaa ja turvallisempaa.

alottelijaa kirjoitti:

//ohjelma kaatuu ilman tätä, mutta nyt muisti jää vapauttamatta?

Jos käytät dynaamista taulukkoa(osoitin = new T[b];), niin tuhoaminen on tehtävä aina käskyllä delete[] osoitin;. Jos taas käytät tavallista osoitin = new T; dynamaamista luontia, niin on käytettävä delete osoitin; käskyä. Näiden käyttö väärinpäin muistaakseni aiheuttaa ongelmia.

Tuhoamisen kohdalla, jos määräät osoittimen kohdaksi 0, hävität muistinkohdan, jonka olet alussa varannut etkä voi tämän jälkeen enää lukea, kirjoittaa tai tuhota/vapauttaa sitä virheittä, koska olet yksinkertaisesti kadottanut sen.

Metabolix [26.02.2009 15:24:43]

#

petrinm kirjoitti:

Taulukon kokoa on mahdollista muuttaa käskyllä realloc

Tämä tietenkin pätee vain *alloc-funktioilla varattuihin muistialueisiin. Luokan muodostin ja tuhoajat jäävät myös ajamatta, koska C:ssä sellaisia ei ole.

petrinm kirjoitti:

tai käyttämällä C++:n std::vectoria tai vastaavaa.

Suosittelen samaa. Itse asiassa suosittelen vector-luokan käyttöä silloinkin, kun muistialueen kokoa ei edes tarvitse muuttaa. Tällöin se nimittäin vapautuu automaattisesti, kun vector tuhoutuu. Luokka itsessään ei juuri sisällä jäsenmuuttujia, joten ylimääräistä tilaa ei kulu kuin muutamia tavuja.

petrinm jo mainitsi asiasta, mutta selvennän vielä: Koodissasi laitat nollan osoittimeen ennen tuhoamista, kun tämä tietenkin pitää tehdä vasta tuhoamisen jälkeen — mistä se ohjelma muka tietää, mitä pitää tuhota, jos siinä on joka tapauksessa jo nolla? :)

alottelijaa [26.02.2009 22:01:14]

#

petrinm kirjoitti:

Ei, vaan muistia varataan sen verran kuin b:n arvo on kun varaaminen tehdään.

Kävelyllä ollessani ajattelin asiaa ja tulin samaan tulokseen, sen takia siis ohjelma kaatui joka 3. - 4. ajokerralla :)

Kiitos vastanneille.

goala [27.02.2009 16:18:09]

#

Jos

osoitin = new T();

niin silloin destructorissa

delete osoitin;

Jos taas

osoitin = new T[x];

niin silloin destructorissa

delete [] osoitin;

Noiden deletejen jälkeen vasta

osoitin = NULL;

koo [27.02.2009 17:12:22]

#

goala kirjoitti:

Noiden deletejen jälkeen vasta osoitin = NULL;

Vähän turhaa touhua tuo nullittaminen, jos kerran ollaan destruktorissa.

Gaxx [27.02.2009 17:15:29]

#

koo kirjoitti:

goala kirjoitti:

Noiden deletejen jälkeen vasta osoitin = NULL;

Vähän turhaa touhua tuo nullittaminen, jos kerran ollaan destruktorissa.

Mielummin laittaa ne joka kerta, kuin että jättää laittamatta siellä missä niiden on joko pakko tai hyvä olla.

koo [27.02.2009 18:16:15]

#

Gaxx kirjoitti:

Mielummin laittaa ne joka kerta, kuin että jättää laittamatta siellä missä niiden on joko pakko tai hyvä olla.

Tässä nyt ei ole pakko nollata eikä nollaamisesta tässä koidu mitään erityistä hyvääkään. Destruktorissa olio osoittimineen on joka tapauksessa häviämässä. Muissa yhteyksissä nollaaminen voi olla täysin tarpeellista tai sitten se voi jopa johdatella harhaan. Mieluummin ei käytä mitään joka-kerta-sääntöä, vaan toimii järkevästi tilanteen mukaan.

class esimerkki {
  T* ptr;
public:
  esimerkki() {
    ptr = new T;
  }
  ~esimerkki() {
    delete ptr;
    ptr = 0; // turhaa koodia, koska ptr häviää muutenkin
  }
};

void huh() {
  T* ptr = new T;
  delete ptr;
  ptr = 0; // tarpeellinen, koska ptr jää henkiin...
  if (ptr) {
    // ... ja sille on mahdollista käyttöä
  }
}

void hah(T* p) {
  delete p;
  p = 0; // harhaanjohtava, sillä...
}

void hei()
{
  T* ptr = new T;
  hah(ptr);
  if (ptr) {
    // ... se voi luoda vääriä kuvitelmia
  }
}

Vai jäikö minulta jotakin huomaamatta?

TsaTsaTsaa [27.02.2009 18:29:53]

#

Onhan se pointteri sikäli hyvä nollata aina jos pointteri jatkaa vielä elämistään, niin sitten ainakin ohjelma kaatuu, mikäli vahingossa osoittelee olemattomiin, mutta jos ei ole nollattu, niin voi olla että ohjelma näyttää toimivansa oikein, vaikka ei toimikaan, ja koodausvirhe voi jäädä huomaamatta.

koo [27.02.2009 18:38:37]

#

Olisiko tästä jokin esimerkki, joka ei vielä ole noissa esittämissäni kolmessa tapauksessa mukana. Noista kolmesta kun vain yksi oli sellainen, että nollaamisesta on hyötyä, koska se suorastaan kuuluu toimintalogiikkaan. Kahdessa muussa tapauksessa nollaaminen oli siis turhaa tai haitallista.

TsaTsaTsaa [27.02.2009 19:10:08]

#

No tällainen aika keinotekoinen esimerkki, mutta vastaavaa voi toki tapahtua oikeissakin ohjelmissa:

#include <iostream>
#include <string>

using namespace std;

const int KOKO = 4;

void teeStringit(string* taulu[]);
void tulostaStringit(string* taulu[]);
void funktio(string* taulu[]);

int main()
{
   string* taulu[KOKO] = { 0 };
   teeStringit(taulu);
   tulostaStringit(taulu);
   funktio(taulu);
   tulostaStringit(taulu);
   return 0;
}

void teeStringit(string* taulu[])
{
   for (int i = 0 ; i < KOKO ; ++i)
   {
      taulu[i] = new string(i+1, 'a');
   }
}

void tulostaStringit(string* taulu[])
{
   for (int i = 0; i < KOKO; ++i)
   {
      if ( taulu[i] ) // tarkistetaan osoittaako pointteri johonkin
      {
         cout << *taulu[i] << endl;
      }
   }
}

void funktio(string* taulu[])
{
   for (int i = 0; i < KOKO ; ++i)
   {
      if ( *taulu[i] == "aa" )
      {
         delete taulu[i];
         taulu[i] = 0; // tämän jos jättää pois, niin
                       // tulostaStringit-funktiossa söhitään
                       // olemattomaan dataan
      }
   }
}

Kokeilin ajaa tuon ilman nollausta, ja ohjelma kyllä kaatui nätisti segmentation faultiin heti virheellisen osoituksen seurauksena...En tiedä käykö niin aina, mutta jos käy, niin sitten nollauksella ei ole merkitystä sillä tavalla mitä edellisessä viestissä väitin.

koo [28.02.2009 11:32:28]

#

Näyttäisi oleva kovasti sama juttu kuin tuo esittämäni kakkos- eli huh-tapaus. Kannattaa vain vähän miettiä sitäkin, miten helposti tällaisesta tuleekin kolmos- eli hah-hei-tapaus, jos vain ääliönä toistaa mantraa, että on niin kovin hyvä aina nollailla ne pointterit eikä "seuraa peliä".

Gaxx [28.02.2009 20:19:15]

#

Ideana tuossa pointterin nollaamisessa jokaisen deleten jälkeen on, ettei sitä voida käyttää sen jälkeen vahingossakaan(esim. ohjelmointivirheen takia). Minulle on toki yksi ja sama, nollaatteko te ne pointterit vai ette — en minäkään niitä nollaa aivan selvissä tapauksissa.

koo: En ymmärtänyt miten hah-hei tapaus puolustaa väitettä, ettei pointteria tulisi nollata joka kerta. Mitä tarkoitat kommentilla "... se voi luoda vääriä kuvitelmia"?

Metabolix [28.02.2009 20:32:58]

#

Gaxx kirjoitti:

koo: En ymmärtänyt miten hah-hei tapaus puolustaa väitettä, ettei pointteria tulisi nollata joka kerta. Mitä tarkoitat kommentilla "... se voi luoda vääriä kuvitelmia"?

Koodi viitannee tyypilliseen aloittelijan harhaluuloon, että muuttujan muuttaminen funktiossa vaikuttaisi sen arvoon funktion ulkopuolella. Tämähän ei tietenkään pidä paikkaansa, kun funktiolle välitetään vain muuttujan arvo eikä itse muuttujaa. (Viittausten kanssa on tietenkin toisin.) Siispä aloittelija voi sortua kuvittelemaan, että osoitin olisi turvallisesti nollattu delete-rivin jälkeen, vaikka todellisuudessa kyseinen rivi ei vaikuta osoittimen arvoon siellä, mistä funktiota kutsuttiin.

Antti [28.02.2009 22:03:35]

#

Lähtökohtaisesti suositus on välttää osoittimien käyttöä jos vain mahdollista - päädyin samaan luettuani tämän teksti ketjun. Ei kannata edes puhua niistä jos ei ole pakko. :D

Metabolix... eikös asia ole juuri toisin päin? Osoittimessa siirretään muuttujan muistiosoite funktiolle. Muutettaessa osoittimen viittaamaa muistialuetta muutetaan arvoa funktion ulkopuolella. Asiasi sanoit kyllä oikein, mutta älä sekoita muuttujaa osoittimeen. (aloittelijan virhe :))

Kysehän on osoittimen tuhoamisesta - eli sen sisältämän muistialueen osoituksen (int) siirtämisestä viittaamaan epäkelpoon osoitteeseen (0), joka vastaa käytännössä null'ia - ei siis muuttujan arvon muuttamisesta.

Metabolix [28.02.2009 22:42:26]

#

Antti kirjoitti:

Metabolix... eikös asia ole juuri toisin päin? Osoittimessa siirretään muuttujan muistiosoite funktiolle.

Luitko aiempaa keskustelua lainkaan? Viestissäni lukee "ei vaikuta osoittimen arvoon", ja nyt on todellakin puhe siitä, minne osoitin osoittaa, eikä kyseisen muistialueen sisällöstä. Tässä tapauksessa siis muuttuja on tyypiltään osoitin, ja osoittimen muuttaminen funktiossa nollaksi ei muuta sitä funktion ulkopuolella:

void f(int *p) {
  p = 0;
  // Funktio muuttaa parametria, ei sen sisältöä eikä myöskään
  // parametriksi annettua muuttujaa kutsuvassa funktiossa.
}
void g() {
  int i;
  int *p = &i;
  f(p);     // Paikallinen p ei muutu miksikään, joten...
  *p = 123; // i = 123;
}

Antti kirjoitti:

Kysehän on osoittimen tuhoamisesta - eli sen sisältämän muistialueen osoituksen (int) siirtämisestä viittaamaan epäkelpoon osoitteeseen (0), joka vastaa käytännössä null'ia - ei siis muuttujan arvon muuttamisesta.

En ymmärrä, mitä tarkoitat. Osoitin tai sen osoittama muistialue eivät tuhoudu nollan sijoittamisesta. Muistialue "tuhotaan" eli vapautetaan delete-operaattorilla, (paikallinen) osoitin taas tuhoutuu normaalien sääntöjen mukaisesti näkyvyysalueensa lopussa aivan kuten mikä tahansa muukin (paikallinen) muuttuja.

koo [01.03.2009 01:29:08]

#

Gaxx kirjoitti:

En ymmärtänyt miten hah-hei tapaus puolustaa väitettä, ettei pointteria tulisi nollata joka kerta. Mitä tarkoitat kommentilla "... se voi luoda vääriä kuvitelmia"?

Voisiko joku nyt kertoa, että MIKSI osoittimet olisi pakko tai edes hyvä nollata aina deleten jälkeen? Minä kun olen rautakangesta vääntäen yrittänyt näyttää, milloin nollaaminen on turhaa, tarpeellista tai haitallista.

Haitallisimmillaan deleten jälkeinen nollaaminen voi aiheuttaa väärän kuvitelman, että nollaaminen estäisi kaikki mahdolliset osoittimen avulla tehtävät väärät viittaukset. Näin ei kuitenkaan ole, kun osoitin ei ole ainoa viittaus tuhottuun olioon. Tsatsatsaan esimerkissä ei tarvitse kuin tehdä aa-tarkastuksesta oma funktionsa, niin on jo riski, että homma menee kiville. Miksi siis vaivautua?

Joka ainoa koodirivi on potentiaalinen virhelähde, erityisesti ylimääräiset koodirivit. Enkä tarkoita tässä sitä, että pitäisi kirjoittaa mahdollisimman tiiviitä ja salaperäisiä koodiloitsuja.

Voidaan jopa pitää erikoistapauksena sitä, jolloin nollaamisesta on hyötyä: Se on silloin, kun osoitin jää henkiin ja se on ainoa viite tuhottuun olioon. Silloin osoittimen nollaaminen on tosiaan osa toimintalogiikkaa. Metabolixinkin kommentit saattavat hieman valaista asiaa niille, jotka osaavat C++:aa.

Sinänsä minullekin on ykshailee, nollataanko pointtereita aina vai ei: Eihän se mahdottomasti cpu-tehoja syö enkä itse ole mukana projekteissa, joissa niin käsketään tehdä. Olisiko parasta nollata käytön jälkeen vielä vaikka kaikki int-muuttujatkin, ettei vain tule laskuvirheitä?

Gaxx [01.03.2009 13:02:27]

#

Heh, tein näemmä aloittelijan virheen tuon hah-hei tapauksen ajattelemisessa. Ilmeisesti olen sitten edelleen aloittelija :). Harvinainen tapaus kyllä omalla kohdallani, mutta viimeksi muistan painiskeelleeni tovin vastaavanlaisen tapauksen kanssa(siihen ei tosin liittynyt deletointia tai nollausta).

koo kirjoitti:

Voisiko joku nyt kertoa, että MIKSI osoittimet olisi pakko tai edes hyvä nollata aina deleten jälkeen? Minä kun olen rautakangesta vääntäen yrittänyt näyttää, milloin nollaaminen on turhaa, tarpeellista tai haitallista.

Oma ajattelumallini on varmasti lähtöisin kouluni ohjelmistotekniikan laitoksen c++ tyylioppaasta (luku 10.1.4). Sieltä löytyy myös edellisessä viestissäni mainitsema perustelu hieman eri sanoin.

Minusta tuo hah-hei tapaus ei ole kuitenkaan sellainen peruste, jonka takia yleispätevään nyrkkisääntöön kannattaisi tehdä poikkeus. Jos funktion tarkoituksena on tuhota pointterin osoittama muisti ja nollata pointteri, ainahan sen osoittimen voi välittää viittauksena.

Olisihan se toki hienoa, jos olisi olemassa "ohjelmointityyli", jolla voisi välttää kaikki virheet.

koo [01.03.2009 19:07:26]

#

Siis kun tyyliohje noin sanoo, niin siihen ei sitten mitkään perustelut tai esimerkit oikein auta. Viittaamasi ohje sisältää aika paljon ohjelmoinnin ohjeistusta ollakseen nimeltään tyyliohje. Mukana on oikein hyviäkin sääntöjä, mutta aika monen pykälän kanssa on käsittämättömän helppo olla eri mieltä. Esimerkiksi olioiden sijoitusoperaattorin toteutuksen sisältö ei oikein vastaa nykykäytäntöjä. Private-perinnän kieltäminen ja vaatimus puhtaasta rajapintaperiyttämisestä taas haittaavat turvallisten kirjastopalveluiden toteuttamista.

Silläkin uhalla, että tästä saa tosi nipottajan maineen, niin jauhetaan nyt tästä aiheesta vielä. Itsensä Stroustrup on myös sivunnut tätä problematiikkaa, lähinnä siltä kannalta, että jos nollaaminen tekisi autuaaksi, niin sehän voisi olla ihan delete-operaattorin ominaisuus.

Mitä tulee tuon tyylioppaan perusteluun tässä nimenomaisessa nippelikysymyksessä, niin sehän kuuluu että:

C++-tyyliopas kirjoitti:

Tuhotun olion käyttö myöhemmin saman osoittimen läpi tehdään näin mahdottomaksi.

Katsotaanpa... class esimerkki -tapauksessa delete on destruktorissa. Destruktori on tosi harvoin muutamaa riviä pidempi, joten jos siellä meinaa käyttää otusta vielä deleten jälkeen, niin koodarilla taitaa olla sellaisia ongelmia, joita ei osoittimen nollaaminenkaan ratkaise. Ja kun kerran ollaan destruktorissa, niin eikös olion käyttö saman osoittimen läpi pitäisi olla muutenkin mahdotonta, kun osoittimen sisältänyttä oliotakaan ei enää ole? Tai sitten koodarilla on viisarit niin sekaisin, että voiko mihinkään muuhunkaan enää luottaa.

Sitten oli se huh-tapaus. Siinähän nollittaminen varta vasten oli osa toimintalogiikkaa. Nollaamista ei tehdä virheiden välttämiseksi vaan ylipäätänsä sitä varten, että valitaan oikea toimintavaihtoehto.

Lopuksi sitten hah-hei-tapauksessa kuvitellaan helposti, että tuhottuun olioon viittaaminen estyy. Joo, niin estyykin siinä funktiossa, mutta jos funktio on - niin kuin ne järkevässä koodissa yleensä ovat - korkeintaan parinkymmenen rivin mittainen, niin siellä yleensä näkee yhdellä silmäyksellä muutenkin, mitä deleten jälkeen ollaan tekemässä. Niin kuin käydystä keskustelustakin käy ilmi, saattaa vain niin kovin helposti jäädä huomaamatta, että nollittamamme pointteri ei sitten ollutkaan ainoa viittaus tuhottuun olioon. Muualla olion tuhoutumisesta ei tiedetty yhtikäs mitään, vaikka tyyliohje miten viuhuisi.

Parempi ohje voisi olla vaikka tällainen: Vältä dynaamisen muistinhallinnan suoraa käyttöä. Jos on tosiaan tarpeen luoda olio dynaamisesti tyyliin new T, älä sijoita tulosta paljaaseen pointterimuuttujaan, vaan aina tyyppiä std::shared_ptr tai std::auto_ptr olevaan muuttujaan. Jos tarkoitus on luoda taulukollinen olioita tyyliin new T[N], älä tee niin, vaan käytä taulukkona luokkaa std::vector. Älä koskaan tuhoa varaamiasi olioita deletellä.

Vieläkö muuta?

Gaxx [01.03.2009 21:22:13]

#

Olen näemmä taas saanut luisutettua keskustelun sivuraiteille heittämällä puolivakavan kommentin, joten yritänpä nyt lopettaa tämän.

Korjataan vielä, että viittaus tyylioppaaseen ei ollut tarkoitettu varsinaiseksi argumentiksi. Näin jälkikäteen ajateltuna siitä saattaa helposti saada sellaisen käsityksen.

Arvostan toki kokeneempieni mielipiteitä ja voin yhtyä tässä keskustelussa esittettyihin suurimmalta osin.

koo kirjoitti:

Vieläkö muuta?

Vielä tekisi mieli mainita yksi asia nollauksesta, mutta jätänpä mainitsematta, jotta tämä melko hyödytön vääntö saadaan päätökseen :)

alottelijaa [01.03.2009 21:26:18]

#

Nyt loppuu :P

ps: tiesin kyllä että new..new[] ja deleten vastaavia joutuu käyttämään niiden vastaavien parien kanssa, mutta koitin kaikenlaista ja lopulta olin niin epätoivoinen että heitin mitä sattuu :).

Myöhemmin sain toimimaan.


Sivun alkuun

Vastaus

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

Tietoa sivustosta