|
|
|
| Johdanto C++-kieleen |
| Vain
muutamaa hyvin pientä poikkeusta lukuun ottamatta, C on C++:n
osajoukko. Kaikki, mitä voidaan tehdä C:ssä, voidaan tehdä myös
C++:ssa. 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
(polymorfism)
- perintä
(inheritance)
|
| Kapselointi |
|
Kapseloinnin avulla
koodi ja data, jota koodi käsittelee, kytkenää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) 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ää hierarkisia järjestelmiä, luokkia ( hierarchical
classification). Tavallisessa elämässäkin asiat ovat käsiteltävissä
vain hierarkisen luokittelun avulla. Esimerkiksi
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.h
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.h>
void main(void)
{
char c;
cout << "Anna merkkejä, käsitellään x:ään asti\n";
do {
cout << ": ";
cin >> c;
cout << "\n " << c;
} while ( c != 'x' );
}
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: ensisilmäys |
|
C++:n yksittäisistä
ominaisuuksista tärkein on luokka-käsite (class). Luokka on
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.h>
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;
}
void 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
}
Kuten yleisiä
funktioita, luokkaan voi kuulua myös yleisiä muuttujia:
#include <iostream.h>
class omaLuokka
{
// tässä on identtinen määrittely struktuurin kanssa
public:
int a;
};
void main( )
{
omaLuokka olio1, olio2;
olio1.a = 10;
olio2.a = 99;
cout << olio1.a << " " << olio2.a << "\n";
}
Esimerkki:
pino
#include <iostream.h>
#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 ];
}
void 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";
}
|
| 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.h>
void 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 ;
}
|
| 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.h>
int abs( int n );
long abs( long n );
double abs( double n );
void main( )
{
cout << "-10:n itseisarvo: " << abs( -10 ) << "\n";
cout << "10L:n itseisarvo: " << abs( 10L ) << "\n";
cout << "10.01:n itseisarvo: " << abs( 10.01 ) << "\n";
}
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.h>
void date( char *date );
void date( int dd, int mm, int yy );
void main()
{
date( "23/8/95" );
date( 23, 8, 95 );
}
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.h>
void f1( int a );
void f1( int a, int b );
void main ( )
{
f1( 10 );
f1( 10, 20 );
}
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 |
|
|