Johdanto C++-ohjelmointiin luentomateriaali - Noppa
Transcription
Johdanto C++-ohjelmointiin luentomateriaali - Noppa
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla Olio-ohjelmointi – Johdanto ohjelmointiin C++-kielellä Tässä osassa tutustutaan C++ - ohjelmoinnin perusrakenteisiin ja siihen, kuinka käyttäjän kanssa kommunikoidaan käyttäen hyväksi konsoli-ikkunaa. Teksti perustuu pääasiallisesti opetusmonisteen [CppTut] ensimmäiseen lukuun. Lukijan oletetaan tuntevan C-kielen siinä määrin kuin se esitetään kurssilla Johdatus ohjelmointiin. C++ perustuu syntaksiltaan Cohjelmointikieleen: Lähes mikä tahansa C-ohjelma on myös C++-ohjelma. Tällä kurssilla pyritään kuitenkin ohjelmoimaan C++:n luonteen mukaisesti, jolloin monien C-kielestä tuttujen toimintatapojen tilalle tulee uusia menetelmiä. 1. C++ -pääohjelma Ohjelman suoritus alkaa aina pääohjelmasta, joka on sekä C- että C++ - ohjelmissa nimeltään main. Pääohjelma on tavallinen C++ -ohjelman funktio, jota käyttöjärjestelmä kutsuu automaattisesti. Vaikka pääohjelma onkin normaali funktio, sitä erottavat tavallisista funktioista seuraavat seikat: 1. Sitä ei voi kutsua erikseen. 2. Sen osoitetta ei voi tallentaa osoitinmuuttujaan. 3. Sitä ei tarvitse esitellä ennen määritystä. 4. Sitä voi olla vain yksi kappale ohjelmassa. C++ -standardin mukaan pääohjelma voi olla kahta muotoa: (1a) int main() (1b) int main(void) (2a) int main(int argc, char **argv) (2b) int main(int argc, char *[]argv) (2c) int main(int argc, char [][]argv) Näistä kaksi ensimmäistä ovat ns. parametrittomia ja kolme viimeistä ns. parametrillisia muotoja pääohjelmasta. Kaksi ensimmäistä muotoa ovat vaihtoehtoisia keskenään ja kolme viimeistä ovat vaihtoehtoisia keskenään. Yleensä C++ -ohjelmissa käytetään muotoa 1a silloin, kun ei tarvita käynnistysparametreja, ja muotoa 2a, kun tarvitaan syöttöparametreja. C++ -pääohjelman paluuarvon tyyppi on aina int. C++ -standardi ei määrittele paluuarvolle merkitystä; siinä ainoastaan luvataan arvon nolla merkitsevän, että ohjelman suoritus on loppunut onnistuneesti. Muut arvot ilmaisevat jonkinlaista virhetilannetta, jonka käyttöjärjestelmä tulkitsee. Paluuarvoa käytetään lähinnä ohjelmissa, jotka on tarkoitettu putkitettavaksi käyttöjärjestelmätasolla siten, että edellisen ohjelman tulostetieto on seuraavan ohjelman syöttötieto. Käyttöjärjestelmä päättää paluuarvon perusteella jatketaanko putken suorittamista vai keskeytetäänkö se. C++ -standardissa on määritelty kaksi symbolista vakiota, joita voi käyttää paluuarvoina. Nämä vakiot on määritelty otsikkotiedostossa <cstdlib>: 1. EXIT_FAILURE tarkoittaa, että ohjelman suoritus on tavalla tai toisella epäonnistunut ja 2. EXIT_SUCCESS tarkoittaa, että ohjelman suoritus on onnistunut. C++ -ohjelmalle voidaan välittää ohjelmaan käynnistämisen yhteydessä parametreja, kun käytetään aiemmin mainittua toista pääohjelman muotoa. Käyttöjärjestelmä välittää parametrit 1 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla C-tyyppisinä merkkijonoina, ts. merkkitaulukkoina, joiden viimeisenä merkkinä on nollamerkki. Ensimmäinen parametri ilmoittaa, kuinka monta parametria ohjelmalle välitettiin. Tämä arvo on aina suurempi kuin nolla, koska käyttöjärjestelmä välittää tavallisesti ohjelman nimen ensimmäisenä parametrina. Ellei ohjelman nimeä välitetä, parametritaulukon ensimmäisenä alkiona on tyhjä merkkijono (””). Oletetaan, että ohjelma nimeltä ”echo” on käynnistetty seuraavasti: echo hello Tällöin parametrien arvot ovat: argc – muuttujan arvo on 2. Ensimmäisessä argv – taulukon alkiossa (argv[0]) on osoitin merkkijonoon ”echo”. Toisessa argv – taulukon alkiossa (argv[1]) on osoitin merkkijonoon ”hello”. Luonnollisesti parametrien nimet voivat olla mitä tahansa, mutta niiden tyyppien pitää olla em. eli int ja char** (tai char*[] tai char[][]). Yleisesti kuitenkin käytetään nimiä argc (eli argument count) ja argv (eli argument values). 2. Lukeminen ja tulostaminen C++-kielessä tulostamiseen käytetään operaattoria <<, jota kutsutaankin nykyään pääsääntöisesti tulostusoperaattoriksi. (Operaattorin toinen merkitys on kokonaislukujen bittien siirtäminen vasemmalle). Operaattori on määritelty standardikirjastossa jokaiselle C++ -kielen sisäiselle tyypille. Kirjaston yksi suunnitteluperiaate on se, että ohjelmoijan omien tyyppien tulostaminen ei eroa kielen sisäisten tyyppien tulostamisesta. Ohjelmoija voi siten määritellä tulostusoperaattorin omille tyypeilleen haluamallaan tavalla. Tähän asiaan tutustutaan kurssilla käsiteltäessä ylikuormittamista. C++ - standardikirjastossa on määritelty cout -niminen oliomuuttuja, joka huolehtii tulostamisesta konsoli-ikkunaan. Seuraava esimerkkiohjelma tulostaa tekstin ”Terve maailma!” ja rivivaihdon. #include <iostream> int main() { std::cout << ”Terve maailma!” << std::endl; return 0; } Huomioidaan ohjelmasta seuraavaa: Lähdekoodiin liitetään #include-direktiiveillä standardikirjaston otsikkotiedostoja, joissa esitettyjä tyyppejä ja funktioita ohjelmassa tarvitaan. Esimerkkiohjelmassa tällaisia ovat cout-oliomuuttujan ja operaattorin << esittelyt. Tulostettava teksti on ohjelmassa esitetty merkkijonovakiona. Tällaisesta ohjelmakoodissa esiintyvästä vakiosta käytetään myös nimitystä literaali. Literaali voi olla muunkin tyyppinen vakio kuin merkkijono. Merkintä std:: entiteettien cout ja endl edessä tarkoittaa sitä, että nämä entiteetit on määritelty standardikirjaston nimiavaruudessa std. Nimiavaruuksien avulla voidaan välttää samannimisten entiteettien esiintymiset ohjelmassa. Tällä kurssilla käytetään ainoastaan standardinimiavaruutta 2 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla std. Merkinnän käyttäminen voidaan tehdä tarpeettomaksi esimerkiksi käyttämällä usingdirektiiviä (using directive), jonka avulla nimiavaruudesta tuodaan kaikki entiteetit. Esimerkkiohjelma voidaan täten kirjoittaa myös muodossa: #include <iostream> int main() { using namespace std; cout << ”Terve maailma!” << endl; return 0; } Voidaan myös käyttää using-esittelyä (using declaration), jolloin valitaan, mitkä entiteetit halutaan tuoda valitusta nimiavaruudesta. Esimerkkiohjelmasta tulisi tällöin: #include <iostream> int main() { using std::cout; using std::endl; cout << ”Terve maailma!” << endl; return 0; } Siirrettävyyden ja ylläpidettävyyden vuoksi using-lauseiden näkyvyys tulisi rajata mahdollisimman suppeaksi. Yleensä ne tulisi sijoittaa funktioiden toteutuksiin. Edellisissä esimerkeissä ne on kirjoitettu main-funktioon. Esimerkkiohjelmasta huomataan myös, että tulostettavia arvoja voidaan ketjuttaa yhteen tulostuslauseeseen lisäämällä tulostettavien arvojen väliin operaattori <<. Tulostuksen muotoilu: minimileveys, täytemerkki ja tasaus C++-ohjelmassa voidaan tulostuksen muotoa muuttaa. C++:ssa tulostus on osittain kenttäpohjaista, toisin sanoen tulostettavalle arvolle voidaan määritellä kentän pituus. Tämä pituus on ainoastaan minimipituus: jos tulostettava arvo vie enemmän tilaa kuin kentän nykyinen leveys, kentän leveyttä kasvatetaan. Lisäksi kentän minimipituus asetetaan jokaisen arvon tulostamisen jälkeen nollaksi, mikä tarkoittaa sitä että jokaisen tulostettavan arvon kohdalla pitää, ohjelmoijan niin halutessa, määritellä kentän minimileveys. Kentän leveys voidaan määritellä ns. manipulaattorin avulla, joka asettaa tulostusvirran muotoilulippuun tietyn arvon. Standardimanipulaattorit on esitelty kahdessa otsikkotiedostossa: Otsikkotiedostossa <iostream> on esitelty ne manipulaattorit, jotka eivät ota parametreja, kuten endl. Otsikkotiedostossa <iomanip> on esitelty ne manipulaattorit, joilla on parametri, kuten minimileveyden asettamiseen käytettävä setw. Seuraavassa ohjelmassa käytetään manipulaattoreita setw, setfill, left ja right muotoilemaan tulostusta: 3 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla #include <iostream> #include <iomanip> int main() { using namespace std; cout << ”C++” << ” ” << ”C++” << endl; cout << setw(5) << ”C++” << ” ” << ”C++” << endl; cout << setw(5) << setfill(’*’) << ”C++” << ” ” << ”C++” << endl; cout << ”C++” << ” ” << ”C++” << endl; cout << setw(5) << setfill(’*’) << left << ”C++” << ” ” << ”C++” << endl; cout << setw(5) << setfill(’*’) << right << ”C++” << ” ” << setw(5) << ”C++” << endl; return 0; } Ohjelma tulostaa samat kuusi riviä eri tavoin muotoiltuna seuraavasti: C++ C++ C++ C++ **C++ C++ C++ C++ C++** C++ **C++ **C++ Tulostuksesta ja koodista huomataan: 1. Oletusleveys on nolla merkkiä, joten tulostuskentät laajenevat automaattisesti. 2. Oletustasaus tehdään tulostuskentän oikeaan reunaan. 3. Asetettu tietokentän leveys nollataan jokaisen arvon tulostamisen jälkeen. 4. Täytemerkki ja tasaus säilyvät asettamisen jälkeen. Yhden sanan lukeminen ja lukuoperaattori Konsoli-ikkunasta lukeminen tapahtuu C++:n IOStream-kirjaston välityksellä, kuten konsoliikkunaan tulostaminenkin. Tässäkään tapauksessa ohjelmoijan omien tyyppien lukemisen ei tulisi erota C++ - kielen sisäisten tyyppien lukemisesta. Tätä varten on ylikuormitettu operaattori >>, joka tarkoittaa myös kokonaisluvun bittien siirtoa oikealle. Tätä operaattoria kutsutaankin nykyään pääsääntöisesti lukuoperaattoriksi. IOStream-kirjastossa on määritelty lukuoperaattorit kielen sisäisille tyypeille. Ko. kirjastossa on määritelty olio cin, jonka välityksellä konsolinäppäimistöltä syötettyjä tietoja luetaan. Lukuoperaattori lukee luettavan tiedon tyypin perusteella niin monta merkkiä kuin tyypin merkkiavaruuteen kuuluu, minkä jälkeen lukeminen loppuu ja merkit muutetaan ko. tyypin arvoksi. Lukeminen on oletusarvoisesti puskuroitua. Luettaessa poistetaan ensimmäiseksi alussa olevat valkomerkit (white space characters), joita ovat mm. rivinvaihto-, tabulaattori- ja välilyöntimerkit. Jäljellä olevat merkit jäävät seuraavan lukuoperaation luettavaksi. Lukeminen tapahtuu siis tietokentittäin. Jos lukemisen yhteydessä ei tallenneta yhtään merkkiä merkkijonoon, asetetaan virhelippu. Tilannetta käsitellään lyhyesti tuonnempana. Luettaessa merkkijonotyyppistä tietoa luettavien merkkien määrä voidaan rajata asettamalla tietokentän leveys samalla tavalla kuin tulostettaessa. Jos luettavien merkkien lukumäärää ei ole 4 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla asetettu, lopetetaan lukeminen ensimmäiseen valkomerkkiin, joka jää seuraavan lukutoimenpiteen käsiteltäväksi. Luettavien merkkien lukumäärä pitää aina asettaa kun luetaan merkkitaulukkoon tietoa, jotta lukuoperaatio ei kohdistuisi taulukon ulkopuolelle. Tarkastellaan seuraavaa ohjelmaa: #include <iostream> #include <iomanip> int main() { const int TAULUKON_KOKO = 12; char hetu[TAULUKON_KOKO]; using namespace std; cout << ”Anna HETU: ”; cin >> setw(TAULUKON_KOKO) >> hetu; cout << ”Syötit seuraavan HETU:n ” << hetu << endl; return 0; } Jos esimerkissä ei olisi asetettu tietokentän leveyttä ja käyttäjä olisikin syöttänyt viisitoista merkkiä, niin kolme viimeistä merkkiä ja merkkijonon loppumisen ilmoittava nollamerkki olisi kirjoitettu taulukon ulkopuolelle, mahdollisesti jopa ohjelmakoodin päälle. Tätä tilannetta nimitetään puskurin ylivuodoksi (buffer overflow), joka on yksi yleisimmistä virusten ja muiden haittaohjelmien käyttämistä tietoturva-aukoista. Kun käytetään C++-standardikirjastossa määriteltyä merkkijonoluokkaa string, ei tarvitse välittää merkkijonon maksimipituudesta, jolloin edellä oleva esimerkki voidaan kirjoittaa muodossa: #include <iostream> #include <string> int main() { using namespace std; cout << ”Anna HETU: ”; string hetu; cin >> hetu; cout << ”Syötit seuraavan HETU:n ” << hetu << endl; return 0; } C++:n merkkijonotyyppi varaa tarvittaessa automaattisesti lisää tilaa, kunnes merkkijonon maksimipituus täyttyy; tämä on yleensä sidoksissa int-tyypin kokoon, jolloin 32-bittissä järjestelmissä tämä maksimikoko on 2^32 – 1 eli 4294967295 merkkiä, ts. noin 4 gigatavua (olettaen että laitteistossa on niin paljon vapaata muistitilaa). Tämän vuoksi C++-ohjelmissa ei juurikaan käytetä C-tyyppisiä merkkijonoja eli merkkitaulukkoja; tämä karsii koodista yhden 5 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla mahdollisen virhepaikan. Haluttaessa voidaan luettavien merkkien lukumäärää rajoittaa myös string-oliota käytettäessä asettamalla tietokentän leveys manipulaattorin setw avulla. Syöttö- ja tulostusvirroilla (cin ja cout) on kolmesta lipusta (eli yksittäisestä bitistä) koostuva tila, joka kertoo, onko virta toimintakykyinen. Nämä bitit ovat eofbit, badbit ja failbit. Kun muuttujaa luettaessa ajaudutaan virhetilanteeseen, muuttujan sisältö on määrittelemätön ja cin-olion jokin virhelippu on asetettu. Tilaa voidaan lukea käyttämällä seuraavia metodeita, jotka kaikki palauttavat bool-tyyppisen arvon: Metodi good() fail() bad() eof() Selitys Palauttaa arvon true, jos kaikki on OK. Palauttaa arvon true, jos on tapahtunut lukuvirhe Palauttaa arvon true, jos virta on käyttökelvoton. Palauttaa arvon true, jos tiedoston loppumerkki on luettu. Lukemisen yhteydessä riittää usein tarkastella tietovuon virhetilaa good()-metodin avulla: #include <iostream> #include <string> int main() { using namespace std; while (cin.good()) // Kutsutaan cin-olion metodia good() { string sana; cout << ”Syötä tietoa: ”; cin >> sana; cout << sana << endl; } return 0; } Edellä oleva ohjelma lukee näppäimistöä, kunnes tapahtuu virhetilanne. Näppäimistöä käytettäessä ainoa virhetilanne on se, että yritetään lukea ohi tiedoston loppumerkin, joka saadaan aikaan Windows/DOS-ympäristössä painamalla yhtä aikaa ctrl ja Z ja Unix/Linuxympäristöissä painamalla yhtä aikaa ctrl ja D. Jos virheellisessä tilanteessa yritetään lukea lisää tietoa, ei lukutoiminto johda mihinkään vaan se epäonnistuu automaattisesti. Täten ennen jokaista lukutoimenpidettä tulisi tarkistaa ja korjata virhetilanne poistamalla virhelippu kutsumalla tietovuon metodia clear. 6 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla Numeerisen arvon lukeminen, tulostaminen ja tulostamisen muotoilu Numeerisen arvon lukemiseen pätevät samat periaatteet kuin yhden sanan lukemiseen, paitsi että asetettua tietokentän leveyttä ei huomioida eikä sitä nollata. Seuraavissa esimerkeissä oletetaan että luettava tieto on kokonaislukutyyppi int. Syöte 123 123% 123123 123.123 -123 +123 123123123123 Luettu arvo 123 123 123123 123 -123 123 Määrittelemätön Taulukon toisessa tilanteessa lukutoiminto onnistuu, mutta tätä seuraava numeerisen tiedon lukuyritys epäonnistuu, koska % - merkki ei ole minkään numeron osa, muuten se olisikin luettu jo ensimmäisellä kerralla. Neljännessä tilanteessa lukeminen loppuu pistemerkkiin, koska se ei osa merkistöä, joita käytetään kokonaislukujen yhteydessä. Viimeisessä tilanteessa tapahtuu ylivuoto, koska syötettä ei voida esittää tyypin int avulla. Tällöin asetetaan cin-olion virhetilalippu. Kokonaislukujen tulkintaa voidaan ohjata myös kolmella manipulaattorilla: hex, jolloin lukujen kantalukuna on 16 ja käytettävä merkistö on ”+0123456789abcdefABCDEFxX” oct, jolloin lukujen kantalukuna on 8 ja käytettävä merkistö on ”+-01234567” dec, jolloin lukujen kantalukuna on 10 ja käytettävä merkistö on ”+-0123456789” Jotta heksadesimaalimuodossa olevissa luvuissa voitaisiin käyttää x-kirjainta, sen edellä pitää olla numero 0. Esimerkiksi 0x12 hyväksytään, mutta muotoa x12 ei hyväksytä. Yhdistelmä ”0x” saa esiintyä vain kerran. Jos syötetty luku ei ala ”0x”, se tulkitaan alkavan sillä eli syöte ”12” tulkitaan syötteeksi ”0x12”. Oktaalilukujen alussa olevat numerot 0 tulkitaan yhdeksi nollaksi. Esimerkiksi 000012 tulkitaan sen olevan 012. Jos syötetty luku ei ala numerolla 0, sen tulkitaan olevan kuitenkin alussa eli syöte ”12” tulkitaan syötteeksi ”012”. Kun luetaan reaalilukua, sallitut merkit ovat ”+-1234567890.eE”. Piste ja e-merkit voivat esiintyä peräkkäin. Mikäli luvussa esiintyy E tai e-kirjain, luvussa täytyy esiintyä jokin numero sekä ennen että jälkeen kirjaimen. Kirjainta seuraavan numeron edessä voi olla myös etumerkki. Seuraavassa on esimerkkejä hyväksytyistä reaaliluvuista: +.1 -1.23 1 1e5 1.e-5 1.23e2 0.342 .5 7 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla Seuraavassa ohjelmassa kysytään käyttäjältä reaalilukuna lämpötila Celsius-asteina ja ohjelma tulostaa lämpötilan Fahrenheit-asteikolla. #include <iostream> int main(){ using namespace std; double celsius; cout << ”Anna lämpötila celsius-asteina: ”; cin >> celsius; if (!cin.good()) { cout << ”Luku ei kelpaa” << endl; cin.clear(); } else { double farenheit = 1.8 * celsius + 32; cout << << << << celsius ” Celsius-astetta on Fahrenheit-asteikolla ” farenheit endl; } return 0; } Ohjelma havaitsee virheellisen syötteen, mutta ei pyydä uutta arvoa. Tällaisen rakenteen toteuttamiseksi tulisi syöttövirrasta poistaa edellinen virheellinen syöte, kuten myöhemmin näytetään. Numeeriset arvot tulostetaan täsmälleen samoin kuin merkkijonot eli käyttämällä tulostusoperaattoria, kuten edellä olevassa esimerkissä näytettiin. Kun ohjelmassa käytetään reaalilukuja, niin usein halutaan muotoilla lukujen tulostusta esimerkiksi rajoittamalla tulostettavien desimaalien määrää. Lukujen muotoilua varten otsikkotiedostoissa <ios> ja <iomanip> on määritelty mm. seuraavat manipulaattorit: Manipulaattori showpos noshowpos left right setw(leveys) fixed scientific setprecision(luku) showpoint Tarkoitus Positiivisten lukujen yhteyteen tulostetaan + merkki Positiivisten lukujen yhteydessä ei tulosteta merkkiä (oletus) Tasataan luku vasempaan reunaan tulostuskenttää Tasataan luku oikeaan reunaan tulostuskenttää (oletus) Asetetaan tulostuskentän minimileveys (oletuksena 0) Käytetään reaalilukujen yhteydessä desimaalimuotoa Käytetään reaalilukujen yhteydessä tieteellistä muotoa (E) Reaaliluvun merkitsevien numeroiden lukumäärä Tulostetaan aina reaalilukujen yhteydessä desimaalipiste Muut manipulaattorit löytää esimerkiksi Cppreference-sivustolta osoitteesta http://en.cppreference.com/w/cpp/io/manip. 8 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla Seuraavassa taulukossa on esimerkkejä reaaliluvun muotoilulippujen vaikutuksesta: normaali showpoint fixed scientific setprecision() 2 6 2 6 2 6 2 6 421.0 4.2e+02 421 4.2e+02 421.000 421.00 421.000000 4.21e+02 4.210000e+02 0.0123456789 0.012 0.01234567 0.012 0.01234567 0.01 0.0123456 1.23e-02 1.2345678e-02 Ainoastaan tulostuskentän leveys nollataan jokaisen luvun yhteydessä, muut muotoilulippujen asetukset säilyvät ensimmäisen asetuksen jälkeen. Yhden rivin lukeminen Edellä esitetyt lukutoimenpiteet olivat kaikki ns. muotoiltuja lukutoimenpiteitä, joissa lukemiseen vaikuttavat muotoiluliput kuten esimerkiksi tietokentän leveys. Muotoiltujen lukutoimintojen lisäksi IOStream-kirjasto määrittelee metodeja ja funktioita, joiden avulla voidaan lukea merkkityyppistä tietoa raakadatana, ilman että siihen kohdistetaan mitään muotoilutoimenpiteitä. Käytetyin muotoilemattoman lukemisen funktion on luokalle string määritelty funktio getline, jonka prototyyppi on seuraava: std::istream& getline(std::istream& is, std::string& str, char delim = ’\n’); Funktio palauttaa ensimmäisen parametrin. Kolmas parametri voidaan jättää pois, jolloin lopettamismerkkinä on rivinvaihtomerkki (’\n’). Funktio toimii seuraavasti: 1. Tyhjennetään parametrina annettu merkkijono. 2. Luetaan silmukassa annetusta vuosta merkkejä ja lisätään ne merkkijonoon, kunnes tapahtuu jokin seuraavista: a. Luetaan tiedoston loppumerkki, jolloin eof-metodi palauttaa arvon true, b. Luettu merkki oli annettu lopetusmerkki (delim), joka poistetaan vuosta mutta ei lisätä merkkijonoon, tai c. Merkkijonon maksimipituus on saavutettu, jolloin fail-metodi palauttaa arvon true. Jos yhtään merkkiä ei luettu, asetetaan lippu fail, jolloin tietovuon metodi fail() palauttaa arvon true. Funktiota käytetään seuraavasti: 9 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla #include <iostream> #include <string> int main() { using namespace std; cout << ”Anna etunimesi välilyönnillä erotettuna: ”; string etunimet; getline(cin, etunimet); return 0; } Kun toteutetaan konsolipohjaista käyttöliittymää getline-funktiota käyttämällä, niin ennen funktion kutsua pitää poistaa lukuoperaattorin mahdollisesti jäljelle jättämä rivinvaihtomerkki. Tämä tapahtuu kutsumalla istream-luokan metodia ignore, jolla on seuraavat muodot: std::istream& ignore(); std::istream& ignore(int lkm); std::istream& ignore(int lkm, int delim); Näistä ensimmäinen poistaa yhden merkin merkkipuskurista. Toinen poistaa annetun lukumäärän verran merkkejä merkkipuskurista ja kolmas poistaa annettuun lopetusmerkkiin saakka korkeintaan annetun lukumäärän verran merkkejä. Jos lukumäärä on yhtä suuri kuin suurin mahdollinen kokonaisluku1, niin silloin poistetaan kaikki merkit kunnes luetaan annettu lopetusmerkki tai tiedoston loppumerkki. Jos puskuri tyhjenee täysin, jäädään odottamaan lisää merkkejä tuhottavaksi. Konsolipohjaisessa käyttöliittymässä kannattaa käyttää seuraavaa rakennetta: int luku; cin >> luku; cin.clear(); cin.ignore(numeric_limits<int>::max(), ’\n’); string rivi; getline(cin, rivi); 1 saadaan kutsumalla funktiota std::numeric_limits<int>::max(), joka on määritelty otsikkotiedostossa <limits> 10 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla 3. Merkkijonon muuttaminen numeeriseksi arvoksi ja päinvastoin Kuten edellä on tullut ilmi, lukujen lukeminen virheettömästi käyttämällä konsolipohjaista käyttöliittymää on varsin hankalaa. Yksi tapa tämän hankaluuden voittamiseksi on lukea kaikki tiedot konsolilta merkkijonoina ja muuttaa ne tarvittaessa luvuiksi käyttäen hyväksi muotoiltua lukutoimenpiteitä merkkijonoon. Tämä onnistuu käyttämällä vuota std::stringstream, joka on määritelty otsikkotiedostossa <sstream>. Seuraavassa esimerkissä vuota käytetään lukemaan kaksi kokonaislukua: #include #include #include #include <iostream> <iomanip> <string> <sstream> int main() { using namespace std; int luku1 = 0; int luku2 = 0; cout << ”Anna ensimmäinen luku: ”; string luku; getline(cin, luku); if (cin.good()) { stringtream strm1(luku); strm1 >> luku1; } cout << ”Anna toinen luku: ”; getline(cin, luku); if (cin.good()) { stringstream strm2(luku); strm2 >> luku2; } double keskiarvo = (luku1 + luku2) / 2.0; cout << ”Lukujen keskiarvo on ” << setprecision(2) << showpoint << keskiarvo; return 0; } 11 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla Tätä merkkijonovuota voidaan myös käyttää muuttamaan numeerinen tieto merkkijonoksi: #include #include #include #include <iostream> <iomanip> <string> <sstream> int main() { using namespace std; int luku1 = 0; int luku2 = 0; cout << ”Anna ensimmäinen luku: ”; string luku; getline(cin, luku); if (cin.good()) { stringtream strm1(luku); strm1 >> luku1; } cout << ”Anna toinen luku: ”; getline(cin, luku); if (cin.good()) { stringtream strm2(luku); strm2 >> luku2; } double keskiarvo = (luku1 + luku2) / 2.0; stringstream strm3; strm3 << setprecision(2) << showpoint << keskiarvo; string str = strm3.str(); cout << ”Lukujen keskiarvo on ” << str; return 0; } 4. Funktiot C++:ssa aliohjelmaa ja funktiota ei eroteta toisistaan; aliohjelmana voidaan pitää funktiota, jonka paluutyyppi on void, ts. kyseessä on funktio, joka ei palauta mitään. Jotta funktiota voidaan käyttää hyväksi eli kutsua, sen muodon on oltava selvillä siinä ohjelman kohdassa, jossa funktiota kutsutaan. Tätä muodon selvittämistä kutsutaan funktion esittelyksi ja funktion esittelylausetta funktion prototyypiksi. Esittelylauseet voivat olla missä ohjelmakoodin kohdassa tahansa; esittelyt ovat voimassa esittelylauseesta lähtien esittelylauseen sisältävän näkyvyysalueen loppuun asti. Yleensä esittelylauseet kerätään yhteen, joko C++ tiedoston alkuun #include-lauseiden jälkeen tai 12 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla otsikkotiedostoihin. Otsikkotiedostoja on käytettävä silloin, kun funktiota on tarkoitus kutsua useasta eri tiedostosta. Esittelyssä funktion nimeen liitetään sen palauttaman arvon tyyppi ja sen tarvitsemat parametrien tyypit ja mahdollisesti parametrien oletusarvot. C++ -ohjelmassa saa olla samannimisiä funktioita, kunhan niiden parametriluettelot poikkeavat toisistaan. Tätä sanotaan funktion ylikuormittamiseksi. Esittelylauseessa annetaan aina funktion paluuarvon tyyppi, funktion nimi ja sulkeissa oleva parametriluettelo. Esittelylause päättyy puolipisteeseen. Parametriluettelossa on lueteltu funktion tarvitsemat parametrit pilkulla erotettuna toisistaan. Jos funktiolla ei ole yhtään parametria, parametriluettelo on tyhjä. Parametrista ilmoitetaan tyyppi ja mahdollisesti parametrin nimi. Lisäksi parametrilla voi olla oletusarvo. Esittelyssä riittää siis parametrien yhteydessä vain niiden tyyppi, mutta koska esittelylause on ainoa kohta, jossa funktion käyttäjä voi saada tietoa funktiosta, kannattaa parametri nimetä sen tarkoituksen mukaisesti ja sisällyttää nimi esittelylauseeseen. Parametreille voidaan antaa oletusarvoja parametriluettelon lopusta lähtien, jolloin annettuja arvoja käytetään, ellei funktion kutsussa parametria käytetä. Oletusparametrien avulla voidaan vähentää funktioiden kuormittamisen tarvetta. Seuraavat kaksi esimerkkifunktiota palauttavat double-tyyppisen arvon ja niillä on yksi doubletyyppinen parametri: double convertToCelsius(double farenheit); double convertToFarenheit(double celsius); Seuraavassa esittelylausessa esitellään funktio nimeltään calculateDistance, joka palauttaa double tyyppisen arvon. Funktiolla on kuusi double tyyppistä parametria, joista kolmella viimeisellä on oletusarvo: double calculateDistance(double x1, double y1, double z1, double x0 = 0, double y0 = 0, double z0 = 0); Mikäli funktiota kutsutaan vain kolmella parametrilla, käytetään funktiossa muuttujien x0, y0 ja z0 arvoina nollaa. Jotta funktio suorittaisi halutut toiminnot, se pitää määritellä. Samalla funktiolla saa olla vain yksi määrittely, sen sijaan esittelylauseita voi olla useita. Kahden funktion katsotaan olevan samoja jos niiden nimet ovat samoja ja parametriluettelo on sama. Kaksi samannimistä funktiota, joilla on eri parametriluettelo, tulkitaan eri funktioiksi, jotka molemmat on määriteltävä erikseen. Määrittelylause alkaa funktion esittelylauseella ilman lopettavaa puolipistettä, jonka tilalle tulee funktion toiminnat toteuttava määrittelylohko. Määrittelylauseet sijoitetaan aina johonkin C++kooditiedostoon. Määrittelylauseen parametriluettelossa pitää olla kaikilla käytettävillä parametreilla nimi. Määrittelyn yhteydessä parametreille ei saa antaa oletusarvoa. 13 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla Seuraavassa esimerkissä määritellään edellä esitellyt kolme funktiota: #include <cmath> // sqrt – funktio double convertToCelsius(double farenheit) { return 5 * ( farenheit – 32 ) / 9; } double convertToFarenheit(double celsius) { return 32 + 9 * celsius / 5; } double calculateDistance(double x1, double y1, double z1, double x0, double y0, double z0) { using std::sqrt; double distance = (x1 – x0) * (x1 – x0); distance += (y1 – y0) * (y1 – y0); distance += (z1 – z0) * (z1 – z0); distance = sqrt(distance); return distance; } Lisäksi on mahdollista määritellä funktioita, joilla voi olla vaihtuva määrä parametreja, mutta koska C++:ssa on mahdollista kuormittaa funktion nimeä eri parametriluetteloilla, niin tällaisia funktioita ei enää C++:ssa juuri käytetä. Tekniikka on kääntäjäriippuvainen mutta rajapinnat on standardoitu otsikkotiedostossa <cstdarg>. Tärkeimpiä asioita funktioiden toiminnan ymmärtämisessä ja toteuttamisessa on mieltää, miten funktiolle välitetään sen tarvitsemat parametrit. C++-kielessä parametrit voidaan välittää joko arvo- tai viittausparametreina. Funktion arvoparametrit Arvotyyppisessä parametrien välityksessä parametriksi annettu muuttujan arvo kopioidaan määrityksen yhteydessä olevaan muuttujaan. Tämä on parametrinvälityksen oletusmuoto C++:ssa. Tällöin siis alkuperäisen muuttujan arvo ei muutu, vaikka funktiolle annettua arvoa muutetaan funktiossa. Seuraavassa esimerkissä funktiossa muutetaan annettua parametria, joka on sille annettu käyttäen arvoparametria: 14 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla #include <iostream> void muuta(int arvo); int main() { using namespace std; int a = 10; cout << ”a (” << hex << &a << ”) = ” << dec << a << endl; muuta(a); cout << ”a (” << hex << &a << ”) = ” << dec << a << endl; return 0; } void muuta(int arvo) { using namespace std; cout << ”arvo (” << hex << &arvo << ”) = ” << dec << arvo << endl; arvo = 123; cout << ”arvo (” << hex << &arvo << ”) = ” << dec << arvo << endl; } Ohjelma voi tulostaa esimerkiksi seuraavaa (muuttujien muistiosoitteet voivat vaihtua): a (0xfedc3823) = 10 arvo (0xfedc3900) = 10 arvo (0xfedc3900)= 123 a (0xfedc3823)= 10 Tulostuksesta nähdään että pääohjelman muuttujan arvo ei muutu ja että parametrimuuttujan muistiosoite on eri kuin pääohjelman muuttujan. Funktion viiteparametrit Jos halutaan että funktiossa muutettu arvo päivittyy myös parametrina annettuun muuttujaan, tieto on syytä välittää käyttäen viitetyypistä parametrin välitystä. Tällöin ei välitetä funktiolle muuttujan arvoa vaan sen osoite, jota kautta kutsuva funktio ja kutsuttava funktio jakavat saman muistialueen ja jommassakummassa funktiossa tapahtuva muutos näkyy molemmissa funktioissa. Kun käytetään viiteparametria, niin ei kopioida parametrina annettua muuttujaa toiseen muuttujaan, mikä on tärkeää varsinkin silloin kun välitetään suuria olioita. Tästä syystä C++:ssa on tapana välittää olioparametrit käyttäen viiteparametria, koska olioiden kopioiminen voi olla erittäin paljon resursseja vievä toimenpide. Arvoparametrina välittäminen ei aina ole edes mahdollista. Kun käytetään viittaustyyppistä viiteparametria, ei tarvitse välittää eksplisiittisesti muuttujien osoitteita, vaan osoitteiden välittämisen hoitaa kääntäjä. Viittaustyyppisiä muuttujia käytetään täsmälleen samoin kuin normaaleja muuttujia. Viittaustyyppinen parametri merkitään 15 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla parametriluettelossa parametrin tyypin yhteydessä olevalla & -merkillä. Seuraavassa esimerkissä käytetään viittaustyyppistä parametrien välitystä: #include <iostream> void muuta(int& arvo); int main() { using namespace std; int a = 10; cout << ”a (” << hex << &a << ”) = ” << dec << a << endl; muuta(a); cout << ”a (” << hex << &a << ”) = ” << dec << a << endl; return 0; } void muuta(int& arvo) { using namespace std; cout << ”arvo (” << hex << &arvo << ”) = ” << dec << arvo << endl; arvo = 123; cout << ”arvo (” << hex << &arvo << ”) = ” << dec << arvo << endl; } Ohjelman tulostus on seuraavan kaltainen (ainoastaan muistiosoitteet voivat vaihtua): a (0xfedc3823) = 10 arvo (0xfedc3823) = 10 arvo (0xfedc3823)= 123 a (0xfedc3823)= 123 Usein halutaan, varsinkin olioita parametreina välitettäessä, välttää parametrin kopiointi, vaikka parametrimuuttujaa ei haluttaisikaan muuttaa funktiossa. Tällöin käytetään viiteparametreja. Jotta muuttujaa ei vahingossa muutettaisi funktiossa, voidaan funktiolta evätä oikeus muuttaa parametrin arvoa välittämällä parametri vakioviittauksen avulla, kuten seuraavassa esimerkissä: 16 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla #include <iostream> #include <string> std::ostream& print(const std::string& merkkijono); std::ostream& printNL(const std::string& merkkijono); int main() { printNL(”Terve maailma\n”); return 0; } std::ostream& print(const std::string& str) { return std::cout << str; } std::ostream& printNL(const std::string& str) { return print(str) << std::endl; } Jos jommassakummassa funktiossa yritettäisiin muuttaa str-parametrin arvoa, kääntäjä antaisi virheilmoituksen. Parametri välitetään siis vakiomuotoisena viiteparametrina eikä arvoparametrina tehokkuussyistä, koska tällöin ei muodosteta str-muuttujaan kopiota tulostettavasta merkkijonosta. Osoittimet funktion parametreina Kun funktion parametrina on osoitin, se välitetään käyttäen arvovälitystä. Osoitin viittaa kuitenkin johonkin muistiosoitteeseen, joten sen kautta voidaan vaikuttaa samaan osoitteeseen kuin alkuperäinen osoitinmuuttuja. Näin ollen osoittimia käyttämällä saa aikaan vaikutukseltaan samantapaisen funktion kuin viiteparametreilla. Näissä kummassakin tapauksessa voidaan puhua muuttujaparametreista, kuten teoksessa [Hie], s. 199. Kun parametri välitetään muuttujaparametrina käyttäen osoitinta parametrina, ei funktiolle annetakaan muuttujan arvoa vaan osoite, jossa muuttuja sijaitsee tietokoneen muistissa. Jokaisella muuttujalla paitsi literaalilla on muistiosoite, myös vakiomuuttujilla. Muuttujan osoite saadaan selville osoiteoperaattorin & avulla. Toinen osoitinmuuttujien yhteydessä käytettävä operaattori on sisältöoperaattori *. Muistipaikan sisältöön viittaamista tämän operaattorin avulla kutsutaan myös dereferenssiksi. Parametrien välityksessä osoiteoperaattoria käytetään funktiokutsun yhteydessä, ja sisältöoperaattoria käytetään kutsuttavan funktion määrittelyssä, kun halutaan lukea tai kirjoittaa parametrin arvoa. Parametriluettelossa osoitintyyppinen parametri ilmaistaan liittämällä tarkoitetyyppiin * - merkki. Seuraavassa esimerkissä on toteutettu edellisen esimerkin kanssa samalla tavoin toimiva ohjelma käyttäen osoitintyyppistä muuttujaparametria: 17 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla #include <iostream> void muuta(int* arvo); int main() { using namespace std; int a = 10; cout << ”a (” << hex << &a << ”) = ” << dec << a << endl; muuta(&a); // Parametriksi muuttujan a muistiosoite cout << ”a (” << hex << &a << ”) = ” << dec << a << endl; return 0; } void muuta(int* arvo) { using namespace std; cout << ”arvo (” << hex << arvo << ”) = ” << dec << *arvo << endl; *arvo = 123; cout << ”arvo (” << hex << arvo << ”) = ” << dec << *arvo << endl; } Ohjelman tulostus on seuraava (jälleen ainoastaan muistiosoitteet voivat vaihtua): a (0xfedc3823) = 10 arvo (0xfedc3823) = 10 arvo (0xfedc3823)= 123 a (0xfedc3823)= 123 Kumpaa muuttujaparametrien välitystapaa käyttää, on lähes puhtaasti tyylikysymys, mutta osoitemuuttujat ja viittausmuuttujat eroavat toisistaan seuraavasti: Viittausmuuttujaa käytettäessä pitää olla olemassa jokin muuttuja, johon viittausmuuttuja kiinnitetään lopullisesti. Viittausmuuttujan kohdetta ei voi vaihtaa määrityksen jälkeen. Osoitinmuuttujan määrityksen yhteydessä osoittimen arvon voi asettaa nollaksi, toisin sanoen osoitettavaa muuttujaa ei tarvitse olla olemassa. Osoitinmuuttuja voidaan muuttaa osoittamaan toiseen muuttujaan myös määrityksen jälkeen. Funktion paluuarvo Funktion paluuarvo voi olla jokin seuraavista: void-tyyppi, jolloin funktio ei palauta mitään (funktio on aliohjelma), tavallinen tyyppi, jolloin paluuarvo kopioidaan, osoitintyyppi, jolloin välitetään palautettavan arvon sisältämän muuttujan osoite viittaustyyppi, jolloin välitetään muodostettu viittausmuuttuja, joka viittaa palautettavan arvon sisältämän muuttujaan. 18 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla Kahdessa viimeisessä tavassa on yksi C++-kielen Akilleen kantapäistä. Se muuttuja, johon viitataan, on voitu tuhota, kun funktiosta palataan sitä kutsuvaan funktioon. Tällainen tilanne voi syntyä varsinkin, kun palautettava viite on viittaus parametrimuuttujaan tai paikalliseen muuttujaan. Nykyaikaiset kääntäjät antavat kyseisissä tilanteissa varoituksia ja virheilmoituksia, joten kannattaa tulkita tarkoin myös kääntäjän antamat varoitukset, nämäkin voivat johdattaa ohjelmointivirheen jäljille. Taulukkotyypin käyttö parametrina ja paluuarvon tyyppinä C++:ssa on mahdollista käyttää ohjelmissa C-kielen mukanaan tuomaan taulukkotyyppiä, jonka koko määritellään käännösaikana eikä sitä voida ohjelmallisesti muuttaa. Taulukon alkioiden tyyppi voi olla mikä tahansa C++:n tyyppi, jolle on määritelty oletusmuodostin, myös taulukkotyyppi, jolloin voidaan rakentaa moniulotteisia taulukoita. Oletusmuodostimen käsitteeseen palataan perehdyttäessä oliopohjaiseen ohjelmointiin. Taulukkomuuttuja määritellään muuttujan perään lisättävillä hakasulkeilla, joiden sisällä ilmaistaan taulukon koko. Tätä kokoa ei ole mahdollista ohjelmallisesti selvittää, joten taulukon koko on aina välitettävä taulukkoa käyttäville funktioille. Taulukon nimi on samalla osoitin ensimmäiseen alkioon. Taulukko välitetään funktioille osoittimena taulukon ensimmäiseen alkioon. Vastaavasti, haluttaessa palauttaa taulukko, paluutyypin arvona on osoitin taulukon ensimmäiseen alkioon. Seuraavassa on esitelty funktiot, joiden ensimmäisenä parametrina on taulukkotyyppi ja toisena tyyppinä taulukon alkioiden lukumäärä. Ensimmäinen funktio palauttaa luettujen alkioiden lukumäärän ja toinen funktio ei palauta mitään. int lueLampotilat(double * lampotilat, int koko); void tulostaLampotilat(double * lampotilat, int elementit); 19 Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto 812347A Olio-ohjelmointi: Johdanto ohjelmointiin C++:lla Niiden toteutukset voivat olla esimerkiksi: int lueLampotilat(double * lampotilat, int koko) { cin.clear(); int count = 0; while (cin.good() && count < koko){ cout << ”Anna lämpötila: ”; double arvo; cin >> arvo; if (cin.good()) // jos lukeminen onnistui { lampotilat[count] = arvo; ++count; } } if (!cin.good()) return -1; return count; } void tulostaLampotilat(double * lampotilat, int elementit) { for (int i = 0; i < elementit; ++i) cout << i + 1 ”. kuukausi: ” << lampotilat[i] << endl; } Funktioita voitaisiin käyttää pääohjelmassa esimerkiksi seuraavasti: int main() { const int TAULU_KOKO = 12; double taulu[TAULU_KOKO]; int lkm = lueLampotilat(taulu, TAULU_KOKO); tulostaLampotilat(taulu, lkm); return 0; } Olioita sisältäviin taulukoihin palataan myöhemmin. Lähteet [CppTut] Juustila, A. Kettunen, H. Kilpi, T. Räisänen, T. Vesanen A.: C++ -tutoriaali (Opetusmoniste) 2004, Saatavana http://herkules.oulu.fi/isbn9514279425/ [Hie] Hietanen, P.: C++ ja olio-ohjelmointi, 3. laitos, Docendo 2004 20