Pekka Alaluukas /C++ ohjelmointi
Olio-ohjelmointi
Johdanto

Tässä oppaassa käsitellään C++ kieltä ja sen olio-ominaisuuksia. Oppaan lähtökohtana, on että lukija on ohjelmoinut C-kielellä.

Aiheeseen liittyviä video-oppaita löytyy sivulta https://www.youtube.com/playlist?list=PLWl0bS7jZq9_6i4B1l4Im6sx9DKwUo7OU

Oppaaseen liittyvien esimerkkien lähdekoodit löytyy sivulta https://github.com/orgs/olio-kurssi/repositories.

Olio-ohjelmoinnin tärkeimmät käsitteet ovat luokka ja olio. Voisit ajatella, että luokka on ohjelmoijan tekemä uusi muuttujatyyppi ja olio muuttuja, jonka tyyppinä on tuo luokka. Voit siis verrata sitä tilanteeseen, jossa määrittelet muuttujan lauseella int age;. Samalla tavalla määrittelet olion lauseella Student myObject;. Sinun on kuitenkin itse luotava tuo luokka nimeltään Student.

Milloin on tarpeen luoda luokka ja olio?
Esimerkiksi teet sovelluksen, jossa käsittelet opiskelijoiden tietoja. Jokaiselle opiskelijalle määritellään nimi, syntymävuosi, osoite, sähköpostiosoite ja puhelinnumero. Yhdelle opiskelijalle sinun on luotava 5 muuttujaa. Jos käsittelet 10 opiskelijan tietoja, sinun on luotava 50 muuttujaa.

Edellistä parempi ratkaisu on luoda tietue, jossa määritellään nuo 5 muuttujaa. Ja sitten luot 10 muuttujaa, joiden tyyppinä on tuo tietue.

Miksi luoda luokka, jos tietuekin toimii?
Luokan sisään voit sijoittaa myös metodeja eli funktioita. Niiden avulla voit esimerkiksi antaa edellä mainutuille 5:lle muuttujalle arvoja hallitummin. Ja voit luoda metodeja jotka tulostavat noiden muuttujien arvoja haluamallasi tavalla.

Kun tehdään graafisia sovelluksia tarvitaan paljon buttoneita, tekstikenttiä, labeleita, alasvetovalikoita jne. Yleensä käytetään jotain valmiita kirjastoja tai frameworkkejä. Niissä on valmiiksi tehtynä esimerkiksi Button luokka, jolla on ominaisuuksia: buttonissa oleva teksti, klikkauksen aiheuttama tapahtuma, buttonin väri, reunojen tyyli jne. Aina kun laitamme omaan sovellukseemme buttonin, luomme Button luokan olion.

Mikä on Qt?
Qt on framework jolla voidaan luoda graafisia sovelluksia. Qt sisältää joukon valmiiksi tehtyjä luokkia, joita voimme hyödyntää. Kun asennat Qt:n voit asentaa myös Qt Creatorin. Qt Creator on graafinen sovellus, jolla voidaan luoda Qt-sovelluksia ja myös C- ja C++-sovelluksia.

Aluksi on syytä tutustua kuinka C++ kielessä tulostetaan konsolin ruudulle ja kuinka sieltä luetaan käyttäjän antamaa dataa. Voit toki käyttää C-kielestä tuttuja funktioita printf ja scanf, mutta niiden sijaan käytetään yleensä olioita: cout ja cin. Molemmat ovat iostream luokan olioita, joten niitä käyttäessäsi sinun tulee lisätä kooditiedostoon alkuun rivi

#include <iostream>

C++ kielessä voidaan käyttää merkkijonoille muuttujatyyppiä string, jota C-kielessä ei ole. C-kielessähän merkkijonoille käytettiin char -taulukkoja. Tuo string, samoin kuin cout ja cin on määritetty nimiavaruudessa eli namespace:ssa nimeltään std.

Voisit tehdä lyhyen ohjelman koodilla

#include <iostream>

int main()
{
    std::string name;
    std::cout<<"Kerro nimesi"<<std::endl;
    std::cin>>name;
    std::cout<<"Terve "<<name<<std::endl;
    return 0;
}
Koska tuohon std namespaceen joudutaan edellä viittamaan monta kertaa, voisi koodiin lisätä tuon namespacen käyttäen using direktiiviä seuraavasti:
#include <iostream>
using namespace std;

int main()
{
    string name;
    cout<<"Kerro nimesi"<<endl;
    cin>>name;
    cout<<"Terve "<<name<<endl;
    return 0;
}

Olio-ohjelmointi

Olio-ohjelmoinnin kaksi keskeistä termiä ovat luokka ja olio. Olio-ohjelmoinnissa tieto ja sitä käsittelevä toiminnallisuus kootaan luokkarakenteeksi. Luokista luodaan olioita.

Olio-ohjelmointia tukevia kieliä ovat mm. C++, C#, Java ja Visual Basic. C++ poikkeaa muista edellä mainituista siten, että C++ sovelluksia ajetaan suoraan käyttöjärjestelmän päällä. Muilla edellä mainituilla kielillä tehdyt sovellukset ajetaan virtuaalikoneessa. Edellä mainitun eron vuoksi C++ sovellukset vaativat vähemmän resursseja koneelta ja toimivat näin nopeammin, varsinkin heikkotehoisissa koneissa. Haittapuolena on, että C++ sovelluksen ohjelmoiminen on työläämpää. C++ ohjelmoijan tulee itse huolehtia esimerkiksi muistin varaamisesta ja vapauttamisesta, kun muissa em. kielissä virtuaalikone huolehtii siitä. Alla olevassa kuvassa kuvataan C++ - ja Java-sovelluksen toimintaa.

Olio-ohjelmoinnin Periaatteet

Tärkeimpiä olio-ohjelmoinnin periaatteita ovat kapselointi, tiedon kätkentä, periytyminen ja polymorfismi.

Kapselointi

Kapselointi tarkoittaa, että olion tiedot ja toiminnot yhdistetään yhdeksi yksiköksi. Olio piilottaa sisäiset tietonsa ja tarjoaa julkisen rajapinnan, jonka avulla ohjelmoija voi käyttää olion toiminnallisuutta ilman, että olion sisäinen rakenne paljastuu. Kapselointi mahdollistaa ohjelmiston rakenteen selkeyttämisen ja tietojen suojaamisen ulkopuolisilta.

Tiedon kätkentä

Tiedon kätkentä liittyy läheisesti kapselointiin. Tiedon kätkennällä pyritään rajoittamaan pääsyä olion sisäisiin tietoihin, mikä estää ulkopuolisia muuttamasta tai väärinkäyttämästä niitä. Tämä saavutetaan määrittelemällä olion tietojen näkyvyys private, protected tai public -avainsanojen avulla. Tiedon kätkentä auttaa pitämään ohjelmiston vakaana ja virheettömänä.

Periytyminen

Periytyminen tarkoittaa, että olio voi periä ominaisuuksia ja toimintoja toiselta oliolta. Periytyminen mahdollistaa yhteisten ominaisuuksien uudelleenkäytön ja laajentamisen, mikä vähentää koodin määrää ja parantaa ohjelmiston ylläpidettävyyttä. Esimerkiksi "Eläin" voi olla perusluokka, josta "Koira" ja "Kissa" voivat periä ominaisuuksia, kuten liikkuminen ja hengittäminen.

Polymorfismi

Polymorfismi tarkoittaa, että sama operaatio voi toimia eri tavoilla eri olioilla. Se mahdollistaa yhteisen rajapinnan käytön erilaisille olioille, jolloin voimme kutsua esimerkiksi metodin ajaa() sekä "Auto"- että "Moottoripyörä"-olioille, vaikka niiden toiminta eroaisi toisistaan. Polymorfismi parantaa ohjelman joustavuutta ja laajennettavuutta.

Luokka

Opiskeltuasi c-kieltä tietue lienee sinulle tuttu käsite. Myös C++ ohjelmassa voit luoda tietueita esimerkiksi seuraavasti:

typedef struct Person_struct{
    int age;
    string name;
}
person;
Nyt sinulla on käytössäsi uusi tietotyyppi ja voit luoda muuttujia, joilla on tietotyyppinä person seuraavasti:
person pe;
Ja voit sijoittaa tietuemuuttujille arvoja seuraavasti:
pe.age=23;
pe.name="Teppo Testi";
Eli person-tietue sisältää muuttujat age ja name. Oheisen esimerkin sovellus, löytyy sivulta https://github.com/olio-kurssi/esim0

Luokka voisi olla samanlainen, mutta usein se sisältää myös metodeja, joiden avulla noita muuttujia käsitellään. Voidaan luoda Person luokka seuraavasti:

class Person{
    private:
        int age;
        string name;
    public:
        int getAge() const {
            return age;
        }
        void setAge(int value){
            age=value;
        }
        string getName() const {
            return name;
        }
        void setName(string value){
            name=value;
        }
};

Luokan sisältämiä muuttujia nimitetään jäsenmuuttujiksi ja luokan sisältämiä funktioita (jotka käsittelevät em. muuttujia) nimitetään jäsenfunktioiksi eli metodeiksi.

Getter ja Setter

public osiossa määritelty getAge-metodi on age muuttujan Getter-metodi. Sen avulla saadaan haettua age muuttujan arvo. Ja setAge on age muuttujan Setter-metodi. Sen avulla muuttujan age arvo voidaan asettaa.

Molemmissa Gettereissä eli getAge ja getName on käytetty const määrettä. Se ei ole pakollinen mutta se aiheuttaa sen, että noiden metodien sisällä ei voida muuttaa muuttujien age ja name arvoja.

private, public, protected

Private tyyppisiin muuttujiin ja metodeihin päästään käsiksi vain luokan sisältä. Public tyyppisiin muuttujiin ja metodeihin päästään käsiksi myös luokasta luodun olion kautta. Protected tyyppisiin muuttujiin päästään käsiksi luokan ja perivän luokan sisältä. Usein noudatetaan seuraavia käytäntöjä:

  1. jäsenmuuttujista tehdään private tyyppisiä
  2. metodeista tehdään public tyyppisiä

Oheisen esimerkin sovellus, löytyy sivulta https://github.com/olio-kurssi/esim1

person.h ja person.cpp

Yleensä luokan muuttujien ja metodien määrittelyt tehdään h-tiedostossa. Kun luodaan Person-luokka, niin person.h tiedoston sisältö voisi olla seuraava

#ifndef PERSON_H
#define PERSON_H

#include <iostream>

using namespace std;

class Person
{
public:
    Person();
    int getAge() const;
    void setAge(int newAge);

    string getFname() const;
    void setFname(const string &newFname);

private:
    int age;
    string fname;
};

#endif // PERSON_H
Edellä siis private osiossa on määritelty jäsenmuuttujat age ja fname ja public osiossa niiden getterit ja setterit.

Ja tuossa h-tiedostossa määritetään metodeista vain niiden palautusarvon tyyppi ja metodin ottamien parametrien tyyppi. Luokan metodien toteutukset kirjoitetaan person.cpp tiedostoon seuraavasti:
#include "person.h"

Person::Person()
{

}

int Person::getAge() const
{
    return age;
}

void Person::setAge(int newAge)
{
    age = newAge;
}

string Person::getFname() const
{
    return fname;
}

void Person::setFname(const string &newFname)
{
    fname = newFname;
}
this osoitin

this on erityinen osoitin, joka viittaa olioon, jonka jäsenfunktiota parhaillaan suoritetaan.

Käytetään, kun tarvitaan viitettä olion jäseniin, erityisesti silloin, kun jäsenmuuttujien ja parametrien nimet ovat samat.

class MyClass {
    int age;
public:
    void setValue(int age) {
        this->age = age;  // Erotellaan jäsenmuuttuja ja parametri
    }
};
Olio

Olio on luokasta luotu ilmentymä eli instanssi. Voit verrata asiaa siihen, että int on muutujatyyppi ja voit luoda muuttujan lauseella int myVariable. Edellä olevassa esimerkissä on luotu luokka Person ja siitä voidaan luoda olio lauseella Person objectPerson.

Kun luot olioita sinun tulee ymmärtää, että sovelluksellasi on käytössä kahdenlaista muistia

  • Stack eli pinomuisti
  • Heap eli kekomuisti
Ja tuon Stack muistin käytöstä huolehtii koneesi käyttöjärjestelmä. Kun sovelluksesi käynnistyy, käyttöjärjestelmä antaa sille vakiomäärän muistia ja huolehtii sen vapauttamisesta. Jos käytät kaiken tuon muistin sovelluksesi lakkaa toimimasta.

Heap muistia allokoidaan dynaamisesti sovelluksen ajon aikana ja sinun on huolehdittava sen vapauttamisesta (jollet käytä smart pointteria).

Perinteisesti C++ ohjelmoinnissa olioita on voinut luoda kahdella tavalla eli joko

Person objectPerson1;
tai näin
//luo osoitin, jonka tyyppinä Person    
Person *objectPerson2;
//varaa osoittimelle muistia new operaattorilla
*objectPerson2 = new Person;
Edellisen voit korvata myös yhdellä lauseella:
Person *objectPerson2 = new Person;

Ensin mainittu tapa luo ns. automaattisen olion Stack muistiin. Tässä tapauksessa käyttöjärjestelmä huolehtii olion muistinvarauksista ja vapauttamisista, olion luonnin ja tuhoamisen yhteydessä.

Jälkimmäinen tapa luo olion Heap muistiin ja sinun on huolehdittava sen tuhoamisesta, kun et enää tarvitse sitä.

Kun olet luonut olion, pääset sen avulla käsiksi Person luokan metodeihin. Niihin viitataan eri tavalla riippuen siitä teitkö olion Stack vai Heap muistiin. Esimerkiksi setAge metodia voidaan kutsua näin:

objectPerson1.setAge(23);
tai
objectPerson2->setAge(23);

Sinun ei itse tarvitse tuhota oliota objectPerson1, mutta sinun on tuhottava olio objectPerson2, kun et sitä enää tarvitse. Se tapahtuu seuraavasti:

delete objectPerson2;
objectPerson2 = nullptr;

Käytä automaattista muistialuetta (pino), kun tiedät olion koon etukäteen ja se on suhteellisen pieni. Oliot automaattisella muistialueella tuhoutuvat automaattisesti, kun niiden käyttöalue (kuten funktio) päättyy. Tämä auttaa välttämään muistivuotoja. Automaattinen muistialue on nopeampi käyttää kuin keko, koska sen hallinta on yksinkertaisempaa.

Käytä dynaamista muistialuetta (keko), kun olion koko ei ole tiedossa etukäteen tai se on suuri. Olet vastuussa olion muistialueen vapauttamisesta käytön jälkeen delete-avainsanalla. Oliot dynaamisella muistialueella säilyvät olemassa, kunnes ne tuhotaan manuaalisesti

Smart pointer

Moderni C++ mahdollistaa resurssien hallinnan älykkäiden osoittimien avulla, mikä helpottaa muistin vapauttamista ja vähentää mahdollisuutta virheisiin.

C++ sisältää kolmenlaisia smart pontereita:

  1. unique_ptr: Omistaa dynaamisesti allokoidun muistin ja huolehtii sen vapauttamisesta, kun osoitin poistuu käytöstä. Se ei ole kopioitavissa.
  2. shared_ptr: Jaettu älyosoitin, joka sallii useiden osoittimien osoittaa samaan resurssiin. Se pitää kirjaa viitteiden määrästä ja vapauttaa muistin, kun kaikki osoittimet ovat vapautettu.
  3. weak_ptr: Heikko osoitin, joka liittyy shared_ptr:iin, mutta ei vaikuta resurssin elinkaareen.

Smart pointer voidaan luoda koodilla:

#include <memory>
unique_ptr<Person> objectPerson = make_unique<Person>();
Edellä olio luodaan kekoon, mutta se tuhoutuu automaattisesti.

shared_ptr

Seuraava esimerkki havainnollistaa, kuinka shared_ptr:ia voidaan käyttää. Huomaa, että osoittimet ptr1 ja ptr2 osoittavat samaan muistipaikkaan

#include <iostream>
#include <memory>

using namespace std;
class TestClass{
public:
    TestClass(string value){
        fname=value;
        cout<<"TestClass olio luotiin"<<endl;
    }
    ~TestClass(){
        cout<<"TestClass olio tuhottiin"<<endl;
    }
    string getName(){
        return fname;
    }
private:
    string fname;
};


void functionFirst();
void functionSecond(shared_ptr<TestClass> ptr);


int main()
{
    functionFirst();
}

void functionFirst(){
    shared_ptr<TestClass> ptr1=make_shared<TestClass>("Teppo");
    cout<<"Funktiossa1 nimi = "<< ptr1->getName()<<endl;;
    functionSecond(ptr1);
}
void functionSecond(shared_ptr<TestClass> ptr2){
    cout<<"Funktiossa2 nimi = "<< ptr2->getName()<<endl;
}
Tuloksena on
TestClass olio luotiin 
Funktiossa1 nimi = Teppo 
Funktiossa2 nimi = Teppo 
TestClass olio tuhottiin 

Piste- ja nuolioperaattori
  • Pisteoperaattoria (.) käytetään suoraan objektin jäsenmuuttujien tai -funktioiden viittaamiseen.
  • Nuolioperaattoria (->) käytetään osoittimen kautta objektiin ja sen jäsenmuuttujiin tai -funktioihin viittaamiseen.
Luokan muodostin ja tuhoaja

Edellä, jo kerrottiin, että luokan muodostin on metodi, jolla on sama nimi kuin itse luokalla. Muodostinta kutsutaan aina kun luokasta luodaan olio. Sillä ei ole koskaan paluuarvoa, eikä paluuarvoksi kirjoiteta edes sanaa void.

Voit lisätä muodostimelle parametrin tai useita parametrejä. Jos edellä muokattaisiin luokan muodostin h tiedostossa muotoon

Person(int value);
Ja cpp-tiedostossa muotoon
Person::Person(int value)
{
    age=value;
}
pitää olioa luotaessa antaa aina myös kokonaisluku eli olion voisi luoda näin:
Person objectPerson(46);
TAI
Person *objectPerson=new Person(46);
Tällöin 46 sijoitetaan age muutujan arvoksi. Nyt ei enää voi luoda oliota antamatta kokonaislukua, eli seuraavasta seuraisi virheilmoitus
Person *objectPerson=new Person;
Jos halutaan, että molemmat toimii, tarvitaan kaksi muodostinta, jolloin h-tiedostoon kirjoitettaisiin
Person();
Person(int value);
Huom! Edellä on kyse metodin ylikuormittamisesta, josta kerrotaan myöhemmin.

Luokan tuhoaja on myös saman niminen kuin luokka, mutta sen edessä on merkki ~ eli seuraava olisi h-tiedostossa

~Person();
Ja seuraava cpp-tiedostossa
Person::~Person()
{
    cout<<"Person luokan tuhoajaa kutsuttiin\n";
}
Jos koodisi olisi seuraava
Person *objectPerson=new Person;
delete objectPerson;
objectPerson=nullptr;
Näkisit tekstin Person luokan tuhoajaa kutsuttiin. Luokan tuhoajaa kutsutaan siis aina, kun olio tuhotaan delete komennolla. Hyvän ohjelmointitavan mukaisesti tuhottuun olioon tulisi asettaa nullptr, kuten edellä.

Sinun ei ole pakko luoda luokalle tuhoajaa, koska kääntäjä luo automaattisesti "näkymättömän tuhoajan". Ohjelmoijan kannattaa luoda tuhoaja vain, jos haluaa lisätä siihen jotain koodia.

Oheiseen esimerkkiin liittyvä sovellus, löytyy sivulta https://github.com/olio-kurssi/esim2

Ylikuormittaminen

C++ mahdollistaa funktioiden ylikuormittamisen (function overloading) eli sen, että saman nimisiä funktioita on useita, mutta niillä on erilaiset parametrit. Myös palautusarvot voivat olla erilaisia, mutta pelkät erilaiset paluuarvot eivät mahdollista ylikuormitusta, jos parametrit ovat samat. Kääntäjä valitsee funktion kutsussa annettujen argumenttien avulla sopivan funktion.

Esimerkki ylikuormittamisesta

void calcSum(int a, int b){
    int sum=a+b;
    cout<<"Kokonaislukujen summa = "<<sum<<endl;
}
void calcSum(double a, double b){
    double sum=a+b;
    cout<<"Desimaalilukujen summa = "<<sum<<endl;
}

Periytyminen

Periytyminen eli inheritance tarkoittaa, että jokin luokka voi periä toisen luokan. Ajatellaan esimerkiksi että rakentaisimme sovellusta, jolla käsitellään jonkin oppilaitoksen dataa. Todetaan, että oppilaitoksessa on opiskelijoita ja opettajia. Molemmilla on monia samalaisia ominaisuuksia, kuten esimerkiksi nimi ja syntymävuosi. Lisäksi opiskelijoilla on ryhmätunnus ja opettajilla on osasto.

Nyt voidaan tehdä niin, että

  1. luodaan kantaluokka Person, johon laitetaan kaikille yhteiset ominaisuudet
  2. luodaan luokka Student, joka perii luokan Person ja luokalla on lisäominaisuus groupName
  3. luodaan luokka Teacher, joka perii luokan Person ja luokalla on lisäominaisuus department
Näin name ja birthYear määritellään vain Person-luokassa, mutta molemmilla perivillä luokilla on ne ominaisuudet.

Perityminen merkitään Student-luokalle näin:

class Student : public Person

Oheiseen esimerkkiin liittyvä sovellus, löytyy sivulta https://github.com/olio-kurssi/esim3. Sovelluksen luokkakaavio näyttää seuraavalta:

Periytymisen yhteydessä on syytä tutustua termiin protected, kun luokka perii toisen luokan se pääsee käsiksi ominaisuuksiin, jotka ovat tyypiltään public tai protected. Seuraavassa taulukossa kuvataan kuinka private, public ja protected rajaavat oikeuksia.

Pääsy public protected private
Luokan sisällä kyllä kyllä kyllä
Perivässä luokassa kyllä kyllä ei
Luokan ulkopuolella kyllä ei ei

Edellä kuvattiin luokan jäsenten suojauksia. Periytymiselle voidaan myös määrittää suojaus seuraavilla vaihtoehdoilla

  • class Student : public Person
    jolloin perivällä luokalla on pääsy perittävän public- ja protected-jäseniin
  • class Student : protected Person
    jolloin perittävän luokan public-jäsenistä tulee perivän luokan protected-jäseniä
  • class Student : private Person
    jolloin perittävän luokan public- ja protected-jäsenistä tulee perivän luokan private-jäseniä

Voit lukea lisätietoa periytymisestä sivulta https://www.tutorialspoint.com/cplusplus/cpp_inheritance.htm

Kantaluokan muodostimen kutsuminen

Mikäli peritävässä luokassa on muodostin, joka ottaa parametreja, voidaan perivän luokan konstruktorissa kutsua tuota muodostinta.

Esimerkiksi, jos Person luokassa on seuraava muodostin

Person::Person (string na){
    name=na;
}
voisi Student luokan muodostin olla seuraava
Student::Student(string gr, string na ) : Person(na){
    groupName=gr;
} 
Edellä name on Person luokan jäsenmuuttuja ja groupName on Student-luokan jäsenmuuttuja.

Jos edellä Person luokalla on oletusmuodostin ja public tyyppinen metodi setName(), voitaisiin Student-luokan muodostin kirjoittaa muotoon:

Student::Student(string gr, string na ) {
    groupName=gr;
    this->setName(na);
} 

Ylikirjoittaminen

Ylikirjoittaminen (overriding) C++-ohjelmoinnissa tarkoittaa, että aliluokka määrittelee uudelleen kantaluokassa perityn virtuaalisen funktion. Kun ylikirjoitettu funktio kutsutaan aliluokan instanssin kautta, suoritetaan aliluokan versio kyseisestä funktiosta. Tämä mahdollistaa polymorfismin, jossa aliluokka voi muokata kantaluokan toiminnallisuutta omien tarpeidensa mukaisesti.

Ylikirjoittaminen edellyttää, että sekä kantaluokan että aliluokan funktiot ovat määritelty samalla nimellä, paluuarvolla ja parametreilla, ja kantaluokan funktion on oltava virtual-avainsanalla merkitty. Aliluokan funktio voidaan myös merkitä override-avainsanalla selkeyden vuoksi.

Seuraavassa esimerkissä lisäsin Person luokkaan metodin sayStatus ja ylikirjoitin sen Students ja Teacher luokissa.

Alla rivit h-tiedostoista

person.h
virtual void sayStatus();

student.h 
virtual void sayStatus() override; 

teacher.h 
virtual void sayStatus() override; 
Ja cpp-tiedostossa metodien toteutukset ovat seuraavat
void Person::sayStatus()
{
    cout<<"Person\n ";
}
void Student::sayStatus()
{
    cout<<"Opiskelija: ";
}
void Teacher::sayStatus()
{
    cout<<"Opettaja: ";
}

Esimerkki löytyy sivulta https://github.com/olio-kurssi/esim4

Koostaminen

Luokkien välillä voi olla seuraavanlaisia suhteita

  • Assosiaatio: Tämä viittaa kahden tai useamman luokan väliseen suhteeseen, jossa toinen luokka käyttää toisen luokan jäseniä. Se ei osoita omistussuhdetta eikä määrittele elinkaarisuhdetta, vaan yksinkertaisesti ilmaisee, että luokat ovat jollain tavalla yhteydessä toisiinsa. Esimerkiksi luokka "Auto" ja luokka "Moottori" voivat olla assosioituneita, koska auto käyttää moottoria.
  • Aggregaatio: Tässä suhteessa yksi luokka "omistaa" toisen luokan ilmentymän. Se on looginen suhde, jossa yksi objekti koostuu toisista objekteista, mutta niillä voi olla oma elinkaari. Esimerkiksi, "Auto" voi sisältää "Moottorin", mutta moottori voi olla olemassa ilman autoakin.
  • Kompositio: Tämä on tiukempi versio aggregaatiosta. Se tarkoittaa sitä, että jos koostavan luokan olio tuhoutuu, myös sen sisältämät toisten luokkien oliot tuhoutuvat. Se on "kokonaisuus ja osa" -suhde, jossa osalla ei ole itsenäistä elämää ilman kokonaisuutta. Esimerkiksi, "Talo" koostuu "Huoneista", ja jos talo tuhoutuu, myös sen huoneet tuhoutuvat.

Assosiaatiossa koostavan luokan konstruktorille tai jollekin metodille annetaan parametrina toisen luokan olio esimerkiksi näin:

    Engine objectEngine;
    Car objectCar(objectEngine);
Edellä siis luodaan kopio oliosta objectEngine.

Aggregaatiossa koostavan luokan konstruktorille tai jollekin metodille annetaan parametrina referenssi toisen luokan olioon esimerkiksi näin:

    Engine objectEngine;
    Engine &refEngine=objectEngine;
    Car objectCar(refEngine);

Kompositio eli vahva kooste tarkoittaa tilannetta, jossa koosteluokka sisältää toisen luokan olion/olioita. Esimerkissä 6 on luokka Classroom, joka sisältää kaksi Student-luokan oliota ja yhden Teacher-luokan olion seuraavasti:

class Student {

}
class Teacher {

}
Class Classroom{
private:
    unique_ptr<Student> objStudent1;
    unique_ptr<Student> objStudent2;
    unique_ptr<Teacher> objTeacher;
}

Sovelluksen luokkadiagrammi pelkistettynä näyttää tältä

Classroom on siis koosteluokka. Tässä esimerkissä Student- ja Teacher-luokan oliot luodaan smart pointtereiden avulla, mutta ne voitaisiin luoda myös "normi" pointtereilla tai pinomuistiin.

Esimerkki löytyy sivulta https://github.com/olio-kurssi/esim6

Kooste vai perintä

Kooste ja perintä ovat molemmat oliosuuntautuneita ohjelmistomalleja. Kooste tarkoittaa has a-tyyppistä suhdetta ja perintä is a-tyyppistä suhdetta. Tämän sivun esimerkeistä voidaan sanoa, että

  • "Student is a Person", joten käytetään perintää
  • "Classroom has a Student", joten käytetään koostetta

Vektori ja oliolista

C++-vektori on osa C++ Standard Libraryä ja se on dynaaminen taulukko, joka voi mukautua erilaisiin datamäärien muutoksiin. Se tarjoaa samankaltaisia ominaisuuksia kuin perinteinen taulukko, kuten nopea elementteihin pääsy, mutta toisin kuin perinteiset taulukot, vektorin koko voi muuttua joustavasti.

Jos sovelluksessa tarvitaan monta oliota samasta luokasta, voidaan niistä luoda lista vektorin avulla. Seuraavassa esimerkissä luodaan oliolista, joka sisältää Student-luokan oliota. Listan käytöstä erillisten olioiden sijaan on etuna se, että listaa voidaan käydä läpi toistorakenteella.

#include "student.h"

#include <iostream>
#include <vector>

using namespace std;

int main()
{
    vector<Student> studentList;

    //luodaan Student-luokan oloita
    Student objectStudent0("Teppo Testi",1999,"TVT23SPL");
    Student objectStudent1("Liisa Joki",1998,"TVT23SPL");
    Student objectStudent2("Aino Virta",1997,"TVT23SPO");
    Student objectStudent3("Matti Virtanen",2001,"TVT23SPO");
    Student objectStudent4("Mikko Vilkas",2001,"TVT23SPL");

    //lisätään luodut oliot listaan
    studentList.push_back(objectStudent0);
    studentList.push_back(objectStudent1);
    studentList.push_back(objectStudent2);
    studentList.push_back(objectStudent3);
    studentList.push_back(objectStudent4);

    for(int x=0; x<=4; x++){
        studentList[x].printStudentData();
    }
    return 0;
}

Esimerkissä 7 on käytetty smart_pointteria ja sen main.cpp on seuraava

#include "student.h"

#include <iostream>
#include <vector>
#include <memory>

using namespace std;

int main()
{
    unique_ptr<vector<Student>> studentList = make_unique<vector<Student>>();

    Student objectStudent0("Teppo Testi",1999,"TVT23SPL");
    Student objectStudent1("Liisa Joki",1998,"TVT23SPL");
    Student objectStudent2("Aino Virta",1997,"TVT23SPO");
    Student objectStudent3("Matti Virtanen",2001,"TVT23SPO");
    Student objectStudent4("Mikko Vilkas",2001,"TVT23SPL");

    studentList->push_back(objectStudent0);
    studentList->push_back(objectStudent1);
    studentList->push_back(objectStudent2);
    studentList->push_back(objectStudent3);
    studentList->push_back(objectStudent4);

    // Hae opiskelijalista studentList-osoittimesta
    vector<Student>& students = *studentList;

    for(int x=0; x<=4; x++){
        // Käytä opiskelijalistan elementtiä `x` ja kutsu printStudentData() -funktiota
        students[x].printStudentData();
    }

    return 0;
}

Esimerkki löytyy sivulta https://github.com/olio-kurssi/esim7

Abstrakti luokka

Abstrakti luokka, tarkoittaa luokkaa, josta ei voi luoda oliota, mutta luokkaa voidaan käyttää kantaluokkana muille luokille. Edellisessä esimerkissä voisimme tehdä Person luokasta abstraktin.

Edellisessä esimerkissä voit luoda Person luokasta olion. Luokka saadaan abstraktiksi, jos siihen lisätään yksi pure virtual method. Muutetaan Person luokassa sayStatus() metodin määrittely muotoon

virtual void sayStatus()=0;
Funktion toteutus voidaan poistaa person.cpp tiedostosta. Nyt Person on abstrakti luokka ja jos, koetat luoda siitä olion, saat virheilmoituksen.

Esimerkki löytyy sivulta https://github.com/olio-kurssi/esim5

Virtuaalifunktioita sisältävän luokan tuhoajan pitää myös olla virtuaalinen.

Interface luokka

Olio-ohjelmoinnissa käsite interface tarkoittaa luokkaa, josta ei tehdä olioita. Siinä määritellään metodeja, muttei niiden toteutuksia. Siis määritetään metodien paluuarvojen tyypit ja parametrien tyypit. Kun jokin luokka perii tuon interface luokan on perivän luokan toteutettava interface luokan metodit, muuten seuraa virheilmoitus.

Javassa ja C#:ssa interface luokka toteutetaan lisäämällä sana interface luokan määrityksen eteen. C++:ssa ei tällaista sanaa ole vaan interface luokka voidaan toteuttaa tekemällä metodeista puhtaita virtuaalimetodeja. Interface luokan nimi alkaa usein kirjaimella I.

Tein esimerkin, jossa on interface luokka nimeltään IPerson ja Student ja Teacher perivät sen, sekä luokan Person. Esimerkki löytyy sivulta https://github.com/olio-kurssi/esim8

Staattinen luokka

Staattinen luokka tarkoittaa luokkaa, jonka metodeita voidaan kutsua luomatta oliota kyseisestä luokasta. C++ kielessä ei voida luoda varsinaista staattista luokkaa, mutta toiminnallisesti käsitettä vastaa luokka, jonka kaikki metodit ovat staattisia.

Esimerkiksi C#:ssa on luokka Math, josta löytyy esimerkiksi funktio sqrt. Ei ole järkevää, että voidaksesi käyttää tuon luokan metodeja sinun tulisi luoda luokasta olio. Voit laskea esimerkiksi luvun 4 neliöjuuren koodilla Math.Sqrt(4).

Tein esimerkin, jossa on luokka nimeltään MyStaticClass ja siellä määritettynä metodi doubleMe. Luokan h-tiedostossa metodi on määritetty näin:

static double doubleMe(double);
ja cpp-tiedostossa toteutettu näin:
double MyStaticClass::doubleMe(double value)
{
    return 2*value;
}
Nyt tuota metodia kutsutaan main.cpp:ssä näin:
myResult=MyStaticClass::doubleMe(myValue);
Metodia siis kutsutaan syntaksilla luokan nimi :: metodin nimi

Voit kuitenkin luoda olioita luokasta MyStaticClass. Jos haluat estää olioiden luomisen, voit muokata luokan oletusmuodostimen h-tiedosssa seuraavaan muotoon:

MyStaticClass()=delete;

Esimerkki löytyy sivulta https://github.com/olio-kurssi/esim9

UML luokkakaavio

UML eli Unified Modeling Language on standardoitu tapa kuvata ohjelmisto- ja järjestelmäsuunnittelua visuaalisesti. Se tarjoaa erilaisia kaavioita ja työkaluja ohjelmistojen rakenteen, toiminnallisuuden ja käyttötapauksien mallintamiseen. UML auttaa suunnittelijoita kommunikoimaan, ymmärtämään ja dokumentoimaan ohjelmistojen arkkitehtuuria ja toiminnallisuutta.

Luokkakaavio on UML-standardin mukainen mallinnustyyppi, joka kuvaa järjestelmän staattisen rakenteen luokkien ja niiden välisten suhteiden avulla.

Luokka piirretään suorakaiteena, joka on jaettu kolmeen osaan; luokan nimi, luokan tiedot (jäsenmuuttujat) ja luokan toiminnallisuus (jäsenfunktiot).

Diagrammeissa käytetyt suojaustasoa kuvaavat symbolit ovat:

  • - : private
  • + : public
  • # : protected

Esimerkiksi jäsenmuuttuja osiossa voisi olla rivi:
-age:int
joka tarkoittaa, että luokka sisältää int tyyppisen muuttujan nimeltään age, jonka suojaustaso on private

Ja jäsenfunktio osiossa voisi olla rivi:
+getInfo(int):float
joka tarkoittaa, että public metodi getInfo ottaa vastaan argumenttina int tyyppisen arvon ja palauttaa float tyyppisen arvon

Kontruktoria ei välttämättä merkitä diagrammiin.

Perintä

Periytyminen ilmaistaan diagrammeissa nuolella, jossa nuoli osoittaa perittävään luokkaan seuraavasti:

Kooste

Kooste ilmaistaan diagrammeissa viivalla, jossa vinoneliö osoittaa koostavaan luokkaan seuraavasti:

Huom! Musta vinoneneliö ilmaisee, että kyseessä on vahva kooste.

Esimerkkien lähdekoodit

Oppaaseen liittyvien esimerkkien lähdekoodit löytyy sivulta https://github.com/orgs/olio-kurssi/repositories.

repositoryaihe
esim0Tietue
esim1Luokka ja olio (pino ja keko)
esim2Muodostin ja tuhoaja
esim3Periytyminen
esim4Virtuaalimetodi
esim5Abstraktiluokka
esim6Koostaminen
esim7Oliotaulukko
esim8Interface luokka
esim9Staattinen luokka
unit_testYksikkätestaus



Toggle Menu