Cpp alkeet
Mureakuha
Sisällysluettelo |
Johdanto C++-kieleen
C++ perustuu C-kieleen (K & R 1978), joten lähes kaikki C -kielellä tehdyt koodit kääntyvät C++:na. C99 -standardin myötä ei voida kuitenkaan puhua, että C olisi C++:n osajoukko - molemmilla kielillä on omat standardinsa. C++:n erilaisista ominaisuuksista on vaikea puhua eristettynä muista C++:n ominaisuuksista, sillä useimmat ominaisuudet liittyvät kiinteästi yhteen toisten ominaisuuksien kanssa. Nyt onkin tarkoitus aluksi saada yleiskuvaa asiasta, kaikkia nyt esitettyjä asioita tullaan käsittelemään tarkemmin myöhemmin.
C++:ssa on useita ominaisuuksia, joiden avulla ohjelmien teko on joustavampaa kuin C:ssä. Vaikka joillain ominaisuuksilla on hyvin vähän tai ei mitään tekemistä olio-ohjelmoinnin kanssa, niitä käytetään jatkuvasti C++-ohjelmissa.C++:lla voidaan tehdä ohjelmia, jotka eivät ole oliopohjaisia. Jokainen voi periaatteessa valita, miten käyttää C++:aa. Tässä kurssissa tarkoituksena on kuitenkin tehdä OO-ohjelmia (object oriented).
Koska ohjelmoinnin oppii parhaiten itse tekemällä (itse asiassa ohjelmointia ei opi, ellei itse ohjelmoi), jatkossa esitettäviä esimerkkejä ja harjoitustehtäviä kannattaa kokeilla. Itse asiassa kirjoittaminen paperikopiolta opettaa jo melkoisesti enemmän kuin kopiointi korpulta toiselle.
C++ ohjelmat näyttävät osittain samanlaisilta kuin C-ohjelmat: ohjelman suoritus alkaa main()-funktiosta. Komentoriviargumentit välitetään samoilla argc ja argv-parametreilla kuin C:ssäkin. C++:ssa on samat kirjastot käytettävissä kuin C:ssäkin. C++:ssa on joitain omia header-tiedostoja sekä kaikki C:n header-tiedostot. C++:ssa on samat ohjausrakenteet kuin C:ssäkin (for, while, do-while, if, else ...) sekä samat sisäänrakennetut tietotyypit (int, float, double, ...).
Mitä on oliopohjainen ohjelmointi (Object Oriented Programming, OOP)?
OOP on uusi (ainakin erilainen kuin totutut) tapa lähestyä ohjelmointiongelmaa. Ohjelmointikielten historian aikana erilaisia lähestymistapoja on kehitetty sitä mukaan, kun ongelmat ovat monimutkaistuneet siinä määrin, ettei vanhoilla tavoilla olla enää pärjätty. Kielet kuten C, Pascal, jne ovat ns. rakenteisia ohjelmointikieliä. Rakenteisten kielien ominaisuuksia ovat hyvin määritellyt ohjausrakenteet, koodilohkot, GOTO-lausekkeen puuttuminen sekä erilliset aliohjelmat, jotka tukevat rekursiota sekä paikallisia muuttujia. Keskiarvoinen ohjelmoija voi tuottaa ja ylläpitää ohjelmia, jotka ovat noin 50000 rivin pituisia.
Rakenteisilla kielillä pärjätään, kunnes koodirivimäärä nousee tietyn pisteen yli. OOP kehitettiin suurien, monimutkaisten ohjelmien tekemistä varten. OOP käyttää hyväkseen rakenteisen kielen hyvät ominaisuudet sekä yhdistää näihin ominaisuuksia, joilla ohjelma voidaan organisoida uudella tavalla. OOP rohkaisee jakamaan ohjelmointiongelmat osaongelmiin. Osaongelmista tulee itsenäisiä olioita, joilla on oma ongelmaan liittyvä data sekä komennot ongelman käsittelyyn. Tällä menettelyllä ohjelman monimutkaisuus pienenee ja ohjelmoija on kykenevä hallitsemaan suurempia ohjelmia.
Kaikissa OOP-kielissä on kolme peruskäsitettä:
- kapselointi (encapsulation)
- polymorfismi, monimuotoisuus (polymorfism)
- perintä (inheritance)
Kapselointi
Kapseloinnin avulla koodi ja data, jota koodi käsittelee, kytketään yhteen ja eristetään ulkomaailmasta, siten että sitä ei pysty käyttämään väärin.
OOP:ssä koodi ja data voidaan sitoa yhteen, siten että syntyy "musta laatikko", jonne on joku tai joitain tarkasti määriteltyjä liittymiä, mutta ei muuta pääsyä. Kyseinen musta laatikko on OLIO, OBJECT. Olio on siis laite, joka tukee kapselointia.
Oliossa koodia, dataa tai molempia voi olla ko olion yksityisessä, sisäisessä käytössä eli ns. privaattina tai yleisenä, ulkomaailmankin käytettävissä. Privaattiin koodiin tai dataan pääsee käsiksi vain olion omat sisäiset osat. Olion ulkopuolelta ei voida käsitellä privaattia dataa tai koodia. Kun koodi tai data on yleistä (public) vaikkakin määriteltynä olion sisällä, siihen pääsee käsiksi olion ulkopuolelta (sekä olion sisältä). Tavallisesti olion yleiset osat muodostavat liittymän olioon ja liittymän avulla voidaan käsitellä olion dataa, olion määrittelemällä tavalla.
Käytännössä olio on muuttuja, joka on käyttäjän määrittelemää tyyppiä (user defined type). Vertaa struktuuri-tyyppi, joka sisältää dataa. Käyttäjä voi määritellä muuttujia ko. tyyppisiksi. Vastaavasti käyttäjä voi määritellä muuttujia (=olioita) tiettyyn oliotyyppiin.
Polymorfismi
Polymorfismi (=many forms, monimuotoisuus) on tekniikka, jonka avulla voidaan käyttää samaa nimeä usealle samantapaiselle mutta teknisesti erilaiselle asialle. OOP:ssä polyforfismia käytetään määrittelemään yleinen toimenpiteiden luokka. Tietotyypin mukaan voidaan päätellä, mistä yksittäisestä toimenpiteestä on kysymys.
Esim. C:ssä on funktiot abs(), fabs() ja labs() itseisarvon ottamiseen erityyppisistä luvuista (integer, double, long). C++:ssa voidaan käyttää yhtä funktiota esimerkiksi absolut(), joka pystyy päättelemään argumentin tietotyypistä, mistä on kysymys ja sen mukaan esimerkiksi kutsutaan funktioita abs(), fabs() tai labs().
C++:ssa voidaan käyttää yhtä nimeä funktioille, jotka suorittavat erilaisia tehtäviä. Kyseessä on funktioiden kuormitus (function overloading).
Yleisemmin polymorfismilla tarkoitetaan: yksi liittymä, monta metodia. Polymorfismin hyöty on siinä, että voidaan kehittää yksi yleinen liittymä, joista käynnistyy joukko toiminteita. Tällaisen yleisen liittymän käyttö on yksinkertaisempaa, jolloin pienennetään monimutkaisuutta. Kääntäjän tehtävänä on valita lopullinen toimenpide.
Polymorfismia on sovellettu sekä funktioihin että operaattoreihin. Esim. Operaattori + tarkoittaa yhteenlaskua. Voidaan suorittaa yhteenlasku 1 + 2 tai 1.4 + 2.745 tai "abc" + "def". Edellä kokonaislukujen ja desimaalilukujen yhteenlasku on toteutettu jo C:ssä, mutta itseasiassa toimenpiteethän ovat sisäisesti erilaisia. Kokonaislukujen tapauksessa lasketaan yhteen esimerkiksi 16 bittisiä lukuja. Desimaalilukujen tapauksessa 16 bittiä ei riitä ja talletustapakin on aivan erilainen, joten yhteenlaskun toteutuskin on erilainen. Merkkijonojen tapauksessa C:ssä ei ole ko. operaatiota, mutta voisi ihan hyvin olla siinä merkityksessä, että "abc" + "def" on "abcdef". Kyseessä on operaattorin kuormitus.
Polymorfismiin liittyen oleellista on ymmärtää, että polymorfismi tarjoaa tavan käsitellä monimutkaisia ongelmia, siten että voidaan luoda standardiliittymiä samankaltaisten asioiden käsittelyyn.
Perintä
Perinnän avulla olio voi sisältää toisen olion ominaisuudet. Tarkemmin ottaen, olio voi periä toiselta oliolta yleisen joukon ominaisuuksia, johon se voi lisätä omia spesifisiä ominaisuuksiaan, jotka ovat siis vain kyseisen uuden olion ominaisuuksia. Perinnän avulla voidaan synnyttää hierarkkisia järjestelmiä, luokkia (hierarchical classification). Tavallisessa elämässäkin asiat ovat käsiteltävissä vain hierarkkisen luokittelun avulla. Esimerkiksi
- ympäristön tila
- vesistöt
- meret
- Itämeri
- Atlantti
- ...
- järvet
- joet
- ...
- meret
- metsät
- ...
- vesistöt
- ...
Lapsiluokka perii ominaisuuksia isältään: esim. ympäristöntilasta voitaisiin periä vaikkapa mekaaniset jätteet, kemialliset saasteet, ... Vesistöt voisivat lisätä edellisiin yleisiin ominaisuuksiin vaikkapa happipitoisuuden tms. jne.
Ilman luokittelua, jokaisessa alimman tason luokassa määriteltäisiin kaikki, mikä siihen liittyy riippumatta siitä, onko asia sellainen, että se on ylemmän tason käsitteessäkin mukana.
Perinnän avulla voidaan määritellä, mihin ylemmän tason luokkiin olio kuuluu sekä olion lisäominaisuudet, jotka tekevät siitä erilaisen muihin nähden.
Esimerkkejä.
- C:ssä kirjasto-ohjelmat ovat eräänlaisia mustia laatikoita.
- Tavallisessa elämässä polymorfismi on jokseenkin tavallista. Esimerkiksi auton ohjauspyörä toimii samalla tavalla riippumatta siitä, millainen akselisto tai muu sisäinen ominaisuus autoon liittyy. Liittymä ulkomaailmaan on sama riippumatta sisäisestä tavasta liikuttaa pyöriä.
- Esimerkki perinnästä: elolliset organismit: kasvit: vihannekset: kurkut: (suolakurkku)
C++ konsoli I/O
Edelleen voidaan käyttää printf ja scanf funktioita (kuten kaikkia C:n ominaisuuksia), mutta C++ tarjoaa vaihtoehtoisen tavan, jossa käytetään I/O-operaattoreita I/O-funktioiden tilalla. Kyseessä on operaattorit kuten + tai - joten, niitä käytetään kuten operaattoreita.
- << Tulostusoperaattori
- >> Syöteoperaattori
(<< ja >> ovat myös ns. bittioperaattoreita ja tämä merkitys säilyy ennallaan)
Tulostetaan standardi tulostuslaitteelle rivinsiirto ja teksti "Tulostusta":
cout << "\nTulostusta";
cout on etukäteen määritelty "virta" (stream) standardi tulostuslaitteelle, vastaavasti kuin stdout C:ssä.
Tulostetaan: 100.98 10 lisää
cout << 100.98 << " " << 10 << " lisää\n";
Tulostetaan kehotteet ja luetaan käyttäjältä erilaisia lukuja:
int num; float num2; cout << "\nAnna kokonaisluku: "; cin >> num; cout << "Anna desimaaliluku: "; cin >> num2; cout << "Anna kokonaisluku ja desimaaliluku: "; cin >> num >> num2;
Edellisten käyttö vaatii, että otetaan mukaan tiedosto iostream.
Kuten C:ssäkin yhden arvon lukeminen lopetetaan aina white space-merkkiin: (tab, space, enter). Syötteet <<:n kanssa ovat puskuroituja, eli mitään ei viedä ohjelmalle käsittelyyn ennen enterin painamista.
Kokeile:
#include <iostream> int main() { char c; cout << "Anna merkkejä, käsitellään x:ään asti\n"; do { cout << ": "; cin >> c; cout << "\n " << c; } while ( c != 'x' ); return 0; }
Koita kahta tapaa antaa syötteet:
abcdefxyzop.
Anna merkit peräkkäin: johonkin väliin x ja lopuksi enter
a b c d x
Anna jokaisen merkin perään enter ja jossakin vaiheessa x.
Tulostuksen formatointi yms. asiat käsitellään myöhemmin, kun meillä on enemmän tietoa C++:n rakenteesta.
C++:n kommentit
/* ... */ on yleinen C:n kommentti. C++:ssa voidaan lisäksi käyttää // kommenttia, joka tarkoittaa, että kyseisestä kohdasta rivin loppuun asti on kommenttia.
Luokat ja oliot: ensisilmäys
C++:n yksittäisistä ominaisuuksista tärkein on luokka-käsite (class). Luokka on rakenne jolla yhdistetään muuttujia ja funktioita. Luokat ovat tietueiden tapaisia, mutta sisältävät jäsenmuuttujien lisäksi siis jäsenfunktioita, joita kutsutaan metodeiksi. Muuttujaa, joka on määritelty luokan tyyppiseksi, kutsutaan olioksi. Olio on siis toisin sanoen luokan ilmentymä. Tietyn luokan tyyppisillä olioilla on jäsenmuuttujat eri muistipaikassa, mutta ne hyödyntävät samoja jäsenfunktioita.
Luokka on siis mekanismi, jota käytetään olioiden luomiseen. Luokan määrittelyn syntaksi on samantapainen kuin struktuurilla:
class luokanNimi { // luokan privaatit funktiot ja muuttujat public: // luokan yleiset funktiot ja muuttujat } oliolista;
Oliolista on vapaaehtoinen. Kuten struktuureillakin, luokan olioita voidaan määritellä myöhemmin. Vaikka luokanNimi on myös vapaaehtoinen, käytännössä sitä käytetään aina. LuokanNimi on itseasiassa uusi tyyppi, jota käytetään, kun luodaan olioita kyseiseen luokkaan.
Luokan sisällä määritellyt muuttujat ja funktiot ovat luokan jäseniä. Oletusarvoisesti kaikki ovat privaatteja, jolloin niitä voidaan käsitellä vain luokan muiden jäsenten avulla. Kun tarvitaan yleisiä jäseniä käytetään public-avainsanaa. Kaikki jäsenet, jotka on määritelty public-avainsanan jälkeen, ovat sellaisia, että niitä voidaan käsitellä sekä luokan sisältä että luokkaan määriteltyjen olioiden välityksellä, luokan ulkopuolella.
Esimerkki:
class omaLuokka { int a; public: void set_a( int x ); int get_a( ); };
Luokkaan on määritelty yksi privaatti jäsen: kokonaislukumuuttuja a ja kaksi yleistä funktiota, jäsenfunktiot: set_a ja get_a. Muuttuja a on privaatti, joten sitä ei voida käsitellä luokan ulkopuolella. Funktiot set_a ja get_a voivat käsitellä muuttujaa a. Koska set_a ja get_a ovat yleisiä, niitä voidaan kutsua luokan ulkopuolelta sellaisten muuttujien välityksellä, jotka on määritelty omaLuokka-tyyppisiksi.
Edellä on ainoastaan esitelty set_a ja get_a funktiot, ne on vielä määriteltävä:
void omaLuokka :: set_a( int x ) { a = x; } int omaLuokka :: get_a( ) { return a; }
Huomaa, että molemmat edelliset käsittelevät nyt privaattia muuttujaa a.
Nyt on vasta tyyppi luotuna ja valmis käytettäväksi. Vielä on luotava muuttujia, jotka ovat kyseistä tyyppiä. Tässä tapauksessa, kun tyyppinä on luokka, muuttujia kutsutaan olioiksi:
omaLuokka olio1, olio2;
Aivan vastaavasti kuin struktuureillakin vasta nyt on varattu tilaa muuttujia varten. omaLuokkaan liittyvät määrittelyt ovat vain tyyppimäärittelyitä. Luokan jäseniä voidaan käyttää nyt vastaavasti kuin struktuurimuuttuja käyttää struktuurin jäseniä:
olio1.set_a(10); olio2.set_a(99); cout << olio1.get_a( ) << " " << olio2.get_a( );
MUISTA: Jokaista luokan oliota varten on oma kopionsa luokkaan määritellyistä muuttujista. Esimerkiksi edellä olio1:een ja olio2:een liittyvät muuttujat a ovat eri muuttujia. Oliot jakavat yhteisen koodin (set_a ja get_a) mutta molemmilla on oma datansa.
Vielä edellinen luokka kokonaisena ohjelmana:
#include <iostream> class omaLuokka { int a; public: void set_a( int x ); int get_a( ); }; void omaLuokka :: set_a( int x ) { a = x; } int omaLuokka :: get_a( ) { return a; } int main() { omaLuokka olio1, olio2; olio1.set_a(10); olio2.set_a(99); cout << olio1.get_a() << " " << olio2.get_a() << endl; // olio1.a = 10; käännösvirhe // cout << olio2.a; käännösvirhe return 0; }
Kuten yleisiä funktioita, luokkaan voi kuulua myös yleisiä muuttujia:
#include <iostream> class omaLuokka { // tässä on identtinen määrittely struktuurin kanssa public: int a; }; int main() { omaLuokka olio1, olio2; olio1.a = 10; olio2.a = 99; cout << olio1.a << " " << olio2.a << "\n"; return 0; }
Esimerkki: pino
#include <iostream> #define SIZE 10 class pino { char puskuri[ SIZE ]; int next_free; public: void init(); void push( char ch ); char pop(); }; void pino::init() { next_free = 0; } void pino::push( char ch ) { if (next_free == SIZE) cout << "\nPino on täynnä"; else { puskuri[ next_free ] = ch; next_free++; } } char pino::pop() { if (next_free==0) { cout << "\nPino on tyhjä"; return 0; } next_free--; return puskuri[ next_free ]; } int main() { pino s1, s2; int i; s1.init(); s2.init(); s1.push('a'); s2.push('1'); s1.push('b'); s2.push('2'); s1.push('c'); s2.push('3'); for (i=0; i<3; i++) cout << "Pop s1: " << s1.pop() << "\n"; for (i=0; i<3; i++) cout << "Pop s2: " << s2.pop() << "\n"; return 0; }
C:n ja C++:n eroja
- C:ssä funktio, joka palauttaa vaikkapa char:n ja ei ota argumetteja esitellään seuraavasti:
char funktio(void);
C++:ssa void on vapaaehtoinen ja yleensä sitä ei käytetä: char funktio();
C:ssä char funktio(); tarkoittaa, että ei kerrota mitään argumenteista. Argumentit voivat siis olla mitä tahansa. C++:ssa edellinen tarkoittaa, että funktiolla ei ole argumentteja.
- C++:ssa kaikilla funktioilla on oltava prototyypit, C:ssä prototyyppien käyttöä vain suositellaan.
- C:ssä voidaan esitellä paikallisia muuttujia vain lohkon alussa ennen varsinaista toimintaa. C++:ssa paikallisia muuttujia voi esitellä missä vain.
Esimerkkejä:
C:ssä määritellään int main(void), C++:ssa void on turha -> int main()
Paikallisia muuttujia voidaan määritellä "missä vain":
#include <iostream> int main() { int i; cout << "Anna luku: "; cin >> i; int j, kertoma = 1; for ( j = i; j >= 1; j-- ) kertoma = kertoma * j ; cout << "Kertoma on " << kertoma ; return 0; }
Funktioiden kuormitus, alkeita
Funktioiden kuormituksella toteutetaan yksi polymorfismityyppi. C++:ssa kaksi tai useampi funktio voi jakaa yhteisen nimen, mikäli argumenttien lukumäärät tai tyypit eroavat toisistaan tai molemmat. Tällöin sanotaan, että funktiota on kuormitettu (function overloading). Kuormituksen avulla ohjelman kompleksisuutta voidaan vähentää, kun samantapaisiin operaatioihin käytetään samannimisiä funktioita.
Funktion kuormituksen toteutus on helppoa: kirjoitetaan kaikki tarvittavat erilaiset funktiot saman nimisiksi. Kääntäjä valitsee automaattisesti oikean version kutsukohdassa riippuen kutsussa annettujen argumenttien tyypeistä ja lukumäärästä.
Tavallisimmin funktioita kuormitetaan, kun halutaan toteuttaa yksi liittymä ja useita menetelmiä liittymän taakse. Klassisena esimerkkinä C:n itseisarvofunktiot: int abs(int), long labs(long) ja double fabs(double). Tarvitaan kolme eri nimistä funktiota komelle eri tietotyypille, turhan monimutkaista.
C++:ssa voidaan käyttää yhtä nimeä kaikille kolmelle:
#include <iostream> int abs( int n ); long abs( long n ); double abs( double n ); int main( ) { cout << "-10:n itseisarvo: " << abs( -10 ) << "\n"; cout << "10L:n itseisarvo: " << abs( 10L ) << "\n"; cout << "10.01:n itseisarvo: " << abs( 10.01 ) << "\n"; return 0; } int abs( int n ) { cout << "Int, abs\n"; return n < 0 ? n : n ; } long abs( long n ) { cout << "Long, abs\n"; return n < 0 ? n : n ; } double abs( double n ) { cout << "Double, abs\n"; return n < 0 ? n : n ; }
Tässä oli kyseessä pieni esimerkki, hyöty näkyy selvemmin suuressa ohjelmassa, kun voidaan tehdä kaikista samantapaisista asioista samannimisiä, jolloin tarvitsee muistaa vain yleisnimi (+argumentit) kutsuttaessa funktiota.
Kuormitetaan date-funktiota siten, että se hyväksyy päivämäärän merkkijonona tai kolmena kokonaislukuna:
#include <iostream> void date( char *date ); void date( int dd, int mm, int yy ); int main() { date( "23/8/95" ); date( 23, 8, 95 ); return 0; } void date( char *date ) { cout << "Date: " << date << "\n"; } void date( int dd, int mm, int yy ) { cout << "Date: " << dd << "//" << mm; cout << "//" << yy << "\n"; }
Kuormitetut funktiot voivat erota myös argumenttien lukumäärän perusteella:
#include <iostream> void f1( int a ); void f1( int a, int b ); int main () { f1( 10 ); f1( 10, 20 ); return 0; } void f1( int a ) { cout << "Yksi argumentti\n"; } void f1( int a, int b ) { cout << "Kaksi argumenttia\n"; }
Funktioiden kuormituksessa on tärkeää, että KÄÄNTÄJÄN ON PYSTYTTÄVÄ JOKA TILANTEESSA PÄÄTTELEMÄÄN, MITÄ FUNKTIOTA ITSEASIASSA TARKOITETAAN. Esimerkiksi pelkkä palautustyyppi ei riitä eroksi:
... int f1( int a ); float f1( int a ); ... f1( 10 ); // kumpaa tarkoitetaan?
C++ kielen varatut sanat
| asm | inline | private | this |
| catch | new | protected | throw |
| class | operator | public | try |
| delete | overload | template | virtual |
| friend |
Kopio lisenssistä (englanniksi) löytyy täältä.
Alkuperäinen (c) Petteri Hämäläinen
