5 1* {3 C-ohjelmointikurssi - Osa 2 {3 --------------------------- Ville-Pertti Keinonen {3Loogiset operaattorit C-kielen loogiset operaattorit, joita käytetään usein erityisesti vertailun yhteydessä, käsittelevät vertailuoperaattoreiden tavoin arvoja vain kahden- tyyppisenä: tosi ja epätosi. Loogisia operaatioita ei pidä sotkea vastaa- viin binäärioperaatioihin, jotka tekevät periaatteessa saman asian jokai- selle operandin/operandien bitille. Näitä loogisia operaattoreita ovat: && looginen ja : tosi vain, jos molemmat operandit ovat tosia. || looginen tai : tosi jos jompikumpi operandeista tai molemmat operandit ovat tosia. ! looginen ei : vain yksi operandi, operaatio on tosi, jos operandi ei ole tosi. Hieman esimerkkejä näistä: oletetaan kokonaislukumuuttuja a, jolla on jokin ennalta tuntematon arvo: if (a > 10 || a < 0) puts("muuttuja a on suurempi kuin 10 tai negatiivinen"); if (a <= 5 && a >= 2) puts("2 <= a <= 5"); if (!(a == 1 || a == 10)) puts("a ei ole 1 eikä 10"); Operaattoria '!' voidaan käyttää korvikkeena vertailulle nollaan: if (!a) puts("a on epätosi, eli nolla"); on sama kuin: if (a == 0) puts("a on epätosi, eli nolla"); Aivan vastaavasti: if (a) puts("a on tosi, eli ei nolla"); on sama kuin: if (a != 0) puts("a on tosi, eli ei nolla"); samoin kuin: if (!(a == 0)) puts("a on tosi, eli ei nolla"); Helppo tapa ajatella loogisia operaatioita on lukea ne aivan tavallisesti "ääneen", eli tulkita sanoiksi: Ehto: (a == 10 || a == 15) "a on 10 tai a on 15" Ehto: (a < 0 && a > -10) "a on pienempi kuin 0 ja a on suurempi kuin -10" Ehto: (!(a > 10 || a <= 0)) "ai ei ole suurempi kuin 10 eikä pienempi tai yhtä suuri kuin 0" Erona tavalliseen ihmisen logiikkaan on ainoastaan se, että "tai"-ehtoon sisältyy myös mahdollisuus, että molemmat ehdot ovat tosia. Vaikka kaikki ylläolevat esimerkit käsittelevätkin ainoastaan yhtä muuttu- jaa, voidaan toki verrata useampia muuttujia: if (a > 10 || b == a || c < 0) puts("joko a on suurempi kuin 10, b on yhtä suuri kuin a tai c on negatiivinen"); Toistaiseksi mainitsen vain, että loogiset operaatiot (paitsi '!') suorite- taan vertailu- ja laskuoperaatioiden jälkeen, joten tässäkään ei tarvinnut sulkeita. Myöhemmin, kun on käsitelty kaikki operaattorit, tulee näiden suoritusjärjestyksestä taulukko. {3Operaattoreiden lyhenteitä Laskennallisista operaatioista on olemassa lyhennemuotoja, joita voidaan käyttää, kun asetettaisiin laskun ensimmäisenä operandina olevaan muuttu- jaan laskun tulos. Yleisesti muoto on: = joka on sama kuin: = ( voi olla myös muu "modifiable lvalue" -ehdon täyttävä lauseke) Käytännössä lyhenteet näyttävät esimerkiksi tällaisilta: Lyhyt muoto "Normaali" muoto a += 10; a = a + 10; a -= 10; a = a - 10; a *= 10; a = a * 10; jne... Tällaisia lyhenneoperaattoreita ovat: +=, -=, *=, /=, %=, |=, &=, ^=, <<= ja >>=. Lyhenneoperaattori on itsessään kiinteä kielellinen elementti, jo- ten sitä EI voida kirjoittaa esimerkiksi näin: a + = 10; /* tämä EI toimi, ei edes käänny */ Ehkä vielä enemmän käytettyjä lyhenteitä ovat (post/pre)(increment/decre- ment)-muodot, jotka vastaavat yhteen- ja vähennyslaskuja, kun lisättävä/vähennettävä arvo on luku 1. Näillä on erikoiset merkitykset lau- sekkeen osana. Normaalilaskuja nämä vastaavat jokseenkin seuraavasti: ++a; /* preincrement */ tai: a++; /* postincrement */ on sama a:lle, kuin: a = a + 1; tai: a += 1; --a; /* predecrement */ tai: a--; /* postdecrement */ on sama a:lle, kuin: a = a - 1; tai: a -= 1; Erityisiä lausekkeen osina nämä ovat, koska muuttujan ja operaattorin arvo lausekkeessa vaihtelee sen mukaan, kummalla puolella muuttujaa operaattori on: /* alussa a on 5 */ b = a++; /* lopussa b on 5, a on 6 */ kun taas: /* alussa a on 5 */ b = ++a; /* lopussa b on 6, a on 6 */ ja vastaavasti: /* alussa a on 5 */ b = a--; /* lopussa b on 5, a on 4 */ ja: /* alussa a on 5 */ b = --a; /* lopussa b on 4, a on 4 */ Silloin kun tällainen muoto ei ole lausekkeen osana, on yhdentekevää kum- malla puolella operaattori on ja sen sijoitus on lähinnä "makuasia". (Poik- keuksena ovat tapaukset, jolloin operaattorin sijoitus vaikuttaa merkityk- seen. Sellaiset tapaukset eivät tosin tähän mennessä käsitellyillä tyy- peillä ole mahdollisia.) {3Silmukat C-kielessä on kolme hieman toisistaan eroavaa silmukkarakennetyyppiä: while () Suoritetaan lausetta toistuvasti niin kauan kuin lauseke on tosi. Lauseke testataan aina ennen lauseen suoritusta. Jos lauseke on epätosi heti, ei lausetta suoriteta ollenkaan. Esimerkkejä: a = 10; while (a++ < 100) puts("iteraatio"); Tämä tulostaa tekstin "iteraatio" 90 kertaa. a = 10; while (++a < 100) puts("iteraatio"); Tämä tulostaa tekstin "iteraatio" 89 kertaa. Hieman monimutkaisempi silmuk- ka voisi olla: a = b + 10; while (a > b) { if (b >= 5 || a == 9) b -= ++a; else --a; b += 10 - a; } Aika järjetön esimerkki, koska se ei varsinaisesti tee mitään. Toistamis- kertojen määrä riippuu b:n lähtöarvosta. for (; ; ) Ensimmäinen lauseke suoritetaan ennen silmukan aloittamista, toinen lauseke toimii silmukan ehtona ja kolmas lauseke suoritetaan aina lauseen jälkeen ennen ehtona olevan lausekkeen testaamista. Periaatteessa melkein mikä tahansa for-silmukka voidaan kirjoittaa while- silmukaksi muotoon: ; while () { ; } Ainoa ero vastaavaan for-silmukkaan olisi continue:n toiminta (jota käsi- tellään myöhemmin). Kaikki lausekkeet for-silmukassa ovat vapaaehtoisia. Jos jätetään lausek- keet 1 ja 3 tyhjiksi, on toiminta täsmälleen kuten while-silmukan. Jos lau- seketta 2 ei ole, suoritetaan silmukkaa loputtomasti tai break-komentoon (käsitellään myöhemmin) asti. Esimerkkejä for-silmukoista: for (a = 0; a < 10; ++a) puts("iteraatio"); Tämä tulostaa tekstin "iteraatio" 10 kertaa. Monimutkaisempi ja älyttömämpi esimerkki olisi taas: for (b = a; a > 10 && b < 20;) { if (a < b) --b; else --a; while (a == b || a < 10) ++a; } do while (); Tällainen silmukka on muuten kuten while-silmukka, paitsi että lauseketta ei testata ennen lauseen ensimmäistä suorituskertaa. Lause siis suoritetaan ainakin kerran, vaikka lauseke ei koskaan olisikaan tosi. Joskus käytetäänkin tällaista muotoa: do { puts("tämä tulostetaan kerran"); } while (0); Tästä on joskus hyötyä makroissa (käsitellään myöhemmin), kun halutaan määritellä muuttujia makrossa, mutta halutaan sen näyttävän funktiokutsul- ta. (Perään laitettava ';'-merkki voisi muuten aiheuttaa virheen käännöksessä.) do while -silmukkaa käytetään kuitenkin yleensä tavallisena silmukkana: a = 0; do { if (b > a) a -= b; else a += --b; } while (a); Kaikilla silmukkarakenteilla voidaan toteuttaa loputtomasti toistettava silmukka: while (1) { /* 1 tai mikä tahansa muu vakio, joka ei ole 0 */ /* toistetaan loputtomasti */ } for (;;) { /* toistetaan loputtomasti */ } do { /* toistetaan loputtomasti */ } while (1); /* mikä tahansa tosi vakio */ Loputtomasti toistuvastakin silmukasta voidaan poistua joillakin tavoilla. Näitä ovat return-komento (joka palaa funktiosta, jossa silmukka on), go- to-komento (jonka käyttö on erittäin huono tapa), funktion kutsuminen, joka poistuu koko ohjelmasta sekä break, joka keskeyttää silmukan välittömästi ja jatkaa ohjelman suoritusta silmukan jälkeen tulevasta lauseesta/lausek- keesta. Esimerkiksi: for (;;) { puts("arvomme lukuja..."); a = rand(); b = rand() + a - rand(); if (a == b) break; } puts("nyt ilmeisesti a == b, koska silmukasta päästiin pois"); rand() on standardi C-funktio. Se palauttaa ns. "satunnaisluvun", joka ei tietenkään ole täysin satunnainen mutta jota voidaan käyttää sellaisena. Esimerkkisilmukka jatkaisi luultavasti toimintaansa hyvin pitkään, riippuen lähinnä rand():n palauttaman arvon maksimiarvosta (joka voi vaihdella kääntäjän mukaan) sekä muuttujien a ja b koosta. Tuossa esimerkissä on lisäksi break varsin hyödytön, sillä saman saisi ai- van hyvin aikaan do { ... } while (a != b); -muodossa. Tietysti break-ko- mentoa voidaan käyttää muissakin kuin loputtomasti toistuvissa silmukoissa, ja se on niissä usein hyödyllisempi: for (a = 0; a < 100; ++a, ++b) { if (a == 50) { a -= b; if (a >= 10 * b && b > 0) break; } if (b == ++c || c == 4) b -= ++a / c; } Jos on useampia sisäkkäisiä silmukoita, poistuu break ainoastaan si- simmästä. Ohjelmassa break, joka ei ole silmukassa eikä switch-rakenteessa (käsitellään myöhemmin), ei merkitse mitään ja tuottaa käännettäessä vir- heilmoituksen. Toinen silmukoissa käytettävä poikkeus on continue, joka hyppää silmukan suorituksessa takaisin iterointiin tai jatkumisehtona olevan lausekkeen testaamiseen (for-silmukassa). Tätä voidaan käyttää esimerkiksi seuraavas- ti: for (a = 0; a < 10; ++a) { if (a == 5) continue; printf("%d\n", a); } printf():n toiminnasta on tarkempaa tietoa myöhemmin. Tässä esimerkissä se tulostaa muuttujan a arvon sekä rivinvaihdon. Yritän välttää käyttämästä muita muotoja ennen varsinaista selostusta toiminnasta. Tämä esimerkki tu- lostaisi siis: 0 1 2 3 4 6 7 8 9 Eli kaikki luvut 0 - 9 paitsi 5. {3Switch-rakenne Jos halutaan tehdä eri asioita riippuen jonkun muuttujan arvosta, voidaan se tehdä yksinkertaisesti if-rakenteilla: if (a == 0) { puts("nolla"); } else if (a == 1) { puts("yksi"); } else if (a == 2) { puts("kaksi"); } else if (a == 3) { puts("kolme"); } else if (a == 4) { puts("neljä"); } else { puts("jotain muuta kuin 0-4"); } Tällainen toimii, mutta ei ole kovin tehokasta (erityisesti jos eri verrat- tavia arvoja on paljon) eikä mukavaa kirjoittaa tai lukea. Saman voisi tehdä switch-rakenteella seuraavasti: switch (a) { case 0: puts("nolla"); break; case 1: puts("yksi"); break; case 2: puts("kaksi"); break; case 3: puts("kolme"); break; case 4: puts("neljä"); break; default: puts("jotain muuta kuin 0-4"); break; } Yleinen muoto switch-rakenteelle on siis: switch () { /* * lauseita, joiden joukossa voi olla case-labeleita, * breakeja ja default-label. */ } Toiminnallisesti switch() hyppää johonkin kohtaan sen jälkeen tulevaa oh- jelmalohkoa riippuen lausekkeen arvosta ja suorittaa ohjelmalohkoa siitä alkaen, kunnes vastaan tulee break tai jokin muu poikkeusohje. Jos edelli- sestä esimerkistä jätettäisiin break-komennot pois, tulostaisi se esimer- kiksi (jos a:n arvo olisi 2): kaksi kolme neljä jotain muuta kuin 0-4 Muodoltaan case-label on seuraavanlainen: case : Lausekkeen on oltava sellainen, että C-kääntäjä voi yksinkertaistaa sen yk- sittäiseksi vakioksi. Esimerkiksi: case 2 * 5: on sama kuin: case 10: Kahdella case-labelilla samassa switch-lauseessa ei saa olla samaa arvoa. Samoin default-labeleita voi olla vain yksi. Se kohta, mihin switchin alusta hypätään, on joko se case-label, jossa ole- van lausekkeen arvo on sama kuin switch:n ehtona olevan lausekkeen tai jos mikään case-label ei vastaa tätä, default. Jos default-labelia eikä mitään lausekkeen arvoa vastaavaa case-labelia ole, ei suoriteta mitään switch:n ohjelmalohkosta, vaan hypätään seuraavaan kohtaan koko switch:n jälkeen. Monimutkaisempi esimerkki switch-rakenteesta: switch (a - 5) { case 0: case 1: puts("(a - 5) on 0 tai 1"); case 2: puts("0 <= (a - 5) <= 2"); break; case 5: puts("a == 0"); case 6: puts("0 <= a <= 1"); case 7: puts("0 <= a <= 2"); break; /* switch:n lopussa oleva break on vain muodollisuus */ } Tämä voi tulostaa yhden tai useamman väittämän, riippuen a:n arvosta. Kaik- ki ne ovat tosia. Mitään ei tulosteta, jos lausekkeen a - 5 arvo ei ole 0, 1, 2, 5, 6 tai 7. Yleisesti hyvä tapa on järjestää switch:n case-labelit numerojärjestykseen, kuten esimerkeissä on ollut, jos se on mahdollista. {3Enemmän tietoa tyypeistä Toistaiseksi käsitellyt muuttujien tyypit ovat olleet yksinkertaisia koko- naislukuja. Nämä riittävät hyvin yksinkertaisiin, laskennallisiin ohjelman osioihin mutta käyvät hyvin hankaliksi, jos käsiteltävää tietoa on paljon tai jos halutaan hyödyntää C-kielen tai käyttöjärjestelmän kehittyneempiä toimintoja. Osoittimet (englanniksi "pointers") Yleisesti osoitin tarkoittaa sellaista lukua, jonka arvolla ei katsota ole- van numeerista merkitystä, vaan arvon merkitys on sen kuvaama muistiosoite. Motorolan 680x0-prosessoreissa muistiosoitteet ovat 32-bittisiä (tosin kaikki nämä prosessorit eivät huomioi koko 32-bittistä lukua) eli 4:n tavun mittaisia lukuja. Luvun arvo kuvaa vain, monesko tavu muistin alkuosoit- teesta laskien on kyseessä. Periaatteessa voitaisiin siis C-kielellä kuvata Amigassa muistiosoitteita long (tai unsigned long) -tyyppisillä muuttujilla. Tämä toimisi mutta olisi hyödytöntä, koska osoittimet eivät merkitse niiden esittämiä numeroita eivätkä normaalit matemaattiset toiminnot tekisi mitään järkevää niille. Vaikka osoitin sinänsä on vain osoitin, eikä sitä voi sen tarkemmin määri- tellä, määritellään C-kielessä osoittimille jokin tyyppi, johon se osoit- taa. Tämä määrää osoittimen tarkemman käytöksen eri operaatioiden suhteen. Muuttuja, joka on osoitin, on siis aina osoitin johonkin tyyppiin. Tällai- nen muuttuja määritellään kuin määriteltäisiin sen olevan sitä tyyppiä, mi- hin se osoittaa, paitsi että muuttujan nimen eteen laitetaan '*'-merkki. Esimerkkejä: char *c; unsigned short *s; long *l; Nämä kaikki määrittelevät siis muuttujan, joka osoittaa johonkin kokonais- lukutyyppiin. Kaikki nämä muuttujat (c, s ja l) ovat osoittimia ja ovat siis kooltaan 4 tavua, vaikka ne ovat osoittimia eri tyyppeihin. (Huomaa, että '*'-merkki ei kuulu muuttujan nimeen.) Mitä osoittimella voidaan siis tehdä? Normaalisti ei ole mitään järkeä an- taa osoittimelle suoraan numeerista arvoa, koska arvolla ei osoitteena oli- si välttämättä mitään merkitystä. Osoitin asetetaankin siis osoittamaan esimerkiksi johonkin muuttujaan. Muuttujan osoite saadaan '&'-merkillä muuttujan nimen edessä ("unary"-operaattorina on '&':n merkitys aivan eri kuin tavallisena). Voimme esimerkiksi tehdä seuraavan: int c; int *ptr; ptr = &c; Tämän seurauksena muuttuja "ptr" sisältää sen muistiosoitteen, jossa muut- tuja "c" sijaitsee. (Huomaa, että muuttujan osoitteen ottaminen rajoittaa C-kääntäjän optimointimahdollisuuksia, koska se pakottaa kääntäjän sijoit- tamaan muuttujan johonkin todelliseen muistisäilytykseen - useimmat kääntäjät pyrkivät sijoittamaan paikallisia muuttujia prosessorin rekiste- reihin niin paljon kuin mahdollista.) Tämäkään ei vielä ole kovinkaan hyödyllistä, joten tullaankin osoittimille hyvin oleelliseen operaatioon eli epäsuoraan osoitukseen. Tämän pitäisi ol- la ainakin assemblerohjelmoijille tuttu käsite. Epäsuora osoitus on osoit- teen ottamisen vastakkainen operaatio. Siinä käytetään '*'-merkkiä unary- operaattorina. (Tätä pitää taas varoa sekoittamasta kertolaskuun tai osoit- timen tyyppimäärittelyyn.) Epäsuora osoitus merkitsee sitä, että käsi- tellään itse osoitinmuuttujan sijasta sitä kohtaa muistista, jonka muistio- soite osoittimessa on. Tätä käsitellään sentyyppisenä, mihin tyyppiin osoi- tin osoittaa. Eli voisimme jatkaa edellistä esimerkkiä seuraavasti: /* * ennen: * ptr osoittaa muuttujaan c * muuttujalla c ei määriteltyä arvoa */ *ptr = 10; /* * jälkeen: * ptr ei ole muuttunut * muuttuja c sai arvon 10 */ Epäsuora osoitus voi siis täyttää ehdon "modifiable lvalue", josta aiemmin oli pari mainintaa. Kun epäsuoran osoituksen kautta asetetaan jollekin ar- vo, laitetaan se arvo muistiin siihen kohtaan, mihin osoitin osoittaa. Tämän takia on osoittimien kanssa oltava huolellinen - epäsuorat osoitukset sellaisen osoittimen kautta, joka ei osoita mihinkään järkevään paikkaan on yksi yleisimmistä bugien aiheuttajista. Assembleria osaaville: Jos oletetaan, että ptr olisi vaikka rekisterissä a0 (jos se ei ole rekisterissä, se tietysti laitetaan väliaikaisesti johonkin rekisteriin epäsuoraa osoitusta varten), olisi ylläolevan assemblervastine: move.l #10,(a0) Useimmat kääntäjät tosin osaisivat kääntää tuon hieman paremmin, eli se olisi käytännössä: moveq #10,d0 move.l d0,(a0) Jälkimmäinen tapa vie 2 tavua vähemmän tilaa ja toimii nopeammin. Joka ta- pauksessa move-komennon koko (tässä .l) määräytyy sen mukaan, mihin tyyp- piin osoittimen on määritelty osoittavan. Tässä tapauksessa oli kyseessä int, joka on normaalisti 32-bittinen. Jos kääntäjässä on short-int optio käytössä (int 16-bittinen), niin vastaava kohta olisikin: move.w #10,(a0) Osoitin voi myös osoittaa osoittimeen tai vaikka osoittimeen, joka osoittaa osoittimeen jne. On vain laitettava riittävä määrä '*'-merkkejä muuttujan määrittelyssä. Vastaavasti on moninkertainen epäsuora osoitus mahdollista. Esimerkiksi: short var, *ptr1, **ptr2; ptr2 = &ptr1; /* ptr2 osoittaa ptr1:een */ *ptr2 = &var; /* nyt ptr1 osoittaa var:iin (asetettu ptr2:n kautta epäsuorasti) */ *ptr1 = 5; /* nyt var saa arvon 5 */ **ptr2 += 2; /* var saa arvon 7 (siihen lisätään 2) */ Osoittimet eivät toimi matemaattisten operaatioiden kanssa aivan samalla tavalla kuin kokonaisluvut. Osoittimille ovat sallittuja ainoastaan jotkut operaatiot. Näitä käsitellään seuraavan tyypin yhteydessä. Osoittimiin läheisesti liittyvä tyyppi on taulukko (englanniksi "array"). Taulukko on periaatteessa vain useampi "muuttuja", jotka sijaitsevat muis- tissa peräkkäisissä osoitteissa (muuttujan tyypin koon välein). Voimme esi- merkiksi määritellä taulukon tällä tavalla: short arr[10]; Tämä määrittelee 10 peräkkäisen "short":n sarjan taulukkoon arr (lyhennetty "array":sta). Koska taulukossa on 10 kpl shorteja, jotka ovat 2 tavun ko- koisia, on taulukon koko muistissa 20 tavua. Tässä arr toimii muuttujani- mekkeenä, jonka avulla taulukkoa käsitellään. Taulukon muuttujanimeke toi- mii kuin se olisi osoitin taulukon ensimmäiseen kohtaan. Voimme siis epäsuoralla osoituksella laittaa taulukon ensimmäiseen kohtaan luvun 0: *arr = 0; Aivan näin yksinkertaista ei ole taulukon muiden kohtien osoittaminen. Niitä varten täytyy tehdä epäsuora osoitus muuhun kohtaan kuin suoraan muuttujanimekkeen suhteen. Laitamme taulukon toiseen kohtaan luvun 1: *(arr + 1) = 1; Tässä teemme epäsuoran osoituksen kohdasta arr + 1. Koska arr toimii tässä kuten osoitin, pätevät siihen osoittimen laskusäännöt. Kun osoittimeen lisätään tai siitä vähennetään jokin kokonaisluku (vakio tai muuttuja), on tuloksen tyyppi sama kuin osoittimen, ja tulos osoittaa muistissa kokonais- luvun verran osoittimen tyypin ilmaisemia paikkoja eteen- tai taaksepäin. Ylläolevassa arr + 1 osoittaa siis kaksi tavua myöhempää muistiosoitetta kuin arr, eli se osoittaa taulukon toista kohtaa, jolloin epäsuora osoitus arr + 1:stä käsittelee tätä kohtaa. Osoittimen yhteenlaskulle ja epäsuoralle osoitukselle on myös lyhennetty muoto, indeksiosoitus, joka on varsinaisesti taulukkoon osoittamista varten tarkoitettu. Ylläolevan voisi kirjoittaa siis seuraavasti: arr[1] = 1; Taas on hyvä huomata ero []-merkkien käytöstä muuttujan määrittelyssä ja sen osoittamisessa. Yleisesti voimme määritellä indeksin siten, että jos on mikä tahansa todelliseen tyyppiin (eli ei void:iin - käsitellään myöhemmin) osoittava osoitin (tai taulukon muuttujanimeke) p ja mikä tahansa kokonais- luku c (joko muuttuja tai vakio), niin p[c] on aina täsmälleen sama asia kuin *(p + c). Vastaavasti &p[c] on sama asia kuin p + c, mutta ehkä hieman epäsiistimpi tapa merkitä tämä. Yleensä lyhennettyä muotoa käytetään tau- lukkoa osoitettaessa, vaikka olisi kyseessä taulukon ensimmäinen kohta, eli esimerkiksi *arr periaatteessa yleensä kirjoitettaisiin arr[0]. Taulukossamme arr on siis 10 kohtaa, joiden numerot näitä osoittaessa ovat 0-9. Taulukkoa käsiteltäessä on aina muistettava olla huolellinen, koska jos esimerkiksi laittaa jotakin kohtaan arr[10], niin se luultavasti menee jonkun toisen muuttujan päälle, koska arr[9] on taulukon viimeinen kohta. Samaten arr[-1]:een ei olisi turvallista laittaa mitään, koska sekin on kääntäjän taulukolle varaaman alueen ulkopuolella. C-kääntäjä ei tarkista osoitusten olevan taulukon sisällä. Jos haluaisimme laittaa esimerkiksi arvon 5 esimerkkitaulukkomme jokaiseen 10:een kohtaan, voisi sen tietysti (erityisesti näin pienen taulukon ta- pauksessa) tehdä yksinkertaisesti näin: arr[0] = 5; arr[1] = 5; arr[2] = 5; arr[3] = 5; arr[4] = 5; arr[5] = 5; arr[6] = 5; arr[7] = 5; arr[8] = 5; arr[9] = 5; Tämä on kuitenkin aika "tyhmä" tapa tehdä asia. Yleensä tämä tehtäisiin jollakin yksinkertaisella silmukalla: int i; for (i = 0; i < 10; ++i) arr[i] = 5; Jos käyttää jotakin yksinkertaista C-kääntäjää (tai vähemmän yksinkertaista ilman että optimointi on päällä), ei tällainen kuitenkaan ole nopein mah- dollinen tapa, koska joudutaan tekemään silmukan joka iteraatiolla uusi in- deksoitu osoitus. Voimme periaatteessa tehdä saman myös väliaikaisen apu- muuttujan avulla: int i; short *ptr; i = 9; ptr = arr; do { *ptr++ = 5; } while (i--); Tässä käydään silmukka läpi 10 kertaa, kuten aiemminkin. Alussa on i:llä arvo 9 ja ptr osoittaa taulukon alkuun. Osoitus *ptr++ tekee epäsuoran osoituksen ptr:n kautta ja siirtää ptr:n seuraavaan kohtaan. Laskurina i toimii tehokkaasti takaperin, koska i:tä tarvitsee vain verrata nollaan (joka käy 680x0-prosessoreissa tst-komennolla) eikä sen ylärajaan. Lisäksi do { } while -muotoisen silmukan ansiosta selvitään yhdellä vähemmän i:n testauksella. Taulukot voivat myös sisältää useampia kuin yhden "ulottuvuuden". Voidaan esimerkiksi määritellä: long matrix[4][4]; Tässä määritellään matrix 4x4 taulukoksi (jota voi hyvin käyttää matriisi- na) longeja (4 tavua/kpl, eli taulukko vie 4 * 4 * 4 = 64 tavua muistia). Tämä taulukko on muistissa järjestynyt siten, että jälkimmäinen indeksikoh- ta on vähemmän merkitsevä muistiosoitteen kannalta, eli kohdat ovat muis- tissa järjestyksessä matrix[0][0], matrix[0][1], matrix[0][2], mat- rix[0][3], matrix[1][0] jne. Jälleen muuttujanimeke on käytökseltään osoit- timen kaltainen, mutta tällä kertaa se on kuin oltaisiin määritelty se "long **matrix;":ksi. Jos käytämme ainoastaan yhtä indeksiä, tämä on eniten merkitsevä indeksi, eli esimerkiksi matrix[1] on osoitin matriisin kohtaan matrix[1][0], eli vastaava kuin &matrix[1][0], tyypiltään se on "long *". Muuttujan tyypin taulukointi on aina toiminnallisesti "uloin" looginen osa tyyppiä, eli: int *ptrarr[32]; Tämä määrittelee taulukon, jossa on 32 kpl osoittimia, jotka osoittavat tyyppiin "int". Taulukolla "char":eja taas on aivan erityinen merkitys, koska char voi olla ASCII-merkki. Tällaisessa taulukossa voidaan siis säilyttää tekstiä. Yleensä tekstiä säilytetään siten, että itse tekstin sisältö on peräkkäisissä tavuissa ja perässä on nollatavu. Tällaista tekstiä kuvaavaa peräkkäisten tavujen sarjaa sanotaan usein merkkijonoksi. Merkkijonojen käsittelyä varten löytyy useita standardeja C-funktioita. Lisäksi suuri osa standardeja funktioita käyttää parametreissaan merkkijo- noja. Esimerkiksi puts(), jota esimerkeissä olemme käyttäneet, ottaa para- metrikseen merkkijonon. Toiminnaltaan puts() tulostaa (normaalisti shel- liin) parametriksi saamansa merkkijonon ja rivinvaihdon. Esimerkeissä on ollut käytettynä pääasiassa merkkijonovakioita. Merkkijonovakio merkitään siten, että kahden '"'-merkin väliin kirjoitetaan haluttu teksti. Teksti voi sisältää melkein mitä tahansa merkkejä joitakin poikkeuksia lukuunottamatta: '"' (koska se päättäisi merkkijonovakion), '\' (koska se toimii poikkeustunnuksena) ja rivinvaihto (tämän tarkempi käytös voi vaihdella kääntäjän mukaan, mutta ANSIn mukaan rivinvaihdot merkkijono- vakiossa eivät ole sallittuja). Nämä sekä useat muut merkit voidaan korvata erilaisilla poikkeuskoodeilla, jotka kaikki alkavat '\'-merkillä. Näitä on käytettävissä seuraavia (^ tarkoittaa että vastaavan koodin saa aikaan painamalla ta ctrl-näppäimen ollessa painettuna): \" '"'-merkki \\ '\'-merkki \a piippaus (^G, ASCII 7, BEL) \b poispyyhintä takaperin (^H, ASCII 8, BS (backspace)) \f sivunvaihto (^L, ASCII 12, FF (form feed)) \n rivinvaihto (^J, ASCII 10, LF (line feed)) \r (suomeksi?) (^M, ASCII 13, CR (carriage return)) \t vaakatabulaattori (^I, ASCII 9, HT (horizontal tab)) \v pystytabulaattori (^K, ASCII 11, VT (vertical tab)) Useat näistä merkeistä voidaan kirjoittaa normaalissakin muodossa, mutta ne ovat kätevämpiä tuossa muodossa, jotta voitaisiin helpommin nähdä suoraan lähdekoodista mitä merkit ovat. Lisäksi voidaan mikä tahansa merkki kir- joittaa antamalla sen ASCII-arvo oktaalina tai heksadesimaalina '\'-merkin jälkeen. Esimerkiksi '\033' tai '\0x1b' olisi ESC-koodi (^[, ASCII 27), jonka käyttö tällaisessa muodossa on erittäin suositeltavaa. Poikkeukselli- sesti '\'-merkin jälkeen ei ole pakko laittaa nollaa alkavan oktaali- tai heksadesimaaliluvun tunnukseksi kuten normaalisti. Merkkijonovakioita voi liittää yhteen yksinkertaisesti kirjoittamalla ne peräkkäin, eli "merkki" "jono" on sama kuin "merkkijono". Merkkijonovakio on toiminnallisesti tyypiltään "char *", eli ohjelmassa se tuottaa siihen kohtaan, johon se on ohjelmassa sijoitettu, osoittimen ky- seisen merkkijonon alkuun (poikkeustapauksia esiintyy muuttujien alustuk- sessa, jota käsitellään myöhemmin). Merkkijonovakio sijoitetaan yleensä oh- jelman koodihunkkiin (eli samaan osaan ohjelmaa, jossa ohjelman varsinainen ajettava koodi on), jota ei saa muuttaa, joten merkkijonovakion sisältöä ei saa muuttaa, vaikka se on helposti mahdollista. Sen sijaan jos haluaa sel- laisen merkkijonon, jota voi muutella, kannattaa määritellä esimerkiksi taulukko ja kopioida merkkijono sinne vakiosta esimerkiksi standardifunk- tiolla strcpy(): char str[100]; strcpy(str, "merkkijono"); strcpy() ottaa kaksi parametriä, molemmat ovat osoittimia tyyppiin char. Toiminnaltaan se kopioi toisena parametrinä olevan merkkijonon sinne, minne ensimmäinen parametri osoittaa (palautusarvo on sama kuin ensimmäinen para- metri). Ylläolevassa esimerkissä on taulukon str koko oltava vähintään 11, muuten kopiointi ylittää sen ja sotkee jotakin muuta. (Kopioinnissa kopioi- daan merkkijono "merkkijono", 10 kirjainta, sekä 0-tavu, joka on sen perässä.) Kun merkkijono on kopioitu taulukkoon (tässä tapauksessa sitä käytetään puskurina), voidaan sitä muuttaa. Voisimme jatkaa esimerkkiä: str[7] = 'a'; Tässä 'a' on merkkivakio. Merkkivakio on merkinnältään samankaltainen kuin merkkijonovakio, paitsi että käytetään '-merkkejä '"'-merkkien sijasta, eikä merkkejä voida määritellä useampia kuin yksi. Merkkivakion arvo on sen numeerinen arvo. Ylläoleva on siis sama kuin tekisi str[7] = 97; koska 97 on a-kirjaimen ASCII-arvo. (Joillakin kääntäjillä toimivat myös sellaiset merkkivakiot, joissa on useampia merkkejä (enintään neljä), jois- ta muodostuva luku muodostetaan kuin ne olisivat peräkkäin muistissa ja luettaisiin sieltä kuin ne olisivat jokin suurempi kokonaislukumuuttuja. ANSI on kuitenkin kieltänyt tällaisen muodon.) Tämän muutoksen jälkeen sisältää taulukko str merkkijonon "merkkijano". Jos nyt suoritaisimme puts()-funktion puts(str); se tulostaisi: merkkijano Koska taulukko str on huomattavasti suurempi kuin siinä nyt oleva merkkijo- no, voidaan siinä olevaa tekstiä myös pidentää. Tähän voidaan käyttää esi- merkiksi funktiota strcat(), joka liittää merkkijonon perään lisää tekstiä, esimerkiksi: strcat(str, " on vakava ongelma."); Tämän jälkeen sisältää str merkkijonon "merkkijano on vakava ongelma.". Merkkijonojen käsittely on varsin tärkeä hallita, joten tästä voimmekin tehdä kokonaisen esimerkkiohjelman: /* * merkkijono.c */ #include #include int main(int ac, char **av) {{ /* * seuraavaksi määritellään taulukko str paikalliseksi muuttujaksi * funktiossa main() * * taulukoita, erityisesti suurikokoisia, ei kannata määritellä * paikallisiksi muuttujiksi Amigalla suurempia ohjelmia tehdessä, * erityisesti jos funktiosta kutsutaan useita muita ohjelman * suuria osia, koska tämä kuluttaa pinomuistia (stack), jota Amigassa * on rajoitetusti käytettävissä */ char str[100]; /* * kopioidaan puskuriin tekstiä */ strcpy(str, "merkkijono"); /* * tulostetaan */ puts(str); /* * muutetaan yksi kirjain keskeltä merkkijonoa * tulostetaan tuloksena oleva puskurin sisältö */ str[7] = 'a'; puts(str); /* * lisätään merkkijonon loppuun lisää tekstiä * ja taas tulostetaan mitä puskurissa operaation jälkeen on */ strcat(str, " on vakava ongelma."); puts(str); /* * tässä voidaan demonstroida myös osoittimen ja kokonaisluvun * yhteenlaskua */ puts(str + 6); return 0; } Ohjelman pitäisi kääntyä helposti ja tulostaa ajettaessa: merkkijono merkkijano merkkijano on vakava ongelma. jano on vakava ongelma. Seuraavaksi tulemmekin hieman monimutkaisempiin struct- ja union-tyyppeihin (mitä lienevätkään varsinaisilta suomenkielisiltä nimekkeiltään). Molempien määrittely näyttää miltei samanlaiselta, mutta niiden merkitykset ovat toi- siinsa nähden aivan erilaiset. Tietoa käsiteltäessä on usein hyödyllistä voida käsitellä tietoa hieman suuremmissa yksiköissä kuin yksittäisissä muuttujissa tai taulukoissa. Jos useat eri tiedot liittyvät kiinteästi yhteen, voi olla hyvä määritellä ne "structure":ksi. Tällainen structure sisältää kenttiä, jotka määritellään kuin ne olisivat muuttujamääritelmiä "struct"-avainsanan jälkeen tulevien aaltosulkeiden välissä. Jos esimerkiksi haluamme säilyttää jonkun tiedoston nimeä ja pituutta yhdessä, niin voimme määritellä: struct { char name[32]; long length; } file; Tällöin meillä on muuttuja "file", joka sisältää tilaa sekä tiedoston ni- melle (enintään 31 merkkiä pitkä, Amigalla FFS huomioi enintään 30 merkkiä, joten tämä riittää) että pituudelle, niille on kullekin omat nimetyt kenttänsä, "name" ja "length". (Eri structureissa voi olla samannimisiä kenttiä. Samoin voidaan käyttää muuttujia, joilla on sama nimi kuin jonkun structuren kentällä.) Itse muuttuja on kooltaan 36 tavua (32 kpl yhden ta- vun kokoisia char:eja ja yksi 4 tavun mittainen long). Kenttiin voidaan laittaa tietoa seuraavalla tavalla: strcpy(file.name, "ksrc10.tar.gz"); file.length = 4706422; Tällä tavalla määritelty muuttuja on kuitenkin harvoin hyödyllinen, koska se on ainoa tätä tyyppiä oleva muuttuja. Siksi yleensä structureita määri- teltäessä annetaan structurelle nimeke ja määritellään muuttuja sen avulla. Nimekkeellä voidaan määritellä useampia muuttujia eri paikoissa, eikä tar- vitse määritellä kaikkia muuttujia varsinaisen structuremäärittelyn yhtey- dessä. Tällöin ylläoleva muuttuja määriteltäisiin esimerkiksi seuraavalla tavalla: struct filedata { char name[32]; long length; }; struct filedata file; Ensimmäinen struct määrittelee structuren ja antaa sille nimekkeeksi "file- data", jolloin voidaan käyttää muotoa "struct filedata" kuvaamaan koko ky- seistä tyyppiä (vaikka se olisi paljon monimutkaisempikin). Tämän avulla annetaankin se sitten tyypiksi muuttujalle "file". Nimike saa olla samanni- minen kuin mikä tahansa muuttujan nimi tai structuren kenttä. Muuttuja tai useita muuttujia voidaan toki määritellä myös structuren al- kumäärittelyn yhteydessä. Lisäksi voidaan määritellä osoittimia structurei- hin, taulukoita jotka sisältävät structureita jne., esimerkiksi: struct filedata { char name[32]; long length; } file, file2, *ptr, files[10], *ptrs[20]; struct filedata moredata[100], *anotherptr; Aiemmin nähtiin, että muuttujassa olevia kenttiä osoitetaan muodossa muut- tuja '.' kentän nimi. Kenttiä voidaan käsitellä myös suoraan osoittimen kautta, esimerkiksi jos nollaamme yllä määritellyn ptr:n avulla muuttujan "file" length-kentän: ptr = &file; ptr->length = 0; Osoitus tapahtuu siis muodossa -> . Yksinkertaisemmissa ohjelmissa harvemmin hyödyllinen "union"-tyyppi on määrittelyltään muuten saman näköinen kuin structuren määrittely, paitsi että sanan "struct" tilalla on "union". Kentät ja nimikkeet määritellään siis samalla tavalla. Varsinaisena erona structureen onkin se, että kenttiä ei säilytetä peräkkäin vaan päällekäin, ts. sitä aluetta muistissa, jossa union-muuttuja on, voidaan käsitellä useampana eri tyyppinä. Muuttujan kooksi tulee sen suurimman kentän koko. Tämäntyyppistä muuttujaa voitaisiin käyttää esimerkiksi erään ANSI-säännöksen kiertämiseen. (ANSIn mukaan cast:ia (käsitellään myöhemmin) ei voida tehdä osoittimen ja kokonaisluku- tyypin välillä, vaikka ne olisivat saman kokoisia (se voi vaihdella koneen mukaan). Tämä on ihan hyödyllistä portattavuutta varten, joten sitä ei kan- nata tehdäkään, mutta jollekin tietylle koneelle ohjelmoidessa voidaan tällaista kuitenkin käyttää.) Voimme säilyttää osoitinta kokonaislukumuut- tujassa (tai päin vastoin): union majorhack { long l; long *ptr; } hackvar; Itse operaatiota varten voitaisiin käyttää funktiota: long lptr2long(long *src) {{ hackvar.ptr = src; return hackvar.l; } Eli jos haluamme muuttujaan "num", tyyppiä long, sen oman osoitteen, sen voi tehdä kutsumalla ylläolevaa funktiota: num = lptr2long(&num); Yksi tapaus, jossa structurelle ei ole välttämättä hyödyllistä määritellä nimekettä, on unionin sisällä. (Unioneja ja structureita voi olla keskenään sisäkkäin rajatta.) Voimme esimerkiksi määritellä unionin, joka käyttäytyy numeroarvoiltaan kuten 680x0:n (tai yleensä 32-bittisten big-endian proses- soreiden) rekisterit, jos sitä osoittaa eri tyypeillä: union be_reg { struct { long val; } l; struct { unsigned long val; } ul; struct { short pad; short val; } w; struct { short pad; unsigned short val; } uw; struct { char pad[3]; char val; } b; struct { char pad[3]; unsigned char val; } ub; }; /* * l- ja ul-kenttien ei tarvitsisi välttämättä olla structureita, mutta * niiden osoitusten on hyvä olla samankaltainen toisten kenttien * osoituksen kanssa ihan siisteyden vuoksi */ Assemblerohjelmoijat varmaan ainakin ymmärtävät, mistä on kyse. Tällainen tyyppi käyttäytyisi esimerkiksi seuraavasti (jos kaikki toimii oikein): union be_reg reg; reg.l.val = -1; /* * tuloksena: * * muuttujan "reg" bitit: 11111111111111111111111111111111 * * merkittävien kenttien arvot: * * l.val -1 * ul.val 0xffffffff (4294967295) * w.val -1 * uw.val 0xffff (65535) * b.val -1 * ub.val 0xff (255) */ reg.b.val = 0; /* * tuloksena: * * muuttujan "reg" bitit: 11111111111111111111111100000000 * * merkittävien kenttien arvot: * * l.val -256 * ul.val 0xffffff00 (4294967040) * w.val -256 * uw.val 0xff00 (65280) * b.val 0 * ub.val 0 */ Useimmiten unioneiden käytökseen ei kuitenkaan pidä luottaa eri tyyppien osalta. Käytännössä unioneita yleensä käytetään siten, että aina tunnetaan, minkätyyppisenä sen arvo on järkevä. (Union voi olla esimerkiksi osana st- ructurea, jossa on myös vaikka kenttä, jonka arvo ilmaisee minkätyyppinen arvo unionissa on.) Erityisesti structure/union-tyyppien kanssa on usein hyödyllistä käyttää "sizeof"-ilmausta, joka kääntyy tyypin kooksi tavuissa. sizeof:in perään voidaan laittaa sulkeissa oleva tyyppinimike, jonka koko halutaan tietää. Esimerkiksi: printf("%d\n", sizeof(long)); Tämän pitäisi Amigalla olla 4. Eli ylläoleva tulostaisi "4". Samoin: printf("%d\n", sizeof(char *)); Tämäkin olisi 4. Huomaa, että tyyppinimeke on muuten samanlainen kuin muut- tujan määritelyssä, paitsi että tunnusosa on "näkymätön". (Myöhemmin saman- lainen muoto esiintyy cast:eissa ja prototyypeissä.) printf("%d\n", sizeof(struct filedata)); Tämä tulostaisi 36, oletettuna että struct filedata on määritelty kuten aiemmissa esimerkeissä. Toinen tapa käyttää sizeof:ia on siten, että kirjoittaa sen perään lausek- keen, jolloin se palauttaa lausekkeen koon. Jos meillä on vaikka muuttuja struct filedata *f; niin: printf("%d\n", sizeof f); /* * tulostaa 4, sama kuin sizeof(struct filedata *) */ printf("%d\n", sizeof *f); /* * tulostaa 36, sama kuin sizeof(struct filedata) */ printf("%d\n", sizeof f->name); /* * tulostaa 32, sama kuin sizeof(char [32]) */ printf("%d\n", sizeof f->name[0]); /* * tulostaa 1, sama kuin sizeof(char) */ Taulukon sisältämien elementtien määrä voidaan myös selvittää tämän avulla (vain jos se on määritelty!). Jos meillä on mikä tahansa taulukko a[n], niin (sizeof a) / (sizeof a[0]) antaa tulokseksi n. Yleisin sizeof:in käyttö lienee muistin varaamisen yhteydessä. Tätä käsi- tellään kuitenkin vasta myöhemmin. Tässä vaiheessa lienee syytä esitellä poikkeava osoitintyyppi. Jo aiemmassa vaiheessa on funktioiden parametrimäärittelyjen yhteydessä käytetty pseu- do-tyyppiä "void". Muuttujia voidaan määritellä osoittimeksi tyyppiin "void", jolloin kyseessä on yleiskäyttöinen osoitin, joka voi osoittaa mi- hin tahansa. Tällaisen osoittimen kautta ei voida tehdä epäsuoria osoituk- sia, mutta arvon siirtäminen toiseen osoittimeen tapahtuu ilman virheitä: void *ptr; long l; char *cptr; ptr = &l; cptr = ptr; /* * jos tekisimme suoraan * * cptr = &l; * * aiheuttaisi se käännösvaiheessa varoituksen */ Useimmiten tällaiseen tehtävään käytetään kuitenkin cast-operaattoria, joka kertoo C-kääntäjälle, mitä tyyppiä jokin on esittävinään, eli ylläoleva toimisi varoituksetta ilman väliosoitinta, jos sen tekisi näin: cptr = (char *)&l; Cast on siis yksinkertaisesti tyyppimääritelmä sulkeissa. Tällaisen avulla voidaan myös käsitellä jotakin muuttujaa kuin se olisi eri tyyppiä: struct mystruct { long vals[4]; char name[20]; }; struct mystruct ms; void *mp; void func(void) {{ mp = &ms; /* * void-osoittimen kautta ei voida normaalisti käsitellä mitään, * mutta cast auttaa tähän, kerrotaan kääntäjälle että mp osoittaa * struct mystruct:iin: */ ((struct mystruct *)mp)->vals[2] = 10; strcpy(((struct mystruct *)mp)->name, "text"); } Tilanteesta riippuen voi cast myös muuttaa jotakin arvoa, jos muunnettavan koko muuttuu ja/tai muunnetaan kokonaisluvun ja liukuluvun välillä. Tyyppinimekkeitä voidaan myös itse määritellä. Muoto on muuten samanlainen kuin muuttujan määrittelyn yhteydessä, paitsi että eteen tulee sana "type- def". Voimme esimerkiksi määritellä lyhyemmät vastineet etumerkittömille kokonaislukutyypeille: typedef unsigned int u_int; typedef unsigned char u_char; typedef unsigned short u_short; typedef unsigned long u_long; /* * nämä ovat yleisiä määritelmiä ja löytyvät include-filestä * sys/types.h, joten niitä ei normaalisti kannata itse määritellä */ Nyt jos määrittelemme vaikka: u_long value; Tämä on sama kuin tekisimme: unsigned long value; Loppukurssin ajan oletamme ylläolevat tyyppimääritelmät olemassaoleviksi. Valitettavasti useimmat Amiga-ohjelmoijat käyttävät Commodoren exec/ty- pes.h:sta löytyviä vastaavia määritelmiä, joita en suosittele kenellekään, koska CAPITALISOIDUT tyyppinimekkeet ovat sekä erittäin rumia (mielipideky- symys, toim. huom.) että erittäin epästandardeja. Includeista ja muusta esikäsittelyyn liittyvästä on tarkempaa tietoa myöhemmin. Koska muoto on sama kuin muuttujia määriteltäessä, voidaan useampia tyyppi- nimekkeitä määritellä kerralla: typedef unsigned short u_short, u_word, ushort, uword; Myös structureille/unioneille voi antaa tällä tavalla tyyppinimekkeitä, esimerkiksi: typedef struct { long no; u_char name[36]; u_char streetaddr[40]; u_char postcode[16]; u_char city[16]; u_char country[16]; u_char phone[32]; } account; Tällöin sizeof(account) olisi 160, ja voisimme kyseisenlaisen tyypin määri- tellä aivan yksinkertaisesti: account act; Toki voidaan samalla määritellä myös structure-tag (kuten muuttujien kans- sa): typedef struct account { ... (sama kuin aiemmin, yritän säästää hieman tilaa) } account; Tai voidaan määritellä typedef vasta jälkeenpäin: struct account { ... }; typedef struct account account; (Tag-nimen ja tyyppinimekkeen ei ole pakko olla sama. Tässä tapauksessa laitoin saman osoittaakseni sitä, ettei näissä tapahdu konfliktia.) On eräs kummallinen poikkeustapaus, jossa käytetään standardissa type- def:issä capitalisoitua nimekettä, FILE. (Luultavasti tämä on ollut alunpe- rin #define:atun makron muodossa.) Muuttujia määriteltäessä voidaan myös antaa tiettyjä lisämääreitä, jotka sijoitetaan (typedef:in tavoin) ennen tyyppinimekettä. Näitä ovat: auto const extern register static volatile "auto" on vanhentunut, eikä sitä näe käytettävän. "const" määrittelee muuttujan vakioksi, jonka arvo ei saa muuttua. Tällöin voidaan muuttuja sijoittaa ohjelman koodihunkkiin. (Tämä on kuitenkin tois- taiseksi hieman hyödytöntä, koska vielä ei ole käsitelty muuttujien initia- lisointia.) Jos kyseessä on osoitin, niin itse osoitin ei ole vakio, vaan se osoittaa johonkin muuttumattomaan. Esimerkiksi: const char *str; Tällainen osoitin voitaisiin esimerkiksi asettaa osoittamaan merkkijonova- kioon tekemättä mitään väärää: str = "this is a string constant and must not be modified"; (Periaatteessa merkkijonovakion tyyppi on const char *, tosin se vaihtelee kääntäjän mukaan, millaisena se sen tulkitsee.) Jos muuttuja on määritelty osoittimeksi johonkin const:iin, voidaan osoitin laittaa osoittamaan myös ei-vakioihin tekemättä virhettä. Jos taas tehdään toisin päin, eli esimerkiksi meillä on: char *sptr; Ja edelleen aiempi määriteltynä, niin: sptr = str; Tämä on virhe, josta kääntäjä varoittaa. Varoituksen välttämiseksi olisi joko myös sptr määriteltävä const:iksi tai käytettävä cast:ia: sptr = (char *)str; Kuitenkaan cast:in käyttäminen tällaisessa ei ole hyvä idea, koska sptr:n kautta voitaisiin epäsuoralla osoituksella muuttaa vakion sisältöä, mikä ei ole sallittua ja mitä kääntäjä ei havaitse. "extern" määrittelee muuttujan tyypin, mutta ei säilytystä muuttujalle. (Assyohjelmoijille: externattu muuttuja xrefataan sen sijaan että sille määriteltäisiin tilaa.) Tätä käytettäessä kannattaa olla aina varma, että määritellyn niminen muuttuja on olemassa (yleensä se on jossain toisessa sorsassa) ja että se on sitä tyyppiä, joksi sen määrittelee. Siitä ei ole haittaa, jos externaa muuttujan useampaan kertaan tai sekä externaa sen että määrittelee tavallisesti samassa sorsassa. "register" toimii vain paikallisten muuttujien yhteydessä. Se kertoo kääntäjälle, että kyseinen muuttuja pitäisi yrittää laittaa johonkin CPU:n rekisteriin muistin sijasta. Useimmiten tämä on turhaa, koska kääntäjät osaavat muutenkin valita varsin hyvin, mitkä muuttujat kannattaa laittaa rekistereihin, jolloin register-lisämääre saatetaan jättää kokonaan huo- miotta tai ottaa huomioon vain viitteellisenä suosituksena. "static":n merkitys vaihtelee sen mukaan, onko kyseessä paikallinen vai globaali muuttuja. Jos kyseessä on paikallinen muuttuja, niin se käyttäytyy poikkeavasti siinä mielessä, että se säilyttää arvonsa, vaikka funktiosta poistuttaisiin tai jos funktiota kutsutaan sen itsensä sisältä. Sitä ei siis sijoiteta pinoon kuten normaalisti, vaan säilytys on samanlainen kuin globaaleille muuttujille, erona vain se, että muuttujaa voidaan osoittaa vain funktion sisältä. Esimerkiksi (vaihteen vuoksi toimiva kokonaisuus): /* * recursive.c */ #include void func(void) {{ static int n; printf("%d\n", n); if (++n < 10) func(); } int main(int ac, char **av) {{ func(); return 0; } Ajettaessa ohjelman pitäisi tulostaa: 0 1 2 3 4 5 6 7 8 9 Globaaleilla muuttujilla static tarkoittaa sitä, ettei muuttujaa voida ex- ternata muihin sorsiin. (Assyohjelmoijille: static-muuttujaa ei xdefata.) "volatile" tarkoittaa, ettei muuttujaa voida säilyttää rekisterissä. Opti- moivilla kääntäjillä se merkitsee myös sitä, että jos muuttujan arvoa muu- tetaan, se on myös tehtävä, vaikka muuttujaa ei käytettäisikään mihinkään. Useimmat kääntäjät osaavat lisäksi useita epästandardeja tai puolistandar- deja lisämääreitä. ANSIn mukaan kääntäjän omien lisämääreiden tulee alkaa kahdella '_'-merkillä. Amigalla useimmista kääntäjistä löytyy ainakin "__aligned", joka pakottaa muuttujan (tai structuren kentän) neljällä jaol- liseen osoitteeseen. {3Funktioiden prototyypit Yksi merkittävimpiä uudistuksia ANSI C -standardin myötä on ollut se, että funktiolle voidaan antaa prototyypit eli määritellä etukäteen, minkätyyppi- set parametrit ja palautusarvo sillä on. Yleensä on hyvä tapa määritellä funktiolle prototyyppi ennen sen kutsumista, ainakin jos funktiota itseään ei olla määritelty. Alunperin C-kielessä voitiin funktiolle määritellä etukäteen vain palautu- sarvo. Esimerkiksi: extern char *strcpy(); /* vanha tyyli */ Jos jotain funktiota ei tällä tavalla externaa etukäteen, oletetaan sen pa- lauttavan tyypin int. Tällä tavalla tai ei ollenkaan merkityille funktioille voi antaa paramet- reiksi mitä tahansa. Tämä on kuitenkin pahasti optimointia rajoittavaa, ja niin ANSIn mukaan funktioille on annettava prototyyppi, jossa määritellään sekä palautettava tyyppi että parametrien tyypit. Parametrimäärittelyt näyttävät muuten samoilta kuin itse funktion määrittelyssä, paitsi ettei niille tarvitse antaa nimekkeitä. strcpy():n prototyyppi on seuraavan näköinen: extern char *strcpy(char *, const char *); Toisena parametrinä olevaa merkkijonoa ei muuteta, joten se on määritelty const:iksi. Tällaiset on hyvä merkitä, jotta kääntäjä ei aiheuta turhia va- roituksia. Commodoren ohjelmoijat eivät ole kuitenkaan tätä älynneet, joten clib/-hakemiston prototyypeistä puuttuu nämä ja varoituksia tulee runsaas- ti, ellei vältä const:eja tai käytä cast:eja. Prototyypin yhteydessä ei ole pakko käyttää extern:iä, joten strcpy()n pro- totyypin voi antaa myös muodossa: char *strcpy(char *, const char *); {3Esikäsittelyn ohjaaminen Ensimmäinen asia, joka C-sorsalle tehdään käännettäessä, on esikäsittely (preprocessing). Tämän vaiheen erillisyys on hyvä huomioida, koska sen "so- keus" itse ohjelman sisällölle vaikuttaa usein sen tuottamiin tuloksiin. Esikäsittelyvaiheelle voi antaa tietynlaisia "komentoja", joille tunnuso- maista on se, että ne ovat aina kokonaisen rivin mittaisia (tai voivat olla useammankin, jos käytetään '\'-merkkejä tai kummallisia kommentteja) ja al- kavat '#'-merkillä. Yhtä tällaista on jo esimerkeissä käytetty, koska se on varsin välttämätön. #include lukee ns. include-tiedoston ja tulkitsee sen kuin se olisi osa oh- jelmaa. C:n include-tiedostoilla on yleensä pääte ".h". Kääntäjän mukana tulee joukko vakio-includeja, joita esimerkeissäkin on käytetty. Yleensä includet sisältävät pääasiassa structureiden määritelmiä, funktioiden pro- totyyppejä ja makroja. #include-komennolle on kaksi muotoa, esim. #inclu- de:aamme vakio-includen "stdlib.h". #include Tämä etsii tiedoston stdlib.h vakio-include-hakemistoista. Tiedoston nimi voi sisältää myöskin hakemistopolun, kuten: #include Polku voi olla myös absoluuttinen tai takaperin relatiivinen, mutta tällai- nen ei sovi portattavaksi tarkoitettujen ohjelmien yhteydessä. Toinen muoto olisi esimerkiksi: #include "program.h" joka etsii tiedoston program.h tämänhetkisestä hakemistosta. Tämä muoto voi myös sisältää polun. Include-tiedostojen liittämisen lisäksi esikäsittely tekee mm. seuraavat asiat: - poistaa kommentit - avaa makrot - liittää yhteen rivejä, jos on käytetty '\'-merkkiä rivin lopussa - tulkitsee ehdollisia prosessointikomentoja Makrot määritellään C-kielessä #define-komennolla. Yksinkertaisemmassa muo- dossaan makrot ainoastaan korvaavat jonkun pätkän jollakin toisella. Esi- merkiksi seuraava makro on standardi ja yleensä määritelty, jos on #inclu- de:annut mitään standardeja includeja: #define NULL 0 Tämä siis periaatteessa korvaa ohjelmassa esiintyvät "NULL":t "0":lla. NULL on standardi merkintä, joka tarkoittaa NULL-osoitinta eli osoitinta, joka ei osoita mihinkään. Usein tämä saattaa olla myös muodossa: #define NULL 0L jossa L 0:n perässä tarkoittaa, että kyseessä on long-vakio, eikä int. Tällä tosin ei ole merkitystä, jos kääntäjällä on int saman kokoinen kuin long. Joskus saattaa esiintyä myös muoto: #define NULL ((void *)0) Tässä korostetaan sitä, että NULL on tyypiltään osoitin. Tällaiset vakioita korvaavat nimekkeet ovat yleensä capitalisoituja. Tässä erityisesti on merkitystä sillä, että korvaaminen tehdään riippumatta siitä, missä yhteydessä jokin esiintyy. (Poikkeuksena ovat lähinnä merkki- jonovakiot, jotka esikäsittely jättää kokonaan rauhaan, eli ne voivat sisältää vaikka /*-merkkejä yms. sellaista, joka normaalisti merkitsisi jo- tain erityistä kääntäjälle.) Eli jos vaikka tehtäisiin: #define if fubar niin kaikki "if":t korvattaisiin "fubar":illa, eikä ohjelma luultavasti enää kääntyisi (yleensä ohjelmassa on "if":ejä). Makron nimessä (kuten muuttujankin) voi olla kirjaimia, numeroita ja '_'-merkkejä, eikä makron nimi voi alkaa numerolla (kuten ei voi muuttujan- kaan). Makron sisältöä sorsaan korvatessa etsitään edelleen makroja, joten voidaan myös määritellä makro suhteessa toiseen. Lisäksi pitää ottaa huomioon, että makrot korvaavat vain kokonaisen sanan. Ohjelmasta täytyy siis löytyä mak- ron nimi siten, että sitä ennen on jokin merkki, joka ei voi olla osa nimeä ja sen jälkeen on merkki, joka ei voi olla osa nimeä. Demonstroidaan edel- lisiä: #define ONE 1 #define TWO 2 #define THREE (ONE + TWO) void func(void) {{ int a, b, c, d; a = ONE; /* antaa a:lle arvon 1 */ b = TWO; /* b:lle 2... */ c = THREE; /* c:lle 3... */ d = ONETWO; /* ei toimi... */ } Esikäsittelyn jälkeen funktio näyttäisi suunnilleen seuraavalta: void func(void) {{ int a, b, c, d; a = 1; b = 2; c = (1 + 2); d = ONETWO; } Tulos olisi ollut sama, vaikka #define:jen järjestystä olisi muutettu, eli ONE ja TWO korvataan THREE:n sisällöstä vasta siinä vaiheessa, kun THREE sijoitetaan itse ohjelman joukkoon. Rivi, jolla on "d = ONETWO;" aiheuttaa virheen, jos tuota yrittää kääntää pitemmälle. Koska siitä ei olisi mitään hyötyä (pikemminkin haittaa, sillä kääntäjä jäisi jumiin), ei samaa makroa etsitä itsensä sisältä, vaan esimerkiksi: #define num (num + 1) Tämä ainoastaan korvaisi kaikki num:it (num + 1):llä, eikä turhaan yrittäisi rekursiivisesti laajentaa tuota muistin loppumiseen asti. Myöskään kahdella toisensa sisältävällä makrolla ei voi huijata kääntäjää jäämään jumiin, vaan esimerkiksi: #define mac1 ((mac2) * 2) #define mac2 ((mac1) - 10) Ohjelmassa oleva mac1 korvattaisiin ((((mac1) - 10)) * 2):lla, vastaavasti mac2 korvattaisiin ((((mac2) * 2)) - 10):lla. Useimmissa kääntäjissä pitäisi olla joitakin makroja tai vastaavia sisäisesti valmiiksi määriteltynä. __STDC__ pitäisi ANSI C:tä ymmärtävissä kääntäjissä ainakin olla määriteltynä, tosin sillä ei ole mitään määrättyä arvoa, koska se on tarkoitettu #ifdef:in (hieman myöhemmin) kanssa käytettäväksi. Lisäksi vastaavasti on yleensä määritelty jotakin muita vas- taavia, joiden pohjalta voidaan tunnistaa käytössä oleva C-kääntäjä, kone yms. Näistä yleensä löytyy luettelo C-kääntäjän mukana. Joitakin makroissa hyödyllisiä standardeja määritelmiä, jotka vaihtelevat kohdan mukaan: __LINE__ korvataan kokonaisluvulla, joka on sen rivin numero, jolla se sor- sassa esiintyy. __FILE__ korvataan merkkijonolla, joka on lähdekooditiedoston nimi, jossa se esiintyy. __DATE__ korvataan merkkijonolla, joka on senhetkinen päivämäärä. __TIME__ korvataan merkkijonolla, joka on senhetkinen kellonaika. __BASE_FILE__ korvataan merkkijonolla, joka on sen lähdekooditiedoston ni- mi, jota varsinaisesti käännetään. Muuten kuten __FILE__, mutta jos esiin- tyy esimerkiksi include-tiedostossa, niin ei korvaudu sen nimellä, vaan al- kuperäisen lähdekoodin. Hieman monimutkaisemmat makrot ovat sellaisia, joille voidaan antaa para- metreja. Nämä ovat hieman funktion näköisiä. Esimerkiksi: #define sum(a,b) ((a) + (b)) Makrossa esiintyvät sen "parametreiksi" määritellyt nimekkeet korvataan makron sisältä makron käytössä annetuilla parametreilla. Tämä on varsin hyödytön esimerkki, joka vain laskee kahden luvun summan. Sulkeet ovat a:n ja b:n ympärillä sitä varten, että laskujärjestys menee oikein vaikka para- metrit olisivat monimutkaisempiakin. Käytännössä tämän makron käyttö näyttäisi esimerkiksi tältä: int calcsomething(int a) {{ return sum(10 / a, 2 + a); } Tästä tulisi käytännössä: int calcsomething(int a) {{ return ((10 / a) + (2 + a)); } Jotkut käyttävät capitalisoituja nimiä myös tämäntyyppisille makroille. Erityinen merkitys makron sisällä on #- ja ##-merkinnöillä. # muuttaa sitä seuraavan kohdan merkkijonoksi. Tällöin kyseiselle kohdalle ei suoriteta mitään makrojenetsintää. Esimerkiksi: #define clrvar(var) var = 0; \ puts(__FILE__ ":" # __LINE__ ": variable " # var \ " cleared") Tässä tuli demonstroitua samalla makron jatkamista seuraavalle riville '\'-merkillä rivin lopussa. Toiminnaltaan ylläoleva makro laajennetaan sel- laiseksi, että se laittaa parametriksi annettuun makroon arvon 0 ja tulos- taa ilmoituksen tästä ilmoittaen myös lähdekooditiedoston nimen ja rivinu- meron, jolla tämä tehtiin. Makron käyttö näyttää funktiokutsulta, mutta kannattaa huomioida, miten tämä toimii missäkin tilanteessa. Esimerkiksi: void func(int p) {{ int a, b, c; clrvar(a); b = 10; c = p + b; if (p == 10) clrvar(b); } Oletetaan, että funktio alkaa vaikka riviltä 50 (jotainhan sitä ennenkin on täytynyt olla, kun tuo makrokin on olevinaan määritelty) lähdekooditiedos- tossa prog.c. Toiminnaltaan se ensin nollaa a:n ja ilmoittaa "prog.c:54: variable a cleared". Tämän jälkeen laitetaan muuttujaan b arvo 10 ja laske- taan muuttujaan c muuttujien p ja b summa. Verrataan p:tä kymmeneen ja jos se on kymmenen, niin nollataan muuttuja b. Mutta teksti "prog.c:58: variab- le b cleared" tulostetaan riippumatta siitä, nollattiinko b oikeasti eli oliko p:n arvo 10, koska vain makron ensimmäinen lause jää if:n ehdollisen koodin alueelle. Tämän voisi korjata käyttämällä aaltosulkeita, eli: if (p == 10) { clrvar(b); } Tuo ei kuitenkaan ole paras mahdollinen ratkaisu, erityisesti koska makron käyttö muistuttaa funktiokutsua, on helppo unohtaa että tuollainen olisi tarpeellista. Tämän takia on hyvä sisällyttää makroon itseensä jotain, joka tekee siitä yhtenäisen. Pelkkä lohko ei riitä, koska tällöin olisi makron käytön perässä tuleva ';' jätettävä pois, mikä saattaisi näyttää oudolta. Aiemmin mainittu do ... while (0) on tähän sopiva, voimme kirjoittaa makron muotoon #define clrvar(var) do { \ var = 0; \ puts(__FILE__ ":" # __LINE__ ": variable " # var \ " cleared"); \ } while (0) jolloin sen pitäisi toimia hyvin kaikissa mahdollisissa tilanteissa. Toinen mainittu merkintä, ##, "liimaa yhteen" sen molemmin puolin olevat osat, eli jättää välit pois. Tämän avulla voidaan makrossa käyttää useia erilaisia parametreistä johdettuja nimikkeitä. Voisimme esimerkiksi tehdä makron, joka ottaa parametriksi taulukon kohdan numeron ja siirtää siitä tiedon vastaavaan muuttujaan, joka on nimetty val_, jossa on taulukon kohdan numero: #define arr2var(n) val_ ## n = arr[n] Eli jos meillä on vaikka: long arr[10], val_1, val_2, val_3; void func(void) {{ arr2var(1); arr2var(2); arr2var(3); } Tästä funktio-osa käsitellään seuraavan näköiseksi: void func(void) {{ val_1 = arr[1]; val_2 = arr[2]; val_3 = arr[3]; } Tämä ei ole kovin hyödyllinen esimerkki, mutta monia tilanteita kuitenkin voi syntyä, joissa ## tulee tarpeeseen, joten sekin on hyvä tietää. Makron määrityksen voi poistaa komennolla #undef. Esimerkiksi: void func(void) {{ int c; #define VALUE 10 c = VALUE; #undef VALUE c = VALUE; } Esikäsittelyn jälkeen tämä on: void func(void) {{ int c; c = 10; c = VALUE; } Muista esikäsittelijän komennoista lienevät tärkeimpiä ehdolliseen käsitte- lyyn liittyvät komennot. Näistä perustoimintoja ovat: #ifdef #ifndef #if #else #endif Yleisesti ottaen ehdollinen käsittely toimii siten, että on ensin jokin eh- don määrittely (#if, #ifdef, #ifndef), sitten koodi, joka halutaan mukaan, jos ehto on tosi. Tämän jälkeen mahdollisesti #else ja se, mitä halutaan sisällyttää käännökseen, jos ehto on epätosi. Lopuksi #endif, josta alkaen otetaan kaikki normaalisti mukaan. Eri ehtoja ovat: #ifdef jossa on makron nimi. Jos kyseinen makro on määritelty, on ehto to- si. Jos halutaan esimerkiksi määritellä prototyyppi main():lle, jos kääntäjä on määritellyt __STDC__:n, voidaan tehdä: #ifdef __STDC__ extern int main(int, char **); #endif Mikäli haluaisimme tälle vaihtoehtoisen määritelmän, jos onkin kyseessä jo- kin vanhanaikaisempi kääntäjä, voidaan laittaa: #ifdef __STDC__ extern int main(int, char **); #else extern int main(); #endif #ifndef Tämä on vain käänteinen toiminto #ifdef:lle, eli se ottaa ehtoa seuraavan osan mukaan, jos ei ole määritelty. Usein NULL on määritelty useam- missa eri include-tiedostoissa, jolloin usein sen määrittelyn näkee muodos- sa: #ifndef NULL #define NULL 0 #endif Eli se määritellään vain, jos sitä ei ennestään olla määritelty. Samoin ko- ko includelle on yleensä määritelty jokin tunnus, jolla estetään sen sisältämän tiedon sisällyttäminen ohjelmaan useampaan kertaan. Includeja kannattaa muutenkin tutkia, niistä näkee paljon hyödyllisiä asioita. #if on monimutkaisin ehto. Sen perässä voidaan antaa periaatteessa tavalli- nen C-kielen lauseke, joka voi sisältää normaaleita operaattoreita, jotka toimivat samalla tavalla kuin yleensäkin. Numeroita voidaan käyttää suo- raan, ja määritellyt makrot toimivat niiden sisällön numeerisen arvon edus- tajina. Lisäksi voidaan käyttää merkintää defined(), joka on tosi jos sille annettu makro on määritelty. Ehtoja voidaan myös määritellä sisäkkäin. Jos haluamme esimerkiksi tutkia, millä ohjelmamme on käännetty: int main(int ac, char **av) {{ #ifdef __GNUC__ #if __GNUC__ >= 2 puts("Compiled using GCC 2.x.x or higher"); #else puts("Compiled using an ancient version of GCC"); #endif #else #ifdef _DCC puts("Compiled using DICE"); #else #ifdef LATTICE puts("Compiled using SAS/C"); #else puts("Compiled using an unknown compiler"); #endif #endif #endif return 0; } Tämä on jälleen huono esimerkki, koska tuon voisi tehdä paljon paremminkin, mutta... Yksi yleinen käyttö näille on, jos halutaan jättää ohjelmasta jokin osa pois. Ohjelman osa saatettaisiin myös kommentoida pois /* */ -merkkien vä- liin, mutta jos kyseisessä osassa on muutenkin jo kommentteja, ei tämä toi- misi (sisäkkäiset kommentit eivät toimi). Eli helppo tapa jättää jokin osa ohjelmaa pois on laittaa sitä ennen #if 0 ja sen jälkeen #endif. Tässä on jälleen hyvä huomioida esikäsittelyvaiheen erillisyys. Jos ehdon ansiosta jotakin ei sisällytetä ohjelmaan, sen sisällöllä ei ole lainkaan merkitystä. Se voi sisältää sellaistakin, joka ei kääntyisi kunnolla, jos se olisi osa ohjelmaa. Ohjelma kääntyisi aivan kiltisti (jos se on muuten oikein), vaikka sen keskellä olisi: #if 0 Trying to compile this text would be impossible, since a C-compiler doesn't understand english. It might also get even more messed up due to some extra }} characters. #endif Ainoa, jota ohjelmasta pois jätetystä osasta katsotaan, ovat toiset ehdol- liset komennot, jotta voidaan laskea, montako #endif:iä ja/tai #else:ä tar- vitaan normaalitilaan palaamiseen. On olemassa pari muutakin esikäsittelijäkomentoa, jotka voivat olla hyödyl- lisiä, mutta niitä harvemmin joutuu käyttämään: #line esiintyy usein koneen generoimassa koodissa, jos C-kielinen lähdekoo- di on generoitu jonkun toisentyyppisen lähteen pohjalta. Nämä kertovat kääntäjälle, missä kohtaa mitä tiedostoa nyt ollaan, jotta ne voivat il- moittaa virheen lähdetiedoston rivinä generoidun C-sorsan kohdan sijasta. #pragma on ANSIn mukaan yleiseen laajentamiseen tarkoitettu. Amigalla sitä useimmiten käytetään systeemikutsujen määrittelemiseen. Nämä ovat kääntäjästä riippuvaisia, ja jos kääntäjä tukee näitä, sen mukana tulee yleensä valmiina määritelmät systeemikirjastoja varten (includeina) ja apuohjelma, jolla voi generoida niitä muiden kirjastojen käyttöä varten. Olisi varmaan aiheellista mainita tässä yhteydessä myös trigraphit, koska ne liittyvät esikäsittelyyn. (Tuossa tulikin jo annettua liikaa tietoa, kun piti ainoastaan mainita...) {3main():in parametrit Kuten aikaisemmin on todettu, funktio main() saa kaksi parametria, tyy- peiltään "int" ja "char **". Näiden perinteiset nimekkeet ovat argc ja argv (itse käytän lyhyempiä nimiä, ac ja av), jotka tulevat luultavasti sanoista "argument count" ja "argument vectors". Nämä liittyvät ohjelman käynnistyk- sessä shelliin annettuihin komentoriviparametreihin. argc ilmoittaa, montako parametria komentorivillä annettiin. Tähän laske- taan mukaan myös itse ohjelman nimi, joten argc:n arvo on vähintään 1. argv on taulukko osoittimia merkkijonoihin, jotka sisältävät varsinaiset komentoriviparametrit. Viimeinen olemassaoleva parametri on aina kohdassa argv[argc - 1]. argv:n voi määritellä myös "char *argv[]":ksi, se tarkoit- taa tässä yhteydessä samaa. argv[argc] on aina NULL ja sen jälkeen kaikki ovat määrittelemättömiä. Jatkoa seuraavassa osassa.