Ohjelman virheet ja poikkeusten käsittely

Samankaltaiset tiedostot
812347A Olio-ohjelmointi, 2015 syksy 2. vsk. X Poikkeusten käsittelystä

Virtuaalifunktiot ja polymorfismi

Operaattoreiden ylikuormitus. Operaattoreiden kuormitus. Operaattoreiden kuormitus. Operaattoreista. Kuormituksesta

Periytyminen. Luokat ja olio-ohjelmointi

Luokan operaatiot. Osoittimet ja viittaukset luokan olioihin

815338A Ohjelmointikielten periaatteet Harjoitus 5 Vastaukset

Osoitin ja viittaus C++:ssa

815338A Ohjelmointikielten periaatteet Harjoitus 3 vastaukset

815338A Ohjelmointikielten periaatteet

Mallit standardi mallikirjasto parametroitu tyyppi

Ohjelmoinnin perusteet Y Python

Sisällys. 1. Omat operaatiot. Yleistä operaatioista. Yleistä operaatioista

Operaattoreiden uudelleenmäärittely

Olio-ohjelmointi Poikkeusten käsittelystä. 1. Johdanto

Luokassa määriteltävät jäsenet ovat pääasiassa tietojäseniä tai aliohjelmajäseniä. Luokan määrittelyyn liittyvät varatut sanat:

14. Poikkeukset 14.1

Sisällys. 14. Poikkeukset. Johdanto. Johdanto

1. Omat operaatiot 1.1

11. Javan toistorakenteet 11.1

12 Mallit (Templates)

T Olio-ohjelmointi Osa 3: Luokka, muodostin ja hajotin, this-osoitin Jukka Jauhiainen OAMK Tekniikan yksikkö 2010

Metodit. Metodien määrittely. Metodin parametrit ja paluuarvo. Metodien suorittaminen eli kutsuminen. Metodien kuormittaminen

Sisällys. 14. Poikkeukset. Johdanto. Johdanto

T Olio-ohjelmointi Osa 5: Periytyminen ja polymorfismi Jukka Jauhiainen OAMK Tekniikan yksikkö 2010

Poikkeustenkäsittely

14. Poikkeukset 14.1

Kääntäjän virheilmoituksia

Lohkot. if (ehto1) { if (ehto2) { lause 1;... lause n; } } else { lause 1;... lause m; } 15.3

Sisällys. 15. Lohkot. Lohkot. Lohkot

Olio-ohjelmointi Virhetilanteiden käsittely

Luokat. Luokat ja olio-ohjelmointi

C++11 Syntaksi. Jari-Pekka Voutilainen Jari-Pekka Voutilainen: C++11 Syntaksi

Lohkot. if (ehto1) { if (ehto2) { lause 1;... lause n; } } else { lause 1;... lause m; } 16.3

C++ rautaisannos. Kolme tapaa sanoa, että tulostukseen käytetään standardikirjaston iostreamosassa määriteltyä, nimiavaruuden std oliota cout:

Sisällys. 11. Javan toistorakenteet. Laskurimuuttujat. Yleistä

11/20: Konepelti auki

12. Javan toistorakenteet 12.1

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op. Poikkeukset ja tietovirrat: Virhetilanteiden ja syötevirtojen käsittely

12. Javan toistorakenteet 12.1

ITKP102 Ohjelmointi 1 (6 op)

Ohjelmoinnin perusteet Y Python

Olion elinikä. Olion luominen. Olion tuhoutuminen. Olion tuhoutuminen. Kissa rontti = null; rontti = new Kissa();

15. Ohjelmoinnin tekniikkaa 15.1

Tehtävä 1. TL5302 Olio-ohjelmointi Koe Malliratkaisuja. Tässä sekä a)- että b)-kohdan toimiva ratkaisu:

Periytymisen käyttö C++:ssa

11. oppitunti III. Viittaukset. Osa. Mikä on viittaus?

C-kielessä taulukko on joukko peräkkäisiä muistipaikkoja, jotka kaikki pystyvät tallettamaan samaa tyyppiä olevaa tietoa.

Tietueet. Tietueiden määrittely

Harjoitus Olkoon olemassa luokat Lintu ja Pelikaani seuraavasti:

Geneeriset luokat. C++ - perusteet Java-osaajille luento 6/7: Template, tyyppi-informaatio, nimiavaruudet. Geneerisen luokan käyttö.

815338A Ohjelmointikielten periaatteet Harjoitus 2 vastaukset

Harjoitustyö: virtuaalikone

Sisältö. 2. Taulukot. Yleistä. Yleistä

Python-ohjelmointi Harjoitus 5

Sisällys. 12. Näppäimistöltä lukeminen. Yleistä. Yleistä

Sisällys. 16. Lohkot. Lohkot. Lohkot

IDL - proseduurit. ATK tähtitieteessä. IDL - proseduurit

ATK tähtitieteessä. Osa 3 - IDL proseduurit ja rakenteet. 18. syyskuuta 2014

13 Operaattoreiden ylimäärittelyjä

ITKP102 Ohjelmointi 1 (6 op)

4. Luokan testaus ja käyttö olion kautta 4.1

2. Lisää Java-ohjelmoinnin alkeita. Muuttuja ja viittausmuuttuja (1/4) Muuttuja ja viittausmuuttuja (2/4)

Chapel. TIE Ryhmä 91. Joonas Eloranta Lari Valtonen

Esimerkkiprojekti. Mallivastauksen löydät Wroxin www-sivuilta. Kenttä Tyyppi Max.pituus Rajoitukset/Kommentit

Sisällys. 12. Javan toistorakenteet. Yleistä. Laskurimuuttujat

15. Ohjelmoinnin tekniikkaa 15.1

ITKP102 Ohjelmointi 1 (6 op)

Ohjelmointi funktioiden avulla

ITKP102 Ohjelmointi 1 (6 op)

Yleistä. Nyt käsitellään vain taulukko (array), joka on saman tyyppisten muuttujien eli alkioiden (element) kokoelma.

Valinnat ja päätökset

Javan perusteita. Janne Käki

1. Mitä tehdään ensiksi?

Ohjelmointi 2. Jussi Pohjolainen. TAMK» Tieto- ja viestintäteknologia , Jussi Pohjolainen TAMPEREEN AMMATTIKORKEAKOULU

Ohjelmointi 1 Taulukot ja merkkijonot

Ohjelmassa muuttujalla on nimi ja arvo. Kääntäjä ja linkkeri varaavat muistilohkon, jonne muuttujan arvo talletetaan.

Java-kielen perusteet

Ohjelmoinnin perusteet Y Python

Koottu lause; { ja } -merkkien väliin kirjoitetut lauseet muodostavat lohkon, jonka sisällä lauseet suoritetaan peräkkäin.

12. Näppäimistöltä lukeminen 12.1

Poikkeusten käsittely

Taulukot. Jukka Harju, Jukka Juslin

Java kahdessa tunnissa. Jyry Suvilehto

Ohjelmoinnin perusteet Y Python

Olio-ohjelmoinnissa luokat voidaan järjestää siten, että ne pystyvät jakamaan yhteisiä tietoja ja aliohjelmia.

Ohjelmoinnin peruskurssi Y1

Sisältö. 22. Taulukot. Yleistä. Yleistä

14. Hyvä ohjelmointitapa 14.1

Pong-peli, vaihe Aliohjelman tekeminen. Muilla kielillä: English Suomi. Tämä on Pong-pelin tutoriaalin osa 3/7. Tämän vaiheen aikana

Ohjelmoinnin perusteet Y Python

Sisällys. 7. Oliot ja viitteet. Olion luominen. Olio Java-kielessä

Osoittimet. Mikä on osoitin?

7. Oliot ja viitteet 7.1

Perusteet. Pasi Sarolahti Aalto University School of Electrical Engineering. C-ohjelmointi Kevät Pasi Sarolahti

Sisällys. Metodien kuormittaminen. Luokkametodit ja -attribuutit. Rakentajat. Metodien ja muun luokan sisällön järjestäminen. 6.2

Tutoriaaliläsnäoloista

Ohjelmassa henkilön etunimi ja sukunimi luetaan kahteen muuttujaan seuraavasti:

Perusteet. Pasi Sarolahti Aalto University School of Electrical Engineering. C-ohjelmointi Kevät Pasi Sarolahti

Olio-ohjelmointi Syntaksikokoelma

C++11 lambdat: [](){} Matti Rintala

Transkriptio:

Ohjelman virheet ja poikkeusten käsittely 17 Ohjelman virheet ja poikkeusten käsittely Poikkeukset ovat tapa ilmoittaa virheistä ja odottamattomista tilanteista C++-ohjelmassasi. Poikkeusten käyttö virheiden ilmoittamisessa ei ole pakollista ja joskus onkin mukavampaa käsitellä ne jollain toisella tavalla. On kuitenkin tärkeää ymmärtää, miten poikkeukset toimivat, koska niitä voi muodostua kielen standardiominaisuuksien käytössä, kuten operaattoreiden new ja dynamic_cast käytössä. Poikkeuksia käytetään runsaasti myös standardikirjastossa. Tämän luvun aiheita ovat: Mikä on poikkeus Miten poikkeuksia käytetään ilmoittamaan ohjelman virhetilanteista Miten poikkeukset käsitellään Mitkä poikkeukset on määritelty standardikirjastossa Miten voidaan rajoittaa minkä tyyppisiä poikkeuksia funktio voi muodostaa Miten käsitellään poikkeukset, jotka on muodostettu muodostinfunktiossa Miten muodostettu poikkeus voi vaikuttaa luokan tuhoajafunktioon Virheiden käsittely Virheiden käsittely on olennainen osa onnistunutta ohjelmointia. Ohjelman tulee pystyä käsittelemään mahdolliset virheet ja epänormaalit tapahtumat. Tämä saattaa vaatia enemmän työtä kuin sen koodin kirjoittaminen, joka suoritetaan asioiden mennessä niin kuin niiden pitäisikin. Virheen käsittelevän koodin laatu ratkaisee, kuinka vakaa ohjelmasi on ja näyttelee usein pääosaa ohjelman käyttäjäystävälliseksi tekemisessä. Sillä on myös suuri vaikutus siihen, miten helppoa on virheiden korjaus. Kaikki virheet eivät ole samanarvoisia ja virheen luonne määrää sen, miten se on parasta käsitellä ohjelmassasi. Usein virheet kannattaa käsitellä heti siellä missä ne tapahtuvatkin. Ajatellaan esimerkiksi syötteen lukua näppäimistöltä: väärän merkin kirjoittaminen saa aikaan virheellisen 669

C++ Ohjelmoijan käsikirja syötteen, mutta se ei ole varsinaisesti vakava ongelma. Virheellinen syöte on yleensä varsin helppo havaita ja kaikkein paras tapa on yleensä hylätä syöte ja pyytää käyttäjää syöttämään se uudelleen. Tässä tapauksessa virheen käsittelevä koodi on rakennettu syötteen lukevaan koodiin. Vakavammistakin virhetilanteista on mahdollista toipua siten, että se ei haittaa ohjelman muuta toimintaa. Kun virhe havaitaan funktiossa, on usein paras palauttaa jonkinlainen virhekoodi kutsujalle, jolloin kutsuja voi päättää, miten virhetilanne on paras käsitellä. Poikkeukset mahdollistavat uuden tavan käsitellä virheitä - ne eivät korvaa edellä kuvattuja käsittelytapoja. Poikkeusten suurin hyöty on siinä, että niiden avulla virheen käsittelevä koodi on aivan erillään itse virheen aiheuttaneesta koodista. Mitä poikkeukset ovat? Poikkeus on tilapäinen, minkä tahansa tyyppinen olio, jota käytetään virheen ilmoittamiseen. Poikkeus voi olla perustietotyypin olio, kuten int tai char*, mutta yleensä se on erityisesti tähän tarkoitukseen määritellyn luokan olio. Poikkeusolion tarkoitus on välittää tietoa poikkeuksen käsittelevälle koodille kohdasta, jossa poikkeus tapahtui. Tämä tapahtuu parhaiten luokan oliona. Kun ohjelmassasi tapahtuu virhe, voit kertoa virheestä muodostamalla poikkeuksen. Koodi, joka muodostaa poikkeuksen, täytyy kirjoittaa aaltosulkeilla ympäröityyn lohkoon, -lohkoon. Jos lause, joka ei ole -lohkossa, muodostaa poikkeuksen, ohjelman suoritus päättyy. Palaamme tähän hetken kuluttua. -lohkon perässä on yksi tai useampia catch-lohkoja. Kukin catch-lohko sisältää koodin, joka käsittelee tietyn tyyppisen poikkeuksen; tästä syystä catch-lohkoa kutsutaan usein virheen käsittelijäksi. Eli jos muodostat poikkeuksia virheen tapahtuessa, koodi, joka käsittelee virhetilanteen, on catch-lohkoissa, täydellisesti erotettuna koodista, joka suoritetaan, kun kaikki menee oikein. Koodi, joka käsittelee normaalit tapahtumat, on täydellisesti erotettu koodista, joka käsittelee epänormaalit tapahtumat. Koodi, joka saattaa muodostaa poikkeuksia, tulee kirjoittaa - lohkoon. Kun poikkeus on muodostettu, catch-lohkot tutkitaan järjestyksessä. Ohjelman suoritus siirtyy ensimmäiseen catch- lohkoon, jonka parametri vastaa muodostetun poikkeuksen tyyppiä. Tarvittaessa tehdään automaattinen muunnos. // Koodi, joka saattaa muodostaa poikkeuksia catch(1. poikkeustyypin määrittelevä parametri) // Koodi, joka käsittelee poikkeuksen catch(2. poikkeustyypin määrittelevä parametri) // Koodi, joka käsittelee poikkeuksen catch(n. poikkeustyypin määrittelevä parametri) // Koodi, joka käsittelee poikkeuksen 670

Ohjelman virheet ja poikkeusten käsittely Kuten kaavio havainnollistaa, -lohko on tavallinen aaltosulkeiden välissä oleva lohko, jonka edessä on -avainsana. Joka kerta kun -lohko suoritetaan, se voi muodostaa erilaisia poikkeuksia. Täten sen perässä voi olla erilaisia catch-lohkoja, joista jokainen käsittelee erilaisen poikkeuksen. Poikkeuksen tyyppi kirjoitetaan catch-avainsanan perässä oleviin sulkeisiin. catchavainsana puolestaan sijoitetaan catch-lohkon aaltosulkeiden eteen. Tietty catch-lohko suoritetaan vain, jos sen tyyppiä vastaava poikkeus on muodostettu. Jos lohko ei muodosta poikkeusta, mitään catch-lohkoa ei suoriteta. Et voi hypätä -lohkoon esimerkiksi goto-käskyllä. Ainut tapa suorittaa -lohko on aloittaa sen alusta, lohkon avaavan aaltosulkeen jälkeen tulevasta ensimmäisestä lauseesta. Poikkeuksen muodostaminen Nyt on korkea aika jo muodostaa poikkeuksia, joten katsotaan, mitä tapahtuu, kun niin teemme! Vaikka sinun tulisikin aina käyttää luokkaolioita poikkeuksissa (kuten teemme myöhemmin tässä luvussa), aloitamme perustietotyypeillä, koska näin koodi pysyy hyvin yksinkertaisena. Poikkeus muodostetaan throw-lauseella, joka kirjoitetaan throw-avainsanan avulla. Seuraavassa on esimerkki poikkeuksen muodostamisesta: // Koodi, joka voi muodostaa poikkeuksen if(testi > 5) throw "testi on suurempi kuin 5"; // Muodostetaan poikkeus // Tämä koodi suoritetaan, jos poikkeusta ei muodosteta catch(const char* viesti) // Koodi, joka käsittelee poikkeuksen // Tämä koodi suoritetaan, jos poikkeuksen tyyppi on char* // tai 'const char*' cout << viesti << endl; Jos muuttujan testi arvo on suurempi kuin 5, throw-lause muodostaa poikkeuksen. Tällöin poikkeus on testi on suurempi kuin 5. Ohjelman suoritus siirtyy välittömästi ulos lohkosta ensimmäiseen poikkeuksen käsittelijään, jonka tyyppi vastaa muodostetun poikkeuksen tyyppiä: const char*. Tässä meillä on vain yksi käsittelijä, joka sattuu sopivasti käsittelemään const char* -tyyppiset poikkeukset, joten catch-lohkon koodi suoritetaan ja poikkeus tulostetaan.! Kääntäjä ei itse asiassa huomioi const-avainsanaa vertaillessaan poikkeuksen tyyppiä catch-parametrin tyyppiin. Käsittelemme tätä tarkemmin myöhemmin. 671

C++ Ohjelmoijan käsikirja Kokeile itse - Poikkeusten muodostaminen ja käsittely Kokeillaan poikkeuksia esimerkin avulla, jossa muodostamme int- ja const char* -tyyppisiä poikkeuksia. Lisäämme koodiin muutamia tulostuslauseita, jotka helpottavat suorituksen seuraamista: // Esimerkki 17.1 Poikkeusten muodostaminen ja käsittely #include <iostream> using namespace std; int main() cout << endl; for(int i = 0 ; i < 7 ; i++) if(i < 3) throw i; cout << " i, ei muodostettu - arvo on " << i << endl; if(i > 5) throw "Tässä on toinen!"; cout << " Try-lohkon loppu." << endl; catch(const int i) cout << " poikkeus i - arvo on " << i << endl; catch(const char* pviesti) cout << " \"" << pviesti << "\"-poikkeus" << endl; cout << "Silmukan loppu (catch-lohkojen perässä) - i on " << i << endl; 672 return 0; Esimerkin tulostus näyttää seuraavalta: poikkeus i - arvo on 0 Silmukan loppu (catch-lohkojen perässä) - i on 0 poikkeus i - arvo on 1 Silmukan loppu (catch-lohkojen perässä) - i on 1 poikkeus i - arvo on 2 Silmukan loppu (catch-lohkojen perässä) - i on 2 i, ei muodostettu - arvo on 3 Try-lohkon loppu. Silmukan loppu (catch-lohkojen perässä) - i on 3 i, ei muodostettu - arvo on 4 Try-lohkon loppu. Silmukan loppu (catch-lohkojen perässä) - i on 4 i, ei muodostettu - arvo on 5 Try-lohkon loppu. Silmukan loppu (catch-lohkojen perässä) - i on 5

Ohjelman virheet ja poikkeusten käsittely i, ei muodostettu - arvo on 6 "Tässä on toinen!"-poikkeus Silmukan loppu (catch-lohkojen perässä) - i on 6 Kuinka se toimii for-silmukassa on -lohko, jossa muodostetaan int-tyyppinen poikkeus, jos i (silmukan laskuri) on pienempi kuin 3, ja const char* -tyyppinen poikkeus, jos i on suurempi kuin 5: if(i < 3) throw i; cout << " i, ei muodostettu - arvo on " << i << endl; if(i > 5) throw "Tässä on toinen!"; cout << " Try-lohkon loppu." << endl; Poikkeuksen muodostaminen siirtää ohjelman suorituksen välittömästi ulos -lohkosta, joten lohkon lopussa oleva tulostuslause suoritetaan vain, jos mitään poikkeusta ei ole muodostettu. Tulostuksesta huomaat, että näin todella tapahtuu. Tulostuslause suoritetaan vain, jos i:n arvo on 3, 4 tai 5. Kaikilla muilla i:n arvoilla muodostetaan poikkeus, eikä tulostuslausetta suoriteta. Ensimmäinen catch-lohko on heti -lohkon perässä: catch(const int i) cout << " poikkeus i - arvo on " << i << endl; -lohkon käsittelijöiden täytyy olla välittömästi -lohkon perässä. Jos sijoitat muuta koodia -lohkon ja ensimmäisen catch-lohkon väliin, tai catch-lohkojen väliin, ohjelma ei käänny. Tämä catch-lohko käsittelee int-tyyppiset poikkeukset ja tulostuksesta huomaat, että se suoritetaan, kun ensimmäinen throw-lause suoritetaan. Tulostuksesta näet myös, että seuraavaa catch-lohkoa ei suoriteta tässä tapauksessa. Kun tämä käsittelijä on suoritettu, ohjelman suoritus siirtyy silmukan viimeiseen lauseeseen. Seuraava käsittelijä käsittelee char*-tyyppiset poikkeukset: catch(const char* pviesti) cout << " \"" << pviesti << "\"-poikkeus" << endl; Kun muodostamme poikkeuksen Tässä on toinen!, ohjelman suoritus siirtyy throw-lauseesta suoraan tähän käsittelijään - ensimmäinen käsittelijä jätetään väliin. Jos poikkeusta ei muodosteta, kumpaakaan catch-lohkoa ei suoriteta. Voit kirjoittaa tämän käsittelijän myös ensimmäiseksi ja ohjelma toimii aivan samaan tapaan. Tässä tapauksessa käsittelijöiden järjestyksellä ei ole merkitystä, mutta näin ei aina ole. Näet myöhemmin tässä luvussa tilanteita, joissa käsittelijöiden järjestyksellä on merkitystä. 673

C++ Ohjelmoijan käsikirja Seuraava lause kertoo silmukan kierroksen päättymisestä: cout << "Silmukan loppu (catch-lohkojen perässä) - i on " << i << endl; Tämä koodi suoritetaan riippumatta siitä, onko poikkeuksen käsittelijä suoritettu vai ei. Kuten huomaat, poikkeuksen muodostaminen ei lopeta ohjelman suoritusta - ellet tietysti halua niin tapahtuvan. Jos pystyt korjaamaan poikkeuksen aiheuttaneen ongelman, ohjelman suoritus voi jatkua. Poikkeusten käsittely Nyt, kun olet nähnyt esimerkin, sinulla on jo varsin hyvä kuva tapahtumien järjestyksestä, kun poikkeus muodostetaan. Muitakin asioita tapahtuu kuitenkin taustalla ja oletkin jo saattanut arvata niistä joitakin, jos olet miettinyt, miten ohjelman suoritus siirtyy -lohkosta catchlohkoon. throw/catch-lauseiden suoritusjärjestystä havainnollistetaan alla olevassa kaaviossa: throw poikkeus; catch(tyyppi1 ex) catch(tyyppin ex) catch(tyyppiviimeinen ex) 1.throw-lauseketta käytetään alustamaan väliaikainen olio temp seuraavasti: PoikkeusTyyppi temp(poikkeus); 4. Poikkeuksen kopiota käytetään alustamaan parametri seuraavasti: tyyppin ex(temp); ja suoritus siirtyy käsittelijään. 3.Valitaan ensimmäinen käsittelijä, jonka parametrin tyyppi vastaa poikkeusta. 5.Jos käsittelijän koodi ei toisin määrää, suoritus jatkuu -lohkon catch-lohkojen jälkeen tulevasta ensimmäisestä lauseesta. 2.Kaikkien -lohkossa määriteltyjen automaattisten olioiden tuhoajafunktioita kutsutaan. -lohko on lauselohko, ja tiedätkin jo, että lauselohko määrittelee aina näkyvyysalueen. Poikkeuksen muodostaminen saa aikaan suorituksen välittömän poistumisen -lohkosta, jolloin -lohkossa jo esitellyt automaattiset oliot tuhotaan. Yksikään niistä ei ole enää olemassa, kun käsittelijää suoritetaan. Tämä on erittäin tärkeää - tämähän tarkoittaa sitä, että et saa muodostaa poikkeusoliota, joka on osoitin olioon, joka on paikallinen -lohkossa. Tämä on myös syy siihen, miksi poikkeusolio kopioidaan. 674 Poikkeusolio tulee olla tyyppiä, joka voidaan kopioida. Sellaisen luokan oliota, jonka kopiomuodostin on yksityinen, ei voida käyttää poikkeuksena.

Ohjelman virheet ja poikkeusten käsittely Koska throw-lauseketta käytetään väliaikaisen olion alustamiseen - eli luo kopion poikkeuksesta - voit muodostaa poikkeusolioita, jotka ovat paikallisia -lohkossa. Muodostetun poikkeusolion kopiota käytetään sitten alustamaan catch-lohkon parametri, kun käsittelijä on valittu. catch-lohko on myös lauselohko, joten kaikki sen paikalliset automaattiset oliot (parametrit mukaan lukien) tuhotaan, kun catch-lohkon suoritus päättyy. Ellei goto- tai return-lausetta käytetä catch-lohkosta poistumiseen, ohjelman suoritus jatkuu kyseisen -lohkon perässä olevaa viimeistä catch-lohkoa seuraavasta lauseesta. Kun poikkeuksen käsittelijä on valittu ja ohjelman suoritus on siirtynyt käsittelijään, muodostettu poikkeus pidetään käsiteltynä. Näin myös silloin, jos jätät catch-lohkon tyhjäksi eikä se tee mitään. Käsittelemättömät poikkeukset Jos -lohkossa muodostettua poikkeusta ei käsitellä missään catch-lohkoissa, kutsutaan (ellei olla sisäkkäisissä -lohkoissa, joita käsittelemme hetken kuluttua) standardikirjaston terminate()-funktiota. Tämä funktio kutsuu ennalta määriteltyä oletusarvoista suorituksen päättymisfunktiota, joka puolestaan kutsuu standardikirjaston funktiota abort(). Käsittelemätön poikkeus saa aikaan funktion terminate()kutsun. set_terminate(pomapäättymisfunktio); joka kutsuu korvaa Oletusarvoinen suorituksen päättymisfunktio joka kutsuu abort() Funktio abort() päättää koko ohjelman suorituksen välittömästi. Toisin kuin exit(), se ei kutsu yhdenkään staattisen olion tuhoajafunktiota. Oletusarvoisen suorituksen päättymisfunktion oletustoiminta voi olla tuhoisaa joissakin tapauksissa - se saattaa esimerkiksi jättää tiedostot epätasapainoiseen tilaan tai puhelinlinja saattaa jäädä auki. Tällaisissa tilanteissa haluat varmastikin, että asiat lopetetaan oikein. Tämän voit tehdä korvaamalla oletusarvoisen suorituksen päättymisfunktion omalla versiollasi. Korvataksesi oletusarvoisen suorituksen päättymisfunktion, kutsut standardikirjaston funktiota set_terminate(), jonka parametri on terminate_handler-tyyppinen ja palauttaa saman tyyppisen paluuarvon. Tämä tyyppi on määritelty standardiotsikkotiedostossa exception seuraavasti: typedef void (*terminate_handler)(); 675

C++ Ohjelmoijan käsikirja terminate_handler on osoitin funktioon, jolla ei ole lainkaan parametrejä eikä palauta mitään paluuarvoa, eli korvaavan funktion tulee olla tämän tyyppinen. Voit tehdä mitä haluat omassa versiossasi suorituksen päättymisfunktiosta, mutta se ei saa palata takaisin ohjelmaan - sen tulee lopulta lopettaa ohjelman suoritus. Oma määrittely funktiostasi saattaisi näyttää vaikka seuraavalta: void omapaattymisfunktio() // Tee tarvittavat toimet, jotta kaikki asiat on lopetettu oikein exit(1); Standardikirjaston exit()-funktion kutsuminen on parempi tapa lopettaa ohjelman suoritus kuin abort()-funktion kutsuminen. exit()-funktion kutsuminen varmistaa, että globaalien olioiden tuhoajafunktiota kutsutaan ja kaikki avoinna olevat syöttö/tulostusvirrat tyhjennetään sekä suljetaan tarvittaessa. Kaikki standardikirjaston väliaikaiset tiedostot poistetaan. exit()-funktiolle välittämäsi kokonaislukuparametri palautetaan käyttöjärjestelmälle tilakoodina. Nollasta poikkeava arvo tarkoittaa ohjelman epänormaalia päättymistä. Yllä olevan funktion asetat suorituksen päättymisfunktioksi seuraavalla lauseella: terminate_handler pvanhafunktio = set_terminate(omapaattymisfunktio); Paluuarvo on osoitin edelliseen päättymisfunktioon, joten voit tarvittaessa palauttaa sen myöhemmin. Kun kutsut set_terminate()-funktiota ensimmäisen kerran, paluuarvo on osoitin oletusarvoiseen suorituksen päättymisfunktioon. Jokainen tämän funktion kutsu palauttaa voimassa olleen päättymisfunktion osoittimen. Tämä tarkoittaa sitä, että voit pitää tietyssä ohjelman osassa voimassa omaa päättymisfunktiotasi ja osassa ohjelmaa oletusarvoista päättymisfunktiota. Voit luonnollisestikin asettaa eri päättymisfunktion ohjelman eri osissa, jotta ohjelman suoritus voidaan päättää asianmukaisesti. Jos ohjelmasi käsittelee esimerkiksi tietokantaa, sinun tulee varmistaa, että tietokanta sammutetaan oikein, kun vakava virhe tapahtuu. Tätä varten voit kirjoittaa oman päättymisfunktion. Ohjelman toinen osa voi käyttää modeemia tiedonsiirtoon, jolloin mitä todennäköisimmin haluat sulkea tiedonsiirtolinjan. Eri tilanteisiin sopivaa päättymisfunktiota voidaan käyttää aina tarvittaessa. 676 Poikkeuksen muodostava koodi Kuten tämän luvun alkupuolella mainittiin, -lohko sisältää koodin, joka voi aiheuttaa poikkeuksen muodostumisen. Tämä ei kuitenkaan tarkoita sitä, että poikkeuksen muodostavan koodin tulisi fyysisesti olla -lohkon aaltosulkeiden sisällä. Sen tulee olla loogisesti -lohkon sisällä. Tämä tarkoittaa sitä, että jos funktiota kutsutaan -lohkosta, funktion mahdollisesti muodostama poikkeus saadaan kiinni -lohkon catch-lohkoissa. Seuraava kaavio havainnollistaa tätä tilannetta.

Ohjelman virheet ja poikkeusten käsittely fun1(); Täällä muodostettu poikkeus normaali paluu void fun1() fun3(); void fun3() fun2(); catch(exceptiontyyppi ex) voidaan käsitellä täällä voidaan käsitellä täällä normaali paluu voidaan käsitellä täällä void fun2() normaali paluu Täällä muodostettu poikkeus Täällä muodostettu poikkeus -lohkossa on kaksi funktion kutsua, fun1() ja fun2(). Funktioissa mahdollisesti muodostuvat exceptiontyyppi-tyyppiset poikkeukset saadaan kiinni -lohkon perässä olevassa catchlohkossa. Funktiossa muodostettu poikkeus, jota ei käsitellä funktiossa, voidaan välittää kutsuvalle funktiolle. Jos sitä ei käsitellä sielläkään, se voidaan edelleen välittää ylemmälle kutsutasolle - tätä havainnollistetaan kaaviossa fun3()-funktiossa muodostetulla poikkeuksella. Jos poikkeus saavuttaa kutsutason, jossa ei enää ole catch-käsittelijöitä eikä poikkeusta ole vieläkään käsitelty, kutsutaan suorituksen päättymisfunktiota ohjelman päättämiseksi. Jos samaa funktiota kutsutaan ohjelman eri osista, funktion koodin muodostama poikkeus voidaan käsitellä eri catch-lohkoissa suorituksen eri aikana. Seuraava esimerkki havainnollistaa tällaista tilannetta. fun1(); catch(exceptiontyyppi ex) fun1(); catch(exceptiontyyppi ex) Kun tätä kutsua suoritetaan, funktion fun1() koodi on loogisesti ylemmässä - lohkossa. Kun tätä kutsua suoritetaan, funktion fun1() koodi on loogisesti alemmassa - lohkossa. void fun1() 677

C++ Ohjelmoijan käsikirja Kun funktio on suorituksessa ensimmäisen -lohkon kutsun seurauksena, kaikki fun1()- funktion muodostamat exceptiontyyppi-tyyppiset poikkeukset voidaan käsitellä tämän lohkon catch-lohkossa. Kun sitä kutsutaan toisesta -lohkosta, tämän -lohkon catch-lohko käsittelee muodostetut exceptiontyyppi-tyyppiset poikkeukset. Tästä huomaat varmastikin, että voit valita kuhunkin tilanteeseen sopivan suorituksen päättymisfunktion. Jos haluat, voit käsitellä kaikki ohjelmassa tapahtuvat poikkeukset kirjoittamalla main()-funktion koodin -lohkoon ja lisäämällä ohjelmaan tarpeellisen määrän catchlohkoja. Sisäkkäiset -lohkot Voit kirjoittaa -lohkon toisen -lohkon sisälle. Niillä jokaisella on omat catch-lohkonsa, jotka käsittelevät siinä muodostuvat poikkeukset. -lohkon catch-lohkot voivat käsitellä vain kyseisen -lohkon poikkeuksia. Tätä havainnollistetaan seuraavassa kaaviossa: // ulompi -lohko // sisempi -lohko catch(exceptiontyyppi ex) catch(exceptiontyyppi ex) Tämä käsittelijä voi käsitellä sisemmässä -lohkossa muodostettuja poikkeuksia. Tämä käsittelijä voi käsitellä kaikkialla ulommassa -lohkossa muodostettuja poikkeuksia, sekä sisemmässä lohkossa käsittelemättömiä poikkeuksia. 678 Kaaviossa on yksi käsittelijä kumpaakin -lohkoa kohti, mutta niitä voi olla useitakin. Kun sisemmän -lohkon koodi muodostaa poikkeuksen, sen käsittelijät saavat ensin mahdollisuuden käsitellä sen. Jokainen käsittelijä tarkistetaan, vastaako sen tyyppi poikkeuksen tyyppiä. Jos yksikään tyyppi ei vastaa poikkeuksen tyyppiä, ulomman -lohkon käsittelijät saavat mahdollisuuden käsitellä poikkeuksen. Voit sijoittaa -lohkoja tällä tavalla sisäkkäin niin monta kuin ohjelmasi vaatii. Kun poikkeus muodostetaan ulomman -lohkon koodissa, tämän -lohkon catch-käsittelijät käsittelevät sen, vaikka poikkeuksen muodostava lause olisi ennen sisempää -lohkoa. Sisemmän -lohkon käsittelijät eivät milloinkaan voi käsitellä ulomman -lohkon koodissa muodostettua poikkeusta. Luonnollisestikin kummankin -lohkon koodi voi kutsua funktioita. Kun funktio on tällöin suorituksessa, sen koodi on loogisesti siinä -lohkossa, josta sitä kutsuttiin. Osa tai kaikki funktion koodista voi myöskin olla omassa -lohkossaan, joka on funktiota kutsuvan lohkon sisällä.

Ohjelman virheet ja poikkeusten käsittely Kokeile itse - Sisäkkäiset -lohkot Tämä kuulostaa sanallisessa muodossa varsin monimutkaiselta, mutta käytännössä se on paljon helpompaa. Voimme rakentaa yksinkertaisen esimerkin, jossa muodostamme poikkeuksen ja katsomme, missä se käsitellään. Jälleen kerran muistutamme, että esimerkki on vain havainnollistamista varten, eli muodostamme int- ja long-tyyppisiä poikkeuksia. Ohjelman koodi, joka havainnollistaa sekä sisäkkäisiä -lohkoja että poikkeusten muodostamista, on seuraava: // Esimerkki 17.2 Poikkeusten muodostaminen sisäkkäisissä -lohkoissa #include <iostream> using namespace std; void muodostase(int i) throw i; int main() for(int i = 0 ; i <= 5 ; i++) cout << endl << "ulompi : "; if(i == 0) throw i; // Muodostaa poikkeuksen // Muodostaa int-poikkeuksen if(i == 1) muodostase(i); cout << endl << " sisempi : "; if(i == 2) throw static_cast<long>(i); // Kutsuu funktiota, joka muodostaa // poikkeuksen // Sisempi -lohko // Muodostaa long-poikkeuksen if(i == 3) muodostase(i); // Kutsuu funktiota, joka muodostaa poikkeuksen // Sisemmän -lohkon loppu catch(int n) cout << endl << "Käsitellään sisemmän -lohkon int. " << "Poikkeus " << n; cout << endl << " ulompi : "; if(i == 4) throw i; // Muodosta int-poikkeus muodostase(i); // Kutsuu funktiota, joka muodostaa poikkeuksen catch(int n) cout << endl << "Käsitellään ulomman -lohkon int. " << "Poikkeus " << n; 679

C++ Ohjelmoijan käsikirja catch(long n) cout << endl << "Käsitellään ulomman -lohkon long. " << "Poikkeus " << n; cout << endl; return 0; Ohjelman tulostus on: ulompi : Käsitellään ulomman -lohkon int. Poikkeus 0 ulompi : Käsitellän ulomman -lohkon int. Poikkeus 1 ulompi : sisempi : Käsitellään ulomman -lohkon long. Poikkeus 2 ulompi : sisempi : Käsitellään sisemmän -lohkon int. Poikkeus 3 ulompi : Käsitellään ulomman -lohkon int. Poikkeus 3 ulompi : sisempi : ulompi : Käsitellään ulomman -lohkon int. Poikkeus 4 ulompi : sisempi : ulompi : Käsitellään ulomman -lohkon int. Poikkeus 5 Kuinka se toimii Funktio muodostase() muodostaa parametrinsä mukaisen poikkeuksen. Jos kutsut tätä funktiota -lohkon ulkopuolelta, ohjelman suoritus päättyy välittömästi, koska poikkeusta ei käsitellä ja oletusarvoista suorituksen päättymisfunktiota kutsutaan. Kaikki poikkeukset muodostetaan for-silmukassa. Silmukassa määräämme ensin milloin ja minkä tyyppisen poikkeuksen muodostamme. Tämä tehdään tutkimalla silmukkamuuttujan i arvoa peräkkäisillä if-lauseilla. Vähintään yksi poikkeus muodostetaan silmukan jokaisella kierroksella. Saapuminen kuhunkin -lohkoon tulostetaan, ja koska jokaisella poikkeuksella on oma arvonsa, näemme selvästi, missä kukin poikkeus muodostetaan ja käsitellään. Ensimmäinen poikkeus muodostetaan ulommasta -lohkosta, kun silmukkamuuttujan i arvo on 0: if(i == 0) throw i; // Muodostaa int-poikkeuksen 680 Huomaat tulostuksesta, että ulomman -lohkon perässä oleva catch-lohko, joka käsittelee inttyyppisiä poikkeuksia, käsittelee tämän poikkeuksen. Sisemmän -lohkon catch-lohkolla ei ole tässä mitään merkitystä, koska se voi käsitellä vain sisemmästä -lohkosta muodostettuja poikkeuksia.

Ohjelman virheet ja poikkeusten käsittely Seuraava poikkeus muodostetaan ulommassa -lohkossa kutsumalla funktiota muodostase(): if(i == 1) muodostase(i); // Kutsuu funktiota, joka muodostaa poikkeuksen Tämä käsitellään myöskin ulomman -lohkon catch-lohkossa, joka käsittelee int-tyyppiset poikkeukset. Seuraavat kaksi poikkeusta muodostetaan kuitenkin sisemmässä -lohkossa: if(i == 2) throw static_cast<long>(i); // Muodostaa long-poikkeuksen if(i == 3) muodostase(i); // Kutsuu funktiota, joka muodostaa poikkeuksen Ensimmäinen näistä on long-tyyppinen poikkeus. Sisemmälle -lohkolle ei ole catch-lohkoa tämän tyyppiselle poikkeukselle, joten se siirtyy ulompaan -lohkoon, jossa long-tyyppinen catch-lohko käsittelee sen, kuten tulostuksesta huomaat. Toinen poikkeus on int-tyyppinen ja muodostetaan muodostase()-funktion rungossa. Tässä funktiossa ei ole -lohkoa, joten poikkeus siirtyy kohtaan, jossa sitä kutsuttiin sisemmästä -lohkosta. Se käsitellään sitten sisemmän lohkon catch-lohkossa, joka käsittelee int-tyyppiset poikkeukset. Kun jokin sisemmän -lohkon käsittelijä käsittelee poikkeuksen, suoritus jatkuu lopuista ulomman -lohkon lauseista. Eli kun i on 3, saamme tulostuksen sisemmän -lohkon catchlohkosta sekä tulostuksen ulomman -lohkon käsittelijästä, joka käsittelee int-tyyppiset poikkeukset. Jälkimmäinen poikkeus muodostuu sisemmän -lohkon lopussa olevasta funktion muodostase() kutsusta. Lopuksi muodostamme kaksi uutta poikkeusta ulommassa -lohkossa: if(i == 4) throw i; // Muodosta int-poikkeus muodostase(i); // Kutsuu funktiota, joka muodostaa poikkeuksen Ulomman -lohkon int-tyyppiset poikkeukset käsittelevä käsittelijä käsittelee nämä molemmat poikkeukset. Toinen näistä poikkeuksista muodostetaan funktion muodostase() rungossa, ja koska sitä kutsutaan ulommasta -lohkosta, ulomman -lohkon catch-lohko käsittelee sen. Vaikka yksikään näistä poikkeuksista ei ollut kovinkaan realistinen - oikeiden ohjelmien poikkeukset ovat aina luokkaolioita - ne havainnollistivat hyvin poikkeusten muodostamista ja käsittelyä sekä sitä, mitä tapahtuu sisäkkäisissä -lohkoissa. Siirrytään nyt tarkastelemaan lähemmin poikkeuksia, jotka ovat olioita. Luokkaoliot poikkeuksina Voit muodostaa poikkeuksen, joka on millainen tahansa luokkaolio. Pidä kuitenkin mielessä, että poikkeusolion ideana on tuoda käsittelijälle tietoa, missä meni väärin. Tästä syystä on usein järkevää määritellä erityinen poikkeusluokka, joka kuvaa tietyntyyppistä ongelmaa. Tämä on varsin sovelluskohtaista, mutta poikkeusluokkasi sisältää lähes aina jonkinlaisen viestin sekä mahdollisesti jonkinlaisen virhekoodin. Lisäksi poikkeusolio voi sisältää lisätietoa virheen syystä haluamassasi muodossa. 681

C++ Ohjelmoijan käsikirja Voimme määritellä oman poikkeusluokkamme. Kirjoitetaan se nimeltään varsin yleiseen otsikkotiedostoon Ongelmat.h: // Ongelmat.h Poikkeusluokan määrittely #ifndef ONGELMAT_H #define ONGELMAT_H class Ongelma public: Ongelma(const char* pmjono = "Tässä on ongelma") : pviesti(pmjono) const char* mika() const return pviesti; private: const char* pviesti; ; #endif Tämä luokka määrittelee poikkeuksen kuvaavan olion, joka sisältää viestin, että törmättiin ongelmaan. Muodostinfunktion parametrille on määritelty oletusarvo, joten voit käyttää oletusmuodostinfunktiota luodaksesi olion, joka sisältää oletusviestin. Jäsenfunktio mika() palauttaa nykyisen viestin. Koska emme varaa muistia vapaasta muistista, oletuskopiomuodostin riittää tässä tapauksessa. Jotta poikkeusten käsittelyn mekanismi pysyy hallittavana, sinun tulee varmistaa, että poikkeusluokan jäsenfunktiot eivät muodosta poikkeuksia. Myöhemmin tässä luvussa näet, miten voit estää jäsenfunktiota muodostamasta poikkeusta. Kokeillaan, mitä tapahtuu, kun poikkeusolio muodostetaan. Kuten edellistenkin esimerkkien kohdalla muodostamme koodissa suoraan poikkeuksia, jotta voimme seurata, mitä tapahtuu eri tilanteissa. Varmistetaan ensin, että tiedämme, miten poikkeusolio muodostetaan. Kokeile itse - Poikkeusolion muodostaminen Voimme testata poikkeusluokkaamme hyvin yksinkertaisella esimerkillä, joka muodostaa silmukassa poikkeuksia: // Esimerkki 17.3 Muodostetaan poikkeusolio #include <iostream> #include "Ongelmat.h" using namespace std; 682 int main() for(int i = 0 ; i < 2 ; i++) if(i == 0) throw Ongelma(); else throw Ongelma("Kukaan ei ole vielä nähnyt tätä ongelmaa");

Ohjelman virheet ja poikkeusten käsittely catch(const Ongelma& t) cout << endl << "Poikkeus: " << t.mika(); return 0; Tämä tuottaa seuraavan tulostuksen: Poikkeus: Tässä on ongelma Poikkeus: Kukaan ei ole vielä nähnyt tätä ongelmaa Kuinka se toimii Muodostamme for-silmukassa kaksi poikkeusoliota. Ensimmäisen, joka sisältää oletusviestin, muodostaa Ongelma-luokan oletusmuodostinfunktio. Toinen poikkeusolio muodostetaan iflauseen else-osassa ja sisältää viestin, jonka välitämme muodostinfunktiolle parametrinä. catchlohko käsittelee molemmat poikkeusoliot. Pidä mielessä, että poikkeusolio kopioidaan aina kun se muodostetaan, joten jos et määrittele parametriä catch-lohkossa viittaukseksi, se kopioidaan toiseen kertaan - aivan tarpeettomasti. Tapahtumien järjestys poikkeusolion muodostamisessa on seuraava: olio kopioidaan ensiksi (luodaan tilapäinen olio) ja alkuperäinen olio tuhotaan tämän jälkeen, koska suoritus poistuu lohkosta ja olion näkyvyysalue päättyy. Kopio välitetään catch-käsittelijälle - viittauksena, jos parametri on viittausparametri. Jos haluat seurata näitä tapahtumia, lisää Ongelma-luokkaan kopiomuodostin ja tuhoajafunktio. Oikean catch-käsittelijän valinta Aikaisemmin mainitsimme, että -lohkon perässä olevat käsittelijät tutkitaan siinä järjestyksessä kuin ne koodissa ovat. Ensimmäinen käsittelijä, jonka tyyppi vastaa poikkeuksen tyyppiä, suoritetaan. Perustietotyyppejä (ei luokkatyyppejä) olevien poikkeusten kohdalla tyyppien tulee vastata toisiaan täydellisesti. Luokkaolioiden kohdalla voidaan mahdollisesti suorittaa automaattinen muunnos, jotta poikkeuksen tyyppi vastaa käsittelijän tyyppiä. Kun parametrin (käsittelijän) tyyppiä verrataan poikkeuksen tyyppiin, seuraavat ajatellaan vastaaviksi: Parametrin tyyppi on const-määrettä lukuun ottamatta sama kuin poikkeuksen tyyppi. Parametrin tyyppi on poikkeusluokkatyypin suora tai epäsuora kantaluokka tai viittaus poikkeusluokan suoraan tai epäsuoraan kantaluokkaan, kun constmäärettä ei huomioida. Poikkeus ja parametri ovat osoittimia ja poikkeuksen tyyppi voidaan muuntaa automaattisesti parametrin tyyppiin. const-määrettä ei huomioida tässäkään. Edellä luetellut mahdolliset tyypinmuunnokset vaikuttavat siihen, mihin järjestykseen käsittelijät sijoitetaan -lohkossa. Jos sinulla on useita käsittelijöitä poikkeuksille, joiden tyyppi on samassa luokkahierarkiassa, kaikkein alimmalla tasolla olevan periytetyn tyypin tulee olla ensin ja kaikkein ylimmällä tasolla olevan kantatyypin tulee olla viimeisenä. Jos käsittelijät ovat toisin päin, kantaluokkatyypin käsittelijä valitaan aina käsittelemään periytetyn luokan poikkeukset. Eli toisin sanoen, periytetyn tyypin käsittelijää ei suoriteta koskaan. 683

C++ Ohjelmoijan käsikirja Lisätään muutama uusi poikkeus Ongelma-luokan sisältämään otsikkotiedostoon käyttämällä Ongelma-luokkaa niiden kantaluokkana. Ongelmat.h-otsikkotiedosto näyttää lisäyksen jälkeen seuraavalta: // Ongelmat.h Poikkeusluokan määrittely #ifndef ONGELMAT_H #define ONGELMAT_H // Poikkeusten kantaluokka class Ongelma public: Ongelma(const char* pmjono = "Tässä on ongelma"); virtual ~Ongelma(); virtual const char* mika() const; private: const char* pviesti; ; // Periytetty poikkeusluokka class LisaOngelma : public Ongelma public: LisaOngelma(const char* pmjono = "Lisää ongelmia"); ; // Periytetty poikkeusluokka class IsoOngelma : public LisaOngelma public: IsoOngelma(const char* pmjono = "Todella suuri ongelma"); ; #endif Huomaa, että kantaluokan mika()-jäsenfunktio ja tuhoajafunktio ovat esitellyt virtuaalisiksi. Funktio mika() on tämän jälkeen virtuaalinen kaikissa Ongelma-luokasta periytetyissä luokissa. Tässä sillä ei ole juurikaan merkitystä, mutta on hyvä totuttautua tapaan, että kantaluokan tuhoajafunktio esitellään virtuaaliseksi. Oletusviestiä lukuun ottamatta periytetty luokka ei lisää kantaluokkaan mitään. Usein pelkkää eri luokan nimeä käytetään erottamaan eri ongelmat toisistaan. Kun törmätään tietyn tyyppiseen ongelmaan, muodostetaan vain sen tyyppinen poikkeus; luokan sisällön ei tarvitse olla erilainen. Käyttämällä erilaista catch-lohkoa jokaisen luokkatyypin kohdalla voidaan erottaa ongelmat toisistaan. Kolmen luokan jäsenfunktioiden määrittelyt voidaan kirjoittaa Ongelmat.cpp-tiedostoon: // Ongelmat.cpp #include "Ongelmat.h" 684 // Ongelma-luokan muodostinfunktio Ongelma::Ongelma(const char* pmjono) : pviesti(pmjono)

Ohjelman virheet ja poikkeusten käsittely // Ongelma-luokan tuhoajafunktio Ongelma::~Ongelma() // Palauttaa viestin const char* Ongelma::mika() const return pviesti; // LisaOngelma-luokan muodostinfunktio LisaOngelma::LisaOngelma(const char* pmjono) : Ongelma(pMjono) // IsoOngelma-luokan muodostinfunktio IsoOngelma::IsoOngelma(const char* pmjono) : LisaOngelma(pMjono) Voimme nyt kokeilla näitä luokkia esimerkin avulla. Kokeile itse - Hierarkian poikkeusluokkien muodostaminen Tämä ohjelma muodostaa Ongelma-, LisaOngelma- ja IsoOngelma-tyyppiset poikkeukset ja käsittelee ne: // Esimerkki 17.4 Luokkahierarkian poikkeusten muodostaminen #include <iostream> #include "Ongelmat.h" using namespace std; int main() Ongelma ongelma; LisaOngelma lisaongelma; IsoOngelma isoongelma; cout << endl; for(int i = 0 ; i < 7 ; i++) if(i < 3) throw ongelma; if(i < 5) throw lisaongelma; else throw isoongelma; catch(isoongelma& ro) cout << " IsoOngelmaa käsitellään: " << ro.mika() << endl; catch(lisaongelma& ro) cout << " LisaOngelmaa käsitellään: " << ro.mika() << endl; 685

C++ Ohjelmoijan käsikirja catch(ongelma& ro) cout << "Ongelmaa käsitellään: " << ro.mika() << endl; cout << "Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on " << i << endl; cout << endl; return 0; Esimerkin tulostus näyttää seuraavalta: Ongelmaa käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 0 Ongelmaa käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 1 Ongelmaa käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 2 LisaOngelmaa käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 3 LisaOngelmaa käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 4 IsoOngelmaa käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 5 IsoOngelmaa käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 6 Kuinka se toimii Kun olemme luoneet yhden olion jokaisesta kolmesta luokasta, suoritamme for-silmukan, joka sisältää seuraavan -lohkon: if(i < 3) throw ongelma; if(i < 5) throw lisaongelma; else throw isoongelma; 686 Silmukkamuuttujan i arvoille, jotka ovat pienempiä kuin 3, muodostamme Ongelma-tyyppisen poikkeuksen. Kun i on 3 tai 4, muodostamme LisaPoikkeus-tyyppisen poikkeuksen. Kun i on 5 tai suurempi, muodostamme IsoOngelma-tyyppisen poikkeuksen. Meillä on käsittelijä jokaiselle luokkatyypille. Ensimmäinen on IsoOngelma-tyyppisille poikkeuksille: catch(isoongelma& ro) cout << " IsoOngelmaa käsitellään: " << ro.mika() << endl;

Ohjelman virheet ja poikkeusten käsittely Muut käsittelijät ovat aivan samanlaiset, niiden viestit ovat vain hieman erilaiset. Funktio mika() palauttaa viestin myös kahden periytetyn käsittelijän kohdalla. Huomaa, että kaikkien catch-lohkojen parametrit ovat viittauksia, kuten oli edellisessäkin esimerkissä. Tällä vältetään toisen kopion muodostaminen poikkeusoliosta. Seuraavassa esimerkissä näemme toisenkin hyvän syyn, miksi käsittelijän parametrin tulisi aina olla viittaus. Kukin käsittelijä tulostaa muodostetun olion sisältämän viestin. Tulostuksesta huomaat, että oikeaa käsittelijää kutsutaan tietyntyyppisen poikkeuksen kohdalla. Käsittelijöiden järjestys on tässä tärkeä, johtuen tavasta, jolla poikkeusten tyyppiä verrataan käsittelijän tyyppiin ja koska esimerkkimme luokat ovat samassa luokkahierarkiassa. Tarkastellaan tätä seuraavaksi hieman tarkemmin. Periytetyn luokan poikkeuksen käsittely kantaluokan käsittelijällä Koska periytetyn luokan poikkeukset muunnetaan automaattisesti kantaluokan tyyppisiksi etsittäessä oikeaa käsittelijää, voimme käsitellä kaikki edellisen esimerkin poikkeukset yhdessä ja samassa käsittelijässä. Muutetaan edellistä esimerkkiä nyt hieman. Kokeile itse - Kantaluokan käsittelijän käyttö Sinun tarvitsee vain poistaa (tai kommentoida) kaksi periytettyjen luokkien käsittelijää edellisestä esimerkistä: // Esimerkki 17.5 Poikkeuksen käsittely kantaluokan käsittelijällä #include <iostream> #include "Ongelmat.h" using namespace std; int main() Ongelma ongelma; LisaOngelma lisaongelma; IsoOngelma isoongelma; cout << endl; for(int i = 0 ; i < 7 ; i++) if(i < 3) throw ongelma; if(i < 5) throw lisaongelma; else throw isoongelma; 687

C++ Ohjelmoijan käsikirja catch(ongelma& ro) // Ainoastaan kantaluokan käsittelijä cout << "Ongelmaa käsitellään: " << ro.mika() << endl; cout << "Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on " << i << endl; cout << endl; return 0; Ohjelman tulostus näyttää seuraavalta: Ongelmaa käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 0 Ongelmaa käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 1 Ongelmaa käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 2 Ongelmaa käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 3 Ongelmaa käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 4 Ongelmaa käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 5 Ongelmaa käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 6 688 Kuinka se toimii Ongelma&-tyyppinen käsittelijä käsittelee nyt kaikki poikkeukset. Jos catch-lohkon parametri on viittaus kantaluokkaan, kaikki periytettyjen luokkien poikkeukset käsitellään sillä. Eli kun tulostus väittää, että Ongelmaa käsitellään, neljä viimeistä ovat itse asiassa Ongelma-luokasta periytettyjä poikkeuksia. Koska dynaaminen tyyppi säilytetään, kun poikkeus välitetään viittauksena, saat dynaamisen tyypin selville typeid()-operaattorilla, jolla voit sen myös tulostaa. Muuta käsittelijän koodi muotoon: catch(ongelma& ro) cout << typeid(ro).name() << " oliota käsitellään: " << ro.mika() << endl; Joissakin kääntäjissä suorituksenaikainen tyypintunnistus ei ole oletusarvoisesti päällä, joten jos tämä ei toimi, tarkista kääntäjän asetukset. Nämä muutokset koodiin havainnollistavat, että periytetyn luokan poikkeukset säilyttävät dynaamisen tyyppinsä, vaikka käytetäänkin viittausta kantaluokkaan. Palauta mieleen, että typeid()-operaattori palauttaa type_info-luokan olion, jonka name()-jäsenfunktio palauttaa luokan nimen const char* -tyyppisenä. Tämän ohjelmaversion tulostus näyttää seuraavalta: class Ongelma oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 0 class Ongelma oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 1

class Ongelma oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 2 class LisaOngelma oliota käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 3 class LisaOngelma oliota käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 4 class IsoOngelma oliota käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 5 class IsoOngelma oliota käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 6 Ohjelman virheet ja poikkeusten käsittely Nyt voit kokeilla muuttaa käsittelijän parametrin tyypin Ongelma-tyyppiseksi, eli poikkeus välitetään nyt arvoparametrinä eikä viittausparametrinä: catch(ongelma o) cout << typeid(o).name() << " oliota käsitellään: " << o.mika() << endl; Kun suoritat ohjelman tämän version, saat tulostuksen: class Ongelma oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 0 class Ongelma oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 1 class Ongelma oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 2 class Ongelma oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 3 class Ongelma oliota käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 4 class Ongelma oliota käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 5 class Ongelma oliota käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 6 Ongelma-tyyppinen käsittelijä valitaan edelleen periytetyn luokan olion kohdalla, mutta dynaamista tyyppiä ei enää säilytetä. Tämä siksi, että parametri alustetaan käyttämällä kantaluokan kopiomuodostinta, joten kaikki periytettyyn luokkaan kuuluvat ominaisuudet menetetään. Tällaisessa tilanteessa säilytetään ainoastaan alkuperäisen periytetyn luokan olion kantaluokan aliolio. Kaikki periytetyn luokan jäsenet poistetaan oliosta. Tämä on esimerkki olion paloittelemisesta. Paloittelu tehdään, koska kantaluokan kopiomuodostin ei tiedä mitään periytetystä luokasta. Olion paloittelu on varsin yleinen virheiden lähde, kun olioita välitetään arvoparametreinä. Paloittelu voi tapahtua sekä tavallisten funktioiden että poikkeusten käsittelijöiden kohdalla. Sinun tulisi aina käyttää viittausparametrejä catch-lohkoissasi. Poikkeuksen uudelleenmuodostaminen Kun käsittelijä käsittelee poikkeusta, se voi muodostaa poikkeuksen uudelleen, jotta ulomman -lohkon käsittelijä voi sen käsitellä. Voit muodostaa nykyisen poikkeuksen uudelleen kirjoittamalla pelkästään avainsanan throw: throw; // Muodostetaan poikkeus uudelleen 689

C++ Ohjelmoijan käsikirja Tämä muodostaa nykyisen poikkeusolion uudelleen kopioimatta sitä. Poikkeus voidaan muodostaa uudelleen esimerkiksi silloin, kun käsittelijä huomaa, että poikkeus tulisi välittää ulommalle -lohkolle. Voit myös haluta rekisteröidä ohjelman kohdan, jossa poikkeus muodostettiin, ja välittää se edelleen esimerkiksi ohjelman johonkin yleiseen osaan, kuten main()-funktioon. // Ulompi -lohko // Sisempi -lohko if( ) throw ex; catch(extyyppi ex) throw; catch(atyyppi ex) catch(extyyppi ex) // Käsitellään ex Tämä käsittelijä käsittelee sisemmässä -lohkossa muodostetun ex-poikkeuksen. Tämä lause muodostaa ex- poikkeuksen uudelleen kopioimatta sitä, joten se voidaan käsitellä ulomman -lohkon käsittelijässä. Huomaa, että jos muodostat poikkeuksen uudelleen sisemmässä -lohkossa, sisemmän lohkon käsittelijät eivät voi sitä kuitenkaan käsitellä. Kun käsittelijä on suorituksessa, muodostettu poikkeus (mukaan lukien nykyinen poikkeus) tulee käsitellä -lohkossa, jonka sisällä nykyinen käsittelijä sijaitsee. Tätä havainnollistetaan yllä olevassa kaaviossa. Se, että uudelleen muodostettua poikkeusta ei kopioida, on tärkeää, varsinkin silloin, kun poikkeus on periytetyn luokan olio, joka alusti kantaluokan viittausparametrin. Havainnollistetaan tätä esimerkin avulla. Kokeile itse - Poikkeuksen uudelleenmuodostaminen Voimme muodostaa joitakin Ongelma-, LisaOngelma- ja IsoOngelma-poikkeusolioita ja muodostaa osan niistä uudelleen: // Esimerkki 17.6 Poikkeuksen uudelleenmuodostaminen #include <iostream> #include "Ongelmat.h" using namespace std; 690 int main() Ongelma ongelma;

Ohjelman virheet ja poikkeusten käsittely LisaOngelma lisaongelma; IsoOngelma isoongelma; cout << endl; for(int i = 0 ; i < 7 ; i++) if(i < 3) throw ongelma; if(i < 5) throw lisaongelma; else throw isoongelma; catch(ongelma& ro) if(typeid(ro) == typeid(ongelma)) cout << "Ongelma-oliota käsitellään: " << ro.mika() << endl; else throw; // Muodostetaan nykyinen poikkeus uudelleen catch(ongelma& ro) cout << typeid(ro).name() << " oliota käsitellään: " << ro.mika() << endl; cout << "Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on " << i << endl; cout << endl; return 0; Ohjelma tulostaa: Ongelma-oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 0 Ongelma-oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 1 Ongelma-oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 2 class LisaOngelma oliota käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 3 class LisaOngelma oliota käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 4 class IsoOngelma oliota käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 5 class IsoOngelma oliota käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 6 691

C++ Ohjelmoijan käsikirja Kuinka se toimii for-silmukka toimii samaan tapaan kuin edellisessäkin esimerkissä, mutta tällä kertaa -lohkon sisällä on toinen -lohko. Poikkeusoliot muodostetaan sisemmässä -lohkossa: if(i < 3) throw ongelma; if(i < 5) throw lisaongelma; else throw isoongelma; Tämä muodostaa samat poikkeusoliot samassa järjestyksessä kuin edellinenkin esimerkki. Käsittelijä käsittelee ne kaikki, koska sen parametri on viittaus kantaluokkaan: catch(ongelma& ro) if(typeid(ro) == typeid(ongelma)) cout << "Ongelma-oliota käsitellään: " << ro.mika() << endl; else throw; // Muodostetaan nykyinen poikkeus uudelleen Tämä käsittelijä käsittelee kaikki Ongelma-tyyppiset poikkeukset ja kaikki Ongelma-luokasta periytetyt poikkeukset. if-lause testaa välitetyn olion tyypin ja tulostaa tulostuksen, jos sen tyyppi oli Ongelma. Muun tyyppisten poikkeusten kohdalla se muodostaa poikkeuksen uudelleen. Voit erottaa tämän catch-lohkon tulostuksen siitä, että se ei ala sanalla class. Uudelleen muodostettu poikkeus voidaan sitten käsitellä ulomman -lohkon käsittelijässä: catch(ongelma& ro) cout << typeid(ro).name() << " oliota käsitellään: " << ro.mika() << endl; 692 Parametri on myös tässä viittaus Ongelma-olioon, joten se käsittelee kaikki periytettyjen luokkien oliot. Tulostuksesta näet, että se käsittelee uudelleen muodostetut poikkeukset, jotka ovat yhä hyvässä kunnossa. Nyt saatat kuvitella, että sisemmän -lohkon käsittelijässä oleva throw;-lause on sama kuin seuraava lause: throw ro; // Muodostetaan nykyinen poikkeus uudelleen Olemmehan vain muodostamassa poikkeuksen uudelleen, vai mitä? Vastaus on että ei ole; tässä on itse asiassa suurikin ero. Tee ohjelman koodiin tämä muutos ja suorita se uudelleen. Saat seuraavanlaisen tulostuksen: Ongelma-oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 0 Ongelma-oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 1

Ongelma-oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 2 class Ongelma oliota käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 3 class Ongelma oliota käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 4 class Ongelma oliota käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 5 class Ongelma oliota käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 6 Ohjelman virheet ja poikkeusten käsittely Poikkeuksen muodostaminen tällä tavalla saa aikaan poikkeuksen kopioinnin Ongelma-luokan kopiomuodostimella. Tässä on jälleen olion paloittelemisongelma. Periytetyn luokan olion osa leikataan pois, joten jäljelle jää tässä tapauksessa vain kantaluokan aliolio. Tulostuksesta huomaat, että typeid()-operaattori tunnistaa kaikki poikkeukset Ongelma-tyyppisiksi. Kaikkien poikkeusten käsittely Voit käyttää kolmea pistettä catch-lohkon parametrin määrittelyssä. Tämä tarkoittaa, että lohko käsittelee kaikki poikkeukset: catch() // Koodi, joka käsittelee kaikki poikkeukset Tämä catch-lohko käsittelee kaiken tyyppiset poikkeukset, joten tällaisten käsittelijöiden tulee aina olla -lohkon käsittelijöistä viimeisenä. Tällöin sinulla ei tietystikään ole mitään tietoa, mikä poikkeus on, mutta voit ainakin estää ohjelman suorituksen päättymisen siksi, että poikkeusta ei käsitellä. Huomaa, että vaikka et tiedä poikkeuksesta mitään, voit muodostaa poikkeuksen uudelleen, kuten teimme edellisessä esimerkissä. Kokeile itse - Minkä tahansa poikkeuksen käsittely Voimme muuttaa edellistä esimerkkiä siten, että sisemmän -lohkon kaikki poikkeukset käsitellään. Tämä tapahtuu lisäämällä kolme pistettä parametrin tilalle: // Esimerkki 17.7 Minkä tahansa poikkeuksen käsittely #include <iostream> #include "Ongelmat.h" using namespace std; int main() Ongelma ongelma; LisaOngelma lisaongelma; IsoOngelma isoongelma; cout << endl; for(int i = 0 ; i < 7 ; i++) 693

C++ Ohjelmoijan käsikirja if(i < 3) throw ongelma; if(i < 5) throw lisaongelma; else throw isoongelma; catch() cout << "Käsittelemme jotakin! Muodostetaan se uudelleen." << endl; throw; // Muodostetaan nykyinen poikkeus uudelleen catch(ongelma& ro) cout << typeid(ro).name() << " oliota käsitellään: " << ro.mika() << endl; cout << "Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on " << i << endl; cout << endl; return 0; Tulostus näyttää nyt tältä: Käsittelemme jotakin! Muodostetaan se uudelleen. class Ongelma oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 0 Käsittelemme jotakin! Muodostetaan se uudelleen. class Ongelma oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 1 Käsittelemme jotakin! Muodostetaan se uudelleen. class Ongelma oliota käsitellään: Tässä on ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 2 Käsittelemme jotakin! Muodostetaan se uudelleen. class LisaOngelma oliota käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 3 Käsittelemme jotakin! Muodostetaan se uudelleen. class LisaOngelma oliota käsitellään: Lisää ongelmia Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 4 Käsittelemme jotakin! Muodostetaan se uudelleen. class IsoOngelma oliota käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 5 Käsittelemme jotakin! Muodostetaan se uudelleen. class IsoOngelma oliota käsitellään: Todella suuri ongelma Silmukan loppu (kaikkien catch-lohkojen jälkeen) - i on 6 694