190
TIE - 02200 Ohjelmoinnin peruskurssi K2017 Luentomoniste ari.suntioinen @ tut.fi

TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

  • Upload
    others

  • View
    5

  • Download
    0

Embed Size (px)

Citation preview

Page 1: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017Luentomoniste

[email protected]

Page 2: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 iSisältö

Python vs. C++

Ohjelmarunko . . . . . . . . . . . . . . . . . . . . . . 1

Muuttujat ja syötteet . . . . . . . . . . . . . . . . . . . 4

Valinta ja silmukat . . . . . . . . . . . . . . . . . . . . 7

Tulkkaus vs. kääntäminen . . . . . . . . . . . . . . . . 9

Muuttujat C++:ssa . . . . . . . . . . . . . . . . . . . . 10

Dynaaminen vs. staattinen tyypitys . . . . . . . . . . . . 13

Eroja perustietotyyppien ominaisuuksissa . . . . . . . . 16

Viitesemantiikka vs. kopiosemantiikka . . . . . . . . . . 19

Funktiot . . . . . . . . . . . . . . . . . . . . . . . . . 24

C++-funktioiden yksityiskohdat . . . . . . . . . . . . . . 26

Arvo- ja viiteparametrit . . . . . . . . . . . . . . . . . . 30

Merkkijonot . . . . . . . . . . . . . . . . . . . . . . . 33

Tiedostojen käsittely . . . . . . . . . . . . . . . . . . . 39

Luokat

Rajapinnan käsite . . . . . . . . . . . . . . . . . . . . 44

Yksinkertainen luokka: Henkilo . . . . . . . . . . . . . 45

Toinen yksinkertainen luokka: Kellonaika . . . . . . . 49

Luokkien käytöllä saavutetut hyödyt . . . . . . . . . . . 54

Nimettömät oliot . . . . . . . . . . . . . . . . . . . . 55

Metodien kuormittaminen . . . . . . . . . . . . . . . . 56

Kopiorakentaja . . . . . . . . . . . . . . . . . . . . . 58

Yleisiä huomioita luokkien suunnittelusta . . . . . . . . . 60

Page 3: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 iiPeriyttämisen alkeet . . . . . . . . . . . . . . . . . . . 61

Abstraktista kantaluokasta periyttäminen . . . . . . . . . 66

Yleisiä huomioita periyttämisestä . . . . . . . . . . . . . 70

Ohjelmointityökaluista

Versionhallinta . . . . . . . . . . . . . . . . . . . . . . 71

Debuggaus . . . . . . . . . . . . . . . . . . . . . . . 75

Standard Template Library (STL)

Standard Template Library: perusideoita . . . . . . . . . 77

STL vector . . . . . . . . . . . . . . . . . . . . . . . . 78

STL-iteraattorit . . . . . . . . . . . . . . . . . . . . . . 83

STL-algoritmit . . . . . . . . . . . . . . . . . . . . . . 87

Muita STL-säiliöitä (set ja map) . . . . . . . . . . . . . 95

set-rakenne . . . . . . . . . . . . . . . . . . . . . . . 96

map-rakenne . . . . . . . . . . . . . . . . . . . . . . . 98

Modulaarisuus ja moduulit

Modulaarisuuden perusideat . . . . . . . . . . . . . . . 103

Esimerkki: geometrialaskut . . . . . . . . . . . . . . . . 104

Esimerkki: bussiaikataulut . . . . . . . . . . . . . . . . 107

Moduulin julkinen rajapinta . . . . . . . . . . . . . . . 117

Moduulin yksityinen rajapinta . . . . . . . . . . . . . . 119

Moduulien suunnittelusta . . . . . . . . . . . . . . . . 121

Modulaarisuuden hyödyt . . . . . . . . . . . . . . . . 124

Page 4: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 iiiMuistinhallinta ja dynaamiset tietorakenteet

Muisti ja muistiosoitteet . . . . . . . . . . . . . . . . . 125

Osoittimet . . . . . . . . . . . . . . . . . . . . . . . . 128

C++ -taulukot . . . . . . . . . . . . . . . . . . . . . . 132

Muistin varaaminen dynaamisesti . . . . . . . . . . . . 135

Älykkäät osoittimet . . . . . . . . . . . . . . . . . . . 138

Dynaamiset tietorakenteet . . . . . . . . . . . . . . . . 143

Tehtävälista (C++-osoittimilla) . . . . . . . . . . . . . . 145

Tehtävälista (shared_ptr-toteutus) . . . . . . . . . . . 152

Kahteen suuntaan linkitetty lista . . . . . . . . . . . . . 156

Tietorakenteen kopiointi: alustus ja sijoitus . . . . . . . . 166

Rekursiiviset funktiot

Rekursio . . . . . . . . . . . . . . . . . . . . . . . . . 171

Rekursio ohjelmoinnissa . . . . . . . . . . . . . . . . . 172

Häntärekursio . . . . . . . . . . . . . . . . . . . . . . 181

Liitteet

Liite A: tulosteiden muotoilu . . . . . . . . . . . . . . . 182

Liite B: funktion oletusparametrit . . . . . . . . . . . . . 184

Liite C: tietueet eli structit . . . . . . . . . . . . . . . . 185

Liite D: for-silmukka . . . . . . . . . . . . . . . . . . 186

Page 5: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 1Python vs. C++: ohjelmarunko

◆ Vertaillaan seuraavia kahta ohjelmaa, jotka ovattoiminnaltaan täysin identtisiä:

#

# Versio 1: Pythonilla

#

def main():

print("Hello world!")

print("Mitä kuuluu?")

main()

//

// Versio 2: C++:lla

//

#include <iostream>

using namespace std;

int main() {

cout << "Hello world!" << endl;

cout << "Mita kuuluu?"

<< endl;

}

◆ Mitä eroja ja yhtäläisyyksiä ohjelmilla on?

Page 6: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 2◆ Ohjelmakoodien eroja ja yhtäläisyyksiä:

● C++-ohjelma käynnistyy aina automaattisesti main-funktiosta.

Pythonissa pääohjelmafunktio nimettiin main:iksi vain"synergiasyistä", koska se on monissa muissakinohjelmointikielissä nimetty niin. Python-kieli itsessään eitällaista nimeämiskäytäntöä vaadi, mutta C++ vaatii.

● Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavastitämä sääntö ei kuitenkaan ole niin kattava, että puolipistettävoisi tunkea aivan joka paikkaan. Tämä on merkittävä virheidenja harmin lähde, kun aloittelevalle C++-ohjelmoijalle ei vielä oletäysin avautunut, minne puolipiste kuuluu ja minne ei.

● Koska käskyt päättyvät useimmiten puolipisteeseen, C++ salliikäskyjen jakamisen usealle riville ilman erityisnotaatiota.Python vaati yleensä katkaistun rivin loppuun \-merkinosoittamaan, että käsky jatkuu seuraavalla rivillä.

● Yhteenkuuluvat ohjelman osat (lohkot) merkitäänaaltosulkeiden sisään, kun Pythonissa ne esitettiin sisennyksenavulla.

● Kommenttimerkki C++-ohjelmassa on //-merkkiyhdistelmä.Pythonissa kommentit ilmaistiin #-merkillä.

● C++:ssa kieli itsessään ei sisällä tulostuskäskyä, vaan tulostus ontoteutettu kirjaston avulla: näytölle tulostaminen janäppäimistöltä lukeminen vaativat iostream-kirjaston:

#include <iostream>

Käytetty #include <...> rakenne on C++:n vastine Pythoninimport-käskylle.

Page 7: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 3● Kun #include <iostream> on tehty, ohjelmasta voidaan

tulostaa tietoa näytölle cout-käskyn ja <<-operaattorin avulla.

Yhdellä cout-käskyllä voi tulostaa niin monta tietoalkiota kuinhaluaa, kunhan muistaa liittää jokaisen eteen <<-operaattorin.

Jos cout:iin tulostaa endl:n, kursori siirtyy seuraavan rivinalkuun.

● C++-koodissa on vielä yksi käskyrivi, jota Python-versiossa einäennäisesti ole:

using namespace std;

Tämä rivi mahdollistaa sen, että iostream-kirjastosta tarvittujanimiä cout ja endl voidaan käyttää sellaisenaan, ilman ettäniiden paikalle olisi aina kirjoitettava std::cout jastd::endl.

Pythonissa on aivan sama mekanismi, joka vaan ei tullutesimerkissä vastaan, koska siinä ei käytetty kirjastoja.Jos Python-ohjelmaan kirjoittaa:

import math

pitää matematiikkakirjaston funktioita kutsua notaatiolla:

math.sqrt(2.0)

Jos taas olisi kirjoittanut:

from math import *

voisi funktioita kutsua suoraan niiden nimellä ilmanmath. -etuliitettä:

sqrt(2.0)

Page 8: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 4Python vs. C++: muuttujat ja syötteet

◆ Suoritetaan vastaava vertailu seuraaville ohjelmakoodeille(jotka myös toimivat identtisesti):

def main():

nimi = input("Syötä nimesi: ")

ikä = int(input("Syötä ikäsi: "))

print("Hauska tavata", nimi, ikä, "v.")

print(50 - ikä, "vuoden päästä olet 50 vuotias.")

main()

#include <iostream>

#include <string>

using namespace std;

int main() {

string nimi = "";

cout << "Syota nimesi: ";

getline(cin, nimi);

int ika = 0;

cout << "Syota ikasi: ";

cin >> ika;

cout << "Hauska tavata " << nimi << " "

<< ika << " v." << endl;

cout << 50 - ika

<< " vuoden paasta olet 50 vuotias."

<< endl;

}

Page 9: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 5◆ Huomioita ja johtopäätelmiä:

● C++-ohjelma on usein pidempi kuin vastaava Python-ohjelma.Yksi esimerkki ei tätä osoita, mutta myös kokemukset puhuvattämän puolesta.

● Pythonissa muuttujia saa käyttöönsä antamalla oliolle uudennimen =-käskyn avulla. Python ei ole nirso siitä, minkätyyppistä tietoa muuttujaan talletetaan ja tyyppi saattaavaihdella ohjelman suorituksen kuluessa.

C++:ssa muuttuja täytyy aina ensin määritellä ennen kuin sitävoi käyttää. Määrittely kertoo C++-kääntäjälle, minkä nimistämuuttujaa ohjelmoija haluaisi jatkossa käyttää ja minkätyyppistä tietoa siihen olisi tarkoitus tallettaa.

Muuttujista on paljon muutakin sanottavaa, joten niihinpalataan myöhemmin erikseen (s. 10).

● Pythonissa muuttujien, funktioiden ja luokkien nimissä olimahdollista käyttää skandinaavisia kirjaimia, C++:ssa se ei olemahdollista. Tämän vuoksi C++-esimerkkikoodissaPython-ohjelman muuttuja ikä oli jouduttu nimeämään ika.

● Periaatteessa merkkijonoissa voisi käyttää C++:sakinskandinaavisia kirjaimia. Kokemus on kuitenkin osoittanut, ettäsiitä seuraa jossain vaiheessa ongelmia. Niinpä ainakaan kurssinesimerkkiohjelmissa skandeja ei käytetä laisinkaan.

● Toisin kuin Pythonissa merkkijonotietotyyppi string ei oleC++:ssa osa kieltä itseään, vaan se on toteutettu kirjastona.

Jos ohjelmassa siis haluaa käsitellä merkkijonoja,#include <string> koodin alussa on välttämätön.

Page 10: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 6● Toisin kuin Pythonin print-funktio, cout ei lisää

automaattisesti välilyöntiä tulostamiensa tietoalkioiden väliin.

● Alkeellinen tapa lukea syötteitä tapahtuu jokogetline-funktiolla, jos halutaan lukea rivillinen tekstiämerkkijonomuuttujaan (analoginen Pythonin input-funktionkanssa), tai cin >> -operaatiolla, jos halutaan lukea lukuja.

● Muuttujat cin ja cout ovat C++:n tapa mahdollistaanäppäimistön ja näytön käsittely ohjelmakoodissa (syöttö- jatulostusoperaatiot).

Page 11: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 7Python vs. C++: valinta ja silmukat

◆ Valinta- ja silmukkarakenteet:

def main():

salaluku = int(input("Syötä salainen luku: "))

arvattu_luku = -1

arvausten_määrä = 0

while salaluku != arvattu_luku:

arvattu_luku = int(input("Anna arvaus: "))

arvausten_määrä += 1

if arvattu_luku < salaluku:

print("Arvaus on liian pieni!")

elif arvattu_luku > salaluku:

print("Arvaus on liian suuri!")

else:

print("Arvasit oikein!")

print("Arvausten lukumäärä: ", arvausten_määrä)

main()

Page 12: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 8#include <iostream>

using namespace std;

int main() {

int salaluku = 0;

cout << "Syota salainen luku: ";

cin >> salaluku;

int arvattu_luku = -1;

int arvausten_maara = 0;

while ( salaluku != arvattu_luku ) {

cout << "Anna arvaus: ";

cin >> arvattu_luku;

arvausten_maara += 1;

if ( arvattu_luku < salaluku ) {

cout << "Arvaus on liian pieni!" << endl;

} else if (arvattu_luku > salaluku ) {

cout << "Arvaus on liian suuri!" << endl;

} else {

cout << "Arvasit oikein!" << endl;

}

}

cout << "Arvausten lukumaara: "

<< arvausten_maara << endl;

}

◆ Merkintätavallisia eroja (puolipisteet, sulut jne.) lukuunottamattaesimerkeissä ei ole mitään uusia radikaaleja eroavaisuuksia.

Page 13: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 9Tulkkaus vs. kääntäminen

◆ Python on tulkattava ohjelmointikieli ja C++ on käännettävä

ohjelmointikieli.

◆ Tulkattavan ohjelmointikielen ohjelmia suoritettaessa jokaisellasuorituskerralla tarvitaan erillinen apuohjelma, jota kutsutaankyseisen ohjelmointikielen tulkiksi (esim. Python-tulkki).

◆ Jos tulkkiohjelma poistetaan tai se tuhoutuu, kyseiselläohjelmointikielellä kirjoitettuja ohjelmia ei voida enää suorittaaennen kuin tulkki asennetaan uudelleen.

◆ Tulkin tehtävänä on muuttaa lähdekoodin käskyjä konekielellesitä mukaa, kun ohjelma etenee, jotta tietokoneen keskusyksikkö(prosessori) pystyy suorittamaan ne.

◆ Käännettävällä ohjelmointikielellä toteutettujen ohjelmiensuorittaminenkin vaatii apuohjelman, jota kutsutaanohjelmointikielen kääntäjäksi (esim. C++-kääntäjä).

◆ Kääntäjä kuitenkin suorittaa koko lähdekooditiedostolle suurenmäärän virhetarkistuksia ja tuottaa siitä kokonaisen konekielisenohjelman, joka yleensä talletetaan kovalevylle erilliseentiedostoon (esim. Windows:issa .exe-päätteiseen tiedostoon).

◆ Kun käännösprosessi on valmis, kääntäjää ei enää tarvita, koskakovalevylle talletettua konekielistä ohjelmaa voidaan suorittaa yhäuudelleen ilman kääntäjäohjelman apua.

◆ Toki, jos lähdekoodiin tehdään muutoksia, se on käännettäväuudelleen konekieliseksi tiedostoksi, eli kääntäjää tarvitaanuudelleen.

◆ On olemassa monia ohjelmointikieliä, jotka sijoittuvat jonnekintulkattavien ja käännettävien kielien välimaastoon: kääntäminennk. välikoodiksi, jota sitten voidaan suorittaa nopeasti javähemmillä virhetarkasteluilla.

Page 14: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 10Muuttujat C++:ssa

◆ C++:ssa muuttuja on määriteltävä, ennen kuin sitä voi käyttää.

◆ Muuttujan määrittely muodostuu kolmesta osasta, joista kaksiensimmäistä on pakollisia:

1. Muuttujan tietotyyppi, eli siihen talletettavan tiedon tyyppi.

2. Muuttujan nimi.

3. Alkuarvon antaminen muuttujalle, eli sen alustaminen.

◆ Esimerkkejä muuttujan määrittelystä:

// Määrittely ilman alustusta

int ika;

double lampotila;

string nimi;

// Määrittely alustuksen kera

int ika = 21;

double lampotila = 16.7;

string nimi = "Teppo";

◆ Alustamattoman muuttujan arvo on epämääräinen, kunnes semyöhemmin asetetaan jollain tavoin ( =-operaattori tai cin >> ):

int ika;

double lampotila;

// Tässä kohdassa seka ika- etta lampotila-

// muuttujien arvo on epämääräinen.

ika = 21;

cin >> lampotila;

// Nyt kummallakin on määrätty arvo.

◆ C++ on tarkka muuttujan tyypistä: muuttujaan ei voi tallettaa

muuta kuin sen määrittelytyyppiä vastaavaa tietoa.

Page 15: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 11◆ Lohkon (siis aaltosulkeiden) sisällä määritelty paikallinen

muuttuja on käytettävissä määrittelykohdastaan lohkonloppusulkuun saakka:

int main() {

int osallistujia = 0;

while ( osallistujia < 100 ) {

int vapaita_paikkoja = 100 - osallistujia;

· · · rivejä poistettu · · ·

// Tämä on viimeinen kohta, jossa muuttujaa

// vapaita_paikkoja voi käyttää.

}

· · · rivejä poistettu · · ·

// Tämä on viimeinen kohta, jossa muuttujaa

// osallistujia voi käyttää.

}

◆ Yleensä puhutaan muuttujan näkyvyysalueesta.

◆ Tietotyypit, joilla C++:ssa pärjää yksinkertaisissa ohjelmissapitkälle, ovat int, double, bool ja string.

C++:n double vastaa Pythonin float-tyyppiä (desimaaliluku).

◆ Lisäksi C++:ssa on tietotyyppi char, jonka avulla voidaan käsitelläyhtä 8-bittistä merkkiä. Esimerkiksi seuraava on mahdollista:

char sukupuoli = ’N’;

◆ Termi literaali tarkoittaa nimetöntä vakioarvoa ohjelmakoodissa,esimerkiksi: 42, 1.4142, ’x’, "teksti" tai true.

Page 16: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 12◆ Huomaa seuraava merkittävä ero Pythonin ja C++:n välillä:

● Pythonissa ei ole erillistä tietotyyppiä merkkien käsittelyyn,vaan ne esitetään merkkijonona, jossa on vain yksi merkki.

● Merkkijonoliteraalit sai Pythonissa kirjoittaa joko lainaus- taiheittomerkkien sisään.

● C++:ssa merkkijonoliteraalit (string-tyyppiset) esitetäänlainausmerkkien "abc" sisällä.

● Heittomerkkien ’x’ sisällä pitää olla yksi merkki ja kyseinenliteraali on tyypiltään char.

◆ C++:ssa muuttujiin liittyy vielä yksi lisäominaisuus, jota vastaavaaPythonissa ei ole laisinkaan: vakiomuuttujat eli const-vakiot:

const double PII = 3.14;

· · · rivejä poistettu · · ·

// VIRHE// Yritys muuttaa const-vakion arvoa myöhemmin

// ohjelmakoodissa tuottaa virheilmoituksen.

PII = 3.141592653589793;

◆ const-vakiot on tarkoitettu sellaisten arvojen nimeämiseenohjelmakoodissa, joiden ei ole tarkoitus muuttua. Luonnonvakiotkuten pii ovat suoraviivaisimpia esimerkkejä, muttaconst-vakioille löytyy muitakin käyttötarkoituksia:

const int LOTTOPALLOJA = 7;

◆ Koska const-vakion arvoa ei voi muuttaa määrittelyn jälkeen, onaika loogista, että se on alustettava määrittelyn yhteydessä.

◆ Ohjelmointityylillisesti const-vakiot nimetään usein pelkilläisoilla kirjaimilla ja käyttäen sanojen erottimena _-merkkiä.

◆ Hyvin valitut const-vakioiden nimet selkeyttävät ohjelmakoodia.

Page 17: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 13Dynaaminen vs. staattinen tyypitys

◆ Miten seuraava Python-ohjelma käyttäytyy (rivinumerot eivät oleosa ohjelmakoodia):

1 def main():

2 print("Alku")

3 arvo1 = 5.3

4 arvo2 = 7.1

5 print(arvo1 + arvo2)

6 arvo2 = "Hei" # Vanhan muuttujan uudelleenkäyttö

7 print("Keskiväli")

8 print(arvo1 + arvo2)

9 print("Loppu")

10 main()

◆ Kun ohjelma suoritetaan, kaikki toimii normaalisti, kunnes rivillä 8ohjelman suoritus päättyy virheeseen:

Alku

12.399999999999999

Keskiväli

Traceback (most recent call last):

File ‘‘koodit/osa-01-04.py’’, line 11, in <module>

main()

File ‘‘koodit/osa-01-04.py’’, line 8, in main

print(arvo1 + arvo2)

TypeError: unsupported operand type(s) for +:

’float’ and ’str’

◆ Käyttäytyminen johtuu siitä, että Pythonissa on käytössä nk.dynaaminen tyypitys, mikä tarkoittaa sitä, että käsiteltävänäolevan tietoalkion tietotyypin sopivuus tarkastetaan vasta sillähetkellä, kun tietoalkiota yritetään käyttää.

Page 18: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 14◆ Käytännössä dynaaminen tyypitys ilmenee siten, että:

● Sama muuttuja voi ohjelman suorituksen aikana esittää erityyppisiä arvoja.

● Ohjelma toimii normaalisti siihen saakka, kunnes koodissayritetään käsitellä väärän tyyppistä tietoa väärässä paikassa.

◆ Dynaaminen tyypitys on hyvin yleinen tulkattavissaohjelmointikielissä.

◆ Vastaava esimerkki C++:lla näyttäisi seuraavalta:

1 #include <iostream>

2 #include <string>

3

4 using namespace std;

5

6 int main() {

7 cout << "Alku" << endl;

8 double arvo1 = 5.3;

9 double arvo2 = 7.1;

10 cout << arvo1 + arvo2 << endl;

11 string arvo3 = "Hei"; // Uusi muuttuja

12 cout << "Keskivali" << endl;

13 cout << arvo1 + arvo3 << endl;

14 cout << "Loppu" << endl;

15 }

◆ Jos tämä ohjelmakoodi yritetään kääntää, saadaan virheilmoitusja käännös jää kesken (virheilmoitusta yksinkertaistettu):

main.cpp:13: error: no match for ’operator+’

cout << arvo1 + arvo3 << endl;

◆ C++:ssa on käytössä staattinen tyypitys: jos ohjelmakoodissayritetään käsitellä tietoa tavalla, jota C++ ei ymmärrä/salli,tuloksena on virhe kääntäjältä.

Page 19: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 15◆ Ohjelmoijalle staattinen tyypitys näkyy käytännössä seuraavasti:

● Muuttujat on määriteltävä ennen käyttöä.

● Muuttujilla on kiinteästi määrätty tyyppi, eikä niihin voi sijoittaakuin niiden tyyppiä vastaavaa tietoa.

● Jos ohjelmassa on tiedon tyyppeihin liittyvä virhe, ohjelmaa eivoi kääntää konekielelle, eikä siis suorittaa tai testata.

◆ Staattinen tyypitys on hyvin yleinen käännettävissäohjelmointikielissä.

◆ Huomaa, kuinka C++-versiossa on rivillä 11 jouduttumäärittelemään uusi string-tyyppinen muuttuja arvo3, koskaarvo2:een ei voi tallettaa merkkijonoa kuten Pythonissa pystyitekemään.

◆ Dynaamisesti tyypitettyjen kielien etuna on se, että ohjelmoijapääsee pienemmällä työmäärällä koodia kirjoittaessaan, kunkaikkea tyypitykseen liittyvää (muuttujien määrittely jne.) eitarvitse mikromanageroida.

Monet ohjelmoijat myös pitävät siitä, että muuttuja voi viitatajuuri sen tyyppiseen arvoon, kun kulloisellakin hetkellä ontarpeen.

◆ Dynaamisesti tyypitettyjen kielien taakkana taas on se, ettäohjelmaan jää helpommin virheitä, aivan kutenesimerkkiohjelmassa tapahtui, kun yritettiin laskea yhteenreaaliluku ja merkkijono.

Lisäksi, saman muuttujan käyttäminen useaan eri tarkoitukseenohjelmakoodissa ei edesauta ohjelman ymmärrettävyyttä.

Page 20: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 16Eroja perustietotyyppien ominaisuuksissa

◆ Seuraavat kaksi ohjelmaa vaikuttavat ensi silmäyksellä identtisiltä:

def main():

luku = int(input("Syötä luku: "))

if luku < 0:

print("Oltava ei-negatiivinen!")

else:

tulos = 1;

while luku > 0:

tulos = tulos * luku

luku -= 1

print("Kertoma on:", tulos)

main()

#include <iostream>

using namespace std;

int main() {

cout << "Syota luku: ";

int luku = 0;

cin >> luku;

if ( luku < 0 ) {

cout << "Oltava ei-negatiivinen!" << endl;

} else {

int tulos = 1;

while ( luku > 0 ) {

tulos = tulos * luku;

--luku;

}

cout << "Kertoma on: " << tulos << endl;

}

}

Page 21: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 17◆ Kuitenkin jos ohjelmat suoritetaan ja annetaan alkuarvoksi

esimerkiksi 17, tulostaa Python-ohjelma 355687428096000 jaC++-ohjelma -288522240.

C++:n laskema tulos on epäilemättä väärin, sillä kertoman arvo(ei-negatiivisten lukujen tulo) ei voi olla negatiivinen.

◆ Mistä ongelma johtuu?

◆ Python on jossain määrin harvinainen ohjelmointikieli, koska siinäkokonaislukujen arvoa ei ole rajoitettu.

◆ Useimmissa muissa ohjelmointikielissä kokonaisluvut (ja muutkinlukutyypit) esitetään konekielitasolle päädyttäessä jollainkiinteällä määrällä bittejä.

Tämä bittimäärä riippuu pääosin käytössä olevastaprosessoriarkkitehtuurista, mutta joskus myös käytetystäkääntäjästä.

Tyypillinen bittimäärä kokonaislukuarvojen tapauksessa on32 bittiä, mutta saattaa olla esimerkiksi 16 tai 64 bittiäkin.

◆ Jos koneen prosessori esittää kokonaislukuarvot 32 bitillä, se eipysty käsittelemään kuin kokonaislukuja, jotka ovat välillä-2147483648 – 2147483647 (yhteensä 232 erilaista).

◆ Jos laskutoimituksen tuloksena syntyy em. prosessorillakokonaisluku, jota ei voi esittää 32 bitillä, tapahtuu ylivuoto.

Ylivuodon seurauksena tulos on virheellinen.

◆ Vastaava tilanne voi syntyä myös reaaliluvuilla laskettaessa,olkoonkin että bittimäärät ja lukuarvot eivät ole samat, koskareaaliluvut esitetään eri muodossa kuin kokonaisluvut.

◆ Alivuodoksi kutsutaan tilannetta, jossa reaaliluvuilla laskettaessasaadaan itseisarvoltaan niin pieni tulos, että prosessori ei osaaerottaa sitä nollasta.

Page 22: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 18◆ C++:ssa ohjelmoija voi jossain määrin yrittää vaikuttaa siihen,

miten lukuja käsitellään: C++:ssa esimerkiksi on useita erikokonaislukujen esittämiseen tarkoitettuja tietotyyppejä, kunPythonissa oli vain yksi.

Koska kyseessä on jonkin verran nippelitietämys, tällä kurssillayritetään selvitä seuraavilla tyypeillä:

int

Normaali kokonaislukutyyppi, joka sopii positiivisten janegatiivisten arvojen käsittelyn.

unsigned int

Kokonaislukutyyppi, jolla voi käsitellä vain ei-negatiivisia lukuja(luonnolliset luvut).

long int

Tyyppi, joka saattaa mahdollistaa suuremman esitysalueen kuinnormaali int-tyyppi, mutta vain jos prosessoriarkkitehtuuritukee sitä.

unsigned long int

Melko looginen yhdistelmä kahta edellistä.

Jos myöhemmin ilmenee tarvetta muille, ne otetaan esiin siinävaiheessa.

◆ Olennainen huomio tässä vaiheessa on se, että C++-kieli eimäärittele, kuinka monella bitillä lukutietotyypit esitetään.

Ei siis kannata luottaa siihen, että int-tyyppiset kokonaisluvutesitetään aina ja kaikkialla 32 bitillä, vaikka se sattumalta onkinniin kurssin työympäristössä.

Page 23: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 19Viitesemantiikka vs. kopiosemantiikka

◆ Pythonissa ei ole muuttujia ainakaan siinä merkityksessä kuinmillaisiksi muuttujat perinteisesti ajatellaan.

◆ Pythonissa on nimiä ja tietoalkioita (olioita), ja mekanismi, jollanimi saadaan sidottua tietoalkioon siten, että myöhemminohjelmakoodissa tietoalkiota voidaan käsitellä (eli siihen voidaanviitata) nimen avulla.

◆ Käytännössä tämä tarkoittaa sitä, että Pythonin =-käskyn avullatietoalkioille voi antaa nimiä.

◆ Johdatus ohjelmointiin -kurssilla tätä nimen ja tietoalkionsuhdetta havainnollistettiin siten, että esimerkiksi koodista:

muuttujaA = 5

muuttujaB = muuttujaA

muuttujaC = 5

piirrettiin suunnilleen seuraava kuvio:

muuttujaA

muuttujaB

muuttujaC

5

Eli on olemassa vain yksi tietoalkio 5, jolla voi olla tarvittaessa yksitai useampia nimiä (muuttujaA, muuttujaB ja muuttujaC ),joiden avulla tietoalkiota voidaan käsitellä.

◆ Tällaista tapaa nimetä tietoalkioita kutsutaan viitesemantiikaksi.

Page 24: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 20◆ C++:ssa oletusmekanismi on erilainen: uuden muuttujan alustus

{alkuarvo } -notaation avulla ja =-operaattori luovattietoalkiosta uuden erillisen kopion:

int muuttujaA = 5;

int muuttujaB = muuttujaA;

int muuttujaC;

muuttujaC = 5;

jolloin kuvallinen havainnollistus näyttääkin seuraavalta:

muuttujaA

muuttujaB

muuttujaC

5

5

5

Eli nyt ohjelmassa onkin käsiteltävänä useita eri tietoalkioita, jotkakuitenkin esittävät samaa arvoa, kokonaislukua 5.

◆ Tätä mekanismia kutsutaan kopiosemantiikaksi (myösarvosemantiikaksi), koska käsiteltävänä olevasta tietoalkiostasyntyy alustuksen ja sijoituksen yhteydessä uusi kopio.

◆ Edellä esitetty havaintokuva ei ole niin havainnollinen kuin voisitoivoa, vaan on parempi miettiä kopiosemantiikan toimintaahiukan yksityiskohtaisemmalla tasolla.

Page 25: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 21◆ Kopiosemantiikkaa noudattavassa ohjelmointikielessä muuttuja ei

tarkoita jollekin tietoalkiolle annettua nimeä, vaan se on tulkittavatietylle keskusmuistialueelle annetuksi nimeksi.

Lisäksi sijoitus ja alustus tulkitaan kyseisellä muistialueella olevantiedon korvaamiseksi jollain uudella tiedolla.

◆ Tästä tulee termi muuttuja: muuttujan nimeämän muistialueensisältö (siis muuttujaan talletettu arvo) voi muuttua.

◆ Tältä pohjalta voisi siis myös esittää väitteen, että Pythonissa ei olevarsinaisia muuttujia, sillä Pythonilla ohjelmoitaessa ainoa asiajoka vaihtelee ohjelman suorituksen kuluessa on se, mistäsuorakaiteesta (nimi) mihin palloon (arvo) viiva piirretään.

Asia ei tietenkään ole aivan näin suoraviivainen ja edellinen väiteon jossain määrin virheellinen. Miksi?

◆ Kuvallisesti havainnollisempi tapa esittää muuttujia C++:ssa ontilanteesta riippuen jompi-kumpi seuraavista:

muuttujaA muuttujaB muuttujaC

5 55

tai jos halutaan esittää asia toteutusteknisemmällä tasolla:

· · ·· · ·

muuttujaA muuttujaBmuuttujaC

555

jossa lokerot kuvaavat keskusmuistissa olevaa tilaa (muistialueita),joihin tietoa voidaan tallentaa.

Page 26: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 22◆ Tässä vaiheessa kysymys kuuluu: miksi oli tarpeen miettiä näin

perustavalla tavalla viite- ja kopiosemantiikan eroa?

Esimerkki vastaa kysymykseen. Tutkitaan jälleen kahta ohjelmaa,joissa käytetään hiukan monimutkaisempaa tietotyyppiä.Python-ohjelmassa tietotyyppinä list, jota C++:ssa vastaa lähes1:1 kirjastotyyppi deque.

def main():

varasto1 = [ 3, 9, 27, 81, 243 ]

varasto2 = varasto1

varasto2[3] = 0

print(varasto1[3], varasto2[3])

main()

#include <iostream>

#include <deque>

using namespace std;

int main() {

deque<int> varasto1 = { 3, 9, 27, 81, 243 };

deque<int> varasto2;

varasto2 = varasto1;

varasto2.at(3) = 0; // Indeksointi .at-metodilla

cout << varasto1.at(3) << " "

<< varasto2.at(3) << endl;

}

◆ Kaiken sen pohjalta, mitä viite- ja kopiosemantiikasta nyttiedetään, mitä eroa edellisten ohjelmien toiminnassa on?

Vihje: Python tulostaa: 0 0 ja C++: 81 0.

Page 27: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 23◆ Selitys on se, että C++:ssa sijoitus varasto2 = varasto1 luo

uuden kopion deque-rakenteesta. Kun kopiosta ( varasto2 )sitten muutetaan yhtä alkiota, alkuperäisen rakenne ( varasto1 )säilyy muuttumattomana.

Pythonissa taas sekä varasto1 että varasto2 ovat vain kaksieri nimeä samalla listalle, jolloin toisen muuttaminen näkyy myöstoisessa.

◆ Myös C++:ssa voi halutessaan luoda viitteitä eli antaa samallemuuttujalle useita nimiä:

#include <iostream>

#include <deque>

using namespace std;

int main() {

deque<int> varasto1 = { 3, 9, 27, 81, 243 };

deque<int>& varasto2(varasto1);

varasto2[3] = 0;

cout << varasto1[3] << " "

<< varasto2[3] << endl;

}

◆ Viite luodaan C++:ssa lisäämällä muuttujan määrittelyssätietotyypin nimen ja muuttujan nimen väliin &-merkki.

Viite on myös alustettava sillä muuttujalla, johon viitteen halutaanviittaavaan (siis mille muuttujalle halutaan antaa lisänimi).

◆ C++:n viitteet eivät edellisen esimerkin mukaisessakäyttötarkoituksessa ole kovin hyödyllisiä, mutta myöhemminnähdään (s. 30), kuinka viitteet voivat olla funktion parametrientyyppinä. Tämä johtaa hyödyllisempään käyttötarkoitukseen.

Page 28: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 24Python vs. C++: funktiot

def keskimmäinen(luku1, luku2, luku3):

if luku2 <= luku1 <= luku3 \

or luku3 <= luku1 <= luku2:

return luku1

elif luku1 <= luku2 <= luku3 \

or luku3 <= luku2 <= luku1:

return luku2

else:

return luku3

def main():

print(keskimmäinen(4, 5, 1))

main()

#include <iostream>

using namespace std;

int keskimmainen(int luku1, int luku2, int luku3) {

if ( (luku2 <= luku1 and luku1 <= luku3)

or (luku3 <= luku1 and luku1 <= luku2) ) {

return luku1;

} else if ( (luku1 <= luku2 and luku2 <= luku3)

or (luku3 <= luku2 and luku2 <= luku1) ) {

return luku2;

} else {

return luku3;

}

}

int main() {

cout << keskimmainen(4, 5, 1) << endl;

}

Page 29: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 25◆ Esimerkkikoodien keskeisimmät erot liittyvät dynaamisen

staattisen tyypityksen asettamiin vaatimuksiin: C++:ssaohjelmoijan täytyy eksplisiittisesti (erikseen) määrätä funktionpaluuarvon ja parametrien tyypit, jotta kääntäjä voi tarkastaa,että funktion kutsukohdassa käytetyt parametrit ovat sopivantyyppisiä ja että paluuarvoa käytetään hyväksyttävällä tavalla.

◆ Varsinainen funktion kutsunotaatio on kummassakin kielessäidenttinen:

funktion_nimi (pilkuilla_erotellut_parametrit )

◆ Myös funktion paluuarvo määrätään kummassakin kielessä aivansamoin return-käskyn avulla.

◆ Lisäksi esimerkeistä käy ilmi vertailuoperaattoreihin liittyväyksityiskohta: Pythonissa vertailuoperaattoreita voi ketjuttaa:

if a <= b <= c:

· · ·

C++:ssa vastaavat loogiset lausekkeet pitää kirjoittaa aukiand-operaattorin avulla:

if ( a <= b and b <= c ) {

· · ·

}

Page 30: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 26C++-funktioiden yksityiskohdat

◆ Ennen kuin C++:ssa voi kutsua funktiota, kääntäjän pitää ollatietoinen funktion olemassaolosta, parametrien tyypeistä jalukumäärästä sekä funktion paluuarvon tyypistä.

◆ Funktio voidaan määritellä (definition) kokonaisuudessaanennen kuin sitä yritetään kutsua:

#include <iostream>

using namespace std;

double keskiarvo(double luku1, double luku2) {

return (luku1 + luku2) / 2;

}

int main() {

cout << keskiarvo(2.0, 5.0) << endl;

}

◆ Funktion määrittelyllä tarkoitetaan funktion koodin kirjoittamistakokonaisuudessaan.

◆ Funktio voidaan myös esitellä (declaration) ennen kutsukohtaaja määritellä vasta kutsukohdan jälkeen:

#include <iostream>

using namespace std;

double keskiarvo(double luku1, double luku2);

int main() {

cout << keskiarvo(2.0, 5.0) << endl;

}

double keskiarvo(double luku1, double luku2) {

return (luku1 + luku2) / 2;

}

Page 31: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 27◆ Funktion esittelyssä funktiosta kerrotaan minimaalinen määrä

informaatiota, jotta kääntäjä voi tarkastaa, että funktiotakutsutaan oikein. C++:ssa funktion esittely on muotoa:

paluuarvon_tyyppi funktion_nimi (

pilkuilla_erotellut_parametrimuuttujien_määrittelyt );

◆ Funktio on tietysti aina määriteltävä jossain vaiheessa, koska vastamäärittely sisältää funktion rungon eli ne käskyt, jotka funktiotakutsuttaessa halutaan suorittaa.

◆ Jos C++:ssa on tarve määritellä funktio, joka ei palauta mitäänarvoa (tällaista funktiota kutsutaan usein aliohjelmaksi), setapahtuu erikoisen tietotyypin void avulla:

#include <iostream>

using namespace std;

void tulosta_kertotaulu(int minka_luvun) {

if ( minka_luvun <= 0 ) {

cout << "Virhe: oltava positiiviluku!" << endl;

return;

}

int kertoja = 1;

while ( kertoja <= 9 ) {

cout << kertoja * minka_luvun << endl;

++kertoja;

}

}

int main() {

tulosta_kertotaulu(7);

}

◆ Aliohjelmafunktiossa ei ole pakko olla return-käskyä ollenkaan,vaan siitä palataan automaattisesti, kun funktion rungonloppusulku tulee käskyjä suoritettaessa vastaan.

Page 32: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 28◆ Koska Pythonissa on dynaaminen tyypitys, yksi ja sama funktio

voi periaatteessa käsitellä useita eri tyyppisiä parametreja jatoimia eri tavoin parametrin tyypistä riippuen. Käytännössä tämätapahtuisi jotenkin seuraavasti:

def funktio(param):

if type(param) is int:

# Mitä jos parametri on int-tyyppinen?

elif type(param) is float:

# Mitä jos parametri on float-tyyppinen?

else:

# Mitä tehdään muussa tapauksessa?

◆ Koska C++:ssa on staattinen tyypitys, edellisen kaltainen järjestelyei toimi, sillä kääntäjän on tiedettävä parametrien tyypit jokäännösaikana.

◆ C++:ssa on kuitenkin useampikin mekanismi, jolla vastaavanlaisiageneerisiä/polymorfisia funktioita voidaan toteuttaa.

◆ Helppotajuisin näistä mekanismeista on funktioidenkuormittaminen, millä tarkoitetaan sitä, että ohjelmoija voimääritellä useita saman nimisiä funktioita, kunhan niidenparametrien tyypeissä ja/tai lukumäärissä on eroja.

Kun kuormitettua funktiota aikanaan kutsutaan, kääntäjä osaapäätellä kutsukohdassa käytetyistä todellisista parametreista, mitäversiota funktiosta on tarkoitus kutsua:

#include <iostream>

using namespace std;

double neliosumma(double a, double b) {

return (a + b) * (a +b);

}

Page 33: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 29int neliosumma(int a, int b) {

return (a + b) * (a +b);

}

int main() {

// Kutsutaan ensimmäistä versiota

cout << neliosumma(1.2, 3.4) << endl;

// Kutsutaan toista versiota

cout << neliosumma(3, 4) << endl;

}

◆ Kuten edellä vihjattiin, C++:ssa on muitakin mekanismejavastaavien tilanteiden käsittelyyn. Niihin ei kuitenkaan tälläkurssilla ehdittäne tutustua.

Page 34: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 30Arvo- ja viiteparametrit

◆ Koska C++:ssa ohjelmoija voi muuttujaa määritellessään päättääitse, luodaanko aito muuttuja vai pelkkä viite olemassa olevaanmuuttujaan, avaa tämä uusia mahdollisuuksia myös funktionmuodollisten parametrien määrittelyssä.

Muodollisiksi parametreiksi kutsutaan muuttujia, joiden avullafunktion rungossa päästään käsiksi funktion kutsukohdassakäytettyihin todellisiin parametreihin tai niiden arvoihin.

◆ Jollei ohjelmoija erikseen määrää toisin, C++:ssa funktionparametrit ovat oletusarvoisesti arvoparametreja:

#include <iostream>

using namespace std;

void arvoparametrifunktio(int luku) {

luku = 2 * luku;

}

int main() {

int muuttuja = 7;

cout << muuttuja << endl; // Tulostuu 7

arvoparametrifunktio(muuttuja);

cout << muuttuja << endl; // Tulostuu edelleen 7

}

Edellisessä funktion muodollinen parametrimuuttuja luku onuusi muuttuja, joka alustetaan todelliselle parametrille muuttuja

evaluoituvalla arvolla 7. Vaikka funktio muuttaa muodollisenparametrinsa arvon kaksinkertaiseksi, ei sillä ole vaikutustatodelliseen parametriin, koska kyseessä on eri muuttuja.

Page 35: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 31◆ Ohjelmoija voi kuitenkin halutessaan määritellä osan tai kaikki

funktion muodolliset parametrit viiteparametreiksi:

#include <iostream>

using namespace std;

// Ainoat erot edelliseen koodiin verrattuna ovat

// selkeyden vuoksi muutettu funktion nimi ja

// seuraavalle riville ilmestynyt &-merkki.

void viiteparametrifunktio(int& luku) {

luku = 2 * luku;

}

int main() {

int muuttuja = 7;

cout << muuttuja << endl; // Tulostuu 7

viiteparametrifunktio(muuttuja);

cout << muuttuja << endl; // Tulostuu 14

}

Uudessa versiossa funktion parametri luku onkin viiteparametri,johon tehdyt muutokset heijastuvat suoraan todelliseenparametriin muuttuja.

◆ Koska C++:ssa viite antaa lisänimen olemassa olevalle muuttujalla(tai tarkemmin ottaen: jo entuudestaan käytössä olevallemuistialueelle), seuraava koodi on virheellinen:

void viiteparametrifunktio(int& luku) {

luku = 2 * luku;

}

int main() {

viiteparametrifunktio(7); // VIRHE}

Syy: literaaliin ei C++:ssa voi olla viitteitä, se ei ole muuttuja.

Page 36: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 32◆ Pythonissa asioiden tila saattaa pikaisesti ajateltuna tuntua siltä,

että funktion parametrit ovat arvoparametreja. TodellisuudessaPythonissa kaikki "muuttujat" noudattavat viitesemantiikkaa (eliovat viitteitä), funktioiden parametrit mukaan lukien.

Virheellinen päätelmä johtunee ainakin osittain siitä, ettäPythonin yhteydessä pitää muistaa myös käsitteet muuttuva jamuuttumaton tietotyyppi (mutable ja non-mutable), jotkaovat Pythonin mekanismi viitesemantiikan toteuttamiseksi osallesen tietotyypeistä. Muuttumatonta tietotyyppiä oleva arvofunktion parametrina käyttäytyy näennäisesti kuin se olisiarvoparametri.

Näistä ei kuitenkaan suuremmin murehdittu Johdatusohjelmointiin -kurssilla, eikä ole hyvää syytä, miksi kannattaisimurehtia tässäkään vaiheessa.

Page 37: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 33Python vs. C++: merkkijonot

◆ Pythonissa merkkijonotietotyyppi (str) kuuluumuuttumattomien (non-mutable) tietotyyppien joukkoon.

Tämä tarkoittaa sitä, että merkkijonotyyppistä arvoa ei voimuuttaa:

def main():

teksti = "abcdefg"

print(teksti[3]) # Tulostuu: d

teksti[3] = "X" # VIRHE!print(teksti)

main()

◆ Tämä ongelma vältettiin sillä, että aina kun merkkijononsisältämää tekstiä haluttiin muokata, luotiin uusimerkkijonotyyppinen arvo, johon muutos oli huomioitu, janimettiin uusi arvo samalla nimellä kuin alkuperäinen:

def main():

teksti = "abcdefg"

print(teksti[3]) # Tulostuu: d

teksti = teksti[:3] + "X" + teksti[4:]

print(teksti) # Tulostuu: abcXefg

main()

Page 38: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 34◆ C++:ssa jo olemassa olevaa merkkijonoakin voi muuttaa

(paitsi jos se on const string -tyyppinen vakio):

#include <iostream>

#include <string>

using namespace std;

int main() {

string teksti = "abcdefg";

cout << teksti.at(3) << endl; // Tulostuu: d

teksti.at(3) = ’X’; // Huomaa char-tyyppi

cout << teksti << endl; // Tulostuu: abcXefg

}

◆ Sekä Pythonissa että C++:ssa merkkijonon merkkien indeksointialkaa nollasta.

◆ C++:ssa merkkijonon yksittäiset merkit ovat char-tyyppisiä, eliindeksointioperaatio merkkijono.at(indeksi ) tuottaa ainachar-tyyppisen arvon, tai jos indeksointioperaatio on sijoituksenkohteena, sijoitettavan arvon on oltava char.

◆ Merkkijonoihin kohdistuvat nimetyt operaatiot noudattavatC++:ssa samanlaista metodifunktioiden kutsunotaatiota kuinPythonissakin:

#include <iostream>

#include <string>

using namespace std;

int main() {

string merkkijono = "Tipitii-tipitii";

cout << merkkijono.length() << endl;

cout << merkkijono.substr(8, 4) << endl;

merkkijono.replace(12, 3, "-ta-daa");

cout << merkkijono << endl;

}

Page 39: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 35◆ Jotkut merkkijonoja käsittelevät operaatiot tuottavat

paluuarvonaan lukuarvoja, esimerkiksi:

merkkijono.length(); // Merkkien lukumäärä

merkkijono.find("abc"); // Mistä kohdasta löytyy

// ensimmäinen "abc"?

Jos näitä merkkijonoihin liittyviä lukuarvoja (merkkien määrä,indeksit) haluaa tallettaa muuttujaan, sen tyypin on oltavastring::size_type:

#include <iostream>

#include <string>

using namespace std;

int main() {

string nimi = "";

string::size_type apu = 0;

cout << "Syota nimesi: ";

getline(cin, nimi);

apu = nimi.length();

cout << "Nimessa on " << apu << " kirjainta" << endl;

apu = nimi.find("nen");

if ( apu == string::npos ) {

cout << "Nimessa ei kirjainyhdistelmaa nen" << endl;

} else {

cout << "Yhdistelma oli kohdassa " << apu << endl;

}

}

Huomaa, kuinka find palauttaa arvon string::npos,jos etsittyä merkkiyhdistelmää ei löydy.

Page 40: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 36◆ Hyödyllisiä C++-merkkijono-operaatioita.

Kaikkia tässä listassa olevia funktioita kutsutaan notaatiolla:muuttuja.funktio (parametrit ).

string::size_type length()

Paluuarvo kertoo, kuinka monta merkkiä merkkijonossa on.

string::size_type pituus = 0;

pituus = merkkijono.length();

char at(indeksi)

Palauttaa merkkijonon indeksinnen merkin. Jos at-funktion kutsu onsijoituksen kohteena, korvaa indeksinnen merkin.

char merkki;

merkki = merkkijono.at(5);

merkkijono.at(2) = ’k’; // Huomaa char-tyyppi

string erase(indeksi)string erase(indeksi, pituus)

Jos vain yksi parametri, tuhoaa merkkijonosta kaikki merkit kohdastaindeksi alkaen. Jos kutsussa myös parametri pituus, tuhoaa niinmonta merkkiä eteenpäin.

merkkijono.erase(4);

merkkijono.erase(2, 5);

string::size_type find(etsittävä)string::size_type find(etsittävä, indeksi)

Etsii merkkijonon alusta (tai kohdasta indeksi ) alkaen ensimmäisenvastaantulevan etsittävä -merkkijonon sijaintikohdan ja palauttaakyseisen kohdan indeksin. Jos etsittävää ei löydy, paluuarvo onvakio string::npos.

string::size_type kohta = 0;

kohta = merkkijono.find("abc", 5);

if ( kohta == string::npos ) {

// Ei löytynyt "abc":tä indeksistä 5 eteenpäin

} else {

// "abc" löytyi.

}

Page 41: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 37string::size_type rfind(etsittävä)string::size_type rfind(etsittävä, indeksi)

Kuten funktio find edellä, mutta etsii merkkijonon lopusta alkua kohti.

string substr(indeksi)string substr(indeksi, pituus)

Palauttaa merkkijonon osan. Jos vain yksi parametri, paluuarvo on kokomerkkijono kohdasta indeksi merkkijonon loppuun saakka. Josannetaan myös parametri pituus, palautuu niin monta merkkiäkohdasta indeksi eteenpäin.

string testimerkkijono = "abcdefg";

string tulos;

tulos = testimerkkijono.substr(3); // "defg"

tulos = testimerkkijono.substr(4, 2); // "ef"

string insert(indeksi, lisäys)

Lisätään merkkijonon indeksinnen merkin eteen teksti lisäys.

string testimerkkijono = "abcd";

testimerkkijono.insert(2, "xy"); // "abxycd"

string replace(indeksi, pituus, korvaaja)

Korvataan merkkijonossa indeksinnestä merkistä alkaen pituus

merkkiä tekstillä korvaaja.

string testimerkkijono = "ABCDEF";

testimerkkijono.replace(1, 3, "xy"); // "AxyEF"

◆ Lisää hyödyllisiä C++-merkkijono-operaatioita.

merkkijono1 == merkkijono2

merkkijono1 != merkkijono2

merkkijono1 < merkkijono2

merkkijono1 > merkkijono2

merkkijono1 <= merkkijono2

merkkijono1 >= merkkijono2

Vertailuoperaattoreilla voi vertailla merkkijonojen suhdetta toisiinsa.Operaattorit, joissa on mukana pienempi- tai suurempi-kuin -operaatio,vertailevat nk. leksikaalijärjestystä, joka on merkkijonojen tapauksessalähes sama asia kuin aakkosjärjestys.

Page 42: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 38merkkijono1 + merkkijono2

Liimataan kaksi merkkijona yhdeksi.

string testi1 = "Qt";

string testi2 = "Creator";

string tulos;

tulos = testi1 + " " + testi2; // "Qt Creator"

merkkijono += lisäys

Liimaa merkkijonon perään lisäyksen, joka voi olla joko merkkijonotai yksittäinen merkki (char-tyyppinen arvo).

string tulos = "Qt";

tulos += ’ ’; // "Qt "

tulos += "Creator"; // "Qt Creator"

◆ Vielä lisää hyödyllisiä C++-merkkijono-operaatioita.

Loput tässä listatuista operaatioista ovat normaaleja funktioita.

getline(virta, rivi)

Luetaan tiedostosta tai näppäimistölä (cin) yksi rivi tekstiä ja talletetaanse merkkijonoviiteparametriin rivi.

int stoi(merkkijono)

Muutetaan parametri merkkijono vastaavaksi kokonaisluvuksi.

string lukumerkkijono = "123";

int luku;

luku = stoi(lukumerkkijono); // 123

double stod(merkkijono)

Vastaava kuin stoi edellä, mutta muuttaa merkkijonon reaaliluvuksi.

string lukumerkkijono = "123.456";

double luku;

luku = stod(lukumerkkijono); // 123.456

Edellisten stoi- ja stod-funktioiden ongelmana on se, että ne eivättunnista merkkijonoa virheelliseksi, jos sen alku vaan näyttää oikeantyyppiseltä luvulta. Esimerkiksi "123abc" kelpaa stoi-funktiolle jafunktio palauttaa 123. Virhettä ei voi kurssin tiedoilla huomata helposti.

Page 43: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 39Python vs. C++: tiedostojen käsittely

◆ Seuraavassa yksinkertainen ohjelma, joka laskee tiedostossaolevien kokonaislukujen summan. Lukujen pitää olla tiedostossajokaisen omalla rivillään. Tyhjiä rivejä ei saa olla.

def main():

tiedoston_nimi = input("Syötä tiedoston nimi: ")

try:

tiedosto_olio = open(tiedoston_nimi, "r")

summa = 0

for rivi in tiedosto_olio:

summa += int(rivi)

tiedosto_olio.close()

print("Lukujen summa: ", summa)

except IOError:

print("Virhe tiedoston avaamisessa.")

except ValueError:

print("Virhe tiedoston rivillä")

main()

Page 44: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 40#include <iostream>

#include <fstream> // Huomaa kirjasto

#include <string>

using namespace std;

int main() {

string tiedoston_nimi = "";

cout << "Syota tiedoston nimi: ";

getline(cin, tiedoston_nimi);

ifstream tiedosto_olio(tiedoston_nimi);

if ( not tiedosto_olio ) {

cout << "Virhe tiedoston avaamisessa."

<< endl;

} else {

int summa = 0;

string rivi;

while ( getline(tiedosto_olio, rivi) ) {

summa += stoi(rivi);

}

tiedosto_olio.close();

cout << "Lukujen summa: " << summa

<< endl;

}

}

◆ Itse asiassa ohjelmat eivät toimi täysin samoin, koskaC++-koodissa tiedostosta luettu rivi muutetaan kokonaisluvuksistoi-funktiolla, joka ei huomaa, jos rivin lopussa on jotainylimääräistä.

Tai jos rivin alussa on jotain kokonaisluvuksi kelpaamatonta,stoi aiheuttaa poikkeuksen, ja ohjelman suoritus päättyy. C++:npoikkeusten käsittelyyn ei tässä vaiheessa ole tietotaitoja.

Page 45: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 41◆ Jos C++:ssa halutaan lukea tiedostoja, toimitaan seuraavasti:

● Otetaan ohjelman alussa käyttöön fstream-kirjasto.

● Riippuen siitä, halutaanko tiedostoa lukea vai kirjoittaa,määritellään joko ifstream- tai ofstream-tyyppinenmuuttuja, jonka alkuarvoksi alustetaan käsiteltävän tiedostonnimi.

● Tiedoston avaamisen epäonnistuminen voidaan tunnistaakäyttämällä tiedostomuuttujaa if-rakenteen ehtona: silleevaluoituu arvo true, jos tiedoston avaaminen onnistui,false muussa tapauksessa.

● Kuten Pythonissakin, helpoin tapa lukea tiedostoa, on suorittaalukeminen silmukassa rivi kerrallaan merkkijonoon, jota sittenjatkokäsitellään merkkijono-operaatioilla.

● Kun tiedosto on luettu tai kirjoitettu loppuun, on hyvä tapasulkea se close-metodia kutsumalla.

◆ ifstream-tyyppisestä tiedostomuuttujasta voidaan lukea tietoamyös >>-operaattorin samalla tavalla, kun on totuttu lukemaancin:stä.

◆ Tiedostoon kirjoittaminen (siis kovalevylle tallentaminen)tapahtuu kohdistamalla ofstream-tyyppiseentiedostomuuttujaan <<-operaattori aivan kuten on totuttutekemään cout:in kanssa.

◆ Yleisnimitys muuttujalle, jolla voidaan käsitellä (lukea ja kirjoittaa)tietokoneen oheislaitteita, on tietovirta (stream).

C++:ssa kaikki oheislaitteiden käsittely on toteutettu tietovirtojenavulla. Lisäksi toteutusmekanismi on niin elegantti, että kaikkiasyötevirtoja voidaan käsitellä toistensa kanssa identtisesti. Samapätee myös tulostusvirroille.

Page 46: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 42◆ Konkreettisesti tämä tarkoittaa sitä, että sekä cin:iä että kaikkia

ifstream-tyyppisiä tietovirtamuuttujia käsitellään samoillaoperaatioilla ja ne käyttäytyvät yhdenmukaisesti. Toisaalta myössekä cout:ia että ofstream-tyyppisiä virtoja käsitellään samoin.

Tämä on hyvä asia, koska ohjelmoijan ei tarvitse opetella kuin yksimekanismi, jota sitten voidaan soveltaa useaankäyttötarkoitukseen.

◆ Lyhyt lista hyödyllisiä operaatioita tietovirtojen käsittelyyn.

cout << tulostettava

tulostusvirta << tulostettava

Tulostusvirtaan saadaan tulostettua tai tallennettua tietoa<<-operaattorin avulla.

cin >> tallennusmuuttuja

syötevirta >> tallennusmuuttuja

Syötevirrasta voidaan lukea tunnetun tyyppinen tietoalkio suoraankyseistä tyyppiä olevaan muuttujaan >>-operaattorilla.

getline(syötevirta, rivi)getline(syötevirta, rivi, erotinmerkki)

Luetaan syötevirrasta rivillinen tekstiä ja talletetaan semerkkijonoon rivi. Jos kutsussa annetaan kolmas parametrichar-tyyppinen erotinmerkki, lukemista ja rivi -muuttujaantallentamista jatketaan, kunnes tietovirrassa tulee vastaan ensimmäinenerotinmerkki.

string tekstirivi = "";

// Yksi rivillinen näppäimistöltä

getline(cin, tekstirivi);

// Seuraavaan kaksoispisteeseen saakka

// (saattaa lukea useita rivejä kerralla)

getline(tiedosto_olio, tekstirivi, ’:’);

Page 47: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 43syötevirta.get(merkki)

Luetaan syötevirrasta yksi merkki (char) muuttujaan merkki, joka onviiteparametri.

// Tiedoston voi lukea läpi merkki kerrallaan:

char luettu_merkki;

while ( tietosto_olio.get(luettu_merkki) ) {

cout << "Merkki: " << luettu_merkki << endl;

}

syötevirta.eof()

Funktiolla voidaan tarkastaa, epäonnistuiko viimeisin lukuyritystietovirrasta sen vuoksi, että tiedosto on luettu loppuun.

Ymmärrä, mitä edellinen virke ja seuraavat esimerkit tarkoittavat, ennenkuin yrität käyttää eof-metodia, muuten mokaat.

// Ensin on yritettävä lukea virrasta jotain

tiedosto_olio.get(merkki);

// Sen jälkeen voi yrittää tutkia epäonnistuiko

// edeltävä lukuyritys siitä syystä, että luettavaa

// ei enää ollut jäljellä.

if ( tiedosto_olio.eof() ) {

// Tiedosto luettu loppuun: muuttujassa merkki

// on nyt epämääräinen ja käyttökelvoton arvo.

} else {

// Muuttujassa merkki on tiedostosta

// onnistuneesti luettu char-tyyppinen arvo.

}

Miksi seuraava ratkaisu on lähes poikkeuksetta virheellinen:

while ( not tiedosto_olio.eof() ) { // VIRHE!getline(tiedosto_olio, rivi);

· · ·

}

◆ Jos myöhemmin ilmenee tarvetta muille tietovirtojen käsittelyynliittyviin operaatioihin, palataan asiaan siinä yhteydessä.

Page 48: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 44Rajapinta

◆ Termi rajapinta tarkoittaa ohjelmoinnin yhteydessä järjestelyä,jonka avulla ohjelmoijan mahdollisuuksia päästä suoraan käsiksijohonkin ohjelman osaan on rajoitettu.

Ohjelmoija voi hyödyntää kätkettyjä/rajoitettuja osia vainjoidenkin ennalta täsmällisesti määriteltyjen operaatioidenvälityksellä.

Rajapinta tarkoittaa konkreettisesti sitä, että ohjelmoijan ei oletarpeen tietää, kuinka jokin asia on toteutettu, mutta hän pystyysilti hyödyntämään sen palveluita.

◆ Esimerkki: string-tietotyyppi ja string-tyyppiset muuttujat.

Keskimääräisellä ohjelmoijalla ei ole tietoa tai ymmärrystä siitä,kuinka string-tyyppi on C++-kirjastossa toteutettu. Minkälaisiaongelmia on pitänyt ratkaista, jotta on saatu toteutettua rakenne,johon voidaan tallentaa ennalta määräämätön määrä tekstiä.

Kuitenkin jokainen sivut 33–38 sisäistänyt pystyy hyödyntämäänstring-tyyppiä ohjelmassaan, koska sille on määritelty joukkofunktioita ja operaattoreita, joiden avulla kaikki tarpeellisetoperaatiot saadaan suoritettua. Nämä valmiit operaatiot ovatstring-tyypin (julkinen) rajapinta.

◆ Julkista rajapintaa voi ajatella eräänlaisena käyttöliittymänä, jokamäärää, mitä jollekin asialle voi tehdä ja mitä ei.

◆ Mitä termi yksityinen rajapinta voisi tarkoittaa?

Page 49: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 45Luokat

◆ Palautetaan mieleen, kuinka yksinkertainen luokka määriteltiinPythonissa ja kuinka sitä voitiin käyttää:

class Henkilö:

def __init__(self, nimi, ikä):

self.__nimi = nimi

self.__ikä = ikä

def hae_nimi(self):

return self.__nimi

def vietä_syntymäpäivää(self, monesko):

self.__ikä = monesko

def tulosta(self):

print(self.__nimi, ":", self.__ikä)

def main():

kaveri = Henkilö("Matti", 18)

print(kaveri.hae_nimi())

kaveri.tulosta()

kaveri.vietä_syntymäpäivää(19)

kaveri.tulosta()

main()

◆ Mikä on luokan ja olion ero?

Luokka on tietotyyppi ja olio on termi, jota käytetään arvosta taimuuttujasta, jonka tietotyyppi on jokin luokka.

Joskus olioita kutsutaan myös luokan instansseiksi.

Page 50: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 46◆ Vastaava luokka toteutettaisiin C++:ssa seuraavasti:

// Edeltä poistettu rivejä tilan säästämiseksi!

class Henkilo {

public:

Henkilo(string nimi, int ika);

string hae_nimi() const;

void vieta_syntymapaivaa(int monesko);

void tulosta() const;

private:

string nimi_;

int ika_;

}; // Huomaa puolipiste!

int main() {

Henkilo kaveri("Matti", 18);

cout << kaveri.hae_nimi() << endl;

kaveri.tulosta();

kaveri.vieta_syntymapaivaa(19);

kaveri.tulosta();

}

Henkilo::Henkilo(string nimi, int ika):

nimi_(nimi), ika_(ika) {

}

string Henkilo::hae_nimi() const {

return nimi_;

}

void Henkilo::vieta_syntymapaivaa(int monesko) {

ika_ = monesko;

}

void Henkilo::tulosta() const {

cout << nimi_ << " : " << ika_ << endl;

}

Page 51: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 47◆ C++:ssa luokan määrittely jakautuu selkeästi kahteen osaan:

● public-osassa esitellään luokan julkinen rajapinta, eli nemetodit (jäsenfunktiot), joiden avulla luokan olioita voidaankäsitellä.

● private-osaan kätketään muuttujat (jäsenmuuttujat,attribuutit), joiden avulla luokan kuvaama käsite halutaanohjelmassa mallintaa.

◆ private-osassa oleviin jäsenmuuttujiin ei pääse suoraan käsiksiluokan ulkopuolelta. Jos ja kun niiden arvoja halutaan käsitellä,luokan julkiseen rajapintaan on lisättävä metodi, jokamahdollistaa tarvittavien operaatioiden suorittamisen.

Metodien rungossa jäsenmuuttujia käsitellään normaalienmuuttujien tavoin, mutta niitä ei tarvitse erikseen määritellä,koska jokaisella oliolla on oma kopio jäsenmuuttujista.

◆ Metodit voidaan luokitella neljään kategoriaan:

rakentaja eli konstruktori

Rakentajafunktion nimi on aina sama kuin luokan nimi, eikäsille merkitä paluuarvon tyyppiä laisinkaan.

Rakentajaa kutsutaan automaattisesti aina, kun uusi olioluodaan. Sen tehtävänä on alustaa luotu olio.

valitsin eli selektori

Valitsimet ovat metodeja, joiden avulla olion tilaa (siis jäsen-muuttujien arvoja) voidaan tutkia, mutta niitä ei voida muuttaa.

Valitsimet ilmaistaan lisäämällä varattu sana const

parametrilistan loppusulun perään.

mutaattori eli muuttaja

Mutaattorien avulla on mahdollista muuttaa olion tilaa, elijäsenmuuttujien arvoja.

purkaja eli destruktori

Purkajaa kutsutaan automaattisesti, kun olion elinkaari päättyy.Esimerkkiohjelmassa ei ollut purkajaa.

Page 52: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 48◆ Rakentajaa ja purkajaa lukuun ottamatta kaikkia metodeja

kutsutaan tutulla notaatiolla:

olio.metodin_nimi (parametrit );

◆ Rakentajan kutsu tapahtuu automaattisesti kulissien takana aina,kun uusi olio on tarpeen alustaa.

Rakentajan parametrit kirjoitetaan (kaari)sulkeissa1 määriteltävänäolevan olion nimen perään.

Rakentajafunktion määrittelyssä olion jäsenmuuttujat alustetaanalustuslistan avulla.

◆ Jos luokalla on purkaja, sitä kutsutaan automaattisesti olionelinkaaren lopussa.

Paikallisten olioiden elinkaari päättyy niiden näkyvyysalueenlopussa.

◆ Luokka on työkalu abstraktioiden (käsitteiden)muodostamiseen ohjelmassa.

Luokan toteutusyksityiskohdat kätketään sen käyttäjältä, jokapystyy hyödyntämään luokkaa vain sen tarjoaman julkisenrajapinnan välityksellä.

Ohjelmaan muodostuu käsitteitä, joiden olemus määräytyyniiden mahdollisten käyttötarkoitusten kautta: "Henkilö on sitä,mitä sille voi tehdä."

Kokemus on osoittanut, että tällaisilla toiminnallisuuden avullamääritellyillä käsitteillä on taipumus selkeyttää ohjelmaa.

1 Tarkasti ottaen aaltosulkeetkin kelpaavat, ja on olemassa tilanteita, joissa niitä on pakko käyttää. Ei kuitenkaan murehdita siitäkään tässävaiheessa, koska kyseessä on ikävä poikkeus muuten melko johdonmukaiseen sääntöön.

Page 53: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 49Kellonaika-luokka

◆ Toteutetaan toinen luokka.

#include <iostream>

#include <iomanip>

using namespace std;

class Kellonaika {

public:

Kellonaika(int tunti, int minuutti);

void tiktok(); // Aika kasvaa yhdellä minuutilla

void tulosta() const;

private:

int tunnit_;

int minuutit_;

};

int main() {

Kellonaika aika(23, 59);

aika.tulosta();

aika.tiktok();

aika.tulosta();

}

Page 54: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 50Kellonaika::Kellonaika(int tunti, int minuutti):

tunnit_(tunti), minuutit_(minuutti) {

}

void Kellonaika::tiktok() {

++minuutit_;

if ( minuutit_ >= 60 ) {

minuutit_ = 0;

++tunnit_;

}

if ( tunnit_ >= 24 ) {

tunnit_ = 0;

}

}

void Kellonaika::tulosta() const {

cout << setw(2) << setfill(’0’) << tunnit_

<< "."

<< setw(2) << minuutit_

<< endl;

}

◆ tulosta-metodissa käytettiin iomanip-kirjaston operaatioitatulostusasun säätämiseen. Ei murehdita niistä.

◆ Tässä esimerkissä ei ole mitään dramaattista uutta aiempaanverrattuna. Se on johdanto seuraavaan esimerkkiin.

Page 55: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 51Kellonaika-luokka hiukan toisin

◆ Muutetaan Kellonaika-luokan toteutusta hiukan.

#include <iostream>

#include <iomanip>

using namespace std;

class Kellonaika {

public:

Kellonaika(int tunti, int minuutti);

void tiktok();

int hae_tunti() const;

int hae_minuutti() const;

void tulosta() const;

private:

// Kello 00.00:sta kuluneet minuutit

int kuluneet_minuutit_;

};

int main() {

Kellonaika aika(23, 59);

aika.tulosta();

aika.tiktok();

aika.tulosta();

}

Page 56: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 52Kellonaika::Kellonaika(int tunti, int minuutti):

kuluneet_minuutit_(60 * tunti + minuutti) {

}

void Kellonaika::tiktok() {

++kuluneet_minuutit_;

if ( kuluneet_minuutit_ >= 24 * 60 ) {

kuluneet_minuutit_ = 0;

}

}

int Kellonaika::hae_tunti() const {

// Kun kokonaisluku jaetaan kokonaisluvulla

// tuloksena on kokonaisluku (jakojäännös

// heitetään menemään).

return kuluneet_minuutit_ / 60;

}

int Kellonaika::hae_minuutti() const {

return kuluneet_minuutit_ % 60;

}

void Kellonaika::tulosta() const {

cout << setfill(’0’) << setw(2) << hae_tunti()

<< "."

<< setw(2) << hae_minuutti()

<< endl;

}

◆ Uudessa toteutuksessa on tapahtunut alkuperäiseen verrattunamuutama mielenkiintoinen muutos.

Page 57: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 53◆ Esimerkistä käy ilmi yksi luokkien (pikemminkin rajapintojen)

hyvä puoli: luokan yksityinen rajapinta (toteutus) on muutettukokonaan, mutta koska julkinen rajapinta on pidettyyhteensopivana alkuperäisen kanssa, luokkaa hyödyntävää koodia(main) ei ole tarvinnut muuttaa laisinkaan.

◆ tulosta-metodissa ei enää ole suoraa viittausta luokanjäsenmuuttujaan, vaan tulostettava kellonaika selvitetään kahdenuuden metodin hae_tunti ja hae_minuutti avulla.

Tämä tarkoittaa sitä, että tulosta-metodin ei tarvitse enää tietää,missä muodossa kellonaika on private-osassa esitetty.

Luokan sisälle on siis muodostettu uusi (epämuodollinen)rajapinta: osa luokan omista metodeista ei käsittelejäsenmuuttujia suoraan, vaan tyytyy operoimaan niillähae_tunti- ja hae_minuutti-metodien välityksellä.

Ratkaisun hyvä puoli on se, että jos luokan toteutusta (siisprivate-osaa ja sitä käsitteleviä metodeja) muutetaan,tulosta-metodin toteutukseen ei tarvitse koskea, kunhanhuolehditaan siitä, että hae_tunti ja hae_minuutti toimivatsamoin.

◆ Huomaa myös, kuinka metodista voidaan kutsua toista samanluokan metodia: kutsunotaatio on sama kuin normaalejaei-metodifunktioita kutsuttaessa:

metodin_nimi (parametrit );

Uusi metodikutsu kohdistuu tällöin samaan olioon, jota käytettiinalkuperäistä metodin kutsuttaessa pisteen edessä.

Page 58: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 54Luokkien käytön hyödyt

◆ Kun käsitteitä mallinnetaan ohjelmassa luokkien avulla, silläsaavutetaan lähes poikkeuksetta etuja.

Itse asiassa nämä edut eivät ole pelkästään luokkien mukanaantuoma etu, vaan kaikki muutkin mekanismit, joiden avullaohjelmaan voidaan muodostaa selkeitä rajapintoja, tuottavatsamat edut.

◆ Luokkien hyviä puolia:

● Toteutuksen muuttaminen yksityisessä rajapinnassa onmahdollista, kun samaan aikaan julkinen rajapinta pysyyyhteensopivana aiemman toteutuksen kanssa.

● Voidaan olla varmoja tiedon eheydestä. Konstruktorit jamutaattorit voivat huolehtia siitä, että olio ei voi saadavirheellistä arvoa.

● Luokat selkeyttävät ohjelmaa, sen ymmärrettävyyttä jaylläpidettävyyttä.

● Luokat ovat usein uudelleenkäytettäviä (reusable).

● Luokat auttavat ohjelman monimutkaisuuden hallinnassa, silläniiden avulla ohjelman loogisia osia voidaan koota yhteen.

◆ Kannattaa panna merkille, että funktiokin muodostaa rajapinnan.

Kun ymmärtää mitä funktiolle on tarkoitus antaa parametrina jaminkä arvon se tuottaa parametreistaan paluuarvona,toteutuksesta ei tarvitse tietää mitään.

Myös funktioihin siis voidaan yhdistää kaikki edellä listatut edut.

Page 59: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 55Nimettömien olioiden muodostaminen

◆ Joskus saattaa tulla eteen tilanne, jossa koodissa on tarpeenkäyttää oliota johonkin hetkelliseen tarpeeseen. Käytön jälkeenoliolle ei ole muuta tarvetta.

C++:ssä ei ole mekanismia literaalisten olioiden esittämiseen,mutta luokan rakentajaa voidaan hyödyntää nimettömienväliaikaisolioiden muodostamiseksi (esimerkin tapaukset 2 ja 3).

◆ Tutkitaan seuraavaa esimerkkiä, joka olettaa, että sivulla 46määritelty Henkilo-luokka on olemassa:

bool lisaa_henkilo_jasenlistalle(Henkilo henk);

int main() {

// Seuraavat kolme vaihtoehtoa toimivat identtisesti:

// Tapaus 1: apumuuttujan avulla

Henkilo testihenkilo("Aleksi", 21);

lisaa_henkilo_jasenlistalle(testihenkilo);

// Tapaus 2: rakentajan avulla

lisaa_henkilo_jasenlistalle( Henkilo("Aleksi", 21) );

// Tapaus 3: kuten edellä, mutta kääntäjä osaa

// päätellä parametrin tyypistä, että

// pitää luoda Henkilo-tyyppinen olio.

// Huomaa aaltosulkeiden käyttö!

lisaa_henkilo_jasenlistalle( {"Aleksi", 21} );

}

bool lisaa_henkilo_jasenlistalle(Henkilo henk) {

cout << "Lisataan jasenlistalle:" << endl;

henk.tulosta();

// Esimerkin kannalta epäolennaista koodia poistettu

}

Page 60: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 56Metodien kuormittaminen

◆ Luokan metodit ovat funktioita, vaikka niillä onkin joitainerityispiirteitä normaaleihin funktioihin verrattuna. Niidenparametrinvälitystä ja paluuarvoja koskevat kuitenkin samatsäännöt kuin muitakin funktioita, niinpä niitä voidaan tarvittaessamyös kuormittaa (paitsi purkajaa).

Valitsimien ja mutaattorien kannalta tässä ei ole mitään erikoista,eikä niistä tarvinne esittää erillistä esimerkkiä.Mielenkiintoisemmaksi asian tekee se, että myös luokan rakentajavoi olla kuormitettu:

#include <iostream>

using namespace std;

class Kompleksiluku {

public:

Kompleksiluku(); // Oletusrakentaja

Kompleksiluku(double r_osa);

Kompleksiluku(double r_osa, double i_osa);

void tulosta() const; // Testausta varten

// Muiden metodien esittely kuuluisi tähän.

// Jätetty pois tilan säästämiseksi.

private:

double reaali_;

double imaginaari_;

};

Page 61: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 57Kompleksiluku::Kompleksiluku():

reaali_(0.0), imaginaari_(0.0) {

}

Kompleksiluku::Kompleksiluku(double r_osa):

reaali_(r_osa), imaginaari_(0.0) {

}

Kompleksiluku::Kompleksiluku(double r_osa, double i_osa):

reaali_(r_osa), imaginaari_(i_osa) {

}

void Kompleksiluku::tulosta() const {

cout << "(" << reaali_ << ", " << imaginaari_ << ")"

<< endl;

}

int main() {

Kompleksiluku komp1; // Oletusrakentaja

Kompleksiluku komp2(2.34);

Kompleksiluku komp3(4.56, 7.89);

komp1.tulosta(); // (0.0, 0.0)

komp2.tulosta(); // (2.34, 0.0)

komp3.tulosta(); // (4.56, 7.89)

}

◆ Kääntäjä osaa päätellä, mitä versiota kuormitetusta rakentajastaon tarpeen kutsua, kun se tutkii olion määrittelykohdassakäytettyjen alustusarvojen lukumääriä ja tyyppejä.

Alustus tapahtuu oletusrakentajan (parametriton) avulla, josmäärittelykohdassa ei ole annettu mitään alkuarvoja.

Luokalle on aina hyvä määritellä oletusrakentaja, vaikka sitä eikaikissa edellisissä esimerkeissä olekaan tehty. Tällä tavoinvältytään tietyiltä kummallisuuksilta, jotka liittyvät siihen, kuinkaC++ joskus määrittelee sen automaattisesti ja joskus ei.

Page 62: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 58Kopiorakentaja

◆ Rakentajaa, joka alustaa olion toisesta saman tyyppisestä joolemassa olevasta oliosta, kutsutaan kopiorakentajaksi.

Kopiorakentaja määritellään siten, että sen parametrin tyyppi onconst-viite. Kehitetään edellistä kompleksilukuesimerkkiä:

class Kompleksiluku {

public:

Kompleksiluku(const Kompleksiluku& alkuarvo);

// Julkinen rajapinta muuten sama kuin aiemmin.

private:

double reaali_;

double imaginaari_;

};

Kompleksiluku::Kompleksiluku(const Kompleksiluku& alkuarvo):

reaali_(alkuarvo.reaali_),

imaginaari_(alkuarvo.imaginaari_) {

}

// Loppujen metodien määrittelyjen pitäisi olla tässä.

int main() {

Kompleksiluku komp1(1.2, 3.4);

Kompleksiluku komp2(komp1);

komp1.tulosta(); // (1.2, 3.4)

komp2.tulosta(); // (1.2, 3.4)

}

Page 63: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 59◆ Huomaa, kuinka kopiorakentajan alustuslistassa päästään

suoraan tutkimaan parametrina saadun alkuarvo-olionprivate-osassa olevia muuttujia.

Tämä on mahdollista vain silloin, kun private-osaan pyrkivämetodi on saman luokan metodi, kuin minkä luokan olioparametri on.

Tämä sama pätee yleistäen myös luokan metodien rungossaolevaan koodin: jos metodi saa parametrinaan samaa luokkaaedustavan olion, se pääsee käsiksi olion jäsenmuuttujiin. -operaattorin avulla.

◆ Funktion parametrin tyyppinä käytetty const-viite onomintakeinen C++:n kikka (idiomi), jonka tausta onpohjimmiltaan tehokkuussyissä.

Koska C++:ssa on kopiosemantiikka, funktion muodollistenparametrien arvot saadaan kopioimalla todellisten parametrienarvot. Jos kyseessä on suurikokoinen tietorakenne, kopiointisaattaa olla hidasta.

Tämä vältetään tekemällä muodollisesta parametrista viite, koskasilloin todellisen parametrin arvoa ei tarvitse kopioida.

const-puolestaan on tyyppimäärittelyssä mukana siksi, ettäfunktio ei voi vahingossa (tai tarkoituksella) muuttaa todellisenparametrin arvoa tilanteissa, joissa se olisi semanttisesti(merkitykseltään) epäjohdonmukaista.

◆ Tätä samaa mekanismia käytetään C++:ssa yleisesti muissakintilanteissa, kun halutaan välttää mahdollinen suurikokoisen arvonkopiointi funktiokutsun yhteydessä.

Esimerkiksi funktiot, joiden (todellisena)parametrina onmerkkijono, jonka ei ole tarkoitus muuttua funktiokutsunseurauksena, on usein toteutettu tämän idean mukaisesti:

bool onko_palindromi(const string& mjono);

Page 64: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 60Yleisiä huomioita luokkien suunnittelusta

◆ Kun suunnittelee omia luokkiaan on hyvä pitää mielessä tiettyjäperussääntöjä, jotka auttavat varmistamaan aika pitkälle, ettäluokista tulee järkeviä.

◆ C++:ssa luokkien tarkoitus tarjota rajapinta private-osaankätketyn tiedon käsittelyyn.

Ei siis ole perusteltua tarvetta määritellä luokkaa, josta private-osa puuttuu kokonaan (siis luokka, joka ei sisällä jäsenmuuttujia).

Tällaisen luokan voi poistaa ja korvata sen metodit normaaleillafunktioilla.

◆ Luokan private-osassa kuulu olla ne jäsenmuuttujat, jotkavälttämättä tarvitaan olion tilan (siis arvon) esittämiseen.

Aloittelevalla ohjelmoijalla tulee helposti houkutus laittaajäsenmuuttujiksi myös sellaista tietoa, joka voitaisiin paremmintallentaa paikallisiin väliaikaismuuttujiin luokan metodeissa.

◆ private-osassa voi olla myös metodien määrittelyitä. Näitäyksityisiä metodeja ei voi kutsua muualta kuin luokan toisistametodeista.

◆ Luokan julkisen rajapinnan pitäisi tarjota kaikki tarpeellisetoperaatiot luokan olioiden käsittelyyn.

Luokan suunnittelussa on yleensä jotain pielessä, jos ohjelmoijanon olion arvoa päivittääkseen haettava vanha arvo luokanulkopuolella jonkinlaisella hakufunktiolla, laskettava sille uusiarvo ja lopulta talletettava uusi arvo takaisin luokan sisällejonkinlaisella asetusfunktiolla.

Page 65: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 61Periyttämisen alkeet

◆ Periyttäminen (inheritance) ohjelmointikielissä on mekanismi,jonka avulla olemassa olevasta luokasta (kantaluokka) voidaanmuodostaa uusia luokkia (aliluokkia, periytettyjä luokkia), joillaon samat perusominaisuudet kuin kantaluokalla, mutta myösjoitain lisäominaisuuksia.

Tämä on erittäin epämuodollinen ja epätäsmällinen määritelmäperiyttämiselle, mutta siitä on voidaan lähteä liikkeelle.

◆ Tutkitaan yksinkertaista esimerkkiä:

#include <iostream>

#include <string>

using namespace std;

//--------------------------------------------------------

class Kulkuneuvo {

public:

Kulkuneuvo(double nopeus, string vari);

double hae_nopeus() const;

void aseta_nopeus(double nopeus);

string hae_vari() const;

void raportoi_matka(double aika) const;

private:

double nopeus_;

string vari_;

};

Page 66: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 62Kulkuneuvo::Kulkuneuvo(double nopeus, string vari):

nopeus_(nopeus), vari_(vari) {

}

double Kulkuneuvo::hae_nopeus() const {

return nopeus_;

}

void Kulkuneuvo::aseta_nopeus(double nopeus) {

nopeus_ = nopeus;

}

string Kulkuneuvo::hae_vari() const {

return vari_;

}

void Kulkuneuvo::raportoi_matka(double aika) const {

cout << hae_vari() << " kulkuneuvo, jonka nopeus on "

<< hae_nopeus() << ", liikkuu "

<< aika << " sekunnissa "

<< hae_nopeus() * aika / 3600.0 * 1000.0 << " m"

<< endl;

}

//--------------------------------------------------------

class Auto: public Kulkuneuvo {

public:

Auto(double nopeus, string vari, string reknum);

string hae_rekisterinumero() const;

void raportoi_tunnusmerkit() const;

private:

string rekisterinumero_;

};

Page 67: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 63Auto::Auto(double nopeus, string vari, string reknum):

Kulkuneuvo(nopeus, vari), rekisterinumero_(reknum) {

}

string Auto::hae_rekisterinumero() const {

return rekisterinumero_;

}

void Auto::raportoi_tunnusmerkit() const {

cout << "Poliisi etsii autoa: vari " << hae_vari()

<< ", rekisterinumero " << hae_rekisterinumero()

<< endl;

}

//--------------------------------------------------------

int main() {

Kulkuneuvo kneuvo(20.0, "punainen"); // 20 km/h

kneuvo.raportoi_matka(1.0); // 1.0 sekunnissa

kneuvo.aseta_nopeus(50.0); // 50.0 km/h

kneuvo.raportoi_matka(2.5); // 2.5 sekunnissa

Auto subaru(75.0, "siniharmaa", "ABC-123");

subaru.raportoi_matka(1.0);

subaru.aseta_nopeus(120.0);

subaru.raportoi_matka(2.5);

subaru.raportoi_tunnusmerkit();

}

◆ Esimerkissä Kulkuneuvo-luokasta on periytetty Auto-luokka.

Tämä tarkoittaa loogisesti sitä, että kaikki autot ovatkulkuneuvoja, mutta kaikki kulkuneuvot eivät ole autoja. Autoillaon siis samat ominaisuudet kuin kulkuneuvoilla, mutta sen lisäksijotain lisäominaisuuksia, jotka erottavat ne kulkuneuvoista.

Page 68: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 64◆ Tai jos saman haluaa ilmaista hiukan toisin: autoille voi tehdä

samoja asioita kuin kulkuneuvoille, mutta sen lisäksi niille voitehdä jotain vain autoille tyypillisiä asioita.

◆ Tällainen nk. is-a -periytyminen saadaan C++:ssa aikaanperiyttämällä aliluokka kantaluokasta avainsanan public avulla:kaikista kantaluokan public-rajapinnassa olevista metodeistatulee automaattisesti aliluokan public-metodeja.

Konkreettisesti tämä tarkoittaa sitä, että aliluokan olioihin voidaankohdistaa kaikki ne metodit, jotka on määritelty kantaluokanjulkisessa rajapinnassa.

◆ Aliluokan omista metodeissa ei kuitenkaan päästä suoraankäsittelemään kantaluokan private-osassa oleviajäsenmuuttujia, vaan kaikki niihin kohdistuvat operaatiot onsuoritettava kantaluokan metodeja kutsumalla.

◆ Kannattaa myös panna merkille syntaksi, jonka avulla aliluokanrakentajan alustuslistassa saadaan alustettua kantaluokalta peritytjäsenmuuttujat:

Auto::Auto(double nopeus, string vari, string reknum):

Kulkuneuvo{nopeus, vari}, rekisterinumero_{reknum} {

}

◆ public-periyttäminen esitetyssä perusmuodossaan onkäyttökelpoinen, jos on olemassa valmis luokka, jonka melkeinvastaa tarvetta, mutta sen julkisesta rajapinnasta puuttuumuutamia operaatioita.

Page 69: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 65◆ Ei esimerkiksi ole kaukana ajatus, että C++:n valmiista

string-luokasta haluttaisiin versio, jonka julkisessa rajapinnassaon operaatio onko_palindromi:

class OmaString: public string {

public:

OmaString();

OmaString(const string& alkuarvo);

bool onko_palindromi() const;

private:

// Ei välttämättä mitään täällä

};

Valitettavasti monimutkaisemmista kantaluokista periyttäminen eiole aivan noin suoraviivaista, mutta ideaa se havainnollistaa hyvin.

◆ Koska kaikki aliluokan oliot kuuluvat myös kantaluokkaan, niitävoi periaatteessa käyttää kaikkialla, missä voisi käyttääkantaluokan oliota.

Käytännössä C++-ei kuitenkaan ole noin joustava: aliluokan oliotavoi kuitenkin käyttää parametrina funktiolle, jonka muodollisenparametrin tyyppi on viite tai osoite1 kantaluokan olioon.

void prosessoi_kulkuneuvo(Kulkuneuvo& kn) {

kn.raportoi_matka(15.0);

}

int main() {

Kulkuneuvo kneuvo(20.0, "punainen");

Auto subaru(75.0, "siniharmaa", "ABC-123");

prosessoi_kulkuneuvo(kneuvo);

prosessoi_kulkuneuvo(subaru);

}

Pohjimmiltaan tämäkin on mekanismi, jolla staattisesti tyypi-tetyssä kielessä yritetään saavuttaa dynaamisen tyypityksen iloja.

1 Osoitteista lisää myöhemmin.

Page 70: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 66Abstraktista kantaluokasta periyttäminen

◆ Variaatio edellä esitetystä is-a -periyttämisestä on tilanne, jossakantaluokan on tarkoitus määrätä, minkälainen julkinen rajapintasiitä periytetyillä aliluokilla on vähintään oltava, vaikka se eivälttämättä itse tarjoa kaikille rajapinnan metodeille toteutusta.

Tällaista kantaluokkaa kutsutaan abstraktiksi kantaluokaksi.

Tämäkin ajatus selkiytyy parhaiten esimerkin avulla: oletetaan ettäpitäisi toteuttaa ohjelma, jossa käsitellään geometrisia kuvioita.

Kaikilla kuvioilla pitää olla samanlainen julkinen rajapinta, jottaniille voidaan tehdä käsitteellisesti tietyt analogiset operaatiot(ympärysmitan ja pinta-alan laskeminen), vaikka operaationtoteutus riippuukin täysin kuvion tyypistä (neliö, ympyrä jne.).

#include <iostream>

#include <string>

using namespace std;

//-----------------------------------------------------

class Kuvio {

public:

Kuvio(const string& tyyppi);

string hae_tyyppinimi() const;

// Puhtaat virtuaalifunktiot: luokka ei tarjoa

// määrittelyä ollenkaan, vaan aliluokan on

// pakko määritellä näistä oma versionsa.

virtual double ymparysmitta() const = 0;

virtual double pinta_ala() const = 0;

private:

string tyyppinimi_;

};

Page 71: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 67Kuvio::Kuvio(const string& tyyppi): tyyppinimi_(tyyppi) {

}

string Kuvio::hae_tyyppinimi() const {

return tyyppinimi_;

}

//-----------------------------------------------------

class Nelio: public Kuvio {

public:

Nelio(double sivun_pituus);

double ymparysmitta() const;

double pinta_ala() const;

private:

double sivun_pituus_;

};

Nelio::Nelio(double sivun_pituus):

Kuvio("nelio"), sivun_pituus_(sivun_pituus) {

}

double Nelio::ymparysmitta() const {

return 4 * sivun_pituus_;

}

double Nelio::pinta_ala() const {

return sivun_pituus_ * sivun_pituus_;

}

Page 72: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 68//-----------------------------------------------------

class Suorakaide: public Kuvio {

public:

Suorakaide(double sivu1, double sivu2);

double ymparysmitta() const;

double pinta_ala() const;

private:

double sivu1_;

double sivu2_;

};

Suorakaide::Suorakaide(double sivu1, double sivu2):

Kuvio("suorakaide"), sivu1_(sivu1), sivu2_(sivu2) {

}

double Suorakaide::ymparysmitta() const {

return 2 * sivu1_ + 2 * sivu2_;

}

double Suorakaide::pinta_ala() const {

return sivu1_ * sivu2_;

}

//-----------------------------------------------------

void raportoi_kuvainfo(Kuvio& kuvio) {

cout << kuvio.hae_tyyppinimi() << " "

<< kuvio.ymparysmitta() << " "

<< kuvio.pinta_ala() << endl;

}

int main() {

Nelio nelio(2.0);

Suorakaide suorakaide(2.0, 4.5);

raportoi_kuvainfo(nelio);

raportoi_kuvainfo(suorakaide);

}

Page 73: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 69◆ C++:ssa luokka, joka sisältää yhdenkin puhtaan

virtuaalifunktion (pure virtual function) on abstraktikantaluokka.

Puhtaiden virtuaalifunktioden esittely public-osassa on muotoa:

virtual normaali metodin esittely = 0;

Abstrakti kantaluokka ei tarjoa mitään valmista toteutustapuhtaille virtuaalifunktioille, vaan jokaisen aliluokan on pakkomääritellä niistä oma versionsa.

Ohjelmassa ei voi määritellä abstraktien kantaluokkien tyyppisiäolioita (miksi ei?), koska se on suunniteltu täysin siihentarkoitukseen, että niistä periytetään aliluokkia.

Viitteitä ja osoittimia abstraktin luokan olioihin kuitenkin voimääritellä, kuten esimerkin raportoi_kuvainfo-funktionparametrissa oli tehty.

◆ Abstrakti kantaluokka on oikea työkalu silloin, kun halutaanvarmistaa aliluokilla olevan (ainakin osittain) samat metoditjulkisessa rajapinnassaan, mutta näiden metodien toteutus ontäysin riippuvainen aliluokan mallintamasta käsitteestä.

Page 74: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 70Yleisiä huomioita periyttämisestä

◆ Periyttäminen on uusi ja jännittävä mekanismi, joten sen käyttöhoukuttaa pelkästään uutuudenviehätyksen vuoksi. Tämä johtaahelposti hölmöihin ratkaisuihin.

◆ Käytä periyttämistä säästeliäästi ja vain tilanteissa, joissa voitperustella itsellesi sen olevan oikeasti hyvä ratkaisu.

Hyvä nyrkkisääntö on seuraava:

Periytä kantaluokasta X aliluokka Y, vain jos pystyt

perustelemaan itsellesi, että jokainen Y on myös X.

Siis auto on perusteltua periyttää kulkuneuvosta, koska jokainenauto on myös kulkuneuvo.

◆ Autoa ei kuitenkaan ole järkevää periyttää polttomoottorista,koska auto ei ole polttomoottori.

Tässä on kyseessä nk. has-a -suhde: autossa on polttomoottori.

Toteutusmekanismin pitäisi siis olla se, että auto-luokanprivate-osassa on polttomoottorityyppinen jäsenmuuttuja.

◆ Edellinen esimerkki oli niin ilmiselvä, että melkein naurattaa.Tosiasia on kuitenkin se, että aloitteleva luokkin suunnittelijalankeaa vastaavaan ansaan vähänkin juonikkaammassatilanteessa, vaikka silloinkin kyseessä saattaa tarkemminanalysoituna olla tismalleen vastaava is-a vs. has-a -tilanne.

◆ Tässä monisteessa esitetyt esimerkit periyttämisestä ovat vainpinnallinen kosketus kaikkiin mahdollisiinperiyttämismekanismeihin, joita C++ tarjoaa.

Jokaisella pitäisi nyt kuitenkin olla perusidea siitä, mitäperiyttämisellä tarkoitetaan.

Page 75: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 71Versionhallinta

◆ Seuraavassa tutustutaan versionhallintajärjestelmien yleisiinperusperiaatteisiin. Kurssilla käytetty versionhallintatyöjärjestelmätulee tutuksi harjoituksissa ja harjoitustöissä, joiden yhteydessäsen käyttöä on ohjeistettu tarkemmin.

◆ Versionhallintaohjelmisto (version/revision control system)on työkalu, jonka avulla projektin lähdekooditiedostoista voidaanhyvin pienellä vaivalla ylläpitää versiohistoriaa.

Versiohistoria tarkoittaa sitä, että mihin tahansa versioon (joka ontalletettu järjestelmään) voidaan tarvittaessa palata tai sitävoidaan tutkia myöhemmin.

◆ Nykyaikaiset versionhallintajärjestelmät tuovat projektinhallintaan monia muitakin hyötyjä versiohistorian lisäksi:

● Useat ohjelmoijat voivat työstää samoja tiedostoja ilmansekaannusten vaaraa.

● Kun muutoksia talletetaan versionhallintajärjestelmään,useimmat järjestelmät pakottavat käyttäjän kirjoittamaanlyhyen kuvauksen tehdyistä muutoksista. Projektinetenemisestä syntyy lähes automaattisesti "päiväkirja".

● Virheelliset muutokset voidaan perua, jos versiohallintaan onjoskus aiemmin talletettu virheetön versio tiedostosta.

● Versionhallintajärjestelmää voi käyttää myös eräänlaisenavarmuuskopiointimekanismina. Aina kun projekti tai sen osa onhyväksi katsotussa tilassa, projekti tallennetaanversionhallintaan.

● Versionhallinnan alaisuudessa projektista on helpompi ylläpitääuseita eri versioita (maksullinen/ilmainen, vanha/uusi,Windows/Linux/Mac jne.).

Page 76: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 72◆ Toiminnallisesti versionhallintajärjestelmässä on tyypillisesti kaksi

osaa: tietovarasto (tietoarkisto, repository) ja yksi tai useampiatyökopioita (working copy).

Tietovarasto

TyökopioTyökopio

työkopionpäivitys

muutostentalletus

työkopionpäivitys

muutostentalletus

◆ Tietovarasto on tietokanta, jonne versionhallintajärjestelmätallentaa eri versiot lähdekoodeista.

Tietovarasto voi modernissa versionhallintajärjestelmässä sijaitajoko paikallisella koneella tai tietoverkossa.

◆ Työkopio on ohjelmoijan tietovarastosta hakema kopio projektintilasta jollain hetkellä (yleensä viimeisin versio), jota sittenvoidaan muokata.

Kun työkopioon on tehty tarvittavat muutokset, ne voidaantallentaa (commit) tietovarastoon uudeksi versioksi.

◆ Versionhallintajärjestelmien yksi tärkeä etu on se, että useampiohjelmoija voi työstää samaa projektia yhtäaikaisesti. Koska tästäseuraa se, että joku saattaa tallentaa tietovarastoon uusia versioitasillä aikaa, kun joku toinen muokkaa omaa työkopiotaan,aika-ajoin on syytä päivittää tietovarastoon tulleet muutoksetomaan työkopioonsa (update).

Page 77: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 73◆ Yleensä järjestelmä päivittää tietovarastoon tehdyt muutokset

työkopioon automaattisesti, eikä käyttäjän tarvitse murehtia siitä.

On kuitenkin mahdollista, että jonkun muun tekemät muutoksettietovarastossa ovat kohdistuneet samoille riveillelähdekooditiedostossa, joita työkopiossa on muutettu. Tällöintuloksena on konflikti (conflict, merge conflict), koskaversionhallintaohjelmisto ei itse kykene arvaamaan, mitkäristiriitaisista muutoksista pitäisi ottaa huomioon.

Jos näin tapahtuu, versionhallinta merkitsee ongelmakohdatlähdekooditiedostoihin työkopiossa, ja työkopion omistajan onjollain tavoin muokattava nämä kohdat tarkoituksenmukaisiksi,ennen kuin hän voi tallentaa työkopionsa uudeksi versioksitietovarastoon.

◆ Hyvin yleisellä tasolla kaikkien versionhallintajärjestelmän käyttömuodostuu seuraavista vaiheista:

1. Projektin alussa tietovarasto luodaan.

2. Kun halutaan lähteä tekemään uutta versiota, ohjelmoija ottaaitselleen työkopion tietovarastosta (checkout, clone tai jotainvastaavaa käytetystä järjestelmästä riippuen).

3. Työkopiota muokataan, käännetään, testataan jne. normaalisti.Tarvittaessa muiden tekemiä muutoksia tietovarastoon voidaanpäivittää työkopioon (update, pull tai jokin muu vastaava).

4. Kun työkopio hyvässä tilassa (käännettävissä ja testattavissa, eivälttämättä kuitenkaan valmis), tehdään vielä yksi päivitys.

5. Talletetaan työkopio uudeksi versioksi tietovarastoon (commit,commit + push tai vastaava, mikä taaskin riippuu käytetystäversionhallintajärjestlemästä).

6. Työkopion voi nyt halutessaan tuhota, tai sen kehittämistävoidaan jatkaa (paluu kohtaan 3).

Page 78: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 74◆ Yleisimmin käytössä olevat versionhallintajärjestelmät voidaan

jakaa kahteen kategoriaan:

● Keskitetyissä (centralized) versionhallintajärjestelmissä onvain yksi tietovarasto, jonne koko versiohistoria on talletettuna.

Vahva puoli on selkeys: on aina selvää, mistä uusin versio onhaettavissa.

Heikkous: jos tietokone, jolla tietovarasto on talletettuna hajoaaja varmuuskopioinnista ei ole huolehdittu, paluuonnettomuutta edeltäneeseen tilaan saattaa olla työlästä.

Tunnetuin keskitetty versionhallintajärjestelmä lieneeSubversion.

● Hajauteutuissa (distributed) versionhallintajärjestelmissä onuseita tietovarastoja, joista mikään ei välttämättä oleensisijainen.

Vahva puoli on luotettavuus: versionhallinnan toiminta ei oleriippuvainen yhdestä tietovarastosta, jonka tuhoutuminensaattaisi johtaa kriisiin.

Heikkous: eri tietovarastot eivät välttämättä ole reaaliaikaisestisynkronoituja, joten on hankala olla varma siitä, onko omaantyökopioon aina päivitetty kaikki viimeisimmät muutokset, taiedes siitä, mistä kaikista tietovarastoista päivityksiä pitäisi hakea.

Tunnetuin hajautettu versionhallintajärjestelmä on git.

Git on itse asiassa varsin joustava järjestelmä: sitä voihalutessaan käyttää myös siten, että projektilla on vain yksitietovarasto (siis keskitettynä versionhallintana).

◆ Luentomonisteessa ei ole selitetty kurssilla käytetynversionhallintajärjestelmän käyttöä, koska sisältö on haluttu pitäätyöympäristöstä riippumattomana.

Käytetyn työympäristön hyödyntämistä opitaan harjoituksissa jaharjoitustöiden teon yhteydessä.

Page 79: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 75Debuggaus

◆ Debuggaus on termi, jota käytetään ohjelmistotyössätarkoittamaan ohjelmassa olevien virheiden paikantamista jakorjaamista.

◆ Usein, joskaan ei aina, virhekohdan löytäminen ohjelmasta ondebuggauksen haastavin vaihe.

◆ Jotta virhekohtaa voisi yleensäkään ottaen yrittää löytää, täytyyolla tietoinen virheen olemassaolosta.

Tämä voidaan saavuttaa vain testaamalla ohjelmaa erilaisillasyötteillä ja vertailemalla saatuja tuloksia oikeiksi tiedettyihintuloksiin.

◆ Testaamisen perusideoita käsiteltiin lyhyesti Johdatusohjelmointiin -kurssilla.

Tässä on tarkoitus esitellä yleisellä tasolla muutamakäyttökelpoinen mekanismi, joiden avulla virheen syytä voidaanetsiä sen jälkeen, kun ohjelman on havaittu toimivan virheellisesti.

◆ Ainoa mekanismi, jota tähän mennessä on hyödynnetty, ontestitulosteet.

Testitulosteilla tarkoitetaan ohjelmakoodiin oletetun virhekohdanympärille puhtaasti testaustarkoituksessa lisättyjä tulostuskäskyjä(print, cout), joiden avulla ohjelman etenemistä jakiinnostavien muuttujien arvoja voidaan seurata.

Ideana on tietysti se, että järkeilemällä tulostuneista muuttujienarvoista ja testitulosteiden tulostumisjärjestyksestä voidaanpäätellä, missä kohdassa toiminta menee pieleen.

Koska testitulosteet ovat entuudestaan tuttu virhekohdanetsintämekanismi, sitä ei ole tarpeen pohtia tätä pidemmälle.

Page 80: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 76◆ Toinen monissa tilanteissa huomattavasti käyttäjäystävällisempi

virheenjäljitysmekanismi on nk. debuggerin käyttö.

Debuggeri on erillinen ohjelma, jonka alaisuudessa testattavaaohjelmaa voidaan suorittaa.

◆ Debuggerin käyttö ja toiminnallisuus riippuvat käytetystäohjelmointikielestä ja kääntäjästä tai tulkista, mutta tyypillisestiainakin seuraavat operaatiot on jollain tavoin tuettu:

● keskeytyskohtien (breakpoint) asettaminen (kriteereinä rivillesaapuminen, määrätyn funktion kutsu tai muuttujan käsittely),

● ohjelman suorittaminen käsky tai funktio kerrallaan(askeltaminen, stepping),

● muuttujien tilan (arvon) seuraaminen ja

● funktiokutsuhierarkkian tutkiminen (stack trace).

◆ Yleensä virhekohdan etsiminen etenee jotenkin seuraavasti:

1. Selvitetään summittainen vikapaikka (siis muodostetaanvalistunut arvaus paikasta, jossa vika voisi sijaita). Tämä voitapahtua joko testaamalla tai, jos ohjelma kaatuu suorituksenaikana, etsimiseen voi käyttää debuggeria.

2. Asetetaan keskeytyskohta ennen oletettua virhekohtaa jakäynnistetään ohjelma debuggerin alaisuudessa.

3. Suoritetaan ohjelmaa keskeytyskohdasta eteenpäin käskykerrallaan ja seurataan muuttujien arvojen kehittymistä,funktioiden paluuarvoja jne., kunnes niistä toivottavastivoidaan päätellä virheen syy.

4. Jos virhe ei löydy ensimmäisellä yrittämällä, on yleensä hyväajatus pyrkiä haarukoimaan virhekohtaa siirtämälläkeskeytyskohtaa eteen- tai taaksepäin.

◆ Kurssillä käytettyyn debuggeriin tutustutaan harjoituksissa.

Page 81: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 77Standard Template Library: perusideoita

◆ C++:ssa monimutkaisemmat tietorakenteet ja niiden käsittelyyntarkoitetut mekanismit eivät ole osa kieltä itseään, vaan ne ontoteutettu kirjastoissa.

Näiden kirjastojen muodostamaa kokonaisuutta kutsutaanyleensä Standard Template Libraryksi (STL).

◆ STL muodostuu kolmesta loogisesta osakokonaisuudesta:

● Säiliöt ovat tietorakenteita, jotka vastaavat Python-kielen list,set ja dict-rakenteita, ja monia muita, joita vastaaviaPythonissa ei ole valmiina.

● Iteraattorit, joita voi tässä vaiheessa ajatella eräänlaisinakirjanmerkkeinä, joihin voi tallettaa yksittäisten tietoalkioidensijainnin säiliön sisällä.

● Algoritmit, jotka tarjoavat valmiita mekanismejaperusoperaatioiden suorittamiseksi säiliöille (esim. sortinavulla säiliön alkiot voi laittaa haluamaansa järjestykseen,pienimmästä suurimpaan tms.).

◆ STL on erittäin laaja kokoelma kirjastoja ja tässä monisteessasiihen ei tutustuta kuin yleissivistävästi.

Pääosin keskitytään säiliötyyppeihin ja algoritmeihin, jotka ovatainakin osin tuttuja Pythonista.

Käsitteellisesti uutena ajatuksena tutustutaan myösiteraattoreihin.

Page 82: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 78STL vector

◆ vector-tietotyyppi muistuttaa hyvin läheisesti Pythoninlist-tyyppiä: se voi sisältää useita tietoalkioita, joihin päästäänkäsiksi, jos tiedetään niiden indeksi.

Pythonista poiketen, C++:n vectorin kaikkien alkioiden on oltavakeskenään samantyyppisiä.

◆ Tutkitaan pieni esimerkkiohjelma, joka osaa optimoida(silmukattoman) labyrintin läpi löydetyn reitin.

Jos esimerkiksi labyrintissä satunnaisesti harhailemalla ononnistuttu löytämään reitti on PLIPIPLIILEILLEI, olisi siitäoptimoitu reitti PI.

#include <iostream>

#include <vector>

#include <string>

using namespace std;

const char POHJ = ’P’;

const char ITA = ’I’;

const char ETELA = ’E’;

const char LANSI = ’L’;

void TulostaReitti(const vector<char>& reittivektori) {

vector<char>::size_type indeksi = 0;

while ( indeksi < reittivektori.size() ) {

cout << reittivektori.at(indeksi);

++indeksi;

}

cout << endl;

}

Page 83: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 79int main() {

string reitti = "";

cout << "Syota kuljettu reitti: ";

getline(cin, reitti);

vector<char> optimoitu_reitti = { };

string::size_type i = 0;

while ( i < reitti.length() ) {

char uusi_suunta = reitti.at(i);

if ( optimoitu_reitti.size() == 0 ) {

optimoitu_reitti.push_back(uusi_suunta);

} else {

char edellinen_suunta = optimoitu_reitti.back();

if ( edellinen_suunta == ITA

and uusi_suunta == LANSI ) {

optimoitu_reitti.pop_back();

} else if ( edellinen_suunta == LANSI

and uusi_suunta == ITA ) {

optimoitu_reitti.pop_back();

} else if ( edellinen_suunta == POHJ

and uusi_suunta == ETELA ) {

optimoitu_reitti.pop_back();

} else if ( edellinen_suunta == ETELA

and uusi_suunta == POHJ ) {

optimoitu_reitti.pop_back();

} else {

optimoitu_reitti.push_back(uusi_suunta);

}

}

++i;

}

TulostaReitti(optimoitu_reitti);

}

Page 84: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 80◆ Vektorityypin käyttämiseksi koodin alkuun tarvitaan:

#include <vector>

◆ Vektorityyppisen muuttujan määrittely tapahtuu:

vector<alkioiden_tyyppi > muuttujan_nimi ;

esimerkiksi:

vector<int> pistemaarat;

jonka tuloksena syntyneeseen muuttujaan pistemaarat

voidaan tallentaa useita kokonaislukuja.

◆ Vektorin loppuun voidaan lisätä uusia alkioita yksi kerrallaan:

pistemaarat.push_back(21);

jolloin vektorin koko (siihen tallennettujen alkioiden lukumäärä)kasvaa yhdellä.

◆ Viimeinen alkio saadaan poistettua:

pistemaarat.pop_back();

jolloin vektorin koko pienenee yhdellä.

◆ Jos vektori on tyhjä (siinä ei ole yhtään alkiota), kun pop_back

suoritetaan, aiheutuu poikkeus, johon ohjelman suoritus päättyy.

Oikeaoppisesti pop_back-metodin kutsua ennen olisi siis syytävarmistua, että vektorissa on poistettavaa jäljellä:

if ( pistemaarat.size() != 0 ) {

pistemaarat.pop_back();

} else {

// Virhe, tyhjästä vektorista ei voi poistaa.

}

jossa size kertoo vektoriin talletettujen alkioiden lukumäärän.

Page 85: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 81◆ Jos size-metodin paluuarvo halutaan tallentaa muuttujaan, sen

tyypin tulisi olla:

vector<alkioiden_tyyppi >::size_type

Esimerkiksi:vector<int>::size_type koko = pistemaarat.size();

· · ·

koko = pistemaarat.size();

◆ Vektorin ensimmäisen ja viimeisen alkion arvoon päästään käsiksi:

cout << "eka: " << pistemaarat.front() << endl

<< "vika: " << pistemaarat.back() << endl;

◆ Vektorin yksittäisiin arvoihin päästään käsiksi samoin kuinmerkkijonon yksittäisiin merkkeihin:

cout << pistemaarat.at(3) << endl;

· · ·

pistemaarat.at(indeksi) = uusi_pistemaara;

Indeksointi alkaa nollasta ja viimeisen alkion indeksi on yhtäpienempi kuin alkioiden lukumäärä.

Jos at-metodilla yritetään indeksoida vektorista alkio, jota ei oleolemassa, seurauksena poikkeus ja ohjelman kaatuminen.

◆ Vektori voi funktion parametrina käytettynä olla joko arvo- taiviiteparametri normaaliin tapaan:

void funktio1(vector<double> mittaustulokset);

void funktio2(vector<double>& mittaustulokset);

◆ Kuten aiemmin oli puhetta (s. 59), isokokoisia tietorakenteita eiyleensä kannata välittää funktiolle arvoparametrina, jos sevoidaan välttää. Tehokkuussyistä kannattaa arvoparametriensijaan käyttää const-viitteitä:

void funktio3(const vector<double>& mittaustulokset);

Page 86: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 82◆ Jos halutaan, vektorin alkioiden määrä voidaan asettaa jo sen

määrittelyvaiheessa:

// Vektori, jossa valmiiksi tilaa 20 kokonaisluvulle.

// Luvut itsessään alustamattomia.

vector<int> luvutA(20);

// Tilaa 10 reaaliluvulle, jokainen niistä

// alustettu arvoon 5.3.

vector<double> luvutB(10, 5.3);

◆ Huomaa, että vektoreille (ja muillekin STL-rakenteille) on useitaeri alustusnotaatioita. Esimerkiksi, mutta ei tyhjentävästi:

vector<string> nimetA(5);

vector<string> nimetB(10, "tuntematon);

vector<string> nimetC = { "Matti", "Maija" };

◆ Yleisesti ottaen STL-vektori on tietorakenne, joka säilyttää alkiotsiinä järjestyksessä, kun ne on siihen talletettu.

STL-terminologiassa tällaisia rakenteita kutsutaan sarjoiksi

(sequence).

Vektori sopii hyvin käytettäväksi tilanteissa, joissa on kyettävätallentamaan paljon tietoalkioita myöhempää prosessointiavarten. Varsinkin jos talletettuun tietoon ei tehdä hakuja, taihakujen nopeudella ei ole erityisen suurta merkitystä.

Luonnollisesti vektori on hyvä valinta myös tilanteessa, jossaalkiot on pystyttävä prosessoimaan samassa järjestyksessä kuin nelisättiin säiliöön.

◆ Joskus aiemmin mainittu deque on hyvin samankaltainenrakenne kuin vector, mutta tehottomampi.

Tosin dequen etu on, että sen alkuun voi lisätä alkioita valmiillametodilla push_front ja sen alusta voi poistaa alkioitapop_front-metodilla.

Page 87: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 83STL-iteraattorit

◆ Iteraattorit ovat tietotyyppejä (tai muuttujia, kontekstistariippuen), joiden avulla voidaan tutkia ja muokata säiliööntalletettuja alkioita.

◆ Iteraattoria voi ajatella kirjanmerkkinä, joka muistaa yhdensäiliössä olevan alkion sijainnin.

◆ Iteraattoreiden ideana on tarjota yhdenmukainen rajapinta, jonkaavulla pystytään samoilla operaatioilla käsittemään säiliönalkioita, riippumatta säiliön täsmällisestä tyypistä.

Esimerkiksi usein vastaan tuleva ongelma on säiliön kaikkienalkioiden läpikäynti. Vektorin tapauksessa tämä ei ole vaikeaa,koska sen alkiot voidaan indeksoida läpi silmukassa. Tämä eikuitenkaan pidä yleisesti paikkaansa, koska on olemassa säiliöitä,joiden alkioilla ei ole järjestysnumeroa.

◆ Jos yhden iteraattorin avulla voidaan pitää kirjaa yhden alkionsijainnista, kahdella iteraattorilla voidaan esittää väli säiliönalkioita: kaikki kahden alkion välissä sijaitsevat alkiot.

Tätä ominaisuutta hyödynnetään myöhemmin STL-algoritmienkanssa.

◆ Kaikki STL:n säiliötyypit tarjoavat ohjelmoijalle joukoniteraattorityyppejä, joilla säiliöitä voi käsitellä. Hyödyllisimmätnäistä ovat:

säiliötyyppi <alkiotyyppi >::iterator

säiliötyyppi <alkiotyyppi >::const_iterator

const_iterator-tyypin avulla säiliön alkioita voidaan tutkia,mutta ei muuttaa (sopii käytettäväksi const-säiliöiden kanssa).

Page 88: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 84◆ Toteutetaan hyvin yksinkertainen ohjelma, joka käy vektorin alkiot

läpi iteraattorin avulla ja tulostaa niiden arvot näytölle:

#include <iostream>

#include <vector>

using namespace std;

int main() {

vector<double> luvut = {1.0, 1.5, 2.0, 2.75};

vector<double>::iterator veciter;

veciter = luvut.begin();

while ( veciter != luvut.end() ) {

cout << *veciter << " ";

++veciter;

}

cout << endl;

}

◆ Metodin begin paluuarvo osoittaa säiliön ensimmäisen alkionsijainnin.

◆ Metodi end palauttaa iteraattorin, joka osoittaa säiliön loppuun:end ei osoita mihinkään varsinaiseen säiliössä olevaan arvoon,vaan kyseessä on eräänlainen loppumerkki.

Usein end-iteraattoria käytetään myös virhettä taiepäonnistumista kuvaavana paluuarvona iteraattorinpalauttavasta funktiosta.

◆ Seuraava kuva havainnollistaa begin- ja end-iteraattoreita:

luvut

luvut.begin() luvut.end()

1.0 1.5 2.0 2.75

Page 89: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 85◆ Iteraattorin osoittamaa säiliön alkiota voidaan käsitellä

kohdistamalla siihen unaarinen *-operaattori. Esimerkiksi:

cout << *veciter << endl;

*veciter = 0.0;

*veciter += 2.5;

funktio(*veciter);

◆ Jos halutaan kohdistaa metodeja olioon, johon iteraattoriosoittaa, käytetään -> -operaattoria (katso esimerkki s. 94).

◆ Iteraattori saadaan osoittamaan seuraavaan vuorossa olevaanalkioon ++ -operaattorilla ja edelliseen alkioon –– -operaattorilla.

◆ Operaattoreilla == ja != voidaan testata, osoittavatko kaksiiteraattoria samaan vai eri alkioon.

◆ Jos ainoa tarve on käydä säiliön (toimii muillakin säiliöillä kuinvektorilla) kaikki alkiot läpi, voi käyttää for-rakennetta:

int main() {

vector<double> luvut = {1.0, 1.5, 2.0, 2.75};

for ( auto alkio: luvut ) {

cout << alkio << " ";

}

cout << endl;

}

joka on toiminnaltaan analoginen verrattuna Pythoninrakenteeseen:

luvut = [ 1.0 1.5 2.0 2.75 ]

for alkio in luvut:

print(alkio, end=" ")

print()

Kulissien takana C++:n säiliön alkiot läpi käyvä versiofor-rakenteesta käyttää iteraattoreita, mutta se ei näyohjelmoijalle.

Page 90: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 86◆ Edellä esitellyn for-rakenteen iterointimuuttuja (esimerkissä

alkio) on oletusarvoisesti kopio käsiteltävän säiliön vuorossaolevasta alkiosta (kopiosemantiikka).

Jos kuitenkin halutaan, että for-rakenteen rungossa voidaanmuuttaa säiliön alkioita, määritellään iterointimuuttuja viitteeksi:

int main() {

vector<double> luvut = {1.0, 1.5, 2.0, 2.75};

for ( auto& alkio: luvut ) {

alkio *= 2;

}

// Tässä kaikki luvut-vektorin alkiot ovat

// kaksinkertaistuneet: 2.0, 3.0, 4.0 5.5

}

◆ Tärkeä yksityiskohta, joka on syytä pitää mielessä: jos säiliöönlisätään tai siitä poistetaan alkioita, iteraattorien arvot, jotka onasetettu osoittamaan kyseisen säiliön alkioihin ennen muutosta,eivät ole enää käyttökelpoisia.

◆ Esimerkeissä käytetty sana auto on C++:n varattu sana, jotavoidaan käyttää määriteltävänä olevan muuttujan tyyppinätilanteissa, joissa kääntäjä osaa päätellä tyypin alustusarvosta:

auto lukumaara = 0; // int

auto lampotila = 12.1; // double

auto iter = luvut.begin(); // vector<double>::iterator

Monisteen esimerkeissä autoa ei kuitenkaan käytetä läheskäänkaikissa tilanteissa, joissa se olisi mahdollista, koska on hyvä oppiaymmärtämään staattisesti tyypitetyn kielentyyppimäärittelymekanismia hiukan itsekin.

Page 91: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 87STL-algoritmit

◆ STL-kirjasto algorithm tarjoaa valmiina yleisimmät operaatiot,joita säiliön sisältämille alkioille on tarpeen tehdä.

◆ Algoritmikirjaston käyttämiseksi on koodiin lisättävä:

#include <algorithm>

◆ STL:n valmiit algoritmit ovat geneerisiä: ne eivät ota kantaasäiliön tyyppiin, vaan ne osaavat operoida millä tahansa säiliöllä,kunhan säiliön alkioihin voidaan viitata iteraattorien avulla.

◆ Käsiteltäväksi haluttu osa säiliön alkoista kerrotaanalgoritmifunktiolle iteraattorivälin avulla: jokaisella funktiolla onaina vähintään kaksi iteraattoriparametria.

◆ Esimerkiksi algoritmi (siis funktio) sort osaa lajitellavector-rakenteen alkiot kasvavaan järjestykseen, kunhan sillekerrotaan iteraattoreilla lajiteltavaksi haluttu osaväli:

vector<double> reaalivektori;

· · ·

sort(reaalivektori.begin(), reaalivektori.end());

◆ Kannattaa pitää mielessä, että begin- ja end-iteraattorien paikallamikä tahansa kahden iteraattorin esittämä osaväli on kelvollinen:

vector<double>::iterator valin_eka = reaalivektori.begin();

vector<double>::iterator valin_vika = reaalivektori.end();

++valin_eka; // Osoittaa nyt toiseen alkioon

--valin_vika; // Osoittaa nyt viimeiseen alkioon

// Lajitellaan vektorin sisällä kaikki alkiot

// ensimmäistä ja viimeistä lukuunottamatta.

// Ensimmäinen ja viimeinen pysyvät paikallaan.

sort(valin_eka, valin_vika);

Page 92: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 88◆ Tärkeä huomio: kun säiliöstä esitetään jokin väli alkioita

kahden iteraattorin avulla, kyseinen väli on tulkittava ylhäältä

avoimeksi väliksi: algoritmikirjaston operaatiot eivät koskejälkimmäisen iteraattoriparametrin osoittamaan alkioon.

Edellisessä esimerkissä siis viimeinen vektorin alkio ei enää tullutlajitelluksi, koska valin_vika osoitti siihen.

◆ Seuraavassa lyhyt lista hyödyllisistä algoritmikirjaston funktioista.Suurimmassa osassa on käytetty esimerkkirakenteena vectoria,koska se on tässä vaiheessa ainoa tuttu STL-säliötyyppi. Kuitenkin,mikäli ei eriksen mainita, funktiot toimivat muillakin säiliötyypeilläja merkkijonoilla (string).

◆ count laskee säiliössä olevien tietyn arvoisten alkioidenlukumäärän:

vector<string> vihamiehet;

· · ·

// Kuinka monta Akia vihamiehet-vektorista löytyy?

cout << count(vihamiehet.begin(), vihamiehet.end(), "Aki")

<< " kappaletta Aki-nimisia vihamiehia!" << endl;

count-funktio ei toimi map-rakenteella aivan yhtä suoraviivaisesti.

◆ min_element ja max_element etsivät säiliöstä arvoltaanpienimmän tai suurimman alkion ja palauttavat iteraattorin, jokaosoittaa kyseiseen alkioon:

vector<int> maarat;

· · ·

vector<int>::iterator pienin_it;

pienin_it = min_element(maarat.begin(), maarat.end());

cout << "Pienin lukumaara: " << *pienin_it << endl;

min_element- ja max_element-funktiot eivät toimimap-rakenteella aivan yhtä suoraviivaisesti.

Page 93: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 89◆ find etsii säiliöstä haluttua arvoa ja palauttaa iteraattorin

ensimmäiseen löytämäänsä alkioon tai käsiteltävänä olevansäiliön end-iteraattorin, jos arvoa ei löydy:

vector<string> potilaat;

· · ·

vector<string>::iterator iter;

iter = find(potilaat.begin(), potilaat.end(), "Kai");

if ( iter == potilaat.end() ) {

// Ei ole Kaita potilasjonossa.

· · ·

} else {

// Kai löytyi ja sijaitse iter:in osoittamassa

// kohdassa: tulostetaan ja poistetaan jonosta.

cout << *iter << endl;

// vector-säiliöllä on metodi erase, jolla

// iteraattorin osoittama alkio voidaan poistaa.

potilaat.erase(iter);

}

Kannattaa pitää mielessä, että string-tietotyypillä jamyöhemmin tutuiksi tulevilla set- ja map-rakenteilla onmetodifunktio nimeltä find, jota kannattaa ehdottomastikäyttää, koska varsinkin kahden jälkimmäisen tapauksessa se onmonta kertaluokkaa nopeampi kuin algoritmi-kirjaston find.

◆ replace korvaa kaikki halutut arvot uudella arvolla:

replace(tekstivektori.begin(), tekstivektori.end(),

"TTY", "Tampereen teknillinen yliopisto");

jossa kaikki vektorissa olevat alkiot, joiden arvo on "TTY"korvataan arvolla "Tampereen teknillinen yliopisto".

replace ei toimi set-rakenteella tai map-rakenteella.

Page 94: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 90◆ fill muuttaa kaikki iteraattorivälin alkiot annetun arvoisiksi:

string merkkijono = "";

· · ·

// Seuraavan fill-operaation jälkeen koko

// merkkijono on täynnä kysymysmerkkejä.

fill(merkkijono.begin(), merkkijono.end(), ’?’);

fill ei toimi set-rakenteella tai map-rakenteella.

◆ reverse kääntää annetun välin takaperoiseen järjestykseen:

vector<Pelaaja> vuorojarjestys;

· · ·

// Seuraavalla pelikierroksella

// vuorojärjestys on käänteinen.

reverse(vuorojarjestys.begin(), vuorojarjestys.end());

reverse ei toimi set- ja map-rakenteilla ollenkaan.

◆ random_shuffle sekoittaa alkiot satunnaiseen järjestykseen:

vector<Pelikortti> korttipakka;

· · ·

random_shuffle(korttipakka.begin(), korttipakka.end());

random_shuffle ei toimi set- ja map-rakenteilla ollenkaan.

◆ copy kopioi säiliön alkiot johonkin toiseen säiliöön:

string merkkijono = "";

· · ·

// Alustetaan merkkivektori siten, että siinä

// on yhtä monta alkiota kuin merkkijonossa on

// merkkejä. Huomaa että tässä on taas tilanne,

// jossa alustuksessa pitää käyttää kaarisulkeita.

vector<char> merkkivektori(merkkijono.length());

// Kopioidaan kaikki merkkijonon merkit

// alkioiksi merkkivektoriin.

copy(merkkijono.begin(), merkkijono.end(),

merkkivektori.begin());

Page 95: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 91Kopioinnin kohteessa (siis siellä minne alkiota kopioidaan) pitääolla valmiiksi tilaa tarjolla: edellä merkkivektori on valmiiksialustettu sisältämään yhtä monta alkiota kuin merkkijono:ssaon merkkejä.

Tämä pätee kaikkiin STL-algoritmeihin, jotka tallettavat tuloksiasäiliöön: kohdesäiliössä pitää olla valmista tilaa kaikilletalletettaville alkioille.

Lähtösäilion ja kohdesäiliön alkioiden tyyppien pitää olla samat.

◆ Useista algorithm-kirjaston funktioista on olemassa versio,jonka toimintaa voidaan säätää funktioparametrin avulla. Otetaantässä esimerkkinä sort-funktio.

Ennen varsinaista asiaa, on tärkeä ymmärtää, että sort-funktio eivoi lajitella suuruusjärjestykseen kuin sellaista tietoa, jolle onmääritelty jonkinlainen suuruusrelaatio. Esimerkiksi vektori, jonkaalkiot ovat värejä: ei ole ollenkaan itsestäänselvää, onko purppurapienempi vai suurempi kuin vaikkapa violetti, koska väreillä ei olesuuruusjärjestystä.

Tämä ei ole ongelma silloin, kun lajiteltavassa säiliössä on alkioinakielen perustyyppistä tietoa, merkkijonoja tai jotain muitaC++:ssa valmiiksi määriteltyjä tyyppejä. Mutta heti, jos yritetäänkäsitellä omatekemiä tyyppejä (luokkia), joita C++ ei osaaautomaattisesti vertailla, törmätään ongelmiin.

Page 96: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 92◆ Tutkitaan pieni esimerkki (tilan säästämiseksi epäolennaisia

koodirivejä jätetty pois):

class Opiskelija {

public:

Opiskelija(string nimi, int opnum);

string hae_nimi() const;

int hae_opnum() const;

void tulosta() const;

private:

string nimi_;

int opnum_;

};

bool vertaile_opiskelijanumeroita(const Opiskelija& op1,

const Opiskelija& op2) {

if ( op1.hae_opnum() < op2.hae_opnum() ) {

return true;

} else {

return false;

}

}

bool vertaile_nimia(const Opiskelija& op1,

const Opiskelija& op2) {

if ( op1.hae_nimi() < op2.hae_nimi() ) {

return true;

} else {

return false;

}

}

Page 97: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 93int main() {

// Nimet ja opiskelijanumerot keksittyjä:

// kaikki yhteydet todellisuuteen sattumaa.

vector<Opiskelija> opiskelijat = {

{ "Teekkari, Tiina", 121121 },

{ "Arkkari, Antti", 111222 },

{ "Fyysikko, Ville", 212121 },

{ "Teekkari, Teemu", 100011 },

{ "Kone, Kimmo", 233233 },

};

sort(opiskelijat.begin(), opiskelijat.end(),

vertaile_opiskelijanumeroita);

for ( auto opisk : opiskelijat ) {

opisk.tulosta();

}

cout << string(30, ’-’) << endl;

sort(opiskelijat.begin(), opiskelijat.end(),

vertaile_nimia);

for ( auto opisk : opiskelijat ) {

opisk.tulosta();

}

}

◆ Esimerkin funktiot vertaile_opiskelijanumeroita javertaile_nimia ovat nk. vertailufunktioita:

● Parametrit (2 kpl) ovat vakioviitteitä sen tietotyypin alkioihin,joita funktion on tarkoitus vertailla.

● Paluuarvon tyyppi on bool ja paluuarvo on true ainoastaansiinä tapauksessa, että ensimmäinen parametri on aidostipienempi kuin toinen parametri.

Page 98: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 94◆ Vertailufunktion voi antaa lisäparametrina sellaisille

algoritmikirjaston funktioille, joiden toiminta riippuukäsiteltävänä olevan säiliön alkioiden suuruudesta.

Esimerkiksi edellisen esimerkin vektorista voitaisiin hakea jatulostaa opiskelijanumeroltaan suurin opiskelija:

vector<Opiskelija>::iterator iter;

iter = max_element(opiskelijat.begin(),

opiskelijat.end(),

vertaile_opiskelijanumeroita);

// Koska seuraava tulosta-metodi kohdistetaan

// iteraattorin osoittamaan olioon, käytetään

// -> -operaattoria normaalin pisteen sijaan.

iter->tulosta();

◆ Kun vertailufunktio annetaan parametrina algoritmikirjastonfunktiolle, kutsukohdassa käytetään pelkkää funktion nimeä. Josfunktion perässä olisi kaarisulkeet, C++ yrittäisi kutsua funktiota jaantaa parametrina vertailufunktion paluuarvon, mikä ei oletarkoitus.

◆ Funktioita, jotka saavat parametrina toisen funktio, kutsutaankorkeamman kertaluvun funktioiksi (higher-order function).

◆ Viikkoharjoituksissa saattaa tulla vastaan algoritmikirjastonfunktiota ja/tai mekanismeja, joista ei edellä ole ollut puhetta. Neon tarpeen mukaan selitetty harjoitusten mallikoodeissa.

Page 99: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 95Muita STL-säiliöitä

◆ Sarjojen (säiliö joka säilyttää alkioiden järjestyksen) lisäksi STL:ssäon nk. assosiatiivisia säiliöitä (associative container), jotkatallettavat alkiot sisäiseen toteutukseensa sellaisessajärjestyksessä, että tiedon haku olisi mahdollisimman nopeaa.

Assosiatiivisissa säiliöissä alkioiden ei voi ajatella olevanperäkkäisjärjestyksessä, joten niillä ei myöskään olejärjestysnumeroa.

Tieto talletetaan assosiatiivisiin säiliöihin (haku)avaimen

perusteella, jonka perusteella sitä myös etsitään.

◆ Tutustutaan tässä kahteen STL:n tarjoamaan assosiatiiviseensäiliötyyppiin:

● set, joka on täysin analoginen Pythonin set-tyypin kanssa,joka puolestaan vastasi matemaattista joukkoa, jossa kukin arvovoi olla korkeintaan yhden kerran.

● map, joka vasta Pythonin dict-tyyppiä: se sisältäähakuavain-hyötytieto -pareja, joista tietty hyötytieto on nopealöytää, jos tiedetään siihen liittyvä hakuavain.

◆ Kuten vector-säiliö, myös set ja map vaativat omaninclude-rivinsä ohjelmankoodin alkuun:

#include <set> // set-rakenteen käyttöä varten

#include <map> // map-rakenteen käyttöä varten

Page 100: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 96Esimerkkejä STL-set-rakenteesta

◆ Määritellään joukko, johon voi tallentaa merkkijonoja:

set<string> laiva_on_lastattu;

Määrittelyn jälkeen joukko on automaattisesti tyhjä.

◆ Joukko voidaan alustaa toisella samantyyppisellä joukolla taisiihen voidaan sijoittaa toinen samantyyppinen joukko:

set<int> lottonumerot_1;

· · ·

set<int> lottonumerot_2(lottonumerot_1);

· · ·

lottonumerot_1 = lottonumerot_2;

◆ Joukon alustaminen ja siihen sijoitaminen voi tapahtua myösaaltosulkulistasta:

set<int> alkulukuja = {2, 3, 5, 7, 11, 13, 17};

· · ·

set<string> kaverit;

· · ·

kaverit = { "Matti", "Maija", "Teppo" };

◆ Joukkoon saadaan lisättyä uusi alkio insert-metodilla:

laiva_on_lastattu.insert("koirilla");

Lisäys toimii silloinkin, kun alkio kuuluu joukkoon joentuudestaan, se vaan ei tee mitään.

◆ Joukon alkioiden lukumäärä saadaan selville size-metodilla:

// Kavereiden lukumäärä

cout << kaverit.size() << endl;

Page 101: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 97◆ Alkion kuulumista joukkoon voidaan tutkia find-metodilla:

// Sana ei ole vielä joukossa, lisätään se.

if ( laiva_on_lastattu.find(sana)

== laiva_on_lastattu.end() ) {

laiva_on_lastettu.insert(sana);

cout << "Ok!" << endl;

// Sana oli joukossa jo ennestään.

} else {

cout << "Hävisit, " << sana

<< " oli jo lastattu!" << endl;

}

◆ Yksittäinen alkio voidaan poistaa joukosta erase-metodilla:

// Teppo ei ole enää kaveri.

kaverit.erase("Teppo");

◆ Metodi clear tyhjentää joukon (poistaa kaikki alkiot):

laiva_on_lastattu.clear();

◆ Vertailuoperaattorit toimivat joukoilla, joiden alkiotyyppi on sama:

if ( lottonumerot_1 == lottonumerot_2 ) {

// Molemmissa joukoissa tismalleen sama alkiot.

· · ·

} else {

// Joukkojen sisällössä eroavaisuuksia.

· · ·

}

◆ Metodi empty palauttaa arvon true, vain jos joukko on tyhjä:

if ( not kaverit.empty() ) {

// On ainakin yksi kaveri.

}

Page 102: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 98Esimerkkejä STL-map-rakenteesta

◆ Muista STL-rakenteista poiketen map-rakenteen määrittely sisältääsekä hakuavaimen että hyötytiedon tyypit:

map<string, double> hinnasto;

Määrittelyn jälkeen alustamaton map-säiliö on tyhjä.

◆ Kuten muitenkin STL-säiliöiden kanssa, myös map voidaan alustaatoisesta saman tyyppisestä mapistä ja siihen voidaan sijoittaasaman tyyppinen map:

map<string, string> sanakirja_1;

· · ·

map<string, string> sanakirja_2(sanakirja_1);

· · ·

sanakirja_1 = sanakirja_2;

◆ Myös aaltosulkulistalla alustaminen tai sijoittaminen toimii:

map<string, double> hinnasto_2 = {

{ "maito", 1.05 },

{ "juusto", 4.95 },

{ "liima", 3.65 },

};

· · ·

hinnasto_2 = {

{ "pippuri", 2.10 },

{ "kerma", 1.65 },

{ "suklaa", 1.95 },

};

Page 103: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 99◆ Hakuavaimeen liittyvän tiedon käsittely tapahtuu at-metodilla:

cout << hinnasto_2.at("kerma") << endl; // 1.65

hinnasto_2.at("kerma") = 1.79;

Käytettäessä at-metodia pitää kuitenkin muistaa vaaratilanne: joshakuavainta ei löydy mapistä, tuloksena on poikkeus ja ohjelmansuorituksen päättyminen.

Huomaa: at-metodilla mapiin ei saa lisättyä uutta tietoa.

◆ Vaaratilanne vältetään sillä, että tarkistetaan onko hakuavainmapissä ennen at-metodin kutsua:

if ( sanakirja_3.find(sana) != sanakirja_3.end() ) {

// Sana löytyi mapista.

cout << sanakirja_3.at(sana) << endl;

} else {

// Sana ei ole mapissa.

cout << "Virhe, tuntematon hakusana!" << endl;

}

Jossa hyödynnetään vanhaa tuttua seikkaa, että find palauttaaend()-iteraattorin, jos etsittyä tietoa ei löydy.

◆ Avain–hyötytieto -parin saa poistettua mapista antamallaerase-metodille parametrina poistettavan hakuavaimen:

if ( hinnasto_2.erase("suklaa") ) {

// Poisto onnistui, "suklaa" ei

// ole enää hinnastossa

· · ·

} else {

// Poisto epäonnistui, "suklaa" ei ollut

// hinnastossa alunperinkään.

· · ·

}

Vaikka olemassa olemattoman hakuavain–hyötytieto -parin poistoei olekaan virhe, ennen poistoa tilanteen voisi halutessaanvarmistaa find-metodilla.

Page 104: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 100◆ Uusi tietopari saadaan lisättyä insert-metodilla:

sanakirja_1.insert( { sana, sana_englanniksi } );

Jos edellisessä sana on mapissä hakuavaimena jo valmiiksi,insert ei tee mitään.

◆ Metodi size-palauttaa mapiin talletettujen tietoparienlukumäärän:

cout << "Sanakirjassa on " << sanakirja_2.size()

<< " sanaparia." << endl;

◆ Myös map-rakenteen alkioiden läpikäynti on toteutettuiteraattorien avulla.

Tämä ei kuitenkaan ole aivan yhtä suoraviivaista kuin muitenSTL-säiliöiden tapauksessa, koska jokainen alkio sisältääkin kaksiosatietoa: hakuavaimen ja hyötytiedon.

Tämä on toteutettu siten, että map-rakenteen alkiot ovat itseasiassa tietueita (vrt. s. 185). Jos siis on esimerkiksi määriteltymap-rakenne:

map<int, string> opiskelijat = {

// opnum, nimi

{ 123456, "Teekkari, Teemu" },

· · ·

};

niin rakenteeseen talletetut alkiot olisivat tyypiltään seuraavankaltaisia tietueita:

struct {

int first;

string second;

};

jossa tietueen kenttien nimet tosiaankin ovat first

hakuavaimelle ja second hyötytiedolle.

Page 105: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 101◆ map-rakenteen alkiot käytäisiin siis iteraattorin avulla läpi

seuraavan mukaisesti:

#include <iostream>

#include <string>

#include <map>

using namespace std;

int main() {

map<int, string> opiskelijat = {

{ 200001, "Teekkari, Tiina" },

{ 123456, "Teekkari, Teemu" },

// ···

};

map<int, string>::iterator iter;

iter = opiskelijat.begin();

while ( iter != opiskelijat.end() ) {

cout << iter->first << " "

<< iter->second << endl;

++iter;

}

}

Koodissa on syytä panna merkille, kuinka iteraattorin osoittamantietueen kenttiä (osatietoja) käsitellään ->-operaattorillatavanomaisen . -operaattorin sijaan.

Page 106: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 102◆ Jos halutaan välttää iteraattoreiden suora käyttö, mapin alkiot

voidaan käydä läpi myös for-rakenteella:

int main() {

map<int, string> opiskelijat = {

{ 200001, "Teekkari, Tiina" },

{ 123456, "Teekkari, Teemu" },

// ···

};

for ( auto tietopari : opiskelijat ) {

cout << tietopari.first << " "

<< tietopari.second << endl;

}

}

Koska tietopari ei ole edellisessä silmukassa iteraattori, vaanvarsinainen mapistä löytyvä tietue, käytetään first- jasecond-kenttien käsittelyyn normaalia . -operaattoria.

◆ Seuraavista linkeistä löydät muutaman laajemman esimerkinSTL-säiliöiden käytöstä:

● kassapääteohjelma1

● kalenteriohjelma2

1 http://www.cs.tut.fi/cgi-bin/run/~opersk/upload-file?/home/opersk/materiaalit/moniste/koodit.public/osa-04-08.public.cpp

2 http://www.cs.tut.fi/cgi-bin/run/~opersk/upload-file?/home/opersk/materiaalit/moniste/koodit.public/osa-04-09.public.cpp

Page 107: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 103Moduulit

◆ Modulaarisuus on ohjelman suunnittelu- ja toteutusvaiheentyökalu, jolla suuri ohjelma saadaan jaettua pienempiin jahelpommin hallittaviin osiin.

Jos ohjelma on modulaarinen, se on jaettu selkeisiinosakokonaisuuksiin, joiden yhteistyön tuloksena saadaan kokoohjelma.

◆ Moduuliksi kutsutaan konkreettista ohjelman osakokonaisuutta.

Useimmissa ohjelmointikielissä jokainen moduuli toteutetaanerillisenä lähdekooditiedostona tai lähdekooditiedostoparina.

◆ Moduulit ovat jossain määrin analogisia luokkien kanssa:jokaisella moduulilla on julkinen ja yksityinen rajapinta.

Luokat ja moduulit eivät kuitenkaan missään tapauksessa tarkoitasamaa asiaa. Ainoat yhtäläisyydet liittyvät siihen, että kumpiakinkäytetään julkisen rajapinnan avulla.

Lisäksi, jos luokka muodostaa selkeän osakokonaisuudenohjelmasta, se on usein järkevää toteuttaa moduulina, kutenmyöhemmissä esimerkeissä käy ilmi.

Page 108: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 104Esimerkki: geometrialaskut

◆ Tutkitaan esimerkki, josta moduulien toteuttamisen mekaanisetperusyksityiskohdat käyvät ilmi.

Tämän esimerkin ohjelma on yksinkertainen prototyyppigeometrialaskin-ohjelmalle. Se koostuu kahdesta moduulista:pääohjelmasta ja geometriset laskutoimitukset sisältävästämoduulista.

◆ Pääohjelmamoduuli tiedostossa laskin.cpp näyttääseuraavalta:

// Moduuli: laskin / tiedosto: laskin.cpp

// Sisältää pääohjelman geometrialaskimelle.

#include "geometria.hh"

#include <iostream>

using namespace std;

int main() {

double dimensio = 0.0;

cout << "syota nelion sivun pituus: ";

cin >> dimensio;

cout << "ymparysmitta: "

<< nelion_ymparysmitta(dimensio) << endl

<< "pinta-ala: "

<< nelion_pinta_ala(dimensio) << endl;

cout << "syota ympyran sade: ";

cin >> dimensio;

cout << "ymparysmitta: "

<< ympyran_ymparysmitta(dimensio) << endl

<< "pinta-ala: "

<< ympyran_pinta_ala(dimensio) << endl;

}

Page 109: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 105◆ C++:ssa jokainen moduuli, joka tarjoaa julkisessa rajapinnassaan

palveluita muille moduuleille, tarvitsee esittelytiedoston

(otsikkotiedosto, header file), jossa kuvataan kaikki ne palvelut,jotka kuuluvat moduulin julkiseen rajapintaan.

Esimerkin ohjelmassa moduuli geometria tarjoaa pääohjelmallevalmiit funktiot ympärysmittojen ja pinta-alojen laskemiseksitarvittaville geometrisille kuvioille. Moduulin esittelytiedostokoostuu siis pääohjelman tarvitsemien funktioiden esittelyistä:

// Moduuli: geometria / tiedosto: geometria.hh

// Moduulin geometria esittelytiedosto:

// sisältää esittelyt geometristen kuvioiden

// laskuihin tarvittaville funktioille.

#ifndef GEOMETRIA_HH

#define GEOMETRIA_HH

double nelion_ymparysmitta(double sivu);

double nelion_pinta_ala(double sivu);

double ympyran_ymparysmitta(double sade);

double ympyran_pinta_ala(double sade);

#endif // GEOMETRIA_HH

◆ Ei murehdita #ifndef-, #define- ja #endif-riveistä.

Mutta sovitaan että niiden on oltava mukana ja että esimerkinmukaisesti #ifndef- ja #define-rivien perässä on kyseessäolevan esittelytiedoston nimi isoilla kirjaimilla ja piste alaviivaksimuutettuna.

Page 110: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 106◆ Viimeistään tässä vaiheessa on syytä panna merkille, kuinka

pääohjelmamoduulissa (laskin.cpp) on rivi:

#include "geometria.hh"

Tämä on käsky, jolla C++-kääntäjälle kerrotaan moduulin haluavankäyttää palveluita toisen moduulin julkisesta rajapinnasta.

Kun em. #include-rivi on lisätty laskin.cpp-tiedoston alkuun,voidaan main-funktiossa kutsua geometria.hh:ssa esiteltyjäfunktioita, vaikka niiden määrittely onkin jossain muualla.

◆ Pääohjelmamoduulille ei yleensä ole tarpeen kirjoittaa omaaesittelytiedostoa, koska se ei tarjoa palveluita muille moduuleille.

◆ Toteutustiedosto geometria.cpp sisältää moduulin julkisessarajapinnassa esiteltyjen funktioiden määrittelyt sekä niidentarvitsemat yksityisen rajapinnan apupalvelut:

// Moduuli: geometria / tiedosto: geometria.cpp

// Moduulin geometria toteutustiedosto:

// sisältää määrittelyt geometristen kuvioiden

// laskuihin tarvittaville funktioille.

const double PII = 3.141593;

double nelion_ymparysmitta(double sivu) {

return 4 * sivu;

}

double nelion_pinta_ala(double sivu) {

return sivu * sivu;

}

double ympyran_ymparysmitta(double sade) {

return 2 * PII * sade;

}

double ympyran_pinta_ala(double sade) {

return PII * sade * sade;

}

Page 111: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 107Monimutkaisempi esimerkki: bussiaikataulut

◆ Toteutetaan bussiaikatauluohjelma, joka osaa kellonajan jabussinumeron kysyyttyään tulostaa kolmen seuraavan kyseisennumeroisen bussin lähtöajat.

◆ Ohjelma koostuu kolmesta moduulista:

aikataulu.cpp

Pääohjelmamoduuli, joka sisältää aikataulutietorakenteenalustuksen, hyvin yksinkertaisen käyttöliittymän ja käyttäjänsyöttämien kellonajan ja bussivuoron perusteella suoritettavanbussivuorojen haun tietorakenteesta.

Käyttää hyväkseen sekä moduulin aika että moduulinapufunktiot julkisen rajapinnan tarjoamia palveluita.

aika.hh + aika.cpp

Moduuli määrittelee luokan Aika, joka mahdollistaakellonaikojen käsittelyn: alustus, asettaminen, näppäimistöltälukeminen, tulostaminen ja pienempi–yhtäsuuri -vertailunsuorittamisen kahdelle Aika-tyyppiselle oliolle.

Käyttää hyväkseen moduulin apufunktiot julkisenrajapinnan palveluita.

apufunktiot.hh + apufunktiot.cpp

Tarjoaa funktiot merkkijonon muuttamiseksi kokonaisluvuksi jakokonaisluvun lukemiseksi näppäimistöltä. Moduulintarjoamat palvelut jossain määrin sekalaisia funktioita, joille eiollut muuta sopivampaa moduulia.

Ei hyödynnä muiden moduulien tarjoamia palveluita.

◆ Ohjelma saattaa vaikuttaa aluksi monimutkaiselta, mutta se eimuutamaa kohtaa lukuunottamatta sisällä mitään uutta. Uuttaasiaa sisältävät kohdat on merkitty kommentilla //*** ja selitettykoodilistauksen jälkeen.

Page 112: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 1081 // Lähdekooditiedosto: aikataulu.cpp2 #include "aika.hh"3 #include "apufunktiot.hh"4 #include <iostream>5 #include <vector>6 #include <map>

7 using namespace std;

8 const int TULOSTETTAVIEN_MAARA = 3;

9 using Aikataulu = map<int, vector<Aika>>;

10 void tulosta_seuraavat_lahtoajat(const Aikataulu& aikataulu,11 const Aika& nyt, int bus);12 int main() {13 Aikataulu aikataulu = {14 // first: bussin numero, second: vektori lähtöaikoja15 { 1, {{"06.00"}, {"09.00"}, {"12.00"}, {"21.00"}} },16 { 14, {{"10.26"}, {"16.26"}, {"22.26"}} },17 { 17, {{"11.02"}} },18 { 25, {{"8.41"}, {"20.41"}} },19 };

20 // Kovin yksinkertainen käyttöliittymä:21 // ei lopetusmahdollisuutta laisinkaan.22 while ( true ) {23 Aika klo_nyt;24 while ( not klo_nyt.kysy("Kellonaika: ") ) {25 cout << "Virheellinen kellonaika!" << endl;26 }27 int bussi = 0;28 while ( not lue_kokonaisluku("Bussin nro: ", bussi) ) {29 cout << "Virheellinen numero!" << endl;30 }31 tulosta_seuraavat_lahtoajat(aikataulu, klo_nyt, bussi);32 }33 }

Page 113: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 10934 void tulosta_seuraavat_lahtoajat(const Aikataulu& aikataulu,35 const Aika& nyt, int bus) {36 Aikataulu::const_iterator iter = aikataulu.find(bus);

37 if ( iter == aikataulu.end() ) {38 cout << "Tuntematon bussi " << bus << "!" << endl;39 return;40 } else if ( iter->second.size() == 0 ) {41 cout << "Bussilla " << bus << " ei lahtoja!" << endl;42 return;43 }

44 const vector<Aika>& aikavek = iter->second; //***

45 vector<Aika>::size_type tutkittava = 0;46 while ( tutkittava < aikavek.size() ) {47 if ( nyt.pienempi_yhtasuuri(aikavek.at(tutkittava)) ) {48 break;49 }50 ++tutkittava;51 }

52 cout << "Bussin " << bus << " seuraavat lahdot:" << endl;

53 int montako_tulostettu = 0;54 while ( montako_tulostettu < TULOSTETTAVIEN_MAARA ) {55 if ( tutkittava >= aikavek.size() ) {56 tutkittava = 0;57 }

58 aikavek.at(tutkittava).tulosta();

59 ++montako_tulostettu;60 ++tutkittava;61 }62 }

Page 114: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 11063 // Lähdekooditiedosto: apufunktiot.hh64 #ifndef APUFUNKTIOT_HH65 #define APUFUNKTIOT_HH

66 #include <string>

67 using namespace std;

68 bool string_intiksi(const string& syote, int& tulos);69 bool lue_kokonaisluku(const string& kehote, int& tulos);

70 #endif

71 // Lähdekooditiedosto: apufunktiot.cpp72 #include <iostream>73 #include <string>

74 using namespace std;

75 namespace { //***76 bool onko_numeromerkkijono(const string& mjono) {77 const string NUMEROMERKIT = "0123456789";

78 // Tyhjä merkkijono ei esitä numeroa.79 if ( mjono.length() == 0 ) {80 return false;81 }

82 string::size_type tutkittava_indeksi = 0;

83 // Jos merkkijonossa on useampia kuin yksi merkki ja84 // ensimmäinen merkki on miinus, kyseessä saattaa olla85 // negatiivinen luku: hypätään miinusmerkki yli.86 if ( mjono.length() > 1 and mjono.at(0) == ’-’ ) {87 ++tutkittava_indeksi;88 }

Page 115: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 11189 // Käydään loput merkit läpi ja varmistetaan, että90 // kaikki ovat numeromerkkejä.91 while ( tutkittava_indeksi < mjono.length() ) {92 char tutkittava_merkki = mjono.at(tutkittava_indeksi);93 if ( NUMEROMERKIT.find(tutkittava_merkki)94 == string::npos ) {95 return false;96 }97 ++tutkittava_indeksi;98 }99 return true;

100 }101 }

102 bool string_intiksi(const string& lahtoarvo, int& tulos) {103 if ( not onko_numeromerkkijono(lahtoarvo) ) {104 return false;105 } else {106 tulos = stoi(lahtoarvo);107 return true;108 }109 }

110 bool lue_kokonaisluku(const string& kehote, int& tulos) {111 cout << kehote;112 string lukustr = "";113 getline(cin, lukustr);

114 if ( not string_intiksi(lukustr, tulos) ) {115 return false;116 } else {117 return true;118 }119 }

Page 116: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 112120 // Lähdekooditiedosto: aika.hh121 #ifndef AIKA_HH122 #define AIKA_HH

123 #include <string>

124 using namespace std;

125 class Aika {126 public:127 Aika(const string& aika = "00.00");128 bool aseta(int tunti, int minuutti);129 bool aseta(const string& ttmm);130 bool kysy(const string& kehote);131 bool pienempi_yhtasuuri(const Aika& vertailtava) const;132 void tulosta() const;

133 private:134 int tunti_;135 int min_;136 };

137 #endif

138 // Lähdekooditiedosto: aika.cpp139 #include "aika.hh"140 #include "apufunktiot.hh"141 #include <iostream>142 #include <iomanip>143 #include <string>

144 using namespace std;

145 Aika::Aika(const string& aika) {146 aseta(aika); //***147 }

Page 117: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 113148 bool Aika::aseta(int tunti, int minuutti) {149 if ( tunti < 0 or tunti > 23 ) {150 return false;151 } else if ( minuutti < 0 or minuutti > 59 ) {152 return false;153 } else {154 tunti_ = tunti;155 min_ = minuutti;156 return true;157 }158 }

159 bool Aika::aseta(const string& ttmm) {160 string::size_type pisteind = 0;

161 pisteind = ttmm.find(’.’);

162 if ( pisteind == string::npos ) {163 return false;164 }

165 int tt = 0; // apumuuttuja tuntiarvon tallettamiseen166 int mm = 0; // apumuuttuja minuuttiarvolle

167 if ( not string_intiksi(ttmm.substr(0, pisteind), tt) ) {168 return false;169 }

170 if ( not string_intiksi(ttmm.substr(pisteind + 1), mm) ) {171 return false;172 }

173 return aseta(tt, mm);174 }

Page 118: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 114175 bool Aika::kysy(const string& kehote) {176 cout << kehote;177 string rivi = "";178 getline(cin, rivi);

179 return aseta(rivi);180 }

181 bool Aika::pienempi_yhtasuuri(const Aika& vertailtava) const {182 if ( tunti_ < vertailtava.tunti_ ) {183 return true;184 } else if ( tunti_ == vertailtava.tunti_185 and min_ <= vertailtava.min_ ) {186 return true;187 } else {188 return false;189 }190 }

191 void Aika::tulosta() const {192 cout << setw(2) << right << setfill(’0’) << tunti_193 << "."194 << setw(2) << right << setfill(’0’) << min_ << endl;195 }

Page 119: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 115◆ Huomaa, että koodirivit on numeroitu esimerkissä juoksevasti,

vaikka ohjelma muodostuu useasta (5) eri lähdekooditiedostosta.

Ohjelma on myös tilan säästämiseksi hyvin niukastikommentoitu, eikä monisteesta löydy sen kummempaa selitystämuille kuin sellaisille kohdille, jotka ovat selkeästi uutta asiaa. Voitkuitenkin ladata hieman paremmin kommentoidut versiotlähdekooditiedostoista seuraavista linkeistä:

● aikataulu.cpp1

● apufunktiot.hh2 ja apufunktiot.cpp3

● aika.hh4 ja aika.cpp5

◆ Esimerkissä oli muutama täysin uusi asia:

Rivi 44

Kannattaa muistaa, että helpoin tulkinta viitteelle on lisänimenantaminen olemassa olevalle muuttujalla. Tästä näkökulmastaajateltuna määrittely:

const vector<Aika>& aikavek = iter->second;

tarkoittaa vain sitä, että jatkossa nimeä aikavek voidaankäyttää tarkoittamaan samaa kuin iter->second. Määrittelynavulla koodia on saatu selkeytettyä ja kirjoittamisen vaivaavähennettyä.

const on mukana siksi, että iter osoittaa const-säiliöönaikataulu, jota ei voi käsitellä kuin vakiona (const).

1 www.cs.tut.fi/cgi-bin/run/~opersk/upload-file?/home/opersk/materiaalit/moniste/koodit.public/osa-05-02/aikataulu.public.cpp

2 www.cs.tut.fi/cgi-bin/run/~opersk/upload-file?/home/opersk/materiaalit/moniste/koodit.public/osa-05-02/apufunktiot.public.hh

3 www.cs.tut.fi/cgi-bin/run/~opersk/upload-file?/home/opersk/materiaalit/moniste/koodit.public/osa-05-02/apufunktiot.public.cpp

4 www.cs.tut.fi/cgi-bin/run/~opersk/upload-file?/home/opersk/materiaalit/moniste/koodit.public/osa-05-02/aika.public.hh

5 www.cs.tut.fi/cgi-bin/run/~opersk/upload-file?/home/opersk/materiaalit/moniste/koodit.public/osa-05-02/aika.public.cpp

Page 120: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 116Rivi 75

Kyseessä on nimetön nimiavaruus, jonka avulla moduulillesaadaan muodostettua yksityinen rajapinta, jonka palveluihinpäästään käsiksi vain saman moduulin (lähdekooditiedoston)sisältä.

Kaikkia esittelyjä ja määrittelyjä, jotka ovat varattua sanaanamespace seuraavien aaltosulkeiden sisällä, voidaan käyttäävain apufunktiot.cpp-tiedostossa.

Nimetön nimiavaruus on moduulimekanismin vastike luokkienprivate-osalle.

Rivi 146

Tämä on ensimmäinen esimerkki luokan rakentajasta, jossaolion alustus ei tapahdu alustuslistassa, se tehdäänrakentajafunktion rungossa olevien käskyjen avulla. Tässä eisinällään ole mitään kummallista: rakentaja on funktio, jonkarungossa voi tarvittaessa olla käskyjä.

Kuitenkin kurssin tässä vaiheessa saavutetulla osaamistasollaAika-luokan rakentajan toteutukseen liittyy ongelma: mitätulisi tehdä, jos parametri aika on virheellinen? Funktioaseta-palauttaa kyllä siinä tilanteessa arvon false, muttarakentaja ei voi tehdä arvolla mitään, koska sillä itsellään ei olepaluuarvoa.

Oikea ratkaisu olisi poikkeuksen aiheuttaminen, kutenPythonissa samankaltaisissa tilanteissa tehtiin. Sitä vaan ei osataC++:lla vielä tehdä.

Muuta täysin uutta asiaa esimerkissä ei pitäisi olla. Olkoonkin,että aiemmin opittuja mekanismeja on saatettu käyttää luovillatavoilla, joten siihen kannattaa tutustua ajatuksen kanssa.

Page 121: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 117Moduulin julkinen rajapinta (.hh-tiedosto)

◆ Moduulinen julkinen rajapinta (siis moduulin toisille moduuleilletarjoamat palvelut) kirjataan moduulin esittelytiedostoon, jonkanimi perinteisesti päättyy C++:ssa .hh-kirjainyhdistelmään.

◆ Esittelytiedosto voi sisältää:

● funktioiden esittelyjä,

● const-vakioiden määrittelyjä,

● uusien tietotyyppien (myös luokkien) määrittelyjä ja

● mitä tahansa yhdistelmiä edellisistä.

Esittelytiedosto ei saa sisältää:

● muuttujien määrittelyjä ja

● funktioiden tai luokan metodien määrittelyjä.

◆ Jokaiseen lähdekooditiedostoon, joka tarvitsee jotain palveluatoisen moduulin julkisesta rajapinnasta, lisätään rivi:

#include "palvelun_tarjoavan_moduulin_nimi.hh"

Vaikka esimerkkikoodissa ei näin ollut tarvetta tehdä, em.include-rivi saattaa olla tarpeen laittaa myös jonkun moduulin.hh-tiedostoon. Millaisessa tilanteessa näin voisi käydä?

Samoin on yleistä, että moduulin .cpp-tiedosto joutuu tekemääninclude-operaation omalle .hh-tiedostolle (vrt. esimerkinrivi 139). Miksi näin on pitänyt tehdä?

◆ #include-käskyä ei saa käyttää siihen, että sen avulla otettaisiinkäyttöön .cpp-tiedostoja. Vaikka tämä joissain tilainteissasaattaakin toimia, se kuvaa ohjelmoijan syvääymmärtämättömyyttä moduulimekanismin toiminnasta.

Page 122: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 118◆ Ohjelmointityylillisesti pidetään hyvänä käytäntönä sitä, että

moduulin "includoidessa" sekä ohjelmoijan omia moduuleja ettäsysteemin standardikirjastoja, omien moduulien includet tulevatensin:

#include "omamoduuli-1.hh"

· · ·

#include "omamoduuli-n.hh"

#include <standardikirjasto-1>"

· · ·

#include <standardikirjasto-n >

◆ Monimutkaisten ohjelmien kanssa eteen tulee usein vastaantilanne, jossa yksi ja sama .hh-tiedosto saattaa tulla otetuksikäyttöön useita kertoja, kun eri lähdekooditiedostot suorittavatsille #include-käskyn omien tarpeidensa täyttämiseksi. Tämäjohtaa tietyissä tilanteissa ongelmiin.

Ongelmasta selvitään sillä, että jokaiseen .hh-tiedostoon lisätäänmekaanisesti, asiaa sen kummemin miettimättä rakenne:

#ifndef MODUULIN_NIMI_ISOILLA_KIRJAIMILLA _HH

#define MODUULIN_NIMI_ISOILLA_KIRJAIMILLA _HH

· · ·

moduulin varsinainen julkinen rajapinta tässä

· · ·

#endif

Se, että edellä moduulin nimi kirjoitetaan isoilla kirjaimilla javälilyönnit korvattuna alaviivoilla, on yleinenohjelmointityylillinen menettely, jota kannattaa noudattaa.

Page 123: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 119Moduulin yksityinen rajapinta (.cpp-tiedosto)

◆ Toteutus- eli .cpp-tiedostossa määritellään kaikki ne funktiot jametodit, jotka esittelytiedostossa on kuvattu moduulin julkiseksirajapinnaksi.

Toteutustiedosto voi toki sisältää mitä tahansa C++-koodia, jotajulkisen rajapinnan funktioiden toteuttamisen avuksi tarvitaan.

◆ Jos .cpp-tiedostossa halutaan toteuttaa moduulin omiaapufunktioita, joita ei voida edes kikkailemalla kutsua muistamoduuleista, ne on esiteltävä ja määriteltävä nimettömännimiavaruuden sisällä:

namespace { // Esittelyosa

void moduulin_yksityinen_funktio();

· · ·

}

· · ·

julkisen rajapinnan funktiomäärittelyt tässä

· · ·

namespace { // Määrittelyosa

void moduulin_yksityinen_funktio() {

· · ·

}

· · ·

}

Esittelyosa voi tuttuun tapaan puuttua, jos funktioidenmäärittelyt järjestetään niin, että kääntäjä on nähnyt funktioidenmäärittelyt ennen kuin niitä yritetään kutsua.

Käytönnössä siis siirtämällä määritelyosa tiedoston alkuun, kutenesimerkkiohjelman apufunktio.cpp-tiedostossa oli tehtyriviltä 75 alkaen.

Page 124: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 120◆ Edellä esitetty nimetön nimiavaruus -mekanismi pätee vain

normaaleihin funktioihin.

Jos .cpp-tiedostossa määritellään luokan metodifunktioita,namespace-mekanismilla ei ole merkitystä, koska luokkahuolehtii omista rajapinta/näkyvyysaluejärjestelyistään public-ja private-mekanismin avulla.

◆ Jos moduulin julkisessa rajapinnassa (.hh-tiedosto) on määriteltyconst-vakioita tai tietotyyppejä, joita tarvitaan myös moduulin.cpp-tiedostossa, joudutaan tekemään #include moduulinomalle .hh-tiedostolle.

◆ Ohjelmoija voi halutessaan määritellä myös nimettyjänimiavaruuksia:

namespace mun_avaruus {

void mun_funktio() {

· · ·

}

}

Tällaisia funktioita voidaan kutsua kahdella tavalla joko

mun_avaruus::mun_funktio();

tai lisäämällä kooditiedostoon using namespace-käsky:

using namespace mun_avaruus;

· · ·

mun_funktio();

Nimetyille nimiavaruuksille kurssilla tuskin tulee käyttöä, muttakyseessä on mukava yleissivistävä selitys sille, miksi kaikkienlähdekooditiedostojen alkuun ollaan uskollisesti kirjoitettu:

using namespace std;

Miksi? Mitä hyötyä sillä saavutetaan?

Page 125: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 121Moduulien suunnittelusta

◆ Oikeassa ohjelmointiprojektissa ohjelman jako moduuleihintehdään ohjelman suunnitteluvaiheessa.

◆ Periaatteessa moduulit löytää, kun miettii mistä loogisistaosakokonaisuuksista toteutettava ohjelma koostuu.

Esimerkkinä käytetty bussiaikatauluohjelma on lähes lapsellisenyksinkertainen ja pieni, jotta moduulijaolla varsinaisestisaavutettiin mitään hyötyjä, mutta silti asiaa miettimällä (jo ennenkoodauksen aloittamista), päällimäisiksi ajatuksiksi kohosi:

● ohjelman pitää pystyä käsittelemään ja vertailemaankellonaikoja,

● koska eri muodossa esitettyjä numeroita pitää pystyä lukemaannäppäimistöltä ja suorittaa muutoksia merkkijonojen janumeroiden välillä, tuntui aika selvältä, että tarvitaanjonkinlainen joukko funktioita käyttäjän syöttämiennumeroiden käsittelyyn,

● myös hyvin yksinkertainen käyttöliittymä tarvittaisiin ja

● algoritmi, joka osaa hakea sopivat bussivuorot ohjelmankäyttämästä tietorakenteesta.

Lopulta, lähinnä esitysteknisistä syistä, käyttöliittymä jahakualgoritmi päätettiin toteuttaa pääohjelmamoduulina, ja kaksimuuta kokonaisuutta omina moduuleinaan, jolloinlopputuloksena ohjelman moduulijaoksi saatiin:

● pääohjelmamoduuli (aikataulu),

● ajankäsittelymoduuli (aika) ja

● apufunktiomoduuli numeroiden käsittelyyn (apufunktiot).

Page 126: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 122◆ Moduulien etsinnässä siis on päämääränä jakaa ohjelma osiin,

joista kukin:

● toteuttaa selkeästi rajatun osan kokonaisuudesta ja

● on riittävän yksinkertainen (mitä jos ei ole?).

◆ Jos ongelma on isohko, kannattaa moduuleita yrittää etsiäpilkkomalla ongelmaa ja saatuja osaongelmia toistuvastipienempiin osiin, kunnes lopulta päädytään niin pieniinosaongelmiin, että ne ovat helposti hallittavissa.

Tällaista lähestymistapaa kutsutaan top-down-menetelmäksi.

◆ Jotain yleisiä suuntaviivoja kokonaisuuksista, jotka tyypillisestikannattaa toteuttaa moduuleina sen kokoluokan ohjelmissa,joihin kurssilla törmätään:

● luokka

● pääohjelma

● käyttöliittymä/monimutkaisen syötteen käsittely

● tiedoston lukeminen ja jäsentely

● yleiskäyttöiset algoritmit (haku, lajittelu)

Oma mielikuvitus auttaa myös paljon.

◆ Moduulien julkisten rajapintojen (.hh-tiedostot) suunnittelu onhaastavampaa kuin moduulijaon suunnittelu.

Varsinkin kokemuksen karttuessa ainakin suurpiirteisenmoduulijaon sen kokoisissa ohjelmissa, joihin peruskursseillatörmätään, näkee lähes suoraan.

Page 127: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 123◆ Julkisen rajapinnan suunnittelussa täytyy ottaa jo huomattavasti

syvällisemmin kantaa siihen, mitä palveluita ja miten moduulinon tarkoitus tarjota muille.

Jotta tämän voi tehdä edes jollain tasolla menestyksellisesti,ohjelman rakennetta ja toteutusperiaatteita on tarpeen miettiämelkoisen tarkasti.

◆ Jos rajapinta on suunniteltu ideaalisen onnistuneesti, tuloksenaon jokaista muille moduuleille palveluita tarjoavaa moduuliakohden .hh-tiedosto, jota ei periaatteessa ole tarpeen muokataprojektin edetessä.

Näin onnistuneeseen lopputulokseen päästään harvoin.Käytännössä projektin edetessä tulee aina vastaan tilanteita, joissaymmärretään, että rajapinnassa on päädytty tekemään jotainhuonosti tai siitä on unohtunut jotain aivan kokonaan.

Näissä tilanteissa rajapintoja joudutaan muuttamaan, mikäsaattaa olla kallista, jos puutteellisen rajapinnan pohjalta onehditty jo kirjoittamaan paljon koodia: muutokset rajapintaanheijastuvat muutoksina kaikkialle, missä sitä käytetään.

Page 128: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 124Moduulaarisuudella saavutettavat hyödyt

◆ Modulaarisuuden hyödyt ovat pitkälti samoja kuin luokkienhyödyt, koska ne juontavat juurensa pääosin rajapintojenkäytöstä:

● Moduulin toteutusta (siis .cpp-tiedostoa eli yksityistärajapintaa) voidaan muuttaa julkisen rajapinnan säilyessäennallaan.

● Ohjelmiston loogisesti yhteenkuuluvat osat voidaan kootasamaan pakettiin, mikä selkeyttää ohjelmaa ja helpottaa sentestaamista ja ylläpitoa.

● Moduuleita voidaan kehittää projektissa rinnakkain sen jälkeen,kun julkinen rajapinta on sovittu.

● Modulaarisuus on hyvä työkalu suurien ohjelmointiprojektienhallintaan.

● Usein moduuleita voidaan uudelleenkäyttää joko kokonaan taiainakin osittain.

● Useimmat modulaarisuutta tukevat ohjelmointikieletmahdollistavat moduulien kääntämisen erikseen. Tämänopeuttaa kehitystyötä ja kuluttaa vähemmän resursseja:muutosten teon jälkeen vain muuttuneet moduulit tarvitseekääntää uudelleen.

Page 129: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 125Muisti ja muistiosoitteet

◆ Tutustutaan seuraavassa melko yleisellä tasolla ja vahvastiyksinkertaistaen siihen, mitä tietokoneen (keskus)muisti on.

◆ Kaikki tietokoneen käsittelemä tieto talletetaan koneenkeskusmuistiin.1

◆ Muistia voi ajatella joukkona numeroituja lokeroita:

· · ·

0 1 2 3 4 5 6 7 8 9 · · · n

joista jokaiseen prosessori voi tallettaa pienen määrän tietoa.

◆ Yhtä lokeroa kutsutaan muistipaikaksi ja sen järjestysnumeroa(muisti)osoitteeksi.

◆ Muistipaikkaan mahtuu tietoa talteen yksi tavu (byte) jokapuolestaan koostuu 8 bitistä.

◆ Yleensä muistia käsitellään kuitenkin tavun monikerran kokoisinapaloina.

◆ Yhteen muistipaikkaan voidaan siis tallettaa tietoalkio, joka onesitettävissä 8 bitillä (esim. char): jos on tarve tallentaa tietoa,joka esitetään suuremmalla bittimäärällä, niin käytetään riittävämäärä peräkkäisiä muistipaikkoja.

1 Ei koko totuus, mutta ainakin kaikki lähtötiedot ovat jossain vaiheessa talletettuina keskusmuistiin.

Page 130: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 126◆ Esimerkiksi jos int-tietotyypin alkiot ovat 32-bittisiä, voitaisiin

yksi kokonaisluku (siis sen binääriesitys) tallettaa neljäänperäkkäiseen muistipaikkaan:

32-bittiä︷ ︸︸ ︷

11101· · ·101 · · ·

0 1 2 3 4 5 6 7 8 9

◆ Vastaavasti 64-bittinen double voitaisiin tallettaa:

64-bittiä︷ ︸︸ ︷

01010· · ·10100110101· · ·011 · · ·

0 1 2 3 4 5 6 7 8 9

◆ Esimerkeistä huomataan, että samoihin muistipaikkoihin voidaantallettaa eri aikoina eri tyyppistä informaatiota.

◆ Koska tietokoneen uumenissa kaikki tieto esitetään bitteinä, niinpelkästään muistipaikan sisältöä tutkimalla ei voi päätellä, minkätyyppistä informaatiota sinne on talletettu.

◆ Tuosta päädytään vanhaan totuuteen: kaikella tiedolla pitää ollatyyppi, jotta sitä osattaisiin käsitellä oikein.

◆ C++:ssa (kopio- eli arvosemantiikka) muuttujan määrittely ontapa antaa nimi jonkin muistipaikan sisällölle.

Kääntäjä valitsee muuttujalle talletuspaikan muistissa ja pitääkirjaa tyyppi-informaatiosta ja muistiosoitteesta.

◆ Tyyppi-informaation perusteella kääntäjä osaa käyttää oikeitakonekäskyjä muuttujan arvon (siis muistipaikkojen sisällön)käsittelyyn.

Page 131: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 127◆ Esimerkiksi kun luodaan kokonaislukumuuttuja:

int luku;

niin kääntäjä etsii jostain riittävän monta vapaata peräkkäistämuistipaikkaa ja varaa ne muuttujan luku talletuspaikaksi:

luku

arvo · · ·

0 1 2 3 4 5 6 7 8 9

◆ Jatkossa aina kun ohjelmakoodissa käytetään muuttujaa luku, setulkitaan tarkoittamaan sille varatuissa muistipaikoissa olevaaarvoa.

◆ Jatkon kannalta on tärkeää pitää selvänä mielessä:

● jokainen muistipaikka identifioidaan yksikäsitteisellämuistiosoitteella,

● muistipaikassa on tallessa varsinainen tieto ja

● muuttuja on nimi muistipaikkaan talletetulle arvolle.

ja näistä johdettuna:

● jokaiseen muuttujaan liittyy jokin muistiosoite (nimittäin sejossa sijaitsevassa muistipaikassa muuttujan arvo on tallettuna).

Page 132: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 128Osoittimet

◆ Osoitintietotyypit ovat tietotyyppejä, joiden arvojoukko koostuutietyntyyppisten tietoalkioiden muistiosoitteista.

◆ Tai jos sen ilmaisee toisin: osoitintyyppiseen muuttujaan(osoittimeen, pointteriin) voidaan tallettaa muistiosoite.

◆ Osoittimeen voidaan siis tallettaa jonkin tarpeellisen tiedonsijainti muistissa.

◆ C++:ssa osoittimia määritellään:

kohdetyyppi * muuttujan_nimi ;

eli aivan kuin oltaisiin määrittelemässä normaalistikohdetyyppi-tyyppistä muuttujaa, mutta muuttujan nimen jatyypin nimien väliin lisätään *-merkki.

◆ Tuloksena saadaan muuttuja, johon voidaan tallettaakohdetyyppi-tyyppisen muuttujan muistiosoite (sen sijaintimuistissa).

◆ Kohdetyyppi on välttämätön, sillä sen perusteella osoittimeentalletetusta muistiosoitteesta löytyvää informaatiota (bittejä)osataan käsitellä oikein.

◆ Kaikista ohjelmassa käytetyistä muuttujista saadaan tarvittaessaselville niiden muistiosoite unaarisella &-operaattorilla.

◆ Osoittimeen talletetussa muistiosoitteessa sijaitsevan muistipaikansisältöön päästään käsiksi unaarisella *-operaattorilla.

Unaarisen *-operaattorin toimintaidea on analoginen sen kanssa,kuinka sitä käytettiin STL-iteraattoreiden kanssa.

Page 133: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 129◆ Tutkitaan yksinkertainen esimerkki:

1 #include <iostream>

2 using namespace std;

3 int main() {

4 int i;

5 int* ip = nullptr; // nullptr selitetty sivulla 130

6 i = 5;

7 ip = &i;

8 *ip = 42;

9 cout << i << "," << *ip << ","

10 << ip << "," << &ip << endl;

11 }

◆ Kun ohjelma suoritetaan, ilmestyy näytölle (tämä vaihteleetietokoneesta, käyttöjärjestelmästä ja suorituskerrasta riippuen):

42,42,0x000002,0x000008

◆ Tutkitaan rivi kerrallaan, mitä suorituksen aikana tapahtuu:

rivi 4: Luodaan kokonaislukumuuttuja i: varataan sille tilaamuistista jostain vapaasta kohdasta (0x000002).

rivi 5: Luodaan osoitin kokonaislukuun ip: varataan sille tilaamuistista jostain vapaasta kohdasta (0x000008).

rivi 6: Sijoitetaan i:hin arvo 5, siis talletetaan 5:n binääriesitysosoitteesta 0x000002 alkaviin muistipaikkoihin.

rivi 7: Selvitetään muuttujan i osoite (0x000002) &-operaattorillaja talletetaan se muuttujaan ip eli osoitteesta 0x000008alkaviin muistipaikkoihin.

rivi 8: Sijoitetaan ip:hen talletetusta osoitteesta (0x000002)alkaviin muistipaikkoihin luvun 42 binääriesitys (samat muisti-paikat joissa muuttuja i on tallessa, siis i:n arvo muuttuu).

rivit 9 ja 10: Tulostetaan mielenkiintoiset arvot. Koska osoitin ip

on muuttuja, senkin sijainti muistissa saadaan &-operaattorilla.

Page 134: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 130◆ Kuvallisesti esimerkin muistinkäyttö näyttäisi lopputilanteessa

(rivi 11) seuraavalta:

i ip

42 binäärisenä 0x000002 · · ·

0 1 2 3 4 5 6 7 8 9 10 11 12

◆ Esimerkissä on oletettu osoittimen olevan kooltaan 4 tavua eli32-bittiä. Todellisuudessa osoittimet ovet useimmissa nykyisissätietokonelaitteissa 64-bittisiä.

◆ Esimerkistä havaitaan myös, että lausekkeessakäytettynä *ip käyttäytyy kuin int-tyyppinen muuttuja: silleevaluoituu arvo muistipaikasta, johon ip osoittaa ja siihenvoidaan suorittaa sijoitus =-operaattorilla, jolloin arvo tallettuukyseiseen muistipaikkaan.

◆ Sama pätee muunkin tyyppisiin osoittimiin.

◆ Osoittimen arvo kertoo, missä jokin tieto sijaitsee.

◆ *-operaattorilla osoitimesta saadaan selville varsinainen

osoitettu tieto.

◆ Sanomattakin(?) on selvää, että osoittimeen voi tallentaa vainsellaisen muuttujan osoitteen, jonka tyyppi on sama kuinosoittimen kohdetyyppi.

◆ Esimerkin rivillä 5 ip-osoittimeen alustetaan arvo nullptr. Tämäon tärkeä pieni yksityiskohta.

Tälläista osoitinta kutsutaan null- tai nil-pointteriksi.

Kaikkiin osoitinmuuttujiin voidaan tallettaa nullptr: osoittimellesaadaan näin asetettua arvo, joka ei osoita minnekään.

Page 135: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 131◆ Osoittimen yhtä- ja erisuuruutta voi vertailla nullptr-arvon

kanssa ja nullptr:ää voi käyttää osoitintyyppisenä parametrina.

Usein myös funktiot, joiden paluuarvo on tyypiltään osoitin,palauttavat virhetilanteessa nullptr:n.

◆ Jos ohjelman suorituksen aikana yritetään seurata1 null-osoitinta,on tuloksena aina ajonaikainen virhe ja ohjelman suorituksenpäättyminen.

◆ Null-osoitinta kannattaa aina käyttää osoitinmuuttujienalustusarvona ja virhepaluuarvona funktioilta, joiden paluuarvontyyppi on osoitin.

◆ Joku on jo ehkä edellä pannut merkille, että osoittimet jaSTL-iteraattorit käyttäytyvät hyvin samankaltaisesti:

● Molemmat edustavat epäsuoruutta: ne eivät suoraan kerro,mikä jonkin tietoalkion arvo on, vaan ne kertovat, mistätietoalkio löytyy.

● Sekä osoittimen että iteraattorin osoittamaa arvoa päästäänkäsittelemään unaarisella *-operaattorilla.

Tämä ei ole ainoa samankaltaisuus, lisäksi seuraavat pätevät:

● Jos osoittimeen on talletettu class- tai struct-tyyppisenarvon muistiosoite, osoitetun tiedon metodeja ja kenttiäkäsitellään -> -operaattorilla.

● Siinä missä STL-säiliöiden sisällä ++- ja ---operaattorit siirtävätiteraattoria eteen- ja taaksepäin, osoitinmuuttujaankohdistettuna ne siirtävät siirtävät sen osoittamaan seuraavaantai edelliseen saman tyyppiseen muistiosoitteeseen.

1 Eli tutkia tietoa, johon osoitin osoittaa.Eli kohdistaa osoittimeen unäärinen *-operaattori tai -> -operaattori.

Page 136: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 132C++-taulukot

◆ Kurssilla on totuttu käyttämään STL-vector-rakennettatilanteissa, joissa keskenään saman tyyppisiä arvoja halutaantallentaa siten, että alkioihin päästään käsiksi järjestysnumerolla(indeksillä).

C++:ssa on kuitenkin myös alkeellisempi tyyppi, joka käyttäytyyosittain samoin vektorin kanssa. Tätä rakennetta kutsutaantaulukoksi (array).

Taulukko on hyvin yksinkertainen kooltaan staattinen

tietorakenne (alkioiden lukumäärä on kiinteä), jonka yksitäisiinalkioihin päästään käsiksi [ ]-operaattorilla. Rakenne on suoraaperintöä C-kielestä, joka on C++:n esi-isä.

◆ Tällä kurssilla taulukoille ei itsessään ole käyttöä, koska kaikkitarvittavat asiat voidaan tehdä helpommin vector:in avulla,mutta yleissivistävistä syistä on hyvä tietää jotain taulukonluonteesta. Tätä tietoa tarvitaan, jos ikinä tulee eteen tarvekirjoittaa ohjelmia C-kielellä.

◆ Taulukkotyyppinen muuttuja määritellään seuraavasti:

int luvut[3]; // Taulukossa tilaa kolmelle int:ille.

Edellisen määrittelyn seurauksena C++ varaa niin pitkänyhtenäisen pätkän muistia, että sinne voidaan sijoittaa peräkkäinkolme int-tyyppistä arvoa:

luvut[0] luvut[1] luvut[2]

· · ·

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

Page 137: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 133◆ Taulukon alkioita voidaan nyt käsitellä [ ]-operaattorilla:

luvut[0] = 6;

luvut[1] = 12;

luvut[2] = 24;

cout << luvut[0] + luvut[2] << endl; // 30

minkä jälkeen tilanne muistissa näyttäisi seuraavalta:

luvut[0] luvut[1] luvut[2]

6 12 24 · · ·

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

◆ Taulukkotyyppi on toteutettu tehokkuussyistä siten, että taulukkoesitetään toteutustasolla vakio-osoittimena sen ensimmäiseenalkioon. Eli sii koodi:

cout << luvut << endl;

tulostaisi esimerkkitaulukosta näytölle osoitteen 0x000004.

◆ Seurauksena siitä, että taulukko samaistetaan aina osoitteeksi senensimmäiseen alkioon, on taulukko melko alkeellinen tietotyyppi:

● Taulukkomuuttujaa ei voi sijoittaa toiseen taulukkomuuttujan=-operaattorin avulla. Taulukkoa ei myöskään voi alustaatoisella taulukolla.

● Jos taulukko annetaan funktiolle parametrina, käyttäytyy seaina viiteparametrin tavoin, koska funktio saa muodollisessaparametrissa tiedon taulukon ensimmäisen alkionmuistiosoitteesta. Kun tätä osoitetta sitten käsitellään[ ]-operaattorilla, päädytään luonnollisesti operoimaanalkuperäisen taulukon alkioilla.

● Taulukko ei voi olla suoraan STL-säiliöiden alkiona.

Page 138: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 134◆ Taulukko voidaan kuitenkin käydä läpi osoittimen avulla:

int* taulukko_osoitin = nullptr;

taulukko_osoitin = luvut;

while ( taulukko_osoitin < taulu + 3 ) {

cout << *taulukko_osoitin << endl;

++taulukko_osoitin;

}

Edellä on hyödynnetty nk. osoitinaritmetiikkaa: taulukonalkuosoitteeseen voidaan lisätä kokonaisluku, jolloin tuloksenasaadaan osoitin alkioon, jonka indeksi vastaa lisättyäkokonaislukua.

Jos lisätty luku on taulukon alkioiden lukumäärä, saadaan osoitinsiihen kohtaan muistissa, joka on heti viimeisen alkion perässä:

cout << taulu + 3 << endl; // Tulostuu muistiosoite 16

Tätä osoitetta voidaan käyttää näppärästi taulukkoa osoittimenavulla läpi käyvän silmukan ehdossa, kuten edellä oli tehty.

◆ Käytännössä osoitinaritmetiikasta seuraa myös se, että esimerkiksiseuraavat tarkoittavat samaa:

cout << taulu[2] << endl;

taulu[1] = 99;

ja

cout << *(taulu + 2) << endl;

*(taulu + 1) = 99;

◆ Kuten todettua, tällä kurssilla taulukot ja niiden käyttäytyminen eiole asiaa, jota itse tarvitsee koodata.

Asia kannattaa kuitenkin tiedostaa yleissivistävistä syistä. Ei oleaivan odottamatonta, että esimerkiksi myöhemmillä kursseilla(Mikroprosessorit, Laitteistonläheinen ohjelmointi tms.) saattaapäätyä kirjoittamaan matalamman tason koodia C--kielellä, jossaasia tulee vastaan.

Page 139: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 135Muistin varaaminen dynaamisesti

◆ Tähän saakka kaikki käytetyt muuttujat ovat olleet sellaisia, ettämuistin varaaminen niille ja muistin vapauttaminen sen jälkeenkun sitä ei tarvita, on tapahtunut automaattisesti:

● Kun muuttuja on määritelty, kääntäjä on huolehtinut siitä, ettäsille varataan jostain tarvittava määrä muistia.

● Kun muuttujan elinikä päättyy,1 kääntäjä on silloinkinjärjestellyt asian niin, että tarpeeton muisti on vapautettu.

◆ On kuitenkin monia tilanteita, jossa joustavien tietorakenteidentoteuttamiseksi ohjelmoijan on pystyttävä itse kontrolloimaanmuuttujien elinkaaria (aika muuttujan muistinvarauksesta sillevaratun muistin vapauttamiseen).

Tällaisia muuttujia, joiden elinkaaren kontrollointi on täysinohjelmoijan vastuulla, kutsutaan dynaamisiksi muuttujiksi.

Mekanismeja ja työkaluja, joilla dynaamimisia muuttujiahallitaan, kutsutaan dynaamiseksi muistinhallinnaksi.

◆ C++:ssa on kaksi peruskäskyä, joilla dynaamista muistia varataanja vapautetaan: new ja delete.

◆ Jos vapaata muistia ei ole riittävästi jäljellä, kun new-operaatiosuoritetaan, siitä aiheutuu poikkeus, joka keskeyttää ohjelmansuorituksen.

Kyseisen poikkeustilanteen käsittely on kuitenkin periaattellisellatasollakin sen verran haastavaa, että tällä kurssilla näytellään ikäänkuin muisti ei koskaan loppuisi.

1 Paikallisilla muuttujilla elinikä päättyy ja muisti vapautuu, kun ohjelman suoritus poistuu muuttujan määrittelylohkosta.Olion jäsenmuuttujien elinikä päättyy ja muisti vapautuu, kun kyseisen olion elinikä päättyy.

Page 140: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 136◆ new-käskyllä ohjelmoija voi varata uuden dynaamisen muuttujan:

● Muuttujan elinkaari alkaa sillä hetkellä, kun new-käsky onnistuumuistin varaamaan.

● Dynaamisella muuttujalla ei ole nimeä, mutta new-operaationarvoksi evaluoituu osoitin, jonka arvo kertoo, missä kohdassakeskusmuistia uusi dynaaminen muuttuja sijaitsee.

● Jotta muuttujalla voisi tehdä jotain hyödyllistä, sen osoite ontalletettava osoitinmuuttujaan.

● Jos muuttujan on tyypiltään luokka, new huolehtii rakentajankutsumisesta.

◆ Kun dynaamista muuttujaa ei enää tarvita, siis siitä halutaanhankkiutua eroon, muisti vapautetaan delete-komennolla:

● Muuttujan elinkaari päättyy ja sille varattu muisti vapautetaanmuuhun käyttöön.

● Jos muuttuja on tyypiltään luokka, jolle on määritelty purkaja,delete huolehtii purkajan kutsumisesta.

◆ Koska kääntäjä ei mitenkään automatisoi dynaamisen muuttujankäsittelyä, on tärkeää muistaa, että jokaikinen new’llä varattumuuttuja pitäisi myös muistaa vapauttaa deletellä sen jälkeen,kun sitä ei enää tarvita.

Jos tätä ei muista tehdä, tai sählää asiat jotenkin niin,1 että kaikkeanew’llä varattua muistia ei pystykään vapauttamaan, seurauksenaon tilanne, jota kutsutaan muistivuodoksi: ohjelma pitää muistiavarattuna, vaikka se ei enää tarvitse sitä.

Muistivuoto on huono asia varsinkin ohjelmissa, joiden suorituskestää pitkään: jos muistia varataan toistuvasti, mutta osaa siitä eikoskaan vapauteta, ohjelman kuluttama keskusmuistin määräkasvaa, mikä kuormittaa koko järjestelmää.

1 Esimerkiksi hukataan sen osoitinmuuttujan arvo, joka pitää kirjaa dynaamisesti varatun muuttujan muistiosoitteesta.

Page 141: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 137◆ Ensimmäinen esimerkki dynaamisesta muistinkäsittelystä:

1 int main() {

2 int* dyn_muuttujan_osoite = nullptr;

3 dyn_muuttujan_osoite = new int(7);

4

5 cout << "Osoite: " << dyn_muuttujan_osoite << endl;

6 cout << "Alku: " << *dyn_muuttujan_osoite << endl;

7

8 *dyn_muuttujan_osoite = *dyn_muuttujan_osoite * 4;

9

10 cout << "Loppu: " << *dyn_muuttujan_osoite << endl;

11

12 delete dyn_muuttujan_osoite;

13 }

Kun koodi käännetään ja suoritetaan, saadaan seuraava tulostus:

Osoite: 0x1476010

Alku: 7

Loppu: 28

Muista tässä ja muissakin suoritusesimerkeissä, joissa osoittimienarvoja tulostetaan näytölle, että muistiosoitteen arvo saattaavaihdella eri suorituskerroilla.

◆ Ohjelman olennaisimmat kohdat:

rivi 2: Luodaan osoitinmuuttuja dyn_muuttujan_osoite, jottarivillä 3 new’llä luodun muuttujan osoitteelle on talletuspaikka.

rivi 3: Varataan uusi dynaaminen muuttuja, alustetaan senarvoksi 7 ja talletetaan sen osoite myöhempää käyttöä varten.

rivit 6–10: Käytetään dynaamista muuttujaa sen muistiosoitteenja *-operaattorin avulla.

rivit 12: Kun dynaamista muuttujaa ei enää tarvita, vapautetaansille varattu muisti delete-operaattorilla.

Page 142: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 138Älykkäät osoittimet

◆ Vaikka kaikki dynaamisen muistin käsittely voitaisiin aina toteuttaaedellä esiteltyjen C++-kielen osoittimien ja new- jadelete-käskyjen avulla, niiden käyttö on usein melko sotkuista.

Varsinkin monimutkaisia dynaamisia rakenteita käsiteltäessäpäädytään hyvin usein tilanteeseen, jossa kaikkea new’llä varattuamuistia ei muisteta vapauttaa deletellä.

Tämän tehtävän helpottamiseksi C++ tarjoaa valmiina työkaluinank. älykkäitä osoittimia (smart pointer).

◆ C++:n älykkäät osoittimet ovat kirjastotietotyyppejä, jotkaautomatisoivat dynaamisesti varatun muistin vapauttamisen senjälkeen, kun kukaan ei enää viittaa siihen, eli siis selkokielellä:varattu muisti vapautetaan automaattisesti, kun ohjelmassa eienää ole yhtään (älykästä)osoitinmuuttujaa, joka osoittaakyseiseen muistialueeseen.

◆ Älykkäät osoitintyypit ovat siitä mukavia, että niiden käyttö eiradikaalisti eroa normaalien osoittimien käytöstä, mutta kaupanpäälle saavutetaan se ilo, että ohjelmoijan ei tarvitse itse murehtiamuistin vapauttamisesta.

◆ Älykkäiden osoittimien käyttää vaatii ohjeman alkuun rivin:

#include <memory>

josta saadaan käyttöön tyypit:

shared_ptr

unique_ptr

weak_ptr

Tällä kurssilla niistä tutustutaan vain shared_ptr-tyyppiin.

Page 143: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 139◆ Yksinkertainen esimerkki shared_ptr-osoittimen käytöstä:

1 #include <iostream>

2 #include <memory> // Tämä pitää muistaa.

3

4 using namespace std;

5

6 int main() {

7 shared_ptr<int> int_oso_1( new int(1) );

8 shared_ptr<int> int_oso_2( make_shared<int>(9) );

9

10 cout << *int_oso_1 << " " << *int_oso_2 << endl;

11 cout << int_oso_1 << " " << int_oso_2 << endl;

12 cout << int_oso_1.use_count() << " "

13 << int_oso_2.use_count() << endl << endl;

14

15 *int_oso_2 = *int_oso_2 - 4;

16

17 int_oso_1 = int_oso_2;

18

19 cout << *int_oso_1 << " " << *int_oso_2 << endl;

20 cout << int_oso_1 << " " << int_oso_2 << endl;

21 cout << int_oso_1.use_count() << " "

22 << int_oso_2.use_count() << endl;

23 }

jonka suoritus tuottaa seuraavat tulosteet:

1 9

0x2589010 0x2589060

1 1

5 5

0x2589060 0x2589060

2 2

Page 144: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 140◆ Tutkitaan koodin mielenkiintoisimmat tapahtumat:

rivi 2: #include <memory> tarvitaan älykkäiden osoittimienkäyttämiseksi.

rivi 7: Määritellään shared_ptr-tyyppinen osoitinmuuttuja,johon voidaan tallentaa int-muuttujan muistiosoite, jaalustetaan se osoittamaan dynaamisesti new’llä varattuunmuuttujaan, jonka arvoksi on alustettu 1.

rivi 8: Pohjimmiltaan sama toimenpide kuin rivillä 7, muttakäytetään shared_ptr-osoittimen alkuarvonamake_shared-funktion muodostamaa osoitinta. Ainoaolennainen ero edelliseen on se, että tämä tapa onhuomattavasti nopeampi.

rivit 10–11, 15 ja 19–20: Suoritetaan shared_ptr-osoittimillesamoja operaatioita kuin sivulla 137 suoritettiin tavallisilleosoittimille. Lähes kaikki operaatiot,1 jotka toimivat tavallisillaosoittimilla, toimivat myös shared_ptr-osoittimilla(unaarinen *, ->, sijoitus,2 vertailu ja tulostus).

rivit 12–13 ja 21–22: Tulostetaan metodilla use_count

kummastakin osoittimesta viitelaskurin arvo, eli tieto, kuinkamonta shared_ptr-tyyppistä osoitinta yhteensä osoittaasiihen new’llä varattuun muistialueeseen, johonuse_count-metodin kohteena oleva osoitin osoittaa.

Varattu muisti vapautetaan automaattisesti, kun viitelaskurinarvo laskee nollaan.

1 ++- ja -- -operaattorit eivät toimi shared_ptr-osoittimille.

2 Sijoitus toimii toisesta samantyyppisestä shared_ptr-osoittimesta.

Page 145: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 141rivi 17: Esimerkin olennaisin asia: asetetaan int_oso_1

osoittamaan samaan muistiosoitteeseen kuin int_oso_2. Nytdynaamisesti varattuun muistiin, johon int_oso_1 alunperinosoitti, ei ole enää jäljellä yhtään shared_ptr-tyyppistäosoitinta: varattu muisti vapautetaan automaattisesti.

rivi 23: Muuttujien int_oso_1 ja int_oso_2 elinikä päättyy,jolloin niiden osoittamaan muistialueseen ei ole enää jäljelläyhtään shared_ptr-osoitinta: varattu muisti vapautetaan

automaattisesti.

◆ Huomaa, kuinka esimerkissä ei ole yhtään delete-käskyä, vaikkadynaamista muistia varataan sekä new’llä ettämake_shared-funktiolla.

Tämähän juuri oli älykkäiden osoittimien, vastuu dynaamisestivaratun muistin vapauttamisesta on siirrettyshared_ptr-olioiden vastuulle.

Tässä käytetään usein termiä omistaja (owner), joka on vainhienompi termi kuvaamaan sitä, kenen vastuulle muistinvapauttaminen kuuluu.

◆ shared_ptr-olioilla on vielä muutama muu hyödyllinenominaisuus, joille saattaa joskus tulla tarvetta:

● Jos shared_ptr-osoittimesta pitää saada muistiosoitenormaalina C++-osoittimena, se tapahtuu get-metodilla:

shared_ptr<double> shared_double_ptr{ new double };

· · ·

double *normaali_double_ptr;

· · ·

normaali_double_ptr = shared_double_ptr.get();

● shared_ptr-osoittimeen ei voi sijoittaa =-operaattorillanormaaliosoitinta.

● shared_ptr-osoittimeen voi kuitenkin sijoittaa nullptr:in.

Page 146: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 142● shared_ptr-osoitinta ei voi vertailla suoraan

normaaliosoittimeen. Vertailu onnistuu kuitenkin, josshared_ptr-osoitin muutetaan get-metodillanormaaliosoittimeksi, esimerkiksi:

if ( normaaliosoitin == sharedososoitin.get() ) {

· · ·

}

● shared_ptr-osoitinta voi vertailla nullptr:iin.

◆ shared_ptr-tyypillä on yksi hankala ominaisuus: jos niidenavulla muodostetaan "silmukka", muistia ei koskaan vapauteta:

#include <iostream>

#include <memory>

using namespace std;

struct Testi {

// Tässä kohdin jotain muita kenttiä.

// ···

shared_ptr<Testi> osoite;

};

int main() {

shared_ptr<Testi> oso1(new Testi);

shared_ptr<Testi> oso2(new Testi);

oso1->osoite = oso2;

oso2->osoite = oso1;

}

Edellisestä on hyvä piirtää kuva, jotta ymmärtää kyseessä olevaneräänlainen muna-ennen-kanaa -ongelma: oso1:n osoittamaamuistia ei voida vapauttaa, koska osoitin oso2->osoite osoittaasiihen. Mutta toisaalta oso2:n osoittamaa muistia ei myöskäänvoi vapauttaa, koska oso1->osoite osoittaa siihen.

Page 147: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 143Dynaamiset tietorakenteet

◆ Yksi perusmekanismi dynaamisten tietorakenteidentoteuttamiseen on nk. linkitetty lista (linked list).

◆ Linkitetyt listat muodostuvat tietue- (struct) -tyyppisistäalkioista, joista jokainen on varattu erikseen new-komennolla.

Jokainen alkio sisältää listaan talletettavan hyötytiedon lisäksiosoittimen, jonka avulla listan seuraava alkio löydetään.

Tämän lisäksi tarvitaan erillinen osoitintyyppinen muuttuja, jossapidetään tallessa listan ensimmäisen alkion sijainti.

Joskus on tehokkuussyistä hyödyllistä ylläpitää myös toistaosoitinmuuttujaa, jossa on tallessa listan viimeisen alkion osoite.

◆ Jos esimerkiksi halutaan tallentaa listaan tekstiä siten, ettäsähköpostiviestin jokainen rivi on listassa erillisenä alkiona,tarvittaisiin seuraava struct-tyyppi ja kirjanpito-osoittimet:

struct Listan_alkio {

string tekstirivi;

Listan_alkio* seuraavan_osoite;

};

Listan_alkio* ensimmaisen_osoite;

Listan_alkio* viimeisen_osoite;

Kun tällaiseen listaan lisätään kolme alkiota, näyttää tilanne tältä:

ensimmaisen_osoite

viimeisen_osoite

"...." "....." "..."

Page 148: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 144◆ Edellisessä havaintokuvassa dynaamisesti varattuja

Listan_alkio-tyyppisiä tietueita on korostettu harmaallataustalla.

Lisäksi nullptr-arvo esitetään kuvissa yleensä "maadoituksena".

◆ Aina kun listaan lisätään uusi alkio, varataan new’llä tilaatietueelle, joka sisältää lisättävänä olevan hyötytiedon lisäksirakenteen toteuttamiseen liittyvää kirjanpitotietoa (siis osoittimenlistan seuraavaan alkioon).

◆ Aina kun listasta poistetaan alkio, sille aiemmin varattu tilavapautetaan delete-komennolla.

◆ Lisäksi sekä lisäys- että poistovaiheessa joudutaan päivittämäänosoittimien arvoja niin, että niissä on tallessa oikeatmuistiosoitteet.

◆ Hyväksi todettu tapa listarakenteiden toteuttamiseen on se, ettäedellä esitetty tietuetyyppi ja osoittimet listan ensimmäiseen javiimeiseen alkioon kätketään luokan private-osaan, jatoteutetaan listan käsittelyyn tarvittavat operaatiot luokanmetodeina.

◆ Monimutkaisemmatkin rakenteet ovat luonnollisesti mahdollisia(esim. puut ja verkot). Vain mielikuvitus asettaa rajat sen jälkeen,kun perusymmärtämys linkkikenttien käsittelystä on sisäistetty.

◆ Toteutetaan seuraavaksi edellä kuvatun kaltainen lista kahdellahiukan eri tekniikalla: ensin C++:n omien osoittimien ja sittenshared_ptr-tyypin avulla.

Page 149: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 145Tehtävälista C++-osoittimilla

◆ Listamoduulin julkinen rajapinta lista.hh:

1 #ifndef LISTA_HH

2 #define LISTA_HH

3 #include <string>

4 using namespace std;

5 class Lista {

6 public:

7 Lista();

8 void lisaa_alkio_loppuun(const string& lisattava_tehtava);

9 bool poista_alkio_alusta(string& poistettu_tehtava);

10 bool onko_tyhja() const;

11 void tulosta() const;

12 ~Lista();

13 private:

14 struct Listan_alkio {

15 string tehtava;

16 Listan_alkio* seuraavan_osoite;

17 };

18 Listan_alkio* ensimmaisen_osoite_;

19 Listan_alkio* viimeisen_osoite_;

20 };

21 #endif

Page 150: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 146◆ Pääohjelmamoduuli paaohjelma.cpp:

22 #include "lista.hh"

23 #include <iostream>

24 #include <string>

25 using namespace std;

26 int main() {

27 Lista tyolista;

28 string poistettu = "";

29 tyolista.lisaa_alkio_loppuun("siivoa tyopoyta");

30 tyolista.lisaa_alkio_loppuun("pese pyykit");

31 tyolista.lisaa_alkio_loppuun("tee kotilaksyt");

32 cout << "Tekemattomat askareet:" << endl;

33 tyolista.tulosta();

34 tyolista.poista_alkio_alusta(poistettu);

35 cout << " suoritettu: " << poistettu << endl;

36 tyolista.lisaa_alkio_loppuun("tiskaa");

37 tyolista.lisaa_alkio_loppuun("vie roskat");

38 cout << endl << "Tekemattomat askareet:" << endl;

39 tyolista.tulosta();

40 while ( not tyolista.onko_tyhja() ) {

41 tyolista.poista_alkio_alusta(poistettu);

42 cout << " suoritettu: " << poistettu << endl;

43 }

44 if ( tyolista.onko_tyhja() ) {

45 cout << "Kaikki askareet suoritettu!" << endl;

46 }

47 }

Page 151: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 147◆ Listamoduulin toteutus lista.cpp:

48 #include "lista.hh"

49 #include <iostream>

50 #include <string>

51 using namespace std;

52 Lista::Lista(): ensimmaisen_osoite_(nullptr),

53 viimeisen_osoite_(nullptr) {

54 }

55 Lista::~Lista() {

56 Listan_alkio* vapautettavan_osoite;

57 while ( ensimmaisen_osoite_ != nullptr ) {

58 vapautettavan_osoite = ensimmaisen_osoite_;

59 ensimmaisen_osoite_

60 = ensimmaisen_osoite_->seuraavan_osoite;

61 delete vapautettavan_osoite;

62 }

63 }

64 void

65 Lista::tulosta() const {

66 Listan_alkio* tulostettavan_osoite = ensimmaisen_osoite_;

67 int jarjestysnumero = 1;

68 while ( tulostettavan_osoite != nullptr ) {

69 cout << jarjestysnumero << ". "

70 << tulostettavan_osoite->tehtava << endl;

71 ++jarjestysnumero;

72 tulostettavan_osoite

73 = tulostettavan_osoite->seuraavan_osoite;

74 }

75 }

Page 152: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 14876 void

77 Lista::lisaa_alkio_loppuun(const string& lisattava_tehtava) {

78 Listan_alkio* uuden_osoite

79 = new Listan_alkio{lisattava_tehtava, nullptr};

80 if ( ensimmaisen_osoite_ == nullptr ) {

81 ensimmaisen_osoite_ = uuden_osoite;

82 viimeisen_osoite_ = uuden_osoite;

83 } else {

84 viimeisen_osoite_->seuraavan_osoite = uuden_osoite;

85 viimeisen_osoite_ = uuden_osoite;

86 }

87 }

88 bool

89 Lista::poista_alkio_alusta(string& poistettu_tehtava) {

90 if ( onko_tyhja() ) {

91 return false;

92 }

93 Listan_alkio* poistettavan_osoite = ensimmaisen_osoite_;

94 poistettu_tehtava = poistettavan_osoite->tehtava;

95 if ( ensimmaisen_osoite_ == viimeisen_osoite_ ) {

96 ensimmaisen_osoite_ = nullptr;

97 viimeisen_osoite_ = nullptr;

98 } else {

99 ensimmaisen_osoite_

100 = ensimmaisen_osoite_->seuraavan_osoite;

101 }

102 delete poistettavan_osoite;

103 return true;

104 }

Page 153: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 149105 bool

106 Lista::onko_tyhja() const {

107 if ( ensimmaisen_osoite_ == nullptr ) {

108 return true;

109 } else {

110 return false;

111 }

112 }

◆ Esimerkin selitys lähdekooditiedostoittain itseopiskeluun.

lista.hh

rivit 8–9: Listan on tarkoitus toimia siten, että uudet alkiotlisätään loppuun ja alkioiden poisto tapahtuu alusta. Kun alkiopoistetaan, sen arvo tallentuu viiteparametriinpoistettu_tehtava.

rivi 12: Ensimmäistä kertaa ollaan tilanteessa, jossa luokalle onpakko toteuttaa purkaja. Jos Lista-tyyppisen olion elinikäpäättyy tilanteessa, jossa lista ei ole tyhjä, purkajan tehtävänäon vapauttaa varattuina olevat listan solut.

Purkajametodin nimi on C++:ssa sama kuin luokan nimi, muttasen eteen on lisätty tilde-merkki (mato). Sitä kutsutaan kulissientakana automaattisesti, kun olion elinkaari päättyy.

rivit 14–17: Määritellään tietuetyyppi, jonka avulla listanyksittäisten alkiot saadaan esitettyä. KoskaListan_alkio-tyyppi on määritelty luokan private-osassa,sitä ei voi käyttää muualla kuin luokan metodeissa.

rivit 18–19: Kirjanpitojäsenmuuttujat, joiden tehtävänä on pitäätallessa listan ensimmäisen ja viimeisen alkion sijainti muistissa.

Tässä esimerkissä on toteutustavaksi valittu se, että myös listanviimeisen alkion osoitteesta pidetään kirjaa. Tämä on yleensäjärkevää silloin, alkiot lisätään listan loppuun. Miksi?

Page 154: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 150paaohjelma.cpp

Pääohjelmassa ei ole mitään erityisen kiinnostavaa dynaamiseenmuistinkäsittelyyn liittyen.

lista.cpp

rivit 52–54: Rakentaja alustaa kummankinkirjanpitojäsenmuuttujan arvoksi nullptr:in, mikä tulkitaanaina jatkossa tarkoittamaan sitä, että listassa ei ole yhtäänalkiota (siis se on tyhjä).

rivit 55–63: Kun Lista-olion elinkaari päättyy, purkajaakutsutaan automaattisesti. Sen tehtävän on vapauttaa kaikkilistassa vielä olevat dynaamisesti varatut alkiot(Listan_alkio-tyyppisiä).

Koska lista on tässä esimerkissä toteutettu normaaleillaC++-osoittimilla, dynaamisen muistin vapauttaminen onohjelmoijan vastuulla. Jos purkaja puuttuisi, muistia jäisi siisvapauttamatta.

Purkajan toimintaperiaate on, että jokaisella silmukankierroksella listan alusta poistetaan yksi alkio ja sille varattumuisti vapautetaan. Tämä käytiin luennolla läpi kuvan avullavaihe-vaiheelta.

rivit 64–75: Listan tulostus on toteutettu"standardimekanismilla", joka toimii aina, kun listan alkiot ontarpeen käydä läpi alusta loppuun.

Osoitinapumuuttuja asetetaan osoittamaan listanensimmäiseen alkioon. Niin kauan kun sen arvo ei ole nullptr

(miksi sen arvoksi väistämättä tulee lopulta nullptr?),tulostetaan osoitetun alkion sisältö. Lopuksi siirretäänapuosoitin osoittamaan listan seuraavaan alkioon.

Page 155: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 151rivit 76–87: Alkion lisääminen listaan vaatii sekin havaintokuvion

avulla animoinnin, johon tässä ei ole mahdollisuutta.

Perusidea on seuraava. Varataan dynaamisesti uusiListan_alkio-tyyppinen tietue ja otetaan sen osoite talteen.Tämän jälkeen päivitetään kaikkia olennaisia osoittimia siten,että listan rakenne säilyy halutun mukaisena. Huomaa, kuinkalisäysoperaatio täytyy tehdä eri tavoin riippuen siitä, ollaankotyhjään listaan lisäämässä ensimmäistä alkiota, vai onko listassaalkioita jo ennestään (miksi?).

rivit 88–104: Listasta poistetaan tässä toteutuksessa aina listanensimmäinen alkio, eli siis se, johon ensimmaisen_osoite_

osoittaa. Ennen kun poisto voidaan, pitää varmistua, ettälistassa on alkioita jäljellä. Jos ei ole, kyseessä virhe (paluuarvofalse).

Varsinainen poisto tapahtuu siten, että ensimmäinen alkiootetaan pois listasta, eli siis päivitetään osoittimia siten, ettäensimmaisen_osoite_-jäsenmuuttuja alkaa osoittamaanpoistettavan alkion perässä tulevaan alkioon (tai nullptr:iin,jos poistettiin viimeinen alkio).

Osoitinpäivitysten jälkeen poistettavalle alkiolle varattu muistivapautetaan.

Huomaa, kuinka taas pitää käsitellä hiukan eri tavoin viimaisenalkion poisto ja poisto listasta jossan on enemmän alkioita.

rivi 105–112: Tämä funktio on yksinkertainen: lista on tyhjä, josensimmaisen_osoite_ on nullptr, kuten rakentajanyhteydessä todettiin.

Page 156: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 152Tehtävälista shared_ptr:n avulla

◆ Edellistä esimerkkiä vastaavan listarakenteen toteutusshared_ptr-tyypin avulla, lista.hh:

1 #ifndef LISTA_HH

2 #define LISTA_HH

3 #include <string>

4 #include <memory>

5 using namespace std;

6 class Lista {

7 public:

8 Lista();

9 void lisaa_alkio_loppuun(const string& lisattava_tehtava);

10 bool poista_alkio_alusta(string& poistettu_tehtava);

11 bool onko_tyhja() const;

12 void tulosta() const;

13 private:

14 struct Listan_alkio {

15 string tehtava;

16 shared_ptr<Listan_alkio> seuraavan_osoite;

17 };

18 shared_ptr<Listan_alkio> ensimmaisen_osoite_;

19 shared_ptr<Listan_alkio> viimeisen_osoite_;

20 };

21 #endif

Page 157: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 153◆ Toteutustiedosto lista.cpp:

1 #include "lista.hh"

2 #include <iostream>

3 #include <string>

4 #include <memory>

5 using namespace std;

6 Lista::Lista(): ensimmaisen_osoite_(nullptr),

7 viimeisen_osoite_(nullptr) {

8 }

9 void

10 Lista::lisaa_alkio_loppuun(const string& lisattava_tehtava) {

11 shared_ptr<Listan_alkio> uuden_osoite(

12 new Listan_alkio{lisattava_tehtava, nullptr});

13 if ( onko_tyhja() ) {

14 ensimmaisen_osoite_ = uuden_osoite;

15 viimeisen_osoite_ = uuden_osoite;

16 } else {

17 viimeisen_osoite_->seuraavan_osoite = uuden_osoite;

18 viimeisen_osoite_ = uuden_osoite;

19 }

20 }

21 bool

22 Lista::poista_alkio_alusta(string& poistettu_tehtava) {

23 if ( onko_tyhja() ) {

24 return false;

25 }

26 shared_ptr<Listan_alkio>

27 poistettavan_osoite = ensimmaisen_osoite_;

Page 158: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 15428 poistettu_tehtava = poistettavan_osoite->tehtava;

29 if ( ensimmaisen_osoite_ == viimeisen_osoite_ ) {

30 ensimmaisen_osoite_ = nullptr;

31 viimeisen_osoite_ = nullptr;

32 } else {

33 ensimmaisen_osoite_

34 = ensimmaisen_osoite_->seuraavan_osoite;

35 }

36 return true;

37 }

38 bool

39 Lista::onko_tyhja() const {

40 if ( ensimmaisen_osoite_ == nullptr ) {

41 return true;

42 } else {

43 return false;

44 }

45 }

46 void

47 Lista::tulosta() const {

48 shared_ptr<Listan_alkio>

49 tulostettavan_osoite = ensimmaisen_osoite_;

50 int jarjestysnumero = 1;

51 while ( tulostettavan_osoite != nullptr ) {

52 cout << jarjestysnumero << ". "

53 << tulostettavan_osoite->tehtava << endl;

54 ++jarjestysnumero;

55 tulostettavan_osoite =

56 tulostettavan_osoite->seuraavan_osoite;

57 }

58 }

Page 159: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 155◆ Algoritmisesti muokatussa esimerkissä ei ole muuta uutta kuin se,

että Lista-luokalle ei ole tarvinnut toteuttaa purkajaa (tai käyttäädelete-käskyjä missään muussakaan yhteydessä), koskashared_ptr-osoittimet huolahtivat muistin vapauttamisesta,kun kukaan ei enää osoita siihen.

◆ kummassakin esimerkissä on kuitenkin yksi yhteinen piirre, jotakannattaa korostaa: tilanteesta riippuen poisto ja lisäys ontoteutettava hiukan eri tavoin.

Tyhjään listaan lisääminen vaatii eri järjestelyn kuin ei tyhjäänlistaan lisääminen. Vastaavasti viimeisen jäljellä olevan alkionpoisto pitää käsitellä toisin kuin muiden alkioiden poisto.

Jotta listan käsittely menisi monimutkaisemmissakin tilanteissaoikein, toteutusvaiheessa on aina syytä pysähtyä miettimään,mitkä seuraavista erikoistapauksista on tarpeen huomioida:

● ensimmäisen alkion lisääminen tyhjään listaan,

● listan ainoan jäljellä olevan alkion poisto,

● listan alkuun lisääminen,

● listan ensimmäisen alkion poistaminen,

● alkion lisääminen listan keskelle,

● alkion poistaminen listan keskeltä,

● alkion lisääminen listan loppuun ja

● alkion poistaminen listan lopusta.

Page 160: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 156Kahteen suuntaan linkitetty lista

◆ Toteutetaan kokonaislukujoukko kahteen suuntaan linkitettynälistana, johon alkiot on talletettu kasvavassa järjestyksessä. Lisäksi,jos joukkoon yritetään lisätä luku, joka on joukossa joentuudestaan, lisäys epäonnistuu (siis jokainen luku voi ollajoukossa korkeintaan yhden kerran).

Esimerkiksi joukko, johon kuuluvat alkiot 2, 5, 6 ja 10, näyttäisiseuraavalta:

4

2 5 6 10

Edellisessä havaintokuvassa shared_ptr-osoittimet on esitettyvalkoisina ympyröinä (©) ja normaaliosoittimet mustina (•).

◆ Toteutuksessa listan ensimmäiseen alkioon osoittavajäsenmuuttuja ja jokaisessa alkiossa seuraavan alkion osoitteensisältävä kenttä on toteutettu shared_ptr-tyypin avulla.

Syynä tähän on se, että listaa toteutettaessa ei tarvitse huolehtialistan alkioiden vapauttamisesta delete-käskyllä.

◆ Miksi viimeiseen alkioon osoittava jäsenmuuttuja ja listan alkiotaedeltävään alkioon osoittava kenttä on ollut kuitenkin järkeväätoteuttaa tavallisena osoittimena?

Page 161: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 157◆ Listamoduulin toteutus ksl-lista.hh:

1 #ifndef KSL_LISTA_HH

2 #define KSL_LISTA_HH

3 #include <memory>

4 using namespace std;

5 class KSL_lista { // Kahteen Suuntaan Linkitetty lista

6 public:

7 KSL_lista();

8 int pituus() const;

9 bool onko_arvo_listassa(int arvo) const;

10 void tulosta() const;

11 void tulosta_takaperin() const;

12 bool poista_arvo(int poistettava);

13 bool lisaa_numerojarjestykseen(int lisattava);

14 // Ei-purkajaa(!)

15 private:

16 struct Listan_alkio {

17 int data;

18 shared_ptr<Listan_alkio> seuraavan_osoite;

19 Listan_alkio* edellisen_osoite;

20 };

21 shared_ptr<Listan_alkio> ensimmaisen_osoite_;

22 Listan_alkio* viimeisen_osoite_;

23 int alkioiden_maara_;

24 };

25 #endif

Page 162: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 158◆ Listamoduulin toteutus ksl-lista.cpp:

26 #include "ksl-lista.hh"

27 #include <iostream>

28 #include <memory>

29 using namespace std;

30 KSL_lista::KSL_lista():

31 ensimmaisen_osoite_(nullptr),

32 viimeisen_osoite_(nullptr),

33 alkioiden_maara_(0) {

34 }

35 int

36 KSL_lista::pituus() const {

37 return alkioiden_maara_;

38 }

39 bool

40 KSL_lista::onko_arvo_listassa(int arvo) const {

41 shared_ptr<Listan_alkio>

42 tutkittavan_osoite = ensimmaisen_osoite_;

43 while ( tutkittavan_osoite != nullptr ) {

44 if ( tutkittavan_osoite->data == arvo ) {

45 return true;

46 } else if ( tutkittavan_osoite->data > arvo ) {

47 return false;

48 }

49 tutkittavan_osoite

50 = tutkittavan_osoite->seuraavan_osoite;

51 }

52 return false;

53 }

Page 163: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 15954 void

55 KSL_lista::tulosta() const {

56 shared_ptr<Listan_alkio>

57 tulostettavan_osoite = ensimmaisen_osoite_;

58 cout << "Listan alkioiden maara: " << pituus() << endl;

59 while ( tulostettavan_osoite != nullptr ) {

60 cout << tulostettavan_osoite->data << " ";

61 tulostettavan_osoite

62 = tulostettavan_osoite->seuraavan_osoite;

63 }

64 cout << endl;

65 }

66 void

67 KSL_lista::tulosta_takaperin() const {

68 Listan_alkio* tulostettavan_osoite = viimeisen_osoite_;

69 cout << "Listan alkioiden maara: " << pituus() << endl;

70 while ( tulostettavan_osoite != nullptr ) {

71 cout << tulostettavan_osoite->data << " ";

72 tulostettavan_osoite

73 = tulostettavan_osoite->edellisen_osoite;

74 }

75 cout << endl;

76 }

Page 164: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 16077 bool

78 KSL_lista::poista_arvo(int poistettava) {

79 if ( pituus() == 0 ) {

80 return false;

81 }

82 shared_ptr<Listan_alkio>

83 poistettavan_osoite = ensimmaisen_osoite_;

84 while ( true ) {

85 if ( poistettavan_osoite->data == poistettava ) {

86 break; // Poistettava arvo löytyi.

87 } else if ( poistettavan_osoite->data

88 > poistettava ) {

89 return false; // Arvo ei voi olla loppulistassa.

90 } else if ( poistettavan_osoite->seuraavan_osoite

91 == nullptr ) {

92 return false; // Viimeinen alkio on käsitelty.

93 } else {

94 poistettavan_osoite

95 = poistettavan_osoite->seuraavan_osoite;

96 }

97 }

98 // Tässä kohdassa tiedetään, että poistettava arvo löytyi

99 // listasta ja poistettavan_osoite osoittaa siihen.

Page 165: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 161100 // Poistettava alkio on listan ainoa alkio.

101 if ( ensimmaisen_osoite_.get() == viimeisen_osoite_ ) {

102 ensimmaisen_osoite_ = nullptr;

103 viimeisen_osoite_ = nullptr;

104 // Poistettava alkio on listan ensimmäinen alkio.

105 } else if ( poistettavan_osoite

106 == ensimmaisen_osoite_ ) {

107 ensimmaisen_osoite_

108 = ensimmaisen_osoite_->seuraavan_osoite;

109 ensimmaisen_osoite_->edellisen_osoite = nullptr;

110 // Poistettava alkio on listan viimeinen alkio.

111 } else if ( poistettavan_osoite.get()

112 == viimeisen_osoite_ ) {

113 viimeisen_osoite_

114 = viimeisen_osoite_->edellisen_osoite;

115 viimeisen_osoite_->seuraavan_osoite = nullptr;

116 // Poistettava alkio on listan keskellä.

117 } else {

118 poistettavan_osoite->edellisen_osoite->seuraavan_osoite

119 = poistettavan_osoite->seuraavan_osoite;

120 poistettavan_osoite->seuraavan_osoite->edellisen_osoite

121 = poistettavan_osoite->edellisen_osoite;

122 }

123 --alkioiden_maara_;

124 return true;

125 }

Page 166: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 162126 bool

127 KSL_lista::lisaa_numerojarjestykseen(int lisattava) {

128 shared_ptr<Listan_alkio> uuden_osoite(new Listan_alkio);

129 uuden_osoite->data = lisattava;

130 // Lisäys tyhjään listaan.

131 if ( pituus() == 0 ) {

132 uuden_osoite->seuraavan_osoite = nullptr;

133 uuden_osoite->edellisen_osoite = nullptr;

134 ensimmaisen_osoite_ = uuden_osoite;

135 viimeisen_osoite_ = uuden_osoite.get();

136 // Lisäys lista alkuun.

137 } else if ( lisattava < ensimmaisen_osoite_->data ) {

138 uuden_osoite->seuraavan_osoite = ensimmaisen_osoite_;

139 uuden_osoite->edellisen_osoite = nullptr;

140 ensimmaisen_osoite_->edellisen_osoite

141 = uuden_osoite.get();

142 ensimmaisen_osoite_ = uuden_osoite;

143 // Lisäys listan loppuun.

144 } else if ( lisattava > viimeisen_osoite_->data ) {

145 uuden_osoite->seuraavan_osoite = nullptr;

146 uuden_osoite->edellisen_osoite = viimeisen_osoite_;

147 viimeisen_osoite_->seuraavan_osoite = uuden_osoite;

148 viimeisen_osoite_ = uuden_osoite.get();

Page 167: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 163149 // Lisäys listan keskelle.

150 } else {

151 shared_ptr<Listan_alkio>

152 tutkittavan_osoite = ensimmaisen_osoite_;

153 // Etsitään listasta ensimmäinen alkio, jonka sisältämä

154 // arvo on suurempi tai yhtäsuuri kuin lisättävä arvo.

155 while ( tutkittavan_osoite->data < lisattava ) {

156 tutkittavan_osoite

157 = tutkittavan_osoite->seuraavan_osoite;

158 }

159 // Arvo ei voi saa olla listassa useammin kuin kerran.

160 if ( tutkittavan_osoite->data == lisattava ) {

161 return false;

162 }

163 // Nyt tutkittavan_osoite osoittaa ensimmäiseen

164 // alkioon listan sisällä, jonka arvo on suurempi

165 // kuin lisättävä arvo: uusi alkio lisätään sen eteen.

166 uuden_osoite->seuraavan_osoite = tutkittavan_osoite;

167 uuden_osoite->edellisen_osoite

168 = tutkittavan_osoite->edellisen_osoite;

169 uuden_osoite->edellisen_osoite->seuraavan_osoite

170 = uuden_osoite;

171 uuden_osoite->seuraavan_osoite->edellisen_osoite

172 = uuden_osoite.get();

173 }

174 ++alkioiden_maara_;

175 return true;

176 }

Page 168: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 164◆ Koodi on melkoisen pitkä, eikä sitä ole mielekästä käydä tässä läpi

rivi-riviltä. Lisäyksen ja poiston suorittavat funktiot onkommentoitu kohtalaisesti keskeisimmiltä osiltaan, joten niitäpystyy tutkimaan itse.

Seuraavassa kuitenkin muutama yleinen huomio toteutuksesta.

◆ Linkkikentät (siis osoittimet) listan alkioissa on jouduttutoteuttamaan siten, että toinen on tyypiltään shared_ptr jatoinen normaaliosoitin, koska

● shared_prt:ien avulla ohjelmoijan ei tarvitse itse huolehtiadynaamisen muistin vapautuksesta.

● Toisaalta, jos listan alkiossa kumpikin osoitin olisishared_ptr-tyyppinen, muistia ei koskaan vapautettaisi,koska peräkkäiset alkiot osoittaisivat toisiinsa.

Tämä on yleisestikin shared_ptr-tyypin heikkous:yksinomaan sitä käyttämällä ei voi toteuttaa rakenteita, joihinsisältyy osoitinsilmukoita (A osoittaa B:hen ja B osoittaa A:han,tai jokin pidempi ketju).

● Listan viimeiseen alkioon osoittava jäsenmuuttuja taas ontyypiltään normaaliosoitin, koska kun ei-tyhjästä listastapoistetaan viimeisenä oleva alkio (rivit 113–114), kyseinenjäsenmuuttuja pitää asettaa osoittamaan alkuperäiseen toiseksiviimeiseen alkioon (siis poistetun alkion edeltäjään):

viimeisen_osoite_

= viimeisen_osoite_->edellisen_osoite;

Jos viimeisen_osoite_ ei olisi normaaliosoitin, jouduttaisiinedellä tilanteeseen, jossa normaaliosoitin olisi tarve sijoittaashared_ptr-osoittimeen muutoin kuin alustuksen yhteydessä.Tämä on vaarallista ja johtaa lähes poikkeuksetta ongelmiin.

Page 169: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 165◆ Aina kun shared_ptr-osoittimia on tarpeen käsitellä samassa

lausekkeessa normaaliosoittimien kanssa, shared_ptr-oliostasaadaan sen osoittama muistiosoite normaaliosoittimenaget-metodilla. Saadun arvon voi sijoittaanormaaliosoitinmuuttujaan ja sitä voidaan vertaillanormaaliosoittimen kanssa.

get-metodilta saatua normaaliosoitinta ei saa itse vapauttaa

delete:llä, koska se sotkee shared_ptr:n sisäisen kirjanpidon.

◆ Kannattaa myös laittaa merkille, kuinka sekä lisäyksen ettäpoiston yhteydessä jouduttiin tutkimaan varsin useitaerikoistapauksia, joista oli puhuttiin sivulla 155.

◆ Aina kun käsitellään vähänkään yhteen suuntaan linkitettyä listaamonimutkaisempia osoitinrakenteita, päädytään lähesväistämättä lausekkeisiin, jotka ovat seuraavan muotoisia:

a ->b ->c

Eli -> -operaattoria joudutaan kirjoittamaan samaanlausekkeeseen perätysten.

Tässä ei varsinaisesti ole mitään kummallista: C++:nlaskujärjestyssäännöt määräävät, että lauseketta tulkitaanvasemmalta oikealle.

Jos asiaa mitenkään helpottaa, nuolioperaattorin paikalle voimielessään korvata tekstin: "osoittaman tietueen kentännimeltään". Edellinen esimerkki siis tulkittaisin:

a :n osoittaman tietueen kentän nimeltään b osoittamantietueen kentän nimeltään c arvo.

Page 170: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 166Rakenteen kopiointi: alustus ja sijoitus

◆ Kertaus: alustus tarkoittaa sitä, että muuttujalle asetetaanalkuarvo samalla kun se määritellään. Sijoitus puolestaantarkoittaa muuttujan arvon asettamista =-operaattorilla.

◆ Tähän saakka alustukseen ja sijoitukseen ei ole ollut pakottavaatarvettaa kiinnittää mitään huomiota, koska C++ huolehtiiautomaattisesti siitä, että ohjelmoijan itse määrittelemilletietotyypeillekin on olemassa nk. kopiorakentaja jasijoitusoperaattori.

Käytännössä tämä tarkoittaa sitä, että seuraava koodi toimii,vaikka ohjelmoija ei ole itse erikseen määritellyt, mitä alustuksenja sijoituksen yhteydessä pitäisi tapahtua:

class Mun_oma_luokka {

· · ·

};

· · ·

Mun_oma_luokka olio;

· · ·

Mun_oma_luokka olio2{olio}; // Kopiorakentajaa kutsutaan

· · ·

olio = olio2; // Sijoitusoperaattorikin toimii

◆ Jos alustus ja sijoitus jätetään C++:n automaattisestimäärittelemän toiminnallisuuden varaan, oletustoiminta on se,että alustusarvo ja sijoitettava arvo kopioidaan bitti-bitiltäkohdearvoksi.

Yksinkertaisten tietorakenteiden kanssa tämä on juuri se, mitähalutaan. Mutta jos kopioitava tieto sisältää osoittimia, varsinkinjos ne osoittavat dynaamisesti varattuun tietorakenteeseen,ajaudutaan lähes poikkeuksetta ongelmiin.

Page 171: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 167◆ Ajatellaan seuraavaa ei sinällään mitenkään monimutkaista

tilannetta, jossa käytetään sivulla 145 normaaliosoittimien avullatoteutettua tehtävälistaa:

Lista lista;

· · ·

if ( · · · ) {

Lista apulista{ lista }; // ✖✖✖

· · ·

}

// Tässä kohdassa homma on mennyt pieleen.

Toinen täysin vastaava tilanne olisi:

Lista lista;

· · ·

if ( · · · ) {

Lista apulista;

· · ·

apulista = lista; // ✖✖✖

· · ·

}

// Tässäkin kohdassa homma on mennyt pieleen.

Pinnallisesti tuossa ei vaikuttaisi olevan mitään väärää, mutta kunpiirretään kuva tilanteesta, jossa ollaan, kun ✖✖✖-merkinnälläkommentoitu rivi on suoritettu:

lista

apulista

"..." "..." "..." "..."

Page 172: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 168◆ Ongelma konkretisoituu heti, kun poistutaan lohkosta, jossa

apulista on määritelty paikalliseksi muuttujaksi: sen elinkaaripäättyy jolloin sen purkajaa kutsutaan.

Purkaja vapauttaa kaiken apulistalle varatun muistin, jokasattuu olemaan sama muisti, joka on varattu listalle.Muuttujan lista jäsenmuuttujat jäävät osoittamaan muistiin,joka on jo vapautettu (jäänneviitteitä).

◆ Ongelmia tulee eteen myös silloin, jos oltaisiin käytettysivulla 152 shared_ptr:ien avulla toteutettua listaa.

Tai tarkasti ottaen shared_ptr:ien kanssa on hiukantulkinnanvaraista, onko kyseessä ongelma vai ominaisuus:jäänneviitteitä ei pääse syntymään, mutta vaikka ohjelmassa onnäennäisesti kaksi eri listaa, ne todellisuudessa tarkoittavat yhtä jasamaa listaa.

Pystytkö näkemään, mitä erikoisia tilanteitashared_ptr-toteutuksessa voisi tulla eteen?

◆ Loppupeleissä edellisen esimerkin pointti on se, että C++:nvalmiiksi määrittelemän kopiorakentajan ja sijoitusoperaattorinkäyttö tilenteissa, joissa kopioitava arvo sisältää osoittimia, johtaahelposti ongelmiin.

◆ Kannattaa muuten huomata, että seuraavassakin esimerkissä asiatmenevät pieleen (miksi?):

void funktio(Mun_oma_luokka muodollinen_parametri) {

· · ·

}

· · ·

int main() {

Mun_oma_luokka todellinen_parametri;

· · ·

funktio(todellinen_parametri);

· · ·

}

Page 173: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 169◆ Mahdolliset ratkaisut sijoitus- ja alustusongelmaan ovat:

● Estetään itse tehtyjen osoittimia sisältävien tietotyyppiensijoittaminen ja alustaminen kokonaan: siis siten, että josjompaa kumpaa vaarallista operaatiota yritetään, saadaankäännösvirhe.

● Toteutetaan tietotyypeille omatekemät versiotsijoitusoperaattorista ja kopiorakentajasta siten, että ne oikeastikopioivat alkiot rakenteesta toiseen: siis varaavat jokaisellekopiotavalle alkiolle uutta muistia new-käskyllä ja sitä kauttarakentavat tyhjästä uuden kopion alkuperäisestä rakenteesta.

Tätä lähestymistapaa kutsutaan syväkopioinniksi (deep copy).

Kun taas mekanismi, jossa kopioidaan vain muistiosoite (tämämenetelmä siis johtaa helposti ongelmiin), on nimeltäänmatalakopiointi (shallow copy):

◆ Tällä kurssilla katsotaan vain helpompi tapa, jossa kopiointiestetään kokonaan. Syväkopioinin pohdinta jätetäänmyöhemmille opintojaksoille, koska siihen liittyy tässä vaiheessaepäolennaisten C++:n ominaisuuksien selittäminen jaymmärtäminen.

◆ C++:n automaattisesti luomien valmiiden kopiorakentajan jasijoitusoperaattorin käyttö estetään helposti lisäämällä luokanjulkiseen rajapintaan:

class Lista {

public:

Lista(const Lista& alustusarvo) = delete;

Lista& operator=(const Lista& sijoitusarvo) = delete;

· · ·

};

Parametrien tyyppien on oletava const-viitteitä luokan olioon jasijoitusoperaattorin on palautettava viite luokan olioon.

Page 174: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 170◆ Tällä kurssilla edellä esitelty esto kannattaa tehdä aina, kun

kyseessä on omatekemä tietorakenne, jonka private-osaankätkeytyy dynaaminen tietorakenne.

Näin toimiessaan välttyy monelta virheeltä ja vaikealtadebuggausrupeamalta.

◆ Tarkoittaako tämä nyt sitä, että ei ole mahdollista määritelläfunktioita, joille annetaan parametrina omatekemiä dynaamisiatietorakenteita?

Ei, koska edelleen on mahdollista määritellä sellaisia funktioita,joiden kutsussa kopiorakentajaa ei tarvita: jos parametrin tyyppion viite tai const-viite, esimerkiksi:

bool lue_tehtavatiedosto(Lista& tehtavat);

bool talleta_tehtavatiedosto(const Lista& tehtavat);

Noissahan ei kummassakaan ole mitään tarvetta estetynkopiorakentajan käytölle.

Kopiorakentajaa tarvitaan vain silloin, kun kyseessä onarvoparametri, koska arvoparametri alustetaan todellisestaparametrista kopiorakentajan avulla.

Page 175: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 171Rekursio

◆ Rekursiolla tarkoitetaan jonkin asian määrittelyä itsensä avulla.

◆ Esimerkiksi matematiikasta tuttu luonnollisen luvun kertoma:

n ! = n · (n − 1) · (n − 2) · . . . · 3 · 2 · 1

◆ Lisäksi on määritelty, että 0 ! on 1.

◆ Nollaa suuremman luvun kertoma on siis sen ja kaikkien sitäpienempien positiivisten kokonaislukujen tulo.

◆ Esimerkiksi 5 ! on 5 · 4 · 3 · 2 · 1 eli 120.

◆ Kertoma voidaan määritellä myös rekursiivisesti:

n ! =

1 mikäli n on 0n · (n − 1) ! mikäli n > 0

◆ Soveltamalla rekursiivista määritelmää 5 ! :n laskemiseksi saadaan:

5! = 5 · 4!

= 5 ·︷ ︸︸ ︷

4 · 3!

= 5 · 4 ·︷ ︸︸ ︷

3 · 2!

= 5 · 4 · 3 ·︷ ︸︸ ︷

2 · 1!

= 5 · 4 · 3 · 2 ·︷ ︸︸ ︷

1 · 0!

= 5 · 4 · 3 · 2 · 1 ·︷︸︸︷

1

= 120

◆ Rekursion idea on pyrkiä esittämään ongelman ratkaisualkuperäisen ongelman kanssa samanmuotoisina, mutta

ratkaisultaan yksinkertaisempina osaongelmina.

◆ Saatuihin osaongelmiin voidaan sitten soveltaa rekursiivistasääntöä uudelleen, kunnes lopulta päädytään niinyksinkertaiseen tapaukseen, että sen ratkaisu nähdään suoraan.

◆ Rekursion päämäärä on hajoittaa ja hallita.

Page 176: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 172Rekursio ohjelmoinnissa

◆ Kun puhutaan rekursiosta ohjelmoinnin yhteydessä, niin yleensäaihe liittyy funktioihin.1

◆ Rekursiivinen funktio on määritelty itsensä avulla eli se kutsuuitseään.

◆ Toteutetaan edellä esitelty kertoman laskeminen rekursiivisellaC++-funktiolla:

unsigned int kertoma(unsigned int n) {

if ( n == 0 ) {

return 1;

} else {

return n * kertoma(n - 1);

}

}

joka siis laskee ja palauttaa parametrinsa n kertoman.

◆ Periaatteessa funktion voi kirjoittaa suoraan kertomanrekursiivisen määritelmän pohjalta (s. 171).

◆ Mutta hyödyllisempää olisi ymmärtää, miksi funktio toimii javieläpä oikein.

◆ Oletetaan, että kertoma-funktiota kutsutaan näin:

int main() {

cout << kertoma(5) << endl;

}

ja tutkitaan vaihe-vaiheelta, kuinka suoritus etenee.

1 Myös tietorakenteet voivat olla rekursiivisia.

Page 177: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 173◆ Pääohjelman funktiokutsun seurauksena päädytään suorittamaan

kertoma-funktion runkoa:

main

kertoma

// parametrin n arvo on 5

unsigned int kertoma(unsigned int n) {

if ( n == 0 ) {

return 1;

} else {

return n * kertoma(n - 1);

}}

◆ Kuvassa peitettynä oleva main-laatikko kuvaa sitä, ettämain-funktion suoritus on kesken, eli vasta kun sen peittäväkertoma-funktio suorittaa return-käskyn, jatkuu main:insuoritus siitä kohdasta, jossa kertomaa kutsuttiin.

◆ Koska n:n arvo on 5, suoritetaan else-haara.

◆ Ennen kuin kertoma kuitenkaan voi palauttaa arvon, sen onlaskettava arvo lausekkeelle n * kertoma(n - 1), eli sen omasuoritus jää kesken niin pitkäksi aikaa, että rekursiivinen kutsukertoma(n - 1) saa laskettua 4:n kertoman.

Page 178: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 174◆ Itse asiassa suoritus etenee tällä rekursiokierroksella täysin samoin

kuin edelliselläkin, paitsi että parametrin n arvo on nyt 4:

main

kertoma

kertoma

// parametrin n arvo on 4

unsigned int kertoma(unsigned int n) {

if ( n == 0 ) {

return 1;

} else {

return n * kertoma(n - 1);}

}

◆ Suoritus ajautuu siis jälleen else-haaraan ja rekursio etenee ainavain syvemmälle.

◆ Huomaa, kuinka taustalle alkaa kertyä myös keskeneräisiäkertoma-funktion suorituksia.

Page 179: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 175◆ Näin ohjelma etenee, kunnes lopulta saavutaan tilanteeseen,

jossa parametrin n arvo on 0:

main

kertoma

kertoma

kertoma

kertoma

kertoma

kertoma

// parametrin n arvo on 0

unsigned int kertoma(unsigned int n) {if ( n == 0 ) {

return 1;

} else {

return n * kertoma(n - 1);

}

}

◆ Enää ei tapahdukaan rekursiivista kutsua, vaan kertoma

palauttaa suoraan arvon 1 ja keskeneräisten funktiokutsujen pinoalkaa purkautua.

Page 180: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 176◆ Toiseksi viimeiselle kertoman kutsulle palautuu siis viimeiseltä

rekursiiviselta kutsulta arvo 1, joka kerrotaan parametrin n

arvolla 1:

main

kertoma

kertoma

kertoma

kertoma

kertoma

// parametrin n arvo on 1

unsigned int kertoma(unsigned int n) {

if ( n == 0 ) {

return 1;

} else {return n * kertoma(n - 1);

}

}

︸ ︷︷ ︸

1 * 1

◆ Huomaa että jokaisella kertoma-funktion kutsukerralla(instanssilla) on oma versionsa parametrimuuttujasta n, jonkaarvo toki on säilynyt ennallaan, kun rekursiivisesti kutsutustafunktiosta palataan takaisin suorittamaan kutsuneen instanssinkäskyjä. Miksi?

◆ Sama pätee kaikkiin paikallisiin muuttujiin.

Page 181: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 177◆ Seuraavaksi vuorossa oleva keskeneräinen kertoma-funktio

jatkuu samaan tapaan:

main

kertoma

kertoma

kertoma

kertoma

// parametrin n arvo on 2

unsigned int kertoma(unsigned int n) {if ( n == 0 ) {

return 1;

} else {

return n * kertoma(n - 1);

}

}

︸ ︷︷ ︸

2 * 1

Page 182: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 178◆ Näin rekursiiviset kutsut purkautuvat ja saavuttavat lopulta

ensimmäisen kertoma-funktion instanssin:

main

kertoma

// parametrin n arvo on 5

unsigned int kertoma(unsigned int n) {

if ( n == 0 ) {return 1;

} else {

return n * kertoma(n - 1);

}

}

︸ ︷︷ ︸

5 * 24

◆ Joka palauttaa pääohjelmalle 5:n kertoman arvon:

int main() {

cout << kertoma(5) << endl;

}︸ ︷︷ ︸

120

◆ Rekursiossa ei ole ohjelmointikielen kannalta mitään erikoista:rekursiiviset funktiot ovat funktioita siinä missä kaikki muutkinfunktiot.

◆ Rekursion vaikeaselkoisuus piilee siinä, että se vaatii ohjelmoijaltaei-luontaista(?) tapaa hahmottaa ongelma ja sen ratkaisu.

Page 183: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 179◆ Rekursiivisten funktioiden suunnittelua ja ymmärtämistä

helpottaa, kun pitää mielessään että:

● Funktio on rekursiivinen, jos se kutsuu itseään (suoraan taiepäsuoraan).

● Suorituksen kuluessa rekursiivisesta funktiosta on "käynnissä"yhtä monta instanssia, kuin mikä on keskeneräistenrekursiivisten kutsujen lukumäärä.

● Jokaisella instanssilla on omat arvonsa parametreille japaikallisille muuttujille.

◆ Rekursiivisella funktiolla on kaksi olennaista ominaisuutta, jotkaon syytä muistaa:

1. Sillä on oltava lopetusehto (tai ehtoja), joka tunnistaaongelman triviaalitapaukset ja reagoi niihin ilman uudenrekursiivisen kutsun tarvetta.

2. Jokaisen rekursiivisen kutsun on yksinkertaistettava ratkaistavaaongelmaa niin, että lopulta päädytään triviaalitapaukseen.

◆ Esimerkin kertoma-funktiossa näitä kohtia edustivat:

1. if-rakenteen ehto n == 0 on triviaalitapaus: nollan kertomanarvo tiedetään suoraan.

2. Jokaisella rekursiokierroksella selvitettiin yhtä pienemmänluvun kertomaa: kutsuparametri pieneni yhdellä.

◆ Jos jomman kumman unohtaa, niin funktiota kutsuttaessa kokoohjelma kaatuu (UNIX:issa segmentation fault).

◆ Tämä johtuu siitä, että funktio päätyy kutsumaan itseäänloputtomiin (päättymätön rekursio): aiempien kutsukertojenmuuttujat 1 kuluttavat muistia, kunnes se yksinkertaisesti loppuu.

1 Ja muu aiempiin rekursiokierroksiin liittyvä informaatio.

Page 184: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 180◆ Rekursiolla saa aikaan toistoa.

◆ Periaatteessa mikä tahansa toistorakenne (silmukka) voidaankorvata rekursiolla.

◆ Parhaiten rekursio sopii kuitenkin sellaisten ongelmien ratkaisuun,jotka ovat luonteeltaan rekursiivisia: niitä voidaan "luonnollisesti"yksinkertaistaa siten, että jäljelle jäävät osaongelmat ovatalkuperäisen ongelman yksinkertaistettuja tapauksia.

◆ Usein rekursiivinen ratkaisu on lyhyempi ja siistimpi kuin vastaavasilmukkarakenteella toteutettu ratkaisu.

◆ Toisaalta, yleensä rekursiivinen ratkaisu on ainakin hiukanhitaampi ja kuluttaa enemmän muistia kuin silmukkarakenne.

◆ Rekursiiviset funktiot voidaan ominaisuuksiensa perusteellajaotella ryhmiin, jotka kannattaa yleissivistyksen nimissä katsoaläpi.

◆ Suoraksi rekursioksi kutsutaan tapausta, jossa funktio kutsuuitseään omassa rungossaan.

◆ Esimerkin kertoma on suoraan rekursiivinen.

◆ Epäsuora rekursio eli rinnakkaisrekursio tarkoittaa tilannetta,jossa funktio func_A kutsuu funktiota func_B, joka taaskutsuu func_A:ta jne:

func_A func_B

◆ Sotkuun voi osallistua useampiakin funktioita, eli syntyvä"kutsukehä" voi olla pidempi.

◆ Epäsuoran rekursion käytössä kannattaa olla pidättyväinen, koskase on usein hankalasti hahmotettava.

Page 185: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 181Häntärekursio

◆ Häntärekursioksi (tail recursion) kutsutaan tapausta, jossarekursiivisen kutsun paluuarvosta tulee ilman lisäoperaatioitakutsuvan instanssin paluuarvo.

◆ Esimerkin kertoma-funktio ei ole häntärekursiivinen: rekursiivisenkutsun paluuarvo pitää ensin kertoa n:llä, jotta siitä saadaanpaluuarvoksi kelpaava tulos:

return n * kertoma(n - 1);

◆ kertoma voitaisiin toteuttaa häntärekursiivisena:

unsigned int kertoma(unsigned int n, unsigned int tulos) {

if ( n == 0 ) {

return tulos;

} else {

return kertoma(n - 1, n * tulos);

}

}

jota pitäisi kutsua siten, että jälkimmäiseksi parametriksiannetaan 1:

cout << kertoma(5, 1) << endl;

◆ Häntärekursiivisille funktioille on usein tyypillistä, ettälopputulosta kerätään ylimääräiseen parametriin (tulos), johonkertynyt arvo voidaan palauttaa sellaisenaan lopetusehdontäyttyessä.

◆ Tuosta seuraa, että ylimääräisen parametrin alkuarvon tulee ollatriviaalitapauksen lopputulos. Miksi?

◆ Häntärekursio on tärkeä käsite, koska se voidaan muuttaamekaanisesti silmukkarakenteeksi: saavutetaan rekursion hyödytmutta päästään eroon sen huonoista puolista.

Page 186: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 182Liite A: tulosteiden muotoilu C++:ssa

◆ C++:ssa tulosteiden muotoilu tapahtuu cout:illa tulostettaessaeri tavoin, kuin mihin Pythonissa totuttiin.

◆ cout:in (ja ofstream-tyyppisten virtojen) kanssa muotoilutapahtuu lisäämällä tulostuskäskyyn tarvittaviin paikkoihintulostukenohjauskomentoja, joista C++ sitten päättelee, mitentulostuksen pitäisi tapahtua.

◆ Ohjauskomentojen käyttämiseksi ohjelman alkuun on syytä lisätä

#include <iomanip>

vaikka aivan kaikki niistä eivät sitä tarvitsekaan.

◆ Seuraavassa on listattu lyhyen esimerkin kera käyttökelpoisimmatohjauskomennot:

setw(leveys)

Seuraava tulostettava arvo kuluttaa näytöllä tilaa leveys

merkkiä, siinäkin tapauksessa, että se on lyhempi kuin leveys:

// | 42|

cout << "|" << setw(5) << 42 << "|" << endl;

left

Käytettäessä setw-komentoa, tulostetaan alkio tulostuskentänvasempaan laitaan (täytemerkit lisätään oikealle puolelle):

// |42 |

cout << "|" << setw(10) << left << 42 << "|" << endl;

Allekkain tulostettaessa saadaan vasemmalta tasattu sarake.

right

Kuten edellinen, mutta tulostus tapahtuu määrätyn levyisenkentän oikeaan laitaan. Käytetään oikealta tasattujensarakkeiden tulostamiseen.

Page 187: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 183setfill(merkki)

Jos setw-komennolla asetettu tulostuskentän leveys onsuurempi kuin tulostettavan arvon todellinen leveys, täytetäänylimääräinen tila merkillä:

// 42***

cout << setw(5) << left << setfill(’*’) << 42 << endl;

setprecision(tarkkuus)

Määrää, millä tarkkuudella reaaliluvut (double) tulostuvat:

// 3.1416

cout << setprecision(5) << 3.141593 << endl;

Oletusarvo on merkitsevien numeroiden lukumäärä.

fixed

Muuttaa edellä esitetyn setprecion-kommennon tarkkuuden

tulkinnaksi desimaalien lukumäärän;

// 3.14

cout << setprecision(2) << fixed

<< 3.1416 << arvo << endl;

scientific

Reaaliluvut tulostetaan muodossa, jossa desimaalipisteenedessä on aina yksi numero ja desimaalipisteen perässäsetprecision-komennon tarkkuus-määreen verrandesimaaleja. Suuruusluokka esitetään kymmenen potenssinataskulaskimista tutulla notaatiolla:

// 1.235+e003

cout << setprecision(3) << scientific

<< 1234.5678 << arvo << endl;

boolalpha

bool-tyyppiset arvot tulostuvat sanoina true ja false

oletusarvoisten 1 ja 0 sijaan.

// 1 true

cout << true << " " << boolalpha << true << endl;

Page 188: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 184Liite B: funktion oletusparametrit

◆ C++:ssa osalle tai kaikille funktion muodollisista parametreistävoidaan antaa funktion esittelyn yhteydessä oletusarvo, jotakäytetään parametrin arvona tilanteissa, joissa ohjelmoija eifunktion kutsukohdassa kerro vastaavaa todellista parametria:

void testifunktio(int maara, int min = 1, int max = 9);

· · ·

int main() {

· · ·

// kutsun muoto // todellinen kutsu

// ------------------- // -------------------

testifunktio(9); // testifunktio(9, 1, 9);

testifunktio(6, 3); // testifunktio(6, 3, 9);

testifunktio(3, 7, 8); // testifunktio(3, 7, 8);

· · ·

}

· · ·

void testifunktio(int maara, int min, int max) {

· · ·

}

◆ Loppupeleissä oletusparametrien käyttö on juuri noinyksinkertaista.

◆ Oikeastaan ainoa asia, joka on syytä pitää mielessä on se, ettäoletusarvoja voi antaa parametreille vain järjestyksessä oikealtavasemmalle. Seuraava on siis väärin:

void funktio(int a = 0, int b, int c = 0); // VÄÄRIN!

Miksi edellisestä muodostuu ongelma?

Page 189: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 185Liite C: tietueet eli structit

◆ Monissa ohjelmointikielissä ohjelmoijan on mahdollistamääritellä omia tietotyyppejään, joiden avulla useita keskenäänerityyppisiä tietoalkioita voidaan koota yhteen.

◆ Tavallaan kyseessä on eräänlainen alkeellisempi versioluokkatyypeistä: mitään funktiorajapintaa ei muodostu, vaanosatietoja (kenttiä) käsitellään suoraan.

◆ Tällaisia tietotyyppejä kutsutaan usein tietueiksi tai C++:ssastructeiksi, koska ne määritellään varatun sanan struct avulla:

struct Tuote {

string tuotenimi;

double hinta;

};

◆ Edellisen määrittelyn jälkeen uutta tyyppiä Tuote voidaankäyttää hyvin pitkälle samoin kuin muitakin tietotyyppejä:

Tuote tavara = {"saippua", 1.23};

// Tuote-tyyppisen muuttujan yksittäisiin osatietoihin

// päästään käsiksi . -operaattorilla.

tavara.hinta = 0.9 * tavara.hinta; // Alennus 10%

cout << tavara.tuotenimi << ": " << tavara.hinta << endl;

tavara = { "appelsiini", 0.45 };

◆ Tietuetta voi luonnollisesti käyttää myös STL-säiliööntalletettavana arvona, funktion parametrina (arvo- taiviiteparametrina) ja paluuarvona.

Page 190: TIE-02200 Ohjelmoinnin peruskurssi K2017 · Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä

TIE-02200 Ohjelmoinnin peruskurssi K2017 186Liite D: for-silmukka

◆ C++:n for-silmukalla on kolme käyttötarkoitusta:

1. STL-säiliön alkioiden läpikäynti:

vector<int> lukuvektori;

· · ·

for ( auto vektorin_alkio : lukuvektori ) {

cout << vektorin_alkio << endl;

}

Tämä käyttö on periaatteessa jo entuudestaan tuttu, ja vastaaPythonin rakennetta:

for alkio in lista:

print(alkio)

2. Halutun kokonaislukuvälin arvojen läpikäynti:

for ( int luku = 5; luku < 10; ++luku ) {

cout << 9 * luku << endl;

}

joka on jonkin verran monisanaisempi vastinePython-rakenteelle:

for luku in range(5, 10):

print(9 * luku)

3. Ikuisen silmukan toteuttaminen:

for ( ;; ) {

· · ·

}

joka vastaa toiminnaltaan täysin rakennetta:

while ( true ) {

· · ·

}