C++/Moderni C++
ESIPUHE
muokkaaLähdin kirjoittamaan uutta C++-oppikirjaa korvaamaan ja täydentämään olemassaolevaa kirjaa C++. Tarkoitus on luoda nopeasti suoraan nykyaikaiseen laadukkaaseen koodiin pureutuva teos, joka ei juurikaan haaskaa aikaa perusteiden kanssa. Lukijalta edellytetään taitoa uusien asioiden nopeaan omaksumiseen, mutta ei aiempaa ohjelmointikokemusta. Kirja on siis suunnattu ihmisille, joista todella saattaisi hyviä ohjelmoijia tulla, eikä asioita jäädä vääntämään ratakiskosta. Käytettävä esitystapa eroaa täysin perinteisistä C++-kirjoista, jotka tyypillisesti etenevät kielen kehityshistorian järjestyksessä C-tyyppisestä ohjelmoinnista olio-ohjelmointiin ja usein itse asiassa päättyvät siihen, jättäen leijonanosan kielestä kokonaan käsittelemättä.
Vaikka C++:lla on pitkät juuret aina vuoteen 1979 saakka, standardoitiin kieli ensi kertaa vasta vuonna 1998. Tätä ennen sen kehitys oli ollut hyvin vapaata ja eri valmistajien kääntäjät varsin epäyhteensopivia keskenään. Painopiste oli olio-ohjelmoinnin lisäämisessä C:hen, eikä 1990-luvun alkupuolen C++ itse asiassa eronnut kovin paljoa 1995 julkaistusta Java 1.0:sta. Samoihin aikoihin suuri mullistus oli kuitenkin tulossa. C++:aan lisättiin tuki geneeriselle ohjelmoinnille, jolloin STL-nimisestä aiemmin Ada-ohjelmointikielelle kehitetystä kirjastosta voitiin ottaa kieleen suuri määrä kehittyneitä työkaluja. Standardikirjaston kehityksen mukana myös itse kieleen tehtiin lisää parannuksia ja vasta tässä vaiheessa oli moderni C++ syntynyt. Tietenkin meni vielä vuosia ennen kuin kääntäjät saatiin korjattua standardia noudattaviksi ja vielä tänäänkin monet kuitenkin ohjelmoivat käytännössä kokonaan ilman kielen tarjoamia hienoja työkaluja. Tällaisesta ominaisuuksien käyttämättömyydestä C++:aa hyvin osaavat ohjelmoijat käyttävät usein leikkisästi termiä C/C++, jolla siis viitataan C-tyyliseen C++:aan. Osansa asiaan on luonnollisesti myös sillä, että nimenomaan modernia C++:aa osaamattomat niputtavat kielet samaan pakettiin, vaikka kielillä on vain vähän yhteistä.
On yksi asia, jota C++ erityisesti ei ole: kaunis. Syntaksia ei turhaan ole kaunisteltu ja käyttäjä joutuu perusasioita toteuttaakseen kirjoittamaan paljon enemmän kuin esimerkiksi Pythonissa. Monesti C++-koodi näyttää kokemattoman silmissä hirvittävältä sotkulta, mutta kokemuksen karttuessa syntaksisokerin puutteen oppii hyväksymään (tai ainakin sietämään) ja aiemmasta sotkusta näkee suoraan mitä siinä tehdään. Koska kieli sallii myös tehokkaan omien työkalujen määrittelyn, on se erityisen soveltuva suurten ja monimutkaisten järjestelmien toteuttamiseen, jolloin myös koodin määrä jää helposti pienemmäksi kuin muuten helpomman näköisillä kielillä. Toinen olennainen piirre C++:ssa - ja samalla merkittävä ero C:hen - on turvallisuus. Resurssivuodoista, puskuriylivuodoista, odottamattomista syötearvoista tai muista tyypillisistä tietoturva-aukoista ei kertakaikkiaan ole vaaraa, kun noudatetaan hyvää C++-ohjelmointitapaa.
Kirja perustuu vuonna 2003 julkaistuun standardiin C++03 (ISO/IEC 14882:2003), joka ei merkittävästi eroa standardin ensimmäisestä versiosta C++98. Koska kaikki kirjassa käytetty koodi on laadukasta standardin mukaista C++:aa, toimii se millä tahansa C++-kääntäjällä, millä tahansa alustalla. Koska varsin radikaaleja lisäyksiä kieleen tuova C++0x on jo ovella, mainitaan kirjassa myös aika-ajoin miten siinä tullaan tässä C++03:lla tehtyjä asioita tekemään. Kirjassa olevat koodiesimerkit on kirjoitettu englanniksi, kuten on hyvän ohjelmointitavan mukaista tehdä, koska yleensä ennemmin tai myöhemmin koodiasi joutuu joku suomea osaamaton lukemaan. Toisaalta samalla vältetään ääkkösten kanssa tulevat merkistöongelmat. Englanninkielisiä termejä on myös paikoin käytetty joko sulkeissa suomenkielisen vastineen perässä tai ainoastaan. Tähän on päädytty, koska suurin osa dokumentaatiosta on englanniksi (erityisesti termejä virallisesti määrittelevä C++-standardi) ja suomenkieliset käännökset eivät ole riittävän vakiintuneita (useille termeille on monia synonyymisiä käännöksiä).
Toistaiseksi kirja on pahasti kesken, mutta rakenne alkaa olla kunnossa. Puuttuvien osien täydentäminen on toivottavaa!
KIELEN PERUSTEITA
muokkaaEnsimmäinen ohjelma
muokkaaAivan aluksi tarvitset C++-kääntäjän ja tekstieditorin. Emme käsittele asiaa kirjassa sen enempää, mutta jos et tiedä mitä asentaisit, voit käyttää vapaata kehitysympäristöä Code::Blocks, jossa tulee kaikki tarpeellinen mukana. Mikä tahansa nykyaikainen (standardin mukainen) C++-kääntäjä, tekstieditori tai kehitysympäristö kuitenkin käy aivan yhtä hyvin kirjan esimerkkiohjelmien kääntämiseen. Jos käytät integroitua kehitysympäristöä (kuten Code::Blocks), luo kutakin ohjelmaa varten uusi "C++ console application"-tyyppinen (tai vastaava) projekti. Lisätietoa työkalujen asentamisesta ja käytöstä kirjassa C++.
1: #include <iostream> 2: #include <string> 3: 4: int main() { 5: std::cout << "Enter your name: "; 6: std::string name; 7: std::getline(std::cin, name); 8: std::cout << "Hello, " << name << std::endl; 9: std::cout << "Did you know that your name contains " << name.size() << " characters?" << std::endl; 10: std::cin.ignore(); 11: }
Ohjelma ei ehkä vaikuta erityisen mielekkäältä, mutta se on sopivan pieni pala haukattavaksi alkuun. Kopioi se kehitysympäristöösi, mieluummin käsin kuin copy&pastella, tallenna se nimellä nametest.cpp ja varmista että saat sen käännettyä (Build, Compile) ja ajettua (Run). Huomaa että rivinumerointia ei tule lähdekoodiin kirjoittaa. Jos et saa ohjelmaa ajettua, selvitä ongelma, äläkä jatka lukemista ennen kuin saat kehitysympäristösi toimimaan.
Kaikki ohjelman toiminta tapahtuu riveillä 5-10 ja muu on vain välttämätöntä rakennetta. #include <iostream> ja #include <string> ovat rivit, joita tulet tarvitsemaan lähes kaikissa tämän kirjan ohjelmissa, mutta emme vielä tässä tutustu niihin sen lähemmin. int main() { ... } taasen on nk. main-funktio, joka on siis se osa ohjelmasi, joka suoritetaan, kun käynnistät ohjelman. Varsinainen koodi tulee aaltosulkeiden sisään ja se on tavallista sisentää, esimerkkikoodin mukaisesti, hieman irti vasemmasta reunasta, painamalla Tab-näppäintä rivin alussa.
Huomasit varmaan myös että kaikki varsinaiset koodirivit päättyvät puolipisteeseen? C++ ei välitä rivinvaihdoista, vaan tulkitsee rivin loppumisen nimenomaan puolipisteen perusteella. Puolipisteen unohtuminen pois on kuitenkin valitettavasti yksi yleisimmistä ohjelmointivirheistä ja kääntäjä voi joskus antaa varsin kryptisiäkin virheilmoituksia sen vuoksi, joten puolipisteiden kanssa on syytä olla huolellinen.
Toinen ilmeinen asia koodissa on std::-etuliite, jota käytetään kaikkien standardikirjastoon kuuluvien asioiden edessä. Sitäkin käsitellään myöhemmin tarkemmin.
Mitä ohjelmassa tapahtuu?
muokkaaSuoritus alkaa main-funktion alusta, eli riviltä 5:
std::cout << "Enter your name: ";
Tässä output streamiin std::cout, eli näytölle, tulostetaan (<<) lainausmerkeissä oleva teksti. Lainausmerkit kertovat kääntäjälle, että kyseessä on merkkijono, joka pitää tulostaa sellaisenaan. Perään ei automaattisesti tule rivinvaihtoa, joten kursori jää rivin loppuun odottelemaan ja ohjelman suoritus jatkuu välittömästi seuraavalta riviltä:
std::string name;
Määritellään muuttuja, jonka nimeksi annetaan name. Muuttujaan voidaan tallentaa tietoa, tässä tapauksessa merkkijono (engl. string), koska muuttujan tyyppi on std::string. Tietotyypit siis määräävät mitä muuttuja voi sisältää ja ne ovatkin C++:ssa hyvin keskeisessä roolissa. Muuttuja vaatii muistia sisältönsä tallentamiseen ja muisti varataan muuttujaa määritellessä, siis tällä rivillä. Muistin vapauttamisesta ei tarvitse huolehtia, sillä kääntäjä hoitaa tuhoamisen automaattisesti, kun muuttuja aikanaan lakkaa olemasta (tässä tapauksessa ohjelman suorituksen päättyessä). Merkkijono on aluksi tyhjä, "", eli siinä ei ole ainoatakaan merkkiä.
std::getline(std::cin, name);
Nyt äsken luotuun muuttujaan name luettiin näppäimistöltä yksi kokonainen rivi kutsumalla funktiota std::getline. Ohjelman suoritus pysähtyy siis tälle riville, kunnes käyttäjä vastaa ruudulle aiemmin tulostettuun kysymykseen. Kun käyttäjä kertoo nimensä, tallennetaan se muuttujaan name, korvaten mahdollinen aiempi sisältö.
std::getline(...) on yhden rivin lukeva funktio, joka ottaa kaksi parametria (engl. parameter, argument):
- input stream, josta luetaan (tässä näppäimistö, eli std::cin)
- muuttuja, johon luettu rivi tallennetaan (tässä merkkijono name)
Parametrien määrä vaihtelee funktioittain. Joillain funktioilla parametreja ei ole lainkaan, jolloin funktion perään merkitään vain tyhjät sulkeet f(). Kun funktio ottaa parametreja, täytyy ne kuitenkin ilmoittaa oikeassa järjestyksessä, eikä välistä saa jättää mitään pois.
std::cout << "Hello, " << name << std::endl;
Ohjelma tervehtii käyttäjää tämän äsken syöttämällä nimellä. Huomaa, että ruudulle tulostettavat asiat erotetaan toisistaan tulostusoperaattorilla << (engl. stream output operator), ja että niitä voi laittaa samalle riville niin monta kuin haluaa. Tässä tulostetaan ensin merkkijono "Hello, ", sitten name-muuttujan sisältö ja lopuksi rivinvaihto (std::endl).
std::cout << "Did you know that your name contains " << name.size() << " characters?" << std::endl;
Ohjelma ei kuitenkaan edellisen tulosteen jälkeen jää odottelemaan mitään, vaan jatkaa välittömästi kertomalla nimen pituuden, joka saadaan funktiokutsulla name.size(). Muuttujan tyypistä riippuu voidaanko size-funktiota yleensäkään kutsua, ja mitä se palauttaa, jos voidaan. Tässä tapauksessa tyyppi oli std::string, jolle size() palauttaa merkkijonon pituuden.
std::cin.ignore();
Tällä rivillä ei ohjelman toiminnan kannalta ole niinkään merkitystä, mutta lisäsin sen jottei ohjelma päättyisi heti tulosteiden jälkeen, jolloin ikkuna saattaa sulkeutua niin nopeasti, ettet näe tulosta. Nyt se odottaa että painat enteriä ennen ohjelman päättymistä. ignore() siis lukee yhden merkin (esim. rivinvaihto) ja heittää sen suoraan menemään, mutta ohjelman suoritus kuitenkin pysähtyy, kunnes käyttäjä syöttää merkin.
Jatkossa ei tätä riviä esimerkkikoodeissa käytetä, mutta jos tarvitset sitä ikkunan avoinna pitämiseen, lisää rivi main-funktion loppuun.
- Huom: Vaikka ignore() lukeekin vain yhden merkin, ei ohjelman suoritus välttämättä silti pääty kun syötät kirjaimen tai yleensäkään jotain muuta kuin rivinvaihdon. Tämä johtuu rivipuskuroidusta I/O:sta, josta johtuen ohjelma saa yleensä rivin ensimmäisenkin merkin luettua vasta, kun käyttäjä painaa Enteriä. Kun syötettä luetaan rivi kerrallaan, esim. std::getline:llä, ei ongelmasta tarvitse kuitenkaan välittää.
Tyyppijärjestelmä
muokkaaTärkeimmät alussa vastaan tulevat tietotyypit ovat:
- bool - totuusarvo, kyllä tai ei (true tai false)
- int - kokonaisluku (negatiivinen tai positiivinen, esim. -2 tai 0)
- double - liukuluku (reaaliluvut, esim. -12.7 tai 0.0)
- char - merkki (lähdekoodissa yksinkertaisissa lainausmerkeissä, esim. 'a')
- std::string - merkkijono (lähdekoodissa tavallisissa lainausmerkeissä, esim. "teksti")
- std::size_t - lukumäärä (positiivinen kokonaisluku, esim. 100)
Näistä neljän eteen ei std::-määrettä tule, koska ne ovat kielen sisäänrakennettuja tyyppejä, eivätkä siis peräisin standardikirjastosta, kuten std::string ja std::size_t.
On tärkeää ymmärtää mitä eroa on esimerkiksi merkillä '0', merkkijonolla "0", kokonaisluvulla 0 ja liukuluvulla 0.0. Lähtökohtaisesti muunnokset tyyppien välillä eivät onnistu ilman nk. eksplisiittistä tyyppimuunnosta ja esim. merkkijonon sijoittaminen int-tyyppiseen muuttujaan ei onnistu (kääntäjä antaa virheilmoituksen). Joidenkin tyyppien välillä implisiittinen tyyppimuunnos kuitenkin onnistuu, esim. int-tyyppinen arvo muuntuu tarvittaessa suoraan double-tyypiksi.
Peruslaskutoimitukset
muokkaaTyyppijärjestelmä määrää myös peruslaskutoimitusten toiminnan. Esimerkiksi kahden merkkijonon liittäminen yhteen onnistuu "yhteenlaskulla" ja lukuarvoille käytettävissä ovat normaalit laskutoimitukset +, -, * ja /, sekä kokonaisluvuille lisäksi jakojäännös % ja myöhemmin käsiteltävät binääriset operaatiot.
std::string s1 = "123"; std::string s2 = "456"; s1 = s1 + "0" + s2;
Tässä s1:n arvoksi asetetaan "1230456".
- Huom: Merkkijonoliteraaleja (lainausmerkeissä olevaa tekstiä) ei voi liittää toiseen merkkijonoliteraaliin +:lla, vaan ne tulee kirjoittaa vain peräkkäin ilman +-merkkiä tai vaihtoehtoisesti tyyppimuuntaa std::stringiksi ennen liittämistä. Tämä johtuu siitä että merkkijonoliteraalit ovat C-tyylisiä merkkijonoja, eivätkä C++:n std::string-tyyppiä. Edellisessä esimerkissä lisättiin "0" s1:n perään kuitenkin onnistuneesti, koska edes yhteenlaskun yhden osapuolen ollessa std::string, osaa kääntäjä muuntaa toisenkin implisiittisesti. Eksplisiittinen tyyppimuunnos, jos sellaista tarvitaan, tehdään kirjoittamalla std::string("...").
Kun kyseessä on muuttujaan tehtävä muutos muotoa i = i * 10, kannattaa tämän sijaan käyttää lyhennysmerkintää i *= 10. Tämä toimii kaikille operaattoreille vastaavalla tapaa.
Jakolaskusta on huomattava, että jos molemmat operandit ovat kokonaislukuja, on myös tulos kokonaisluku, kohti nollaa pyöristettynä. Esim. 19 / 10 antaakin arvoksi ykkösen, eikä 1.9:ää. Jos halutaan liukulukujakolasku, on ainakin yhden operandin oltava liukuluku, eli kirjoitetaan 19.0 / 10.0. Molempien operandien ollessa integer-muuttujia (ei literaaleja) joudutaan käyttämään tyyppimuunnosta, esim. double(i) / j, jossa yksi muuttujista eksplisiittisesti muunnetaan liukuluvuksi, jolloin toinenkin muuntuu automaattisesti.
Integer-tyyppien (int, std::size_t, yms) jakolaskuissa ja jakojäännöksessä on pyöristyksen lisäksi huomioitava myös nollalla jakamiset, joiden toimintaa standardi ei määrittele (käytännössä ohjelma yleensä kaatuu hämäävällä virheilmoituksella Floating-Point Exception). Ennen jakolaskua tai jakojäännöstä on siis syytä varmistaa, ettei jakaja ole nolla tai vaihtoehtoisesti suorittaa jakolasku liukuluvuilla, jolloin tulokseksi tulee ääretön, miinus ääretön tai NaN (not a number; tuloksena esim. nollan jakamisesta nollalla). Lisäksi tulee huomata, ettei jakojäännöstä voi laskea liukuluvuilla (kääntäjä antaa virheen, jos yrität).
- Varoitus: Vaikka mieli tekisi laskea kaikki mahdollinen liukuluvuilla ja muuntaa tulos vain tarvittaessa takaisin integer-tyyppiin, ei näin kannata tehdä, sillä liukulukulaskutoimitukset ovat aina epätarkkoja. Helposti jo pienilläkin luvuilla laskiessa laskutoimituksen tulokseksi tuleekin hitusen liian pieni arvo, joka integeriksi muuntaessa pyöristyy kohti nollaa, yhtä pienemmäksi kuin on tarkoitus. Samasta syystä liukulukujen yhtäsuuruutta ei kannata testata, jos niillä on suoritettu laskutoimituksia, sillä jopa samojen laskutoimitusten tuloksena voi pyöristysvirheiden vuoksi olla hieman eri arvot, jolloin lukuja ei pidetäkään yhtä suurina.
Ohjausrakenteet
muokkaaEdellä ollut ohjelma suoritettiin järjestyksessä main-funktion ensimmäiseltä riviltä eteenpäin rivi kerrallaan, loppuun saakka. Monimutkaisemmissa ohjelmissa tarvitaan kuitenkin lisäksi myös ohjausrakenteita, joilla samaa koodia saadaan toistettua useita kertoja tai koodin suoritus riippuu esim. muuttujan arvosta. Seuraavassa esimerkki:
#include <iostream> #include <string> int main() { std::cout << "Enter password:" << std::endl; std::string s; std::getline(std::cin, s); if (s == "secret") { std::cout << "Correct password!" << std::endl; } else { std::cout << s << " is not the correct password" << std::endl; } std::cout << "Terminating..." << std::endl; }
Tässä käytettiin if-else-ohjausrakennetta ohjelman suorituksen ohjaamiseen. Muuttujan s sisältöä verrataan ehtolauseessa merkkijonoon "secret". Jos salasana täsmää, suoritetaan if-lauseen sisällä oleva koodi ja jos se ei täsmää, suoritetaan else-osion koodi. Tämän jälkeen suoritus jatkuu riville, jossa tulostetaan teksti "Terminating...".
Osio else ei ole pakollinen ja itse asiassa useimmiten sitä ei olekaan lainkaan. Toisaalta mukaan voidaan ottaa myös else if-osioita, joissa testataan jotain muuta ehtoa, jos alkuperäinen ehto ei toteutunut. Lopullinen else-osio (jos sellainen on olemassa) suoritetaan ainoastaan, jos yksikään ehdoista ei toteudu.
if (ehtolause1) { } else if (ehtolause2) { } else if (ehtolause3) { ... } else { }
Kun halutaan valita arvo kahdesta eri vaihtoehdosta jonkin ehdon perusteella, kannattaa käyttää if-else:n sijaan ehto-operaattoria ehto ? arvo1 : arvo2, esim:
std::cout << players << (players == 1 ? " player" : " players") << std::endl;
Tässä ehto-operaattorin ympärille on laitettu sulkeet, jotta saadaan oikea laskujärjestys. Muulloin myös operaattoriin kuulumattomat osat lausekkeesta tulisivat siihen mukaan.
Samaa ohjelmakoodia voidaan suorittaa toistuvasti uudelleen while-loopilla, joka suorittaa sisältöään uudelleen aina kun ehtolause on voimassa. Jos ehtolause ei ole voimassa looppiin ensimmäistä kertaa saavuttaessa, ei loopin sisältöä suoriteta kertaakaan.
while (ehtolause) {}
Harvemmin vastaan tuleva versio samasta asiasta on do-while-rakenne, joka suorittaa sisällään olevan koodin aina vähintään kerran ja testaa ehtolausetta vasta seuraavilla kierroksilla. Huomaa tässä loppuun tuleva puolipiste.
do { // ... } while (ehtolause);
While-loopilla voidaan toteuttaa 10 kertaa toistettava silmukka, joka tulostaa ruudulle luvut 0-9, asettamalla ensin i:n nollaksi ja sitten toistaen silmukkaa aina kun i:n arvo on pienempi kuin kymmenen. Silmukan sisällä i:n arvoa kasvatetaan joka kierroksella yhdellä:
{ int i = 0; while (i < 10) { std::cout << i << std::endl; ++i; } }
Tässä ulommaiset aaltosulkeet rajaavat muuttujan i eliniän niin, ettei muuttujaa vahingossa käytetä tämän koodiblockin ulkopuolella.
Koska tällaisia looppeja tarvitaan hyvin usein, on niitä varten erillinen rakenne, for-looppi. Tämä koodi tekee täsmälleen saman asian kuin edellinen pidempi versio:
for (int i = 0; i < 10; ++i) { std::cout << i << std::endl; }
int i = 0 on alustuslause, jossa voidaan määritellä uusia muuttujia tai suorittaa jokin tavallinen lause. Alustuslauseessa määritellyt muuttujat ovat olemassa vain loopin sisällä, eivätkä ne jää "roikkumaan", kun looppi päättyy.
Puolipisteen jälkeen tulee ehtolause i < 10, jota testataan joka kerta ennen loopin sisällön suorittamista. Toisen puolipisteen jälkeen tulee i:n arvoa kasvattava increment-lause, joka suoritetaan jokaisen kierroksen jälkeen, juuri ennen seuraavan kierroksen ehtolausetta.
Jos ohjausrakenteen sisällä on vain yksi lause, voidaan myös aaltosulkeet jättää pois ja kirjoittaa sisältö samalle riville ohjauslauseen perään, jolloin ohjelmakoodi pysyy selkeänä ja rivimäärä pienenä. Jos sisälle tulevia lauseita on useampia, ovat aaltosulkeet pakolliset.
Ehtolause voi olla vertailu, kuten edellisissä esimerkeissä, mutta arvoksi kelpaa myös mikä tahansa lukuarvo. Jos arvo on mitä tahansa muuta kuin 0, katsotaan testi onnistuneeksi. Itse asiassa myös vertailuoperaattorit palauttavat lukuarvon - 1:n jos testi on tosi, 0:n muulloin.
Vertailuoperaattorit ovat:
- yhtäsuuruus ==
- erisuuruus !=
- pienempi kuin <
- pienempi tai yhtäsuuri kuin <=
- suurempi kuin >
- suurempi tai yhtäsuuri kuin >=
Monesti halutaan testata useampaa asiaa kerralla, esimerkiksi että luku on tietyllä välillä:
if (num >= min && num <= max) ...
Tässä useita ehtolauseita on yhdistetty loogisella AND-operaattorilla && yhteen. AND-operaatiossa kummankin ehdon pitää olla tosi, jotta tuloseksi tulisi arvo 1 (tosi). Toinen vastaava operaattori on looginen OR ||, jossa riittää että kumpi tahansa puoli on tosi.
Loogisia operaatioita käytettäessä on hyvä tuntea C++:ssa (useimpien muiden kielten tapaan) käytössä oleva short circuiting -ominaisuus. Tämä tarkoittaa että ehtolauseita suoritetaan vain sen verran kuin on tarpeen tuloksen saamiseksi. Jos esimerkiksi &&:n vasemmalla puolella oleva lause ei ole tosi, ei oikealla puolella olevaa lausetta ajeta lainkaan, koska tulos (epätosi) tiedetään muutenkin. Tällä on toki osansa ohjelman optimoinnissa, mutta tärkeämpi vaikutus on siinä että se mahdollistaa "vaarallisten" lauseiden laittamisen oikealle puolelle, kun vasemmalla testataan että ne voidaan turvallisesti suorittaa. Esimerkiksi nollalla jako voidaan välttää tällaisella rakenteella:
if (div && num / div > 10) { ... }
Kuten aiemmin todettiin, mikä tahansa lukuarvo kelpaa ehtolauseeksi. Tässä muuttujaa div on käytetty vasemmalla puolella suoraan, sen sijaan että kirjoitettaisiin div != 0. Tulos on sama. Short circuiting estää jakolaskun suorittamisen mikäli div on 0, jolloin ohjelma ei kaadu virheelliseen jakolaskuun.
AND- ja OR-operaatioita voidaan yhdistää myös useita peräkkäin ja tarvittaessa vielä yhdistellä sulkein niitä monimutkaisemmiksi rakenteiksi.
Funktiot
muokkaaKäytimme jo ensimmäisessä esimerkissä standardikirjaston funktiota std::getline, mutta seuraavaksi määrittelemme kaksi omaa funktiota. Omien funktioiden määrittely kannattaa aina, kun muuten joutuisi copy&pastella monistamaan melkein samanlaista koodia useaan paikkaan. Vaikka funktiota kutsutaan useaan kertaan, on se määritelty vain kertaalleen ja näin koodin monistamiselta vältytään.
Ensimmäinen omista funktioistamme tulostaa ruudulle rivin tekstiä, joten annamme sille nimeksi println. Määrittelemme myös funktion inputln, joka lukee rivin näppäimistöltä ja antaa sen paluuarvonaan (engl. return value).
#include <iostream> #include <string> void println(std::string line) { std::cout << line << std::endl; } std::string inputln() { std::string tmp; std::getline(std::cin, tmp); return tmp; } int main() { println("Please enter a line of text:"); println("You entered: " + inputln()); println("Enter more text:"); std::string text = inputln(); println("You entered:"); println(text); }
Funktio println ei palauta mitään arvoa, sillä sen nimen edessä oleva paluuarvon tyyppi on void. Funktio ottaa parametrinaan (engl. parameter, argument) std::string-tyyppisen arvon, joka kopioituu funktion paikalliseen muuttujaan line.
println("Please enter a line of text:"); println("You entered: " + inputln());
Tässä ensimmäinen rivi kutsuu määrittelemäämme funktiota, joka siis tulostaa sulkeiden sisällä olevan merkkijonon.
Toinen rivi on mielenkiintoisempi, sillä siinä tapahtuu varsin paljon. Ensin inputln():ää kutsutaan ja se lukee näppäimistöltä rivin. Funktion paluuarvo taasen yhdistetään merkkijonon "You entered: " perään ja merkkijonojen yhdistelmä välitetään funktiolle println. Funktiota println ei siis kutsuta ennen kuin inputln on tullut valmiiksi ja merkkijonojen yhdistäminen on tehty.
println("Enter more text:"); std::string text = inputln(); println("You entered:"); println(text);
Tässä taasen luettu rivi tallennetaan suoraan muuttujaan text, tulostetaan rivi You entered: ja sitten tulostetaan tallennettu rivi omalle rivilleen.
Tietorakenteet
muokkaaSeuraava työkalu on askel ylöspäin tavallisista yhden arvon sisältävistä "tavallisista" muuttujista - kontainerit (engl. container) ovat muuttujia, joihin voi tallentaa suuren määrän tietyn tyyppisiä arvoja.
#include <algorithm> #include <fstream> #include <iostream> #include <set> #include <string> void println(std::string line) { std::cout << line << std::endl; } int main() { std::set<std::string> lines; { std::ifstream file("sort.cpp"); for (std::string tmp; std::getline(file, tmp); lines.insert(tmp)); } std::for_each(lines.begin(), lines.end(), println); }
Ohjelman suorituksen alkaessa määritellään aluksi erikoinen uusi muuttuja:
std::set<std::string> lines;
Muuttujan nimi on lines ja tyyppi std::set<std::string>. Kyseessä on kontaineri, johon voidaan säilöä kulmasuluissa mainitun tyyppisiä objekteja, tässä tapauksessa merkkijonoja. Standardikirjasto sisältää lukuisia eri käyttötarkoituksiin soveltuvia kontainereita, joista std::set on sellainen, joka järjestää sisältönsä automaattisesti nousevaan järjestykseen, std::string:n kohdalla siis aakkosjärjestykseen, ja poistaa samalla duplikaatit.
Huomaa myös uudet headerit <algorithm>, <fstream> ja <set>, joista viimeinen luonnollisestikin tarvitaan tätä kontaineria varten. Kaikki edellisessä ohjelmassa käytetyt standardikirjaston osat löytyivätkin headereista <iostream> (std::cin, )std::cout) ja <string> (std::string).
Jos lisäämme kontaineriin lines merkkijonoja seuraavasti:
lines.insert("Simpson, Homer"); lines.insert("Flanders, Ned"); lines.insert("Simpson, Bart"); lines.insert("Flanders, Ned");
On kontainerin sisältö tämän jälkeen { "Flanders, Ned", "Simpson, Bart", "Simpson, Homer" }, eli lisäysjärjestyksellä ei ollut lopputulokseen vaikutusta, eikä yksikään nimistä esiinny kahteen kertaan.
Tarkoituksena kuitenkin on lukea rivit tiedostosta, käyttäen std::ifstream:a (header <fstream>). Tiedostoon kirjoittaminen tapahtuisi vastaavasti, mutta std::ofstream:lla.
std::ifstream file("sort.cpp");
Tämä avaa tiedoston sort.cpp luettavaksi (ohjelma lukee omaa lähdekoodiaan, jos huomasit sen tuolla nimellä tallentaa). Tiedostoa käsitellään muuttujan file kautta.
std::string tmp; while (std::getline(file, tmp)) lines.insert(tmp);
Varsinainen lukeminen tapahtuu tässä. tmp on väliaikainen muuttuja, johon rivi luetaan ennen sen lisäämistä linesiin. Kuten aiemmin todettiin, std::getline hoitaa rivin lukemisen, mutta tässä std::cin on korvattu muuttujalla file, jolloin lukeminen tapahtuukin sieltä. Huomataan, että näppäimistön voi suoraan korvata tiedostolla (myöhemmin myös esim. TCP-yhteydellä tai yleensäkin millä tahansa input streamilla). Vastaavasti tiedostoon kirjoittaminen onnistuisi korvaamalla std::cout tulostuslausekkeessa std::ofstream-tyyppisellä muuttujalla.
Tässä ehtolauseena on std::getline, joka siis lukee rivin sille parametrina annettuun muuttujaan. Miten ihmeessä tätä voi käyttää ehtona? Kyseessä on kohtuullinen määrä C++:n magiaa suoraan standardikirjastosta, mutta tässä kohtaa riittää tietää että lukuoperaatioita (file >> muuttuja ja std::getline(file, line)) voi suoraan käyttää ehtona. Jos lukuoperaatio (mikä tahansa vaihe siitä) epäonnistuu, palauttaa lauseke false:n ja ehto jää toteutumatta. Näin käy mm. tiedoston loppuessa kesken.
... ja onnistuneen lukemisen tuloksena loopin sisällä tallennetaan luettu merkkijono tmp kontaineriin lines ennen seuraavan rivin lukemista.
Tässä kohtaa on hyvä pitää pieni tauko ja vähän sulatella asiaa, nimittäin uusia asioita tuli äsken täysillä, eikä se tähän vielä lopu.
std::for_each(lines.begin(), lines.end(), println);
Tässä käytetään standardikirjaston algoritmia (kyllä, arvasit oikein, #include <algorithm> on std::for_each:a varten) kontainerin läpikäyntiin. Funktio ottaa kolme parametria - aloituskohdan, lopetuskohdan ja käytettävän käsittelyfunktion. Tässä käymme lines:n läpi alusta loppuun ja käsittelijänä toimii aiemmin määritelty funktio println. Kääntäjä tarkistaa, että println ottaa oikeanlaisen parametrin (joka vastaa kontainerin sisältöä), ja jos näin ei ole, tulee kääntäjältä helposti monikymmenrivinen virheilmoitus, josta ei ihan helpolla selvää otakaan.
std::for_each siis kutsuu funktiota println, antaen sille kunkin lines:sta löytyvän rivin kerrallaan argumenttina.
Entäs sitten nuo "turhat" aaltosulkeet, joista en sanonut vielä mitään? Niillä rajataan muuttujien file ja line elinaikaa. Muuttujat lakkaavat olemasta, kun ne sisältävä koodiblockki - tässä tapauksessa juuri tämä aaltosuljeviritelmä - päättyy. Tämä onkin toivottavaa, koska nyt nämä vain lukemisessa apuna käytetyt muuttujat tuhoutuvat automaattisesti heti lukemisen valmistuttua, eivätkä ne jää lopuksi aikaa turhaan roikkumaan ja tuhlaamaan resursseja tai varaamaan tiedostoa käyttöönsä. Tätä periaatetta, jossa muuttujat siivoavat itsensä automaattisesti pois (esim. tiedosto suljetaan, kun blockista poistutaan), kutsutaan RAII:ksi ja se on erittäin keskeinen osa modernia C++-ohjelmointia. Aiheeseen palataan vielä lukuisia kertoja.
Iteraattorit
muokkaaEdellisen ohjelman println-funktio on itse asiassa tarpeeton, sillä standardikirjastosta olisi löytynyt jo valmiiksi vastaavan tulosteen tekevä iteraattori:
#include <algorithm> #include <fstream> #include <iostream> #include <iterator> #include <set> #include <string> int main() { std::set<std::string> lines; { std::ifstream file("sort.cpp"); std::string line; while (std::getline(file, line)) lines.insert(line); } std::copy(lines.begin(), lines.end(), std::ostream_iterator<std::string>(std::cout, "\n")); }
Iteraattori tulostaa jokaisen sille annetun kulmasulkeissa kerrotun tyyppisen objektin ensimmäisenä argumenttina annettuun output streamiin, lisäten perään toisena argumenttina annetun merkkijonon (\n tarkoittaa rivinvaihtoa). Aihetta käsitellään kuitenkin toisaalla syvällisemmin.
Referenssit ja const
muokkaa(TODO)
Virheenkäsittely (throw, try-catch)
muokkaa(TODO)
I/O
muokkaaVaikka olemme jo käyttäneet I/O:ta aiemmissa esimerkeissä, tutustutaan I/O-virtoihin tarkemmin vasta tässä osassa.
Tiedostot
muokkaaTiedostosta lukeminen toimii samoin kuin näppäimistöltä tai mistä tahansa muusta istreamista lukeminen, mutta tiedosto täytyy ennen käyttöä avata ja käytön jälkeen sulkea. C++:ssa tämä tehdään luomalla std::ifstream-tyyppinen muuttuja. Tiedosto avataan rivillä, jolla muuttuja luodaan ja suljetaan muuttujan tuhoutuessa, automaattisesti.
#include <fstream> #include <iostream> #include <string> int main() { std::ifstream file("test.txt"); for (std::string word; file >> word;) { std::cout << word << " (" << word.size() << " characters)" << std::endl; } }
Ohjelma avaa tiedoston "test.txt" (muista tehdä sellainen ennen ohjelman käyttöä) ja lukee sieltä sanan kerrallaan muuttujaan word. Lukuoperaatio file >> word on loopin ehtolauseena, jotta loopin suoritus loppuu, kun sanaa ei saada luettua (tiedosto päättyy tai tapahtuu muu virhe).
Tiedoston luominen ja siihen kirjoittaminen toimii taasen vastaavasti kuin std::cout, mutta luodun muuttujan tulee olla tyyppiä std::ofstream. Tällaisen muuttujan määrittely luo uuden tiedoston tai korvaa olemassaolevan tyhjällä tiedostolla. Seuraavassa esimerkissä kopioidaan test.txt:n sisältö upper.txt:hen, muuttaen samalla kaikki kirjaimet isoiksi.
#include <cctype> #include <fstream> int main() { std::ifstream in("test.txt"); std::ofstream out("upper.txt"); for (char ch; in.get(ch); out << std::toupper(ch)); }
Yllä käytettiin yksittäisen merkin lukemiseen lauseketta in.get(ch), jotta lukeminen tapahtuisi merkki kerrallaan ilman muotoilua. Lausekkeella in >> ch heitettäisiin menemään kaikki whitespace, eli välilyönnit, rivinvaihdot, yms, ja luettaisiin vain näkyvät kirjoitusmerkit.
Olemassaolevaa tiedostoa voi myös muokata (lukea ja kirjoittaa), jolloin käytetään std::fstream-tyyppiä, joka vastaa toiminnaltaan edellä käsiteltyjä.
Stringstreamit
muokkaaToisin kuin useimmissa muissa kielissä, C++:ssa ei ole erillisiä funktioita merkkijonojen muuntamiseksi lukuarvoiksi tai lukuarvojen muuntamiseksi merkkijonoiksi. Näiden sijaan muunnokset tehdään stringstreameilla, jotka toimivat vastaavasti kuin std::cin ja std::cout, mutta näytön ja näppäimistön sijaan tietoa luetaan merkkijonosta (std::istringstream) tai kirjoitetaan merkkijonoon (std::ostringstream). Seuraavassa lyhyt esimerkki, jossa määritellään funktiot lukuarvon muuntamiseen merkkijonoksi ja merkkijonon muuntamiseen lukuarvoksi.
#include <sstream> #include <string> int str2int(std::string s) { std::istringstream iss(s); // Construct a new stringstream and initialize it with the contents of s int ret = 0; // Initialized to zero, just in case the reading fails iss >> ret; // WARNING: No error handling! return ret; } std::string int2str(int val) { std::ostringstream oss; oss << val; return oss.str(); // Return a copy of oss contents }
Muunnosten lisäksi stringstreameja voidaan hyödyntää myös muissa asioissa, tässä esimerkkinä näppäimistöltä lukeminen rivi kerrallaan:
#include <iostream> #include <sstream> #include <string> int main() { std::cout << "Enter a number and a name (number firstname lastname): "; std::string line; std::getline(std::cin, line); std::istringstream iss(line); int num; std::string first, last; if (iss >> num >> first >> last) { std::cout << "You entered num=" << num << ", first=" << first << ", last=" << last << std::endl; } else { std::cout << "Invalid input!" << std::endl; } }
Jos ohjelmassa oltaisiin luettu num, first ja last suoraan std::cin:stä, ei lukeminen olisi loppunut kuin streamin joutuessa virhetilaan (esim. syötetty kirjain lukuarvon sijaan) tai kun kaikki tiedot on syötetty. Nyt syöte luetaan std::getline:llä, joka ottaa kokonaisen rivin, tutkimatta rivin sisältöä. Käyttäjät ovat tottuneet siihen, että syöte päätetään painamalla enteriä, joten tällaisen ohjelman käyttö on luontevampaa. Merkkijonoon luettu syöte luetaankin sitten stringstreamin avulla varsinaisiin muuttujiin. Lopussa voidaan vielä tarkistaa lukematta jäänyt syöte helposti.
Vastaavasti kuin tiedostojen kanssa, myös stringstreameista on myös sekä lukemista että kirjoittamista tukeva versio std::stringstream, jonka senkin tarpeetonta käyttöä tulisi välttää.
- Varoitus: Stringstreamista lukeminen ei poista luettua sisältöä streamin sisäisestä merkkijonosta, joten .str() palauttaa myös jo pois luetut merkit.
Virheenkäsittely
muokkaaKuten aiemmin on todettu, input streamista (esim. std::cin tai std::ifstream in) voi lukea operaattorilla >> tai std::getline-, std::get- ja std::ignore-funktioilla, testaten paluuarvon avulla onnistuiko operaatio. Näiden lisäksi löytyy myös vastaavalla tapaa toimiva funktio read, joka soveltuu binääridatan lukemiseen, kun luettavan alueen koko tunnetaan jo ennestään, mutta sitä ei tässä käsitellä enempää. Aiemmissa esimerkeissä näitä työkaluja on käytetty tarkastelematta sitä miten ne oikeastaan toimivat, mutta tässä kohtaa on hyvä ymmärtää mitä konepellin alla tapahtuu.
Kun syötteestä luetaan >>-operaattorilla, heitetään ensin kaikki löytyvä whitespace menemään, merkki merkiltä, kunnes löydetään jokin ei-whitespace-merkki. Vasta tällöin alkaa varsinainen lukuoperaatio, eli seuraavaa merkkiä yritetään tulkita annetun muuttujan tyyppisenä tietona, esim. integerinä. Jos luettaessa tulee vastaan merkki, jota ei voi tulkita ko. tyyppisenä tietona, pysähtyy lukeminen siihen ja merkki jätetään (palautetaan, putback) input streamiin seuraavaa lukuoperaatiota varten. Jos jo luetut merkit voidaan tulkita niin, että lukeminen on onnistunut, sijoitetaan luettu arvo annettuun muuttujaan ja ohjelman suoritus jatkuu normaalisti. Esimerkiksi syöte " \n -.5x" double-muuttujaan luettuna tuottaa arvon -0.5 ja jättää x-kirjaimen odottelemaan seuraavaa lukuoperaatiota.
Virhetilanteissa taasen viimeiseksi luettu merkki on kyllä palautettu streamiin, mutta annettuun muuttujaan ei sijoiteta mitään (se säilyttää aiemman arvonsa). Lisäksi streamin fail-flag asetetaan, jolloin std::cin >> value:a testaava ehtolause ei toteudu. Huomaa, ettei aiempia merkkejä palauteta! Esimerkiksi double-muuttujaan lukeminen syötteellä " \n -.x" syö ensin whitespacet pois, lukee sitten miinus-merkin ja pisteen normaalisti (ne näyttävät lukuarvon alulta) ja vasta x:ään törmätessään havaitsee virheen. Tällöin kaikki syöte pisteeseen saakka on hukattu, vaikkei muuttujaan luettu mitään.
Päällä oleva fail-flag estää myös kaikki tulevat lukuoperaatiot, vaikka ne saisivatkin ongelmia aiheuttaneen merkin luettua pois. Virhetila tulee nollata .clear()-funktiolla, jotta streamista saataisiin luettua mitään.
- Varoitus: .clear() ei poista merkkejä streamista, vaan ainoastaan nollaa virhetilat. Sitä ei sovi siis sekoittaa std::string:n tai muiden tietorakenteiden .clear()-funktioihin, jotka tyhjentävät tietorakenteen. C++:ssa ei ole mitään tapaa puskurissa olevan syötteen pois heittämiseen, sillä tällainen ei ole edes mahdollista läheskään kaikilla alustoilla. Aloittelijat haluaisivat usein hoitaa puskuriin jääneen ylimääräisen tiedon (esim. rivin loppuun jäänyt x) poistamisen näin, mutta tarkemmin ajatellen tuo ei edes toimi, sillä syöte voi olla esim. copy&pastettu, jolloin puskurissa saattaa olla jo seuraavakin rivi odottamassa ja se hukattaisiin samalla. Oikea ratkaisu tähän ongelmaan on lukea, kunnes löydetään haluttu lopetusmerkki (esim. rivinvaihto). Tähän soveltuu erityisesti input streamien funktio .ignore(n, ch), joka nimensä mukaisesti heittää merkkejä menemään, kunnes merkki ch löydetään (myös se hävitetään) tai kunnes n merkkiä on luettu. Lukumääräksi voi antaa std::numeric_limits<int>::max(), eli int-muuttujan maksimiarvon (yleensä vähän päälle kaksi miljardia), jolloin merkkien määrää ei ole rajoitettu (edes reiluun kahteen miljardiin). std::numeric_limits:ä varten tarvitaan header <limits>.
#include <iostream> #include <sstream> int main() { std::istringstream iss(" \n -.x 12"); double val = 1.23; char c = 'a'; iss >> val; // Eats everything until the x, iss is set to fail state iss.get(c); // Stream in fail state, does not read anything std::cout << "val=" << val << ", c=" << c << std::endl; std::cout << "good=" << iss.good() << ", fail=" << iss.fail() << ", eof=" << iss.eof() << ", bad=" << iss.bad() << std::endl; iss.clear(); // Reset the error state iss.get(c); // Read "x" iss >> val; // Read " 12" std::cout << "val=" << val << ", c=" << c << std::endl; std::cout << "good=" << iss.good() << ", fail=" << iss.fail() << ", eof=" << iss.eof() << ", bad=" << iss.bad() << std::endl; }
Kokeile ajaa tämä ohjelma koneellasi ja tutki miten virheflagit käyttäytyvät. Erityisesti, huomaa kuinka eof-flag menee päälle arvoa 12 luettaessa ja kuinka tämä aiheuttaa good-flagin nollautumisen, vaikka lukuoperaatio onnistuukin. Tämä johtuu siitä, että iss >> val joutuu merkin 2 luettuaan tarkistamaan olisiko luvulle vielä jatkoa, koska se ei voi muuten saada selville olevansa streamin lopussa, sillä vasta streamin lopun yli lukeminen asettaa eof-flagin päälle.
On kuitenkin hyvin harvinaista joutua tarkastelemaan muita flageja kuin failia, eikä sitäkään normaalisti lueta suoraan, vaan sen sijaan käytetään lukuoperaatiota ehtolauseessa. Tällöin ehto toteutuu, jos fail-flag ei ole päällä.
- Lukuoperaation testaaminen virheiden selvittämiseksi on itse asiassa varsin monimutkainen operaatio, jonka todellista toimintaa ei aloittelijan välttämättä tarvitse ymmärtää. Lauseke std::cin >> v1 >> v2 ei suinkaan palauta bool-tyyppistä arvoa, vaan se toimii seuraavasti: ensiksi suoritetaan std::cin >> v1, joka kutsuu headerissa <istream> esiteltyä funktiota operator>>, joka ottaa parametreinaan operaattorin kanssa käytetyt operandit std::cin ja v1. Funktio suorittaa lukuoperaation ja palauttaa sille annetun input streamin takaisin, paluuarvonaan. Tälle paluuarvolle suoritetaan sitten jälkimmäinen osa, >> v2, vastaavalla tavalla ja jälleen saadaan alkuperäinen stream takaisin. Virheen sattumista voitaisiin siis yhtä hyvin testata vasta myöhemmin, esim. seuraavalla rivillä, käyttämällä ehtolauseessa std::cin:ä. Näin ei kuitenkaan yleensä tehdä, sillä on käytännöllisempää tarkistaa virheet heti lukuoperaation yhteydessä. Input streamin käyttö ehtolauseen ehtona taasen perustuu siihen, että sillä on olemassa ns. tyyppimuunnosoperaattori tyyppiin void*, joka soveltuu totuusarvoksi, vaikkei olekaan bool. Palautettava arvo on 0 (NULL), jos fail-flag on päällä, ja mitä tahansa muuta, jos ei ole. Syynä void*:n käyttöön bool:n sijaan on tyyppiturvallisuus, sillä bool muuntuu automaattisesti mihin tahansa integer-tyyppiin, myös vahingossa, kun taasen void*:llä ei voi tehdä oikeastaan muuta kuin käyttää sitä ehtolauseen ehtona.
Myös tulostusoperaatiot voivat epäonnistua vastaavasti ja myös niitä voi käyttää ehtolauseissa. Näin ei kuitenkaan läheskään aina tehdä, koska tulostusvirheet ovat harvinaisempia ja toisaalta yleensä voidaan tyytyä tarkistamaan fail-tila vasta aivan lopuksi, sillä välissä tehdyt tulostusyritykset on kuitenkin fail-flagin ansiosta estetty (mitään ei tulostu). Tiedostoon kirjoittaessa epäonnistuminen voi aiheutua mm. levytilan loppumisesta tai kirjoitusoikeuden puuttumisesta. Vastaavia virheitä voi syntyä myös std::cout:n kanssa, sillä sekin voi olla ulkoisesti (UNIX-shellissä, Windowsin komentoriviltä tai toisen ohjelmiston sisältä käynnistettynä ohjelmana) ohjattu menemään tiedostoon näytön sijaan.
Localet
muokkaa(TODO)
LUOKAT JA ALGORITMIT
muokkaaTässä osiossa käydään läpi keskeisimmät asiat omien luokkien määrittelyyn liittyen, mutta ei kuitenkaan mennä vielä oliopohjaiseen ohjelmointiin. Tutkitaan samalla miten kutakin ominaisuutta voi hyödyntää standardikirjaston tietorakenteiden ja algoritmien kanssa.
Enemmän tietoa muuttujaan omalla rakenteella (struct)
muokkaa(välitys funktion parametrina ja käyttö standardikirjaston algoritmien kanssa)
Tavalliset jäsenfunktiot (member function)
muokkaa(std::mem_fun_ref algoritmien kanssa)
Konstruktorit ja init-lista
muokkaa(esimerkki väliaikaisen objektin käytöstä funktion parametrina)
Destruktori
muokkaa(esimerkki kontainerin tuhoamisesta?)
Copy constructor ja sijoitusoperaattori (operator=)
muokkaa(milloin käytetään kumpaakin)
Funktiokutsuoperaattori (operator())
muokkaa(käyttö algoritmien kanssa)
Stream-operaattorit (operator<< ja operator>>)
muokkaa(käyttö stream-iteraattoreiden kanssa)
Vertailuoperaattorit (operator<, operator==, ...)
muokkaa(käyttö vertailevissa containereissa ja vertailevilla algoritmeilla) a == b a on yhtäsuuri kuin b. a < b a on pienempi kuin b. a > b a on suurempi kuin b. a <= b a on pinempi tai yhtäsuuri kuin b. a >= a on suurempi tai yhtäsuuri kuin b. a != b a on erisuuri kuin b. a % b == 0 a on jaollinen b:llä.
! operaattorilla voidaan ilmaista negaatiota, esim. !(a % b == 0): a ei ole jaollinen b:llä (voitaisiin toki kirjoittaa myös (a % b != 0)).
Aritmeettiset operaattorit (operator+, operator++, operator+=, ...)
muokkaa(<numeric>?)
Tyyppimuunnosoperaattorit
muokkaaNäkyvyysmääreet (public, private ja class)
muokkaa(ADT:n teko, accessorit)
Kopiointisemantiikat ja "Rule of Three"
muokkaa(miten estetään kopiointi)
ITERAATTORIT, POINTTERIT JA DYNAAMINEN MUISTINVARAUS
muokkaaPerusteet
muokkaaMuistin varaaminen (new ja delete)
muokkaa(myös new[]/delete[] ja maininta siitä että niiden sijaan kannattaa käyttää std::vectoria)
Pointteriaritmetiikka ja suhde iteraattoreihin
muokkaa(esim. komentoriviparametrien käyttö containerin alustukseen)
OLIOPOHJAINEN OHJELMOINTI
muokkaa(tästä osiosta on vasta hyvin karkea rakennesuunnitelma)
Ankka on Lintu
muokkaa(perusteet ja jäsenten näkyvyysmääre protected)
Polymorfismi (virtuaalifunktiot)
muokkaa(tähän jokin käytännönläheinen esimerkki ja reilusti tekstiä)
Kantaluokkien funktioiden kutsuminen
muokkaaViipaloituminen (object slicing)
muokkaaMoniperintä
muokkaaExplicit qualifiers
muokkaaVirtuaalinen perintä
muokkaaAlustusjärjestys
muokkaaTyyppimuunnokset periviin luokkiin
muokkaa(static_cast ja dynamic_cast)
Yksityinen perintä
muokkaa(private ja protected)