previous next Title Contents Index

10. C- kielen ohjausrakenteista ja operaattoreista


Mitä tässä luvussa käsitellään?

* if-else - lause

* loogiset operaattorit: &&, || ja !

* bittitason operaattorit: &,|,^ ja ~

* silmukat while, do-while ja for

* silmukan "katkaisu" break, continue, goto

* sijoituslauseet: = += -= jne.

* valintalause switch

Syntaksi:

	lause  joko       ylause;                            // HUOM! Puolipiste 
	       tai        lohko                              // eli koottu lause            
	ylause            yksinkertainen lause              
	  esim              a = b + 4
	                    vaihda(a,b)
	lohko             { lause1 lause2 lause3 }           // lauseita 0-n
	  esim              { a = 5; b = 7; }     
	ehto              lauseke joka tuottaa 0 tai != 0
	  esim              a < 5
	                    ( 5 < a ) && ( a < 10 )
	                    !a                               // jos a=0 => 1, muuten 0
	  HUOM!           Vertailu a == 5
	
	if-else           if ( ehto ) lause1
	                  else lause2                        // ei pakollinen
	while             while ( ehto ) lause;
	do-while          do lause while ( ehto );
	for               for ( ylause1a,ylause2a; ehto ; ylause1k,ylause2k ) lause
	  esim              for ( i=0,s=0; i<10; i++ ) s += i;  // ylause1a                 
	swicth            switch ( lauseke ) {
	                    case arvo1: lause1 break;        // valintoja 0-n   
	                    case arvo2:                      // arvolla 2 ja 3 sama
	                    case arvo3: lause2 break;
	                    default:    laused break;        // ei pakollinen
	                  }
Ohjelma jossa ei ole minkäänlaista valinnaisuutta tai silmukoita on varsin harvinainen. Kertaamme seuraavassa C/C++- kielen tarjoamat mahdollisuudet suoritusjärjestyksen ohjaamiseen. Samalla näemme kuinka suomenkielisen algoritmin kääntäminen ohjelmointikielelle on varsin mekaanista puuhaa.

10.1 if- lause

Mikäli meillä on kaksi lukua, jotka pitäisi olla suuruusjärjestyksessä, voisimme hoitaa järjestämisen seuraavalla algoritmilla:
	1. Jos luvut väärässä järjestyksessä, 
	     niin vaihda ne keskenään
Tämän kirjoittamiseksi ohjelmaksi tarvitsemme ehto- lausetta:
	if ( ehto ) ylause1;
	lause2; 
Huomattakoon, että tässä sulut ehdon ympärillä ovat pakolliset. lause1 suoritetaan vain kun ehto on voimassa. lause2 suoritetaan aina. Lause voitaisiin kirjoittaa myös muodossa
	if(ehto) ylause1;
	lause2; 
muttei näin tehdä, jotta erottaisimme paremmin funktion ja if- lauseen toisistaan. Saa tulee koskemaan myös for, while ja muita vastaavia rakenteita.

10.1.1 Ehdolla suoritettava yksi lause

Olkoon meillä aliohjelma nimeltään vaihda, joka suorittaa itse vaihtamisen:
	if ( a > b ) vaihda(&a,&b); 

10.1.2 Ehdolla suoritettava useita lauseita

Mikäli aliohjelmaa ei ole käytössä, täytyisi meidän voida suorittaa useita lauseita muuttujien vaihtamiseksi. C- kielessä voidaan lausesuluilla kasata joukko lauseita yhdeksi lauseeksi (lohko, koottu lause, block):

Vinkki

Sisennä kauniisti

	if ( a > b ) {
	  t = a; 
	  a = b; 
	  b = t;
	}
Huomautus! Lauseiden kirjoittaminen samalle riville ei auttaisi mitään, sillä
	if ( a > b ) t = a; a = b; b = t;
	/*  vastaisi loogisesti rakennetta: */
	if ( a > b ) t = a; 
	a = b; 
	b = t; 
Koodia voidaan kuitenkin usein lyhentää kirjoittamalla asioita samalle riville:
	if ( a > b ) {
	  t = a; a = b; b = t;
	}
	/* tai joskus jopa */
	if ( a > b ) { t = a; a = b; b = t; }
Niin kauan kuin todella hallitsee asian, voi olla helpointa laittaa aina if- lauseen ainoakin suoritettava lause lausesulkuihin
	if ( a > b ) {
	  vaihda(&a,&b);
	}
Tästä on se etu, että myöhemmin monimutkaisten makrojen kanssa ei tule ongelmia, sekä se, että nyt if- lauseen suoritettaviksi lauseiksi on helppo lisätä uusia lauseita. Mikäli sulkuja ei olisi, täytyisi toisen lauseen lisäyksen yhteydessä muistaa lisätä myös sulut (tosin eihän hyvin suunniteltua ohjelmaa tarvinnut enää jälkeenpäin paikata?).

Tehtävä 10.91 vaihda

Esitä pöytätestin avulla miksei vaihtaminen onnistu pelkästään lauseilla:
a = b; b = a;

Tehtävä 10.92 abs

Kirjoita funktio
int itseisarvo(int i),
joka palauttaa i:n itseisarvon (negat. muutet. posit.).

Tehtävä 10.93 jarjesta2

Kirjoita aliohjelma
void jarjesta2(int *a, int *b),
joka järjestää a:n ja b: siten, että a<=b.

Tehtävä 10.94 maksimi ja minimi

Kirjoita funktio
int maksimi(int a, int b),
joka palauttaa suuremman kahdesta luvusta.
Kirjoita vastaava funktio minimi.

10.2 Loogiset lausekkeet

C- kielessä mikä tahansa lauseke voidaan kuvitella loogiseksi lausekkeeksi. Arvo 0 on epätosi ja kaikki muut arvot ovat tosia.
	a = 4;
	if ( a ) ... 

10.2.1 Vertailuoperaattorit

Vertailuoperaattorin käyttö muodostaa loogisen lausekkeen, jonka arvo on 0 tai 1. Vertailuoperaattoreita ovat:
	==  yhtäsuuruus
	!=  erisuuruus
	<   pienempi kuin
	<=  pienempi tai yhtä kuin
	>   suurempi kuin
	>=  suurempi tai yhtä kuin
Esimerkkejä vertailuoperaattoreiden käytöstä:
	if ( a < 5 )  printf("a alle viisi!\n);
	if ( a > 5 )  printf("a yli viisi!\n);
	if ( a == 5 ) printf("a tasan viisi!\n);
	if ( a != 5 ) printf("a ei ole viisi!\n); 

10.2.2 Sijoitus palauttaa arvon!

Yhtäsuuruutta verrataan == operaattorilla, EI sijoituksella =. Tämä on eräs tavallisimpia aloittelevan (ja kokeneenkin) C- ohjelmoijan virheitä:

c-silm\ifsij.c - esimerkki sijoituksesta ehdossa

	/* Seuraava tulostaa vain jos a == 5 */
	if ( a == 5 ) printf("a on viisi!\n");
	/* Seuraava sijoittaa aina a = 5 ja tulostaa AINA! */	
	if ( a = 5 ) printf("a:ksi tulee AINA 5!\n"); 

Sijoitus a=5 on myös lauseke, joka palauttaa arvon 5. Siis sijoitus kelpaa tästä syystä vallan hyvin loogiseksi lausekkeeksi. Onneksi useat C- kääntäjät osaavat varoittaa tästä katalasta virhemahdollisuudesta.

Joskus ominaisuutta voidaan tarkoituksella käyttää hyväksikin. Esimerkiksi halutaan sijoittaa AINA a=b ja sitten suorittaa jokin lause, mikäli b!=0. Tämä voitaisiin kirjoittaa useilla eri tavoilla:

c-silm\ifsij2.c - esimerkki tahallisesta sijoituksesta ehdossa

	/*1*/  a = b; if ( b ) printf("b ei ole nolla!\n");
	/*2*/  a = b; if ( b != 0 ) printf("b ei ole nolla!\n");
	/*3*/  if ( a = b ) printf("b ei ole nolla!\n");
	/*4*/  if ( (a=b) != 0 ) printf("b ei ole nolla!\n");
Edellisistä tapa 3 on C- mäisin, mutta kääntäjä saattaa varoittaa siitä (ja tämä varoitus kannattaa ottaa todesta). Jotta C- mäinen tapa voitaisiin säilyttää, voidaan käyttää tapaa 4 jolloin varoitusta ei tule, mutta generoidaan aivan vastaava koodi. Oleellista on, että sijoitus on suluissa (muuten tulisi sijoitus a =(b!=0) ). Mikäli asian toimimisesta on pieninkin epäilys kannattaa käyttää tapaa 1 tai 2!

10.3 Loogisten lausekkeiden yhdistäminen

Loogisia lauseita voidaan yhdistää loogisten operaatioiden avulla. Tietysti lauseita voidaan yhdistää myös normaaleilla operaatioilla (+,- ,*,/), mutta tämä ei ole oikein hyvien tapojen mukaista.

10.3.1 Loogiset operaattorit &&, || ja !

	&&  ja
	||  tai
	!   muuttaa ehdon arvon päinvastaiseksi (eli 0- >1, <>0 - >0) 
Mikäli yhdistettävät ehdot koostuvat esimerkiksi vertailuoperaattoreiden käytöstä, kannattaa ehtoja sulkea sulkuihin, jottei seuraa turhia epäselvyyksiä.
	if ( ( rahaa > 50 ) && ( kello < 19 ) )  printf("Mennään elokuviin!\n);
	if ( ( rahaa < 50 ) || ( kello >3 ) )    printf("Ei kannata mennä kapakkaan!\n");
	if ( ( 8 <= kello ) && ( kello <= 16 ) ) printf("Pitäisi olla töissä!\n");
	if ( ( !rahaa ) || ( sademaara < 10 ) )  printf("Kävele!\n");
Usein tulee vastaan tilanne, jossa pitäisi testata on luku jollakin tietyllä välillä. Esimerkiksi onko
	1900 <= vuosi <= 1999
palauttaisi C- kielisenä lauseena aina 1. Miksikö? Koska lause jäsentyy
	( 1900 <= vuosi ) <= 1999   
	     0 tai 1      <= 1999  eli aina 1
Oikea tapa kirjoittaa väli olisi:
	if ( ( 1900 <= vuosi ) && ( vuosi <= 1999 ) ) ... 
Huomattakoon edellä miten väliä korostettiin kirjoittamalla välin päätepisteet lauseen laidoille.

C- kielen sidontajärjestyksen ansiosta lause toimisi myös ilman sisimpiä sulkuja, mutta ne kannattaa pitää mukana varmuuden vuoksi. Vertailtavat kannattaa kirjoittaa nimenomaan tähän järjestykseen, koska tällöin vertailu muistuttaa eniten alkuperäistä väliämme!

Vastaavasti jos arvon halutaan olevan välin ulkopuolella, kannattaa kirjoittaa:

	if ( ( vuosi < 1900 ) || ( 1999 < vuosi ) ) ... 
Tällöin epäyhtälöiden suuntaa ei joudu koskaan miettimään, vaan arvot ovat aina siinä järjestyksessä kuin lukusuorallakin:
	          1900  vuosi   1999               1900<=vuosi && vuosi <=1999
	-----------o==============o--------------------
	
	  vuosi  1900          1999  vuosi         vuosi<1900 || 1999 <vuosi 
	===========o--------------o====================

10.3.2 Loogisen lausekkeen suoritusjärjestys

Loogiset lausekkeet suoritetaan AINA vasemmalta oikealle, kunnes ehdon arvo on selvinnyt.

Siis: Loogisen lausekkeen evaluoiminen lopetetaan heti kun ehdon arvo selviää (boolean expression shortcut).

Esimerkiksi:

	if ( a || ( (b=c)==0 ) ) printf("Kukkuu\n");
Tai- operaattorin (||) oikealla puolella oleva sijoitus suoritetaan vain mikäli a==0:

a

b
c

sij.suor
tulostetaan

0

0

5

5

?

?

?

?

0

3

0

3


kyllä

kyllä

ei

ei

kyllä

ei

kyllä

kyllä


Tätä ominaisuutta voidaan käyttää hyväksi esimerkiksi tiedostoja luettaessa:
	while ( f && f >> luku )
	   summa += luku;
Tällöin lopussa olevaa tiedostoa ei enää lueta, koska AND- operaation (&&) arvo voidaan päättää epätodeksi heti ensimmäisestä osalauseesta. Tosin edellinen silmukka toimii myös muodossa
	while ( f >> luku ) summa += luku;

10.4 Bittitason operaattorit

Yksi C- kielen vahvoista piirteistä erityisesti alemman tason ohjelmoinnissa on mahdollisuus käyttää bittitason operaattoreita.

Loogisia operaattoreita &&,|| ja! ei pidä sotkea vastaaviin bittitason operaattoreihin:

	&   bittitason AND
	|   bittitason OR
	^   bittitason XOR
	~   bittitason NOT
	<<  rullaus vasemmalle, 0 sisään oikealta 
	>>  rullaus oikealle, 0 sisään vasemmalta (unsigned int ja int >=0)
	                    , voi tulla 0 tai 1 sisään vasemmalta (int joka <0)
	                      (laiteriippuva, esim. Turbo C:ssä tulee 1). 
Bittitason operaattoreita voidaan käyttää vain kokonaisluvuiksi muuttuviin operandeihin.

Operaattoreiden toimintaa voidaan kuvata seuraavasti. Olkoon meillä sijoitukset a=5; b=14;. Kuvitellaan kokonaisluvut tilapäisesti 8 bitin mittaisiksi (oikeasti yleensä 16 tai 32 bittiä):


Binaarisena
desim.

a
0000 0101
5

b
0000 1110
14

a & b
0000 0100
4

a | b
0000 1111
15

a ^ b
0000 1011
11

~a
1111 1010
-6

a<<2
0001 0100
20

b>>3
0000 0001
1

a && b
0000 0001
1

a || b
0000 0001
1

!a
0000 0000
0

Huomautus! Tyypillinen ohjelmointivirhe on sotkea keskenään loogiset ja bittitason operaattorit:

Tehtävä 10.95 Loogiset/bittitason operaattorit

Mitä tulostaa seuraava ohjelman osa.
	{ /* binoper.c */
	  int a=5; b=2;
	  if ( a&&b ) printf("On ne!\n");
	  if ( a&b )  printf("Ei ne ookkaan!\n");
	  if ( a ) printf("a on!\n");
	  if ( ~b ) printf("b ehkä on!\n");
	  if ( !b ) printf("b ei ole!\n");
	}

Tehtävä 10.96 Luku parilliseksi

Kirjoita funktio parilliseksi, joka palauttaa parametrinaan olevan kokonaisluvun pienemmäksi parilliseksi luvuksi "katkaistuna". Eli esim. 3 - > 2. 5 - > 4. 4 - > 4.

10.5 if - else - rakenne

if - lauseesta on myös versio, jossa jotakin voidaan tehdä ehdon ollessa epätosi:
	if ( ehto ) ylause1; 
	else ylause2; 
Jälleen, mikäli jommassa kummassa osassa tarvitaan useampia lauseita, suljetaan lausejoukko lausesuluilla. Tosin kannattaa taas harkita lausesulkujen käyttöä aina myös yhdenkin lauseen tapauksessa.
	/* Samalle riville: */
	  if ( a < 5 ) printf("a alle viisi!\n");
	  else printf("a vähintään viisi!\n");
	  
	/* Eri riville: */
	  if ( a < 5 ) 
	    printf("a alle viisi!\n");
	  else 
	    printf("a vähintään viisi!\n");
	  
	/* Lausesulkujen käyttö: */
	  if ( a < 5 ) {
	    printf("a alle viisi!\n");
	  }
	  else { 
	    printf("a vähintään viisi!\n");
	  }
	
	/* Seuraavaa tyyliä käytetään myös usein: */
	  if ( a < 5 ) {
	    printf("a alle viisi!\n");
	  } else { 
	    printf("a vähintään viisi!\n");
	  }

10.5.1 Sisäkkäiset if- lauseet

Meillä oli aikaisemmin tehtävänä kirjoittaa funktio, joka palauttaa toisen asteen yhtälön ax2+bx+c=0 toisen juuren. Tällöin oletuksena oli, että a<>0 ja D>=0. Mikäli ratkaisukaavaa sovelletaan sellaisenaan ja a=0 tai D<0, niin tällöin ohjelman suoritus päättyy ajonaikaiseen virheeseen.

Voisimme muuttaa tehtävän määrittelyä siten, että kumpikin juuri pitää palauttaa ja funktion nimessä palautetaan tieto siitä, tuliko ratkaisussa virhe, eli jollei juuret olekaan reaalisia.

	if ( a != 0 ) {
	  D = b*b -  4*a*c;
	  if ( D > 0 ) {
	    ...
	  }
	  else {
	    ...
	  }
	}
	else {
	  ...
	}
Tosin yhtälö pystytään mahdollisesti ratkaisemaan myös kun a==0. Tällöin tehtävä jakautuu useisiin eri tilanteisiin kertoimien a,b ja c eri kombinaatioiden mukaan:







juuret



a
b
c
D

yhtälön muoto
reaalisia
x1
x2

0
0
0
?

0 = 0
juu
0
0

0
0
c
?

c = 0
ei
0
0

0
b
?
?

bx - c = 0
juu
-c/b
-c/b

a
?
?
>=0

ax2 + bx + c = 0
juu
(-b-SD)/2a
(-b+SD)/2a

a
?
?
<0

- " -
ei



Algoritmiksi kirjoitettuna tästä seuraisi:

	1.  Jos a=0, niin 
	      Jos b=0
	         Jos c=0 yhtälö on muotoa 0=0 joka on aina tosi
	           palautetaan vaikkapa x1=x2 =0
	         muuten (eli c<>0) yhtälö on muotoa c=0 joka on
	           aina epätosi, palautetaan virhe
	      muuten (eli b<>0) yhtälö on muotoa bx=c 
	          joten voidaan palauttaa vaikkapa x1=x1=- c/b
	2.  Jos a<>0, niin
	      Jos D>=0 kyseessä aito 2. asteen yhtälö ja käytetään
	        ratkaisukaavaa
	      muuten (eli D<0) ovat juuret imaginaarisia
Funktio ja sen testiohjelma voisi olla esimerkiksi seuraavanlainen:

c-silm\p2_2.cpp - esimerkki 2. asteen yhtälön ratkaisemiseta

	#include <iostream.h>
	#include <math.h>
	
	int ratkaise_2_asteen_yhtalo(double &x1, double &x2,
	                             double a, double b, double c)
	{
	  double D,SD;
	  x1 = x2 = 0;
	  if ( a == 0 ) {                     /*       bx + c = 0 */
	    if ( b == 0 ) {                   /*            c = 0 */
	      if ( c == 0 ) {                 /*            0 = 0 */
	        return 0;                     /* id. tosi         */
	      }                 /* c==0 */
	      else {            /* c!=0 */    /*       0 != c = 0 */
	        return 1;                     /* Aina epät.       */
	      }                 /* c!=0 */
	    }                   /* b==0 */
	    else {              /* b!=0 */    /*       bx + c = 0 */
	      x1 = x2 = - c/b;
	      return 0;
	    }                   /* b!=0 */
	  }                     /* a==0 */
	  else {                /* a!= 0 */   /* axx + bx + c = 0 */
	    D = b*b -  4*a*c;
	    if ( D >= 0 ) {                   /* Reaaliset juuret */
	      SD  = sqrt(D);
	      x1 = (- b- SD)/(2*a);
	      x2 = (- b+SD)/(2*a);
	      return 0;
	    }                   /* D>=0 */
	    else {                            /* Imag. juuret     */
	      return 1;
	    }                   /* D<0  */
	  }                     /* a!=0 */
	}
	
	double P2(double x, double a, double b, double c)
	{
	  return (a*x*x + b*x + c);
	}
	
	int main(void)
	{
	  double a,b,c,x1,x2;
	  do {
	    cout << "Anna 2. asteen yhtälön a b c >";
	    cin >> a >> b >> c;
	    if ( ratkaise_2_asteen_yhtalo(x1,x2,a,b,c) ) {
	      cout << "Yhtälöllä ei ole reaalisia juuria!" << endl;
	    }
	    else {
	      cout << "1. ratkaisu on "  << x1
	           << ".  Arvoksi tulee tällöin " << P2(x1,a,b,c) << endl;
	      cout << "2. ratkaisu on "  << x2
	           << ".  Arvoksi tulee tällöin " << P2(x2,a,b,c) << endl;
	    }
	  } while (a>0);
	  return 0;
	}
Edellinen funktio on äärimmäinen esimerkki sisäkkäisistä if- lauseista. Jälkeenpäin sen luettavuus on erittäin heikko ja myös kirjoittaminen hieman epävarmaa. Parempi kokonaisuus saataisiin lohkomalla tehtävää pienempiin osasiin aliohjelmien tai makrojen avulla.

Sisäkkäisten if- lauseiden kirjoittamista voidaan helpottaa kirjoittamalla niitä sisenevästi, eli aloittamalla ensin tekstistä:

	  if ( a == 0 ) {                     /*       bx + c = 0 */
	  }                     /* a==0 */
	  else {                              /* axx + bx + c = 0 */
	    D = b*b -  4*a*c;
	  }                     /* a!=0 */
Sitten täydennetään vastaavalla ajatuksella sekä if- osan että else- osan toiminta.

Jos funktiosta karsitaan kaikki ylimääräinen (kommentit ja ylimääräiset lausesulut) pois, saamme seuraavan näköisen kokonaisuuden:

c-silm\p2_2l.cpp - karsittu versio 2. asteen yhtälöstä

	int ratkaise_2_asteen_yhtalo(double &x1, double &x2,
	                             double a, double b, double c)
	{
	  double D,SD;
	  x1 = x2 = 0;
	  if ( a == 0 )
	    if ( b == 0 ) {
	      if ( c == 0 ) return 0;
	      else return 1;
	    }
	    else {
	      x1 = x2 = - c/b;
	      return 0;
	    }
	  else {
	    D = b*b -  4*a*c;
	    if ( D >= 0 ) {
	      SD  = sqrt(D);
	      x1 = (- b- SD)/(2*a);
	      x2 = (- b+SD)/(2*a);
	      return 0;
	    }
	    else return 1;
	  }
	}
Joskus kannattaa harkita olisiko luettavuuden kannalta paras esitystapa sellainen, että käsitellään "normaaleimmat" tapaukset ensin:

c-silm\p2_2n.cpp - normaalit tapaukset ensin ratkaisussa

	int ratkaise_2_asteen_yhtalo(double &x1, double &x2, 
	                             double a, double b, double c)
	{
	  double D,SD;
	  x1 = x2 = 0;
	  if ( a != 0 ) {
	    D = b*b -  4*a*c;
	    if ( D >= 0 ) {
	      SD  = sqrt(D);
	      x1 = (- b- SD)/(2*a);
	      x2 = (- b+SD)/(2*a);
	      return 0;
	    }
	    else return 1;
	  }
	  else /* a==0 */
	    if ( b != 0 ) {
	      x1 = x2 = - c/b;
	      return 0;
	    }
	    else { /* a==0, b==0 */
	      if ( c == 0 ) return 0;
	      else return 1;
	    }
	}
Usein aliohjelman return- lauseen ansiosta else osat voidaan jättää poiskin:

c-silm\p2_2r.cpp - else -osat pois

	int ratkaise_2_asteen_yhtalo(double &x1, double &x2, 
	                             double a, double b, double c)
	{
	  double D,SD;
	  x1 = x2 = 0;
	  if ( a == 0 ) {
	    if ( b == 0 ) {
	      if ( c == 0 ) return 0;
	      return 1;
	    }
	    x1 = x2 = - c/b;
	    return 0;
	  }
	
	  D = b*b -  4*a*c;
	  if ( D < 0 ) return 1;
	
	  SD  = sqrt(D);
	  x1 = (- b- SD)/(2*a);
	  x2 = (- b+SD)/(2*a);
	  return 0;
	}
Edellä oli useita eri ratkaisuja saman ongelman käsittelemiseksi. Liika kommenttien määrä saattaa myös sekoittaa luettavuutta kuten 1. esimerkissä. Toisaalta liian vähillä kommenteilla ei ehkä kirjoittaja itsekään muista jälkeenpäin mitä tehtiin ja miten. Jokainen valitkoon edelläolevista itselleen sopivimman kultaisen keskitien.

Huomattakoon vielä lopuksi, että rakenne

	if ( c == 0 ) return 0;
	else return 1; 
voitaisiin korvata rakenteella
	return ( c != 0 ); 

Tehtävä 10.97 else - osat pois

Kirjoita ratkaise_2_asteen_yhtalo /* p2_2n.c */ ilman else - osia.

10.5.2 Useat peräkkäiset ehdot

Vaikka rakenne
	if (ehto1) lause1;
	else
	  if (ehto2) lause2;
	  else
	    if (ehto3) lause3;
	    else lause4; 
jossain mallissa sisennetäänkin ylläkuvatulla tavalla, on ajatus useimmiten lähempänä seuraavaa sisennystä:

c-silm\postimak.c - esimerkki samanarvoisista ehtolauseista

	double postimaksu(double paino) 
	/* Palautetaan kirjemaksun suuruus. 0 tarkoittaa pakettia */
	{
	  if      ( paino < 50 )   return  2.10;
	  else if ( paino < 100 )  return  3.50;
	  else if ( paino < 250 )  return  5.50;
	  else if ( paino < 500 )  return 10.00;
	  else if ( paino < 1000 ) return 15.00;
	  else                     return  0.00;
	}
Sovimme siis, että rakenne onkin muotoa:
	if      ( ehto1 ) lause1
	else if ( ehto2 ) lause2
	else if ( ehto3 ) lause3
	else              lause4 

Tehtävä 10.98 Lääni

Kirjoita aliohjelma
void laani(char rek_1_merkki)
joka tulostaa missä läänissä auto on rekisteröity.
Kirjaimen yhtäsuuruutta testataan if ( c == 'a' ) ...

Tehtävä 10.99 if- else

Mitä on muuttujien arvot seuraavien ohjelmanpätkien jälkeen (pöytätesti!)?

if (a<5)

/*1*/ a=1; b=2; c=3;

b=3;

a=6;

c=7;


if (a<0) a=3; else

/*5*/ a=1; b=2; c=3;

if (a>2) b=3; a=6;

c=7;






/*2*/ a=1; b=2; c=3;

if (a<5) b=3; a=6; c=7;


/*6*/ a=1; b=2; c=3;

if (a<- 5) if (a<0) a=6;

else a=2; c=7;






/*3*/ a=1; b=2; c=3;

if (a<5) {b=3; a=6;}

c=7;


/*7*/ a=1; b=2; c=3;

if (a<- 5) b=3;

if (a<5) a=6;

else a=2; c=7;






/*4*/ a=1; b=2; c=3; if (a<5)

b=3; else { a=6; c=7; }


/*8*/ a=1; b=2; c=3;

if (a<0) a=3; else;

if (a>2) b=3; a=6;

c=7;


Sisennä ohjelmanpätkät "asianmukaisesti".

10.6 do- while - silmukka

Aikaisemmin olemme tutustuneet erääseen algoritmiin selvittää onko luku alkuluku vai ei. Koska algoritmi on valmis, voimme kirjoittaa vastaavan ohjelman (% - operaattori antaa jakojäännöksen, 10 % 3 == 1 ):

c-silm\alkuluku.c - testataan onko luku alkuluku

	#include <stdio.h>
	
	/****************************************************************************/
	int pienin_jakaja(int luku)
	/* Palautetaan luvun pienin jakaja (alkuluvulle 1).
	 Algoritmi:
	   Negatiivisesta luvusta otetaan itseisarvo.
	   Kokeillaan jokaista pienempää jakajaa 2,3,5,7 jne, kunnes
	   jako menee tasan tai jakaja on liian iso ollakseen luvun jakaja.
	*/
	{
	  int jakojaannos,jakaja=2,kasvatus=1;
	  if ( luku < 0 ) luku = - luku;
	  if ( luku == 2 ) return 1;
	  do {
	    jakojaannos = luku % jakaja;
	    if ( jakojaannos == 0 ) return jakaja;
	    jakaja  += kasvatus;
	    kasvatus = 2;
	  } while ( jakaja < luku/2 );
	  return 1;
	}
	
	/****************************************************************************/
	int main(void)
	{
	  int luku,jakaja;
	  printf("Tutkin onko luku alkuluku.  Anna luku >");
	  scanf("%d",&luku);
	  jakaja = pienin_jakaja(luku);
	  if ( jakaja == 1 )
	    printf("Luku on alkuluku.\n");
	  else
	    printf("Luku on jaollinen esimerkiksi luvulla %d.\n",jakaja);
	  return 0;
	}
Käytimme tässä silmukkaa:
	do 
	  lause 
	while (ehto); 
Koska esimerkin silmukassa oli useita suoritettavia lauseita, oli lauseet suljettu lausesuluilla. Jälleen voi olla hyvä tapa käyttää AINA lausesulkuja.

Huomautus! Silmukoiden kanssa on syytä olla tarkkana sekä 1. kierroksen että viimeisen kierroksen kanssa. Myös silmukan lopetusehdon on syytä muuttua silmukan suorituksen aikana.

Eräs tyypillinen esimerkki do- while silmukan käytöstä olisi seuraava:

c-silm\dowhile.c - lukujen lukeminen kunnes halutulla välillä

	#include <stdio.h>
	
	int main(void)
	{
	  int luku;
	  do {
	    printf("Anna luku väliltä [0- 20]>");
	    fflush(stdin); /* Standardin mukaan ei määritelty! */
	  } while ( ( scanf("%d",&luku) < 1 ) || ( luku < 0 ) || ( 20 < luku ) );
	  printf("Annoit luvun %d\n",luku);
	  return 0;
	}

10.7 while - silmukka

do- while - silmukka suoritetaan aina vähintään 1. kerran. Joskus on tarpeen silmukka, jonka runkoa ei suoriteta yhtään kertaa. Muutamme edellisen esimerkkimme käyttämään while - silmukkaa:
	while ( ehto ) lause 
Muutamme samalla algoritmia siten, että 2:lla jaolliset käsitellään erikoistapauksena. Näin pääsemme eroon "inhottavasta" kasvatus- muuttujasta.

c-silm\alkuluk2.c - alkulukutesti while-silmukalla

	int pienin_jakaja(int luku) 
	{
	  int jakaja=3;
	  if ( luku < 0 ) luku = - luku;
	  if ( luku == 2 ) return 1;
	  if ( (luku % 2) == 0 ) return 2;
	  while ( jakaja < luku /2 ) {
	    if ( (luku % jakaja) == 0 ) return jakaja;
	    jakaja += 2;
	  }
	  return 1;
	}

10.8 for - silmukka, tavallisin muoto

Eräs C- kielen hienoimmista rakenteista on for- silmukka. Usein C- hakkereiden tavoite on saada kirjoitettua koko ohjelma yhteen for- silmukkaan. Tätä ei tietenkään tarvitse tavoitella, mutta se osoittaa for- silmukan mahdollisuuksia.

Tyypillisesti for- silmukkaa käytetään silloin, kun silmukan kierrosten lukumäärä on ennalta tunnettu:

c-silm\valinsum.c - esimerkki for-silmukasta

	/*  Lasketaan yhteen luvut 1..ylaraja */
	int valin_summa(int ylaraja)
	{
	  int i,summa=0;
	  for (i=1; i<=ylaraja; i++) 
	    summa += i;
	  return summa;
	}

Tehtävä 10.100 valin_summa

Muuta valin_summa - aliohjelmaa siten, että myös alaraja viedään parametrina. Kirjoita pääohjelma, jolla toiminta voidaan testata.
Käytännössä tällaisia silmukoita ei saa tehdä, koska ongelman ratkaisuun on valmis kaava. Millainen?

10.9 C- kielen lauseista

10.9.1 Sijoitusoperaattori =

Olemme tutustuneet jo C- kielen "normaaliin" sijoitusoperaattoriin =.

Sen ansiosta, että myös sijoitus palauttaa arvon, pystyimme tekemään mm seuraavia temppuja:

	if ( (b=a) != 0 ) ... /* Suoritetaan jos a!=0 */
	a = b = c = 0; 
Sijoitus monelle muuttujalle yhtäaikaa onnistuu, koska sijoitus jäsentyy seuraavasti:
	1.  a = ( b = (c = 0) ); -    sijoitus c=0 palauttaa arvon 0
	2.  a = ( b =    0 );    -    sijoitus b=0 palauttaa arvon 0
	3.  a =     0; 

10.9.2 Sijoitusoperaattori +=

valin_summa aliohjelmassa meillä esiintyi myös kaksi uutta sijoitusoperaattoria, jotka ovat lyhenteitä tavallisille sijoituksille:

lyhenne

tavallinen sijoitus
summa += i;

i++

summa = summa + i;

i = i + 1;

+= sijoituksessa + voidaan korvata millä tahansa operaattoreista:
	+   -    *   /   %   <<   >>   ^   &  |
Esimerkiksi luvun kertominen ja jakaminen 10:llä voitaisiin suorittaa:
	luku *= 10;
	luku /= 10; 
Siis muuttuja O= operandi voidaan ajatella korvattavaksi seuraavasti:
	0.  laita sulut operandin ympärille
	      muuttuja O= (operandi)
	1.  kirjoita muuttujan nimi kahteen kertaan
	      muuttuja muuttuja O= (operandi)
	2.  siirrä = - merkki muuttujien nimien väliin
	      muuttuja = muuttuja O (operandi) 

Tehtävä 10.101 +=

Mitä ovat muuttujien arvot seuraavien sijoitusten jälkeen:
	int a=10,b=3,c=5;
	a %= b;
	b *= a+c;
	b >>= 2; 

10.9.3 Lisäysoperaattori ++

Erittäin tyypillisiä C- operaattoreita ovat ++ ja- - .

Nämä operaattorit lisäävät tai vähentävät operandin arvoa yhdellä. Operandin tyypin tulee olla numeerinen tai osoitin.

Operandeista on kaksi eri versiota: esilisäys ja jälkilisäys.

lyhenne

vastaa lauseita
a = i++;

a = i--

a = ++i;

a = --i;

a = i; i = i+1;

a = i; i = i-1;

i = i+1; a = i;

i = i-1; a = i;

Vaikka C- hakkerit rakentavatkin mitä ihmeellisimpiä kokonaisuuksia ++ - operaattorin avulla, kannattaa operaattorin liikaa käyttöä välttää. Esimerkiksi lauseet joissa esiintyy samalla kertaa useampia lisäyksiä samalle muuttujalle, saattavat olla jopa määrittelemättömiä:

c-silm\plusplus.c - esimerkki ei-yksikäsitteisestä ++ operaattorin käytöstä

	#include <stdio.h>
	int main(void)
	{
	  double i=1.0,a;
	  a = i++/i++;
	  printf("a = %5.2lf, i = %5.2lf\n",a,i);
	  return 0;
	}
Ohjelma saattaa C- kääntäjän toteutuksesta riippuen tulostaa mitä tahansa seuraavista a:n ja i:in kombinaatioista:
	a: 0.5 1.0 2.0
	i: 2.0 3.0
Aluksi ++ - operaattoria kannattaa ehkä käyttää vain yksinäisenä lauseena lisäämään (tai vähentämään) muuttujan arvoa.
	i++;
Lisäysoperaattoria EI PIDÄ käyttää seuraavissa tapauksissa:
1.
Muuttuja johon lisäysoperaattori kohdistuu, esiintyy samassa lausekkeessa useammin kuin kerran.
1.
Makrojen kutsuissa.
1.
Myös funktioiden kutsut ovat vaarallisia, koska funktio saattaakin olla makro!
Kiellettyjä on siis esimerkiksi:
	a = ++i + i*i;
	b = MACRO(i- - );
	c = fun(++i,a,2*i); 

10.10 for - silmukka, yleinen muoto

Yleensä ohjelmointikielissä for- silmukka on varattu juuri siihen tarkoitukseen, kuin ensimmäinen esimerkkimmekin; tasan tietyn kierrosmäärän tekemiseen.

C- kielen for- silmukka on kuitenkin yleisempi:

	/*     1.             2.  5.          4.  7.           3.  6.  */
	for (alustus_lauseet; suoritus_ehto; kasvatus_lauseet) lause; 
for- silmukka vastaa melkein while- silmukkaa (ero tulee continue- lauseen käyttäytymisessä):
	alustus_lauseet;               /* 1.     */
	while ( suoritus_ehto ) {      /* 2.  5. */
	  lause;                       /* 3.  6. */
	  kasvatus_lauseet;            /* 4.  7. */
	}
Mikäli esimerkiksi alustuslauseita on useita, erotetaan ne toisistaan pilkulla:

c-silm\valinsum.c - useita alustuslauseita for-silmukassa

	/*  Lasketaan yhteen luvut 1..ylaraja */
	int valin_summa_2(int ylaraja)
	{
	  int i,summa;
	  for (summa=0, i=1; i<=ylaraja; i++) 
	    summa += i;
	  return summa;
	}
Erittäin C:mäinen tapa tehdä yhteenlasku olisi:

c-silm\valinsum.c - C:mäinen silmukka

	int valin_summa_3(int i)
	{
	  int s;
	  for (s=0; i; s += i- - );
	  return s;
	}
Tämä viimeinen esimerkki on juuri niitä C- hakkereiden suosikkeja, joita ehkä kannattaa osin vältellä.

Tehtävä 10.102 1+2+..+i

Miksi valin_summa_3 laskee yhteen luvut 1..i?

10.11 break ja continue

10.11.1 break

Joskus kesken silmukan tulee vastaan tilanne, jossa silmukan suoritus haluttaisiin keskeyttää. Tällöin voidaan käyttää C- kielen break- lausetta, joka katkaisee sisimmän silmukan suorituksen.

c-silm\break.c - silmukan katkaisu keskeltä

	#include <stdio.h>
	
	int main(void)
	{
	  int summa=0,luku;
	  printf("Anna lukuja.  Summaan niitä kunnes annat 0 tai summa > 20\n");
	  while ( summa <= 20 ) {
	    printf("Anna luku>");
	    scanf("%d",&luku);
	    if ( luku == 0 ) break;
	    summa += luku;
	  }
	  printf("Lukujen summa on %d.\n",summa);
	  return 0;
	}
Jos edellä olisi ollut alustus luku=1, olisi tietenkin while - silmukan ehto voitu kirjoittaa muodossa
	  while ( ( luku != 0 ) && ( summa <= 20 ) ) {
mutta aina ei voida break - lausetta korvata näin yksinkertaisesti. Esimerkiksi seuraava olisi jo hankalampi muuttaa:

c-silm\break2.c - esimerkki, jossa vaikea tulla toimen ilman breakiä

	  while ( summa <= 20 ) {
	    printf("Anna luku>");
	    if ( !scanf("%d",&luku ) break;
	    if ( luku == 0 ) break;
	    summa += luku;
	  }
break - lauseen vika on lähinnä siinä, ettei siitä suoraan nähdä sisäkkäisten silmukoiden tapauksessa sitä, mihin saakka suoritus katkeaa. Epäselvissä tapauksissa silmukan katkaisu voidaan hoitaa goto - lauseella.

Silmukka voidaan katkaista tietenkin myös muuttamalla silmukan lopetusehtoon vaikuttavia muuttujia. Varsinkin for- lauseen tapauksessa silmukan indeksin arvon muuttaminen muualla kuin kasvatus- lauseessa on todella väkivaltaista ja rumaa, eikä tällaista pidä mennä tekemään.

10.11.2 continue

Vastaavasti saattaa tulla tilanteita, jolloin itse silmukan suoritusta ei haluta katkaista, mutta menossa oleva kierros halutaan lopettaa. Tällöin continue - lauseella voidaan suoritus siirtää suoraan silmukan loppuun ja näin lopettaa tämän kierroksen suoritus:

c-silm\continue.c - silmukan lopun ohittaminen

	#include <stdio.h>
	
	int main(void)
	{
	  int alku= - 5, loppu=5,i;
	  double inv_i;
	  printf("Tulostan lukujen %d -  %d käänteisluvut\n",alku,loppu);
	  for (i = alku; i<=loppu; i++ ) {
	    if ( i == 0 ) continue;
	    inv_i = 1.0/i;
	    printf("%3d:n käänteisluku on %5.2lf.\n",i,inv_i);
	  }
	  return 0;
	}

Tehtävä 10.103 continuen korvaaminen

Kirjoita käänteislukujen tulostusohjelma ilman continue- lausetta.

10.12 goto - lause

Lähes kaikissa ohjelmointikielissä on goto- lause, mutta usein strukturoidun ohjelmoinnin kannattajat ovat julistaneet sen pannaan. Julistus on aivan hyvä, sillä ainakin 90% tapauksista, joissa goto- lausetta tekisi mieli käyttää, voidaan korvata strukturoidummalla tavalla.

Seuraavissa tapauksissa goto- lause voidaan hyväksyä:

	1.  silmukan suorituksen katkaisu
	2.  aliohjelman loppuun hyppääminen (silloin kun returnia
	    ei lopetustoimenpiteiden vuoksi voida käyttää)
	3.  jos strukturoitu rakenne on selvästi monimutkaisempi
Mikäli yhteen aliohjelmaan tulee useita goto - lauseita, on rakenne selvästikin suunniteltu väärin, ja se on mietittävä uudelleen.

goto - lauseen syntaksiin kuuluu paikan nimi, johon hypätään. Tämä nimiö täytyy esitellä ohjelmassa laittamalla se tarvittavaan kohtaan:

c-silm\goto.c - hyppy ulos silmukasta

	#include <stdio.h>
	
	int main(void)
	{
	  int summa=0,luku;
	  printf("Anna lukuja.  Summaan niitä kunnes annat 0 tai summa > 20\n");
	  while (summa <= 20) {
	    printf("Anna luku>");
	    if ( !scanf("%d",&luku) ) goto luvut_loppu;
	    if ( luku == 0 ) goto luvut_loppu;
	    summa += luku;
	  }
	luvut_loppu:
	  printf("Lukujen summa on %d.\n",summa);
	  return 0;
	}
Siis break ja continue ovat "piilotettuja" goto- lauseita. Mikäli break tai continue pitäisi saada toimimaan 2 tasoa ulospäin, on pakko käyttää goto- lausetta.

Huom! Ensisijaisesti pyri välttämään goto- lauseita!

Tehtävä 10.104 Eri silmukoiden vertailu

Kirjoita lukujen alaraja- ylaraja summausfunktio käyttäen
a) while - lausetta
b) do- while - lausetta
c) goto - lausetta
Muista, että alaraja saattaa olla suurempi kuin yläraja, eli summa väliltä [3,0] on 0!

10.13 switch - valintalause

Jäsenrekisteriohjelmamme päävalinta olisi näppärintä toteuttaa switch - lauseella:

menut.05\kerho.cpp - päävalinta switch -lauseella

	int paavalinta(cKerho &kerho)
	{
	  char nappain;
	
	  while (1) {
	
	    paamenu(kerho);  // Huom päämenun muuttunut parametrilista
	
	    nappain = odota_nappain("?012345",0,MERKKI_ISOKSI);
	
	    switch (nappain) {
	      case '?': avustus(nappain);                break;
	      case '0': return 0;
	      case '1': lisaa_uusi_jasen(nappain);       break;
	      case '2': etsi_jasenen_tiedot(nappain);    break;
	      case '3': tulosteet(nappain);              break;
	      case '4': tietojen_korjailu(nappain);      break;
	      case '5': paivita_jasenmaksuja(nappain);   break;
	      default : cout << "Näin ei voi käydä!" << endl;  return 1;
	    }
	
	  }
	}
switch - lauseessa case osien lopuksi break on yleensä välttämätön. break estää suorittamasta seuraavia rivejä.

Joskus harvoin breakin puuttumista voidaan käyttää hyväksi, mutta tällöin pitää olla todella tarkkana:

c-silm\switch.c - swicth, jossa break tahallaan jätetty pois

	switch (operaatio) {
	  case 5:                 /* Operaatio 5 tekee saman kuin 4 */
	  case 4: x *= 2; break;  /*           4 laskee x=2*x       */
	  case 3: x += 2;         /*           3 laskee x=x+4       */
	  case 2: x++;            /*           2 laskee x=x+2       */ 
	  case 1: x++; break;     /*           1 laskee x=x+1       */ 
	  default: x=0; break;    /* Muut nollaavat x:än            */
	}

Lause default suoritetaan jos mikään case- osista ei ole täsmännyt (tai tietysti jos jokin break puuttuu). default- lauseen ei tarvitse olla viimeisenä, mutta tällöin vaaditaan taitavaa breakin käyttöä, siis paras pitää default viimeisenä!

Yleistä switch- lausetta ei voi korvata joukolla if- lauseita käyttämättä goto- lausetta. Mikäli kuitenkin jokaisen case rakenteen perässä on break, voidaan switch- korvata sisäkkäisillä if- else - rakenteilla.

Tehtävä 10.105 switch - > if- goto

Kirjoita switch.c ohjelmanpätkä käyttäen if ja goto - rakenteita muuttamatta itse suoritettavia lauseita.

Tehtävä 10.106 Päävalinta

Kirjoita paavalinta käyttäen vain if ja else rakenteita.

Tehtävä 10.107 lääni, versio 2

Kirjoita laani- aliohjelma käyttäen switch- rakennetta.

10.13.1 || ei toimi switch - lauseessa!

On huomattava, että jos halutaan suorittaa jokin switch- lauseen osista kahdella eri arvolla, EI voida käyttää rakennetta:
	switch (operaatio) {  /* VÄÄRIN: */ 	
	  case 4 || 5: x *= 2; break;  /*    5 tai  4 laskee x=2*x       */ 	
	  case 3: x += 2;              /*           3 laskee x=x+4       */
	  case 2: x++;                 /*           2 laskee x=x+2       */ 
	  default: x=0; break;         /* Muut nollaavat x:än            */
	}
Kääntäjä ei tästä varoita, koska kaikki on aivan kieliopin mukaista. 4 || 5 on kahden loogisen lausekkeen OR eli 1 || 1 eli 1. Siis
	case 4 || 5:
	on sama kuin
	case 1: 
Jos esimerkistämme ei olisi poistettu lausetta case 1:, olisi kääntäjä varoittanut koska 1 olisi esiintynyt kahdesti.

10.14 Ikuinen silmukka

Usein silmukat lipsahtavat tahottomasti sellaisiksi, ettei niistä koskaan päästä ulos. Ikuisen silmukan huomaa heti esimerkiksi siitä, ettei silmukan rungossa ole yhtään lausetta joka muuttaa silmukan ehdon totuusarvoa.

Joskus kuitenkin C- kielessä tehdään tarkoituksella "ikuisia" - silmukoita:

	for (;;) {
	  ...
	  if (lopetus_ehto) break;
	  ...
	} 
	
	while (1) {
	  ...
	  if (lopetus_ehto) break;
	  ...
	} 
	
	do {
	  ...
	  if (lopetus_ehto) break;
	  ...
	} while (1); 
Näissä kahdessa ensimmäisessä korostuu silmukan ikuisuus. Viimeinen ei ole hyvä vaihtoehto.

Tällaiset ikuiset silmukat ovat hyväksyttävissä silloin, kun silmukan lopetusehto on luonnollisesti keskellä silmukkaa. Usein kuitenkin lauseiden uudelleen järjestelyllä lopetusehto voidaan sijoittaa silmukan alkuun tai loppuun, jolloin tavallinen while- , do- while - tai for - silmukka kelpaa.

10.14.1 Yhteenveto silmukoista


previous next Title Contents Index