1. Luokan jäsenet Luokassa määriteltävät jäsenet ovat pääasiassa tietojäseniä tai aliohjelmajäseniä. Luokan määrittelyyn liittyvät varatut sanat: class luokan_nimi tyypit: enum, struct, class, typedef tiedot: const, static aliohjelmat: static, inline, const, virtual ystävät: friend ; Luokassa esiteltävät tietojäsenet voivat olla oliokohtaisia tai luokkakohtaisia. Jokaisella luokkaa luodulla oliolla on omat oliokohtaiset tietonsa, joita voi normaalisti käsitellä vain olio itse luokkansa aliohjelmilla (esimerkiksi suorakulmio-luokka ja sen aliohjelmat). Luokkakohtaiset tiedot ovat kaikkien saman luokan olioiden käytettävissä. Tarkastellaan seuraavaksi luokan tieto- ja aliohjelmajäseniä tarkemmin. 1.1 Luokan tietojäsenet 1.1.1. Olion tiedot Olion tietojen muistinvarausluokka voi olla sama kuin olion muistinvarausluokka tai olion tiedot voivat olla dynaamisia. Ilman osoittimia esiteltyjen tietojen muistinvarausluokka on sama kuin itse olion muistinvarausluokka. Dynaamisia tietoja käsiteltäessä luokassa on esiteltävä osoitinmuuttuja. Tietojäsenten ja olion tunnuksen muistinvarausluokkaa ei pidä sekoittaa keskenään. laatikko a; //automaattinen olio laatikko=*b new laatikko; //dynaaminen olio static laatikko c;//staattinen olio static laatikko *d new laatikko; staattinen olio 1.1.2 Luokan tiedot
Luokan tiedot (static data members) ovat luokkakohtaisia tietoja, jotka eivät kuulu millekään oliolle luokassa. Kaikilla luokan olioilla on yhtäläiset oikeudet käsitellä luokkakohtaisia tietoja. Luokkakohtaiset tiedot esitellään avainsanalla static. Luokkakohtaisen tiedon esittely: class Luokka static tyyppi nimi;... ; Luokkakohtaisen olion näkyvyyssäännöt ovat samat kuin oliokohtaisten tietojen näkyvyyssäännöt. Jos static-tietojäsen esitellään private-sanan jäljessä, sitä voi käyttää vain luokan aliohjelmissa. Tyypillinen luokkakohtaisen tiedon käyttötarkoitus on pitää kirjaa luotujen olioiden määrästä. Silloinhan tieto ei voi liittyä mihinkään yksittäiseen olioon vaan itse luokkaan: Ohessa luokan Henkilo esittely: class Henkilo char etunimi[15]; char sukunimi[25]; static int lkm; ; lkm voi sisältää esimerkiksi luotujen Henkilö-tyyppisten olioiden lukumäärän. Muuttujan lkm arvoa voidaan muuttaa muodostimessa ja hajottimessa, esimerkiksi: Henkilo::Henkilo() lkm++; Henkilo::~Henkilo() lkm--;
Tässä tilanteessa luokkakohtainen muuttuja on alustettava nollaksi ennen pääohjelmaa, eli ennen kuin yhtään Henkilö-oliota on luotu: int Henkilo::lkm=0; int main() Henkilo herra47; //Kutsutaan oletusmuodostinta, lkm kasvaa yhdellä... Tehtävä: Lisää edellisellä tunnilla tehtyyn suorakulmioluokkaan muuttuja, joka ilmaisee, montako suorakulmiota on luotu. 1.2 Aliohjelmajäsenet Luokassa esitellyt aliohjelmat on tarkoitettu oliokohtaisten tai luokkakohtaisten tietojen käsittelyä varten. Ryhmittelemme luokassa esiteltävät aliohjelmat käsittelyn -Oliokohtaisia tietoja käsittelevät aliohjelmat -Muodostimet -Hajotin -Sijoitusoperaattori (alustus tilanvarauksen jälkeen) -Muut ohjelmoijan määrittelemät aliohjelmat, voidaan käyttää lisämääreitä inline, const ja virtual -Luokkakohtaisia tietoja käsittelevät aliohjelmat -esiteltävä varatulla sanalla static, voidaan käyttää lisämääreitä inline ja const. 1.2.1 Inline-aliohjelmat luokan jäseninä inline-aliohjelma luokan jäsenenä on aliohjelma, joka on toteutettu suoraan esittelyn yhteydessä tai jonka määrittely alkaa varatulla sanalla inline. Inline-aliohjelmia käytetään yleensä lyhyissä aliohjelmissa. joiden suoritusaikaa halutaan optimoida. Sen suoritus on nopeampaa kuin tavallisen aliohjelman, koska kääntäjä korvaa aliohjelman kutsun aliohjelman rungolla käännösaikana. class Luokka tyyppi Aliohjelma1( )... inline tyyppi Aliohjelma2( )... inline tyyppi Aliohjelma3( ) ;
inline tyyppi Luokka::Aliohjelma3( )... Aliohjelma1 on automaattisesti inline-aliohjelma, koska sen runko on toteutettu suoraan esittelyn yhteydessä. Samoin Aliohjelma2. Sen kohdalla inline-sanaa ei välttämättä tarvittaisi. Aliohjelma3:n esittelyssä ja määrittelyssä on varattu sana inline. Esittelyssä varatun sanan käyttö ei ole pakollista. Aliohjelman toteutus voidaan kirjoittaa luokan määrittelyn jälkeen. Tarkastellaan edellistä Henkilö-luokkaa. Lisätään sinne inline-aliohjelma, joka palauttaa etunimi-kentän arvon: class Henkilo char etunimi[15]; char sukunimi[25]; static int lkm; char *Etunimi() return etunimi; ; Vastaavasti pääohjelmassa: int main() Henkilö herra47; //Kutsutaan oletusmuodostinta, lkm kasvaa yhdellä cout << Etunimi oli << herra47.etunimi();... Inline-aliohjelman esittely ja toteutus erikseen: class Henkilo char etunimi[15]; char sukunimi[25]; static int lkm; char *Etunimi(); ; inline char *Henkilo::Etunimi() return etunimi;
Tehtävä: Muuta suorakulmio-luokan naytamitat ja laskeala inline-funktioiksi. 1.2.2 Vakioaliohjelmat Vakioaliohjelma ei voi muuttaa olion tietoja. Tällaisia ovat olion tilasta kertovat aliohjelmat. Vakioaliohjelmaksi voidaan esitellä esimerkiksi tietojäsenen arvon palauttava aliohjelma. Aliohjelma esitellään vakioksi kirjoittamalla const aliohjelman esittelyssä loppuun ennen esittelyn päättävää puolipistettä. Tällä varmistetaan, että aliohjelma ei voi sisältää operaatioita, joilla voisi muuttaa olion tietojäseniä. tyyppi Aliohjelma(parametrit) const; Olemme jo käyttäneet vakioaliohjelmia, esimerkiksi suorakulmio-luokan yhteydessä: double laskeala() const; double suorakulmio::laskeala() const return korkeus*leveys; Jos vakioaliohjelma palauttaa return-lauseella olion tiedon kutsuvalle ohjelmalle, on päätettävä, onko kutsuvalla ohjelmalla oikeus muuttaa saamaansa tietoa. Jos ei ole, on aliohjelman tyypin edessä oltava myös sana const. Esimerkiksi edellisen laskeala()-funktiota kutsuvan ohjelman ei oleteta muuttavan pintaalaa. Siksi kannattaa määritellä myös paluuarvo vakioksi: const double laskeala() const; const double suorakulmio::laskeala() const return korkeus*leveys; 1.2.3 Vakioparametri Varattua sanaa const voidaan myös käyttää aliohjelmien parametrien välityksessä ja aliohjelmien palauttaman tiedon yhteydessä. Jos parametrin esittelyssä on varattu sana const, ei aliohjelma voi muuttaa parametrin sisältöä. Vakioparametrin esittely:
tyyppi Aliohjelma(const parametri); Lisätään edellä käsiteltyyn Henkilö-luokkaan aliohjelma VaihdaEtunimi, joka nimensä mukaan vaihtaa olion etunimi-tietojäsenen arvon. Aliohjelmalle välitetään vakiotyyppiä oleva merkkijono: class Henkilo char etunimi[15]; char sukunimi[25]; void VaihdaEtunimi(const char *); ; void Henkilo::VaihdaEtunimi(const char *p) strcpy(etunimi,p); int main() Henkilo herra47;... char uusinimi[15]= Maija ; herra47.vaihdaetunimi(uusinimi);... return 0; Tehtävä: Lisää suorakulmioluokkaan vakioparametria käyttävä aliohjelma, joka muuttaa olemassa olevan olion leveyttä ja korkeutta. 1.2.4 Vakio-oliot Aliohjelmat voivat saada parametrina olion. Parametrina saatu olio voidaan määritellä vakio-olioksi. Tällöin vakio-olio parametrina saanut aliohjelma ei voi kutsua vakio-olion kautta muita kuin luokan vakioaliohjelmia. Tämän tyyppinen olio on käsitelty kopiomuodostimen yhteydessä: class laatikko double leveys; double korkeus;
static int lkm; laatikko();//oletusmuodostin laatikko(double,double);//parametrillinen muodostin laatikko(laatikko const &);//kopiomuodostin ~laatikko(); void asetamitat(); void naytamitat() const; double laskeala() const; ; laatikko::laatikko(laatikko const &p) cout << "Kopiomuodostinta kutsuttu!" << endl; cout << "Olion osoite " << this << endl; leveys=p.leveys; korkeus=p.korkeus; lkm++; Tehtävä: Lisää suorakulmio-luokkaan aliohjelma, joka vertaa kahden suorakulmion pinta-aloja. Toinen suorakulmio välitetään aliohjelmaan vakio-oliona. 1.2.5. Luokkakohtaisia tietoja käsittelevät static-aliohjelmat Static-aliohjelmat on tarkoitettu luokkakohtaisten tietojen käsittelyä varten. Ne eivät voi käsitellä minkään yksittäisen olion yksityisiä tietojäseniä. Static-aliohjelmaan liittyviä sääntöjä: Esittely alkaa static-määreellä Static-aliohjelmassa ei voi käyttää this-osoitinta Static-aliohjelmassa ei voi käsitellä suoraan minkään olion tietojäseniä Static-aliohjelma ja oliokohtaisia tietojäseniä käsittelevä aliohjelma eivät saa esiintyä luokassa samalla nimellä ja samoilla parametreilla. static-aliohjelman esittely: static tyyppi Aliohjelma(parametrit); Aliohjelman esittelyn edessä on varattu sana static. Muuten aliohjelman esittely vastaa tavallista olion tietoja käsittelevän aliohjelman esittelyä. Jatketaanpa vanhan kunnon laatikko-luokan tarkastelua vielä hetki. Laatikko-luokan määrittely näyttää tältä: class laatikko double leveys; double korkeus; static int lkm; laatikko();//oletusmuodostin
; laatikko(double,double);//parametrillinen muodostin laatikko(laatikko const &);//kopiomuodostin ~laatikko(); void asetamitat(); void naytamitat() const; double laskeala() const; static int lukumaara(); Viimeisellä rivillä on esitelty luokan staattinen aliohjelmajäsen lukumaara(), jonka avulla voidaan selvittää luotujen laatikoiden lukumäärä. Aliohjelman runko on seuraavan näköinen: int laatikko::lukumaara() return lkm; Eli ei kovin kummoinen. Se siis palauttaa luokan yksityisen staattisen tietojäsenen lkm arvon. Miksi lukumaara-aliohjelma sitten piti tehdä? Siksi, että koska lkm on luokan yksityinen jäsen, siihen ei olio-ohjelmoinnin periaatteiden mukaisesti pääse käsiksi kuin luokan omilla aliohjelmilla. Nyt laatikoiden lukumäärä saadaan pääohjelmassa selville seuraavasti: int main() laatikko x;//oletusmuodostin laatikko *y = new laatikko; laatikko z; cout << "Laatikoita on " << laatikko::lukumaara() << " kappaletta." << endl; delete y; return 0;
Static-aliohjelmaa voidaan siis kutsua joko suoraan aliohjelman nimellä tai olion tunnuksen kautta: Luokka::Aliohjelma(); Olio.Aliohjelma(); Olio->Aliohjelma(); Ensimmäistä versiota voidaan käyttää aina static-aliohjelman kutsussa. Toisella rivillä olevaa muotoa voidaan käyttää silloin, kun aiohjelmaa kutsutaan automaattisesti luodun olion kautta. Kolmannella rivillä olevaa muotoa voidaan käyttää dynaamisesti luodun olion yhteydessä. Vaikka kutsun yhteydessä esiintyisi olion tunnus, ei static-aliohjelma voi sisältää viittauksia olion tietojäseniin. Järjestelmä ei siis käytännössä huomioi olion tunnusta. Tehtävä: Lisää laatikko-luokkaan tieto- ja aliohjelmajäsenet, joiden avulla pidetään kirjaa luokkaan luotujen laatikko-olioiden pinta-alojen keskiarvosta. Keskiarvoa tulee päivittää aina, kun uusi olio luodaan tai tuhotaan. Luokassa tulee olla aliohjelma, jolla voidaan selvittää laatikoiden keskimääräinen pinta-ala. 2. Dynaamisten tietojäsenten käsittely Dynaaminen tietojäsen tarkoittaa tietojäsentä, jolle varattavan muistitilan koko vaihtelee oliokohtaisesti. Se toteutetaan osoittimen ja dynaamisen muistinhallinnan avulla new- ja delete-operaattoreilla. Tilanvaraus tehdään yleensä olion alustuksen yhteydessä muodostimessa ja vapautus hajottimessa. Dynaamisten tietojäsenten varaamat tilat eivät vapaudu automaattisesti olion tuhoutumisen yhteydessä, koska C++-kielessä ei ole automaattista muistinhallintaa. Jos olio tuhotaan tietojäsenten tilaa vapauttamatta, jäävät tietojäsenet keskusmuistiin, vaikka niihin ei enää olisi käytössä olevaa osoitinta. Ohjelmoijan on siis itse toteutettava muodostimet, hajotin ja sijoitusoperaattori. Huomaa, että dynaamisia tietojäseniä voi olla kaikilla oliolla riippumatta olion muistinvarausluokasta! 2.1 Dynaamisten tietojäsenten alustus oletusarvoilla ja oletusmuodostin Seuraavassa esimerkissä on yksinkertainen Henkilo-luokan toteutus, jossa tietojäsenet sukunimi ja etunimi ovat dynaamisia. Ohjelma ei oikeastaan tee vielä yhtään mitään, se luo oletusmuodostimessa kaksi tyhjää merkkijonoa ja tuhoaa ne sitten:
#include <iostream> using namespace std; class Henkilo char *etunimi; char *sukunimi; Henkilo() etunimi=null;sukunimi=null; ~Henkilo(); ; Henkilo::~Henkilo() if (etunimi) delete [] etunimi; etunimi=null; if (sukunimi) delete [] sukunimi; sukunimi=null; cout << "Tuhottiin Henkilo-olio osoitteesta " << this << endl; int main() Henkilo x; return 0; 2.2 Dynaamisten tietojäsenten alustus parametrillisella muodostimella Lisätään ohjelmaan parametrillinen muodostin, jonka avulla Henkilo-oliolle saadaan annettua nimi, sekä Näytä-metodi, jonka avulla nimi voidaan näyttää. class Henkilo char *etunimi; char *sukunimi; Henkilo() etunimi=null;sukunimi=null; Henkilo(const char *, const char *); void Nayta() const if (etunimi sukunimi) cout << etunimi << " " << sukunimi << endl;
~Henkilo(); ; Parametrillinen muodostin siis ottaa parametrina kaksi vakiomerkkijonoa (tyyppiä const char *). Nayta()-aliohjelma on kirjoitettu inline-tyyppisenä, joka toisaalta nopeuttaa aliohjelman suoritusta, mutta voi vaikuttaa hieman sotkuiselta. Sama asia voitaisiin tehdä kirjoittamalla luokan määrittelyyn ainoastaan void Nayta() const; ja sitten varsinainen funktion runko erikseen: Henkilo::Nayta() const if (etunimi sukunimi) cout << etunimi << << sukunimi << endl; Edellä oleva if-lause voitaisiin kirjoittaa muotoon if (etunimi!=null sukunimi!=null) Parametrillisen muodostimen toteutus on esitetty alla. Muodostin varaa uusien olioiden dynaamisille tietojäsenille keskusmuistitilaa tilanvarauksen verran. Muodostin tutkii strlen-aliohjelmalla parametreina saatujen merkkijonojen pituudet. Koska strlen ei laske pituuteen mukaan merkkijonon lopetusmerkkiä, on varattavan tilan pituuteen lisättävä 1. Henkilo::Henkilo(const char *en, const char *sn) int pituus1=strlen(en)+1; int pituus2=strlen(sn)+1; etunimi=new char[pituus1]; strcpy(etunimi,en); sukunimi=new char[pituus2]; strcpy(sukunimi,sn); cout << "Luotiin Henkilo-olio osoitteeseen " << this << endl; Pääohjelma näyttää nyt seuraavalta: int main() Henkilo x( Jukka, Jauhiainen ); x.nayta(); return 0;
jolloin ohjelma tulostaa: 2.3 Dynaamisten tietojäsenten muuttaminen Lisätään seuraavaksi metodi, jonka avulla nimeä voi muuttaa: class Henkilo char *etunimi; char *sukunimi; Henkilo() etunimi=null;sukunimi=null; Henkilo(const char *, const char *); void Nayta() const if (etunimi sukunimi) cout << etunimi << " " << sukunimi << endl; void Muuta(); ~Henkilo(); ; Muuttamisessa syntyy ongelma. Hyvin todennäköisesti muutetut merkkijonot ovat joko pitempiä tai lyhyempiä kuin alkuperäiset. Siksi alkuperäiset tietojäsenet pitää ensin tuhota ja luoda uudet oikein mittaiset sen jälkeen, kun niiden pituudet on tiedossa: void Henkilo::Muuta() char en[30]; char sn[30]; if (etunimi) delete [] etunimi;
etunimi=null; if (sukunimi) delete [] sukunimi; sukunimi=null; cout << "Etunimi :" << endl; cin >> en; cout << "Sukunimi :" << endl; cin >> sn; int pituus1=strlen(en)+1; int pituus2=strlen(sn)+1; etunimi=new char[pituus1]; strcpy(etunimi,en); sukunimi=new char[pituus2]; strcpy(sukunimi,sn); Jos pääohjelma näyttää tältä int main() Henkilo x("jukka","jauhiainen"); Henkilo y; x.nayta(); y.muuta(); y.nayta(); return 0; niin ohjelma tulostaa Huomaa, että oletusmuodostin, joka luo olion y, ei tässä esimerkissä tulosta tietoa olion luonnista.
2.4 Dynaamisten tietojäsenten kopiointi oliosta toiseen ja kopiomuodostin Kokeillaanpa seuraavaksi suorittaa alla olevaa ohjelmaa. Tarkoituksena on, että olio y alustetaan olion x arvoilla. Tämä aiheuttaa kopiomuodostimen kutsun. Idiootti ohjelmoija ei ole sitä toteuttanut, joten kääntäjä generoi oletuskopiomuodostimen. Jos kopioitavan olion (tässä x) jokin tietojäsen on osoitinmuuttuja, kopioi muodostin osoitteen. Tämä aiheuttaa tilanteen, että sekä x:n että y:n tietojäsenet etunimi ja sukunimi osoittavat samoihin keskusmuistiosoitteisiin. Kun ohjelmassa muutetaan y:n arvoa, menee olio x sekaisin, koska sen arvot muuttuvat samalla, vaikka niin ei tietenkään pitäisi käydä! int main() Henkilo x("jukka","jauhiainen"); Henkilo y=x; x.nayta(); y.nayta(); y.muuta(); x.nayta(); y.nayta(); return 0; Ohjelman suoritus näyttää tältä:
Dynaamisia tietojäseniä sisältävillä olioille on siis AINA SYYTÄ KIRJOITTAA KOPIOMUODOSTIN, joka huolehtii siitä, että kopioitavat tiedot, eikä niiden osoitteet, kopioituvat! Tässä tapauksessa kopiomuodostin näyttäisi tältä: Henkilo::Henkilo(Henkilo const &p) int pituus1=strlen(p.etunimi)+1; int pituus2=strlen(p.sukunimi)+1; etunimi=new char[pituus1]; strcpy(etunimi,p.etunimi); sukunimi=new char[pituus2]; strcpy(sukunimi,p.sukunimi); cout << "Kopio Henkilo-oliosta luotiin osoitteeseen " << this << endl;
Eli kopioitava olio p viedään viittausparametrin välityksellä muodostimelle, tutkitaan tietojäsenten pituudet, luodaan uuden olion tietojäsenet ja kopioidaan vanhan olion arvot niihin. Pääohjelma on sama kuin edellä. Ohjelman suoritus näyttää nyt tältä. Kopiomuodostimen avulla olioon y todellakin kopioituu olion x tietojäsenten sisältö. 3. Operaattorien ylikuormaus Edellä Muuta()-aliohjelman avulla voitiin muuttaa olemassa olevan olion tietoja. Kopiomuodostimella voitiin luoda uusi olio ja kopioida vanhan olion tiedot siihen. Jos ohjelmassa halutaan sijoittaa jo olemassa olevan olion tiedot johonkin toiseen olemassa olevaan olioon, voidaan määritellä sijoitusoperaattori = siten, että yhtäsuuruusmerkin oikealla puolella olevan olion tietojäsenten sisällöt kopioituvat vasemmalla puolella oleviin vastaaviin tietojäseniin. Tässä tulee vastaan sama ongelma, mikä edellä Muuta()-aliohjelmassa, eli sen olion, johon kopioidaan, koko muuttuu. Siksi olion tietojäsenet on ensin tuhottava ja varattava sopivan kokoiset tilat ennen uusien tietojen sijoittamista. Lisätään luokkaan sijoitusoperaattori: class Henkilo char *etunimi; char *sukunimi; Henkilo() etunimi=null;sukunimi=null; Henkilo(const char *, const char *); Henkilo(Henkilo const &); Henkilo &operator=(const Henkilo &);
void Nayta() const if (etunimi sukunimi) cout << etunimi << " " << sukunimi << endl; void Muuta(); ~Henkilo(); ; Henkilo &Henkilo::operator = (const Henkilo &p) if (this == &p) return (*this); int pituus1=strlen(p.etunimi)+1; int pituus2=strlen(p.sukunimi)+1; delete [] etunimi; delete [] sukunimi; etunimi=new char(pituus1); strcpy(etunimi,p.etunimi); sukunimi=new char(pituus2); strcpy(sukunimi,p.sukunimi); return (*this);