C++-kieli‎ > ‎

Perusteita



Olio-ohjelmoinnin peruskäsitteitä

Luokka

Ihmiselle asioiden luokittelu on hyvin luonteenomaista. Jo Platon (Kreikkalainen filosofi 350 ekr) piti tärkeänä olevaisten olentojen ideaa yleensä, ei niinkään yksittäistä olentoa sinänsä. Platonin oppilas Aristoteles jatkoi asioiden luokittelua ja jakoi esim. kasvikunnan kasvit yli 500 luokkaan. Luokittelu helpottaa ajattelua. Jos omistat mersun, sinua pidetään ylempään sosiaaliluokkaan kuuluvana. Jos olet insinööri, sinun oletetaan ajattelevan teknisesti jne. Eläinmaailmasta tiedämme, että susien kuuluu, ollakseen susia, syödä lampaita jne. Teknisten ongelmien ratkaisussa luokka-ajattelu auttaa jäsentämään ongelmaa ja helpottaa päättelemään mille laitteelle, ohjelmalle tai ohjelman osalle minkäkin tehtävän suorittaminen kuuluu. Sinänsä tietokone ei tarvitse luokkakäsitettä, se on apuväline ohjelman kehittäjälle.


Luokan käsite ohjelmoinnissa

Luokan käsite helpottaa asioiden hahmottamista. Ohjelmoitaessa esim. teknistä mittausjärjestelmää, voidaan ongelma jakaa osaongelmiksi, jakamalla ohjelma luokkiin vaikka järjestelmään liittyvien laitteiden tai ohjelman tehtävien perusteella. Useissa tapauksissa järkevien ja tehtävän ratkaisua helpottavien luokkien löytäminen on vaativa ja aikaa vievä työ. Luokkien muodostamiseksi on kehitetty useita ns. CASE-työkaluja, joilla voidaan graafisesti kuvata ohjelman luokkarakennetta, kuitenkaan ne eivät auta itse luokkien keksimisessä, vaan ne on keksittävä itse.


Olio, luokan todellinen edustaja

Olio on luokan todellinen edustaja eli instanssi. Esimerkiksi tämän tekstin kirjoittaja on todennäköisesti ihminen eli luokan ihminen edustaja (instanssi). Yhden luokan perusteella voidaan luoda haluttaessa useita olioita. Luokan edustajat eli oliot ovat periaatteessa samanlaisia, mutta niiden ominaisuuudet voivat erota toisistaan. Kaikkien saman luokan edustajien toiminnot ovat samanlaisia.


Luokka (class)

Luokat ovat abstraktioita, jotka mallintavat reaalimaailman asioita. Luokat tarjoavat palveluita (metodit) ja voivat tallentaa tilansa jäsenmuuttujiin. Luokkien mallintaminen on oma taiteenlajinsa, eikä ole välttämättä oikeata tapaa mallintaa asioita, on vain parempia ja huonompia tapoja.

Ohjelmassa käytettävät luokat esitellään yleensä omissa .h -tiedostoissaan ja luokan metodit toteutetaan omissa .cpp -tiedostoissaan. Toisaalta C++-kääntäjän kannalta ei ole väliä, onko luokat omissa tiedostoissaan vai kaikki samassa. Kun luokkien lukumäärä kasvaa, on erittäin tärkeätä, että ne löytyvät helposti. Jos kaikki luokat ovat samassa tiedostossaan, tämä vaikeuttaa löytämistä huomattavasti.

Luokka esitellään seuraavasti:

class CElain
{
    // Tänne luokan esittely, eli jäsenmuuttujat ja metodit...
} ; // <- Muistakaa puolipiste loppuun!

Metodeihin etuliitteitä ei yleensä laiteta.

Luokan palveluita käytetään olioiden avulla.

On muistettava, että muuttujia ei voi normaalisti alustaa luokan esittelyn yhteydessä, vaan ne täytyy alustaa muodostimessa, jossa oliot luodaan.


Jäsenmuuttujat (Member Variables)

Jäsenmuuttujien esittely

Jäsenmuuttujat esitellään kuten normaalit muuttujat, tosin class {}-rakenteen sisällä. Ne esitellään monesti m_-etuliitteellä. Tämä ei ole mitenkään pakollista, mutta se helpottaa koodin lukemista. Niihin päästään käsiksi luokasta luodun olion kautta tai luokan sisällä normaaliin tapaan. 

Esimerkki 1. Jäsenmuuttujien esittely.

class CElain
{
public: // kts Näkyvyys
    char m_Nimi[100]; // Eläimen nimi, koko 100 merkkiä.
     float m_Massa;     // Eläimen massa.
};

Luokka CElain pitää sisällään kaksi jäsenmuuttujaa: m_Nimi ja m_Massa. Niihin päästään käsiksi luokasta luodun olion kautta. Kts esimerkki 4.

Jäsenmuuttujien näkyvyys

Jäsenmuuttujaa voi muuttaa luokan metodi, luokan perillisen metodi tai joku ulkopuolinen funktio. Riippuen siitä mitkä tahot pääsevät jäsenmuuttujaan kiinni, puhutaan yksityisestä, suojatusta tai julkisesta jäsenmuuttujasta.

Jäsenmuuttujien näkyvyys eri paikoissa

luokan funktiot näkevätperilliset näkevät ulkopuoliset näkevät
privatekylläeiei
protectedkylläkylläei
publickylläkylläkyllä

Esimerkki 2. Jäsenmuuttujien näkyvyys.

class CElain
{
private: // saatavissa vain luokan  metodeilla
    char m_Nimi[100];
    float m_Massa;

public: // voidaan käyttää luokan ulkopuolelta
    bool AsetaNimi(char Nimi[]);
    bool KysyNimi(char Nimi[]);
    bool AsetaMassa(int Massa);
    bool KysyMassa();
};

Jäsenmuuttujien käyttö

Jäsenmuuttujiin voidaan viitata luokan sisällä ja riippuen näkyvyydestä, myös luokan ulkopuolelta.

Esimerkki 3. Esimerkin 1 jäsenmuuttujien käsittely luokan sisällä.

class CElain
{
public:
    char m_Nimi[100];             // Eläimen nimi, koko 100 merkkiä.
    float m_Massa;                // Eläimen massa.
    void AsetaArvot(char, float); // Lisätään metodi muuttujien arvojen asettamiseen.
};

// Metodin AsetaArvot() toteutus.
void CElain::AsetaArvot(char Nimi[], float Massa)
{
    strcpy( m_Nimi, Nimi); // Kopioidaan parametrina saatu Nimi jäsenmuuttujaan m_Nimi.
    m_Massa= Massa;      // Sijoitetaan parametrina saatu Massa jäsenmuuttujaan m_Massa.
}

int main()
{
    CElain kissa;

    kissa.AsetaArvot("Miuku", 3.12);
}

Esimerkki 4. Esimerkin 1 jäsenmuuttujien käsittely luokan ulkopuolella.

int main()
{
    CElain kissa;

    kissa.m_Massa = 3.12;
    strcpy(kissa.m_Nimi, "Miuku");
}

HUOM muuttujien pitää olla public-tyyppisiä.

Staattiset jäsenmuuttujat

Joissain tapauksissa luokan olioilla on ominaisuuksia, jotka ovat yhteisiä kaikille olioille, esim. olioiden lukumäärä. Tällaiset muuttujat voidaan luoda staattisina, jolloin jokaisella luokasta tehdyllä oliolla näkyy sama arvo.

Esimerkki 5. Staattisen jäsenmuuttujan käsittely.

#include <iostream>

using namespace std;

class CTyontekija
{
private:
    static int s_Laskuri;       // laskuri laskee työntekijöiden määrää

public:
    CTyontekija();              // kts. Muodostimet ja hajoittimet
    ~CTyontekija();
};

int CTyontekija::s_Laskuri = 0; // Alustetaan staattinen muuttuja

CTyontekija::CTyontekija()
{
    s_Laskuri++;
    cout << "CTyontekija() - s_Laskuri = " << s_Laskuri << endl;
}

CTyontekija::~CTyontekija()
{
    s_Laskuri--;
}

int main()
{
    CTyontekija Tyontekijat[4];
    for (int i=0; i<4; i++) {
        cout << "Tyontekijat[" << i <<"].s_Laskuri = " << Tyontekijat[i].s_Laskuri << endl;
    }
}

Ajakaa yllä oleva esimerkki ja selvittäkää miksi se käyttäytyy sillä tavalla.

Vakiot jäsenmuuttujina

Luokkien jäsenmuuttujat, jotka ovat const-tyyppisiä, tulee aina alustaa initialisointilistassa. Jäsenmuuttujia, jotka on määritelty vakioiksi, ei voi muuttaa ajon aikana.

Esimerkki 6. Vakio jäsenmuuttujan alustaminen.

class CYmpyra
{
private:
    const float m_Pii;        // Vakio, koska piin arvo ei muutu.

public:
    CYmpyra();
};

// Muodostimen toteutus:
CYmpyra::CYmpyra() :
    m_Pii(3.145)              // Kaikki const-tyyppiset muuttujat alustetaan initialisointilistassa.
{
}

Tässä esimerkissä esitellään luokka, jolla on yksi jäsenmuuttuja, joka on vakio. Se täytyy alustaa muodostimen initialisointilistassa.

Esimerkki 7. Olio vakiona jäsenmuuttujana.

class CKannet
{
public:
    CKannet(const char Nimi[])
    {
    }
};

class CKirja
{
private:
    const CKannet m_Kannet;   // Vakio

public:
    CKirja();
};

// Muodostimen toteutus:
CKirja() :
    m_Kannet("Moby Dick")    // Tämä kutsuu CKannet-luokan muodostinta.
{
}

Tässä esimerkissä käytetään koostumista . HUOM! Initialisointilistassa kutsutaan nyt CKannet-luokan muodostinta.

Staattiset vakiot jäsenmuuttujina

Luokkien jäsenmuuttujat, jotka ovat const static-tyyppisiä, pitää alustaa suoraan esittelyssä, sillä ne ovat luokkakohtaisia (static) ja vakioita (const).

Esimerkki 8. Staattisen vakio-jäsenmuuttujan alustaminen.

class CLuokka
{
private:
     const static int VAKIO = -6; // Yksi yleinen tapa on kirjoittaa vakiot ISOILLA KIRJAIMILLA. ei pakollista.

public:
    const int HaeVakio()
    {
        return VAKIO;
    }
};

int main()
{
    CLuokka l;
    printf("VAKIO = %d", l.HaeVakio());
    return 0;
}

Luokan metodit (jäsenfunktiot)

Metodit kirjoitetaan yleensä luokan .cpp -tiedostoon. Tiedoston alkuun sisällytetään #include "tiedosto.h" -lauseella otsikkotiedosto, jossa ko. luokka on esitelty. Kun kirjoitetaan metodin toteutusta .cpp -tiedostoon, tulee muistaa laittaa luokan_nimi:: metodin nimen eteen, esim. CElain::.

Esimerkki 1. CElain luokan esittely ja toteutus.

    Elain.h-tiedosto:

class CElain
{
private:
    char m_Nimi[100];
    float m_Massa;

public:
    void AsetaNimi(char Nimi[]);
    char* KysyNimi();

    void AsetaMassa(float Massa);
    float KysyMassa();
};

    Elain.cpp-tiedosto:

#include "Elain.h"

void CElain::AsetaNimi(char Nimi[])
{
    strcpy(m_Nimi, Nimi); // merkkijonon (char muuttuja[]) kopioiminen tapahtuu strcpy()-funktion avulla.
}

char* CElain::KysyNimi()
{
    return m_Nimi;
}

void CElain::AsetaMassa(float Massa)
{
    m_Massa = Massa;
}

float CElain::KysyMassa()
{
    return m_Massa;
}

Metodien toteutus eroaa tavallisen funktion toteutuksesta siinä, että metodin paluuarvon ja nimen väliin laitetaan luokan nimi, johon metodi kuuluu. Useimmiten jäsenmuuttujat ovat yksityisiä ja siksi niiden hakuun joudutaan tekemään asetus ja hakumetodit. Seuraavassa esimerkki jäsenmuuttujan palauttavasta metodista.

int CElain::KysyMassa()
{
    return m_Massa;
}

Olion metodien käyttö

Muutettaessa olion ominaisuuksia tai haluttaessa käyttää olion metodeita, annetaan ensin olion nimi ja sen jälkeen pisteellä erotettuna metodi.

Koira.AsetaNimi("Rekku");

Kun olio on esitelty osoittimena , käytetään pisteen tilalla nuolta:

pKissa->AsetaNimi("Misse"); 

Näkyvyys

Luokan metodien näkyvyys toimii samalla periaatteella kuin luokan jäsenmuuttujien. Kts. jäsenmuuttujat.

Metodien parametrit

Metodien parametreja määriteltäessä on tärkeätä muistaa asettaa arvot vakioiksi, jos niitä ei haluta muuttaa. Parametrit, joiden muutoksien halutaan näkyvän metodin ulkopuolella, tulee määritellä osoittimiksi tai referensseiksi.

Esimerkki 2: Metodien parametrit.

class CLuokka
{
public:
    void AsetaArvo( const int); // Parametrina const int, koska sen arvoa ei haluta muuttaa
    int PalautaArvo()  const;   // Vakiofunktio, sillä se ei muuta luokan tilaa.

private:
    int m_Arvo;
};

// Toteutukset:

void CLuokka::AsetaArvo( const int Arvo)
{
    m_Arvo = Arvo;
}

int CLuokka::PalautaArvo()  const
{
    return m_Arvo;
}

Ylikuormitus

C++ tarjoaa ohjelmoijalle mahdollisuuden ylikuormittaa funktioita (ja metodeita). Tällä tarkoitetaan sitä, että voi olla samannimisiä funktioita, jotka eroavat parametrien lukumäärässä. Kutsuttava funktio/metodi tunnistetaan parametrien tyypistä. HUOM! palautustyyppiä ei voi ylikuormittaa. Muodostimien kanssa käytetään usein ylikuormitusta.

Esimerkki 3. Metodin ylikuormitus.

class CElain
{
private: // saatavissa vain luokan metodeilla
    char m_Nimi[100];
    float m_Massa;

public: // voidaan käyttää luokan ulkopuolelta
    bool AsetaTiedot(char Nimi[], float Massa); // 'Alkuperäinen metodi'
    bool AsetaTiedot(char Nimi[]); // 1. Ylikirjoitettu  metodi
    bool AsetaTiedot(float Massa); // 2. ylikirjoitettu  metodi
};

bool CElain::AsetaTiedot(char Nimi[], float Massa)
{
    printf("AsetaTiedot(char[], float)\n");
    AsetaTiedot(Nimi);
    AsetaTiedot(Massa);
    return true;
}

bool CElain::AsetaTiedot(char Nimi[])
{
    printf("AsetaTiedot(char[])\n");
    strcpy(m_Nimi, Nimi);
    return true;
}

bool CElain::AsetaTiedot(float Massa)
{
    printf("AsetaTiedot(float)\n");
    m_Massa = Massa;
    return true;
}

int main()
{
    CElain kissa;

    kissa.AsetaTiedot("Miuku", 2.4); // kutsuu metodia AsetaTiedot(char Nimi[], float Massa)
    kissa.AsetaTiedot("Mauku");       // kutsuu metodia AsetaTiedot(char Nimi[])
    kissa.AsetaTiedot(2.4);           // kutsuu metodia AsetaTiedot(float Massa)
}

Oletusparametrit

Metodeille (ja myös funktioille) voidaan antaa oletusparametreja, joita käytetään, ellei metodin kutsussa ole annettu kaikkia parametreja.

Esimerkki 4. Metodin oletusparametrit.

class CKuutio
{
public:
     // Voit kutsua Tilavuus-metodia 1, 2 tai 3:lla parametrilla
    int Tilavuus(int a, int b = 25, int c = 1);
};

int CKuutio::Tilavuus(int a, int b, int c) // Huom, täällä ei enää anneta oletusparametreja!
{
    cout << "pituus=" << a << ", leveys=" << b << ", korkeus=" << c << " - ";
    return (a * b * c);
}

int main()
{
    CKuutio kuutio;

    int koko = kuutio.Tilavuus(100, 50, 20);
    cout << "Ensimmäinen tulos: " << koko << "\n";

    koko = kuutio.Tilavuus(99, 49);
    cout << "Toinen tulos: " << koko << "\n";

    koko = kuutio.Tilavuus(98);
    cout << "Kolmas tulos: " << koko << "\n";
}

Tässä esimerkissä kutsutaan kolme kertaa CKuutio-luokan Tilavuus()-metodia, antaen 3, 2 ja 1 parametria. Kun metodille annetaan vähemmän kuin yksi parametri, niin metodia suoritettaessa parametri c saa arvon 1, kun annetaan kaksi parametria, ja kun annetaan vain yksi parametri, saa c arvon 1 ja b arvon 25.

Vakiofunktiot (const member functions)

Funktiot, jotka eivät muuta luokan muuttujien tilaa voidaan määritellä vakioiksi (const). Tämä mahdollistaa lisäksi funktion kutsumisen const-tyyppisen olion kautta. Jos const:ksi määritelty funktio kuitenkin yrittää muokata luokan jäsenmuuttujaa, antaa kääntäjä virheilmoituksen ja koodi ei käänny.

Yksi olio-ohjelmoinnin periaatteista on sallia vain tarvittavat oikeudet, eli jos luokan jäsenfunktio ei muokkaa luokan tilaa (eli jäsenmuuttujia), niin se tulee merkitä const-tyyppiseksi. Tämä helpottaa koodin tarkistamista virhetilanteissa, sillä osa luokan funktioista voidaan suoraan unohtaa, sillä ne eivät voi muokata luokan tilaa.

Esimerkki 5. Vakiofunktiot (const).

#include <cstdlib>
#include <iostream>

using namespace std;

class CLuokka
{
private:
    int m_Luku;

public:
    CLuokka();
    void Tulosta()  const;
};

CLuokka::CLuokka() :
    m_Luku(0)
{
}

void CLuokka::Tulosta() const
{
    cout << "CLuokka - m_Luku = " << m_Luku << endl;
}

int main()
{
    const CLuokka luokka;
    luokka.Tulosta();     // Voidaan kutsua, sillä funktio on const-funktio.

    return 0;
}

Staattiset funktiot

Funktiot voidaan määritellä staattisiksi, mikä tarkoittaa että funktio silloin ei toimi oliotasolla vaan luokkatasolla. Silloin funktiosta on olemassa vain yksi kopio, kun taas 'normaaleista' funktioista (metodeista) on olemassa yhtä monta kopioita kuin on olioita.

Esimerkki 6. Staattiset funktiot, luokka toteuttaa Singleton-mallin.

#include <cstdlib>
#include <iostream>

using namespace std;

class CLuokka
{
public:
     static CLuokka* Luo();
     static void Tuhoa();
    void Tulosta() { cout << "CLuokka - m_Itse = " << hex << s_Itse << ", this = " << this << endl; }

private:
    CLuokka() {}
    ~CLuokka() {}

private:
     static CLuokka* s_Itse; // Osoitin 'itseensä'.
};

// Staattiset jäsenmuuttujat tulee  luoda globaalissa nimiavaruudessa:
CLuokka* CLuokka::s_Itse = NULL;

CLuokka* CLuokka::Luo()
{
    if (s_Itse == NULL) {
        s_Itse = new CLuokka;
    }
    return s_Itse;
}

void CLuokka::Tuhoa()
{
    if (s_Itse != NULL) {
        delete s_Itse;
        s_Itse = NULL;
    }
}

int main()
{
    CLuokka* luokka = CLuokka::Luo();  // Staattisia funktioita kutsutaan muodossa: LUOKAN_NIMI::FUNKTION_NIMI
    luokka->Tulosta();

    CLuokka* luokka2 = CLuokka::Luo();
    luokka2->Tulosta();

    CLuokka::Tuhoa();

    return 0;
}

Oliot

Luokat tarjoavat palveluita, mutta ne ovat abstraktioita. Jotta luokkia voidaan käyttää, tulee niistä luoda konkreetisia ilmentymiä, olioita. Poikkeuksena tähän sääntöön ovat luokan staattiset metodit.

Esimerkki 1. Luokan esittely.

class CLuokka
{
private:
    int m_Arvo;
public:
    void Tulosta();
};

Esimerkki 2. Olion luonti esimerkin 1 luokasta.

int main()
{
    CLuokka luokka; // Luodaan olio luokasta CLuokka.
    
    return 0;
};

Esimerkki 3. Luokan ja olion ero.

class CKissa
{
private:
    char m_Nimi[100];
    float m_Massa;
public:
    void Aantele();
};

int main()
{
    CKissa Miuku; // Luodaan olio luokasta CKissa.
    CKissa Mauku; // Luodaan olio luokasta CKissa.
}

Tässä esimerkissä esitellään luokka CKissa, joka 'mallintaa' reaalimaailman eläintä kissaa. Siitä on luotu kaksi ilmentymää: Miuku ja Mauku.

Luokan jäsenmuuttujien käyttö

Luokan jäsenmuuttujiin voidaan viitata olion nimellä, jos muuttujien näkyvyys sen sallii.

class CLuokka
{
public:
    int m_Muuttuja;
};

int main()
{
    CLuokka Olio;
    Olio.m_Muuttuja = 10; // Asetetaan Olion jäsenmuuttujan m_Muuttuja arvoksi 10.
}

Enkapsulointi

Enkapsulointi piilottaa toteutukseen liittyvät muuttujat ja metodit muiden luokkien näkyvistä. Enkapsuloitu luokka tarjoaa rajapinnan (interface), jolla luokkaa voidaan käyttää. Enkapsuloinnin avulla voidaan luokan toteutusta muuttaa ilman, että sitä käyttävät tahot joutuvat muuttamaan omaa koodiaan.

Esimerkiksi CElain-luokka:

class CElain
{
private: // privaatti, vain luokan sisällä voidaan muuttaa
    char m_Nimi[100];    // Eläimen nimi
    float m_Massa;       // Eläimen massa, paino

    void TulostaNimi();  // Tulostaa ruudulle eläimen nimen
    void TulostaMassa(); // Tulostaa ruudulle eläimen massan

public: // julkinen, kaikki voi kutsua
    CElain(char Nimi[], float Massa); // Muodostin, parametreina Nimi ja Massa
    bool AsetaNimi(char Nimi[]);      // Asettaa eläimen nimen
    bool AsetaMassa(float Massa);     // Asettaa eläimen massan
    void Tulosta();
};

int main()
{
    CElain kissa("ei nimeä", 0.0);

    kissa.Tulosta();
}

Yllä olevassa esimerkissä Tulosta()-metodi on ainoa julkinen metodi. Jatkossa saatetaan haluta muuttaa luokkaa siten, että siihen lisätään vaikka seuraavat muuttujat: väri, säkäkorkeus, elinalueet. Näille kirjoitetaan TulostaXXX()-metodit. Koska CElain-luokka tarjoaa yhden julkisen Tulosta()-metodin, on helppo tehtävä lisätä nämä uudet muuttujat tulostettaviksi.





Koostuminen

Luokan jäsenmuuttujina voi olla myös muita luokkia. Tätä kutsutaan koostumiseksi.

Esimerkki 1. Koostuminen.

class CProsessori
{
};

class CRAM
{
};

class CTietokone
{
private:
    CProsessori prossu;
    CRAM ram;
};

Tietokone koostuu prosessorista ja emolevystä (+ muista komponenteista). CTietokone-luokka siis koostuu CProsessori ja CRAM -luokista


Koostettujen luokkien käyttö

Koostettuja luokkien metodeja kutsutaan kuten muitakin luokkien, eli olioiden kautta. Eli: olion_nimi.metodin_nimi();

Esimerkki 2. Koostettujen olioiden kutsuminen.

// CProsessori-luokka
class CProsessori
{
public:
    int Laske();
};

// CRAM-luokka
class CRAM
{
public:
    void Talleta();
};

class CTietokone
{
private:
    CProsessori prossu;
    CRAM muisti;

public:
    void Suorita() {
        prossu.Laske();
        muisti.Talleta();
    }
};


Periytyminen

Luokkahierarkiaa suunniteltaessa kannattaa yleiset, usean luokan tarvitsemat toiminnot ja ominaisuudet kerätä omiin luokkiinsa, joista sitten periytetään varsinaiset ohjelmassa käytettävät luokat. Periyttämisestä käytetään myös nimeä erikoistaminen (specialization) ja vastaavasti yhteisten metodien ja muuttujien siirtämistä omaan luokkaansa kutsutaan yleistämiseksi (generalization). Perimimisjärjestyksessä aikaisempaa luokkaa kutsutaan vanhemmaksi (parent) ja siitä perivää luokkaa lapseksi (child).

Periytyminen C++:ssa esitetään seuraavasti:

class Perillinen : [määre] Vanhempi
{
    ...

jossa [määre] on yleensä public (joskus harvoin protected tai private).

Esimerkki 1. Periytyminen.

Seuraavassa on esitetty ensin yleinen kantaluokka CElain:

class CElain
{
private: // saatavissa vain luokan funktioilla
    char m_Kutsumanimi[100];
    int m_Massa;

public: // voidaan käyttää luokan ulkopuolelta
    void AsetaNimi(char Nimi[]);
    char* KysyNimi(char Nimi[]);
    void AsetaMassa(int Massa);
    int KysyMassa();
};

Seuraavaksi on erikoistettu luokka, joka esittää eläintä kissa, siihen sisällytettään kaikki vain kissalle ominaiset piirteet.

class CKissa : public CElain
{
private: // saatavissa vain luokan funktioilla
    

public: // voidaan käyttää luokan ulkopuolelta
    void Kehraa();
}

Luokan CKissa edustaja perii protected ja public-tyyppiset CElain-luokan metodit ja jäsenmuuttujat. Kts. näkyvyys.

Huom! public-määre ennen perittävää luokkaa määrittelee perinnän näkyvyyden. Perinnässä voidaan käyttää myös private ja protected -määreitä, mutta niiden käyttö on erittäin harvinaista.

Vanhemman funktioiden ylikirjoitus perillisessä

Perityn luokan funktoita voidaan ylikirjoittaa perillisessä. Kutsuttaessa funktoita perillisen olion kautta suoritetaan perillisessä ylikirjoitettu funktio.

Esimerkki 2. Funktion ylikirjoitus perillisessä.

#include <iostream>

using namespace std;

class CElain
{
public: // voidaan käyttää luokan ulkopuolelta
    void TulostaLuokanNimi() {
        cout << "Luokan nimi on CElain." << endl;
    }
};

class CLehma : public CElain
{
public:
    void TulostaLuokanNimi() {
        cout << "Luokan nimi on CLehma." << endl;
    }
};

int main()
{
    CLehma lehma;
    lehma.TulostaLuokanNimi();

    CElain elain;
    elain.TulostaLuokanNimi();
}

Testatkaa mitä main():ssa tehdyt funktiokutsut tulostavat.

Moniperiytyminen

Tyypillisesti luokka periytyy suoraan ainoastaan yhdestä luokasta, mutta joskus luokka kannattaa periyttää useista luokista. Kyseessä on silloin moniperiytyminen.

Esimerkiksi, jos ajattelemme pegasusta, lentävää hevosta, joka omaa ominaisuuksia hevosesta ja linnusta. Helpoin tapa toteuttaa edellä kuvattu luokka on periyttää sen ominaisuudet kahdesta luokasta: hevonen ja lintu.

Esimerkki 3. Moniperiytyminen.

class CHevonen
{
    void Aantele() {
        printf("Iii-haa-haa!\n");
    }
};

class CLintu
{
public:
    void Lenna() {
        printf("Lintu lentää\n");
    }
};

class CPegasus :
    public CHevonen,
    public CLintu
{
};

int main()
{
    CPegasus pegasus;

    pegasus.Aantele();
    pegasus.Lenna();
}

Nyt lentävä hevonen perii sekä hevosen että linnun piirteet.

Yksi ongelma moniperiytymisessä tulee vastaan, jos molemmilla perittävillä luokilla on samanniminen metodi. Esimerkiksi Syo()-metodi.

Esimerkki 4. Moniperiytymisen ongelma.

class CHevonen
{
public:
    void Syo() {
    }
};

class CLintu
{
public:
    void Syo() {
    }
};

class CPegasus :
    public CHevonen,
    public CLintu
{
};

int main()
{
    CPegasus pegasus;

    pegasus.Syo();
}

Yllä olevassa esimerkissä C++-kääntäjä ei tiedä kumpaa Syo()-funktiota tarkoitetaan ja se antaa virheilmoituksen.

Ongelma voidaan kiertää määrittelemällä kumpaa Syo()-metodeista halutaan käyttää:

int main()
{
    CPegasus pegasus;

    pegasus. CLintu::Syo();
}

Moniperintä ohjelmointikielissä

Kaikki olio-ohjelmointikielet eivät tue moniperintää, kuten esim Java. Moniperintä on tuettu C++-kielessä.



Abstraktit luokat

Yksi olio-ohjelmoinnin perusajatuksista on abstrahointi (abstraction), eli asioiden käsitteleminen abstraktilla tasolla. 

Esimerkki 1: Abstrakti luokka.

class CElain
{
public:
    virtual ~CElain() {}        // Toteutus tulee olla (tyhjäkin käy).
    virtual void Aantele() = 0; // Aito virtuaalifunktio .
};

Yllä esitellään luokka CElain, jossa on yksi aito virtuaalifunktio, Aantele(). Tämä tarkoittaa, että CElain luokasta ei voida luoda oliota. Aantele()-metodin toteutus tulee olla toteutettuna perivässä luokassa.

Esimerkki 2: Abstraktin luokan toteuttava luokka.

class CKissa : public CElain
{
public:
    void Aantele() {
        printf("Miau!\n");
    }
};

Tässä peritään CElain-luokka ja toteutetaan sen aito virtuaalifunktio Aantele().

Abstraktien luokkien käsittely

Abstrakteista luokista ei voida luoda olioita, ainoastaan niiden perillisistä (olettaen, että ne eivät ole abstrakteja), joita myös kutsutaan toteuttaviksi luokiksi. Abstraktit luokat toimivat rajapintana (interface) toteuttavalle luokalle. Kts polymorfismi.

Esimerkki 3: Olioiden käsittely abstraktin (kanta)luokan avulla.

#include <iostream>

using namespace std;

// Abstrakti luokka, eli kantaluokka.
class CElain
{
public:
    virtual ~CElain() {}    // Toteutus tulee olla (tyhjäkin käy).
    virtual void Syo() = 0; // Aito virtuaalifunktio .
};

// CElain-kantaluokan toteuttava luokka.
class CKissa : public CElain
{
public:
    void Syo() {
        cout << "Kissa syo kalaa...\n";
    }
};

// CElain-kantaluokan toteuttava luokka.
class CKoira : public CElain
{
public:
    void Syo() {
        cout << "Koira syo lihaa...\n";
    }
};

// Luokka johon voidaan tuoda CElain-luokan perillisiä.
class CElainTarha
{
protected:
    CElain* Elaimet[100];
    int ElainLaskuri;

public:
    CElainTarha() : ElainLaskuri(0)
    {
    }

    ~CElainTarha()
    {
        for (int i=0; i<ElainLaskuri; i++) {
            delete Elaimet[i];
        }
    }

    bool LisaaElain(CElain* Elain) {
        if (ElainLaskuri < 100) {
            Elaimet[ElainLaskuri] = Elain;
            ElainLaskuri++;
            return true;
        } else {
            return false;
        }
    }

    void SyotaElaimet() {
        for (int i=0; i<ElainLaskuri; i++) {
            Elaimet[i]->Syo();
        }
    }
};

int main()
{
    CElainTarha ElainTarha;

    ElainTarha.LisaaElain(new CKissa);
    ElainTarha.LisaaElain(new CKoira);
    ElainTarha.LisaaElain(new CKissa);
    ElainTarha.LisaaElain(new CKoira);
    ElainTarha.LisaaElain(new CKissa);

    ElainTarha.SyotaElaimet();
}

Esimerkki tulostaa seuraavat rivit:

Kissa syo kalaa...
Koira syo lihaa...
Kissa syo kalaa...
Koira syo lihaa...
Kissa syo kalaa...

Esimerkissä luodaan kantaluokka, CElain ja siitä periytetään kaksi perillistä, CKissa ja CKoira. Lisäksi luodaan CElainTarha-luokka, johon voidaan tuoda CElain-tyyppisiä osoittimia olioihin, riippumatta siitä, onko ne luotu CKissa vai CKoira -luokista. Tämä onnistuu siksi, että molemmilla luokilla on sama kantaluokka, CElain. Kutsumme lopuksi CElainTarhan metodia SyotaElaimet(), joka 'syöttää' jokaista eläintä, eli kutsuu sekä kissojen että koirien Syo()-metodeita.

Etuna tässä toteutustavassa on, että jatkossa voidaan luoda uusi CElain-luokan perillinen, esim. CKirahvi. Tästä luokasta tehty olio voidaan viedä CElainTarha-luokkaan,  ilman että sen koodia tarvitsee muuttaa ollenkaan.




Polymorfismi

Polymorfismilla tarkoitetaan olioiden käsittelyä kantaluokan kautta. Kantaluokka on luokka, jonka olion luokka on perinyt. Täten kantaluokka muodostaa yhteisen rajapinnan perillisten käsittelyyn.

Yksi tapa hahmottaa polymorfismi on ajatella olioita substantiiveina. Esimerkiksi esineitä, jotka ovat pöydällä: kahvikuppi, lehti, lautanen. Kaikkiin näihin voidaan soveltaa verbejä, kuten kaataa kuppi, lukea lehti, hajoittaa lautanen, jne. On myös olemassa verbejä, jotka soveltuvat lähes kaikkiin substantiiveihin, kuten hae, aseta, siirrä. Polymorfismilla tarkoitetaan 'verbejä' eli funktioita, jotka tekevät jotain 'substantiiveille', eli erilaisille objekteille. Tämä onnistuu ohjelmointikielissä perinnän kautta, olioita voidaan käsitellä tietämättä eksaktia luokkaa, josta ne on luotu, kunhan ne kaikki omaavat saman kantaluokan.

Esimerkki 1: Olioden käsittely kantaluokan kautta.

#include <iostream>

using namespace std;

// Kantaluokka
class CElain
{
public:
    virtual ~CElain() {}        // Toteutus tulee olla (tyhjäkin käy).
    virtual void Aantele() = 0;  // Aito virtuaalifunktio.
};

// Perillinen, joka toteuttaa Aantele()-funktion
class CKissa : public CElain
{
public:
    void Aantele() {
        cout << "Miau!" << endl;
    }
};

// Perillinen, joka toteuttaa Aantele()-funktion
class CKoira : public CElain
{
public:
    void Aantele() {
        cout << "Vuh!" << endl;
    }
};

int main()
{
    CElain* elain;           // Luodaan CELain-tyyppinen osoitin.

    CKissa kissa;           // Luodaan uusi olio CKissa-luokasta.

    elain = &kissa;         // Pistetään CElain-tyyppinen osoitin osoittamaan olioon kissa.
    elain->Aantele();       // Kutsutaan CKissa-luokassa toteutettua Aantele()-metodia.

    CKoira koira;           // Luodaa uusi olio CKoira-luokasta.

    elain = &koira;          // Pistetään CElain- tyyppinen osoitin  osoittamaan olioon koira.
    elain->Aantele();       // Kutsutaan CKoira-luokassa toteutettua Aantele()-metodia.
};

Esimerkissä luodaan oliot CKissa- sekä CKoira-luokista. Lisäksi luodaan CElain-tyyppinen osoitin. Tämän osoittimen kautta voidaan käsitellä kaikkia CElain-luokan esittelemiä (public ja protected) metodeja, kuten Aantele()-metodia. CElain-luokka toimii yhteisenä rajapintana CKissa ja CKoira -luokille.


Muodostimet (Constructor)

Aina, kun luodaan uusi olio suoritetaan automaattisesti muodostin-funktio. Lisäksi kutsutaan mahdollisen vanhemman muodostinta, sekä kaikkien olioiden muodostimia, mistä luokka koostuu. Muodostimen nimi on sama kuin luokan nimi.

Esimerkki 1: Muodostin.

class CElain
{
public:
     CElain(); // Muodostin
};

Yllä olevassa esimerkissä on esitelty parametriton muodostin (ilman toteutusta).

Muodostimessa voidaan antaa oliolle alkuarvoja, varata muistia jne. Jos muodostimelle halutaan antaa parametreja, laitetaan ne sulkuihin, kuten mihin tahansa funktioon. Muodostin ei palauta mitään, joten nimen eteen ei saa laittaa mitään tyyppiä, ei edes void-määrittelyä.

Esimerkki 2: Luokkaan CElain on lisätty muodostin, joka antaa luokan oliolle heti nimen ja massan.

class CElain
{
private:
    char m_Nimi[100];
    float m_Massa;

public:
     CElain(char Nimi[], float Massa); // Muodostin

    bool AsetaNimi(char Nimi[]);
    bool KysyNimi(char Nimi[]);

    bool AsetaMassa(float Massa);
    bool KysyMassa();
};

Muodostimelle täytyy tehdä vielä toteutus, joka voi olla esim. seuraavanlainen:

CElain::CElain(char Nimi[], float Massa)
{
    strcpy(m_Nimi, Nimi);
    m_Massa = Massa;
}

Luokalla on aina oletusmuodostin, esim CElain::CElain(), mutta jos ohjelmoija tekee yhdenkin oman muodostimen ei oletusmuodostinta voida enää käyttää. Yllä olevassa esimerkissä tulisi kaikki CElain-luokan oliot muodostaa antamalla niille nimi ja massa. Esimerkiksi:

CElain kissa("misu", 5.2);

Mutta koska oletusmuodostinta ei voida enää käyttää, seuraava ei ole validia koodia:

CElain kissa;

Huom! Jos parametritonta muodostinta halutaan käyttää, tulee myös se lisätä luokkaan:

class CElain {
...
public:
     CElain();
    CElain(char Nimi[], float Massa);
...

Muodostimen initialisointilista (Initialization List)

C++-kielessä luokan jäsenmuuttujat on tarkoitus alustaa ns. initialisointilistassa. Jotkin muuttujatyypit, kuten vakiomuuttujat ja referenssit, tulee alustaa initialisointilistassa. Lisäksi joissain tapauksissa initialisointilistassa alustaminen on nopeampaa kuin muodostimessa 'normaaliin tapaan'. Tämä johtuu siitä, että kun kopioidaan ei sisäänrakennettuja tyyppejä (int, float, jne) joudutaan 'normaaliin tapaan' tehtäessä muodostamaan tilapäiskopio kopioitavasta objektista. Tätä ei tapahdu initialisointilistassa, jos

Esimerkki 3: Luokan jäsenmuuttujan alustaminen initialisointilistassa.

class CElain
{
public:
    CElain();
    void Tulosta();

private:
    int m_Muuttuja;
};

CElain::CElain() :
    m_Muuttuja(0) // Alustetaan luokan jäsenmuuttuja m_Muuttuja arvoon 0.
{
}

void CElain::Tulosta()
{
    cout << "m_Muuttuja = " << m_Muuttuja << endl;
}

int main()
{
    CElain kissa;
    kissa.Tulosta();
}

Hajoittimet (Destructor)

Hajoittimen nimi on muotoa mato(~) ja sen perään luokan nimi, esim. ~CElain().

Hajoittimen ominaisuuksia:

  • Hajoittimessa tulee vapauttaa muodostimessa varatut muistit.
  • Kutsutaan automaattisesti, kuten muodostintakin.
  • Hajoittimelle ei voi antaa parametreja.
  • Vaikka muodostin on ylikirjoitettu, voidaan silti käyttää oletushajoitinta.

  Kopiomuodostimet (Copy Constructor)

Kopiomuodostin on muodostin, jolle annetaan parametrina referenssi itsensä tyyppiseen olioon. Kopiomuodostimen idea on mahdollistaa olion monistaminen eli kopioiminen. C++ tarjoaa oletuskopiomuodostimen joka osaa tehdä matalan kopioinnin (shallow copy), tarkoittaen että se kopioi kaikkien jäsenmuuttujien arvot, myös osoittimien, mikä on ongelma, kts. Esimerkki 5.

Esimerkki 4: Kopiomuodostimen esittely.

class CElain
{
private:
    float m_Massa;

public:
    CElain() {}            // Tyhjä oletusmuodostin.
    CElain( const CElain&); // Kopiomuodostin
};

Yllä olevassa esimerkissä on esitelty kopiomuodostin (ilman toteutusta). Toteutus voisi olla seuraavanlainen:

CElain::CElain( const CElain& Instanssi) {
    this->m_Massa = Instanssi.m_Massa; // Kopioidaan annetun olion muuttujan m_Massa arvo omaamme.
}

Yllä annettu toteutus on itse asiassa sama kuin oletuskopiomuodostin. Se kopioi kaikkien jäsenmuuttujien arvot.

Esimerkki 5: Kopiomuodostin, kun luokassa on osoittimia.

class CElain
{
private:
    float m_Massa;
    char* m_Nimi;                             // Osoitin.

public:
    CElain(char Nimi[])
    {
        m_Nimi = new char[strlen(Nimi) + 1];  // Varataan muistia, annetun merkkijonon verran.
    }

    ~CElain()
    {
        delete [] m_Nimi;                     // Vapautetaan varattu muisti.
    }

    CElain() { }
    CElain(const CElain& Instanssi)
    {
        this->m_Massa = Instanssi.m_Massa;
        this->m_Nimi = Instanssi.m_Nimi;     // Tässä kopioidaan ainoastaan osoittimen arvo, ei itse muistia!
    }
};

Yllä oleva kopiomuodostin (joka siis on kuten oletuskopiomuodostin) aiheuttaa ongelmia. Ajatellaan seuraavaa tilannetta:

  • Luodaan ensimmäinen olio, sille annetaan nimi ja varataan muodostimessa tarvittavan määrän muistia nimeä varten.
  • Luodaan toinen olio, käyttämällä kopiomuodostinta, antamalla ensimmäinen olio parametrina.
  • Nyt molemmat oliot omaavat saman massan ARVON ja saman osoittimen ARVON muistiin, jossa sijaitsee nimi. Siis molemmat oliot osoittavat samaan muistialueeseen.
  • Tuhotaan ensimmäinen olio, jolloin kutsutaan sen hajoitin, joka vapauttaa varatun muistin.
  • Nyt meillä on enään toisena luoto olio, jonka m_Nimi osoittaa muistiin, joka juuri vapautettiin!
  • --> Meillä on katastrofi käsissä!

Yllä kuvatun ongelman välttämiseksi voimme kirjoittaa oman kopiomuodostimen, jossa suoritetaan syvä kopiointi (deep copy), eli varataan tarvittava muisti ja kopioidaan merkkijono sinne:

class CElain
{
...
    CElain(CElain& Instanssi)
    {
        this->m_Massa = Instanssi.m_Massa;
        this->m_Nimi = new char[strlen(Instanssi.m_Nimi) + 1]; // Varataan tarvittava muisti merkkijonolle.
         strcpy(this->m_Nimi, Instanssi.m_Nimi);                // Tässä kopioidaan merkkijono.
    }
};

Tämä kopiomuodostin kopioi osoittimen sisältämän muistin oikein. Nyt voidaan muodostaa haluttu määrä kopioita ja ne käyttäytyvät kuten pitääkin.



Taulukkomuuttujat, kertausta C-ohjelmoinnista

Taulukot ovat kokoelmia samantyyppisiä muuttujia. Luodessa määritellään kuinka monta alkiota taulukossa on. Tämän jälkeen voidaan taulukon kaikkiin alkioihin viitata []-operaattorin avulla. Huom indeksit alkavat nollasta!

Esimerkki 1: Yksiulotteisen taulukon luonti ja käsittely.

#include <iostream>

using namespace std;

int main()
{
    // Luodaan int-taulukko viidelle alkiolle:
    int OmaTaulukko[5];

    // Asetetaan taulukon ensimmäinen alkio arvoon 1234:
    OmaTaulukko[0] = 1234;

    // Taulukon viimeisen alkion tulostaminen (Huom! viimeisen alkion indeksi on neljä, sillä indeksointi alkaa nollasta):
    cout << "OmaTaulukko[4] = " << OmaTaulukko[4] << endl;

    // Taulukon kaikkien alkioiden tulostaminen:
    for (int i=0; i<5; i++) {
        cout << "OmaTaulukko[" << i << "] = " << OmaTaulukko[i] << endl;
    }
}

Taulukko voidaan myös luonnin yhteydessä alustaa alkuarvoihin.

int OmaTaulukko[5] = { -1, -2, -3, -4, -5 };

Tämä alustaa taulukon alkiot järjestyksessä ensimmäisestä viimeiseen arvoihin -1, -2...-5.


Kaksiuloitteinen taulukko

int OmaTaulukko[5][4];

Voidaan ymmärtää, että ensimmäinen on rivi-indeksi ja toinen on sarakeindeksi. Huomaa, että ensimmäinen indeksi on välillä 0...4 ja toinen 0...3. Taulukon haluttuun paikkaan päästään indeksien avulla seuraavasti:

int Arvo;
Arvo = OmaTaulukko[1][2];

Pelkkä taulukon nimi on osoitin taulukon alkuun. Eli kaksi seuraavaa tapaa osoittaa samaan alkioon:

Arvo = **OmaTaulukko;

tai

Arvo = OmaTaulukko[0][0];

Haluttaessa osoittaa toista taulukon alkiota, voidaan se tehdä seuraavasti:

int *Osoitin;
Osoitin = OmaTaulukko;
Osoitin++; // lisätään osoitetta yhdellä
Arvo = *Osoitin;

Osoitin ”juoksee” taulukon läpi rivi kerrallaan.

Testattava ohjelma:

#include <stdio.h>

int main()
{
    int OmaTaulukko[5][4] = { // Alustetaan taulukko
        { 1,2,3,4 },
        { 5,6,7,8 },
        { 9,10,11,12 },
        { 13,14,15,16 },
        { 17,18,19,20 }
    };

    int Arvo;
    Arvo = OmaTaulukko[1][2];  printf("Arvo = %d\n", Arvo);
    Arvo = **OmaTaulukko;      printf("Arvo = %d\n", Arvo);
    Arvo = OmaTaulukko[0][0];  printf("Arvo = %d\n", Arvo);

    int *Osoitin;
    Osoitin = OmaTaulukko;
    Osoitin++; // lisätään osoitetta yhdellä
    Arvo = *Osoitin;           printf("Arvo = %d", Arvo);
}

Taulukoiden luominen dynaamisella muistinvarauksella

Taulukoita voidaan luoda myös dynaamiseen muistiin. Oikeastaan kaikki yhtä tietotyyppiä suuremmat varaukset ovat taulukoita, tarkoittaen että niiden jokaiseen alkioon päästään käsiksi []-operaattorilla.

float* DynTaulu = new float[5]; // Luodaan viiden alkion kokoinen taulukko float-tyyppejä.

DynTaulu[3] = -12.3;             // Asetetaan neljäs alkio arvoon -12.3.

delete [] DynTaulu;              // Vapautetaan varattu muisti, HUOM! [] kun vapautetaan taulukko!

Vakiot ja muualla määrättävät muuttujat


Vakiot

Sellaiset tiedot, joita ei haluta muuttaa ohjelman suorituksen aikana, kannattaa määritellä vakioiksi. Esimerkiksi pii on koko ohjelman ajan sama.

C:ssä käytetään #define -makroa vakioiden määrittelyyn:

#define PII 3.14

Edellämainitulla tiedolla ei kuitenkaan ole tyyppiä ja siksi C++:ssa tulisi käyttää määrittelyä:

const float Pii = 3.14;

const-avainsana kertoo, että Pii on vakio, eikä sitä voida muuttaa määrittelyn jälkeen.


Muuttuva tieto (volatile)

Kun tietoa luetaan esim. joltain laiteportilta, tulee se tehdä seuraavasti:

const volatile unsigned char *port=0x30;

Ohjelma lukee portilta, jonka heksadesimaalinen osoite on 0x30, arvon, jonka ohjelma ymmärtää vakiona ja siten se kykenee ainoastaan käyttämään ko. muuttujan arvoa, muttei muuttamaan sitä. Volatile-määre tarkoittaa sitä, että muuttujan arvo päivitetään joka viittauskerran yhteydessä, sillä tieto voi muuttua milloin vain.


Esikääntäjän ohjaaminen

Seuraavassa esikääntäjälle ohjeet siitä mitä ohjelman osia otetaan mukaan käännökseen:

#define LINUX // Määritellään LINUX

#ifdef LINUX
    // tämä mukaan jos LINUX on määritelty
#else
    // tämä mukaan jos LINUX ei ole määritelty
#endif // LINUX

Toinen yleinen tapa on käänteinen edelliseen verrattuna, eli testataan EIKÖ LINUX ole määritelty:

#define LINUX // Määritellään LINUX

#ifndef LINUX
    // tämä mukaan jos LINUX ei ole määritelty
#else
    // tämä mukaan jos LINUX on määritelty
#endif // !LINUX

Kun samaa header-tiedostoa käyttää useampi käännösyksikkö (cpp-tiedosto) tulee huolehtia, että headerin sisältö tulee ohjelmaan vain kerran (muuten tulee päällekkäisiä määrityksiä). Se hoidetaan esikääntäjää ohjaamalla.

#ifndef Luokka_h
#define Luokka_h

// Headerin sisältö tulee #ifndef ja #endif väliin. Tyyliin:
// class CLuokka
// {
// ...
// };

#endif // Luokka_h

Tämän kun lisää jokaiseen header-tiedostoon, niin välttyy ongelmilta. Jotkin C++ kehitysympäristöt lisäävät nämä automaattisesti ja Microsoft Visual Studio lisää oman #pragma once -määrittelynsä joka ajaa saman asia, eikä luultavastikaan toimi muissa kääntäjissä (ei varmistettu).


Nimiavaruus (namespace)

Nimiavaruudella tarkoitetaan keinoa sitoa tietty joukko nimiä yhteen nippuun, joka voidaan ottaa käyttöön kokonaan tai nimi kerrallaan. Tämä keino on tehty nimiavaruuden ns. saastumisen estämiseksi.

Esimerkiksi yrityksen koodissa voisi olla CNet-luokka, jolla hoidetaan verkkoliikenne. Myöhemmin yritys hankkii CANopen-protokollapinon (LINKITÄ), jossa on myös CNet-luokka. Jos kumpikaan tahoista ei ole paketoinut koodiaan tiettyyn nimiavaruuteen ei koodia voida kääntää, sillä kääntäjä näkee kaksi saman nimistä luokkaa. Jos taas jompi kumpi tai molemmat on omassa nimiavaruudessaan, voidaan ongelmilta välttyä.

Alla esimerkki kahden eri nimiavaruuden käytöstä (molemmissa on määritelty objekti nimeltä cin):

kuva nimiavaruudesta
using namespace std;

int main()
{
    int luku;
    cin >> luku;
}
using namespace oma;

int main()
{
    int luku;
    cin >> luku;
}

Vasemman puoleinen koodi käyttää std-nimiavaruuden cin-muuttujaa ja oikeanpuoleinen taas oma-nimiavaruuden cin-muuttujaa.


Osoitin  ja referenssi

C-kielen osoittimet


Referenssi

Referenssi toimii monessa suhteessa kuten osoitin, mutta ehkä suurin ero on siinä, että referenssin kohdetta ei voi muuttaa. Kun osoitin voi osoittaa mihin tahansa (sillä se on vain muistiosoite), niin referenssin kohde pitää määritellä aina luomishetkellä, eikä sitä voi sen jälkeen vaihtaa. Referenssit ovat huomattavasti turvallisempia kuin osoittimet. Lisäksi niiden käyttö on mukavampaa, sillä niiden kanssa ei tarvitse tehdä muutoksia viitteestä muistipaikaksi tai toisin päin. Lisäksi oliota käsitellään pisteen avulla, kuten staattiseen muistiin luotuja olioita.

Esimerkki 1: Referenssi tulee alustaa luontihetkellä.

int main()
{
    int luku1 = 34;
    int& ref1 = luku1; // Toimii
    printf("ref = %d", ref1);
}

Esimerkki 2: Referenssin alustus, joka EI toimi.

int main()
{
    luku1 = 34;
    int& ref0;     // Ei toimi!
    ref0 = luku1; // Ei toimi!
    printf("ref = %d", ref0);
}

Esimerkki 3: Referenssi funktion parametrina.

#include <iostream>

using namespace std;

void vaihda( int &x, int &y); // esitellään parametrit viittauksina

void main()
{
    int x = 5, y = 10;
    cout << "Main-funktio. Ennen vaihda-funktion kutsua, x: " << x << " y: " << y << "\n";
    vaihda(x, y);
    cout << "Main-funktio. Vaihda-funktion kutsun jälkeen, x: " << x << " y: " << y << "\n";
    cin >> x;
}

void vaihda( int &x, int &y)
{
    int apu;
    cout << "Vaihda-funktio. Ennen vaihtoa, x: " << x << " y: " << y << "\n";
    apu = x;
    x = y;
    y = apu;
    cout << "Vaihda-funktio. Vaihdon jälkeen, x: " << x << " y: " << y << "\n";
}

Esimerkissä 3 esitellään funktio vaihda(...), jonka parametreina on kaksi int-tyyppistä referenssiä. Huomion arvoista on funktion kutsu : vaihda(x, y);

Se eroaa tapauksesta, jossa parametri olisi osoitin. Toinen huomioitava seikka on se, miten refensseihin viitataan vaihda()-funktion sisällä. Ne käyttäytyvät kuten normaali muuttuja, sillä poikkeuksella että funktion sisällä tehdyt muutokset näkyvät funktion ulkopuolella.

Luokan referenssimuuttujat

Jos luokassa on referenssi jäsenmuuttujana, tulee se alustaa muodostimen initialisointilistassa.

Esimerkki 4: Referenssi luokan jäsenmuuttujana.

class CElain
{
private:
    int m_Muuttuja;

public:
    CElain();
    int& JulkMuuttuja;       // Referenssi int-tyyppiseen muuttujaa, alustetaan muodostimen initisointilistassa.
};

CElain::CElain() :
    m_Muuttuja(0),
    JulkMuuttuja(m_Muuttuja) // Alustetaan referenssi JulkMuuttuja osoittamaan m_Muuttuja-muuttujaan.
{
}

Referenssi olioon

Aivan kuten sisäänrakennettujen tietotyyppien (char, int, float, jne) kanssa, niin referenssi voi myös osoittaa olioon.

Esimerkki 5. Referenssi olioon.

class CAjoneuvo
{
public:
    virtual int Liiku(const int) const = 0;
};

class CRekka : public CAjoneuvo
{
public:
    int Liiku(const int Matka_m) const { /* toteutus */ }
};

class CHenkiloAuto : public CAjoneuvo
{
public:
    int Liiku(const int Matka_m) const { /* toteutus */ }
};

class CKatsastus
{
public:
    bool Katsasta( const CAjoneuvo& ajoneuvo) { /* toteutus */ } // Parametrina CAjoneuvo-tyyppinen referenssi. CAjoneuvo-luokan perilliset käyvät parametreiksi.

private:
    bool TestaaAjoa(const CAjoneuvo& ajoneuvo)
    {
        int AjettuMatka = ajoneuvo.Liiku(1000); // Kutsutaan metodia referenssin kautta.
        if (AjettuMatka == 1000)
            return true;
        else
            return false;
    }
};

int main()
{
    CKatsastus katsastusasema;

    CRekka rekka;
    CHenkiloAuto henkiloauto;

    katsastusasema.Katsasta(rekka);       // Välitetään referenssi Katsasta()-funktiolle.
    katsastusasema.Katsasta(henkiloauto); // Välitetään referenssi Katsasta () -funktiolle.
}

Tekstin tulostus ja lukeminen

C-kielessä tämä toteutettin printf() ja scanf() -funktioiden avulla. C++ taas on oliokieli, joten on luonnollista että tekstin tulostus ja lukeminen käyttäjältä on toteutettu olioilla. C++ tarjoaa iostream-headerissa oliot cout, cin ja cerr. Näiden avulla voidaan tulostaa ruudulle tekstiä.

Etuna cout ja cin-olioiden käyttöllä on, että ne tuntevat tietotyypit, mitä niille annetaan. Tämä tarkoittaa, ettei ohjelmoijan enään tarvitse muistaa kaikkia %s, %d, jne tulostusmääreitä. Lisäksi voidaan toteuttaa omaan luokkaan oma operaattori <<, mutta siitä lisää myöhemmin.

Esimerkki 1: Tekstin syöttö ja tulostus.

#include <iostream>
#include <string>

using namespace std; // Tarvitaan, sillä tässä nimiavaruudessa on määritelty cin ja cout.

int main()
{
    string nimi, sukunimi;
    int ID;

    // Syötteen lukeminen käyttäjältä:
    cout << "anna etunimesi: ";
    cin >> nimi;

    cout << "anna sukunimesi: ";
    cin >> sukunimi;

    cout << "anna ID numero ";
    cin >> ID;

    // Tulostus ruudulle:
    cout << "Terve " << nimi << " " << sukunimi <<", ID:si on " << ID << endl;

    return 0;
}

Esimerkki 2: lukujen näyttö eri lukujärjestelmissä.

#include <iostream>

using namespace std; // Tarvitaan, sillä tässä nimiavaruudessa on määriteltu cin ja cout.

int main()
{
    int Arvo = 0;
    cout << "ANNA JOKIN LUKU: ";
    cin >> Arvo;
    do {
        cout << "Oletus: " << Arvo << endl;
        cout << "Desimaali: " << dec << Arvo << endl;
        cout << "Oktaali: " << oct << Arvo << endl;
        cout << "Heksa: " << hex << Arvo << endl;
         // showbase näyttää heksa ja oktalukujen etuliitteet
        cout << "Heksa ja oktaluvut etuliitteen kanssa:" << showbase << endl;
        cout << "Dec: " << dec << Arvo << endl;
        cout << "Oktaali: " << oct << Arvo << endl;
        cout << "Heksa: " << hex << Arvo << endl;
        cout << "ja takaisin" << noshowbase << endl;

        cout << "ANNA JOKIN LUKU: ";
        cin >> Arvo;
    } while (Arvo != 1 && Arvo != 0);

    return 1;
}

 Arvojen testaaminen (assert)

Yksi tehokkaista ohjelmoijan apuvälineistä on assert()-funktio. Sen avulla voidaan tarkastaa ehtoja. Se löytyy cassert-tiedostosta.

Hyvä puoli assert()-funktiossa on että se voidaan poistaa lopullisesta ohjelmasta asettamalla #define NDEBUG -määritys ennen #include <cassert>:ia, jolloin kaikki assert()-funktion kutsut muuttuvat ei-operaatioiksi eli ne eivät tule mukaan käännettyyn ohjelmakoodiin.

assert():in syntaksi:

void assert(int expression);

assert()-funktio testaa parametrinsa arvon ja jos se on tosi, ohjelma jatkaa toimintaansa. Silloin kun parametri on epätosi se tulostaa virheilmoituksen ja tiedoston ja rivin missä assert()-funktiota kutsuttiin ja lopettaa ohjelman.

Esimerkki 1. assert():in käyttö.

#include <iostream>
#include <cassert>    // assert():in määrittely löytyy täältä.

using namespace std;

int main()
{
    cout << "Anna positiivinen luku: ";
    int luku;
    cin >> luku;
    assert(luku >= 0); // Testataan onko annettu luku positiivinen.
    cout << "Antamasi luku (" << luku << ") on positiivinen!" << endl;
}

Testatkaa yllä olevaa koodia!



Attachments (0)

Attach a file:

Comments (0)

_displayNameOrEmail_ - _time_ - Remove

_text_

Subscribe to page changes
Page settings
Print
Move
Delete
Subscribe to site changes
Site sharing
Sitemap

Comments