STL:n vektori ja sen läpikäynti (for-lause)¶
C++:ssa monimutkaisemmat tietorakenteet (kuten vektori) ja niiden käsittelyyn tarkoitetut mekanismit eivät ole osa kieltä itseään, vaan ne on toteutettu kirjastoissa. Näiden kirjastojen muodostamaa kokonaisuutta kutsutaan yleensä Standard Template Libraryksi (STL).
STL muodostuu kolmesta loogisesta osakokonaisuudesta:
- säiliöt eli tietorakenteet, jotka vastaavat Python-kielen
list
,set
jadict
-rakenteita, ja monia muita, joita vastaavia Pythonissa ei ole valmiina - iteraattorit, joita voi tässä vaiheessa ajatella eräänlaisina kirjanmerkkeinä, joihin voi tallettaa yksittäisten tietoalkioiden sijainnin säiliön sisällä
- algoritmit, jotka tarjoavat valmiita mekanismeja perusoperaatioiden
suorittamiseksi säiliöille (esim.
sort
-algoritmin avulla säiliön alkiot voi laittaa haluamaansa järjestykseen).
STL on erittäin laaja kokoelma kirjastoja.
Tällä kurssilla siihen ei tutustuta kuin yleissivistävästi.
(Esimerkiksi opintojaksolla TIE-20100
Tietorakenteet ja algoritmit jatketaan aiheesta syvällisemmin.)
Tällä kierroksella tutustumme vector
-säiliöön.
Seuraavalla kierroksella otamme käyttöön muutaman muunkin säiliön
ja tutustumme STL:n muihin osiin.
Python-ohjelmissasi on todennäköisesti usein esiintynyt sellainen
tilanne, että sinulla on jokin tietorakenne ja haluat käydä sen läpi
alusta loppuun alkio kerrallaan.
Tähän tarkoitukseen olet käyttänyt for
-lausetta.
Myös C++:ssa on for
-lause, mutta se poikkeaa syntaksiltaan Pythonin
vastineesta.
C++:ssa tietorakenteita voi käydä läpi myös edellä mainittujen
iteraattoreiden avulla.
STL:n vektori¶
STL:n (Standard Template Library) vector
-tietotyyppi muistuttaa hyvin
läheisesti Pythonin
list
-tyyppiä.
Molemmat voivat sisältää useita tietoalkioita, joihin
päästään käsiksi, jos tiedetään niiden indeksi.
Pythonista poiketen C++:n vektorin kaikkien alkioiden on oltava
keskenään samantyyppisiä.
Tutkitaan pientä esimerkkiohjelmaa, joka osaa optimoida
(silmukattoman) labyrintin läpi löydetyn reitin. Reitti ilmaistaan
merkkijonona, jossa merkki P tarkoittaa yhden ruudun pituista
siirtymää pohjoiseen, merkki E yhden ruudun pituista siirtymää
etelään, jne. Jos esimerkiksi labyrintissä satunnaisesti harhailemalla
on onnistuttu löytämään reitti PLIPIPLIILEILLEI
, olisi siitä
optimoitu reitti PI
, koska merkkijonosta voidaan poistaa
esimerkiksi toisena ja kolmantena olevat merkit LI
ja vastaavasti
monta muutakin turhaa kohtaa.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
const char POHJ = 'P';
const char ITA = 'I';
const char ETELA = 'E';
const char LANSI = 'L';
void TulostaReitti(const vector<char>& reittivektori) {
vector<char>::size_type indeksi = 0;
while ( indeksi < reittivektori.size() ) {
cout << reittivektori.at(indeksi);
++indeksi;
}
cout << endl;
}
int main() {
string reitti = "";
cout << "Syota kuljettu reitti: ";
getline(cin, reitti);
vector<char> optimoitu_reitti = { };
string::size_type i = 0;
while ( i < reitti.length() ) {
char uusi_suunta = reitti.at(i);
if ( optimoitu_reitti.size() == 0 ) {
optimoitu_reitti.push_back(uusi_suunta);
} else {
char edellinen_suunta = optimoitu_reitti.back();
if ( edellinen_suunta == ITA
and uusi_suunta == LANSI ) {
optimoitu_reitti.pop_back();
} else if ( edellinen_suunta == LANSI
and uusi_suunta == ITA ) {
optimoitu_reitti.pop_back();
} else if ( edellinen_suunta == POHJ
and uusi_suunta == ETELA ) {
optimoitu_reitti.pop_back();
} else if ( edellinen_suunta == ETELA
and uusi_suunta == POHJ ) {
optimoitu_reitti.pop_back();
} else {
optimoitu_reitti.push_back(uusi_suunta);
}
}
++i;
}
TulostaReitti(optimoitu_reitti);
}
Vektorityypin käyttämiseksi koodin alkuun tarvitaan:
#include <vector>
Vektorityyppisen muuttujan määrittely tapahtuu näin:
vector<alkioiden_tyyppi> muuttujan_nimi;
esimerkiksi
vector<int> pistemaarat;
jonka tuloksena syntyneeseen muuttujaan pistemaarat
voidaan
tallentaa useita kokonaislukuja.
Vektorin loppuun voidaan lisätä uusia alkioita yksi kerrallaan:
pistemaarat.push_back(21);
jolloin vektorin koko (siihen tallennettujen alkioiden lukumäärä) kasvaa yhdellä.
Viimeinen alkio saadaan poistettua:
pistemaarat.pop_back();
jolloin vektorin koko pienenee yhdellä.
Jos vektori on tyhjä (siinä ei ole yhtään alkiota), kun pop_back
suoritetaan, aiheutuu poikkeus. Tutustumme C++:n poikkeusten
käsittelemiseen myöhemmässä vaiheessa (emme vielä tällä kurssilla).
Oikeaoppisesti pop_back
-metodin kutsua ennen olisi siis syytä
varmistua, että vektorissa on poistettavaa jäljellä, kuten on tehty
koodissa:
if ( pistemaarat.size() != 0 ) {
pistemaarat.pop_back();
} else {
// Virhe, tyhjästä vektorista ei voi poistaa.
}
missä size
kertoo vektoriin talletettujen alkioiden lukumäärän.
Jos size
-metodin paluuarvo halutaan tallentaa muuttujaan, sen
tyypin tulisi olla
vector<alkioiden_tyyppi>::size_type
Esimerkiksi
vector<int>::size_type koko = pistemaarat.size();
...
koko = pistemaarat.size();
Vektorin ensimmäisen ja viimeisen alkion arvoon päästään käsiksi:
cout << "eka: " << pistemaarat.front() << endl
<< "vika: " << pistemaarat.back() << endl;
Vektorin yksittäisiin arvoihin päästään käsiksi samoin kuin merkkijonon yksittäisiin merkkeihin:
cout << pistemaarat.at(3) << endl;
...
pistemaarat.at(indeksi) = uusi_pistemaara;
Indeksointi alkaa nollasta ja viimeisen alkion indeksi on yhtä pienempi kuin alkioiden lukumäärä.
Jos at
-metodilla yritetään indeksoida vektorista alkio, jota ei
ole olemassa, seurauksena on poikkeus, joka käsittelemättömänä kaataa
ohjelman.
Vektori voi funktion parametrina käytettynä olla joko arvo- tai viiteparametri normaaliin tapaan:
void funktio1(vector<double> mittaustulokset);
void funktio2(vector<double>& mittaustulokset);
Suurikokoisia tietorakenteita ei yleensä kannata välittää funktiolle
arvoparametrina, jos se voidaan välttää. (Mieti, mitä
arvonvälityksessä tapahtuu. Piirrä vaikkapa kuva.
Tai palauta mieleesi materiaalin kohta 3.4.)
Tehokkuussyistä kannattaa arvoparametrien sijaan käyttää const
-viitteitä:
void funktio3(const vector<double>& mittaustulokset);
Viitteen yhteydessä avainsana const
aiheuttaa sen, että viitteen
osoittamaa kohdetta ei voi muuttaa kyseisen viitteen kautta. Tämä
tekee viiteparametrin välittämisen turvallisemmaksi:
Funktion kutsuja tietää, että funktio ei tee muutoksia parametrina
välitettävään muuttujaan.
Jos halutaan, vektorin alkioiden määrä voidaan asettaa jo sen määrittelyvaiheessa:
// Vektori, jossa valmiiksi tilaa 20 kokonaisluvulle.
// Luvut itsessään alustamattomia.
vector<int> luvutA(20);
// Tilaa 10 reaaliluvulle, jokainen niistä
// alustettu arvoon 5.3.
vector<double> luvutB(10, 5.3);
Huomaa, että vektoreille (ja muillekin STL-rakenteille) on useita eri alustusnotaatioita. Esimerkiksi, mutta ei tyhjentävästi:
vector<string> nimetA(5);
vector<string> nimetB(10, "tuntematon);
vector<string> nimetC = { "Matti", "Maija" };
Yleisesti ottaen STL-vektori on tietorakenne, joka säilyttää alkiot siinä järjestyksessä, kuin ne on siihen talletettu. STL-terminologiassa tällaisia rakenteita kutsutaan sarjoiksi (sequence). STL:ssä on useita sarjoja:
- Vektori sopii hyvin käytettäväksi tilanteissa, joissa on kyettävä tallentamaan paljon tietoalkioita myöhempää prosessointia varten, varsinkin jos talletettuun tietoon ei tehdä hakuja, tai hakujen nopeudella ei ole erityisen suurta merkitystä. Luonnollisesti vektori on hyvä valinta myös tilanteessa, jossa alkiot on pystyttävä prosessoimaan samassa järjestyksessä kuin ne lisättiin säiliöön.
- Aiemmin mainittu
deque
on hyvin samankaltainen rakenne kuinvector
, mutta tehottomampi. Tosindeque
:n etu on, että sen alkuun voi lisätä alkioita valmiilla metodillapush_front
ja sen alusta voi poistaa alkioitapop_front
-metodilla. - Tämän lisäksi löytyy mm.
list
, jolla ei kuitenkaan ole mitään tekemistä Pythonin list-tietorakenteen kanssa, eikä sitä kannata käyttää, jos et ole perehtynyt siihen, miten dynaaminen muistinhallinta toimii.
Säiliöiden välisiin eroihin perehdytään kurssilla TIE-20100 Tietorakenteet ja algoritmit mm. tehokkuuden näkökulmasta.
Virheiden etsinnän helpottamiseksi¶
Kun ohjelmakoodissa käytetään STL:n säiliöitä, kannattaa Qt Creatorin projektitiedostoon lisätä seuraava rivi:
QMAKE_CXXFLAGS += -D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_PEDANTIC
Tällä tavoin kääntäjä antaa enemmän varoituksia STLn säiliöihin liittyvästä epäilyttävästä ohjelmakoodista (esim. ohi-indeksoinneista), mikä yleensä auttaa virheiden löytämisessä.
Kurssin henkilökunta on lisännyt tämän rivin projektitiedostoihin niissä tehtävissä, joissa käytetään STLn säiliöitä.
C++:n for
-silmukka¶
C++:n for
-silmukkaa voi käyttää pääpiirteissään kolmella eri
tavalla:
Säiliön alkioiden läpikäynti:
vector<int> lukuvektori; ... for ( int vektorin_alkio : lukuvektori ) { cout << vektorin_alkio << endl; }
Tämä käyttö on periaatteessa jo entuudestaan tuttu ja vastaa Pythonin rakennetta:
for alkio in lista: print(alkio)
Halutun kokonaislukuvälin arvojen läpikäynti:
for ( int luku = 5; luku < 10; ++luku ) { cout << 9 * luku << endl; }
joka on jonkin verran monisanaisempi vastine Python-rakenteelle:
for luku in range(5, 10): print(9 * luku)
Ikuisen silmukan toteuttaminen:
for ( ;; ) { ... }
joka vastaa toiminnaltaan täysin rakennetta
while ( true ) { ... }
Säiliön alkioiden läpikäynti¶
Tutkitaan nyt tarkemmin ensimmäistä tapausta for
-silmukan
käytöstä:
for ( int vektorin_alkio : lukuvektori ) {
cout << vektorin_alkio << endl;
}
Tässä vektorin_alkio
on ikään kuin “parametri”. Oletuksena
käytetään arvon välitystä ja vektorin_alkio
:n muuttaminen ei muuta
säiliöön tallennettua arvoa. Jos haluat muokata säiliön sisältämiä
alkioita, niin kannattaa kirjoittaa:
for ( int& vektorin_alkio : lukuvektori ) {
vektorin_alkio = vektorin_alkio * 2;
}
eli käyttää ikään kuin viitteenvälitystä.
Huomaa lisäksi, että tällaisen for
-silmukan rungossa ei saa lisätä
alkioita kyseiseen säiliöön tai poistaa niitä.
Tässä yhteydessä on myös hyödyllistä tutustua C++:n avainsanaan
auto
, joka päättelee tietotyypin. Esimerkiksi rivin:
int i = 42;
voisi kirjoittaa myös:
auto i = 42;
koska kääntäjä pystyy sijoitettavasta arvosta päättelemään, että
muuttujan tyyppi on int
.
Tosin tässä tapauksessa on parempi käyttää int
-sanaa, sillä se on
informatiivisempi ihmislukijalle, eikä auto
-sana edes lyhentäisi koodia.
Edellisen for
-silmukan voit siis ilmaista myös yksinkertaisemmin:
vector<int> lukuvektori;
...
for ( auto vektorin_alkio : lukuvektori ) {
cout << vektorin_alkio << endl;
}
Tämä on kätevää varsinkin, jos säiliön alkioiden tietotyyppi on monimutkaisempi.
Halutun kokonaislukuvälin arvojen läpikäynti¶
Toisesta käyttötapauksesta havaitaan, että C++:n for
-silmukka on
monimutkaisemman näköinen kuin Pythonin:
for ( int luku = 5; luku < 10; ++luku ) {
cout << 9 * luku << endl;
}
Sulkeiden sisällä on kolme puolipistein eroteltua osaa, joita voidaan nimittää esimerkiksi:
for ( alustus; ehto; kasvatus ) {
runko
}
Näistä alustus suoritetaan yhden kerran, eli vain silmukkaan tultaessa. Ehdon arvo tarkastetaan aina ennen silmukan rungon suorittamista. Sen perusteella päätetään, suoritetaanko runko vielä. Kasvatus suoritetaan aina viimeisenä rungon suorittamisen jälkeen.
Nice to know: Jos haluat käydä läpi jonkin lukujonon, voit kirjoittaa
for( int i : { 2, 3, 5, 7, 11, 13, 17 } ) {
...
}
missä { 2, 3, 5, 7, 11, 13, 17 }
on alustuslista.