* ohjelman jakaminen osiin
* ehdollinen kääntäminen
* makefile
* projekti
Kommentoimme jatkossa aliohjelmat huolellisesti. Seuraavana on kuvattu eräs tapa. Tietenkin on miljoona muutakin tapaa, mutta ainakin seuraavan tavan ajatus kannattaa pitää mielessä.
//*************************************************************************** int // funktio( // Funktion eri paluuarvot // p1 ,// s Selitys Parametrin merkitys p2 ,// t Selitys p3 // s Selitys ) /* ** Funktiolla ... Lyhyt (mutta riittävä) kuvaus siitä, MITÄ funktio tekee ** ** Globaalit: Globaalit muuttujat joita ohjelma tarvitsee ** Muuttuu: Muuttujat (yleensä glob) joita muutetaan ja eivät ole param.listassa ** Syöttö: Mistä saadaan syöttö ** Tulostus: Mihin tulosteet ** Kutsuu: Mitä aliohjelmia kutsutaan (usein tarpeeton rivi) ** Tekijä: Vesa Lappalainen ** Pvm: 26.01.1997 ** Algoritmi: Miten funktio tekee tehtävänsä ** Esimerkki: Muutama selventävä esimerkki - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ { }Lohkokommenteissa (kommenttia monta riviä peräkkäin) ** pyritään saamaan alkamaan sarakkeesta 1. Miksikö? Tällöin ohjelman dokumentointia voidaan automatisoida siten, että kerätään kustakin tiedostosta ne rivit, joissa on ** 1. sarakkeessa.
Parametrilistan kommenteissa on esimerkiksi seuraavat symbolit:
s = syöttömuuttuja (tulee aliohjelmaan) t = tulosmuuttuja (aliohjelma muuttaa tämän arvoa) s,t = parametri on sekä syöttö, että tulosMikäli joku muuttuja on puhtaasti syöttömuuttuja, mutta siitä huolimatta sen arvo muuttuu (esim. jotkin pätkimisaliohjelmat), kannattaa parametri mainita vielä Muuttuu: kohdassa.
Globaalit: globaalit muuttujat joita aliohjelma tarvitsee Muuttuu: ne muuttuvat arvot jotka eivät ole parametrilistassa = lähinnä globaalit muuttujat (hyvä aliohjelma ei näitä tarvitse) Syöttö: pääte, tiedostot Tulostus: pääte, tiedostot Kutsuu: mitä EI- tunnettuja aliohjelmia tai makroja tarvitaan C:n standardikirjastojen kutsuja ei tarvitse luetella jolleivat ne ole kovin eksoottisia, ei myöskään samassa tiedostossa tai projektissa olevia "tunnettuja" aliohjelmia Tekijä: kuka tehnyt (jos triviaali aliohjelma, voidaan jättää pois) Pvm: milloin tehty ja myös muutokset Algoritmi: algoritmin karkea kuvaus ja mahdolliset lähdeviitteet Esimerkki: esimerkkejä, joilla havainnollistetaan aliohjelman toimintaa ja saadaan itsellekin kirjattua ylös se mitä ollaan tekemässä
Näistä tarpeettomat (triviaalit tai tyhjiksi jäävät) rivit poistetaan.
i=5; /* sijoitetaan i on 5 */ /* TURHA! */
i=5; /* aloitetaan puolestavälistä */
typedef struct { int jasen_maara; int nayton_koko; ... } globaalit_tyyppi; globaalit_tyyppi GLOBAALIT; ... GLOBAALIT.jasenmaara=5;
Tämän takia aliohjelmat kirjoitetaan omaksi tiedostokseen, vaikkapa nimelle mjonot.c. Aliohjelmien otsikot (esittelyrivit) ja muut kaikille tarkoitetut määrittelyt kirjoitetaan tiedostoon mjonot.h.
/* Makro jolla saadaan muuttujan nimi ja koko peräkkäin */ #define N_S (nimi) nimi,sizeof(nimi) /****************************************************************************/ /* vakiot syötön onnistumiselle */ #define SYOTTO_OK 2 #define EI_MAHDU 1 #define OLETUS 0 #define TIEDOSTO_LOPPU - 1 #define VIRHE_SYOTOSSA - 2 extern char *VALIMERKIT; /****************************************************************************/ char *tee_jono(char *); int f_lue_jono(FILE *, char *, int); int lue_jono(char *, int ); ... int wildmat(register char *, register char *);Tämä tiedosto on ehkä helpointa tehdä kopioimalla kaikki alkuperäiset aliohjelmat tiedostoon ja tämän jälkeen tuhoamalla aliohjelmien suoritusosat. On olemassa myös ohjelmia, jotka tekevät tiedostosta näitä prototyyppitiedostoja.
Huomattakoon, että myös seuraavia muotoja voi esiintyä .h - tiedostoissa:
int lue_jono(char *jono, int max_pituus); ... extern int lue_jono(char *jono, int max_pituus); ... int lue_jono(char *, int); ... extern int lue_jono(char *, int);Funktioiden yhteydessä extern oleminen tai puuttuminen ei kuitenkaan haittaa mitään. Toisin on muuttujien kanssa!
Huom! Nyt funktioiden esittelyjen perään täytyy muistaa laittaa puolipiste!
/****************************************************************************/ /* ** M J O N O T . C ** ** Yleisiä merkkijonojen käsittelyyn liittyviä aliohjelmia. ** ... ** Aliohjelmat: ** tee_jono - luo uuden merkkijonon jonne jono kopioidaan ** kopioi_jono - kopioi kork. annetun määrän merkkejä ... ** merkkijonossa ** wildmat - vertaa onko sana == maski, missä maskissa ** voi olla jokeri- merkkejä ... */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include "mjonot.h" char *VALIMERKIT=" .,- ;:?!"; /****************************************************************************/ char /* = jonon kopion osoite */ *tee_jono( /* NULL = ei voida kopioida */ char *jono /* s Kopioitava merkkijono */ ) ... ... Kuten ennenkin ... ... /****************************************************************************/ int /* */ wildmat( /* 0 = jono täsmää maskiin */ /* 1 = jono ei täsmää maskiin */ const register char *s ,/* s Tutkittava jono */ const register char *m /* s Maski, johon jonoa verrataan */ ) /* ** Funktiolla tutkitaan täsmääkö annettu jono verrattavaan maskiin. ** Maski saa sisältää seuraavia erikoismerkkejä: ** * vastaa 0-n merkkiä ** ? vastaa mitä tahansa yhtä merkkiä ** ** Algoritmi: Kysymysmerkki ja tavallinen kirjain normaalisti ** Jos tulee vastaan tähti joka ei ole jonon lopussa, ** niin ongelmahan on oikeastaan ** (koska tähän asti kaikki on ollut oikein) ** "Onko loppujono sama kuin toisen jonon loppu JOSTAKIN ** kohdasta alkaen"? ** Siis kokeillaan sovittaa loppujonoa aliohjelman itsensä ** (rekursio) avulla kaikkiin mahdollisiin loppupaikkoihin. ** Esimerkki: s = "Kissa" m = "*ss*" -> 0 ** = "*ss" -> 1 ** Vika alkuperäisessä algoritmissa: ** Jos m="*a" ja s="" ja s[1]!=0 (mikä tietysti sallittua!) ** niin vastaus oli 0. ** Korjattu 29.1.1994/vl muuttamalla rekursion jälkeinen ** if (!*++s) return 1; ** muotoon ** if (!*s || !*++s) return 1; ----------------------------------------------------------------------------*/ { while (*m) { /* Jos kokeiltavaa jonoa on jäljellä */ if (*m == '?') { /* Jos kysymysmerkki, niin kaikki kelpaa */ if (!*s) return 1; /* paitsi jos jono on loppunut! */ } else if (*m == '*') { /* Jos * niin katsotaan onko viimeinen */ if (*++m) /* Jollei * viimeinen, niin kokeillaan */ while (wildmat(s, m)) /* loppujonoa jokaiseen eri paikkaan. */ if (!*s || !*++s) return 1;/* Jos jono loppuu kesken ei täsmää! */ return 0; /* Muuten samat (* viimeinen tai loppujono*/ } /* täsmäsi) */ else if (*s != *m) /* Jos samat merkit niin tietysti OK! */ return 1; s++; m++; /* Kummassakin jonossa eteenpäin */ } return *s; /* Jos jono loppui yhtäaikaa, niin OK! */ } ...
Olemme lisänneet kirjastoon muutamia uusia aliohjelmia, joiden tarpeellisuuden totesimme jo kerhorekisterin suunnitteluvaiheessa.
Kirjastossa on mm. wildmat- aliohjelma merkkijonojen samaistamiseksi, kun jonossa saa esiintyä jokerimerkkejä * ja ? (vrt. MS- DOS).
aluksi s = "Kissa"; *s='K' s++ - > s = "issa" *s='i' s++ - > s = "ssa" *s='s'
#include <stdio.h> #include "mjonot.h" /****************************************************************************/ /* testiohjelmat: */ void lue_jono_testi(void) { /* täytä! */ } int f_lue_jono_testi(void) { /* täytä! */ } void lue_kok_testi(void) { /* täytä! */ } void lue_jono_oletus_testi(void) { int paluu; char st_ole[50],st[50] = "Ankka Aku"; printf("Testi loppuu, kun painat ^Z.\n"); printf("1234567890123456789012345678901234567890\n"); do { strcpy(st_ole,st); paluu=lue_jono_oletus("Anna jäsenen nimi",19,33,st_ole,st,50); poista_tyhjat(st); jono_alku_isoksi(st); } while ( paluu >= OLETUS ); } void wild_testi(void) { char jono[80],maski[80]; while (!feof(stdin)) { printf("Anna jono ja maski>"); scanf("%s %s",jono,maski); /* Älä käytä oikeasti!!!! */ printf("%d <- %s %s\n",wildmat(jono,maski),jono,maski); } } int main(void) { #if 0 lue_jono_testi(); #endif #if 0 f_lue_jono_testi(); #endif #if 0 lue_kok_testi(); #endif #if 0 lue_jono_oletus_testi(); #endif #if 1 wild_testi(); #endif return 0; }Huomattakoon, että nyt täytyy olla lainausmerkit lauseessa
#include "mjonot.h"Jotta ohjelma voidaan kääntää, tarvitsemme projektin tai MAKEFILEn. Kunnes olemme ne käsitelleet, voidaan "kerho.h" tilapäisesti korvata "kerho.c". Normaalisti c- tiedostoja EI SAA "includeta"!
Kyseessä on esiprosessorin direktiivi, jolla määrätään minkä osan esiprosessori antaa varsinaiselle kääntäjälle käsiteltäväksi.
Tällä periaatteella voidaan rakentaa ohjelmia, joissa tekstit tulevat eri kielillä (englanti, ruotsi, suomi) sen mukaan, mitä ohjelmassa on määritelty.
Samalla tempulla teimme myös lue_merkki - aliohjelman toimimaan joko Turbo- C:ssä tai standardi ANSI- C:ssä sen mukaan onko määritelty sana __TURBO__ (huomaa kaksi _ kummallakin puolella!):
#ifdef __TURBOC__ # define GETCH # include <conio.h> #endif ... char lue_merkki(void) { #ifdef GETCH ... /* lukeminen conio- kirjaston avulla */ #else ... /* lukeminen standardikirjaston avulla */ #endif }Usein kääntäjään liittyy kääntäjän määrittelemiä symboleja, joiden olemassaoloa testaamalla voidaan tehdä laiteriippuvia osia. Juuri tällainen oli __TURBOC__. Ehtona voi olla myös jokin oma sana kuten edellä GETCH. Usein itse määriteltyjä sanoja käytetään estämään .h- tiedoston moninkertaiset esiintymät:
... #ifndef MJONOT_H #define MJONOT_H ... /* varsinainen h- tiedoston sisältö */ #endifNyt voidaan pääohjelmassa kirjoittaa
#include "mjonot.h" /* Lukee mjonot.h - tiedoston kokonaan */ #include "mjonot.h" /* Ei tee mitään koska MJONOT_H määr. */
int a,i; ... a = ynnaa_yksi(i);voidaan suluista päätellä aina, että kyseessä on aliohjelman kutsu. Loppu onkin kääntäjälle vaikeampaa. Aliohjelmalle pitäisi välittää oikean tyyppinen parametri. Kääntäjä voi yrittää arvata, että esimerkin aliohjelma olisi muotoa
int ynnaa_yksi(int luku);koska sille välitetään parametrina int - tyyppinen luku. Mikäli arvaus osoittautuisi vääräksi ja aliohjelma olisikin esitelty
double ynnaa_yksi(double luku);olisi käännös mennyt väärin arvauksen kohdalta, koska kokonaisluku ja reaaliluku talletetaan aivan eri tavalla. Virhe voidaan normaalisti välttää sillä, että kirjoitetaan (esitellään) aliohjelma ennen sen käyttöä.
Aliohjelmakirjastojen kanssa on hankalampaa. Jottei aliohjelmia tarvitsisi esitellä useita kertoja, on aliohjelman parametrien tyypit mahdollista esitellä prototyypillä
int lue_jono(char *jono, int max_pituus);Kun kaikkien tarvittavien aliohjelmien prototyypit laitetaan .h - tiedostoon, ja tämä tiedosto luetaan esiprosessorin
#include "mjonot.h"direktiivillä käännöksen aikana koodin mukaan, saadaan kullekin aliohjelmalle oikea tyyppi.
Vastaavasti koko ohjelmaan saattaa tulla eri tiedostojen välisiä globaaleita muuttujia, ja kuitenkin muuttujan saa esitellä vain yhdessä paikassa. Jotta kaikki tiedostot näkisivät globaalit muuttujat, voidaan ne esitellä header- tiedostossa extern - määreellä. Itse varsinainen muuttuja varataan sitten siinä TASAN YHDESSÄ tiedostossa, johon se "eniten" kuuluu. Tällöin muuttujaan ei tule extern - määritystä.
kerho.h: extern const char *VERSIO; ... kerho.c const char *VERSIO = "18.2.1992" .. muissa tiedostoissa esimerkiksi: printf("Versio %s\n",VERSIO);Siis yleensä header- tiedostoon tulee
Kukin kokonaisuuteen kuuluva osa voidaan kääntää erikseen. Lopuksi nämä osat linkitetään yhteen, jolloin muodostuu haluttu valmis ohjelma.
Ongelmana on sitten muistaa kääntää uudestaan aina kaikki muuttuneet osat. Tietysti voisimme aina varmuuden vuoksi kääntää kaikki osat, mutta tämä veisi turhaa aikaa.
Tätä varten on MAKE- niminen ohjelma, jolle kirjoitetaan ohjetiedosto siitä, mitkä tiedostot muodostavat valmiin työmme ja miten eri tiedostot riippuvat toisistaan.
Esimerkiksi mikäli mjonot.c tiedostoon tulee muutos, tarvitsee se kääntää sekä suorittaa linkitys uudestaan. Mikäli mjonot.h muuttuu, pitää sekä mjonot.c ja t_mjonot.c kääntää uudelleen.
# makefile kerho- ohjelmaa varten kerho: kerho.o kerhorak.o kerhoets.o kerhotar.o kerhoali.o\ kerhotal.o kerhoopt.o kerholra.o \ mjonot.o pvm.o help.o cc - Aa - o kerho *.o - lm .c.o: cc - c - Aa $*.cHuomattakoon, että cc- alkuisten (c- kääntäjän kutsu Unixissa) rivien on oltava sisennetty TAB- näppäintä käyttäen. Seuraavassa hieman selityksiä:
kerho: - tiedosto kerho riippuu näistä .o (obj) tiedostoista cc ... - jos jossakin .o:ssa on uudempi päiväys kuin kerho tiedostossa, luodaan uusi kerho tällä komennolla - Aa = ANSC- C käännös - o = tulostiedosto (output) - lm = linkitetään matematiikkakirjasto .c.o: - .c tiedostoista tehdään .o tiedosto seuraavasti jos .c:n päiväys on uudempi kuin .o:n.Käännös ja ajaminen suoritetaan seuraavasti
$ make[RET] ... $ kerho[RET] ... Tällä ohjelmalla...
Projektin merkitys on vastaava kuin MAKE- tiedostonkin: pitää kirjaa siitä, mitkä tiedostot kuuluvat kokonaisuuteen. Toisaalta projektitiedostoa tarvitaan erityisesti linkityksessä.
Mikäli merkkijonontestaus esimerkkimme mjonot.c käännettäisiin irrallisena, syntyisi tiedosto mjonot.obj. Tähän ei vielä projektia tarvita. Vastaavasti voidaan kääntää pääohjelma t_mjonot.c. Vieläkään ei projektia tarvittaisi. Ongelmaksi tuleekin linkitys. Päämodulissa viitataan aliohjelmaan lue_jono. Mistä linkittäjä arvaisi etsiä tätä aliohjelmaa tiedostosta mjonot.obj?
Tämä tieto löytyy projektista, johon esimerkissämme mainittaisiin ne C- tiedostot, jotka kääntämisessä tarvitaan, eli
t_mjonot.c mjonot.cMikäli C- kielistä koodia ei ole olemassa, voidaan projektiin usein ilmoittaa myös jo käännettyjä tiedostoja, eli esimerkiksi
t_mjonot.c mjonot.objSiis linkittäjä etsii aliohjelmia ja globaaleita muuttujia projektissa mainituista tiedostoista C- kielen standardikirjastojen lisäksi.
Esimerkiksi Turbo- C:ssä valitaan aluksi uuden projektin luonti. Tämän jälkeen projekti- ikkunaan osoitetaan ne *.c tiedostot, joita haluamme käyttää. Voidaan myös lisätä *.obj tai *.lib tiedostoja, mikäli lähdekielisiä tiedostoja ei ole.
Header- tiedostoja (*.h) ei koskaan laiteta projektiin!
Itse kääntäminen suoritetaan sitten normaalisti.
Projektin perusteella kääntäjä ensin kääntää kaikki ne .c- tiedostot, joista ei vielä ole .obj - tiedostoa tai .obj- tiedoston päiväys on vanhempi kuin .c- tiedoston. Tämän jälkeen linkitetään kaikki projektissa mainitut .obj- tiedostot yhdeksi .exe- tiedostoksi.
Huomattakoon että projektissa tulee olla tasan yksi .c- tiedosto, jossa on main- funktio.
Rungossa osa aliohjelmista on hyvin yleisluonteisia tulostus/luku aliohjelmia ja nämä kaikki voitaisiin siirtää vaikkapa tiedostoon nimeltä ioali.c. Muuten jako kannattaa tehdä luokkapohjaisesti:
class cLuokka { ... };Varsinainen todellinen koodi tulee sitten vastaaviin .cpp - tiedostoihin.
Lisäksi käytämme tietysti valmiita kirjastoja sekä aikaisemmin tehtyä merkkijonokirjastoa mjonot.
On turha toivo, että keksisimme kaikki määritykset ja aliohjelmat kerralla. Tehtävää täytyy hahmotella palanen kerrallaan. Kun jokin homma tuntuu venyvän liian pitkäksi tai monimutkaiseksi, määrittelemme tehtävän useampaan alatoimintoon ja toteutamme nämä toiminnot sitten aliohjelmina/luokkina. Aliohjelmien/metodien parametrit saattavat vielä myöhemmin muuttua, kun huomataan saman tehtävän käyvän sekä tähän että tuohon tehtävään. Esimerkiksi etsiminen ja selailu käy samantien myös korjailuun ja poistoon. Ainoana erona on, että korjaus- ja poistonäppäimet eivät ole pelkässä etsimisessä sallittuja.