Rakenteet

Ehtolauseet

Taas lisää kaikkea hämmentävää, nimittäin ehtolauseita. Yksi ohjelmien tärkeä ominaisuus on reagoida eri tilanteisiin eri tavoin ja tämä hoituu ehtolauseilla. Ehtolause rakentuu näin:

if (ehto) tee_jotain;

Esimerkiksi näin:

if (ika == 5) cout << "Voi pikkuista.."; 

Ehdossa voidaan käyttää näitä vertailuoperaattoreita:

== yhtäsuuri kuin
!= erisuuri kuin
> suurempi kuin
< pienempi kuin
>= suurempi tai yhtäsuuri kuin
<= pienempi tai yhtäsuuri kuin

Nyt viritä päänsisäiset vastaanottimesi oikealle kanavalle (onhan tv-lupa maksettu?), tulee hyvin tärkeä asia. Yhtäsuuruusoperaattorissa on kaksi yhtäsuuruusmerkkiä. Siis kaksi, yhtä monta kuin polkupyörässä on renkaita. Yksipyöräisellä kikkailevat sirkuspellet koodaakin sitten Pascalilla. C++-kääntäjä natisee melkein aina kun vastuuttomuuttasi töpelehdit ohjelmoidessa, mutta tässä tapauksessa kääntäjä on hiljaa. Yhtäsuuruuden korvaaminen sijoituksella, siis toisen = merkin unohtaminen, on C++-kieliopin mukainen temppu. Sijoituksen arvo on itse sijoitettu arvo, joten ehto on tosi aina kun sijoitetaan joku muu arvo kuin nolla, kuten tulet kohta huomaamaan. Tämä on erittäin vaikea virhe aloittelijan huomata. Opi se nyt, tai kantapään kautta - kuten Akilles teki.

Muutamia esimerkkejä vertailemisesta:

if (aika < 10) cout << "Aikaa ei ole paljon.";
if (ika >= 18) cout << "Olet kahdeksantoista tai yli.";
if (ika > 17) cout << "Olet siis täysi-ikäinen."; // sama kuin edellinen (kokonaisluvuilla)
if (robottiID != 2373) cout << "Et ole se betazoidi jota etsin.";

Entä jos halutaan etsiä semmoista parikymppistä, mutta ei kuitenkaan yli kolmekymppistä neitoa? (etsinnän tarkoituksen jätän jokaisen oman mielikuvituksen varaan)

Tietenkin, onhan toki mahdollista tehdä näin:

if (ika >= 18)
  if (ika < 30) cout <<"Käytkö täällä usein?";

Mutta mutta... Voihan sitä ajaa pyörälläkin ilman satulaa, mutta kovin mukavaa se ei ole. Niinpä C++ tarjoaakin meille mahdollisuuden yhdistellä vertailuja näpsäkillä loogisilla operaattoreilla, jotka ovat:

&& JA
|| TAI
! EI

Nyt voimme kirjoittaa edellisen lauseen muotoon:

if (ika >= 18 && ika < 30) cout << "Pidän silmiesi väristä.";

Lisää valaisevia esimerkkitapauksia:

if (a == 5 || b < 3) cout << "Olette syyllinen, koska a on viisi tai b on pienempi kuin kolme.";
if (x == 3 || x == 2) cout << "Nyt selvisi. x on kolme tai kaksi, tai molemmat.(teoriassa..)";

if (tupakointiStatus == 1 && ika > 50) cout << "Aijai. Kuulutte keuhkosyövän riskiryhmään.";

Vielä vähän tuosta viimeisestä Sosiaali- ja terveysministeriön suosittelemasta esimerkistä. tupakointiStatus kuvaa henkilön tupakoimista. Se on nolla, jos henkilö ei polta ja 1 jos hän polttaa. Miksi näin? Siksi näin, että tämä on standardi C++-kielessä. Kun lause on tosi, sen arvo on nollasta poikkeva. Kun taas epätosi, se on nolla. Lauseen 5==5 arvo on aina tosi, eli nollasta poikkeava.

Tätä voi kokeilla näin:

cout << (5 == 5); // tulos pitäisi olla 1
cout << (4 == 5); // tulos pitäisi olla 0

Mitäs sitten? Sitäs sitten, että ehdossa ei aina tarvitse olla operaattoreita, vaan pelkkä arvo riittää. Voidaan esim. tehdä näin:

int arvo = 1;
if (arvo) cout << "Arvo on nollasta poikkeava.";

Tällä tavalla voimme edistää kansaterveyttä hieman yksinkertaisemmin, eli lause muuntuu muotoon:

if (tupakointiStatus && ika > 50) cout << "Aijai. Kuulutte keuhkosyövän riskiryhmään.";

Täysin vastaavia lauseet eivät ole: jälkimmäisessa tupakointiStatus saa olla mikä tahansa ei-nolla, mutta edellisessä se sai olla vain yksi. Nyt voimmekin käyttää tupakointiStatusta ilmoittamaan, montako sikaaria henkilö tupruttaa päivässä. Jo yksi tupakka päivässä kuitenkin riittää viemään hänet riskiryhmään.

Vielä pikkuisen tuosta !-merkistä, nimittäin EI-operaattorista. Se kääntää jonkun lausekeen arvon toisin päin, tällä tavalla:

if (!onMuusikko) cout << "Et ole muusikko.";
if (!(ika < 50)) cout << "Ikäsi ei ole pienempi kuin 50.";
if (ika > 50) cout << "Ikäsi on suurempi kuin 50."; // sama kuin edellinen, paitsi
                                                    // että 50 ei lasketa mukaan
if (ika >= 50) cout << "Ikäsi on suurempi tai yhtäsuuri kuin 50."; // täysin sama
                                                                   // kuin ensimmäinen

 

Jos ei niin sitten..

Monesti, jos if lause ei ole tosi, on tarve reagoida jollain muulla tavalla. Niinpä if-käskyyn voidaankin lisätä osa else, eli jos ei niin sitten..

if (numero == 123) cout << "Aha, olet siis osa numero 123";
else cout << "Olet joku muu osa";

iffin ja elsen jälkeen suoritettavaksi voidaan myös laittaa lohko eli { ja } -merkkien rajaama alue tähän tyyliin:

if (ika < 18)
{
  cout << "Ei taida ikä riittää...";
  PotkaiseUlos(); // Funktiokutsu, TSM
}

else
{
  cout << "Tervetuloa!";
  PotkaiseSisaan();
}

Näitä pikku pirulaisia voi sitten yhdistellä aivan mielin määrin, kuten esimerkissä:

if (ika > 65)
{
  if (onSotaveteraani) cout << "Olet sotaveteraani.";
  else cout << "Olet muu eläkeläinen.";
}

else
{
 if (ika < 35)
  {
    if (osaaCpp) cout << "Lupaava alku..";
    else cout << "Et ole ajatellut tutustua C++-oppaaseen?";
  }

  else cout << "Sitä suurta ikäluokkaa, vai?";
}

 

Toistorakenteet: while ja for

int taulukko[2][3] = {1, 2, 3, 4, 5, 6};

cout << taulukko[0][0] << " " << taulukko[0][1] << " " << taulukko[0][2] << endl;
cout << taulukko[1][0] << " " << taulukko[1][1] << " " << taulukko[1][2] << endl;

Aloitamme esimerkillä, jota vilkuilimme taulukkoja käsittelevässä kappaleessa. Taulukkojen käsittely käsipelillä voi olla melko raskasta. Jo kyseisen 2x3-taulukon tulostamiseen saa kirjoittaa merkin jos toisenkin. Ja entä kun kyseessä on 640x480x3-taulukko, joka voisi olla vaikka modernin grafiikkajärjestelmän näyttömuisti? Eihän siitä mitään tule, kun käsin pitää kaikki tehdä, vaikka automaattista tietokonetta rämpätään. Tuupataan toistorakentein tietokone töihin:

#include <iostream.h>

void main()
{
        int taulukko[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
                           
        int a = 0;
        while (a < 10)
        {
            cout << taulukko[a] << " ";
            a = a + 1;
        }
}

while-suorittaa alla olevassa lohkossa olevaa koodia, kunnes suluissa oleva lauseke on epätosi. Eli tällainen lause:

while(1)
{
	cout << "Juuhuu";
}

...tulostaa "Juuhuu"-tekstin MONTA kertaa.(niin kauan kuin sähköä riittää).

Toinen vaihtoehto toistoon on for:

int a;
for (a = 0; a < 10; a = a + 1) { .. }

Ensimmäisessä forin osassa on muuttujan alustus, toisessa ehto ja kolmannessa muuttujan päivittäminen. Muuttujan luonnin voi yhdistää foriin näin kätevästi:

for (int a = 0; a < 10; a++) // a++ on sama kuin a = a + 1

 

Rakenteiden yhdistely ja do..while

Joskus silmukan ehto ei ole pelkkä yksinkertainen lauseke, jonka voisi mahduttaa whilen ja forin ehdoksi. Silloin yksi vaihtoehto on laittaa se whilen rungon sisälle ja käyttää jotain muuttujaa ehdossa, kuten ehtoTosi, jolla seurataan milloin silmukan voi lopettaa. ehtoTosin arvo saadaan jollain monimutkaisellakin tavalla silmukan sisällä. Ongelma on kuitenkin, että ehtoTosilla ei ole mitään järkevää arvoa kun silmukkaa ei vielä ole suoritettu - ja toisaalta ehtoTosin arvo pitäisi tietää, että ohjelma voi päättää alkaako suorittamaan silmukkaa ollenkaan. Tämän taas voi ratkaista kopioimalla ehdon silmukan sisältä myös silmukan edelle, jotta ehtoTosi saa järkevän arvon jo ennen silmukkaa. Kopiointi on kuitenkin hirveän vaarallista. Se kasvattaa ohjelmaa ihan turhaan ja tekee siitä mahdottoman kehitettävän. Kyseessähän on automaattinen tietokone, joten parempiakin vaihtoehtoja on.

Jos haluamme luoda silmukan, joka suoritetaan ainakin kerran, oli sitten tilanne mikä tahansa, niin avainsana on do..while. Kielioppi on:

do {
	// silmukan sisältö
} 
while (ehto); // huomaa puolipiste lopussa

Siis itse silmukka tulee don ja whilen väliin. Normaalin whilen perään ei saa laittaa puolipistettä, ellei nyt välttämättä halua tehdä tyhjää silmukkaa, mutta nyt whilen loppuun tulee puolipiste. Sisältö suoritetaan aina vähintään kerran.

Silmukoita ja ehtoja voi yhdistellä mielin määrin. Jos haluaisimme pyytää käyttäjältä 20 int-lukua, jotka ovat kaikki erilaisia, voisi sen tehdä esimerkiksi niin kuin olen alas koodaillut. Idea siis on, että aina kun käyttäjä antaa uuden luvun, niin tarkistamme ettei jo annetuissa luvuissa vielä esiintynyt kyseistä lukua:

int luvut[20];
int lukuja = 0;

while (lukuja < 20)
{
	int uusiLuku;
	int onVanha;
	
	do // pitää kysyä ainakin kerran 
	{
		cout << endl << "Anna " << lukuja << ". luku ";
		cin >> uusiLuku;
		
		onVanha = 0; 
		for (int a = 0; a < lukuja; a++) if (luvut[a] == uusiLuku) onVanha = 1; // löytyi edellisistä

	} while (onVanha); // tivataan kunnes saadaan ihan uusi luku
	
	luvut[lukuja] = uusiLuku; // lisätään täysin uusi luku
	lukuja++;
}

 

break ja continue

for ja while-silmukoista pääsee pois myös break-käskyllä. Itse tapasin ajatella, että break on merkki huonosti suunnitellusta silmukasta. Ja sitten kehittelin kaikenlaisia purkkaviritelmiä millä pääsin eroon sen käytöstä. Mutta loppujen lopuksi, mitä sitä väistää väistämätöntä, break on joskus ihan helv... erinomaisen kätevä. Eli kaikessa rumuudessaan ja kätevyydessään estradille astuu: break, ja kitaralla säestää continue.

Pähkinänkuoreen puristettuna: break lopettaa silmukan suorituksen siihen paikkaan. Eli jos esimerkiksi jonkin erityisen tapauksen (muisti loppuu tms..) vuoksi joudutaan lopettamaan silmukka kesken tai jos silmukan toistoehto on osa itse silmukan sisältöä, kannattaa breakia käyttää. continue, kuten nimestä voi päätellä, jatkaa silmukkaa. Se siis hyppää silmukaan alkuun, jättäen silmukan lopun ohjelmakoodin suorittamatta, mutta jatkaen kuitenkin silmukan suoritusta.

#include<iostream.h>

void main()
{
  int luku;

  while (1) // päättymätön silmukka
  {
    cout << endl << "Arvaa luku: ";
    cin >> luku;

    if (luku == 5)
    {
        cout << endl<< "OIKEIN!";
        break;
    }

    if (luku == 0)
    {
    	cout << endl << "Ei nolla ole mikään kunnon luku, uudelleen..";
	    continue;
    }

    cout << endl<< "Väärin, arvaa uudelleen..";
  }
}

Tämäkö muka teennäinen esimerkki...

 

switch..case -rakenne

On muuttuja, jolla voi olla kymmenen eri arvoa. Se voisi olla vaikka muuttuja, joka kertoo mitä käyttäjä on valinnut jostain valikosta. Kun meidän pitää valinnan mukaan päättää mitä teemme, tulee jo opittuja konsteja käyttäen ongelmia. Jos käytämme normaalia if-rakennetta, saamme aikaan melkoisen if-hässäkän, josta ei sitten enää ota pirukaan selvää. Mutta hädän hetkellä apuun ryntää tiukan punaisissa trikoissaan switch..case-rakenne!

switch (muuttuja)
{
  case 0: cout << "Arvo on nolla"; break;
  case 1: cout << "Arvo on yksi"; break;
  case 2: cout << "Arvo on kaksi"; break;
  default: cout << "Arvo on joku muu";
}

Ideana on tämä: muuttuja otetaan tässä vivuksi (switch englanniksi). switchia seuraavassa lohkossa hypätään siihen kohtaan, joka vastaa muuttujan arvoa. Jos vastaava arvoa ei löydy, mutta default -kohta löytyy, hypätään siihen. Paino sanalla hypätään. Suoritus jatkuu nimittäin siitä kohdasta aivan loppuun asti. Niinpä pitääkin jokaisen vaihtoehdon käsittelyn jälkeen laittaa break-käskyt, jos halutaan käsitellä vain ja ainoastaan siihen vaihtoehtoon liittyvä ohjelmakoodi.

Aina ei kuitenkaan näin ole. Otetaan esimerkki käytännön maailmasta. Teollisuusvartiointipalvelu Pamppu ja sateenvarjo oy laajenee lamakauden jälkeen ja palkkaa 150 uutta ja verestä vartijaa. Palkat on ennen laskettu paristokäyttöisellä parin kympin taskulaskimella. Nyt tarvitaan kuitenkin jotain vähän järeämpää. Sinut on palkattu suunnittelemaan ohjelma, joka laskee jokaisen vartijan palkan. Peruspalkka on 7000 mk/kk, vaarallisen työn lisä on 500 mk/kk ja yötyölisä 1000 mk/kk. Koska alue on hyvin levotonta ja kaikenlaiset perverssit kukkuvat siellä yöaikaan, tulee yötyön tekijälle myös vaarallisen työn lisä automaattisesti. Vaarallisen työn tekijälle ei kuitenkaan aina tule yötyölisää; paikallisen ammustehtaan vartiointi on melko vaarallista hommaa päiväsaikaankin. Kaikille tulee tietenkin peruspalkka.

Olet saanut ohjelman jo muuten kasaan, mutta juuri se kohta, joka lisää palkan, sinulta vielä puuttuu. Otat yhteyttä paikalliseen C++-guruun ja kun pari kokispulloa ja jokunen puoliksi syöty kylmä pitsa vaihtaa omistajaansa, hän antaa sinulle vinkin:

// tyonTyyppi: 0 = normaali, 1 = vaarallinen, 2 = yötyö

switch(tyonTyyppi)
{
  case 2:
  palkka += 1000;

  case 1:
  palkka += 500;

  case 0: // default: kävisi myös
  palkka += 7000;

}

Noin. Yötyön tekijä saa 1000+500+7000=8500 mk/kk, kun taas päivisin karkkikauppaa vahtiva vain 7000 mk/kk. Esimerkki on kieltämättä aika teennäinen ja oikeasti toteutus olisi melko erilainen: palkkamäärät olisivat vähintään vakiomuuttujia, ei suoraan koodin kirjoitettuja.

 

Nimiavaruudet *

Kerronpa tarinan. Kaikki tapahtui kun olin vielä nuori ja kokematon (siis ohjelmoijana). Kehitin iltojeni iloksi ja vapaapäiviäni ratoksi järjestelmää graafisten esitysten rakentamiseen. Järjestelmä toimi niin, että siihen kytkettiin komponenttejä (kolmiulotteisia efektejä, videoita, kuvia jne...) ja järjestelmä hoiti efektien ajastuksen ja lopputuloksen piirtämisen ruudulle. Järjestelmä oli yhteydessä efektikomponenttehin Interface-nimisen oman tietotyypin avulla.

Aluksi kehitin järjestelmää käyttäen omaa grafiikkakoodia, siis toteutin itse grafiikan piirron ruudulle. Koska oma grafiikkakoodini oli hyvin yksinkertainen, se ei riittänyt pitkälle. Minulla ei ollut aikaa eikä mahdollisuutta kehittää sitä eteenpäin, olisinhan kaikenlisäksi tarvinnut kymmeniä erilaisia näytönohjaimia jolla testata grafiikanpiirtoa. Joten päätin käyttää hyvää ja valmista grafiikkakirjastoa. Silloin törmäsin ongelmaan: kirjasto käytti myös Interface-tietotyyppiä. Kun lisäsin kirjaston mukaan ohjelmaani, ei sitä voinut kääntää koska kääntäjä havaitsi saman nimen määrittelyn kahteen kertaan eikä siten pystynyt päättämään että kumpaa aina kulloinkin käytettiin. Lopputulos oli, että projektini kuivui kasaan ja minun oli keksittävä jotain muuta tekemistä iltojeni ratoksi.

Kun yhdistetään kahta eri ohjelmaa, on hyvin yleistä että törmätään nimiristiriitoihin (name clash) - kuten äskeisessä tarinassa kävi. Kyseessä ei tarvitse olla edes kahden eri henkilön tekemät ohjelmat, vaan riittää että yrittää käyttää valmista ohjelmanpätkää jonka on viikko sitten ohjelmoinut - silloinkin saattaa jo törmätä tilanteeseen, jossa harmittelee että miksi sitä piti mennä antamaan noille kahdelle muuttujalle samat nimet, nyt kääntäjä on sekaisin niiden kanssa... Yksi vaihtoehto on muuttaa nimiä, mutta usean kymmenen tuhannen rivin pituisessa ohjelmassa se ei kuulostaa enää kovin herkulliselta vaihtoehdolta, ja mikä vielä epäherkullisempaa, jos unohtaa muuttaa yhdenkin nimen, niin kääntäjä luulee sitä toiseksi ja ohjelma toimii täysin järjettömästi. Silloin viimeistään menee hermot ja päätyy vaihtamaan alaa.

Nimiristiriitojen rauhanomaiseen ratkaisuun C++-kielessä on ominaisuus nimeltään nimiavaruudet. Niiden avulla voidaan suuremmat kokonaisuudet (ohjelmat tai ohjelman osat) sijoittaa omiin avaruuksiinsa, niin että ne voivat sisältää päällekkäisiä nimiä. Totuushan nimittäin on, että hyviä nimiä on maailmassa aika vähän. Tässäpä lyhyt ja teennäinen esimerkki (minulla oli mielessä myös paljon vähemmän teennäinen esimerkki, mutta koska ohjelmointimaailmassa suurin osa esimerkeistä on hyvin teennäisiä, niin näen velvollisuudekseni totuttaa sinut jo tässä vaiheessa):

namespace Piirto_ohjelma 
{ 
  int pallo; 
  int nelio; 
  int kuutio; 
}
namespace Jalkapallopeli 
{ 
  int pallo; 
  int palloilija; 
  int huligaani; 
}
Piirto_ohjelma::pallo = Jalkapallopeli::pallo;

Nimiavaruus määritellään namespace-käskyllä ja sen perässä olevassa lohkossa määritellyt nimet tulevat nimiavaruuden sisälle. Viimeisellä rivillä pistetään leikisti piirto-ohjelma piirtämään jalkapallopelin pelitilanne, jota voidaan sitten käyttää futispelin markkinointimateriaalissa. Nimiavaruuden sisällä olevaa muuttujaa käytetään niin, että kirjoitetaan nimiavaruuden nimi ja kaksi kaksoispistettä ja muuttujan nimi. Yleisesti ottaen sekä jalkapallopelit että piirto-ohjelmat ovat hieman monimutkaisempia kuin kolme kokonaislukumuuttujaa... Koska nimiavaruuden jatkuva toistaminen on työlästä, voidaan todeta että nyt pallo tarkoittaa jalkapallopelin nahkakuulaa ellei toisin sanota. Eli tehdä näin:

using Jalkapallopeli::pallo;
// using Jalkapallopeli;
pallo = Piirto_ohjelma::pallo;

Nyt pelkkä pallo viittaa pallopeliin, koska kerroimme että käytämme jalkapallopelin palloja. Piirto-ohjelman palleroiden pyörittely vaatii tarkkaa viittausta, siis Piirto_ohjelma::pallo. Emme tietenkään voi tuoda molempia palloja yhtä aikaa käyttöön, koska kääntäjä ei tietäisi enää kumpi on kumpi. Mikäli using-käskylle antaa syötteeksi namespace ja nimiavaruuden (eli using namespace Piirto_ohjelma;), ottaa se kaikki nimiavaruuden nimet käyttöön. Se ei välttämättä ole hyvä idea, koska oikeiden ohjelmien nimiavaruuksissa on paljon nimiä ja Murphyn lakia noudatellen kuitenkin jotkut sitten menevät päällekkäin ohjelman nimien kanssa.

Myös C++:n mukana tulevat tietotyypit on sijoitettu omaan nimiavaruuteensa, nimeltään std (niinkuin standard). Tästä johtuen oikeaoppinen "Hello World" -ohjelma tulisikin olla tämmöinen:

#include <iostream>

void main()
{
   std:: cout << "Hello world!";
}

Siis cout tulee kaivella sieltä std-nimiavaruuden kätköistä. Tarkkasilmäisimmät saattavat havaita, että iostreamin lopusta puuttuu se piste ja hoo. Nimiavaruudet ovat uudehko ominaisuus ja olisi ollut kovin kurjaa, jos niiden käyttöönoton jälkeen olisivat vanhat C++-ohjelmat lopettaneet toimintansa. Niinpä perinteiset .h-päätteiset otsikkotiedostot määrittelevät kaiken nimettömään nimiavaruuteen, siis oletusarvoiseen nimiavaruuteen. Esimerkiksi pelkkä cout siis viittaa nimettömään avaruuteen. Kun taas käytetään otsikkotiedostojen hoottomia versioita, on kaikki siirretty std-avaruuteen ja ylläoleva esimerkki ei siis toimisi jos std:: otettaisiin coutin alusta pois. Nimettömän avaruuden käyttöä tulisi välttää, joten myös .h-otsikkotiedostot eivät ole suositeltavia (tätä opasta ei ole näiltä osin ole vielä päivitetty joka paikasta, .h-tiedostojen käyttöön saattaa törmätä).

Toki toimiva vaihtoehto on myös:

#include <iostream>

using std::cout;

void main() 
{ 
  cout << "Hello world!"; 
}

Kuten huomaat, nimiavaruuksien käyttäminen tuo hieman lisää vaivaa. Siksi en turhaan monimutkaista lyhyitä esimerkkejä nimiavaruuksilla. Kokonaisissa esimerkkiohjelmissa pyrin noudattamaan oikeaoppista nimiavaruuksien käyttöä, mutta kuten totesin, päivityksen ovat vielä kesken. Esimerkit siis kyllä toimivat uusillakin kääntäjillä, mutta .h-otsikkotiedostojen käyttö ei ole kaikkien ohjelmointitaiteen sääntöjen mukaista.

Takaisin