Cpp taulukot,osoittimet ja referenssit
Mureakuha
Tässä luvussa tarkastellaan jo C:stä tuttuja käsitteitä: taulukot ja osoittimet. Nyt vain kyseessä on oliotaulukot ja olioiden osoitteet. Lisäksi kappaleessa esitellään C++:n eräs keskeinen ominaisuus: referenssi.
Sisällysluettelo |
Oliotaulukot
Todennäköisesti olet jo käyttänytkin oliotaulukoita, ainakin sellaisia, joiden konstruktoriin ei välitetä argumentteja. Tällaisissa tilanteissa toimitaan aivan samoin kuin vaikkapa integer-taulukon tapauksessa.
Ohjelmassa luodaan 4 alkioinen samp-taulukko, taulukon alkioiden a-jäsenten arvoiksi asetetaan arvot 0...3 ja lopuksi ne tulostetaan.
#include <iostream> class samp { int a; public: samp() { cout << "konstruktorissa\n"; } ~samp() { cout << "destruktorissa\n"; } void set_a( int n ) { a = n; } int get_a() { return a; } }; main() { samp ob[ 4 ]; int i; for ( i = 0; i < 4; i++ ) ob[ i ].set_a( i ); for ( i = 0; i < 4; i++ ) cout << ob[ i ].get_a() ; cout << "\n"; return 0; }
Ohjelma tulostaa:
konstruktorissa
konstruktorissa
konstruktorissa
konstruktorissa
0123
destruktorissa
destruktorissa
destruktorissa
destruktorissa
Taulukon alkiot voidaan alustaa, mikäli luokalla on konstruktori.
#include <iostream> class samp { int a; public: samp(int n) { a = n; cout << "konstruktorissa\n"; } ~samp() { cout << "destruktorissa\n"; } int get_a() { return a; } }; main() { samp ob[ 4 ] = { -1, -2, -3, -4 }; int i; for ( i = 0; i < 4; i++ ) cout << ob[ i ].get_a() << ' '; cout << "\n"; return 0; }
Ohjelma tulostaa:
konstruktorissa konstruktorissa konstruktorissa konstruktorissa -1 -2 -3 -4 destruktorissa destruktorissa destruktorissa destruktorissa
Alustuslauseke samp ob[ 4 ] = { -1, -2, -3, -4 }; on lyhennös pidemmästä muodosta: samp ob[ 4 ] = { samp( -1 ), samp( -2 ), samp( -3 ), samp( -4 ) }; Ensimmäinen tapa on tavallisempi ainakin 1-ulotteisilla taulukoilla. Lyhennysmuoto toimii vain jos konstruktorille välitetään 1 argumentti.
Taulukko voi olla moniulotteinen.
#include <iostream> class samp { int a; public: samp(int n) { a = n; cout << "konstruktorissa\n"; } ~samp() { cout << "destruktorissa\n"; } int get_a() { return a; } }; main() { samp ob[ 4 ] [ 2 ] = { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } }; int i, j; for ( i = 0; i < 4; i++ ) { for ( j = 0; j < 2; j++ ) cout << ob[ i ][ j ].get_a() << ' '; cout << "\n"; } return 0; }
Kun konstruktori haluaa enemmän kuin 1 argumentin, alustuksessa on käytettävä pidempää muotoa.
#include <iostream> class samp { int a, b; public: samp( int n, int m ) { a = n; b = m; } int get_a() { return a; } int get_b() { return b; } }; main() { samp ob[ 4 ] [ 2 ] = { { samp( 0, 0 ), samp( 0, 1 ) }, { samp( 1, 0 ), samp( 1, 1 ) }, { samp( 2, 0 ), samp( 2, 1 ) }, { samp( 3, 0 ), samp( 3, 1 ) } }; int i, j; for ( i = 0; i < 4; i++ ) { for ( j = 0; j < 2; j++ ) cout << ob[i][j].get_a() << ',' << ob[i][j].get_b() << ' '; cout << "\n"; } return 0; }
Ohjelma tulostaa:
0,0 0,1 1,0 1,1 2,0 2,1 3,0 3.1
Olio-osoittimien käyttö
Olio-osoittimia käytetään samoin kuin struktuuriosoittimia. Olion jäseneen viitataan -> operaattorilla pisteen sijasta. Osoitinaritmetiikkaa suoritetaan oliosta riippuen kuten tavallisilla muuttujillakin. Esimerkiksi jos x on osoitin integeriin ja integer vie kaksi tavua muistia, x++ osoittaa seuraavaan integeriin muistissa eli kaksi tavua eteenpäin. Vastaavasti, jos joku olio vie vaikkapa 5 tavua muistia ja y on osoite ko. olioon, ++y osoittaa 5 tavua eteenpäin ko. oliosta.
#include <stdio.h> class samp { int a, b; public: samp( int n, int m ) { a = n; b = m; } int get_a() { return a; } int get_b() { return b; } }; main() { samp ob[4] = { samp(1, 2), samp(3, 4), samp(5, 6), samp(7, 8) }; int i; samp *p; p = ob; // p = &ob[0]; for ( i = 0; i < 4; i++ ) { printf("%p: %d %d\n", p, p->get_a(), p->get_b() ); p++; } return 0; }
Ohjelma tulostaa:
3657:21D6: 1 2 3657:21DA: 3 4 3657:21DE: 5 6 3657:21E2: 7 8
this-pointer
C++:ssa on erityisosoitin, jonka nimi on this. this-pointteri välitetään automaattisesti jäsenfunktioon, kun jäsenfunktiota kutsutaan. this-pointter on osoite olioon, joka generoi kutsun. Tässä vaiheessa this-pointteria ei käytetä sen kummemmin mihinkään erityistarkoitukseen, nyt on oleellista ainoastaan ymmärtää, että sellainen on olemassa. Myöhemmin osoittimelle löytyy käteviä käyttötapoja.
Esimerkiksi:
... samp x, y; x.get_a(); // get_a-funktioon välitetään x:n osoite this-pointterin arvona y.set_a(); // set_a-funktioon välitetään y:n osoite this-pointterin arvona
Jäsenfunktiossa on mahdollista käyttää this-pointterin arvoa.
Seuraavassa pieni ohjelma sekä kommenteissa vastaavat ohjelman kohdat siten, että käytetään this-pointteria.
#include <stdio.h> #include <string.h> class inventory { char item[20]; double cost; int on_hand; public: inventory( char *i, double c, int o ) { strcpy( item, i ); // strcpy( this->item, i ); cost = c; // this->cost = c; on_hand = o; // this->on_hand = o; } void show() { printf("%s: %lf mk varastossa: %d kpl\n", item, cost, on_hand ); /* printf("%s: %lf mk varastossa: %d kpl\n", this->item, this->cost, this->on_hand ); */ } }; main() { inventory ob("vasara", 19.50, 6 ); ob.show(); return 0; }
new ja delete
Aiemmin dynaamisen muistin varauksen yhteydessä olemme käyttäneet C:n standardifunktioita malloc ja free muistin varaukseen ja vapautukseen. C++ tarjoaa helpomman ja turvallisemmankin tavan: muistia varataan new-operaattorilla ja vapautetaan delete operaattorilla.
pointteri = new tyyppi; delete pointteri;
tyyppi on sen tyypin nimi, jonka tyyppiselle muuttujalle halutaan varata muistia ja pointteri on osoitin muuttujaan, joka on tyyppi-tyyppinen.
Esimerkki: halutaan varata muistia integerille:
... // #include <stdlib.h> int *p; p = new int; // p = (int *)malloc( sizeof( int ) ); if (!p) return; *p = 5; cout << "\n" << *p; delete p; // free( p ); ...
Kuten malloc:ssa, jos muistia ei ole tarpeeksi new palauttaa nul-pointterin.
New:n ja deleten käytössä mallociin ja freehin nähden on joitain etuja:
- new varaa automaattisesti tarpeeksi muistia, ei tarvita sizeof..., eli yksi mahdollinen virhepaikka vähemmän.
- new palauttaa automaattisesti oikean tyyppisen osoitteen, ei tarvita tyyppimuunnosta (int *)
- new:ta ja deleteä voidaan kuormittaa, jolloin voit rakentaa oman muistinvarausjärjestelmän.
- new:n avulla varattu muisti voidaan alustaa.
- ei tarvita enää #include malloc.h tai stdlib.h lauseita
#include <iostream> class samp { int i, j; public: samp(){ cout << "\nKonstruktorissa"; } ~samp() { cout << "\nDestruktorissa"; } void set_ij( int a, int b ) { i = a; j = b; } int get_product() { return i*j; } }; main() { samp *p; p = new samp; if (!p) { cout << "\nVarausvirhe"; return 1; } p->set_ij( 4, 5 ); cout << "\nTulo on " << p->get_product() << "\n"; delete p; return 0; }
Ohjelma tulostaa:
Konstruktorissa
Tulo on 20
Destruktorissa
Lisää new- ja delete-operaattoreista
- Dynaamisesti varatuille olioille voidaan antaa alkuarvot.
- Voidaan luoda dynaamisesti taulukoita, joille ei kuitenkaan voi antaa alkuarvoja esittelyn yhteydessä.
Alkuarvon asetus luonnin yhteydessa: p-var = new type ( alkuarvo );
Esim. luodaan integer, jolle annetaan alkuarvo 5:
int *x; x = new integer (5 );
Varataan dynaaminen 1-ulotteinen taulukko: p-var = new type [ koko ]; Nyt p-var osoittaa koko-kokoisen 1-ulotteisen taulukon 1. alkiota
Taulukolle varatun muistin vapautus: delete [ koko ] p-var; HUOM! HUOM!
Edellä esitetty delete aiheuttaa sen, että jokaiselle taulukon alkiolle kutsutaan destruktori-funktiota. Itse varattu muisti vapautetaan vain kertaalleen. Jos koko-jätetään pois, muisti kyllä vapautetaan, mutta destruktoreita ei kutsuta (jos destruktoreita ei ole, koko on turha).
Ohjelma luo luokan koe olion ja taulukollisen olioita.
#include <iostream> #include <stdio.h> class koe { int i, j; public: koe( int a, int b ) { cout << "\nkonstr."; i = a; j = b; } koe() { cout << "\ndef. konstr."; i = 0; j = 0; } ~koe() { cout << "\ndestr"; } void set( int a, int b ) { i=a; j = b; } void show() { cout << "\ni=" << i << " j=" << j; } }; main() { koe *muuttuja, *taulu; int i; muuttuja = new koe ( 3,4 ); if (!muuttuja) { cout << "\nvarausvirhe"; return 1; } taulu = new koe[3]; if (!taulu) { cout << "\nvarausvirhe"; return 1; } muuttuja->show(); for (i = 0; i < 3; i++) taulu[i].show(); // Huomaa piste, ei nuoli // delete muuttuja; // delete [3] taulu; return 0; }
Ohjelma tulostaa:
konstr. def. konstr. def. konstr. def. konstr. i=3 j=4 i=0 j=0 i=0 j=0 i=0 j=0 Jos lopussa lisäksi delete lauseet -> destr. destr. destr. destr.
Jos jälkimmäisestä delete-lauseesta olisi jätetty koko [3] pois, destruktoria oltaisiin kutsuttu yhteensä vain 2 kertaa.
Referenssi
C++:ssa on referenssi-niminen ominaisuus, joka on "sukua" osoittimelle. Referenssi käyttäytyy kuten muuttujan toinen nimi, se on viittaus tiettyyn muuttujaan. Referenssiä voidaan käyttää:
- funktion argumenttina (tärkein käyttötapa)
- funktion palautusarvona (return)
- riippumattomana referenssinä
Seuraavassa esimerkissä käytetään vasemmalla puolella tavallista osoitinta ja oikealla puolella referenssiä funktion argumenttina:
#include <iostream> #include <iostream> void f ( int *n ); void f ( int &n ); main() main() { { int luku = 0; int luku = 0; f ( &luku ); f ( luku ); cout << "luku=" << luku << "\n"; cout << "luku=" << luku << "\n"; return 0; return 0; } } void f ( int *n ) void f ( int &n ) { { *n = 100; n = 100; } }
Referenssin hyödyt osoittimeen nähden:
* Funktion kutsussa ei tarvitse muistaa ottaa muuttujan osoitinta (&), muuttujan osoitin välitetään automaattisesti funktioon. * Funktioliittymä on siistimpi, elegantimpi (mielipide?). * Kun funktioon välitetään olio referenssinä, ei tehdä kopiota.
Olioiden referenssien välittäminen
Kuten muistanet aiemmasta, kun olio välitetään funktioon arvoparametrina, oliosta tehdään kopio. Vaikka kopiolle ei kutsuta konstruktoria, destruktoria kutsutaan. Tästä saattoi olla vakavia seurauksia tilanteissa, joissa destruktori sisälsi esimerkiksi muistin vapautuksia.
Edellä olleen ongelman eräs ratkaisu on käyttää referenssejä arvoparametrien sijasta: kopiota ei tehdä, eikä näin ollen myöskään destruktoria kutsuta.
HUOMAA: Referenssi ei ole osoitin, siis käytetään pistettä ( . ) ei nuolta ( -> ).
Seuraavassa rinnakkain sama ohjelma muuten, mutta vasemmalla funktiota kutsutaan tavallisella arvoparametrilla ja oikealla käytetään referenssiä.
#include <iostream> #include <iostream> class myclass { class myclass { int who; int who; public: public: myclass ( int n ) { myclass( int n ) { who = n; who = n; cout << "konstr: " << who << "\n"; cout << "konstr: " << who } } ~myclass() ~myclass() { { cout << "destr: " << who; cout << "destr: " << who; cout << "\n"; cout << "\n"; } } int id() { return who; } int id() { return who; } }; }; void f ( myclass o ) void f ( myclass &o ) { { cout << "f:ssä " << o.id(); cout << "f:ssä " << o.id(); cout << "\n"; cout << "\n"; } } main() main() { { myclass x( 1 ); myclass x ( 1 ); f( x ); f( x ); return 0; return 0; } }
konstr: 1 konstr: 1 f:ssä 1 f:ssä 1 destr: 1 destr: 1 destr: 1
Referenssin palauttaminen funktiosta
Funktio voi palauttaa referenssin. Tämä on erittäin käyttökelpoinen tietyissä operaattoreiden kuormitustilanteissa. Näistä kuitenkin valitettavasti vasta myöhemmin. Referenssiä voidaan kuitenkin käyttää siten, että funktio voi olla sijoituslausekkeen vasemmalla puolella. f() = 5; Omituista vai mitä.
#include <iostream> int x; int &funktio() { return x; } main() { funktio( ) = 100; cout << x << "\n"; return 0; }
Tässä funktio palauttaa referenssin kokonaislukuun, tarkemmin sanoen globaaliin muuttujaan x. funktion sisällä lauseke return x; ei palauta x:n arvoa vaan oikeastaan x:n osoitteen referenssin muodossa. Tällöin main:ssa lauseke funktio() = 100; sijoittaa arvon 100 kyseiseen osoitteeseen eli muistipaikkaan x.
Kun palautetaan referenssi johonkin, on oltava tarkkana, että kyseisen muuttujan voimassaoloalue yltää myös kutsukohtaan. Esimerkiksi, jos x olisi ollut funktion sisällä määritelty muuttuja, sen voimassaoloalue olisi päättynyt funktion suorituksen loppuun, eli sijoitus main:ssa olisi tapahtunut muistialueelle, joka ei enää olisi ollut käytettävissä. Siis seuraava olisi väärin:
W int &funktio() W { W int x; W return x; W }
Ainakin Borland C++ 4.5 havaitsee asian jo käännösvaiheessa: Attempting to return a reference to local variable ...
Eräs tapa käyttää funktiota, joka palauttaa referenssin, löytyy esimerkiksi indekstit tarkistavasta taulukkotyypistä. C:ssä ja C++:ssa ei tehdä automaattisesti mitään indeksien järkevyystarkistuksia käsiteltäessä taulukoita.
Esim.
... int i; int x[5]; for (i=-100; i<100; i++) x[i] = 0; ...
menee nätisti kääntäjästä läpi.
C++:ssa voidaan luoda oma luokka: taulukko, jossa suoritetaan indeksien raja-arvotarkistukset. Taulukko-luokka sisältää kaksi ydinfunktiota: funktio joka tallettaa arvon taulukon alkiolle ja funktio, joka lukee arvon taulukon alkiosta. Nämä funktiot voivat tarkistaa ajonaikana, ettei raja-arvoja rikota.
// raja-arvot tarkistava char-taulukko #include <iostream> #include <stdlib.h> class charArray { int size; char *p; public: charArray( int num ); char &put( int i ); // ei vie taulukkoon mitään palauttaa, //referenssin haluttuun alkioon char get( int i ); // palauttaa alkion halutusta kohdasta }; charArray::charArray( int num ) { p = new char [ num ]; if (!p) { cout << "\nVarausvirhe\n"; exit(1); } size = num; } char &charArray::put( int i ) { if ( i < 0 || i >= size ) { cout << "\nraja-arvo virhe\n"; exit(1); } return p[ i ]; } char charArray::get( int i ) { if ( i < 0 || i >= size ) { cout << "\nraja-arvo virhe\n"; exit(1); } return p[ i ]; } main( ) { charArray a( 10 ); a.put( 3 ) = 'X'; a.put( 2 ) = 'R'; cout << a.get( 3 ) << " " << a.get( 2 ) << "\n"; a.put( 10 ) = 'S'; // Raja-arvovirhe return 0; }
Raja-arvojen tarkistusta on syytä käyttää, mikäli on olemassa todellinen mahdollisuus raja-arvojen rikkomiseen, johtuen jostain ulkoisesta seikasta, jota ei olla voitu etukäteen ohjelmoida koodiin. Aina raja-arvotarkistuksia ei kannata käyttää, sillä ne tietenkin hidastavat ohjelman suoritusta, kun jokainen taulukkoviittaus tarkoittaa erillisen funktion kutsumista. Riippumattomat referenssit ja referenssien rajoitukset Vaikkakin riippumatonta referenssiä ei yleensä käytetä ja itse asiassa sen käyttöä tulee välttää, sellaisen luominen on mahdollista. Riippumaton referenssi on kaikin tavoin toinen nimi muuttujalle. Koska referenssiin ei voi sijoittaa arvoa, referenssi on aina alustettava esittelyn yhteydessä (kerrottava, minkä muuttujan toinen nimi se on ).
Kaikkiin referensseihin liittyy joukko rajoituksia:
- Referenssi ei voi viitata toiseen referenssiin
- Referenssistä ei voi ottaa osoitetta
- Ei voida luoda referenssi-taulukoita
- Referenssi on alustettava esittelyn yhteydessä, paitsi jos referenssi on luokan jäsen tai referenssi palautetaan funktiosta tai välitetään funktioon argumenttina Seuraavassa esimerkki riippumattomasta referenssistä:
#include <iostream> main() { int x; int &ref = x; int &ref2 = 99; x = 10; ref = 100; cout << x << " "<< ref << "\n"; x = ref2; cout << x << " "<< ref << "\n"; return 0; }
Tulostaa:
100 100 99 99
copy-konstruktori
Aikaisemmin havaittiin, että välitettäessä funktioon olio tai palautettaessa funktiosta olio ja yleisesti sellaisissa tilanteissa, joissa järjestelmä luo automaattisesti väliaikaisen olion, kutsuttiin luokan destruktoria muttei konstruktoria. Tästä aiheutui ongelmia, mikäli destruktorissa oli esimerkiksi muistin vapautuksia.
Eräs ratkaisu ongelmaan on se, että suunnitellaan luokka siten, ettei destruktorissa tarvitse vapauttaa muistia. Usein tällainen toteutus on kuitenkin vaikea.
Parempi ratkaisu asiaan on kuormittaa konstruktorifunktiota ns. copy-konstruktorilla. Copy konstruktori on aina tiettyä muotoa ja järjestelmä tunnistaa sen siten, että väliaikaisille olioille kutsutaan automaattisesti kyseistä konstruktoria.
Copy-konstruktori on muotoa: X( const X& )
#include <iostream> #include <stdio.h> class samp { int a; public: samp( int x ) { a = x; printf("konstr: %p %d\n", this, a ); } // copy konstruktori // samp( const samp &z ) { // a = z.a; // printf("copy-konstr: %p %d\n", this, a ); // } ~samp() { printf("destr: %p %d\n", this, a ); } int get_a() { return a; } void set_a(int x) { a = x; } }; samp nelio( samp x ) { samp tulos(-1); tulos.set_a ( x.get_a() * x.get_a() ); return tulos; } main () { samp ob(5); samp tulos( 0 ); tulos = nelio( ob ); printf("%p: tulos.a = %d\n", &tulos, tulos.get_a() ); return 0; }
Testitulokset vasemmalla ilman copy-konstruktoria ja oikealla copy-konstruktorin kanssa
Ohjelma tulostaa: Ohjelma tulostaa: konstr: 09CF : 2278 5 konstr: 34F7:22A2 5 konstr: 09CF : 2276 0 konstr: 34F7:22A0 0 konstr: 09CF : 2246 -1 copy-konstr: 34F7:227E 5 (argum.) destr: 09CF : 2246 25 konstr: 34F7:2270 -1 destr: 09CF : 2254 5 (argum.) copy-konstr: 34F7:229E 25 (return) destr: 09CF : 2274 25 (return) destr: 34F7:2270 25 09CF: 2276: tulos.a = 25 destr: 34F7:227E 5 (argum.) destr: 09CF : 2276 25 destr: 34F7:229E 25 (return) destr: 09CF : 2278 5 34F7:22A0: tulos.a = 25 destr: 34F7:22A0 25 destr:34F7:22A2 5
Kopio lisenssistä (englanniksi) löytyy täältä.
Alkuperäinen (c) Petteri Hämäläinen
