Cpp luokat tarkemmin
Mureakuha
Sisällysluettelo |
Olion sijoitus
Olion arvo voidaan sijoittaa toisen olion arvoksi, mikäli molemmat ovat samaa tyyppiä (saman luokan olioita). Jos olio A sijoitetaan olioon B, kaikki A:han liittyvä data sijoitetaan B:n dataksi vastaaviin datajäseniin.
#include <iostream> class myclass { int a, b; public: void set( int i, int j ) { a = i; b = j; } void show( ) { cout << a << ' ' << b << "\n"; } }; main ( ) { myclass o1, o2; o1.set( 10, 4 ); o2 = o1; // sijoitus o1.show(); // tulostaa 10 4 o2.show(); // tulostaa 10 4 o2.set( 20, 5 ); o1.show(); // tulostaa 10 4 o2.show(); // tulostaa 20 5 o1.set( 15, 3 ); o1.show(); // tulostaa 15 3 o2.show(); // tulostaa 20 5 return 0; }
On tärkeää havaita, että sijoitusoperaattori kopioi ainoastaan olion datan, oliot ovat edelleen täysin itsenäisiä. Edellisessä esimerkissä olion o1 datalle on jossain päin muistia varattu tilaa 2 kokonaisluvulle ja olion o2 datalle toiset 2 kokonaislukua.
Sijoituslauseen molempien operandien on oltava täsmälleen samaa tyyppiä. Ei riitä, että luokkien datajäsenet ovat samoja, olioiden on kuuluttava täsmälleen samaan luokkaan.
Kaikki data kopioidaan, myös esimerkiksi taulukoiden sisällöt. Seuraavassa pino-esimerkissä pinoon p1 on viety merkkejä. Pinoon p2 ei ole viety mitään push-operaatiolla, mutta sijoitus p2 = p1 aiheuttaa sen, että kaikki data pinosta p1 kopioidaan pinoon p2.
#include <iostream> #define KOKO 10 class pino { char puskuri[ KOKO ]; int vapaa; public: pino() { cout << "Konstruktorissa\n"; vapaa = 0; } void push( char ch ); char pop(); }; void pino::push( char ch ) { if ( vapaa == KOKO ) { cout << "Täynnä"; return; } puskuri[ vapaa ] = ch; vapaa++; } char pino::pop() { if ( vapaa == 0 ) { cout << "Tyhjä"; return 0; } vapaa--; return puskuri[ vapaa ]; } main() { pino p1, p2; int i; p1.push( 'a' ); p1.push( 'b' ); p1.push( 'c' ); p2 = p1 ; // kloonaus for ( i = 0; i < 3; i++ ) cout << "pop p1: " << p1.pop() << "\n"; for ( i = 0; i < 3; i++ ) cout << "pop p2: " << p2.pop() << "\n"; return 0; }
Sijoitusoperaattoreiden kanssa on oltava varovainen. Jos esimerkiksi luokan jäsenenä on osoitin ja konstruktorissa varataan tilaa ko. osoitteeseen, sijoitusoperaattori kopioi vain kyseisen osoittimen, eli molemmat osoittavat samaan varattuun muistialueeseen.
Seuraavassa on virhe:
#include <iostream> #include <stdio.h> #include <malloc.h> #include <string.h> #include <stdlib.h> class mjono{ char *p; int len; public: mjono( char *ptr ); ~mjono(); void show(); }; mjono::mjono( char *ptr ) { len = strlen( ptr ); p = (char *) malloc( len + 1 ); if ( !p ) { cout << "Varausvirhe\n"; exit( 1 ); } printf( "Varattu %d tavua osoitteesta %x\n", len+1, p ); strcpy( p, ptr ); } mjono::~mjono() { printf("Vapautetaan puskuri osoitteesta %x\n", p ); free( p ); } void mjono::show() { cout << p << " - pituus: " << len; cout << "\n"; } main() { mjono mj1("Testi"), mj2("C++ on kivaa"); mj1.show(); mj2.show(); mj1 = mj2; // tämä aiheuttaa virheen! mj1.show(); mj2.show(); return 0; }
Kun mj1 ja mj2 luodaan, molemmat varaavat muistia merkkijonoilleen. Varattujen alueiden osoittimet on talletettu p-jäseniin. Kun mjono-olio tuhotaan (destruktorissa), varattu muisti vapautetaan. Kun mj2 sijoitetaan mj1:een, molemmat p:t saavat samat arvot, siis molemmat osoittavat muistialuetta, joka alunperin varattiin mj2:lle (C++ on kivaa). Osoitin varatulle muistialueelle, joka sisältää tekstin "Testi", ei ole enää ohjelman käytössä, eli ko. aluetta ei voida enää vapauttaa. Kun mj1 tuhoutuu, se vapauttaa "C++ on kivaa"-puskurin ja kun mj2 tuhoutuu, se yrittää vapauttaa saman puskurin.
Olion välittäminen funktioon argumenttina
Olio voidaan viedä funktioon argumenttina aivan kuten kaikki muutkin muuttujat. Oletusarvoisesti olio välitetään arvoparametrina ( vaikka funktiossa muutettaisiin argumentin arvoa, muutokset eivät näy kutsuvassa kohdassa ohjelmaa).
#include <iostream> class samp { int i; public: samp( int n ) { cout << "konstruktorissa\n"; i = n; } ~samp() { cout << "destruktorissa\n";} int get_i() { return i; } }; int nelio( samp o ) { return o.get_i() * o.get_i(); } int main() { samp a(10), b(9); cout << "1.\n"; cout << nelio( a ) << "\n"; cout << "2.\n"; cout << nelio( b ) << "\n"; cout << "3.\n\n"; return 0; }
Ohjelma tulostaa:
konstruktorissa konstruktorissa 1. destruktorissa 100 2. destruktorissa 81 3. destruktorissa destruktorissa
Voit ihmetellä jo nyt, miksi destruktoria on kutsuttu 4 kertaa ja konstruktoria vain 2 kertaa. Asia selvitetään hetken kuluttua.
HUOM! Mikäli käytössäsi on Borland C++ 2.x, testitulostukset tulevat hieman eri järjestyksessä: myös funktion sisäiset muuttujat tuhotaan vasta main:n lopussa.
Arvoparametrien yhteydessä kutsuargumentista otetaan kopio, jota käytetään itse funktiossa. Jos funktion sisällä muutetaan argumentin arvoa, muutos ei näy kutsuvassa kohdassa ohjelmaa.
#include <iostream> class samp { int i; public: samp( int n ) { cout << "konstruktorissa\n"; i = n; } ~samp() { cout << "destruktorissa\n";} int get_i() { return i; } void set_i( int n ) { i = n; } }; int nelio( samp o ) { int temp; temp = o.get_i() * o.get_i(); o.set_i( 0 ); cout << "funktiossa asetetaan kopiolle arvo " << o.get_i() << "\n"; return temp; } int main() { samp a(10); cout << "a ennen kutsua: " << a.get_i() << "\n"; cout << "a:n nelio: " << nelio( a ) << "\n"; cout << "a kutsun jälkeen: " << a.get_i() << "\n"; return 0; }
Tulostaa:
konstruktorissa a ennen kutsua: 10 funktiossa asetetaan kopiolle arvo 0 destruktorissa a: nelio: 100 a kutsun jälkeen: 10 destruktorissa
Jos argumenttina välitetään osoite, alkuperäistä arvoa voidaan muuttaa.
#include <iostream> class samp { int i; public: samp( int n ) { cout << "konstruktorissa\n"; i = n; } ~samp() { cout << "destruktorissa\n";} int get_i() { return i; } void set_i( int n ) { i = n; } }; int nelio( samp *o ) { int temp; temp = o->get_i() * o->get_i(); o->set_i( 0 ); cout << "funktiossa asetetaan arvo " << o->get_i() << "\n"; return temp; } int main() { samp a(10); cout << "a ennen kutsua: " << a.get_i() << "\n"; cout << "a:n nelio: " << nelio( &a ) << "\n"; cout << "a kutsun jälkeen: " << a.get_i() << "\n"; return 0; }
Tulostaa:
konstruktorissa a ennen kutsua: 10 funktiossa asetetaan kopiolle arvo 0 destruktorissa a: nelio: 100 a kutsun jälkeen: 0 destruktorissa
Kun funktioon välitetään argumenttina olio, luodaan uusi väliaikainen olio, jonka arvoksi kopioidaan kutsuolion arvo. Kun kyseisen funktion suoritus päättyy, luotu väliaikainen olio tuhotaan.
- Kutsutaanko kopion luonnissa konstruktoria ?
- Kutsutaanko kopion tuhoamisessa destruktoria ?
VASTAUS: Konstruktoria ei kutsuta mutta destruktoria kutsutaan !!!
Konstruktoria ei kutsuta, sillä funktioon halutaan välittää olion sen hetkisen tilan kopio, ei mitään alustustilaa, mikä syntyisi konstruktorissa. Konstruktoriahan käytetään alustustarkoituksiin.
Katso nyt aiempia ohjelmia, joissa näkyy, että kopiolle ei kutsuta konstruktoria, mutta kutsutaan destruktoria.
Se seikka, että konstruktoria ei kutsuta, mutta destruktoria kutsutaan, voi olla potentiaalinen virheen aiheuttaja: Jos konstruktorissa varataan muistia ja destruktorissa on tarkoitus vapautta kyseinen muisti, päädytään virhetilanteeseen (kääntäjä ei toki huomaa mitään). Kun tällainen olio välitetään funktioon, luodaan kopio, jossa on tn. kerrottuna alkuperäiselle oliolle varatun muistilohkon osoite. Funktiosta poistuttaessa vapautetaan alkuperäiselle oliolle varattu muisti, sillä nyt kutsutaan kopion destruktoria ja kopio viittaa samaan muistilohkoon alkuperäisen kanssa.
Olion palauttaminen funktiosta return lauseella
Olio voidaan palauttaa funktiosta määrittelemällä funktio olion tyyppiseksi ja palauttamalla saman tyyppinen olio return lauseella.
#include <iostream> #include <string.h> class samp { char s[80]; public: samp() { cout << "konstruktorissa\n"; } ~samp() { cout << "destruktorissa\n"; } void show() { cout << s << "\n"; } void set( char *str ) { strcpy( s, str ); } }; samp input() { char s[80]; samp temp; cout << "2.\n"; cout << "Anna merkkijono: "; cin >> s; temp.set( s ); cout << "3.\n"; return temp; } main() { samp ob; cout << "1.\n"; ob = input(); cout << "4.\n"; ob.show(); return 0; }<ccode> ohjelma tulostaa: <ccode>konstruktorissa 1. konstruktorissa 2. Anna merkkijono: abc 3. destruktorissa destruktorissa 4. abc destruktorissa
Huomaa taas yksi ylimääräinen destruktori: tästä myöhemmin
Kun olio on saatu palautettua kutsuvaan kohtaan ohjelmaa, palautetun olion voimassaolo päättyy eli kutsutaan luokan destruktoria. Jos nyt destruktorissa esimerkiksi vapautetaan muistia, tästä seuraa virhetilanne.
// Ohjelmassa on suunnitteluvirhe #include <iostream> #include <stdlib.h> #include <stdio.h> #include <string.h> class dyna2 { char *p; public: dyna2 () { p = 0; cout << "konstruktorissa\n"; } ~dyna2(); void show() { cout << p << "\n"; } void set( char *str); }; void dyna2::set( char *str) { p = (char *) malloc( strlen( str ) ); if (!p) { cout << "Varausvirhe\n"; exit(1); } cout << "merkkijononasetus\n"; printf("varataan: %p\n", p ); strcpy( p, str ); } dyna2::~dyna2() { cout << "destruktorissa\n"; if (p) { printf("vapautetaan: %p\n", p ); free( p ); } } dyna2 input() { char s[80]; dyna2 temp; cout << "2.\n"; cout << "Anna merkkijono: "; cin >> s; temp.set( s ); cout << "3.\n"; return temp; } main() { dyna2 ob; cout << "1.\n"; ob = input(); cout << "4.\n"; return 0; }
ohjelma tulostaa:
konstruktorissa (ob) 1. konstruktorissa (temp) 2. Anna merkkijono: abc merkkijononasetus varataan: 33DF:00F6 3. destruktorissa (temp) vapautetaan: 33DF:00F6 destruktorissa (väliaikainen, näkymätön) vapautetaan: 33DF:00F6 4. abc destruktorissa (ob) vapautetaan: 33DF:00F6 Null pointer assignment
Ylimääräisistä destruktoreista:
Ensimmäinen konstruktorin kutsu tulee main-funktion alussa ob:n luonnista. Toinen konstruktorin kutsu tulee input-funktiossa temp:n luonnista. Kun funktio palauttaa jotain return-lauseella, ennen funktion päättymistä luodaan väliaikainen käyttäjälle näkymätön muuttuja, jossa palautusarvo välitetään kutsuvaan ohjelman kohtaan. Väliaikaisen muuttujan destruktorifunktiota kutsutaan, kun arvo on saatu välitettyä kutsuvaan ohjelmakohtaan. Kun input funktio päättyy, luodaan väliaikainen näkymätön muuttuja, johon kopioidaan temp:n arvo ja paikalliset muuttujat tuhotaan, eli temp tuhotaan, siis kutsutaan destruktoria temp-muuttujalle. Kun väliaikaisen muuttujan arvo on saatu välitettyä kutsuvaan ohjelman kohtaan, väliaikainen muuttuja tuhotaan eli kutsutaan destruktoria. Ohjelman päättyessä tuhotaan vielä pääohjelmassa luotu ob muuttuja eli kutsutaan sille destruktoria.
Friend-funktiot esittely
Joskus on tarvetta funktiolle, joka ei ole luokan jäsenfunktio, mutta kuitenkin funktiolla on pääsy luokan privaatteihin muuttujiin. Tällainen funktio on ns. friend-funktio. Friend-funktioita käytetään pääasiassa operaattoreiden kuormitustilanteissa ( myöhemmin ) sekä tietyissä I/O-funktioissa (myöhemmin). Friend-funktiot ovat käyttökelpoisia myös tilanteissa, joissa funktion pitää käsitellä kahden tai useamman luokan privaattijäseniä.
Friend-funktio määritellään tavalliseksi ei-jäsenfunktioksi. Sen luokan määrittelyssä, jonka friend-funktiosta on kysymys, kerrotaan kyseinen friend-funktio suhde:
#include <iostream> class myclass { int n, d; public: myclass( int i, int j ) { n = i; d = j; } friend int isfactor( myclass ob ); }; int isfactor( myclass ob ) { if ( !( ob.n % ob.d ) ) return 1; else return 0; } main() { myclass ob1( 10, 2 ), ob2( 13, 3 ); if ( isfactor( ob1) ) cout << "2 is a factor of 10\n"; else cout << "2 is not a factor of 10\n"; if ( isfactor( ob2) ) cout << "3 is a factor of 13\n"; else cout << "3 is not a factor of 13\n"; return 0; }
Huomaa, että luokan jäsenfunktio (myclass-konstruktori) viittaa privaatteihin jäseniin suoraan (n ja d) Friend-funktion on viitattava olio.jäsen, sillä kutsuttaessa friend-funktiota siihen liittyvää oliota ei ole mitenkään kiinnitetty kuten jäsenfunktioiden tapauksessa.
Koska friend-funktioita ei ole kiinnitetty mihinkään olioihin, niihin voidaan välittää useita olioita argumentteina.
Friend-funktiot eivät periydy: Jos luokka B perii luokan A ja A:lla on friend-funktio, B ei peri friend-funktiota.
Friend-funktio voi olla usean luokan friend-funktio.
Eräs tilanne friend-funktion käyttöön on se, kun usealla eri luokalla on jokin yhteinen ominaisuus, jota joudutaan vertailemaan eri luokkiin kuuluvien olioiden kesken.
#include <iostream> class kuormuri; // etukäteen esittely class auto_ { int matkustajat; int nopeus; public: auto_( int m, int n ) { matkustajat = m; nopeus = n; } friend int auto_nopeampi( auto_ a, kuormuri k ); }; class kuormuri { int paino; int nopeus; public: kuormuri( int p, int n ) { paino = p; nopeus = n; } friend int auto_nopeampi( auto_ a, kuormuri k ); }; int auto_nopeampi( auto_ a, kuormuri k ) { return a.nopeus - k.nopeus; } main() { int nopeusero; auto_ a1( 6, 110 ), a2( 2, 220 ); kuormuri k1( 10000, 100 ), k2( 20000, 140 ); cout << "\nverrataan a1:stä ja k1:stä: "; nopeusero = auto_nopeampi( a1, k1 ); if ( nopeusero >= 0 ) cout << "a1 nopeampi kuin k1\n"; else cout << "a1 hitaampi kuin k1\n"; cout << "\nverrataan a1:stä ja k2:stä: "; nopeusero = auto_nopeampi( a1, k2 ); if ( nopeusero >= 0 ) cout << "a1 nopeampi kuin k2\n"; else cout << "a1 hitaampi kuin k2\n"; cout << "\nverrataan a2:stä ja k1:stä: "; nopeusero = auto_nopeampi( a2, k1 ); if ( nopeusero >= 0 ) cout << "a2 nopeampi kuin k1\n"; else cout << "a2 hitaampi kuin k1\n"; cout << "\nverrataan a2:stä ja k2:stä: "; nopeusero = auto_nopeampi( a2, k2 ); if ( nopeusero >= 0 ) cout << "a2 nopeampi kuin k2\n"; else cout << "a2 hitaampi kuin k2\n"; return 0; }
Funktio voi olla jonkin luokan jäsen ja samalla toisen luokan friend. Seuraavassa sama kuin edellä paitsi että auto_nopeampi-funktio on auto_:n jäsen ja kuormurin friend.
#include <iostream> class kuormuri; // etukäteen esittely class auto_ { int matkustajat; int nopeus; public: auto_( int m, int n ) { matkustajat = m; nopeus = n; } int auto_nopeampi( kuormuri k ); }; class kuormuri { int paino; int nopeus; public: kuormuri( int p, int n ) { paino = p; nopeus = n; } friend int auto_::auto_nopeampi( kuormuri k ); }; int auto_::auto_nopeampi( kuormuri k ) { return nopeus - k.nopeus; } main() { int nopeusero; auto_ a1( 6, 110 ), a2( 2, 220 ); kuormuri k1( 10000, 100 ), k2( 20000, 140 ); cout << "\nverrataan a1:stä ja k1:stä: "; nopeusero = a1.auto_nopeampi( k1 ); if ( nopeusero >= 0 ) cout << "a1 nopeampi kuin k1\n"; else cout << "a1 hitaampi kuin k1\n"; cout << "\nverrataan a1:stä ja k2:stä: "; nopeusero = a1.auto_nopeampi( k2 ); if ( nopeusero >= 0 ) cout << "a1 nopeampi kuin k2\n"; else cout << "a1 hitaampi kuin k2\n"; cout << "\nverrataan a2:stä ja k1:stä: "; nopeusero = a2.auto_nopeampi( k1 ); if ( nopeusero >= 0 ) cout << "a2 nopeampi kuin k1\n"; else cout << "a2 hitaampi kuin k1\n"; cout << "\nverrataan a2:stä ja k2:stä: "; nopeusero = a2.auto_nopeampi( k2 ); if ( nopeusero >= 0 ) cout << "a2 nopeampi kuin k2\n"; else cout << "a2 hitaampi kuin k2\n"; }
Yleensä ei käytetä täydellisiä luokkamäärittelyitä esimerkiksi funktiokutsuissa, mutta niin voitaisiin tehdä haluttaessa:
nopeampi = a1.auto_nopeampi( k1 ); === nopeampi = a1.auto_::auto_nopeampi( k1 );
Kopio lisenssistä (englanniksi) löytyy täältä.
Alkuperäinen (c) Petteri Hämäläinen
