Toinen projekti

Kääntäminen ja moduulien suunnittelu


Tällä sivulla jatketaan modulaarisuutta geometriamoduulin käännöksellä, tarkastellaan isompaa bussiaikatauluesimerkkiä ja kerrataan, miten moduulien rajapinnat kannattaa järjestää.

Geometrialaskimen kääntäminen

Qt Creator hoitaa kääntämisen puolestasi. Kun käännät ohjelman komentoriviltä, näet selvemmin, miten useasta lähdekooditiedostosta muodostetaan suoritettava ohjelma. Siirry ensin esimerkin hakemistoon examples/07/geometry. Hakemistossa ei ole Qt Creatorin .pro-projektitiedostoa.

Yksinkertaisimmillaan molemmat toteutustiedostot voi kääntää ja linkittää yhdellä komennolla:

g++ calculator.cpp geometry.cpp

Otsikkotiedostoa geometry.hh ei kirjoiteta komentoon, koska esiprosessori liittää sen sisällön calculator.cpp-tiedostoon #include-direktiivin kohdalla. Komento luo tavallisesti suoritettavan tiedoston nimeltä a.out, jonka voi käynnistää näin:

./a.out

Kääntämisen ja linkityksen voi tehdä myös erikseen:

  1. Käännä geometriamoduuli objektitiedostoksi: g++ -c geometry.cpp.
  2. Käännä pääohjelma objektitiedostoksi: g++ -c calculator.cpp.
  3. Linkitä objektitiedostot suoritettavaksi ohjelmaksi: g++ calculator.o geometry.o.

Valitsin -c pyytää kääntäjää kääntämään lähdekoodin linkittämättä. Tuloksena syntyvä .o-tiedosto sisältää konekoodia, mutta ei yksinään ole suoritettava ohjelma. Linkityksessä objektitiedostot yhdistetään ja niiden väliset viittaukset ratkaistaan. Suoritettavan tiedoston nimen voi valita valitsimella -o:

g++ -o calculator calculator.o geometry.o

Kun ohjelma kasvaa, käännöksen ja linkityksen erottaminen auttaa tunnistamaan, missä vaiheessa virheilmoitus syntyy. Qt Creator käyttää käännöksen automatisointiin make-ohjelmaa; sitä voi käyttää myös komentoriviltä.

Esimerkki: bussiaikataulut

Hakemiston examples/07/bus_timetables ohjelma kysyy kellonajan ja bussin numeron ja tulostaa kolme seuraavaa kyseisen vuoron lähtöaikaa. Ohjelma koostuu kolmesta moduulista:

  • timetable.cpp on pääohjelmamoduuli. Se alustaa aikataulutietorakenteen, hoitaa käyttöliittymän ja etsii käyttäjän antamien tietojen perusteella sopivia bussivuoroja. Se käyttää time- ja utilities-moduulien julkisia rajapintoja.
  • time.hh ja time.cpp muodostavat Time-luokan sisältävän moduulin. Luokka käsittelee kellonaikoja: alustaa ja asettaa ajan, lukee ja tulostaa sen sekä vertailee aikoja. Moduuli käyttää utilities-moduulia.
  • utilities.hh ja utilities.cpp tarjoavat yleisiä apufunktioita, kuten merkkijonon muuntamisen kokonaisluvuksi ja kokonaisluvun lukemisen näppäimistöltä. Tämä moduuli ei käytä muita ohjelman moduuleita.

Esimerkissä on muutamia aiemmista aiheista poikkeavia kohtia:

  • Viite voi antaa olemassa olevalle arvolle lyhyemmän nimen. Esimerkiksi const vector<Time>& timevec = iter->second; antaa säiliön osalle nimen timevec. Viitteen ansiosta säiliötä ei kopioida. const kertoo, ettei sitä muuteta; iteraattori viittaa aikatauluun, jota käsitellään vakiona.
  • Nimetön nimiavaruus rajaa apukoodin yksityiseksi. Kun apufunktion esittely tai määrittely on .cpp-tiedoston nimettömässä namespace-lohkossa, muut lähdekooditiedostot eivät voi kutsua sitä. Tämä vastaa moduulissa yksityistä apupalvelua.
  • Rakentaja voi sisältää tavallisia käskyjä. Esimerkin Time-rakentaja asettaa arvon rungossaan. Jos annettu aika on virheellinen ja set_value palauttaa false, rakentajalla ei ole paluuarvoa, jolla se voisi ilmoittaa epäonnistumisesta. Myöhemmin opittavat poikkeukset tarjoavat tähän ratkaisun; tällä kurssilla niitä ei vielä käytetä.

Julkinen rajapinta: hh-tiedosto

Moduulin julkinen rajapinta kuvaa palvelut, joita muut moduulit saavat käyttää. C++:ssa se kirjoitetaan tavallisesti .hh-otsikkotiedostoon. Siinä voi olla:

  • funktioiden esittelyjä
  • const-vakioiden määrittelyjä
  • uusien tietotyyppien, myös luokkien, määrittelyjä.

Julkiseen rajapintaan ei kirjoiteta muuttujien määrittelyjä eikä funktioiden tai luokan metodien toteutuksia. Jokainen lähdekooditiedosto, joka käyttää toisen moduulin palvelua, sisällyttää palvelun tarjoavan otsikon:

#include "palvelun_tarjoavan_moduulin_nimi.hh"

Jos otsikkotiedosto tarvitsee toisessa moduulissa määriteltyä tyyppiä, sekin voi tarvita #include-rivin. Moduulin oma .cpp-tiedosto sisällyttää usein oman .hh-tiedostonsa, jotta toteutus kääntyy täsmälleen julkisen esittelyn mukaisesti. Älä käytä #include-direktiiviä .cpp-tiedoston sisällyttämiseen.

Hyvä käytäntö on luetella ensin omat otsikkotiedostot ja vasta sitten standardikirjaston otsikot:

#include "omamoduuli-1.hh"
#include "omamoduuli-n.hh"

#include <standardikirjasto-1>
#include <standardikirjasto-n>

Tämä käytäntö auttaa havaitsemaan puuttuvat sisällytykset ja tarkistamaan, että moduulin omat tiedostot muodostavat itsenäisesti kääntyvän kokonaisuuden. Include guard -direktiivit estävät otsikkotiedoston sisällön käsittelyn moneen kertaan.

Yksityinen toteutus: cpp-tiedosto

Moduulin .cpp-tiedostossa määritellään julkisessa rajapinnassa esitellyt funktiot ja metodit sekä niiden tarvitsemat apupalvelut. Jos apufunktion halutaan olevan käytettävissä vain tässä moduulissa, sen voi sijoittaa nimettömään nimiavaruuteen:

namespace {
    void moduulin_yksityinen_funktio() {
        // Moduulin sisäinen apufunktio
    }
}

// Julkisen rajapinnan funktioiden määrittelyt

Erillistä esittelyä ei tarvita, jos apufunktion määrittely on ennen sen kutsukohtaa. Nimetöntä nimiavaruutta käytetään tavallisiin apufunktioihin; luokan metodeissa näkyvyys hoidetaan luokan public- ja private-osilla. Jos toteutustiedosto tarvitsee omassa otsikossaan määriteltyä vakiota tai tyyppiä, sen pitää sisällyttää oma otsikkonsa.

Ohjelmoija voi määritellä myös nimetyn nimiavaruuden:

namespace mun_avaruus {
    void mun_funktio() { ... }
}

Sen funktiota kutsutaan nimellä mun_avaruus::mun_funktio(), tai nimiavaruus voidaan ottaa käyttöön using namespace mun_avaruus;-lauseella. Nimiavaruuksilla voidaan välttää samannimisten tunnisteiden törmäyksiä: esimerkiksi Opiskelija::nimi ja Opintojakso::nimi voivat tarkoittaa eri nimiä. Standardikirjaston nimiavaruus on syy siihen, että koodissa näkee usein lauseen using namespace std;.

Suunnittele moduulijako osaongelmista

Moduulit kannattaa tunnistaa jo ohjelman suunnitteluvaiheessa etsimällä loogisia osakokonaisuuksia. Bussiaikatauluohjelmassa tarvitaan kellonaikojen käsittelyä ja vertailua, lukujen lukemista ja muuntamista, käyttöliittymä sekä aikataulujen hakualgoritmi. Näiden pohjalta ohjelma jaettiin pääohjelma-, ajankäsittely- ja apufunktiomoduuliin.

Hyvä moduuli toteuttaa selkeästi rajatun tehtävän ja pysyy riittävän yksinkertaisena. Suuren ongelman voi pilkkoa pienemmiksi osaongelmiksi ja pilkkoa niitä edelleen, kunnes jokainen osa on hallittava. Tätä lähestymistapaa kutsutaan top-down-menetelmäksi.

Tämän kurssin ohjelmissa moduuleiksi sopivat usein esimerkiksi:

  • luokka tai siihen liittyvä kokonaisuus
  • pääohjelma
  • käyttöliittymä tai monimutkaisen syötteen käsittely
  • tiedoston lukeminen ja jäsentely
  • yleiskäyttöiset algoritmit, kuten haku ja lajittelu.

Moduulit voidaan toteuttaa myös luokkina, joilla on omat julkiset ja yksityiset rajapintansa. Luokat ovatkin C++:ssa tyypillinen tapa muodostaa ohjelmaan modulaarisia kokonaisuuksia.

Rajapinnan suunnittelu ja modulaarisuuden hyödyt

Moduulijaon hahmottaminen on yleensä helpompaa kuin julkisten rajapintojen suunnittelu. Rajapintaa määriteltäessä pitää päättää tarkasti, mitä palveluja moduuli tarjoaa ja miten muut osat käyttävät niitä. Hyvä rajapinta pysyy mahdollisimman vakaana ohjelman kehittyessä. Jos puutteita huomataan vasta, kun moni muu osa jo käyttää rajapintaa, muutokset voivat olla työläitä ja vaikuttaa laajasti ohjelmaan.

Modulaarisuudesta on useita hyötyjä:

  • Moduulin toteutusta voidaan muuttaa julkisen rajapinnan säilyessä ennallaan.
  • Loogisesti yhteenkuuluvat osat pysyvät yhdessä, mikä selkeyttää testaamista ja ylläpitoa.
  • Moduuleja voidaan kehittää rinnakkain, kun niiden rajapinnat on sovittu.
  • Moduulit auttavat hallitsemaan suuria ohjelmointiprojekteja ja mahdollistavat koodin uudelleenkäytön.
  • Erikseen käännettävissä ohjelmissa vain muuttuneet moduulit tarvitsee usein kääntää uudelleen, mikä säästää aikaa ja resursseja.

Muista: onnistunut moduulijako antaa ohjelmalle selkeät, rajatut vastuut. Hyvin suunniteltu julkinen rajapinta puolestaan auttaa pitämään nämä moduulit toisistaan riippumattomina.