Oliot, osoittimet ja taulukot

Luokkien toteutus ja kapselointi


C++:ssa luokan määrittely ja metodien toteutukset sijoitetaan tavallisesti eri tiedostoihin. Tämä helpottaa luokan käyttöä useissa ohjelman osissa ja erottaa julkisen rajapinnan toteutuksen yksityiskohdista.

Otsikko- ja toteutustiedostot

Pääohjelma ottaa luokan määrittelyn käyttöön otsikkotiedostosta:

#include "henkilo.hh"
#include <iostream>

using namespace std;

int main() {
    Henkilo kaveri("Matti", 18);
    cout << kaveri.hae_nimi() << endl;
    kaveri.tulosta();
    kaveri.vieta_syntymapaivaa(19);
    kaveri.tulosta();
}

Otsikkotiedosto henkilo.hh ilmoittaa luokan nimen, julkiset metodit ja yksityiset jäsenmuuttujat:

#include <string>

class Henkilo {
public:
    Henkilo(const std::string& nimi, int ika);
    std::string hae_nimi() const;
    void vieta_syntymapaivaa(int uusi_ika);
    void tulosta() const;

private:
    std::string nimi_;
    int ika_;
};

Metodien toteutukset ovat tiedostossa henkilo.cpp. Toteutustiedosto ottaa ensin käyttöön vastaavan otsikon:

#include "henkilo.hh"
#include <iostream>

using namespace std;

Henkilo::Henkilo(const string& nimi, int ika)
    : nimi_(nimi), ika_(ika) {}

string Henkilo::hae_nimi() const {
    return nimi_;
}

void Henkilo::vieta_syntymapaivaa(int uusi_ika) {
    ika_ = uusi_ika;
}

void Henkilo::tulosta() const {
    cout << nimi_ << " : " << ika_ << endl;
}

Kirjastojen otsakkeet kirjoitetaan yleensä kulmasulkeisiin, kuten <iostream>. Omiin tiedostoihin viitataan lainausmerkeillä, kuten "henkilo.hh". Kääntäjälle täytyy lisäksi kertoa, että henkilo.cpp kuuluu ohjelman käännettäviin lähdetiedostoihin.

Tavallisesti jokaista luokkaa kohti tehdään otsikkotiedosto ja toteutustiedosto. Otsikkotiedosto sisältää luokan määrittelyn; toteutustiedosto sisältää metodien rungot. Esimerkissä käytetään päätteitä .hh ja .cpp.

Luokan lisääminen Qt Creatorissa

Qt Creatorissa uuden luokan voi lisätä valitsemalla New File or Project, sitten C++ ja C++ Class. Työkalu luo luokalle otsikko- ja toteutustiedoston ja lisää toteutustiedoston projektin käännettäviin lähdetiedostoihin.

Projektin .pro-tiedostossa käännettävät lähdetiedostot on lueteltu tavallisesti SOURCES-kohdassa. Otsikkotiedostoja ei lisätä tähän samalla tavalla. Kurssin alussa Qt Creator voi hoitaa nämä asetukset automaattisesti.

Kellonaika-luokka: kaksi toteutusta

Seuraava esimerkki näyttää, miten luokan yksityisen toteutuksen voi vaihtaa julkista rajapintaa muuttamatta. Ensimmäisessä versiossa kellonaika tallennetaan erillisiin tunti- ja minuuttikenttiin.

Versio 1: tunnit ja minuutit erikseen

class Kellonaika {
public:
    Kellonaika(int tunti, int minuutti);
    void tiktok();
    void tulosta() const;

private:
    int tunnit_;
    int minuutit_;
};

Luokan käyttävä koodi kutsuu julkisia metodeja eikä näe jäsenmuuttujia:

#include "kellonaika.hh"

int main() {
    Kellonaika aika(23, 59);
    aika.tulosta();
    aika.tiktok();
    aika.tulosta();
}

tiktok() siirtää aikaa minuutilla eteenpäin. Tulostus muotoillaan <iomanip>-kirjaston toiminnoilla:

#include "kellonaika.hh"
#include <iostream>
#include <iomanip>

using namespace std;

Kellonaika::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;
}

iomanip-kirjaston tulostusmuotoiluun ei tarvitse tässä keskittyä. Tärkeää on, että metodit toimivat luokan julkisen rajapinnan kautta.

Versio 2: kuluneet minuutit

Toisessa toteutuksessa kellonaika tallennetaan yhteen jäsenmuuttujaan: minuutteina vuorokauden alusta. Julkiset metodit ja niiden kutsut pysyvät samoina.

class Kellonaika {
public:
    Kellonaika(int tunti, int minuutti);
    void tiktok();
    void tulosta() const;

private:
    int kuluneet_minuutit_;
    int hae_tunti() const;
    int hae_minuutti() const;
};
Kellonaika::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 {
    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;
}

Nyt tulosta() ei käsittele tallennustapaa suoraan, vaan pyytää tunnit ja minuutit yksityisiltä apumetodeilta. Jos tallennustapa myöhemmin muuttuu, tulostusmetodiin ei tarvitse koskea, kunhan nämä metodit säilyttävät toimintansa.

Tämä on kapseloinnin hyöty: luokan yksityinen toteutus voi muuttua, vaikka sen käyttäjien koodi ja julkinen rajapinta säilyvät ennallaan. Luokan sisällä metoditkin voivat käyttää toisiaan kutsumalla niitä ilman olion nimeä.

Luokkien käytön hyötyjä

  • Yksityinen toteutus voidaan vaihtaa, kunhan julkinen rajapinta säilyy yhteensopivana.
  • Rakentajat ja tilaa muuttavat metodit voivat pitää huolta siitä, ettei olio päädy virheelliseen tilaan.
  • Luokat kokoavat yhteen käsitteeseen liittyvää tietoa ja toimintaa, mikä helpottaa ohjelman ymmärtämistä ja ylläpitoa.
  • Hyvin suunnitellut luokat ovat uudelleenkäytettäviä ja auttavat hallitsemaan ohjelman monimutkaisuutta.

Nämä edut eivät kuulu vain luokille. Myös funktiot tarjoavat rajapinnan: kun niiden parametrit ja paluuarvot ovat selvät, toteutuksen yksityiskohtia ei tarvitse tuntea.

Lisää esimerkkejä

Kurssin esimerkeistä hakemistossa examples/05/students on lisää luokkien käyttöä. Esimerkeissä voi olla metodeja, joiden toteutusta ei vielä tarvitse ymmärtää kokonaan.