|
|
|
| Koodausohjeita |
|
Ohjelmankehityssilmukka
|
|
Ohjelmaa
kirjoitetaan yleensä silmukassa: kirjoita tai muuta koodia,
käännä se, kokeile ohjelmaa. Tämä silmukka on yleensä syytä
pitää melko lyhyenä. On harvoin järkevää kirjoittaa suuria
määriä koodia kerrallaan, vaan on yleensä parempi kirjoittaa ja
kokeilla pieniä ohjelman osia. Mikäli ohjelmaa muutetaan monesta
kohtaa ja kokeillaan ohjelmaa vasta kaikkien muutosten jälkeen,
eikä ohjelma toimi, on hankala selvittää, mikä muutos rikkoi
ohjelman. Jopa syntaksivirheet on helpompi korjata, mikäli niitä
on vähän.
Kaikkein pahin tapa on kirjoittaa ohjelma ensin kokonaisuudessaan
ja viikkoa ennen kuin ohjelman on oltava valmis, ilmoittaa ''ohjelma
on valmis, se tarvitsee enää vain kääntää''.
Ohjelmointi kokeilun ja erehdyksen kautta on uhkapeliä. Mieti
ensin, mitä on tarkoitus tehdä, mikä on paras tapa tehdä se,
sitten tee se niin. Korjaa virhe vasta sen jälkeen, kun tiedä täsmälleen,
mikä virheen aiheuttaa ja ole aina tietoinen, millä tavalla
korjaus korjaa virheen. On erittäin helppoa muutella koodia
sattumanvaraisesti ja saada virhe häviämään; virhettä ei
kuitenkaan ole korjattu, se on vain piilossa.
|
|
Koodin kirjoittamisjärjestys
|
|
On yleensä
järkevämpi suunnitella ohjelma ''ylhäältä alas'', eli jakaa
kokonaisuus pienempiin, helpommin hallittavissa oleviin osiin (ja
nämä osat tarvittaessa edelleen pienempiin osiin). Tämä johtaa
yleensä parempilaatuisiin ohjelmiin kuin ''alhaalta ylös'' tai
''sieltä täältä'' -suunnittelumenetelmät.
Ohjelmakoodia sen sijaan voi olla järkevä kirjoittaa muutenkin
kuin aloittaen pääohjelmasta. Jotta ohjelmankehityssilmukan voisi
pitää lyhyenä ja jotta ohjelmaa voisi jo sen kirjoitusaikana
testata, se kannattaa kirjoittaa aloittaen itsenäisistä paloista.
Tämä voi hyvinkin tarkoittaa esimerkiksi sitä, että ensin
toteutetaan ohjelmasta alimman tason osat.
Ohjelmakoodi kannattaa kirjoittaa heti oikein. Huonon version
kirjoittaminen aiheuttaa monikertaisesti työtä, koska huonokin
versio pitää saattaa edes jotenkuten toimivaksi ja sen jälkeen
hyvän version joutuu kuitenkin kirjoittamaan kokonaan uusiksi. Tämä
ei kuitenkaan tarkoita, etteikö prototyyppejä pitäisi tehdä ja käyttää,
tai useampia eri lähestymistapoja tutkia ja verrata käytännössä,
vaan esimerkiksi sitä, että virheenkäsittely koodataan mukaan
heti, ei vasta loppuvaiheessa.
|
|
Työkalujen oikeasta käytöstä
|
|
Ohjelmoijan
tärkeimmät työkalut ovat editori ja kääntäjä. Editorin
käytöstä ei tässä sen enempää.
Modernit kääntäjät osaavat auttaa ohjelmoijaa monin tavoin.
Esimerkiksi ne osaavaat analysoida koodia ja varoittaa monista
yleisistä virheistä. Varoitukset on usein erikseen käännettävä
päälle. On harvoin järkevää pitää varoituksia pois kytkettynä
(pois lukien jotkin varoitukset, jotka ovat hyödyllisiä vain
tietyissä tilanteissa). Toisaalta on aina pyrittävä siihen, että
kääntäjä ei varoita mistään; varoituksiin on yleensä
suhtauduttava kuten virheilmoituksiin.
Erityisesti on C-koodia käännettäessä käytettävä
varoituksia prototyypeistä. Kielen määritelmän mukaan on
sallittua kutsua funktiota, jolle ei ole annettu prototyyppiä. Käytännössä
tämä on miltei aina virhe. Useimmat kääntäjät osaavat
varoittaa tälläisestä kutsusta.
Mikäli ohjelma on jaettu useampaan tiedostoon, on myös
toivottavaa, että kääntäjä varoittaa sellaisista funktioista,
joille ei ole annettu prototyyppiä, kun ne määritellään. Tämä
on tärkeää sen takia, että muuten on vaikea tarkistaa, että
prototyyppi ja määrittely ovat yhtäpitäviä. Mikäli koko
ohjelma on yhdessä tiedostossa, on usein riittävää, että
aliohjelmat järjestetään siten, että määrittely tulee ennen
kutsua.
|
|
Tunnusten valinta
|
|
Tunnusten valinta on
tärkeää, koska hyvin valitut tunnukset helpottavat ohjelman
ymmärtämistä ja huonot vaikeuttavat sitä. Hyvä tunnus on
kuvaava, eli tunnus kertoo vakion, muuttujan, tyypin tai aliohjelman
tarkoituksen. Moni tunnus pärjää pelkällä hyvällä nimellä,
eikä vaadi kommenttia selittämään olemassaolonsa tarkoitusta.
|
|
Tunnsten kuvaavuus
|
|
Koska tunnuksia käytetään eri tavoin, ei kaikkien tunnusten
tarvitse olla yhtä kuvaavia. Esimerkiksi silmukkamuuttujien
käyttö on itsestään selvää jokaiselle vähänkään
ohjelmointia harrastaneelle, eikä silmukkamuuttujan nimen tarvitse
olla kovinkaan kuvaava. Itse asiassa, koska asia kuitenkin on
selvä, on pitkä silmukkamuuttujan nimi häiritsevä, koska pitkä
nimi on hankala lukea ja kiinnittää turhan paljon huomiota
itseensä. Samoin muutkin paikalliset muuttujat lyhyissä,
yksinkertaisissa aliohjelmissa on yleensä parempi nimetä lyhyesti,
vaikkapa käyttäen yleisesti tunnettuja lyhennyksiä.
etsittyjen_lukumaara = 0;
for (tutkittava = 0; tutkittava < taulukon_koko; ++tutkittava)
if (taulukko[tutkittava] == etsittava)
++etsittyjen_lukumaara;
lkm = 0;
for (i = 0; i < taulukon_koko; ++i)
if (taulukko[i] == etsittava_alkio)
++lkm;
Tunnuksia i, j ja k käytetään
usein silmukkamuuttujina, laskureina tai muuten apumuuttujina.
Tunnukset n ja m ovat yleensä lukumääriä.
Monilla sovellusaloilla on vakiintuneita käytäntöjä suureiden
nimeämiseksi; näitä on syytä noudattaa.
Kaikkien tunnusten ei kuitenkaan ole hyvä olla lyhyitä.
Paikallisten muuttujien lyhyyden eräänä puolusteluna on se, että
ne ovat yleensä apumuuttujia (joten niihin ei pidä joutua kiinnittämään
kovin paljoa huomiota) ja lisäksi niitä käytetään vain pienessä
osassa ohjelmaa (olettaen että aliohjelma on lyhyt, kuten hyvä
on). Globaaleja tunnuksia sen sijaan käytetään -- tai ainakin
voidaan käyttää -- missä tahansa kohtaa ohjelmassa. Tällöin
niiden tulee olla huomattavasti kuvaavampia kuin paikallisten
tunnusten, koska tunnuksen määrittely on hankalampi löytää.
|
|
Tunnusten yksikäsitteisyys eri moduleissa
|
|
Ohjelman voi usein jakaa alijärjestelmiin tai moduleihin. Jotta
eri moduleissa olevat nimet eivät menisi sekaisin, on jokaiselle
modulille valittava yksikäsitteinen etuliite, joka liitetään
ainakin jokaisen modulin ulkopuolelle näkyvän nimen eteen.
Esimerkiksi listojan käsittelevän modulin kaikki aliohjelmat nimetään
tyyliin list_create, list_next, jne., ei create_list,
next_in_list.
Kaikki tunnukset pyritään määrittelemään funktion tai
tiedoston sisäisiksi, mikäli niillä ei ole erityistä syytä olla
globaaleja.
|
|
Tunnusten kirjoitustapa
|
|
Tunnuksissa käytetään pääsääntöisesti vain pieniä
kirjaimia; sanat erotetaan alaviivalla. Tunnuksissa pyritään välttämään
muita kuin ohjelmaprojektin yhteydessä muutenkin käytettäviä
lyhenteitä.
Vakioiden nimeämiseen käytettävät makrot kirjoitetaan
kokonaan suurilla kirjaimilla. Funktionomaiset makrot kirjoitetaan
kokonaan pienillä kirjaimilla. Makrojen käyttöä muihin
tarkoituksiin on vältettävä.
Tietotyypit (typedef) voi nimetä isolla
alkukirjaimella.
|
|
Kommentointi
|
|
Kommentoinnin tarkoituksena on kertoa sellaista, mitä koodista
itsestään ei näy. Kommentoinnin tarkoituksena ei
siis ole toistaa sitä, mitä koodi tekee.
Kommentit on syytä kirjoittaa käyttäen hyvää kieltä: niin
kirjoitusasultaan kuin kieliopiltaan ja kirjoitustyyliltään
kommenttien tulee olla korkeatasoisia. Kommentit saavat olla lyhyitä
ja muistuttaa toisiaan. Mikäli kaksi aliohjelmaa tekee
samantapaisia asioita, on jopa hyvä, että niiden kuvauksetkin ovat
samantapaisia. Sen sijaan ei ole järkevää pelkästään viitata
toisen aliohjelman kommenttiin, koska se vaatii lukijalta selaamista
ja ohjelman muuttajalta tietoa siitä, että toisen aliohjelman
kommentti itse asiassa kuvaakin kahta erillistä aliohjelmaa.
Kommenttien kirjoittamisessa on erityisesti vältettävä
passiivia. Älä siis kirjoita ''lajitellaan talukko päivämäärän
mukaan'', vaan mieluummin ''lajittele taulukko päivämäärän
mukaan'' (imperatiivi), tai vieläkin mieluummin ''aliohjelma
lajittelee taulukon päivämäärän mukaan'' (kuvaus). Sen sijaan
tieteellisissä teksteissä usein käytettävä kertomuksellinen
muoto ''lajittelemme taulukon päivämäärän mukaan'' on
kelvollinen.
Mikäli jokin asia voidaan kertoa koodina, jonka kääntäjä
tarkistaa tai joka tarkistaa asioita ajon aikana, ei ole syytä
kertoa asiaa (pelkästään) kommenttina. Automaattiset tarkistukset
auttavat löytämään virheitä automaattisesti; kommentit auttavat
korkeintaan silloin, jos joku ne lukee. C-kielessä on valmiita työkaluja,
esikääntäjä ja assert, tarkistusten lisäämiseksi
koodiin; monissa nykyaikaisissa kielissä on samantapaisia tai
kehittyneempiä työkaluja. Kaikissa kielissä on kuitenkin
mahdollista kirjoittaa tarkistuksen tekevä koodi suoraan muun
koodin sekaan.
Kommentteja on pidettävä ajan tasalla. Mikäli koodia
muutetaan, on aina samalla päivitettävä vastaava kommentti.
Kommentteja ei kuitenkaan saa kirjoittaa vasta jälkikäteen, kun
ohjelma muuten on valmis, koska tällöin kaikki ohjelman
luomisvaiheen perustelut ovat kaikonneet muistista. Kaiken
kukkuraksi, mikäli kommentit kirjoittaa ensin, ohjelman rakennetta
tulee ajatelleeksi tarkemmin, joten ohjelman laatukin on usein
parempi.
Kommenteissa käytetään jotakin kieltä, jota lukijakunta ymmärtää.
Kaikissa kommenteissa käytetään samaa kieltä. Kommenttien kielen
ja tunnusten kielen on hyvä, mutta ei välttämätöntä, olla
sama.
|
|
Aliohjelmien kommentointi
|
|
Ennen jokaista aliohjelmaa on kirjoitettava kommentti, joka
kertoo mitä funktio tekee, miten sitä kutsutaan,
sen kytkennät, sekä sen olettamat ja tekemät tilanvaraukset
(kuka tekee ja kuka vapauttaa). Sen sijaan toiminnasta ei tarvitse
kertoa, mikäli algoritmi ja koodi ovat yksinkertaisia.
Funktioiden palauttama arvo sekä sen laskemistapa on kerrottava.
Jokaisesta parametrista on kerrottava sen tarkoitus ja luonne
(in, out, inout), sekä mitä aliohjelma olettaa parametrista
kutsuttaessa (eli minkälaisia arvoja aliohjelma olettaa saavansa)
ja mikä on parametrin arvo kutsun jälkeen. Tyyppi selviää
ohjelmakoodista, mutta kaikenlaiset rajoitukset on selvitettävä,
esimerkiksi mikäli kokonaislukutyyppiselle muuttujalle kelpaavat
vain positiiviset arvot.
Jokaisesta aliohjelman käyttämästä globaalista muuttujasta,
tiedostosta ja muusta aliohjelman ulkopuolisesta oliosta on
kerrottava vastaavat asiat kuin parametreista.
Aliohjelman kommentin tarkoituksena on siis selostaa tarkasti,
miten aliohjelmaa käytetään. Tarkoituksena on, että pelkästään
aliohjelmaa edeltävän kommentin ja aliohjelman otsikon perusteella
aliohjelmaa pystyy käyttämään muutkin, kuin sen alkuperäinen
tekijä.
On usein järkevää kirjoittaa aliohjelman otsikko ja kommentti
ensin, ennen lauseosaa. Kommenttia kirjoitettaessa joutuu miettimään
tarkkaan, miten aliohjelman pitäisi eri tilanteissa käyttäytyä,
joten koodin kirjoittaminen jälkeenpäin on helpompaa.
Alkukommentin voi muotoilla haluamallaan tavalla. Eräs hyvä
tapa on esitelty alla. Sen ideana on tehdä kommentista
visuaalisesti hyvin erottuva (vasemman laidan tähtirivi), sekä jäsentää
kaikki aliohjelmakommentit samalla tavalla ja samalla varmistaa, että
kaikki tarpeellinen tieto tulee mukaan. Visuaalista erottuvuutta voi
lisätä vielä piirtämällä tähtimerkeillä laatikko koko
kommentin ympärille, mutta tämä tekee kommentin muokkaamisesta
hankalaa.
/*
* Purpose: Greet the user.
* Arguments: `username' is the username of the user.
* Description: greet_user will output to the standard output
* a greeting to the user named in the `username'
* argument. It will make sure that the greeting has
* actually been output to the terminal by flushing all
* buffers (in stdio and the kernel), and will check for
* errors.
* Return: -1 for error, 0 for OK.
*/
int greet_user(const char *username) {
...
}
Aliohjelman sisältöä ei yleensä tarvitse kommentoida, mikäli
aliohjelma on melko lyhyt ja yksinkertainen, eikä koodissa ole
snobbailtu kielen yksityiskohtien hallinnalla. (Tämän voikin
asettaa tavoitteekseen koodia kirjoittaessaan.)
Loppusulkuja ei tarvitse kommentoida. Koodin rakenne osoitetaan järjestelmällisellä
sisennyksellä. Mikäli alkusulku ei näy samalla sivulla,
aliohjelma on liian pitkä ja monimutkainen ja se on strukturoitava
uudelleen. (Tähän on joitakin poikkeuksia, mutta harvemmin kuin
luulisi.)
|
|
Globaalit muuttujat, vakiot ja tietotyypit
|
|
Globaalit muuttujat, vakiot ja tietotyypit kommentoidaan
vastaavasti kuin aliohjelmat. On usein järkevää ryhmitellä
useampi muuttuja, vakio ja tietotyyppi yhteen, esimerkiksi
seuraavalla tavalla.
/*
* Purpose: The list of all possible items in the storage.
* Description: The items in the storage are identified by
* a key (field `key' below). Each record holds the
* number of items left in the storage, and the unit
* price of each (in cents).
*/
struct item {
int key;
int count; /* number of articles, >= 0 */
double price; /* price in cents, >= 0 */
};
#define MAX_ITEMS 32
struct item item_table[MAX_ITEMS];
Muuttujien sallittu arvoalue ja yksikkö on kerrottava.
|
|
Sisennykset
|
|
Koodin rakenne ilmaistaan visuaalisesti sisennyksillä.
Sisäkkäiset ohjausrakenteet sisennetään, jolloin on helppo
nähdä, mikä koodin osa ohjaa mitä muita osia. Ei siis näin:
void kanna_merkkijono(char *s) {
size_t i, j;
char c;
for (i = 0, j = strlen(s); i+1 < j; ++i, --j) {
c = s[i];
s[i] = s[j];
s[j] = c;
}
}
vaan näin:
void kanna_merkkijono(char *s) {
size_t i, j;
char c;
for (i = 0, j = strlen(s); i+1 < j; ++i, --j) {
c = s[i];
s[i] = s[j];
s[j] = c;
}
}
Sisennykset voi tehdä välilyönneillä ja/tai
tabulaattorimerkeillä. Tabulaattoriväli on yleensä 8, mutta sen
voi yleensä säätää haluamakseen (tabulaattorivälin muuttaminen
on kuitenkin harvoin järkevää). On kuitenkin muistettava, että tällöin
kaikkien koodin lukijoiden on myös säädettävä tabulaattorivälinsä
samaksi. Tabulaattoriväli ja sisennyksen suuruus eivät välttämättä
ole sama asia, monissa editoreissa ne voi säätää erikseen.
Sisennyksen suurus on yleensä 2--8 merkkiä. Suuruudella ei ole
kovin suurta väliä, kunhan se on sama koko ohjelmassa.
|
|
Muuta asettelusta
|
|
Koodirivit saavat olla korkeintaa 78 merkkiä pitkiä (kun
käytetään kahdeksan sarakkeen sarkaimia).
Aliohjelman muu ulkonäkö (välilyöntien ja tyhjien rivien käyttö,
sulkujen asettelu, jne) selvinnee seuraavasta esimerkistä. Muitakin
tapoja on ja niitä saa noudattaa, mutta on tärkeää noudattaa
samaa tapaa kaikkialla yhden projektin ohjelmakoodissa.
int foo(int bar) {
int i;
assert(bar != 0);
if (bar < 0) {
for (i = 0; i > bar; --i) {
while (globaali_muuttuja < 0) {
globaali_muuttuja += i;
printf("%d\n", i);
}
}
} else {
do {
--bar;
switch (bar % 3) {
case 0:
printf("0\n");
break;
case 1:
printf("1\n");
/*FALLTHROUGH*/
case 2:
printf("2\n");
break;
default:
assert(0); /* eeek! */
abort(); /* die! */
}
} while (bar > 0);
}
return bar;
}
|
|
Ehtofunktiot
|
|
Ohjelmissa on usein monimutkaisia ehtoja. Nämä voi usein
kirjoittaa omiksi funktioikseen, vaikka niitä käytettäisiinkin
vain kerran. Ehdon käyttökohta on tällöin lyhyt ja ytimekäs ja
ehtokin on usein helpompi kirjoittaa selkeästi. Lisäksi, kun
miettii miten välittää kaikki ehtofunktiolle välitettävä
tieto, ohjelman rakennekin voi selkiintyä.
Esimerkki: on käytettävissä tietue, jonka kenttiin on
talletettu päivämäärän eri osat (vuosi, kuukausi, päivä).
Tehtävänä on tulostaa kahdesta päivämäärästä aikaisempi,
kun päivämäärät on talletettu muuttujiin pvm1 ja pvm2.
if (pvm1.vv < pvm2.vv ||
(pvm1.vv == pvm2.vv &&
(pvm1.kk < pvm2.kk ||
(pvm1.kk == pvm2.kk && pvm1.pp < pvm2.pp))))
printf("%d.%d.%d\n", pvm1.pp, pvm1.kk, pvm1.vv)
else
printf("%d.%d.%d\n", pvm2.pp, pvm2.kk, pvm2.vv);
Tuota ehtoa ei hevin voi sanoa selkeäksi! Jos ehdon kirjoittaa
ehtofunktioksi, ratkaisu on huomattavasti selkeämpi.
int pvm_on_aikaisempi(Paivamaara pvm1, Paivamaara pvm2) {
if (pvm1.vv < pvm2.vv)
return 1;
else if (pvm1.vv > pvm2.vv)
return 0;
else if (pvm1.kk < pvm2.kk)
return 1;
else if (pvm1.kk > pvm2.kk)
return 0;
else
return pvm1.pp < pvm2.pp;
}
...
if (pvm_on_aikaisempi(pvm1, pvm2))
printf("%d.%d.%d\n", pvm1.pp, pvm1.kk, pvm1.vv)
else
printf("%d.%d.%d\n", pvm2.pp, pvm2.kk, pvm2.vv);
Itse ehtofunktio on hieman pidempi, mutta silti selkeämpi. Ehdon
käyttökohta on huomattavasti selkeämpi. Huomaa myös, että
ehtofunktio on kirjoitettu siten, että sitä on helppo käyttää
muissakin yhteyksissä ja että sen nimi on valittu siten, että se
kuullostaa ehdolta.
|
|
Testaamisesta ja luotettavuudesta
|
|
Testaaminen on oleellinen osa ohjelman kehitysprosessia. Ohjelmaa
on testattava koko sen kehityksen ajan. Suunnitteluvaiheessa
ohjelmaa testataan ''päässä''; koodausvaiheessa ohjelmaa
kokeillaan edes vähän sitä mukaan kuin koodia saadaan
kirjoitetuksi. Testaus ei siis ole kokonaan erillinen vaihe, vaikka
koodauksen valmistuttua voidaankin vielä pitää erillinen
perusteellisempi testausvaihekin. Testauksesta ei tässä
yhteydessä sen enempää.
Koodin luotettavuudella tarkoitetaan sen virhetilanteiden
huomaamista ja käsittelyä. Hyvä ohjelma huomaa kaikki
virhetilanteet ja käsittelee kunkin sille sopivalla tavalla,
kuitenkin aina niin, että käyttäjän data ja työ eivät häviä.
Kääntäen: ohjelma joka ei toimi näin on huono.
Virhetilanteiden huomaamiseksi on jokainen käsky tai
operaatio, joka palauttaa jonkinlaisen virhekoodin, tarkistettava.
Ne käskyt, jotka eivät palauta mitään virhekoodia joko eivät
voi epäonnistua tai niitä ei voi käyttää hyvässä ohjelmassa.
(Esimerkiksi ohjelmointikieli, joka ei salli ohjelman selviytyä
tulostusoperaation yhteydessä tapahtuvasta virheestä, ei
mahdollista hyvän ohjelman kirjoittamista ollenkaan, pois lukien
sellaiset ohjelmat, jotka eivät tulosta mitään.) Joissakin
kielissä virhetilanteiden tarkistaminen on toteutettu toisin,
esimerkiksi poikkeuksin (englanniksi exception), mutta sama periaate
pätee.
Kun virhe on huomattu, se pitää käsitellä. Vain harvat
virheet voi jättää huomiotta (tälläisten virheiden käsittely
on kommentoitava, jotta lukija ei luulisi virheen jääneen
vahingossa huomiotta). Virheen oikea käsittelytapa vaihtelee
ohjelmasta toiseen; yhdessä ohjelmassa voi riittää
virheilmoituksen tulostaminen näytölle ja ohjelman suorituksen
katkaisu, toisessa virheilmoitus pitää kirjoittaa lokitiedostoon
ja ohjelman pitää jatkaa suoritustaan kadottamatta tai vääristämästä
tietoja. Virheiden huomaaminen ja käsittely ei yleensä ole
helppoa, mutta se on välttämätöntä ja jotta se toimisi hyvin,
se pitää suunnitella mukaan ohjelmaan alusta asti.
|
|
assert ja asen käyttö
|
|
Ohjelman jokaisessa kohdassa on tiettyjä ehtoja, joiden
oletetaan olevan voimassa. Jos ne eivät ole, ohjelmassa on virhe,
yleensä pahanlaatuinen. Nämä ehdot voi C-kielessä helposti
kirjoittaa koodin sekaan assert-makrolla ja tarkistaa
ne automaattisesti. Jos esimerkiksi jonkin aliohjelman
osoitinparametri ei saa olla null-osoitin, se voidaan tarkistaa
helposti assert-makrolla:
#include <assert.h>
...
int something(char *s) {
assert(s != NULL);
/* loppuosa funktioita voi olettaa, että s ei ole NULL */
}
Mikäli parametrina annettu ehto ei ole voimassa, assert
keskeyttää ohjelman ja tulostaa ongelmallisen ehdon, sekä lähdekooditiedoston
nimen ja rivinumeron (nämä helpottavat vian etsintää).
Monimutkaiset ehdot kannattaa esittää useamman assert:n
avulla, mikäli mahdollista. Ei siis näin:
assert(s != NULL && *s != '\0');
vaan näin:
assert(s != NULL);
assert(*s != '\0');
Näin assert:n tulostamasta virheilmoituksesta näkee
heti, mikä osaehto ei toteutunut.
Silmukoilla on ns. silmukkaehto (englanniksi loop invariant),
jonka on oltava voimassa silmukan lauseosan alussa ja lopussa.
Esimerkiksi binäärihaussa silmukkaehto voisi olla:
while (left < right) {
assert(table[left] <= x);
assert(x <= table[right]);
mid = (left + right) / 2;
if (table[mid] == x)
left = right = mid;
else if (table[mid] < x)
left = mid + 1;
else
right = mid - 1;
}
assert(left > right || table[left] == x);
Tietorakenteille on hyvä kirjoittaa tarkistusfunktio, joka
tarkistaa edes yleisimmät tietorakenteen virhetilanteet
(esimerkiksi syklinen lista tai puu). Tälläistä funktiota on hyvä
käyttää assert:n kautta tietorakennetta käyttävän
funktion alussa (ja ehkä lopussa). assert:lla ei saa
ikinä tarkistaa muunlaisia ehtoja, vain ohjelman oikeellisuuden
kannalta tärkeitä ehtoja. Esimerkiksi syötteiden oikeellisuutta
ei saa tarkastaa assert:lla. Koodin korvaamisesta
tietorakenteella
Pitkän, tautologisen koodin voi usein korvata sopivalla
tietorakenteella ja lyhyemmällä koodilla, joka tulkitsee
tietorakennetta. Tyypillinen esimerkki on komentotulkin komentoja
tunnistava osa. Suoraviivainen tapa on tehdä se seuraavasti:
if (strcmp(line, "help") == 0)
help();
else if (strcmp(line, "exit") == 0)
do_exit();
else if (strcmp(line, "north") == 0 || strcmp(line, "n") == 0)
go_north();
...
else
unknown_command();
Tätä koodia ei kuitenkaan ole kovin miellyttävää muokata tai
lukea. Siitä saa paljon helpomman seuraavalla tavalla:
struct {
char *name;
void (*func)(void);
} commands[] = {
{ "help", help },
{ "exit", do_exit },
{ "north", go_north },
{ "n", go_north },
...
{ NULL },
};
int i;
for (i = 0; commands[i].name != NULL; ++i) {
if (strcmp(line, commands[i].name) == 0) {
commands[i].func();
break;
}
}
if (commands[i].name == NULL)
unknown_command();
|
|
Funktioiden rajapinnoista
|
|
Funktioiden on hyvä olla mahdollisimman löyhästi kytkettyjä
muuhun ohjelmaan, eli mahdollisimman itsenäisiä. Itsenäisyys
tarkoittaa tässä sitä, että muuta ohjelmaa voi muuttaa ilman,
että funktiota tarvitsee muuttaa ja päinvastoin.
Itsenäisyyteen vaikuttavat esimerkiksi seuraavat tekijät:
- Globaalien muuttujien käyttö.
- Itse määriteltyjen tyyppien käyttö.
- ...
Itsenäistä funktiota on usein helpompi käyttää uudestaan
toisessa ohjelmassa.
Rajapinnan on myös hyvä olla yksinkertainen. Näin kutsujan on
helpompi käyttää sitä.
Usein kannattaa yrittää määritellä funktion tehtävä siten,
että funktio ei voi epäonnistua. Näin kutsujan ei tarvitse
tarkistaa virhettä.
Mikäli funktio voi epäonnistua, virhekoodi palautetaan funktion
arvona ja kaikki muu tieto parametrien kautta. Yleensä on hyvä
pyrkiä noudattamaan samantapaista paluukoodia; esimerkiksi
kirjoittaja käyttää yleensä tapaa ``-1 on virhe, >=0 on
onnistunut'' (0 voi esimerkiksi olla syötteen loppu, >0 datan
luvun onnistuminen). Paluukoodiin ei ole hyvä sekoittaa muuta
tietoa, esimerkiksi luetun datan määrää, koska tällöin
toisaalta paluukoodin merkitys hämärtyy, toisaalta ongelmaksi voi
tulla paluukoodin arvoalueen riittämättömyys. Esimerkiksi UNIXin read-systeemikutsu
palauttaa -1 virhetilanteessa, 0 tiedoston lopussa ja >0 luetun
tiedon määränä. Tällöin on toisaalta virhetilanteessa mahdoton
tietää, kuinka paljon jo ehdittiin lukea, toisaalta luetun tiedon
määrä on korkeintaan puolet kokonaisluvun arvoalueesta, mikä ei
esimerkiksi 16-bittisissä järjestelmissä ole ollenkaan tarpeeksi.
|
|
Tilanvaraus
|
|
C-kielessä eräs rajapintasuunnittelun ongelma on tilanvaraus.
Vaihtoehtoja on lueteltu alla.
Staattinen muuttuja funktion sisällä
char *neliomj(int n) {
static char str[100];
sprintf(str, "%d", n*n);
return str;
}
Tässä ongelmana on se, että merkkijono muuttuu, kun funktiota
kutsutaan seuraavan kerran. Esimerkiksi seuraava kutsutapa tuottaa
ongelmia:
printf("eka: %s\ntoka:%s\n", neliomj(1), neliomj(2));
(Tässä ei edes ole mitään takeita siitä, tulostuuko
``1 1'' vai ``4 4''!)
Globaali muuttuja
char str[100];
...
void neliomj(int n) {
sprintf(str, "%d", n*n);
}
Tässä ei siis kannata edes palauttaa mitään, koska kutsuja
voi yhtä hyvin käyttää globaalia muuttujaa suoraan. Ainoa ero tämän
ja funktion sisäisen staattisen muuttujan kanssa onkin, että
kutsuja voi käyttää muuttujan nimeä.
Dynaamisesti varattu muistitila
char *neliomj(int n) {
char *p;
p = malloc(100);
if (p == NULL)
retu
sprintf(p, "%d", n*n);
return p;
}
Tässä ongelma on se, että kutsu voi epäonnistua. Kutsujan täytyy
siis aina tarkistaa, että kutsu onnistui. Lisäksi kutsujan täytyy
muistaa vapauttaa muistitila, muuten ohjelma hukkaa muistia.
Kutsuja varaa tilan ja välittää osoittimen parametrina
void neliomj(int n, char *buf, int max) {
int used;
used = sprintf(buf, "%d", n*n);
assert(used < max);
}
Tässä kutsuja on varannut muistitilan valmiiksi ja funktion
itsensä tarvitsee vain huolehtia siitä, ettei se ylitä varattua
muistitilaa. (sprintf:n tapauksessa ei ole mahdollista
rajata muistitilan käyttöä; tässä sen takia varmistetaan assert:lla,
ettei sprintf ylittänyt rajoja.)
Tämä on usein miellyttävin niin funktion kirjoittajalle kuin
sen kutsujalle. Mikään tapa ei ole kovin miellyttävä, koska joka
tapauksessa ainakin kutsuja joutuu tekemään jonkin verran töitä
vain saadakseen yhden vaivaisen funktiokutsun hoidettua. Tässä
tavassa on kuitenkin se toivo, että jos useampi funktio käyttää
samaa tapaa ja niitä käytetään saman merkkijonon muokkaamiseen,
ei kutsujan tarvitse huolehtia tilan varauksesta ja siihen
mahdollisesti liittyvästä virheentarkistuksesta kuin kerran.
|
|
Tehokkuudesta ja optimoinnista
|
|
Ohjelman
tehokkuus lähtee algoritmista. Huono algoritmivalinta johtaa
tehottomaan ohjelmaan ja kääntäen tehotonta ohjelmaa voi
parhaiten optimoida vaihtamalla parempaan algoritmiin.
Optimointia ei pidä tehdä säkki päässä, vaan ohjelman käyttäytymistä
on mitattava ennen ja jälkeen optimointiyrityksiä.
UNIXissa tähän on käytettävissä työkalut gprof ja
time.
Lars
Wirzenius, liw@iki.fi
|
|