970 likes | 1.17k Views
Olio-ohjelmoinnin perusteet luento 6: C++ tekniikoita, virhetilanteet ja poikkeukset. Jani Rönkkönen LTY/Tietotekniikan osasto Kalvot on muokattu Sami Jantusen luentokalvoista viime vuodelta. Sisältö. Operaattoreiden suoritusjärjestys C++ tekniikoita Nimiavaruus Luokkamuuttujat
E N D
Olio-ohjelmoinnin perusteetluento 6: C++ tekniikoita, virhetilanteet ja poikkeukset Jani Rönkkönen LTY/Tietotekniikan osasto Kalvot on muokattu Sami Jantusen luentokalvoista viime vuodelta.
Sisältö • Operaattoreiden suoritusjärjestys • C++ tekniikoita • Nimiavaruus • Luokkamuuttujat • Luokkafunktiot • Virhetilanteet ja poikkeukset • Syitä poikkeuksiin • Virhetilanteisiin reagointi • Virhehierarkiat • Poikkeuksien heitto ja sieppaaminen • Poikkeukset ja oliot • Yhteenveto
Operaattoreiden Suoritusjärjestys Assosiatiivisyys
Operaattoreiden Suoritusjärjestys Assosiatiivisyys
Operaattoreiden Suoritusjärjestys Assosiatiivisyys
Operaattoreiden suoritusjärjestys • Matemaattisissa kaavoissa suoritusjärjestys noudattaa matematiikan sääntöjä • Kannattaa käyttää sulkuja varmistamaan oikea järjestys jos epäselvyyttä • Assosiatiivisyys (operaattoreiden suoritus suunta) oletuksena vasemmalta oikealle, mutta joissakin operaatioissa oikealta vasemmalle • Sama suoritusjärjestys koskee myös operaattoreita, joissa toiminta on uudelleenmääritelty
Ongelma // vendor1.h ... various stuff ... class String { ... }; myProgram.cpp #include "vendor1.h" #include "vendor2.h" void main() { … } // vendor2.h ... various stuff ... class String { ... }; Löydätkö esimerkistä ongelman?
Ongelman kuvaus • Käyttökelpoisia luokkien, funktioiden ja muuttujien nimiä on rajallinen määrä • Isommissa ohjelmistoissa törmätään helposti nimikonflikteihin. • Nimikonflikteja syntyy etenkin silloin, kun käytetään hyväksi monta eri ohjelmistomoduulia: • Esimerkkejä: • Ohjelmistossa käytössä olevista kirjastoista 2 määrittelee Tulosta() -funktion. • Käyttämässäsi 3. osapuolen luokkakirjastossa on määritelty samanniminen luokka kuin omassa koodissasi. ohjelma ei käänny
Nimiavaruudet (Namespaces) • C++ ratkaiseen suurten ohjelmistojen rajapintojen nimikonfliktit nimiavaaruutta käyttämällä • Nimiavaruuksien tarkoituksena on tarjota kielen syntaksin tasolla oleva hierarkkinen nimeämiskäytäntö. • Hierarkia auttaa jakamaan ohjelmistoa osiin • Samalla estetään nimikonfliktit eri ohjelmiston osien välillä
Nimiavaruuden määrittely • Toisiinsa liittyvät ohjelmakokonaisuudet voidaan koota yhteen nimiavaruuteen namespace –avainsanalla • Käyttöesimerkki. Kootaan kaikki päiväykseen liittyvät tiedot (tietorakenteet, tietotyypit, vakiot, oliot ja funktiot) yhteen nimikkeen Paivays alle: • paivays.cpp • #include “paivays.h” • namespace Paivays { • Pvm luo (int paiva, int kuukausi, int vuosi) • { • Pvm paluuarvo; • ... • return paluuarvo; • } • void tulosta (Pvm kohde) • { • ... • } • paivays.h • namespace Paivays { • struct Pvm { int p_, k_, v_; }; • Pvm luo (int paiva, int kuukausi, int vuosi); • void tulosta (Pvm kohde); • ... • }
Näkyvyystarkenninoperaattori • Nimiavaruuden sisällä määritetyt jäsenet ovat näkyvissä vain kyseisen nimiavaruuden sisällä • Nimiavaruuden ulkopuolelta em. jäseniin pääsee käsiksi näkyvyystarkenninoperaattorin :: avulla. • Ohjelmoijan tulee jäseniä käytettäessä ilmaista mitä kokonaisuutta ja mitä alkiota sen sisällä hän haluaa käyttää • Paivays::tulosta() • Kirja::tulosta() • Ylimääräistä kirjoittelua, mutta toisaalta selkeyttää koodia
NimiavaruudetHyödyt • Nimikonfliktien vaara vähenee merkittävästi • jokaisen moduulin rajapintanimet ovat omassa nimetyssä näkyvyysalueessaan • Moduulin määrittelemien rakenteiden käyttö on kielen syntaksin tasolla näkyvän rakenteen (näkyvyystarkennin::) vuoksi selkeämpää • Hierarkisuudesta huolimatta moduulin sisällä on käytettävissä lyhyet nimet. • Koodista nähdään syntaksin tasolla esimerkiksi, mitkä funktiokutsut kohdistuvat saman moduulin sisälle ja mitkä muualle ohjelmistoon
Korjataan ongelma • Enää ei ole käytössä kahta String-luokka • String-luokkien sijasta meillä on käytössä luokat: Vendor1::String Vendor2::String // vendor1.h ... various stuff ... namespace Vendor1 { class String { ... }; } // vendor2.h ... various stuff ... namespace Vendor2 { class String { ... }; }
std nimiavaruus • C++ standardi määrittelee omaan käyttöönsä std-nimiavaruuden • Käytössä kaikissa nykyaikaisissa kääntäjissä • std-nimiavaruus sisältää lähestulkoon kaikki C++ ja C-kielissä määritellyt rakenteet. Esim: • std::printf • std::cout • Varattu pelkästään sisäiseen käyttöön. • Et saa lisätä omia rakenteita std-nimiavaruuteen
Uudet otsikkotiedostot • std nimiavaruuden käyttöönotto aiheuttaa muutoksia myös otsikkotiedostojen nimissä • Kääntäjän omat otsikkotiedostot sisällytetään ilman .h –ekstensiota • #include <iostream.h> #include<iostream> • C-kielestä perittyihin otsikkotiedostoiden nimiin lisätää ‘c’ eteen • #include <cstring>
Esimerkki std-nimiavaruuden käytöstä • paivays.cpp • #include <cstdlib>//pääohjelman paluuarvo EXIT_SUCCESS • #include <iostream> //C++ tulostus • #include <cstring> //C:n merkkitaulukkofunktiot • int main() • { • const char* const p = “Jyrki Jokinen”; • char puskuri [42]; • std::strcpy(puskuri, “Jyke “); • std::strcat(puskuri, std::strstr(p, “Jokinen”) ); • std::cout << puskuri << std::endl; • return EXIT_SUCCESS; • }
std::cout, std::endl joko väsyttää? • std:: toistaminen jatkuvasti turhauttaa • Jos samassa ohjelmalohkossa käytetään useita kertoja samaa nimiavaruuden sisällä olevaa nimeä, kannattaa käytttää using -lausetta • using mahdollistaa valittujen rakenteiden käytön nimiavaruuden ulkopuolella ilman :: -tarkenninta • normaali using -lause nostaa näkyville yhden nimen • using namespace nostaa näkyville kaikki nimiavaruuteen kuuluvat rakenteet
usingEsimerkki • paivays.cpp • #include “paivays.h” • void kerroPaivays (Paivays::Pvm p ) • { • using std::cout; • using std::endl; • using namespace Paivays; //Käytetään kaikkia nimiavaruuden nimiä • cout << “Tänään on: “; • tulosta(p); //Kutsuu Paivays::Tulosta • cout << endl; • }
usingOhjeita • Käytä using-lausetta mielellään vasta kaikkien #include-käskyjen jälkeen • Vältä using-lauseen käyttöä otsikkotiedostossa • Käytä using-lausetta mahdollisimman lähellä sitä aluetta, jossa sen on tarkoitus olla voimassa • Paljon käytetyissä rakenteissa on usein kuitenkin selkeämpää kirjoittaa using heti siihen liittyvän otsikkotiedoston #include-käskyn jälkeen.
ehdotus using-lauseen käyttöstä eri otsikkotiedostojen kanssa • //Omat rakenteet esitellään std-kirjastoja ennen • //(tämä siksi että saamme tarkastettua niiden sisältävän kaikki • //tarvittavat #include-käskyt ts. ne ovat itsenäisesti kääntyviä yksikköjä • #include “paivays.h” • #include “swbus.h” • #include “tietokanta.h” • #include “loki.h” • //Kaikista yleisimmin tässä tiedostossa käytetyt std-rakenteet esitellään • //heti niihin liittyvän otsikkotiedoston jälkeen • #include <iostream> • using std::cout; • using std::endl; • #include <vector> • using std::vector; • #include <string> • using std::string • //lopuksi omiin moduuleihin liittyvät using-lauseet • using Paivays::PVM; • using Loki::varoitus; • using Loki::virhe;
Nimiavaruuden synonyymi • Nimiavaruudelle voidaan määritellä synonyymi (alias) • alias-nimeen tehdyt viittaukset käyttäytyvät alkuperäisen nimen tavoin • Käyttökohteet • moduulin korvattavuus helpottuu • helpompi nimi pitkille nimiavaruuksien nimille
Nimiavaruuden synonyymiEsimerkki • #include “prjlib/string.h” • #include <string> • int main() • { • #ifdef PRJLIB_OPTIMOINNIT_KAYTOSSA • namespace Str = ComAcmeFastPrjlib; • #else • namespace Str = std; • #endif • Str::string esimerkkijono; • . • . • . • }
Missä mennään? • Operaattoreiden suoritusjärjestys • C++ tekniikoita • Nimiavaruus • Luokkamuuttujat • Luokkafunktiot • Virhetilanteet ja poikkeukset • Syitä poikkeuksiin • Virhetilanteisiin reagointi • Virhehierarkiat • Poikkeuksien heitto ja sieppaaminen • Poikkeukset ja oliot • Yhteenveto
Ongelma • Haluamme pitää kirjaa siitä kuinka monta samaa tyyppiä olevaa oliota on hengissä kullakin hetkellä • Mikä olisi hyvä ratkaisu? • Milloin tiedetään, että olio syntyy tai kuolee? • Missä pidetään kirjaa hengissä olevien olioiden lukumäärästä?
Aloitetaan ongelman ratkaisu • Meillä pitää olla laskuri! • laskuria lisätään kun olion rakentajaa kutsutaan • laskuria vähennetään kun olion purkajaa kutsutaan • Millä näkyvyysalueella laskuri sijaitsee? • Luokan jäsenmuuttujana? • Globaali muuttuja?
Hmmm… • Laskurin sijoittaminen olion jäsenmuuttujaksi ei toimi • se olisi oliokohtainen muuttuja. Muut eivät pääsisi päivittämään sitä • Globaali muuttuja toimisi • Olio-ohjelmoijina emme tykkäisi ideasta. Tämä olisi sotkuinen ratkaisu
Hmmm… • Huomaamme, että joskus olisi tarvetta sellaisille jäsenille, jotka on yhteisiä kaikille luokan olioille! • toisaalta kaikille olioille • toisaalta ei millekkään niistä
Ratkaisu! • C++ kielessä on mekanismi, mikä ratkaisee ongelmamme • On mahdollista esitellä luokan jäsen luokkamuuttujana (static data member) • Luokkamuuttujan esittely on muuten samanlainen kuin jäsenmuuttujankin, mutta esittely alkaa avainsanalla static
Luokkamuuttuja • Luokkamuuttuja on luonteeltaan hyvin lähellä normaalia globaalia muuttujaa • Se on olemassa, vaikka luokasta ei olisi vielä luotu ainuttakaan oliota. • Itse asiassa luokkamuuttujat luodaan jo ennen main ohjelman aloitusta, kuten globaalit muuttujatkin • Koska luokkamuuttuja ei kuulu mihinkään olioista, sitä ei voi alustaa luokan rakentajassa. • Jossain päin koodia täytyy olla erikseen luokkamuuttujan määrittely, jonka yhteydessä muuttuja alustetaan: int X::luokkamuuttuja_ = 2;
Luokkamuuttujan käyttö • Luokan omassa koodissa luokkamuuttujaan voi viitata aivan kuten jäsenmuuttujaankin • Luokan ulkopuolelta luokkamuuttujaan voi viitata syntaksilla: Luokka::lmuuttuja • Toinen tapa on viitata luokkamuuttujiin luokan olion kautta: X xolio; int arvo = xolio.luokkamuuttuja_; • Käytetään pitkälti saman tyylisesti kuin jäsenmuuttujiakin • Pyri pitämään luokkamuuttujat privaatteina
Luokkafunktiot (static member functions) • Edustavat sellaisia luokan palveluja ja operaatioita, jotka eivät kohdistu mihinkään yksittäiseen olioon • Samankaltainen jäsenfunktion määrittelyn kanssa • Lisätään static –avainsana • Ei saa käyttää toteutuksessa minkään olion jäsenmuuttujia eikä this-osoittimia
Ja ratkaistaan ongelma! // static members in classes #include <iostream> using namespace std; class CDummy { public: static int n; CDummy () { n++; }; ~CDummy () { n--; }; }; int CDummy::n=0; int main () { CDummy a; CDummy b[5]; CDummy * c = new CDummy; cout << a.n << endl; delete c; cout << CDummy::n << endl; return 0; } • Mikä on oheisen koodin lopputulos? Vastaus: 7 6
Hieman konkreettisempi esimerkki! • On hyvin tavallista että tietystä luokasta pitäisi olla olemassa vain ja ainoastaan yksi instanssi • Kyseinen instanssi pitäisi olla kuitenkin mahdollisimman helposti saatavilla muille olioille • Ongelma: • Kuinka varmistat, että luokkaa ei missään tilanteessa luoda enemää kuin yksi olio? • Kuinka voit samalla taata sen, että kuka tahansa voi päästä käsiksi kyseiseen olioon?
Ratkaistaan ongelma! • Mitä jos loisimme luokkamuuttujan joka olisi osoitin luokan tyyppiseen olioon? • jos osoitin = 0, yhtään oliota ei ole vielä luotu • jos osoitin != 0, olio on jo luotu • Voisimme vielä luoda luokkafunktion (getInstance), joka palauttaisi osoittimen yhteen ja ainoaan olioon • jos luokkamuuttujaosoitin = 0, funktio loisi uuden olion ja palauttaisi sen osoitteen • jos olio olisi jo olemassa, funktio palauttaisi sen osoitteen
Singleton Pattern class Singleton {public:static Singleton *get_instance();protected: Singleton(); Singleton( const Singleton& s);private:static Singleton *instance;};Singleton::instance = 0; Singleton *Singleton::get_instance() {if ( instance == 0 ) { instance = new Singleton; }return instance;} «Singleton» theInstance getInstance Company theCompany Company «private» getInstance if (theCompany==0) theCompany= new Company(); return theCompany;
Mitä tuli tehtyä? • Käytimme luokkamuuttujaa ja luokkafunktiota fiksusti yhteen • nyt voimme olla varmoja, että ei ole koskaan mahdollista luoda enempää kuin yksi olio kyseistä tyyppiä • Olioon on kuitenkin todella helppo päästä käsiksi • Loimme itse asiassa yhden yleisimmistä Design Patterneista (singleton)!
Vaaroja • Emme tiedä etukäteen missä järjestyksessä static (tai globaalit) oliot luodaan • Jos tälläisen olion muodostajassa käytetään toisen vastaavan olion metodia, voi käydä niin, että oliota, jonka metodia yritettiin kutsua ei ole vielä luotu -> ohjelma kaatuu • Ongelma voidaan kiertää käyttämällä singleton ideaa hyväksi siirtämällä static olio funktion sisään, jossa se ensimmäisellä kutsukerralla luodaan dynaamisesti • Ongelmana on, että nyt olion tuhoajaa ei kutsuta koskaan
Esimerkki x.cpp #include ”class1.h” class1 x; //global class1.cpp #include ”class1.h” class1::class1() { … y.method(); … } y.cpp #include ”class2.h” class2 y; //global 50% todennäköisyys että y on luotu ennen x:ää, jolloin ei ongelmia mutta 50% tapauksista ohjelma kaatuu
Esimerkki class1.cpp #include ”class1.h” class1::class1() { … y().method(); … } x.cpp #include ”class1.h” class1 x; //global y.cpp #include ”class2.h” class2& y() { static class2* ans = new class2(); return *ans; } Funktion kutsu olion sijasta Static rivi suoritetaan vain kerran, ennen main ohjelman alkua, sen jälkeen y () vain palauttaa viitteen static olioon
Missä mennään? • Operaattoreiden suoritusjärjestys • C++ tekniikoita • Nimiavaruus • Luokkamuuttujat • Luokkafunktiot • Virhetilanteet ja poikkeukset • Syitä poikkeuksiin • Virhetilanteisiin reagointi • Virhehierarkiat • Poikkeuksien heitto ja sieppaaminen • Poikkeukset ja oliot • Yhteenveto
Virhetilanteet ja poikkeukset • Virhetilanteisiin varautuminen ja niihin reagoiminen on aina ollut yksi vaikeimpia ohjelmoinnin haasteista • Virhetilanteissa ohjelman täytyy yleensä toimia normaalista poikkeavalla tavalla • Uusien ohjelman suoritusreittien koodaaminen tekee ohjelmakoodista helposti sekavaa • Useiden erilaisten virhetilanteiden viidakossa ohjelmoijalta jää helposti tekemättä tarvittavia siivoustoimenpiteet • Usein myös vaaditaan, että ohjelman tulee toipua virheistä • täytyy pystyä peruuttamaan virhetilanteen vuoksi kesken jääneet operaatiot
Poikkeukset (exception) • C++ tarjoaa virheiden käsittelyyn erityisen mekanismin, poikkeukset (exception) • Poikkeusten toiminta perustuu luokkahierarkioihin
Syitä virheisiin • Määrittelyvirheet • yritetään tehdä ohjelmalla jotain mihin sitä alunperinkään ei ole tarkoitettu • Suunnitteluvirheet • toteutukseen ei ole otettu mukaan kaikkia määrittelyssä olleeita asioita • toteutus on suunniteltu virheelliseksi • Ohjelmointivirheet • ohjelmointityössä on tapahtunut virhe
Virheisiin varautuminen • Ohjelmointityössä ei pysty vaikuttamaan määrittelyn ja suunnittelun aikaisiin virheisiin • ne paljastuvat ohjelmiston testauksessa tai huonoimmassa tapauksessa vasta tuotantokäytössä • Ohjelmoinnissa voidaan varautua etukäteen pohdittuihin vikatilanteisiin • laitteistovirheet • ohjelmistovirheet
Laitteistovirheet • Näkyvät ohjelmistolle sen ympäristön käyttäytymisenä eri tavoin kuin on oletettu. Esimerkkejä: • viallinen muistipiiri saattaa esim. aiheuttaa odottomattoman muuttujan arvon muutoksen. • tiedoston käsittely voi mennä pieleen levyn täyttymisen tai vikaantumisen vuoksi • Ohjelman latauksessa on voinut tapahtua virhe, mikä on johtanut ohjelman suorituksessa tapahtuneisiin muutoksiin
Laitteistovirheet • Laitteistovirheistä saadaan tietoa yleensä käyttöjärjestelmän kautta • Laitteistovirheitä voi kuitenkin tapahtua siten, ettei niistä tule mitään ilmoitusta • Mihin laitteistovirheisiin tulisi reagoida? • Kaikkia laitteistovirheitä on vaikea ottaa huomioon. • On tehtävä kompromissi. • Yleinen tapa on varautua käyttöjärjestelmän ilmoittamiin vikoihin ja jättää muut huomioimatta luottaen niiden olevan erittäin harvinaisia
Ohjelmistovirheet • Ulkoiset virheet: Koodia pyydetään tekemään jotain, mitä se ei osaa tai mihin se ei pysty. Esim: • Funktion parametrilla on väärä arvo • syötetiedosto ei noudata määriteltyä muotoa • käyttäjä on valinnut toimintosekvenssin jossa ei ole “järkeä” • Sisäiset virheet: Toteutus ajautuu itse tilanteeseen jossa jotain menee pieleen. Esim: • muisti loppuu • toteutusalgoritmissa tulee jokin ääriraja vastaan
Virheiden havaitsemisesta • Virheiden havaitseminen on yleensä helppoa • tähän toimintaan käyttöjärjestelmät, ohjelmakirjastot ja ohjelmointikielet tarjoavat lähes aina keinoja • Havaitsemista paljon vaikeampaa on suunnitella ja toteuttaa se, mitä vikatilanteessa tehdään!
Varautuva ohjelmointi (defensive programming) • Ohjelmointityyli, jota voisi verrata autolla ajossa ennakoivaan ajotapaan. • Vaikka oma toiminta olisi täysin oikein ja sovittujen sääntöjen mukaista, kannattaa silti varautua siihen, että muut osallistujat voivat toimia väärin. • Usein ajoissa tapahtunut virheiden ja ongelmien havaitseminen mahdollistaa niihin sopeutumisen jopa siten, että ohjelmissa käyttäjän ei tarvitse huomata mitään erityistilannetta edes syntyneen
Virhetilanteisiin reagointi • Sopiva suhtautuminen virheeseen on ohjelmakomponentin suunnitteluun kuuluva asia • Ei ole olemassa yhtä ainoaa oikeata tai väärää tapaa • Hyvin suunniteltu komponentti voi ottaa virheisiin reagoinnin omalle vastuulleen • Yhtä hyvänä ratkaisuna voidaan pitää myös sellaista, joka “ainoastaan” ilmoittaa havaitsemansa virheet komponentin käyttäjälle