Kirjoittaja: Metabolix (2009).
Opassarjan alussa esitellyillä perustietotyypeillä ei tosielämässä pitkälle pötkitä – tai ainakin pötkiminen on todella vaivalloista. Koodin selkeyttä ja siten myös ohjelmoinnin nopeutta parantaa omien tietotyyppien ja taulukoiden käyttö. Niiden avulla rakenteista saadaan monin paikoin paremmin tarkoitukseensa sopivia.
C++ tarjoaa mahdollisuuden antaa tyypeille uusia nimiä. Jos esimerkiksi int
ei miellytä, sille voi antaa uuden nimen kokonaisluku
. Uuden nimen esittely vastaa muuten muuttujan määrittelyä, mutta rivi alkaa sanalla typedef
:
typedef int kokonaisluku; typedef unsigned short int pieni_luku;
Tyypin esittelyn jälkeen se toimii vanhan nimen synonyyminä:
pieni_luku x; unsigned short int y; // x ja y ovat samaa tyyppiä.
Myös mutkikkaampia tyyppejä voi nimetä aivan samalla tavalla, ja näiden kanssa todellinen hyöty vasta tuleekin esille. Myöhemmin käsiteltävät funktio-osoittimet ja mallit ovat tästä oivia esimerkkejä, vaikkei niitä vielä tarvitsekaan ymmärtää:
// Funktiotyypin typedef-rivi vastaa funktion esittelyä: typedef float kahden_luvun_funktio(float x, float y); kahden_luvun_funktio* funktio_osoitin; // C++:n standardikirjaston luokkamalleja yhdistelemällä syntyy ihmeitä: typedef std::string aine; typedef unsigned short int arvosana; typedef std::string nimi; typedef std::map<aine, arvosana> oppilaan_arvosanat; typedef std::map<nimi, oppilaan_arvosanat> oppilaiden_arvosanat; oppilaiden_arvosanat data; // Ilman typedef-rivejä tästä tulisi pitkä: // std::map<std::string, std::map<std::string, unsigned short int> > data;
Yksinkertaisimpia itse määriteltävistä tietotyypeistä ovat luetellut tyypit (engl. enumeration). Tällaista tyyppiä määritellessään ohjelmoija ilmoittaa, minkä nimiset arvot tyyppiin kuuluvat. Esimerkiksi pelikorttien maat voisivat muodostaa luetellun tyypin, jossa arvot olisivat nimiltään ruutu, risti, hertta ja pata.
Jokaiseen luetellun tyypin arvoon liittyy myös lukuarvo; arvojen nimet ovat vain ohjelmoijaa varten ja ne korvataan käännösvaiheessa vastaavilla lukuarvoilla. Normaalisti lukuarvot alkavat nollasta, mutta tarvittaessa ne voi myös asettaa itse. Tyyppi määritellään sanalla enum
, jota seuraa tyypin nimi ja aaltosuluissa arvojen nimet sekä haluttaessa myös tarkat lukuarvot:
enum kortin_maa { maa_ruutu, // 0 maa_risti, // 1 maa_hertta, // 2 maa_pata // 3 }; enum kortin_arvo { kortti_2 = 2, // Lukuarvot jatkuvat järjestyksessä edellisestä annetusta, // joten loppuja ei tarvitse antaa. kortti_3, kortti_4, kortti_5, kortti_6, // 3, 4, 5, 6 jne. kortti_7, kortti_8, kortti_9, kortti_10, kortti_jatka, kortti_rouva, kortti_kuningas, kortti_assa, // Uusiakin arvoja voi silti kirjoittaa: kortti_jokeri = 1337 };
Tyypin määrittelyn jälkeen voi määritellä tämän tyypin muuttujia ja antaa niille nimettyjä arvoja:
kortin_maa m1 = maa_pata; kortin_arvo k1 = kortti_rouva; // Seuraavat rivit eivät toimisi, koska arvot eivät kuulu oikeaan tyyppiin: // kortin_maa m2 = 2; // kortin_maa m3 = kortti_rouva;
Tarvittaessa arvot muuttuvat myös koodissa kokonaisluvuiksi:
int i = kortti_rouva, j = kortti_jokeri; // i = 12, koska kortti_rouva on 12. // j = 1337, koska kortti_jokeri on 1337.
Kun johonkin asiaan liittyy monta eri arvoa, niistä voidaan koota tietueita. Esimerkiksi pisteellä on x- ja y-koordinaatit ja tiellä leveys, pituus ja ehkä myös asfalttipäällyste. Nämä ovat tietueiden jäseniä.
Tietueen määrittely alkaa sanasta struct
(engl. structure, rakenne). Tätä seuraa tälle tietotyypille annettava nimi. Aaltosulkeiden väliin kirjoitetaan tietueeseen kuuluvien muuttujien esittelyt tuttuun tapaan. Loppuun tarvitaan vielä puolipiste. Äsken mainituista esimerkkitapauksista voisi tehdä seuraavanlaiset tietueet:
struct piste { double x, y; }; struct tie { float pituus; float leveys; bool asfaltoitu; };
Tietueen nimi toimii nyt aivan muiden tietotyyppien tavoin, ja sen avulla voi määritellä omia tietuemuuttujia eli objekteja. Jäsenten alkuarvot annetaan aaltosuluissa siinä järjestyksessä, kuin ne on tietueen määrittelyssä esitelty.
// origo: (x, y) = (0, 0) piste origo = {0, 0}; // kaksi toistaiseksi määrittelemätöntä pistettä: piste a, b; // tie: {pituus, leveys, asfaltoitu} tie silakkapolku = {56.5, 2.8, false};
Myöhemmin jäseniin pääsee käsiksi operaattorilla .
(piste):
// Levennetään tietä ja asfaltoidaan se: silakkapolku.leveys += 0.5; silakkapolku.asfaltoitu = true;
Tietueet voivat olla yksinkertaisia mutta silti oikeissa ohjelmissa erittäin tärkeitä koodin selkeyden kannalta. Myöhemmin olio-ohjelmoinnin yhteydessä tietueisiin lisätään muuttujien lisäksi vielä funktioita, jolloin tietueet muuttuvat luokiksi ja objektit olioiksi.
Tietueen jäsenestäkin voi tehdä const
-määreellä vakion, jolloin sen arvoa ei voi muuttaa, vaan se säilyttää aina aaltosuluissa annetun alkuarvonsa. Määreellä mutable
saadaan aikaan päinvastainen tulos: tällaista jäsentä voi aina muokata, vaikka koko tietue olisi määritelty vakioksi.
#include <iostream> struct tietue { // Tavallinen jäsen. int jasen; // Vakiojäsen; tätä ei voi koskaan muuttaa. const int vakio; // Tätä taas voi muuttaa aina, vaikka tietue muuten olisi vakio. mutable int muuttuja; }; int main() { // Tietuemuuttuja. Vain const-jäsenen muokkaaminen on estetty. tietue a = {1, 2, 3}; a.jasen = 44; // a.vakio = 55; // VIRHE! Tämä jäsen on vakio. a.muuttuja = 66; // Tietuevakio. Vain mutable-jäsenen muokkaaminen on mahdollista. const tietue b = {1, 2, 3}; // b.jasen = 44; // VIRHE! Koko tietue on vakio. // b.vakio = 55; // TUPLAVIRHE! Vakiotietueen vakiojäsen. b.muuttuja = 66; // OK! Tämä jäsen on merkitty muutettavaksi. std::cout << a.jasen << ", " << a.vakio << ", " << a.muuttuja << std::endl; std::cout << b.jasen << ", " << b.vakio << ", " << b.muuttuja << std::endl; }
Yhdistelmät (engl. union) ovat tyyppejä, joissa on tietueen tapaan monta jäsentä. Niissä kuitenkin jäsenet tallennetaan muistissa päällekkäin, joten vain yhtä näistä voi käyttää kerralla.
Funktioiden ja tietueiden sisällä yhdistelmämuuttujan nimen voi jättää pois, jolloin sen sisältämät jäsenet näkyvät suoraan muuttujina tai tietueen jäseninä.
Yhdistelmätyyppiin voitaisiin tallentaa vaikkapa kokonaisluku ja liukuluku. Väärän arvon tulostaminen tuottaa omituista dataa. Yhdistelmässä itsessään ei ole tietoa, mihin jäseneen arvo on sijoitettu, joten ohjelmoijan täytyy yleensä säilyttää tätä tietoa itse vaikka erillisessä muuttujassa.
#include <iostream> union kokoliuku { short koko; float liuku; }; int main() { kokoliuku x; // Tulostetaan koot; näistä nähdään, että tilaa ei ole molemmille. std::cout << "sizeof(x): " << sizeof(x) << std::endl; std::cout << "sizeof(x.koko): " << sizeof(x.koko) << std::endl; std::cout << "sizeof(x.liuku): " << sizeof(x.liuku) << std::endl; // Sijoitetaan jäseniin arvot ja tulostetaan. // Kokonaisluku sijoitetaan myöhemmin, joten liukuluku ei säily. // Kuitenkin short on yleensä pienempi kuin float, joten // vain osa liukuluvun datasta muuttuu. x.liuku = 1; std::cout << "sij. x.liuku = " << x.liuku << ";" << std::endl; x.koko = 1234; std::cout << "sij. x.koko = " << x.koko << ";" << std::endl; std::cout << "nyt x.liuku == " << x.liuku << "!" << std::endl; x.liuku = 3.14159; std::cout << "sij. x.liuku = " << x.liuku << ";" << std::endl; std::cout << "nyt x.koko == " << x.koko << "!" << std::endl; // Määritellään nimetön yhdistelmä. Sen jäseniä käytetään kuin // tavallisia muuttujia. Niiden muistipaikka on silti sama. union { int i; float f; }; // Sijoitetaan arvo ensin kokonaislukuun ja sitten liukulukuun. // Tulostuksesta nähdään taas, että kokonaisluvun arvo ei säily. i = 1234; std::cout << "sij. i = " << i << ";" << std::endl; f = 1.41; std::cout << "sij. f = " << f << ";" << std::endl; std::cout << "nyt i == " << i << "!" << std::endl; }
Yksi tyypillinen esimerkki yhdistelmien käytöstä on monen grafiikkakirjaston kuten SDL:n tapa kuljettaa käyttäjän toimintoihin liittyviä viestejä: ensin määritellään tietue kutakin erilaista tapahtumaa varten, sitten yhdistetään nämä yhteen yhdistelmätyyppiin. Tällaisessa käytössä kunkin tapahtumatietueen alussa täytyy olla samanlainen jäsenmuuttuja, joka kertoo, mistä tyypistä on kyse.
Jos samanlaisia muuttujia tarvitaan paljon, ne on usein kätevä määritellä taulukkona. Taulukon määrittely alkaa normaalien muuttujien tapaan tietotyypillä ja nimellä. Tämän jälkeen ilmoitetaan hakasuluissa, kuinka monta jäsentä eli alkiota taulukko sisältää. C++:n taulukko ei siis ole itsenäinen muuttuja, vaan se on vain tapa määritellä kerralla useita muuttujia:
// Määritellään kolme muuttujaa erikseen: int muuttuja0, muuttuja1, muuttuja2; // Määritellään kolme int-alkiota sisältävä taulukko: int taulukko[3];
Taulukon alkiot toimivat tavallisten muuttujien tapaan. Yksittäiseen alkioon viitataan kirjoittamalla taulukon nimi ja sen perään hakasulkuihin käsiteltävän alkion indeksi. Taulukon indeksit alkavat nollasta, eli yllä määritellyssä kolmen alkion taulukossa indeksit ovat 0, 1 ja 2:
taulukko[0] = 10; taulukko[1] = taulukko[0] / 2; taulukko[2] = 5 * (taulukko[0] + 7 * taulukko[1]);
Täysin alustamaton taulukko voi sisältää mitä tahansa, samoin kuin muutkin alustamattomat muuttujat. Taulukon alkioiden alkuarvot voi luetella aaltosuluissa, kuten jo aiemmin tietueiden yhteydessä tehtiin. Jos alustuksessa annetaan liian vähän arvoja, loput taulukosta täytetään nollilla.
int satunnaista[13]; int nollia[7] = {0, 0, 0, 0, 0, 0, 0}; int nollia_helpommin[7] = {0}; int yksi_kaksi_kolme_nolla[4] = {1, 2, 3};
Jos taulukon koko selviää alkuarvojen määrästä, sen voi jättää kertomatta:
int luvut[] = {0, 1, 1, 2, 3, 5, 8}; // int luvut[7] = {0, 1, 1, 2, 3, 5, 8};
Taulukon etu irrallisiin muuttujiin nähden on, että indeksinä voi käyttää mitä tahansa kokonaislukua, olipa se sitten vakio, muuttuja tai laskulauseke. Niinpä taulukkoa on helppo käsitellä esimerkiksi for-silmukalla tai käyttäjän valintojen mukaan.
#include <iostream> int main() { // Fibonaccin lukujonossa seuraava luku on aina kahden edellisen summa. // Lukujono alkaa 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ... // Lasketaan Fibonaccin lukujonon 27 ensimmäistä lukua. const int maara = 27; // Luodaan taulukko lukuja varten ja asetetaan siihen ensimmäiset arvot. int fibonacci[maara] = {0, 1}; // Käydään silmukassa läpi taulukon loput kohdat; viimeinen indeksi // on maara - 1, eli silmukan ehtona on, että i < maara. for (int i = 2; i < maara; ++i) { // Sijoitetaan muuttujan i ilmoittamaan kohtaan taulukossa uusi // luku, joka Fibonaccin lukujonossa on kahden edeltävän summa. fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2]; } // Käydään taulukko läpi alusta (i = 0) alkaen ja tulostetaan luvut. for (int i = 0; i < maara; ++i) { std::cout << i << ". luku on " << fibonacci[i] << "." << std::endl; } }
Taulukon alkiona voi olla taulukko, jolloin syntyy tavallaan moniulotteinen taulukko. Seuraava taulukko sisältää kolme taulukkoa, joista kukin sisältää viisi lukua:
int taulu2d[3][5] = { { 0, 1, 2, 3, 4}, {10, 11, 12, 13, 14}, {20, 21, 22, 23, 24} }; int taulu2d_nollattuna[2][3] = {{0}}; // Kahdet aaltosulut! std::cout << taulu2d[1][4] << std::endl; // 14
Taulukko voi sisältää myös tietueita, tai tietueen jäsenenä voi olla taulukko.
#include <iostream> // Kolmiluku sisältää kolme osaa ja niiden summan. struct kolmiluku { int osat[3]; int summa; }; int main() { // Tässä taulukossa on kaksi kolmilukua. kolmiluku luvut[2] = { // Kolmiluku sisältää kaksi jäsentä, joista ensimmäinen taulukko. // Alustuksessa on siis ensin taulukon alustus {1, 2, 3} ja sitten // summan alustus 6. {{1, 2, 3}, 6}, {{4, 5, 6}, 15} }; // Tulostetaan jälkimmäisen luvun osat: std::cout << luvut[1].osat[0] << " + " << luvut[1].osat[1] << " + " << luvut[1].osat[2] << " = " << luvut[1].summa << std::endl; // Tarkistetaan vielä summa: if (luvut[1].summa == luvut[1].osat[0] + luvut[1].osat[1] + luvut[1].osat[2]) { std::cout << "Oikein meni." << std::endl; } else { std::cout << "Väärin, hyi!" << std::endl; } }
Koska taulukko on oikeasti vain joukko numeroituja muuttujia, sitä ei voi kerralla sijoittaa toiseen taulukkoon, vaan arvot täytyy kopioida vaikkapa for-silmukalla. Taulukon kokoakaan ei voi muuttaa jälkikäteen, minkä vuoksi se ei sovellu kaikkiin tarkoituksiin. Joustavampia ratkaisuja esitellään myöhemmin.
C-kielen tapa tekstin käsittelyyn on tallentaa se merkkitaulukkoon. Tällöin tekstin pituus on rajoitettu taulukon kokoon ja taulukon loppuun täytyy vielä mahtua yksi ylimääräinen merkki, josta tunnistetaan, mihin kohti teksti todella loppuu. Lopetusmerkin lukuarvo on 0. (Tämä on eri asia kuin merkki '0', jonka arvo ASCII-järjestelmässä on 48!) Tällaista tekstiä voi käyttää vaikkapa näin:
#include <iostream> int main() { char etunimi[16]; std::cout << "Kuka olet?" << std::endl; std::cin >> etunimi; std::cout << "Hei, " << etunimi << "!" << std::endl; }
Tämä tapa on kuitenkin vaarallinen, koska esimerkiksi tässä esitetyn ohjelman saa kaadettua syöttämällä liikaa merkkejä ilman välilyöntejä. (Luultavasti varsinainen kaatuminen tapahtuu vasta satojen merkkien jälkeen, mutta muita virheitä voi ilmaantua jo yhden merkin ylityksestä. Kokeile!) Tällaisen tekstin käsittelyyn tarvitaan myös mutkikkaita funktioita. Siksipä C++:n standardikirjasto tarjoaakin tekstiä varten apuluokan string
, josta kerrotaan lyhyesti seuraavaksi.
C++:n standardikirjasto sisältää luokan nimeltä string
. Sen käyttöön paneudutaan tarkemmin opassarjan myöhemmissä osissa. Tässä vaiheessa on kuitenkin hyvä käydä läpi muutama yksinkertainen esimerkki ilman sen tarkempaa pohdiskelua.
Tärkeimmät string
-olioiden ominaisuudet ovat automaattisesti tarpeen mukaan muuttuva koko, yhdistely +
-operaattorilla sekä tulostus ja luku tuttuun tapaan. Lisäksi kokonaisen tekstirivin voi lukea getline
-funktiolla. Jos string
-olion sisältämä teksti pitää välittää C-kielellä tehdylle kirjastolle, C-tyylisen merkkijonon voi hakea string
-olion c_str
-jäsenfunktiolla.
// Tarvitaan otsikkotiedosto, jossa string-luokka esitellään. #include <string> #include <iostream> int main() { // Määritellään string-olioita. std::string etunimi, sukunimi, koko_nimi; std::cout << "Kerropa etu- ja sukunimesi nimesi." << std::endl << "> "; // Luetaan kokonainen syöterivi. std::getline(std::cin, koko_nimi); // Tulostetaan nimi. std::cout << "Terve, " << koko_nimi << "!" << std::endl; std::cout << "Kirjoita nyt etunimesi uudestaan." << std::endl << "> "; // Luetaan sana. std::cin >> etunimi; // Tulostetaan nimen pituus. std::cout << "Kas, etunimesi on " << etunimi.length() << " char-muuttujaa pitkä." << std::endl << "Kirjoita vielä sukunimesi." << std::endl << "> "; // Luetaan toinen sana. std::cin >> sukunimi; // Tekstejä ja string-olioita voi yhdistellä +:lla ja vertailla ==:lla. // Jokaisen operaation (+ tai ==) toisen puolen täytyy kuitenkin olla // string-olio! Toisin sanoen "A" + "B" == "AB" sisältää kaksi virhettä: // ensin yritetään laskea yhteen kahta tekstiä, jotka eivät ole // string-olioita, ja tätä (mahdotonta) lauseketta yritetään vertailla // kolmanteen, joka ei myöskään ole string. Sen sijaan string-olioilla // tämä onnistuu: if (koko_nimi == (etunimi + " " + sukunimi)) { std::cout << "Hienoa, osasit noudattaa ohjeita!" << std::endl; // C-tyylisen merkkijonon eli char-taulukon saa haettua // jäsenfunktiolla c_str. Tämä on tarpeen esimerkiksi monien // C:llä tehtyjen lisäkirjastojen kanssa toimittaessa. std::cout << "Sukunimesi on myös C-kielisenä " << sukunimi.c_str() << '.' << std::endl; } else { std::cout << "Nyt kirjoitit jotain hassusti." << std::endl << '"' << koko_nimi << '"' << " on eri kuin " << '"' << etunimi << " " << sukunimi << '"' << std::endl; } }
Tähän asti oppaassa on neuvottu laittamaan tyyppimäärittelyjen loppuun aina puolipiste. On kuitenkin mahdollista määritellä myös muuttujia samalla kertaa. Seuraavat koodit toimivat täsmälleen samalla tavalla:
struct T { int a, b, c; }; T x, y, z;
struct T { int a, b, c; } x, y, z;
Oppaassa on myös annettu ymmärtää, että tyypillä on aina oltava nimi. Joissain tilanteissa on kuitenkin mahdollista käyttää myös nimettömiä tyyppejä. Tämä ei ole juuri koskaan tarpeen, mutta esimerkiksi näin sitä toisinaan käytetään:
struct palikka { // Tyyppi toisen tyypin sisällä voi auttaa jäsenten ryhmittelyssä. // Seuraavalla tietueella ei itsessään ole nimeä. struct { int x, y; } paikka, nopeus; }; void f() { palikka p; p.paikka.x = 3; p.paikka.y = 5; p.nopeus.x = 7; p.nopeus.y = 11; }
Tämä on hyödyllinen opas! Oon koodannut C++:lla melkein kaksi vuotta, mutta tämä osa sai minut tuntemaan, että en osaa mitään!
Pisteet ja kunnoitus Metabolixille o/
Noniin nyt luonnistuu multaki toi stringien käyttö... Tästä oli paljon hyötyä elikkäs kiitos tästä Metalbolix!
Kiitos!
Huomasin pienen virheen kohdassa:
int i = kortti_rouva, j = kortti_assa; // i = 12, koska kortti_rouva on 12. // j = 1337, koska kortti_assa on 1337.
Jokeri on 1337 eikä ässä.
osku91, tarkka havainto! Korjasin.
Huomio! Kommentoi tässä ainoastaan tämän oppaan hyviä ja huonoja puolia. Älä kirjoita muita kysymyksiä tähän. Jos koodisi ei toimi tai tarvitset muuten vain apua ohjelmoinnissa, lähetä viesti keskusteluun.