previous next Title Contents Index

23. Yleisiä virheitä


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

* Käydään läpi nopeasti yleisiä "sudenkuoppia", joihin C/C++ - ohjelmoija helposti lankeaa C- kieli tarjoaa käyttäjälleen mm. Pascalia enemmän vapauksia. Toisaalta nämä vapaudet johtavat helposti virhetilanteisiin, joiden löytyminen saattaa olla todella vaikeata. Seuraavassa varoitukseksi koottu muutamia "sudenkuoppia".

23.1 Kirjoitusvirheet

Emme seuraavassa käsittele varsinaisia syntaksivirheitä, koska kääntäjä osaa niistä huomauttaa.

Kuitenkin muutama kirjoitusvirhe on ylitse muiden:

23.1.1 Unohtunut kommentin loppumerkki

Mikäli kommentin loppumerkki unohtuu, on kaikki kommenttia kunnes sattumoisin tulee vastaan jonkin toisen kommentin loppumerkki. Kaikki koodi tältä väliltä jää puuttumaan. Onneksi tästä yleensä seuraa määrittelemättömiä aliohjelmia yms, mistä kääntäjä osaa huomauttaa.

Mikäli saat hirveän listan täysin käsittämättömiltä tuntuvia virheilmoituksia, saattaa syynä olla puuttuva kommentin loppumerkki.

Nykyisten ohjelmointiympäristöjen (Borland C++, MS Visual C++) värilliset syntaksin esitystavat auttavat huomattavasti poistamaan näitä virheitä. Siis seuraa värejä ja hurraa niiden keksijöille.

23.1.2 Väärät tai puuttuvat lainausmerkit

Myös vakiomerkkijonoja esiteltäessä merkkijonon aloittavan tai lopettavan lainausmerkin unohtaminen saa yllätyksiä aikaan.

Lisäksi tulee muistaa, että

	'a'  -  kirjainvakio a (tavu 61H)
	"a"  -  merkkijonovakio jossa tavut (61H ja 00) 
Lainausmerkeissäkin editorin värit auttavat!

23.2 Virheitä joista kääntäjä ei huomauta

23.2.1 switch - lauseesta puuttuu break

Erittäin petollinen on tilanne, jossa switch - lauseen jostakin case - osasta on break unohtunut pois. Pääsääntöisesti jokainen case loppuu joko break tai return.

23.2.2 Ylitetään merkkijonolle varattu tila

C- kielessä ei yleensä valvota taulukkojen indeksejä. Tästä syystä erilaiset taulukoiden ylitykset ovat yleisiä. Kaikkein useimmiten tämä tapahtuu merkkijonoille esim. strcpy - lauseella.

Myös dynaamisesti varatun jonon ylittäminen on vaarallista:

	char *p;
	p = tee_jono("Kissa!");
	strcpy(p,"Kissa istuu puussa!"); /* VÄÄRIN! */ 	

23.2.3 Dynaamiset muuttujat unohtuu vapauttaa

Dynaamisia muuttujia käytettäessä tulee muistaa, että vaikka niitä on helppo varata, pitää ne muistaa myös vapauttaa. Esimerkiksi seuraava on väärin aliohjelmassa:
	char *ali(...)
	{
	  char *p;
	...
	  p = tee_jono("Kissa!"); 	
	...
	  if ( f == NULL ) return VIRHE; /* !!! poistutaan vapauttamatta p:n tilaa !!! */
	...
	  free(p);
	  return NULL;
	}
Jokaisella aliohjelman kutsukerralla varataan uusi tila merkkijonolle ja mikäli kutsuja tulee riittävästi, muisti loppuu pelkkiin Kissoihin! Tätä voidaan C++:ssa välttää ovelalla hajottimien käytöllä.

23.2.4 Osoitinta ei ole alustettu

Hyvin tavallinen virhe on käyttää alustamatonta osoitinta:
	char *p, jono[50];
	strcpy(p,"Kissa");  /* VÄÄRIN! */ 	
	p = jono+3;
	strcpy(p,"Kissa");  /* Kelpaa */

23.2.5 Taulukoiden indeksointi 0..n- 1

Usein unohtuu, että kun varataan taulukko
	int luvut[10]; 
niin viimeinen indeksi onkin 9!

23.2.6 scanf

Varsinainen supervaara on scanf. Normaaleissa aliohjelmissa oikein käytettynä kääntäjä osaa varoittaa tyyppivirheestä, mikäli osoitteena esiteltyä parametria kutsutaan arvolla. scanf on kuitenkin toteutettu siten, että tyypit saavat vapaasti olla mitä tahansa ja unohtuneesta &- merkistä ei kukaan valita.

Lisäksi toinen erittäin vaarallinen tilanne syntyy, mikäli format- osassa on eri määrä parametrejä kuin varsinaisessa parametrilistassa.

Myös parametrin tyypin ja format- osassa olevan formaatin sotkeminen saattaa kaataa koko koneen.

	int i;
	scanf("%lf",&i); /* VÄÄRIN! */ 	
Aina kun kirjoittaa jonkin scanfin sukuisen lauseen, pitää havahtua ja tarkistaa ainakin 3 kertaa lauseen olevan kunnossa. Tämänkin monisteen malliohjelmia kirjoitettaessa scanf on ollut kymmeniä kertoja väärin!

23.2.7 printf

printfin kanssa on vähän vastaavia ongelmia. Tyypin ja vastaavan format- osan erot aiheuttavat kuitenkin lähinnä vain hassuja tulosteita.

Usein myös newline merkki '\n' unohtuu rivin lopusta. Tällöin tietysti kaikki tulosteet tulevat enempi tai vähempi sekaisin näytölle.

23.2.8 #define

Makrot muodostavat oikeastaan kokonaan oman virheryhmänsä. Niin paljon kuin ne auttavatkin kirjoittamista, saattavat ne tosi paljon myös sotkea ohjelmaa.
	#define b 10*10
	...	
	a = 5.0/b; /*  - > a==5.0  !!!! */
Erityisesti runsas sulkujen käyttö auttaa välttämään ongelmia.

23.2.9 Funktion prototyyppi puuttuu

Mikäli käännöksessä ei käytetä ANSI- optioita, on mahdollista kääntää ohjelman osia vaikkei funktioita olisi vielä esiteltykään. Tällöin kääntäjä tekee omat oletuksensa funktioiden tyypeistä ja ne saattavat poiketa huomattavasti siitä, mitä kirjoittaja on tarkoittanut.

23.2.10 #include - unohtuu

Jotkin ohjelman osat saattavat mennä käännöksestä läpi, vaikka tarvittavat #include lauseet olisivat unohtuneet pois. Erityisesti malloc saattaa tällöin aiheuttaa ongelmia.

23.2.11 'ä' < 'a'

Kirjainvakiot muutetaan kokonaisluvuiksi. Järjestelmästä riippuen kirjain- tyypin arvo voi olla joka etumerkitön (unsigned) (usein hyvä asia) tai etumerkillinen (signed). Itse merkin sisäinen esitys kummassakin tapauksessa on sama, mutta laajennettaessa merkki kokonaisluvuksi saattaa etumerkki sotkea koko homman. Käytännössä on kyse seuraavasta:
	'a'  =  0x61 = 0110 0001  - > 0000 0000 0110 0001 (unsigned)
	                             0000 0000 0110 0001 (signed)
	'ä'  =  0x84 = 1000 0100  - > 0000 0000 1000 0100 (unsigned)
	                             1111 1111 1000 0100 (signed) 
Erityisesti tämä on muistettava käytettäessä kirjaimia indekseinä:
	
	char c;
	...
	kirjaimet[c]++;  /* Lisätään kirjainten lkm. */ /* VÄÄRIN */ 	
	...
	kirjaimet[(unsigned char)c]++; /* OIKEIN! */
	... 

23.2.12 Väärä tyypin muunnos

Väärä tyypin muunnos saattaa myös aiheuttaa harmaita hiuksia:
	#include <stdio.h>
	int main(void)
	{
	  double d; int i;
	  i = 5;
	  d = i/2; 	
	  printf("d = %4.2lf\n",d);
	  return 0;
	}
Edellinen ohjelma tulostaa d = 2.00. Lausekkeen arvo kussakin vaiheessa on sama kuin sen laskemisessa siihen saakka tarvitun "monimutkaisimman" tyypin arvo. Mallissa ollaan kokoajan arvossa int. Siis myös jakolaskun tulos on int. Vika voidaan korjata kahdella tavalla:
	d = i/2.0;
	d = ((double)i)/2; 

23.2.13 Pyöristys- ja katkaisuvirheet

Seuraava ohjelma voisi tulostaa i = 0:
	
	#include <stdio.h>
	int main(void)
	{
	  float d = 0.0001; int i;
	  i = 10000;
	  i = d*i;
	  printf("i = %4d\n",i);
	  return 0;
	}

Miksikö? Koska reaaliluku 0.0001 voisi sisäisenä esityksenä olla jotakin 0.000099999 ja kun tämä kerrotaan 10000:lla, tulee vähän alle 1 joka kokonaisluvuksi katkaistuna on 0!

23.2.14 Alustuksessa liian vähän pilkkuja

Esimerkiksi tietueen ja merkkijonotaulukon alustus on kriittinen pilkkujen suhteen:
	char *nimet[] = {
	  "Mikko" 	
	  "Pekka",
	  "Matti"
	}
Tästä seuraisi kahden nimen taulukko "MikkoPekka", "Matti"!

23.2.15 Palautetaan lokaalin muuttujan osoite

Erityisesti merkkijonotyyppisiä funktioita tehtäessä pitää muistaa, ettei vahingossa palauta lokaalin muuttujan osoitetta:
	char *kissa()
	{
	  char jono[50];
	  strcpy(jono,"kissa");
	  return jono;    /* VÄÄRIN! */ 	
	  return "kissa"; /* OIKEIN! */
	}

23.2.16 Käytetään muuttunutta muistipaikkaa

Funktiot, jotka palauttavat merkkijonojen osoitteita, saattavat aiheuttaa yllätyksiä huolimattomasti käytettyinä:
	char rivi[80],*p1,*p2; int j;
	...
	lue_jono(N_S(rivi));
	p1 = palanen(rivi," ",&j);
	...                         /* Täällä ei viitata p1:een! */
	lue_jono(N_S(rivi));
	p2 = palanen(rivi," ",&j);
	if ( strcmp(p1,p2 ) ...     /* ON AINA p1=p2!!! */

23.2.17 Käytetään vahingossa pilkkuoperaattoria

Vaikka seuraava kutsu onkin C- kielessä laillinen, tekee se aivan muuta kuin käyttäjä olisi ajatellut:
	double d;
	d = 2,5;    // Laskee 2 ja 5 ja sijoittaa d = 2; 	


previous next Title Contents Index