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/.
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?
Paitsi, että luokan sisään voidaan sijoittaa useita muuttujia eli jäsenmuuttujia, voidaan luokan sisään sijoittaa myös metodeja eli jäsenfunktioita. Niiden avulla voit esimerkiksi antaa edellä mainituille jäsenmuuttujille 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. (Qt frameworkissä luokka on QPushButton)
Mikä on Qt?
C++-sovellusten luominen on helpompaa, jos käytetään siihen tarkoitettua ohjelmointiympäristöä, kuten Qt Creatoria.
Qt on alustariippumaton ohjelmistokehys (framework), joka sisältää kirjastoja ja työkaluja esimerkiksi graafisten sovellusten kehittämiseen Windowsille, Linuxille ja macOS:lle. Qt-framework sisältää joukon valmiiksi tehtyjä luokkia, joita voimme hyödyntää. Kun asennat Qt:n, saat mukaan myös Qt Creator -kehitysympäristön. Qt Creatorilla voidaan kehittää sekä Qt-frameworkia hyödyntäviä sovelluksia että tavallisia C- ja C++-sovelluksia, jotka eivät käytä Qt-frameworkia.
Qt Creatorin sijaan C++-ohjelmia voidaan kehittää esimerkiksi Visual Studiolla, Visual Studio Codella tai CLionilla. Visual Studio on monipuolinen Windows-kehitysympäristö, jossa on C++-kääntäjä, graafinen debugger ja tuki CMake-projekteille. Visual Studio Code on kevyt editori, jota laajennetaan C++- ja CMake-laajennuksilla; kääntäjä ja debugger asennetaan ja määritetään erikseen. CLion on C- ja C++-ohjelmointiin suunniteltu ympäristö, jossa on älykäs koodin täydennys, koodianalyysi, CMake-tuki ja debugger. Näillä kaikilla voidaan tehdä sekä Qt-frameworkia hyödyntäviä että tavallisia C- ja C++-sovelluksia, kun tarvittavat kääntäjät, Qt-kirjastot ja laajennukset on asennettu.
C-kielestä C++-ohjelmointiin
C++-ohjelmoinnissa voit käyttää C-kielestä tuttuja printf- ja scanf-funktioita, mutta tulostamiseen ja syötteen lukemiseen käytetään yleensä std::cout- ja std::cin-olioita. Niiden avulla tulostat tekstiä konsoliin ja luet käyttäjän antamia tietoja. cout ja cin ovat standardikirjaston valmiita olioita, joita ohjelmoijan ei tarvitse itse luoda. Ota ne käyttöön lisäämällä kooditiedoston alkuun iostream-otsaketiedosto seuraavalla rivillä:
#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;
}
Miksi cout- ja cin-olioita käytetään operaattoreilla?
Olion toimintoja kutsutaan usein pisteen avulla, esimerkiksi opiskelija.tulosta(). C++:ssa luokalle voidaan kuitenkin määritellä myös operaattorien toimintaa. Tätä kutsutaan operaattorien ylikuormittamiseksi.
Lauseessa std::cout << "Hei"; operaattori << lisää tekstin tulostevirtaan, joka on tavallisesti yhdistetty konsoliin. Vastaavasti lauseessa std::cin >> age; operaattori >> lukee syötevirrasta arvon muuttujaan age.
Operaattorien käyttö kutsuu niiden toteuttavia funktioita. Osa näistä on luokan jäsenfunktioita eli metodeja ja osa luokan ulkopuolisia funktioita. Myös cout- ja cin-olioilla on tavallisia metodeja, mutta näissä esimerkeissä käytämme niitä operaattorien avulla.
Tulostuksia voi myös ketjuttaa: std::cout << "Ikä: " << age;. Tämä onnistuu, koska << palauttaa viittauksen samaan tulostevirtaan, jolloin seuraava << voi jatkaa sen käyttöä.
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;
}
Vaikka using namespace std; tekee koodista lyhyemmän ja helpommin luettavan pienissä ohjelmissa, se ei ole suositeltava käytäntö isommissa projekteissa seuraavista syistä:
count-funktio ja käytät using namespace std;, voi syntyä ristiriita std::count-funktion kanssa.std::cout on selvempi kuin pelkkä cout.using namespace std; tulee välttää .h-tiedostoissa, koska se pakottaa kaikki tuon headerin includettavat tiedostot käyttämään koko std-nimiavaruutta.Parempia vaihtoehtoja:
std::cout, std::string jne.using std::cout; using std::string; - näin otat käyttöön vain tietyt nimet, ei koko nimiavaruutta
Tämän oppaan esimerkeissä käytetään using namespace std; yksinkertaisuuden vuoksi, jotta voit keskittyä olio-ohjelmoinnin oppimiseen. Kun siirryt tekemään isompia projekteja, harkitse std::-etuliitteen käyttöä.
Olio-ohjelmointia tukevat esimerkiksi C++, Java ja C#. Niissä käytetään samoja peruskäsitteitä, kuten luokkia ja olioita, mutta ohjelmien suoritusympäristöt ja muistinhallinta eroavat toisistaan.
C++ on näistä laiteläheisempi: ohjelma käännetään tavallisesti suoraan kohdealustan konekoodiksi, ja ohjelmoijalla on enemmän mahdollisuuksia hallita muistia ja resurssien käyttöä.
Java-ohjelmat suoritetaan yleensä Java-virtuaalikoneessa (JVM) ja C#-ohjelmat .NET-ajoympäristössä (CLR). Nämä suoritusympäristöt tarjoavat muun muassa automaattisen muistinhallinnan roskienkeruun avulla.
Seuraava C++:n ja Javan vertailu havainnollistaa suoritusmallien eroa.
C++-ohjelma käännetään tavallisesti tietylle käyttöjärjestelmälle ja suoritinarkkitehtuurille sopivaksi konekoodiksi. Syntynyt ohjelmatiedosto voidaan suorittaa kyseisellä alustalla ilman erillistä virtuaalikonetta. Toiselle alustalle siirryttäessä ohjelma täytyy yleensä kääntää uudelleen.
Java-ohjelma käännetään Java-tavukoodiksi, jonka Java Virtual Machine (JVM) suorittaa. JVM:stä on omat toteutuksensa eri alustoille, joten sama tavukoodi voidaan yleensä suorittaa useissa käyttöjärjestelmissä ilman uudelleenkääntämistä. JVM voi tulkita tavukoodia ja JIT-kääntää sitä suorituksen aikana konekoodiksi.
Java- ja C#-ohjelmien suoritusympäristöt huolehtivat automaattisesta muistinhallinnasta roskienkeruun avulla, mutta tarvitsevat myös itse muistia ja suoritinaikaa. C++-ohjelma ei tavallisesti tarvitse erillistä virtuaalikonetta eikä roskienkeruuta, ja ohjelmoija voi hallita tarkasti muistin ja muiden resurssien käyttöä. Siksi C++ on usein hyvä valinta esimerkiksi sulautettuihin järjestelmiin, joissa muistia ja laskentatehoa on rajallisesti, tai sovelluksiin, joissa tarvitaan laiteläheistä ohjelmointia ja tarkkaa suoritusajan hallintaa. Vastapainona ohjelmoijalla on enemmän vastuuta resurssien oikeasta käsittelystä. Tätä helpottavat esimerkiksi älyosoittimet, jotka automatisoivat muistin vapauttamista.
Olio-ohjelmoinnin kaksi keskeistä termiä ovat luokka ja olio. Olio-ohjelmoinnissa tieto ja sitä käsittelevä toiminnallisuus kootaan luokkarakenteeksi. Luokista luodaan olioita.
Tärkeimpiä olio-ohjelmoinnin periaatteita ovat kapselointi, tiedon kätkentä, periytyminen ja polymorfismi.
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ä 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 tarkoittaa, että luokka voi periä ominaisuuksia ja toimintoja toiselta luokalta. Perivää luokkaa kutsutaan aliluokaksi, ja sitä luokkaa, jolta ominaisuudet ja toiminnot peritään, kutsutaan kantaluokaksi tai yliluokaksi. Periytyminen mahdollistaa yhteisten ominaisuuksien ja toimintojen uudelleenkäytön sekä laajentamisen, mikä vähentää koodin toistoa ja parantaa ohjelmiston ylläpidettävyyttä. Esimerkiksi Eläin voi olla kantaluokka, josta Koira ja Kissa perivät ominaisuuden, kuten iän, ja toimintoja, kuten liikkumisen ja hengittämisen.
Polymorfismi tarkoittaa, että sama operaatio voi toimia eri tavoin erityyppisillä olioilla. Se mahdollistaa yhteisen rajapinnan käytön erilaisille olioille. Esimerkiksi Auto- ja Moottoripyörä-oliota voidaan käsitellä yhteisen rajapinnan kautta ja kutsua niiden ajaa()-metodia, jolloin kummankin olion oma toteutus suoritetaan. Tämä edellyttää, että luokat on toteutettu siten, että ne tukevat tällaista yhteistä käyttöä. Pelkkä samannimisen metodin määrittely kummassakin luokassa ei itsessään tarkoita periytymiseen perustuvaa polymorfismia. Polymorfismi parantaa ohjelman joustavuutta ja laajennettavuutta.
Lisätietoa asiasta on harjoituksessa 3b.
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.
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ä metodin lopussa. Tämä kertoo, että metodi ei muuta olion tilaa (jäsenmuuttujia).
Miksi const-metodit ovat tärkeitä?
1. Kääntäjä estää vahingossa tapahtuvat muutokset
Kun merkitset metodin const:ksi, kääntäjä estää sinua muuttamasta jäsenmuuttujia metodin sisällä:
class Person {
int age;
public:
int getAge() const {
return age; // OK: vain lukeminen
}
void badGetter() const {
age = 25; // VIRHE: const-metodi ei voi muuttaa jäsenmuuttujia!
}
};
2. Const-oliot voivat kutsua vain const-metodeja
Jos luot const-olion tai käytät const-viittausta, voit kutsua vain const-metodeja:
void printPersonInfo(const Person &p) {
cout << p.getAge(); // OK: getAge() on const-metodi
// p.setAge(30); // VIRHE: setAge() ei ole const, ei voi kutsua!
}
3. For-silmukoissa const-viittausten kanssa
Kun käyt vektorin läpi const-viittauksilla, voit kutsua vain const-metodeja:
vector<Person> personList
// ... lisätään henkilöitä ...
for(const Person &p : personList) {
cout << p.getAge(); // OK: getAge() on const
// p.setAge(30); // VIRHE: setAge() ei ole const
}
4. Parempi dokumentaatio
Const-metodit kertovat muille ohjelmoijille (ja sinulle myöhemmin), että metodi ei muuta olion tilaa. Se on itsedokumentoivaa koodia.
Yhteenveto:
const:ksi, koska ne vain lukevat tietoaconst, koska ne muuttavat jäsenmuuttujiaEsimerkki hyvistä käytännöistä:
class Student {
private:
string name;
int age;
public:
// Getterit ovat const - eivät muuta olion tilaa
string getName() const { return name; }
int getAge() const { return age; }
// Setterit eivät ole const - muuttavat olion tilaa
void setName(string n) { name = n; }
void setAge(int a) { age = a; }
// Tulostusmetodi on const - ei muuta olion tilaa
void printInfo() const {
cout << name << ", " << age << " vuotta" << endl;
}
};
Suositus: Merkitse aina metodi const:ksi, jos se ei muuta olion tilaa. Tämä on hyvä C++-käytäntö ja auttaa välttämään virheitä.
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ä:
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;
}
Esimerkin sovellus, löytyy sivulta https://github.com/olio-kurssi/esim1
this on erityinen osoitin, joka viittaa olioon, jonka jäsenfunktiota parhaillaan suoritetaan. Se on implisiittinen parametri jokaisessa ei-staattisessa jäsenfunktiossa.
Milloin this on pakollinen?
class Person {
string name;
public:
void setName(string name) {
this->name = name; // Pakollinen! Ilman this->name viittaisi parametriin
}
};
class Person {
string name;
int age;
public:
Person& setName(string n) {
name = n;
return *this; // Pakollinen! Palautetaan olio itse
}
Person& setAge(int a) {
age = a;
return *this;
}
};
// Käyttö: voidaan ketjuttaa metodikutsuja
Person p;
p.setName("Teppo").setAge(25);
class Student {
string name;
public:
void registerToSystem() {
// Välitetään this-osoitin toiselle funktiolle
systemDatabase.addStudent(this);
}
};
Milloin this on vapaaehtoinen?
Kun jäsenmuuttujien ja parametrien nimet ovat erilaiset, this-osoittimen käyttö on vapaaehtoista. Seuraavat ovat identtiset:
void setAge(int value) {
age = value; // Toimii ilman this
}
void setAge(int value) {
this->age = value; // Toimii myös this:llä (selkeämpi)
}
Hyvä käytäntö: Käytä aina erilaisia nimiä jäsenmuuttujille ja parametreille (esim. jäsenmuuttuja: age, parametri: value tai newAge), jolloin vältät this-osoittimen pakollisen käytön ja koodi on selkeämpi.
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
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);
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; //vapautetaan muisti objectPerson2 = nullptr; //osoitin ei enää osoita vapautettuun muistiinJälkimmäinen koodirivi on tarpeen, koska se estää ns. dangling pointer-tilanteen ja parantaa koodin turvallisuutta. Jos ei mainittua riviä lisätä ja yrität vahingossa deletoida olion uudelleen tai viitata olioon uudelleen, saat undefined behavior-tilanteen.
Undefined behavior voi johtaa ohjelman kaatumiseen, muistivirheisiin tai satunnaiseen toimintaan.
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
Moderni C++ mahdollistaa resurssien hallinnan älykkäiden osoittimien avulla, mikä helpottaa muistin vapauttamista ja vähentää mahdollisuutta virheisiin.
C++ sisältää kolmenlaisia smart osoittimia:
Smart pointer voidaan luoda koodilla:
#include <memory> unique_ptr<Person> objectPerson = make_unique<Person>();Edellä olio luodaan kekoon, mutta se tuhoutuu automaattisesti.
Muistivuoto syntyy, kun dynaamisesti varattu muisti jää vapauttamatta eikä ohjelmalla ole enää mahdollisuutta vapauttaa sitä. Tavallista osoitinta käytettäessä näin voi käydä, jos ohjelmoija unohtaa kirjoittaa delete-lauseen tai suoritus poistuu funktiosta ennen sitä esimerkiksi ennenaikaisen return-lauseen tai poikkeuksen vuoksi. Pelkkä osoitinmuuttujan poistuminen näkyvyysalueelta ei vapauta sen osoittamaa muistia.
Smart pointer estää muistivuodot (memory leak) automaattisella muistinhallinnalla. Vertaillaan perinteistä osoitinta ja unique_ptr:ää:
Esimerkki 1: Tavallinen osoitin – muistivuoto ennenaikaisessa paluussa
void processData(bool jokinEhto) {
Person* p = new Person();
p->setAge(25);
if (jokinEhto) {
return; // Funktiosta poistutaan ennen deleteä: olio jää vapauttamatta.
}
delete p; // Olio vapautetaan vain, jos suoritus pääsee tähän.
}
Esimerkki 2: unique_ptr - ei muistivuotoa!
void processData(bool jokinEhto) {
unique_ptr<Person> p = make_unique<Person>();
p->setAge(25);
if (jokinEhto) {
return; // OK: unique_ptr vapauttaa muistin automaattisesti!
}
// unique_ptr vapauttaa muistin myös normaalin paluun yhteydessä
}
unique_ptr huolehtii olion tuhoamisesta myös silloin, kun lohkosta poistutaan poikkeuksen käsittelyn yhteydessä. Tavallista osoitinta käytettäessä poikkeus voi ohittaa delete-rivin ja aiheuttaa muistivuodon.
unique_ptr:n käyttö metodeissa
unique_ptr<Person> p = make_unique<Person>();
// Metodien kutsu nuolioperaattorilla (kuten tavallinen osoitin)
p->setAge(30);
p->setName("Teppo");
// Ei tarvitse delete:ä - unique_ptr hoitaa kaiken!
Yhteenveto: Miksi unique_ptr on parempi?
Suositus: Käytä aina unique_ptr:ää (tai shared_ptr:ää) perinteisten osoittimien sijasta, kun luot olioita dynaamisesti heap-muistiin. Tämä on moderni C++ -käytäntö ja estää lukuisia muistiongelmia.
Kappaleessa Koostaminen selitetään mitä eroa unique- ja shared- pointtereilla on.
Oletetaan, että meillä on luokka Student, joka sisältää jäsenmuuttujat name (nimi), studentNumber (opiskelijanumero) ja average (keskiarvo). Luodaan kolmella eri tavalla Student-luokan olioita ja käytetään niille nimiä objStudent1, objStudent2 ja objStudent3:
Student objStudent1; luo itse Student-olion paikalliseksi muuttujaksi, tyypillisesti pinoon. Olio tuhoutuu automaattisesti lohkosta poistuttaessa.Student* objStudent2 = new Student; luo Student-olion kekoon. Paikallinen osoitinmuuttuja objStudent2 sijaitsee itse pinossa ja sisältää olion muistiosoitteen. Olio täytyy tuhota delete-operaattorilla.unique_ptr<Student> objStudent3 = make_unique<Student>(); luo Student-olion kekoon. Paikallinen unique_ptr-olio sijaitsee itse pinossa ja omistaa Student-olion. Student-olio tuhoutuu automaattisesti, kun sen omistava älyosoitin tuhoutuu.Osoittimen oma osoite ja sen sisältämä osoite ovat eri asioita.
&objStudent2 kertoo osoitinmuuttujan oman muistipaikan, kun taas objStudent2 sisältää sen kohteena olevan Student-olion osoitteen.
Älyosoittimesta kohdeolion osoitteen saa kutsulla objStudent3.get(); &objStudent3 on älyosoitinolion oma osoite.
Osoittaa tarkoittaa, että osoitin sisältää olion muistiosoitteen.
Omistaa tarkoittaa vastuuta olion elinkaaresta: tässä unique_ptr tuhoaa omistamansa olion automaattisesti.
Kaavio esittää alla olevan koodin paikalliset muuttujat ja niiden kohdeoliot. Osoitteet ovat keksittyjä, eivätkä laatikoiden koot tai järjestys kuvaa todellista muistiasettelua. Pino ja keko kuvaavat tässä tavallista toteutusta; C++ määrittelee olioiden säilytysajat, ei niiden fyysistä sijoittelua. Olioiden mahdollisia sisäisiä muistivarauksia (esimerkiksi merkkijonojen dataa) ei näytetä.
#include <memory>
{
Student objStudent1;
Student* objStudent2 = new Student;
unique_ptr<Student> objStudent3 = make_unique<Student>(); // C++14 tai uudempi
// 1. Kaikki kolme oliota ovat elossa.
delete objStudent2; // 2. Kohdeolio tuhoutuu, osoitin jää jäljelle.
objStudent2 = nullptr; // 3. Osoitin ei enää osoita vapautettuun muistiin.
// 4. objStudent3:n kohdeolio ja objStudent1 tuhoutuvat automaattisesti.
}
1. Kaikki kolme oliota ovat elossa. Osoittimilla ja niiden kohdeolioilla on omat muistipaikkansa.
&objStudent1 = 0x1000name, studentNumber, average
Elossa · tuhoutuu lohkon lopussa&objStudent2 = 0x10500x50000x5000name, studentNumber, average
Elossa · tuhotaan delete-operaattorilla&objStudent3 = 0x1060objStudent3.get() = 0x60000x6000name, studentNumber, average
Elossa · unique_ptr huolehtii tuhoamisestaTuhoutuneiden olioiden laatikot jäävät kaavioon vain havainnollistamaan aiempia muistipaikkoja.
Niiden osoitteita ei voi enää käyttää olioiden käsittelyyn. delete ei nollaa osoitinta,
eikä pelkkä nullptr-arvon asettaminen tuhoa kohdeoliota.
Tässä käytetään unique_ptr-osoitinta; shared_ptr-osoittimilla kohde tuhoutuu vasta viimeisen omistajan luopuessa siitä.
Viittaus (reference) on C++:n ominaisuus, jota ei ole C-kielessä. Viittaus on alias eli vaihtoehtoinen nimi muuttujalle. Se on vaihtoehto osoittimelle, mutta turvallisempi ja usein helpompi käyttää.
Viittauksen määrittely:
int age = 25; int &ageRef = age; // ageRef on viittaus age-muuttujaan ageRef = 30; // Muuttaa age-muuttujan arvon 30:ksi cout << age; // Tulostaa: 30
Ero osoittimeen:
| Ominaisuus | Viittaus (int &ref) | Osoitin (int *ptr) |
|---|---|---|
| Alustus | Pakollinen heti määrittelyssä | Ei pakollinen, voi olla nullptr |
| Muuttaminen | Ei voida vaihtaa osoittamaan toiseen muuttujaan | Voidaan vaihtaa osoittamaan toiseen muuttujaan |
| Syntaksi | Käytetään kuten tavallista muuttujaa | Vaatii * ja -> operaattoreita |
| Turvallisuus | Ei voi olla "tyhjä" | Voi olla nullptr, voi aiheuttaa virheitä |
Viittaus on toinen nimi samalle muuttujalle. Osoitin puolestaan on erillinen muuttuja, jonka arvona on muistiosoite.
Viittaus vs. Osoitin - Esimerkki:
int x = 10; // Viittaus int &ref = x; ref = 20; // Muuttaa x:n arvon 20:ksi cout << x; // 20 // Osoitin int *ptr = &x; *ptr = 30; // Muuttaa x:n arvon 30:ksi (vaatii *) cout << x; // 30 ptr = nullptr; // OK: osoitin voi olla nullptr int y = 20; // ref = y; // VIRHE: viittausta ei voi vaihtaa osoittamaan toiseen muuttujaan
Viittaukset parametreina (tärkein käyttötapa):
Viittaukset ovat erityisen hyödyllisiä funktioiden parametreina. Ne välttävät kopioinnin ja mahdollistavat muutosten tekemisen alkuperäiseen muuttujaan.
// Kopioi arvon (hidas isoilla olioilla)
void setAge1(Person p) {
p.setAge(25); // Muuttaa vain KOPION ikää, ei alkuperäistä!
}
// Viittaus (nopea, muuttaa alkuperäistä)
void setAge2(Person &p) {
p.setAge(25); // Muuttaa alkuperäisen olion ikää
}
// Const-viittaus (nopea, ei voi muuttaa - PARAS tapa lukemiseen)
void printPerson(const Person &p) {
cout << p.getName(); // Vain lukeminen, ei kopiointia
// p.setAge(30); // VIRHE: const-viittaus
}
Milloin käyttää viittausta vs. osoitinta:
Viittaus for-silmukassa:
Olet jo nähnyt viittauksen käytön for-silmukassa aiemmin oppaassa:
for(const Student &student : studentList) {
student.printStudentData(); // Ei kopiointia, nopea
}
Yhteenveto:
const Type&) funktioparametreissa kun haluat välttää kopioinnin mutta et halua muuttaa arvoaEdellä, 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;
}
Vaihtoehtoisesti jäsenmuuttujan voi alustaa muodostimen alustuslistassa:
Person::Person(int value)
: age(value)
{
}
Alustuslista alkaa kaksoispisteellä ennen muodostimen runkoa. Merkinnässä age(value)
age on jäsenmuuttuja ja value muodostimen parametri.
Näin jäsenmuuttuja alustetaan suoraan parametrin arvolla ennen rungon suorittamista.
Ensimmäisessä vaihtoehdossa arvo sijoitetaan jäsenmuuttujaan vasta muodostimen rungossa.
Suosi yleensä alustuslistaa jäsenmuuttujien alustamiseen. Esimerkiksi merkkijonojäsen alustetaan silloin suoraan halutulla arvolla. Jos arvo sijoitetaan vasta muodostimen rungossa, merkkijono muodostetaan ensin tyhjäksi ja sille sijoitetaan arvo sen jälkeen.
Alustuslista on välttämätön esimerkiksi const- ja viitejäsenten alustamiseen, kun niille ei ole annettu alkuarvoa luokan määrittelyssä. Sitä tarvitaan myös, kun kantaluokan tai jäsenolion muodostimelle pitää antaa argumentteja. Muodostimen runkoon voi kirjoittaa muut alustamiseen liittyvät toimenpiteet.
Huomaa järjestys: jäsenmuuttujat alustetaan aina siinä järjestyksessä, jossa ne on esitelty luokan määrittelyssä. Kirjoita myös alustuslista samaan järjestykseen.
Kummassakin vaihtoehdossa olioa luotaessa pitää antaa 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 muodostimien 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
C++ mahdollistaa funktioiden ylikuormittamisen (function overloading). Tämä tarkoittaa, että samalla nimellä voidaan määritellä useita funktioita, kunhan niiden parametrilistat eroavat toisistaan. Paluuarvo voi olla sama tai erilainen, mutta pelkkä erilainen paluuarvo ei riitä ylikuormittamiseen. Kääntäjä valitsee kutsuttavan funktion argumenttien tyyppien perusteella.
Esimerkki ylikuormittamisesta:
void doubleMe(int a)
{
int doubled = 2 * a;
cout << "Kokonaisluku " << a << " tuplattuna = " << doubled << endl;
}
void doubleMe(double a)
{
double doubled = 2 * a;
cout << "Desimaaliluku " << a << " tuplattuna = " << doubled << endl;
}
Kutsun yhteydessä kääntäjä valitsee argumenttien tyyppien perusteella sopivan version:
doubleMe(7); // Tyyppi int: ensimmäinen versio doubleMe(5.5); // Tyyppi double: toinen versio
Funktiomalli mahdollistaa edellisen kaltaisen toteutuksen yhdellä funktiolla. Funktiomallissa tyypin paikalla käytetään tyyppiparametria T. Kääntäjä päättelee tyypin funktion kutsun perusteella ja muodostaa tarvittaessa funktiosta kyseiselle tyypille sopivan version.
Esimerkki funktiomallista:
template <typename T>
void doubleMe(T a)
{
T doubled = 2 * a;
cout << "Luku " << a << " tuplattuna = " << doubled << endl;
}
Tätä funktiomallia voidaan kutsua eri tyyppisillä argumenteilla:
doubleMe(7); // T päätellään tyypiksi int doubleMe(5.5); // T päätellään tyypiksi double
Milloin käytetään ylikuormittamista ja milloin funktiomallia?
Ylikuormittaminen sopii tilanteeseen, jossa eri tyyppiset argumentit vaativat erilaisen toteutuksen. Funktiomalli sopii tilanteeseen, jossa sama algoritmi toimii useilla tyypeillä. Edellä doubleMe-funktion toiminta on sama sekä kokonaisluvuille että desimaaliluvuille, joten funktiomalli vähentää toistuvaa koodia. Jos eri tyyppien käsittely vaatisi erilaisen algoritmin, funktioita voisi ylikuormittaa erikseen.
Periytyminen eli inheritance tarkoittaa, että luokka voi periä ominaisuuksia ja toimintoja toiselta luokalta. Ajatellaan esimerkiksi, että rakentaisimme sovellusta, jolla käsitellään jonkin oppilaitoksen dataa. Todetaan, että oppilaitoksessa on opiskelijoita ja opettajia. Molemmilla on monia samankaltaisia ominaisuuksia, kuten nimi ja syntymävuosi. Lisäksi opiskelijoilla on ryhmätunnus ja opettajilla on osasto.
Nyt voidaan tehdä niin, että
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 määrittää perintämuoto seuraavilla vaihtoehdoilla:
| Kantaluokan näkyvyys | Näkyvyys perivässä luokassa eri perintämuodoilla | ||
|---|---|---|---|
| public-perintä | protected-perintä | private-perintä | |
| public | public | protected | private |
| protected | protected | protected | private |
| private | ei käytettävissä | ei käytettävissä | ei käytettävissä |
Student-luokan aliluokissa. Mieti siis perintämuotoa huolellisesti, jos Student-luokkaa aiotaan periyttää edelleen.
Voit lukea lisätietoa periytymisestä sivulta https://www.tutorialspoint.com/cplusplus/cpp_inheritance.htm
Kun luodaan perivän luokan (Student) olio, kutsutaan aina myös kantaluokan (Person) muodostinta.
public- tai protected-näkyvyydellä.Jos kantaluokassa (Person) on parametreja ottava muodostin, se voidaan valita perivän luokan (Student) muodostimen alustuslistassa.
Esimerkiksi, jos kantaluokassa Person on seuraava muodostin:
Person::Person (string na){
name=na;
}
Perivän Student-luokan muodostin voisi 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.
Nyt siis Student-olio voidaan luoda seuraavasti:
Student student("TVT23SPL", "Teppo Testi");
Tässä luodaan student-olio ja välitetään muodostimelle ryhmän nimi sekä henkilön nimi. Alustuslistan Person(na) valitsee kantaluokan muodostimen, joka tallentaa henkilön nimen. Sen jälkeen groupName(gr) alustaa ryhmän nimen. Vasta näiden jälkeen suoritetaan Student-muodostimen runko, joka on tässä esimerkissä tyhjä.
Jos Person-luokalla on lisäksi käytettävissä oleva oletusmuodostin ja public-metodi setName(), Student-luokan muodostin voitaisiin kirjoittaa myös seuraavasti:
Student::Student(string gr, string na ) {
groupName=gr;
this->setName(na);
}
Tässä vaihtoehdossa kutsutaan ensin Person-luokan oletusmuodostinta ja oletusalustetaan groupName-jäsenmuuttuja tyhjäksi merkkijonoksi. Sen jälkeen Student-luokan muodostimen rungossa sijoitetaan ryhmän nimi ja asetetaan henkilön nimi setName(na)-kutsulla.
Ylikirjoittaminen (overriding) C++-ohjelmoinnissa tarkoittaa, että aliluokka määrittelee uudelleen kantaluokassa määritetyn virtuaalisen metodin. Kun ylikirjoitettua metodia kutsutaan aliluokan instanssin kautta, suoritetaan aliluokan versio kyseisestä metodista. Tämä mahdollistaa polymorfismin, jossa aliluokka voi muokata kantaluokan toiminnallisuutta omien tarpeidensa mukaisesti.
Ylikirjoittaminen edellyttää, että sekä kantaluokan että aliluokan metodit ovat määritelty samalla nimellä, paluuarvolla ja parametreilla, ja kantaluokan metodin on oltava virtual-avainsanalla merkitty. Aliluokan metodi 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: ";
}
void Student::sayStatus()
{
cout<<"Opiskelija: ";
}
void Teacher::sayStatus()
{
cout<<"Opettaja: ";
}
Esimerkki löytyy sivulta https://github.com/olio-kurssi/esim4
virtual ja aliluokan korvaava metodi sanalla override.
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.
Esimerkki 1: Perinteinen taulukko
#include <iostream>
int main() {
int luvut[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; ++i) {
std::cout << luvut[i] << " ";
}
return 0;
}
Esimerkki 2: std::vector
#include <iostream>
#include <vector>
int main() {
std::vector<int> luvut = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; ++i) {
std::cout << luvut[i] << " ";
}
return 0;
}
Huom! Kummassakin esimerkissä edellä for-silmukka voitaisiin korvata myös versiolla:
for (int luku : luvut) {
std::cout << luku << " ";
}
Huomioita
std::vector on turvallisempi ja joustavampi kuin perinteinen taulukko.push_back(), size(), ja at().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 olioita listaan
studentList.emplace_back("Teppo Testi", 1999, "TVT23SPL");
studentList.emplace_back("Liisa Joki", 1998, "TVT23SPL");
studentList.emplace_back("Aino Virta", 1997, "TVT23SPO");
studentList.emplace_back("Matti Virtanen", 2001, "TVT23SPO");
studentList.emplace_back("Mikko Vilkas", 2001, "TVT23SPL");
for(int x=0; x<=4; x++){
studentList[x].printStudentData();
}
return 0;
}
Edellä for silmukka voitaisiin korvata myös seuraavalla rakenteella, jossa ei tarvitse tietää montako alkiota vektorissa on.
for(Student student: studentList){
student.printStudentData();
}
Edellä esitetty for-silmukka kopioi jokaisen opiskelijan uuteen muuttujaan student. Jos haluat vain lukea tietoja, tämä toimii, mutta on tehotonta isoilla olioilla.
Kaksi tapaa käydä vektori läpi:
1. Kopiointi (hidas, muutokset eivät vaikuta alkuperäiseen)
for(Student student: studentList){
student.setAge(25); // Muuttaa vain KOPION ikää, ei alkuperäistä!
}
2. Viittaus (nopea, muutokset vaikuttavat alkuperäiseen)
for(Student &student: studentList){
student.setAge(25); // Muuttaa alkuperäisen opiskelijan ikää!
}
3. Const-viittaus (nopea, ei voi muuttaa - SUOSITUS lukemiseen)
for(const Student &student: studentList){
student.printStudentData(); // Vain lukeminen, ei kopiointia
// student.setAge(25); // VIRHE: ei voi muuttaa const-viittausta
}
Yhteenveto:
for(Student student : list) - Kopioi olion, hidas, muutokset eivät vaikuta alkuperäiseenfor(Student &student : list) - Viittaus, nopea, voit muuttaa alkuperäistäfor(const Student &student : list) - Const-viittaus, nopea, vain lukeminen (PARAS tapa lukemiseen)Suositus: Käytä aina const-viittausta (const Student&), kun haluat vain lukea tietoja. Se on nopea eikä kopioi olioita, mutta estää vahingossa tapahtuvat muutokset.
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>>();
studentList->emplace_back("Teppo Testi", 1999, "TVT23SPL");
studentList->emplace_back("Liisa Joki", 1998, "TVT23SPL");
studentList->emplace_back("Aino Virta", 1997, "TVT23SPO");
studentList->emplace_back("Matti Virtanen", 2001, "TVT23SPO");
studentList->emplace_back("Mikko Vilkas", 2001, "TVT23SPL");
for(Student &student : *studentList){
student.printStudentData();
}
return 0;
}
Edellä siis:
Esimerkki löytyy sivulta https://github.com/olio-kurssi/esim7
Huom! Yleensä vektori kannattaa luoda pinoon, koska se on helpompi tapa. Tuota smart pointteria kannattaa käyttää vain, jos vektori on todella suuri.
emplace_back vs push_back: Esimerkeissä on käytetty emplace_back-metodia olioiden lisäämiseen vektoriin. Metodi emplace_back konstruoi olion suoraan vektoriin ottamalla vastaan konstruktorin parametrit, mikä on tehokkaampi tapa kuin push_back. Metodi push_back ottaa vastaan valmiin olion ja kopioi tai siirtää sen vektoriin, mikä voi vaatia ylimääräisiä kopio- tai siirto-operaatioita.
Esimerkki push_back-metodin käytöstä:
// Luodaan ensin Student-olio
Student student("Teppo Testi", 1999, "TVT23SPL");
// Lisätään olio vektoriin push_back-metodilla
studentList.push_back(student);
// Tai voidaan luoda olio suoraan push_back-kutsun yhteydessä
studentList.push_back(Student("Liisa Joki", 1998, "TVT23SPL"));
Vertailun vuoksi sama emplace_back-metodilla:
// emplace_back konstruoi olion suoraan vektoriin
studentList.emplace_back("Teppo Testi", 1999, "TVT23SPL");
Assosiaatio tarkoittaa luokkien välistä yhteyttä, jossa toisen luokan oliot eivät omista toisen luokan olioita. Käytännössä tämä tarkoittaa että oliot ovat yhteydessä toisiinsa tilapäisesti, esimerkiksi metodin parametrina välitettynä viittauksena.
Esimerkki: Doctor-luokka ja Patient-luokka ovat assosiaatiossa. Käytännössä tämä tarkoittaa, että Doctor-olio voi käsitellä Patient-oliota metodissaan, mutta ei omista sitä. Molemmat oliot voivat olla olemassa itsenäisesti.
UML-diagrammi:
classDiagram
direction TB
class Doctor {
-string name
+Doctor(string name)
+diagnose(Patient& patient) void
}
class Patient {
-string name
+Patient(string name)
+getName() string
}
Doctor ..> Patient : uses
Assosiaation pääpiirteet:
Käytännön ero koodissa:
Assosiaatiossa toisen luokan olio ei ole jäsenmuuttuja, vaan se välitetään metodille parametrina:
class Doctor {
private:
string name;
// EI Patient-jäsenmuuttujaa!
public:
void diagnose(const Patient& patient) { // Vain parametri
// Käyttää Patient-oliota tilapäisesti
}
};
Assosiaation erikoistapaukset:
Aggregaatiossa ja kompositiossa toisen luokan olio on jäsenmuuttuja:
Koostaminen (composition) tarkoittaa "osa–kokonaisuus"-suhdetta luokkien välillä. Koostaminen voi olla joko heikkoa (aggregaatio) tai vahvaa (kompositio). Erona on, että aggregaatiossa osat voivat olla olemassa myös ilman kokonaisuutta, mutta kompositiossa eivät.
Aggregaatio on assosiaation erikoistapaus, jossa toinen luokka nähdään kokonaisuutena ja toinen sen osana. Osat voivat kuitenkin olla olemassa myös itsenäisesti ilman kokonaisuutta.
Esimerkki: Team-luokka koostuu Player-luokan olioista. Käytännössä tämä tarkoittaa, että Team-olio sisältää Player-olioita, mutta Player-oliot voivat olla olemassa ilman Team-oliota. Sama pelaaja-olio voi kuulua useampaan joukkue-olioon (jaettu omistajuus).
UML-diagrammi:
classDiagram
direction TB
class Team {
-string name
-vector~shared_ptr~Player~~ players
+Team(string name)
+addPlayer(shared_ptr~Player~ player) void
+printPlayers() void
}
class Player {
-string name
+Player(string name)
+getName() string
}
Team o-- Player : has
note for Team "shared_ptr = jaettu omistajuus<br/>Player voi olla olemassa<br/>ilman Team-oliota"
Aggregaatiossa osaoliot luodaan yleensä koostavan olion ulkopuolella, eikä koostava olio vastaa niiden tuhoamisesta. Tässä yhteydessä on kätevää käyttää shared_ptr-osoittimia, joilla jaettu omistajuus voidaan ilmaista. Osaolio tuhoutuu, kun viimeinen sitä omistava shared_ptr vapautuu. Tämä tekee selvän eron aggregaation ja komposition välille: kompositiossa koostava olio vastaa osaolioiden elinkaaresta, aggregaatiossa ei.
Aggregaation pääpiirteet:
shared_ptr:a jaetun omistajuuden ilmaisemiseenshared_ptr-osoittimilla, osaolio tuhoutuu, kun viimeinen sitä omistava shared_ptr vapautuuKäytännön toteutus koodissa:
class Team {
private:
vector<shared_ptr<Player>> players; // Jäsenmuuttuja!
public:
void addPlayer(shared_ptr<Player> player) {
players.push_back(player); // Tallennetaan luokkaan
}
};
Lisää esimerkkejä:
Kompositio on vahvin erikoistapaus aggregaatiosta. Suunnittelutasolla osien elinkaari on sidottu kokonaisuuteen. Käytännössä tämä tarkoittaa, että kun kokonaisuus-olio tuhoutuu, sen osa-oliotkin tuhoutuvat automaattisesti. Koostava olio omistaa osaoliot ja vastaa niiden elinkaaresta: se luo osaoliot ja huolehtii niiden tuhoamisesta. Osaolio voidaan luoda esimerkiksi koostavan olion konstruktorissa, jäsenmuuttujan alustuksessa tai muussa koostavan olion hallitsemassa toiminnossa.
Esimerkki: Car-luokka koostuu Engine-luokasta. Käytännössä tämä tarkoittaa, että Car-olio omistaa Engine-olion. Engine-olio ei voi olla olemassa ilman Car-oliota, ja kun Car-olio tuhoutuu, myös Engine-olio tuhoutuu automaattisesti.
UML-diagrammi:
classDiagram
direction TB
class Car {
-string model
-unique_ptr~Engine~ objEngine
+Car(string model)
+printInfo() void
}
class Engine {
-string type
+Engine(string type)
+getType() string
}
Car *-- Engine : owns
note for Car "unique_ptr = yksinomainen omistajuus<br/>Engine-olio tuhoutuu<br/>Car-olion mukana"
Komposition pääpiirteet:
unique_ptr:ää yksinomaisen omistajuuden ilmaisemiseenKäytännön toteutus koodissa:
class Car {
private:
unique_ptr<Engine> objEngine; // Jäsenmuuttuja!
public:
Car(const string& model) {
objEngine = make_unique<Engine>("V8"); // Luodaan konstruktorissa
}
// ~Car() tuhoaa automaattisesti myös objEngine:n
};
Lisää esimerkkejä:
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ä
Abstrakti luokka on luokka, josta ei voi luoda oliota, mutta jota voidaan käyttää kantaluokkana. C++:ssa luokka saadaan abstraktiksi lisäämällä siihen vähintään yksi puhdas virtuaalimetodi (pure virtual method). Esimerkiksi Person-luokan sayStatus()-metodin esittely muutetaan muotoon:
virtual void sayStatus() = 0;
Tässä metodille ei kirjoiteta toteutusta person.cpp-tiedostoon. Person-luokassa voi silti olla jäsenmuuttujia, muodostimia ja muita metodeja valmiine toteutuksineen. Perivän luokan pitää toteuttaa sayStatus(), jotta siitä voidaan luoda olio. Muuten myös perivä luokka jää abstraktiksi.
Abstraktius ei itsessään vaadi virtuaalista tuhoajaa. Kantaluokan tuhoajan pitää kuitenkin olla virtuaalinen, kun perivän luokan olio tuhotaan kantaluokan osoittimen kautta. Esimerkiksi:
Person* person = new Student(); delete person;
Tämä edellyttää, että Student voidaan muodostaa ilman argumentteja ja Person-luokan julkisessa osiossa on virtuaalinen tuhoaja:
virtual ~Person() = default;
Näin myös Student-luokan tuhoaja suoritetaan oikein. Ilman virtuaalista tuhoajaa edellä olevan delete-lauseen toiminta on määrittelemätöntä. Sama vaatimus koskee std::unique_ptr<Person>-älyosoitinta, joka omistaa Student-olion. Merkintä = default pyytää kääntäjää tuottamaan tuhoajan toteutuksen.
Esimerkki löytyy sivulta https://github.com/olio-kurssi/esim5
Rajapinta (interface) määrittelee, mitä metodeja sen toteuttavan luokan pitää tarjota. C++:ssa rajapintakin on abstrakti luokka. Erillistä interface-avainsanaa tai muuta merkintää, joka muuttaisi abstraktin luokan rajapinnaksi, ei ole.
Kun luokka suunnitellaan puhtaaksi rajapinnaksi, sen toimintametodit esitellään public-osiossa puhtaina virtuaalimetodeina (virtual ... = 0). Siihen ei lisätä jäsenmuuttujia tai toimintametodien toteutuksia. Näin luokka määrittelee vain vaaditun toiminnan. Esimerkiksi:
class IPerson {
public:
virtual ~IPerson() = default;
virtual void sayStatus() = 0;
};
Virtuaalinen tuhoaja mahdollistaa olion tuhoamisen IPerson-osoittimen kautta; = default pyytää kääntäjää tuottamaan tuhoajan toteutuksen. Tuhoaja ei tee luokasta rajapintaa. Myös nimen alun I on vain nimeämiskäytäntö.
Rajapinta toteutetaan julkisella periytymisellä ja toteuttamalla sen puhtaat virtuaalimetodit. Luokan h-tiedostossa esimerkiksi:
class Student : public IPerson {
public:
void sayStatus() override;
};
Metodin toteutus kirjoitetaan student.cpp-tiedostoon. Rajapinnan perivän luokan on toteutettava kaikki rajapinnan puhtaat virtuaalimetodit, jotta luokasta voidaan luoda olio. Muuten myös perivä luokka jää abstraktiksi.
Ero käytännössä: abstrakti kantaluokka voi jakaa periville luokille yhteistä tietoa ja valmista toimintaa. Puhdas rajapinta määrittelee vain toimintametodien vaatimukset. Tämä on C++:ssa suunnittelukäytäntö, ei kääntäjän valvoma erillinen luokkatyyppi.
Esimerkki, jossa Student ja Teacher perivät sekä IPerson-rajapinnan että Person-luokan, löytyy sivulta https://github.com/olio-kurssi/esim8.
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.
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
auto on C++11:ssä esitelty avainsana, joka mahdollistaa tyyppi-inferenssin eli kääntäjä päättelee muuttujan tyypin automaattisesti annetun arvon perusteella.
Perusesimerkki:
// Perinteinen tapa - eksplisiittinen tyyppi int age = 25; string name = "Teppo"; double price = 19.99; // Auto - kääntäjä päättelee tyypin auto age = 25; // Tyyppi: int auto name = "Teppo"; // Tyyppi: const char* (HUOM!) auto price = 19.99; // Tyyppi: double
Hyödylliset käyttötapaukset:
1. Smart pointerit - koodi selkeämpää
// Ilman autoa - pitkä ja toistuva unique_ptr<Person> p1 = make_unique<Person>(); shared_ptr<Student> s1 = make_shared<Student>(); // Autolla - lyhyempi ja selkeämpi auto p1 = make_unique<Person>(); auto s1 = make_shared<Student>();
2. For-silmukat - erityisen hyödyllinen
vector<Student> studentList;
// Ilman autoa
for(const Student &student : studentList) {
student.printStudentData();
}
// Autolla - yhtä selkeä, vähemmän kirjoitusta
for(const auto &student : studentList) {
student.printStudentData();
}
3. Monimutkaiset tyypit
// Ilman autoa - vaikealukuista // begin() palauttaa iteraattorin (osoittimen kaltainen objekti) vektorin alkuun vector<unique_ptr<Student>>::iterator it = studentList.begin(); // Autolla - paljon selkeämpi, kääntäjä päättelee tyypin auto it = studentList.begin();
Milloin VÄLTTÄÄ auto:a:
int age = 25; eikä auto age = 25; kun opettelet.auto result = calculateSomething(); - mikä on result:n tyyppi? Eksplisiittinen tyyppi dokumentoi koodia paremmin.auto name = "Teppo"; on tyyppiä const char*, ei string! Käytä string name = "Teppo"; tai auto name = string("Teppo");Hyvät käytännöt:
auto:a kun tyyppi on ilmeinen oikean puolen perusteella (esim. make_unique, make_shared)auto:a for-silmukoissa, erityisesti const-viittausten kanssa: for(const auto &item : list)auto:a kun tyyppi on pitkä ja monimutkainen (esim. iteraattorit)Auto ja viittaukset:
Person p; auto p1 = p; // Kopioi p:n -> p1 on tyyppiä Person auto &p2 = p; // Viittaus p:hen -> p2 on tyyppiä Person& const auto &p3 = p; // Const-viittaus -> p3 on tyyppiä const Person& p1.setAge(25); // Muuttaa vain p1:n ikää p2.setAge(30); // Muuttaa p:n ikää (viittaus!) // p3.setAge(35); // VIRHE: const-viittaus
Yhteenveto:
auto on hyödyllinen työkalu, joka tekee koodista lyhyempää ja helpommin ylläpidettävääauto:a järkevästiauto:a vain siksi että se on lyhyempi - käytä sitä kun se tekee koodista selkeämpääOppaaseen liittyvien esimerkkien lähdekoodit löytyy sivulta https://github.com/orgs/olio-kurssi.
| repository | aihe |
|---|---|
| esim0 | Tietue |
| esim1 | Luokka ja olio (pino ja keko) |
| esim2 | Muodostin ja tuhoaja |
| esim3 | Periytyminen |
| esim4 | Virtuaalimetodi |
| esim5 | Abstraktiluokka |
| esim6 | Vahva kooste |
| esim6b | Heikko kooste |
| esim7 | Oliotaulukko |
| esim8 | Interface luokka |
| esim9 | Staattinen luokka |
| unit_test | Yksikkötestaus |