Creative Commons License

Johdanto

Yhteenveto materiaalin tuottajista:

  • Sisällöt: Pasi Sarolahti

  • Web-sivu rakennetty Markus Holmströmin laatimilla skripteillä

  • Kiitokset:

    • TMC-kehittäjät (Matti Luukkainen, Jarmo Isotalo, Tony Kovanen, Martin Pärtel) ovat tukeneet palautusjärjestelmän käyttöönotossa ja auttaneet alkuun tehtävien laatimisessa.
    • Jotkut osat kurssimateriaalista perustuvat Raimo Nikkilän aiemmin vetämään kurssiin
    • Nykyiset ja aiemmat kurssiassistentit ovat antaneet paljon palautetta sisältöä koskien, laatineet tehtäviä ja tehneet bugikorjauksia. Heihin kuuluvat mm. Riku Lääkkölä, Konsta Hölttä, Markus Holmström, Essi Jukkala, Tero Marttila ja Aapo Vienamo.

Alkusanat

Tämä materiaali ei ole tarkoitettu kattavaksi C-referenssiksi, vaan päämääränä on tarjota riittävä informaatio tukemaan C-ohjelman opiskelua ohjelmointitehtävien kautta. Lisätietoa saa verkosta tai aiheeseen liittyvästä kirjallisuudesta. Usein käytetty oppikirja on "The C Programming Language" (2. painos), jonka ovat kirjoittanete Brian W. Kernighan and Dennis M. Ritchie. Myöhemmin saatetaan viitata tähän kirjaan lyhyesti K&R:nä.

Tärkeä osa C-kielen (ja ohjelmoinnin) opiskelua on tehdä runsaasti ohjelmointiharjoituksia, joita on sisällytetty materiaalin sekaan. Tehtävät on pyritty sijoittelemaan siten, että tiettyyn aihesisältöön liittyvä tehtävä löytyy tekstin läheltä. Tarkoitus on, että alat lukea sisältöä alusta järjestyksessä, ja teet tehtäviä sitä mukaa kun niitä tulee vastaan.

Tehtäviä tehdään karkeasti ottaen seuraavan kaavan mukaan (katso tarkemmat ohjeet erilliseltä sivulta):

  1. Kirjoita koodi editorilla
  2. Käännä koodi käyttäen tehtäväpohjien mukana tulevia Makefile:jä. Jos käännös tuottaa virheitä tai varoituksia, hankkiudu niistä eroon korjaamalla koodiasi, kunnes varoituksia tai virheitä ei enää tule.
  3. Suorita käännetty ohjelmasi ajamalla src/main - ohjelma jonka kääntäjä on tuottanut tehtävähakemistoon. Mikäli ohjelma ei toimi odotetulla tavalla, palaa kohtaan 1 ja korjaa ohjelmaa
  4. Aja paikalliset TMC-testit. Mikäli testit eivät mene läpi, palaa kohtaan 1 ja korjaa ohjelmaa annettujen virheilmoitusten perusteella. Paikalliset testit eivät välttämättä toimi kaikilla alustoilla (esim. Windows). Mikäli paikallinen testi ei toimi, siirry suoraan kohtaan 5.
  5. Jos paikalliset TMC-testit menivät läpi, lähetä koodisi TMC-serverille. Se tarkastaa testit uudestaan, mahdollisesti eri testejä käyttäen kuin paikallinen testi. Mikäli nämä testit menevät onnistuneesti läpi, olet suorittanut tehtävän. Mikäli testeistä tulee virheitä, palaa takaisin kohtaan 1.

Kurssilla on yhteensä 84 tehtäväkohtaa. Osa tehtäväkohdista kytkeytyy toisiinsa ja muodostaa yhdessä yhden hieman isomman ohjelman.

Ensimmäisessä modulissa käydään läpi C-kielen perusteet ja perusrakenteet. Näitä ovat perustietotyypit, funktiot ja kontrollirakenteet. Lisäksi katsomme lyhyesti kuinka tulostus ja syöte toimivat C-ohjelmassa. Materiaalissa on oletettu, että jotain pohjatietoa ohjelmoinnista on (esimerkiksi "Johdatus ohjelmointiin" - kurssi), mutta se on toisaalta pyritty kirjoittamaan niin, että aloittelevakin ohjelmoija pääsee kärryille.

Hauskaa koodailua!

Johdantoa C-kieleen

C on proseduraalinen ohjelmointikieli, jossa ohjelmat rakentuvat funktioista ja datasta, joka organisoidaan tietorakenteiksi. C ei tue erillisiä nimiavaruuksia tai luokkia, joten kaikki funktiot ja muut nimetyt asiat (globaalit muuttujat, tietotyypit, jne.) ovat samassa globaalissa tietorakenteessa.

C-ohjelma kirjoitetaan tekstimuotoiseen lähdekooditiedostoon, joka miltei aina käyttää .c - päätettä tiedostonimessään. Tyypillisesti ohjelma koostuu useista tällaisista tiedostoista, jotka saattavat olla jäsennelty useisiin hakemistoihin vähänkään isommassa ohjelmassa. Lisäksi on .h - päätteisiä otsaketiedostoja, joissa määritellään tietotyyppejä, tietorakenteita, funktiorajapintoja, jne. Näitä tiedostoja voi käsitellä millä tahansa tekstieditorilla (kuten emacs, vi, kate, jne.). Tämän lisäksi nykyään käytetään usein integroituja kehitysympäistöjä (IDE), jotka yhdistävät editorin ja muita kehitykseen tarvittavia työkaluja yhtenäisen graafisen käyttöliittymän alle. Tällaisia ovat esimerkiksi Microsoftin Visual Studio, Applen XCode tai Netbeans.

Lähdekoodi ei toimi sellaisenaan, vaan se pitää kääntää kääntäjällä, joka muuttaa koodin tietokoneen ymmärtämäksi binääriformaatiksi. Kääntäjän tuottama ohjelma sovittuu alla olevaan tietokonearkkitehtuuriin. Siksi käännettyä ohjelmaa ei yleensä voi siirtää koneesta toiseen. Sen sijaan lähdekoodin on tarkoitus esittää ohjelma järjestelmästä riippumattomalla tavalla. Kun ohjelma siirretään uuteen järjestelmään, se pitää siis kääntää uudestaan lähdekoodista. Samoin, kun ohjelman lähdekoodia muutetaan, se pitää kääntää uudestaan. Tämä on merkittävä ero moniin korkeamman tason ohjelmointikieliin, kuten Pythoniin.

C-ohjelman käännös koostuu seuraavista vaiheista:

  • Esikäännös: Muuntaa ohjelman makrot, otsaketiedot, jne. pelkistetyksi C-koodiksi, jota on tyypillisesti vaikeampi lukea, mutta on edelleen tekstimuotoista. Esikääntäjän toiminnasta tarkemmin myöhemmin kurssilla (Moduli 5).

  • Käännös: Muuntaa tekstimuotoisen ohjelman konekohtaiseksi objektitiedostoksi, jonka ymmärtäminen on vaikeaa tavalliselle pulliaiselle. Kutakin C-lähdetiedostoa kohden muodostetaan vastaava objektitiedosto. Koska yleensä ohjelmat koostuvat useista lähdetiedostoista, tuloksena syntyy myös useampia objektitiedostoja.

  • Linkkaus: Yhdistää objektitiedostot toimivaksi ohjelmaksi. Lisäksi mukaan otetaan ulkopuolisia ohjelmakirjastoja, joita ohjelma saattaa käyttää esimerkiksi laskutoimituksiin tai grafiikkaan littyen. Eri C-ohjelmat käyttävät erilaisia kirjastoja tarpeen mukaan, mutta kaikki ohjelmat käyttävät standardikirjastoa, joka toteuttaa monia perustoimintoja järjestelmään liittyen, kuten syöte, tulostus, tiedostonkäsittely tai muistinhallinta.

Ohjelman suoritus alkaa aina main - funktiosta, jonka täytyy löytyä jostain mukaan linkatusta C-lähdetiedostosta.

Näiden vaiheiden hahmottaminen on tärkeää, jotta osaa jäsennellä C-käännöksestä mahdollisesti tulevia virheilmoituksia. Tyypillisesti kääntäjä tekee kaikki vaiheet peräjälkeen automaattisesti, mutta sitä voi myös pyytää suorittamaan edellä mainitut askelet erikseen.

Kaksi tällä hetkellä yleisintä kääntäjää ovat gcc ja clang, jotka tekevät saman asian, mutta tuottavat hieman erilaista tietoa esimerkiksi virhetilanteissa. Näitä voi käyttää joko komentoriviltä tai IDE:n kautta, tyypillisesti jotain käyttöliittymän nappulaa painamalla.

C-kielestä löytyy paljon lisätietoa verkosta (tai kirjoista). Yksi lähtöpiste on Wikipedia, joka kertoo kielen historiasta ja muuta yleiskuvausta.

Yleinen aloittelevien ohjelmoijien käyttämä tietolähde on myös Stack Overflow, joka on vastauspalvelu erilaisiin ohjelmointiin liittyviin ongelmiin. Yhdessä Googlen (tai muiden hakupalvelujen) kanssa moniin ongelmiin saattaa löytyä ratkaisu siis pelkän verkkohaun kautta. Näiden käyttö saattaa kuitenkin olla petollista: yksittäisen ongelman ratkaisemisen lisäksi on tärkeää, että ymmärrät syvällisemmin mikä oli ongelma, ja miksi ratkaisu toimi. Koska Stack Overflowssa kuitenkin löytyy mielenkiintoisia tiedonjyväsiä, kiinnostavia kysymyksiä ja vastauksia on linkitettynä tähänkin materiaaliin sinne tänne.

TMC:n toiminta

Harjoitustehtävissä käytettävä TMC toimii seuraavalla tavalla:

  • Opiskelijan lähettämä lähdekoodi käännetään palvelimella olevalla kääntäjällä. Tämä on usein eri kääntäjä tai ohjelmaversio kuin paikallisella koneellasi käyttämä, joten varoitukset ja virheilmoitukset saattavat näyttää erilaisilta.

  • Lisäksi tehtäväpaketissa on kurssihenkilökunnan kirjoittama tarkistinkoodi, joka on myös C:tä. Tarkistinkoodi kutsuu opiskelijan toteuttamia funktioita ja vertailee tulosta "mallitulokseen". Mikäli nämä poikkeavat, TMC ei hyväksy ratkaisua.

  • Kurssihenkilökunnan koodi ja opiskelijan koodi linkitetään suoritettavaksi testiohjelmaksi. Lisäksi mukaan otetaan check - kirjasto, joka sisältää työkaluja testauksen helpoittamiseksi. Kirjastoa käytetään yleisemminkin yksikkötestaukseen isommissakin ohjelmistoissa, jolloin halutaan varmistaa että ohjelman funktiot toimivat halutulla tavalla.

  • Palvelin ajaa ohjelman ("test/test"), joka tuottaa tulostetta, jossa kerrotaan toimivatko opiskelijan funktiot odotetulla tavalla, vai oliko niissä virheitä. Nämä tiedot toimitetaan palvelimelle ja opiskelijan nähtäväksi.

Opiskelijan tuottama koodi on aina tehtäväpohjien src - hakemistossa, ja kurssihenkilökunnan koodi test - hakemistossa. Testiohjelmaan ei koskaan linkitetä mukaan src/main.c - tiedostoa, vaan se käyttää test/test_source.c - tiedostosta löytyvää main() - funktiota ohjelman käynnistämiseksi. Toisin sanoen voit muokata src/main.c - tiedostoa miten vain: testeri ei tiedä siitä mitään.

Ensimmäinen C-ohjelma

Alla on hyvin yksinkertainen C-ohjelma. Se tulostaa yhden rivin tekstiä ja lopettaa sen jälkeen saman tien.

1
2
3
4
5
6
7
#include <stdio.h>

int main(void)
{
    /* The following line will print out some text */
    printf("Hey! How are you?\n");
}

Ohjelman ensimmäinen rivi kertoo, että aiomme käyttää standardi-I/O - funktioita, jotka on toteutettu C:n standardikirjastossa. Otamme siis ohjelmaan mukaan stdio.h - otsaketiedoston, jossa kyseiset funktiot on määritelty. Näitä funktioita käytetään esimerkiksi tekstin tulostamiseen ruudulle, tai käyttäjän syötteen lukemiseen. Palaamme kirjaston toimintaan tarkemmin myöhemmin. Tämän rivin käsittelee esikääntäjä, mikä on ilmaistu rivin alussa olevalla # - merkillä.

Kuten mainittua, jokaisen suoritettavan C-ohjelman täytyy sisältää main-funktio, jota järjestelmä kutsuu kun ohjelman suoritus käynnistyy. main - funktion määrittely näkyy rivillä 3. Palaamme hetken kuluttua siihen, mitä "int" ja "void" kyseisellä rivillä tarkoittavat.

main - funktion toteutus on sisällytetty ohjelmalohkoon, joka merkitään aaltosuluilla (riviltä 4 riville 7). Funktio (tai ohjelmalohko) sisältää lauseita, jotka suoritetaan järjestyksessä. Nämä muodostavat funktion toiminnan. Jokainen lause päättyy puolipisteeseen. Tässä funktiossa on siis vain yksi lause rivillä 6. Puolipisteen puuttuminen aiheuttaa aina käännösvirheen ohjelmaa käännettäessä.

Rivi 5 ei ole C-kielen lause, vaan kommentti, joka dokumentoi ohjelmaa lukevalle koodarille ohjelman toimintaa. Kommentti alkaa merkeillä /* ja päättyy merkkeihin */, ja se voi sijoittua useammalle riville. Esikäännösvaiheessa kaikki kommentit poistetaan, koska ne eivät vaikuta ohjelman toimintaan mitenkään. Tässä tapauksessa kommentti on mukana lähinnä esimerkin vuoksi (kokematonkin koodari todennäköisesti arvaa, mitä kommenttia seuraava rivi tekee). Monimutkaisemmissa ohjelmissa kommentteja kannattaa käyttää silloin kun ohjelmalogiikan toiminta ei välttämättä aukea ensi lukemalta. (valitettavasti kommentteja ei käytetä läheskään aina silloin kun siitä olisi hyötyä)

Ohjelman ainoa C-kielinen lause rivillä 6 kutsuu standardikirjaston printf - funktiota, joka tulostaa tekstiä ruudulle. Tämä funktio on määritelty stdio.h otsakkeessa. Mikäli rivi 1 olisi unohtunut ohjelmasta, ohjelma ei kääntyisi, koska kääntäjä ei tällöin tietäisi printf - funktiosta mitään.

Tulostettava teksti (eli merkkijono) on eristetty lainausmerkein. Lisäksi merkkijonon lopussa on \n, mikä kertoo järjestelmälle, että tulostetun huudahduksen perään täytyy siirtyä seuraavalle riville käyttäjän konsolilla.

Funktiokutsun tunnistaa ohjelmakoodissa siitä, että funktion nimen jälkeen seuraavat aina sulkeet. Sulkeiden sisällä on lista (pilkulla erotettuja) parametreja, jotka toimitetaan funktiolle. On myös mahdollista, että funktiolla ei ole parametreja lainkaan. Tässä tapauksessa printf-funktiokutsussa rivillä 6 oli siis yksi **parametri, joka oli merkkijono. C-kieli on tarkka siitä että tätä rakennetta noudetaan.

Kun ohjelma käännetään, ja tuloksena syntynyt koodi suoritetaan, ruudulle tulostuu (yllättäen?) teksti "Hey! How are you?". Tässä kohtaa voit testata kääntäjän käyttämistä: kopioi ohjelma omaan tekstieditoriisi, tallenna tiedosto esimerkiksi nimellä "hello.c", ja käännä se. Jos käytät komentoriviä, voit esimerkiksi kokeilla komentoa

gcc -o hello hello.c

Kun nyt suoritat ohjelman kirjoittamalla "./hello", jotain pitäisi näkyä.

IDE:n kanssa toimittaessa ei komentoriviä käytetä. Sen sijaan luot uuden ohjelmaprojektin, kirjoitat koodin kehittimen luomaan pohjaan (etsi sieltä main-funktio), ja painelet käynnistys- ja suoritus-nappuloita.

Merkittävä piirre C:ssä -- esimerkiksi Pythonista poiketen -- on, että C-kääntäjä ei välitä ohjelman muotoilusta lainkaan (tosin esikääntäjä välittää, mutta siitä lisää myöhemmin). Voit esimerkiksi kirjoittaa pitkänkin rimpsun puolipisteellä erotettuja lauseita yhdelle riville. Tästä huolimatta on tärkeää, että ohjelmat on muotoiltu kauniilla ja johdonmukaisella tavalla, jotta niiden lukeminen olisi helppoa. Kun ohjelma kasvaa, ja jos se on huonosti muotoiltu, sen toimintaa on hyvin vaikea ymmärtää. Useimmiten saman koodin parissa työskentelee useampia ihmisiä, joten koodin luettavuus on tärkeää.

Joitain hyviä ohjelmointikäytäntöjä:

  • Sisennä koodi johdonmukaisesti: aina kun uusi ohjelmalohko alkaa, lisää sisennystä. Kun lohko päättyy, vähennä sisennystä sama määrä. Asia ei kiinnosta C-kääntäjää, mutta helpotat lähipiirisi (esim. työkaverit, C-assarit) elämää sisentämällä koodin selkeästi.

  • Nimeä funktiot, tietotyypit ja muuttujat johdonmukaisesti.

  • Jaa ohjelma selkeisiin ja johdonmukaisiin funktioihin, jotka eivät ole liian pitkiä

  • Käytä kommentteja silloin kun tarpeen, eli kun koodissa on syheröistä logiikka joka ei välttämättä aukea ensi lukemalla

Suuri osa C-ohjelmointiin tarkoitetuista editoreista tai IDE:istä pyrkivät sisentäämään koodin automaattisesti. Toisinaan ohjelmoijan ja editorin käsitykset hyvästä sisennyksestä saattavat tosin poiketa toisistaan.

Task 02-intro-1: Hei C! (1 pts)

Tavoite: Testaa, että kehitysympäristösi toimii, ja voit palauttaa harjoituksia TMC:hen. Saat myös ensituntuman C-ohjelmointiin.

Toteuta funktio three_lines, joka löytyy tiedostosta source.c TMC:n harjoituspohjassa. Funktion tulee tulostaa kolme riviä seuraavaan tyyliin (rivinumerot eivät ole osa tulostetta):

1
2
3
January
February
March

Myös viimeisen rivin lopussa pitää olla rivinvaihto.

Tietotyypit ja muuttujat

Tietokoneohjelma sijoittuu tietokoneen muistiin, johon se on tallennettu binäärisessä muodossa. Ohjelman toiminta muodostuu karkeasti ottaen 1) koodista, joka sisältää ohjeet tietokoneen prosessorille ohjelman suorittamiseksi, sekä 2) datasta, johon tallennetaan ohjelman suorituksen aikana käytettävää tilatietoa. C-ohjelmassa (ja monissa muissa ohjelmointikielissä) data esitetään muuttujien avulla. Kullakin muuttujalla on tietotyyppi joka kertoo minkälaisen lukujoukon muuttujalla voi esittää. Tietotyypin perusteella määräytyy myös se, kuinka monta bittiä muuttuja tarvitsee tietokoneen muistista.

Ohjelmoija nimeää muuttujat, joihin viitataan myöhemmin ohjelmakoodissa. Nimen lisäksi ohjelmoija määrittää myös muuttujan tietotyypin. Nimi on melko vapaasti valittavissa: se voi sisältää aakkosia, numeroita ja alaviivan (_), joskaan ensimmäinen merkki ei saa olla numero. Kirjainmerkeissä isot ja pienet kirjaimet tulkitaan eri merkeiksi. Tietotyypin pitää olla jokin kääntäjän tuntemista tietotyypeistä. C:ssä on muutama perustietotyyppi, sekä lisäksi ohjelmoija voi määrittää uusia tietotyyppejä itse. C-kääntäjä tarkistaa tietotyyppien yhteensopivuuden ja oikean käytön jo käännösaikana: väärä tietotyyppi saattaa aiheuttaa käännösvaroituksen tai -virheen, mutta tietyissä tapauksissa jää huomaamatta. Alla esimerkki muuttujan "numero" määrittelystä. Muuttuja käyttää int - tietotyyppiä, joka esittää kokonaislukua:

int numero;

C-kielessä muuttujat on aina esiteltävä ennenkuin niitä voi käyttää muualla ohjelmakoodissa. Määrittelyn ansiosta kääntäjä osaa varata muuttujalle sen tarvitseman tilan tietotyypin perusteella.

Toinen merkittävä C-kielen ominaispiirre on, että muuttujan alkuarvo on tuntematon esittelyn jälkeen. Emme siis tiedä edellä olevan rivin jälkeen mitään "numero" - muuttujan arvosta, ennenkuin se on ohjelmassa määritelty: se voi olla 0, tai se voi olla -3295957.

Kokonaisluvut

Suurin osa C-ohjelmissa käytetyistä muuttujista esittää kokonaislukuja, ja ovat jotain kokonaislukutietotyyppiä. Kokonaislukutietotyyppejä on useita, ja ne eroavat siinä, millaista lukujoukkoa ne voivat esittää, sekä siinä kuinka paljon tilaa ne tarvitsevat tietokoneen muistista. C-kielessä on valmiiksi määriteltynä seuraavat kokonaislukutietotyypit:

  • char -- koko 8 bittiä (eli yksi tavu), esittää joko etumerkillisiä lukuja välillä -127 ja 127, tai etumerkittömiä lukuja välillä 0 ja 255.
  • short int -- 16 bittiä (2 tavua), etumerkillisiä lukuja välillä -32767 to 32767, tai etumerkittömiä lukuja välillä 0 to 65535.
  • int -- vähintään 16 bittiä, mutta useimmiten 32 bittiä (4 tavua), etumerkillisiä lukuja välillä -(231 - 1) ja 231 - 1, tai etumerkittömiä lukuja välillä 0 to 232 - 1.
  • long int -- vähintään 32 bittiä, mutta voi olla 64 bittiä (8 tavua)
  • long long int -- 64 bittiä, etumerkillisiä lukuja välillä -(263 - 1) ja 263 - 1, etumerkittömiä lukuja välillä 0 ja 264 - 1.

Kukin edellämainituista tyyppimäärittelyistä voi olla etumerkillinen tai etumerkitön. Etumerkittömyys ilmaistaan unsigned - avainsanalla ennen tietotyypin nimeä. Muuttujat ovat oletusarvoisesti etumerkillisiä (paitsi mahdollisesti char tyypin kanssa), mutta etumerkillisyyden voi myös ilmaista ohjelmassa halutessaan signed - avainsanalla.

Yksi C-kielen ominaispiirre on, että monien perustietotyyppien varsinaista kokoa (ja siten mahdollista arvoaluetta) ei ole tarkkaan standardoitu, vaan se voi vaihdella eri tietokonealustoilla ja kääntäjillä. Tämän takia edellä olevassa listassa on myös käytetty hieman epämääräisiä ilmaisuja: emme esimerkiksi varmasti voi tiettää, minkä kokoinen tietotyyppi int on allaolevassa järjestelmässä.

long int ja short int voidaan ohjelmassa ilmaista lyhyemmin long and short - avainsanoilla, ja näin yleensä tehdäänkin.

Alla on muutamia esimerkkejä muuttujien määrittelystä yllä olevia tietotyyppejä käyttäen. Esimerkistä käy ilmi myös se, että muuttujan esittelyn yhteydessä sille voidaan määritellä alkuarvo. Mikäli näin ei tehdä, kuten muuttujan varC yhteydessä rivillä 6, muuttujan arvo jää tuntemattomaksi. Emme voi olettaa sen olevan 0, tai mitään muutakaan. Määrittelemättömien muuttujien käyttö on yleinen virhe aloittelevilla C-ohjelmoijilla.

1
2
3
4
5
6
7
8
int main(void)
{
    char varA = -50;
    unsigned char varB = 200;
    unsigned char varB2 = 500;  // Error, exceeds the value range
    int varC;  // ok, but initial value is unknown
    long varD = 100000;
}

Yllä olevassa esimerkissä näkyy myös toisenlainen tapa ohjelman kommentointiin. Kun ohjelmarivillä on kaksi kenoviivaa (//), kaikki niiden perässä oleva teksti tulkitaan kommentiksi rivin loppuun asti. Seuraava rivi tulkitaan jälleen normaaliksi C-ohjelmakoodiksi. Koska C on muotoilun suhteen vapaamielinen, tällainen kommentti voidaan lisätä C-kielisen ohjelmarivin perään.

Edellisessä esimerkissä kannattaa huomioida myös puolipisteiden käyttö. Jokainen muuttujan esittely on lause, jonka perään tulee laittaa puolipiste.

Rivillä 5 yllä olevassa ohjelmassa sijoitetaan liian suuri luku unsigned char - tyyppiseen muuttujaan. Kääntäjä todennäköisesti varoittaa tästä, mutta kääntää siitä huolimatta ohjelman, ja se on mahdollista suorittaa. Ohjelman toiminta on kuitenkin suurella todennäköisyydellä virheellistä, koska muuttujaan on tallentunut väärä arvo. C-kääntäjä tekee parhaansa tuottaakseen suoritettavan ohjelman, mikä on monesti petollista, koska tässäkin tapauksessa ohjelma on selvästi virheellinen ja tulisi toimimaan väärin. Kääntäjän tuottamat varoitukset tuleekin aina ottaa vakavasti ja kyseiset rivit korjata.

Tiedoksi: mikäli epävarmuus tietotyypin varsinaisesta koosta häiritsee, ja haluat eksplisiittisesti määritellä muuttujan koon, C99 - standardi sisältää seuraavat tietotyyppimäärittelyt. Nämä sisältyvät stdint.h - otsakkeeseen, joka tulee sisällyttää #include <stdint.h> - ohjeella ohjelman alussa:

  • uint8_t, int8_t: etumerkitön ja etumerkillinen 8-bittinen kokonaisluku.
  • uint16_t, int16_t: etumerkitön ja etumerkillinen 16-bittinen kokonaisluku.
  • uint32_t, int32_t: etumerkitön ja etumerkillinen 32-bittinen kokonaisluku.
  • uint64_t, int64_t: etumerkitön ja etumerkillinen 64-bittinen kokonaisluku.

Vakiot ovat kiinteitä arvoja, jotka ohjelmoija antaa ohjelmaa kirjoittaessaan. Kokonaislukuvakiot ovat oletusarvoisesti int - tietotyyppisiä, ellei toisin määritellä. Edellä olevassa ohjelmassa käytimmekin (desimaalimuotoisia) vakioita muuttujien alkuarvon määrittelyyn. Vaikka useimmiten käytetäänkin tuttuja desimaalivakioita, voidaan myös käyttää heksadesimaalilukuja (eli 16-kantaista lukujoukkoa). Nämä merkitään käyttämällä 0x - etuliitettä ennen varsinaista lukua. Heksadesimaaliluvuissa käytetään numeroiden 0-9 lisäksi merkkejä A, B, C, D, E ja F. Nämä voivat esiintyä vakiossa joko pienellä tai isolla kirjoitettuna (vaikka nimissä C erotteleekin isot ja pienet kirjaimet).

Lisäksi voidaan käyttää oktaalivakioita, jotka ovat 8-kantaisia ja voivat siten sisältää lukuja 0-7. Oktaalivakiot merkitään 0-etuliitteellä. Näitä kuitenkin näkee nykyään harvemmin käytettävän.

Alla esimerkkejä erilaisten vakioiden käytöstä.

1
2
3
short a = 012; /* set variable a to octal 012, equal to decimal 10 */
short b = -34; /* just using decimal number here */
short c = 0xffff; /* hexadecimal constant, equal to decimal 65535 */

On oleellista huomioida, että erikantaiset vakiot ovat vain erilaisia esitysmuotoja samankaltaisille luvuille. Esimerkiksi 0xff esittää samaa lukuarvoa kuin 255 tai 0377.

Mikäli vakiolla halutaan esittää long - muotoista pitkää kokonaislukua, tulee numeroarvon perään lisätä L. Esimerkiksi: long la = 10000000000L;.

Vaikka C:n tietotyypit on staattisesti määritelty käännösaikana, C-kääntäjä pyrkii tekemään tarpeen mukaan tyyppimuunnoksia kokonaislukutyyppien välillä automaattisesti käännöksen yhteydessä. Tämä mahdollistaa int - tyyppisten vakioiden käyttämisen esimerkiksi char - tyyppisen muuttujan yhteydessä. Näiden kanssa tulee kuitenkin olla tarkkana, jotta muuttujalle määriteltyä lukuarvoa ei ylitetä, kuten edellä olleessa esimerkissä nähtiin. Tällöin tuloksena on epämääräisen lukuarvon päätyminen muuttujaan.

Liukuluvut

Jos halutaan esittää erityisen suuria lukuja, tai kokonaislukujen osia, tulee käyttää liukulukutyyppiä. Koska tietokoneen muisti on rajallinen ja koostuu diskreeteistä biteistä, rajatun kokoisilla liukuluvuilla ei voi esittää rajattoman suuria tai pieniä arvoja, vaan ne esittävät tiettyjä pisteitä jatkuvassa (ja todellisuudessa äärettömässä) lukuavaruudessa.

Sisäisesti liukuluvun esitys tietokoneen muistissa koostuu kolmesta komponentistä:

lukuarvo = (-1)etumerkki * 1.mantissa * 2eksponentti

Kullekin komponentille on varattu liukulukuesityksessä tietty määrä bittejä riippuen käytetystä liukulukutyypistä. Tämä määrittää sen, kuinka tarkasti liukulukuesitys pystyy lukuavaruuden esittämään.

C-kääntäjä tuntee kolmenlaisia liukulukutyyppejä:

  • float -- 32 bittä (1b etumerkki + 23b mantissa + 8b eksponentti)
  • double -- 64 bittiä (1b etumerkki + 52b mantissa + 11b eksponentti)
  • long double -- 80 or 128 bittiä

Koska bittejä on käytössä rajoitettu määrä, liukuluvuillakaan ei voi esittää kaikkia äärettömän reaalilukuavaruuden lukuja. Siksi liukuluvuilla toimitettujen laskutoimitusten tulokset saattavat pyöristyä arvoksi joka ei aivan täsmällisesti esitä matemaattisesti oikeaa tulosta. Tämä on hyvä ottaa huomioon esimerkiksi lukujen yhtäsuuruutta vertaillessa.

Yleensä C-ohjelmassa käytetään oletusarvoisesti kokonaislukuja, ellei ole erityistä syytä käyttää liukulukuja. Kokonaislukujen käsittely on tyypillisesti prosessorille tehokkaampaa, vaikka liukuluvutkin pyörivät nykyarkkitehtuureissa varsin jouhevasti.

Lisätietoa liukuluvuista löytyy esimerkiksi aiheeseen liittyvästä Wikipedia -artikkelista.

Liukulukuvakiot voivat olla joko perinteistä desimaalilukuformaattia (esim. 1.543), tai eksponenttiformaattia (esim. 1e-2), tai molempien yhdistelmä. Oletustyyppi liukulukuvakioilla on double, mutta vakion voi pakottaa myös float-tyyppiseksi lisäämällä F vakion perään. Alla esimerkkejä:

1
2
3
float d = 0.534;
double e = 2e10;
float g = 0.111F;

Merkkivakiot

Tietokonejärjestelmässä merkit (esimerkiksi kirjaimet) ovat vain erilainen (256-kantainen) esitysmuoto samoille lukuarvoille, joita voi esittää esimerkiksi desimaalilukujen avulla. Merkkien lukuarvot esitetään perinteisesti ASCII-koodauksella, vaikkakin vaihtoehtoisia merkkikoodauksia on lukuisia. ASCII-taulukosta näkee miten erilaiset 8-bittiset arvot muuntuvat merkeiksi, ja mikä esimerkiksi vastaava desimaaliesitys olisi. Merkkivakioita voi käyttää samojen kokonaislukutyyppisten muuttujien kanssa kuin numerovakioitakin, ja ne erotellaan yksinkertaisilla lainausmerkeillä (') esimerkiksi näin:

int char_A = 'A';

ASCII-taulusta näemme, että 'A' vastaa desimaalilukua 65. Jos tulostaisimme muuttujan char_A arvon kokonaislukuja käyttäen, ruudulle ilmaantuisi numero 65. Jos tulostaisimme desimaaliluvun 66 merkkikoodausta käyttäen, ruudulle ilmestyisi 'B' (ilman lainausmerkkejä).

Joskus on tarkkaa huomioida puhutaanko merkeistä vai lukuvakioista. Esimerkiksi ASCII-koodauksen mukainen merkkivakio '1' vastaa kokokonaislukua 49. Siten lainausmerkkien unohtaminen tai mukaanottaminen muuttaa esitetyn lukuarvon täysin toiseksi.

Merkkivakioita käytetään esimerkiksi silloin, kun käyttäjälle tulostetaan tekstiä, tai käyttäjän syötteestä luetaan tekstiä.

Lausekkeet

Ohjelman toiminta ja sen lauseet sisältävät tyypillisesti lausekkeita, eli operaatioita jotka tuottavat jonkin tulokset. Lausekkeet on usein on sidottu toisiinsa käyttäen aritmeettisiä operaattoreita. Perusoperaattori on sijoitus (=), joka asettaa operaattorin vasemmalla puolella olevan muuttujan vastaamaan sijoituksen oikealla puolella olevassa lausekkeessa esitettyä arvoa. Vakiot voivat esiintyä vain sijoitusoperaattorin oikealla puolella. Sijoituksen oikealla puolella olevien lausekkeiden osana voi esiintyä myös muuttujia tai funktioita, eli toisin sanoen komponentteja, jotka tuottavat tuloksenaan jonkin arvon jota voi käyttää lausekkeen osana.

Sijoitusoperaattori itseään voi myös käyttää osana lauseketta, joka on toisen sijoitusoperaattorin oikealla puolella. Tällöin sen arvo lausekkeessa vastaa oikeanpuolisen sijoituksen tulosta.

Lausekkeita voi käyttää myös lauseissa joissa ei ole sijoitusoperaattoreita. Tällainen tilanne on yleensä funktiokutsut, joiden paluuarvo ei kiinnosta ohjelmaa. Funktioiden parametreissa voi käyttää mitä tahansa lausekkeita, joita muuallakin C-ohjelmassa.

Alla oleva esimerkki esittää sijoitusoperaattorin käyttöä, ja miten se toimii silloin kun samassa lauseessa on useita sijoituslausekkeita.

1
2
3
4
int var;  // arvo on tuntematon
var = 10;   // nyt arvo tiedetään
var = 20;
int varB = var = 20 + 10;  // molempiin muuttujiin tulee 30

Suurin osa aritmeettisista operaattoreista toimii kuten normaalisti muillakin ohjelmointikielillä. Operaattoreita ovat esimerkiksi + (plus), - (miinus), * (kertolasku), / (jakolasku) ja % (jakojäännös). Jakojäännösoperaattori toimii vain kokonaisluvuilla, mutta muita edellä mainittuja operaattoreita voi käyttää kaikkien numeeristen tyyppien kanssa. Laskujärjestys toimii kuten yleensä, eli kerto- ja jakolaskut arvioidaan ensin, ja suluilla voi ohjata laskujärjestystä. Alla joitain esimerkkejä:

1
2
3
4
float fa = 5.0 / 2; /* '5.0' kertoo että käytetään liukulukuvakioita */
int ia = 5 / 2; /* lopputulos eri kuin edellä, koska nyt käytössä kokonaisluvut */
char cb = 3 * (1 + 2);
long lc = cb * fa;

Yllä olevasta esimerkistä nähdään kuinka lausekkeet voivat muodostua useasta operaattorista, ja ne voivat sisältää sekä vakioita että muuttujia. Esimerkistä nähdään myös kuinka C-kääntäjä tekee tyyppimuunnojset perustietotyyppien välillä automaattisesti.

Yllä mainittujen operaattoreiden lisäksi C tarjoaa vaihtoehtoisen tavan yksinkertaisiin lisäys- ja vähennysoperaatioihin, jollaisia ohjelmissa ja esimerkiksi sen toisto-operaatioissa usein tarvitaan. Seuravaa esimerkki valaisee sitä kuinka näitä käytetään:

1
2
3
4
5
6
7
int a = 0;
b = a++;  // a:n arvo on nyt 1, b:n arvo 0
b = a--;  // a:n arvo on taas 0, b:n arvo 1
b = ++a;  // a:n arvo on 1, b:n arvo on myös 1
b = --a;  // a:n arvo on 0, niin myös b:n arvo
a += 5;   // a:n arvo on 5
a -= 3;   // a:n arvo on 2

++ ja -- lisäävät ja vähentävät niihin liitetyn muuttujan arvoa yhdellä. Tämä yksiosainen (unäärinen) operaattori voi olla joko muuttujan jälkeen, tai sitä ennen. Ero on toiminnassa osana lauseketta, kuten yllä sijoituksessa. Kun operaattori on muuttujan jälkeen, lauseke käyttää muuttujan arvoa ennen muutosta. Kun operaattori on ennen muuttujaa, sen arvo muutetaan ennenkuin muuttujaa käytetään osana lauseketta.

Riveillä 6 ja 7 on esimerkki toisesta unäärisestä operaattorivariaatiosta: a += 5 merkitys on sama kuin a = a + 5, mutta se on nopeampi kirjoittaa. Tätä variaatiota voi käyttää yhteen-, vähennys-, kerto- ja jakolaskun kanssa samaan tyyliin. Sen sijaan ++ ja -- ovat käytössä vain yhteen- ja vähennyslaskuissa. Operaattoreita ** ja // ei ole olemassa. Potenssilaskua varten C:ssä ei ole sisäänlaskettua operaattoria, vaan se täytyy tehdä C:n standardikirjaston tarjoamaa funktiota käyttäen (kerrotaan myöhemmin).

Alla vielä yksi esimerkki:

1
2
3
4
5
6
7
int main(void)
{
    int varA; /* Value is unspecified now */
    varA = 10; /* value is set to 10 */
    varA++; /* value is 11 */
    varA *= 2; /* value is 22 */
}

Tyyppimuunnoksista

C tekee implisiittisiä tyyppimuunnoksia perustietotyyppien välillä silloin kun se on mahdollista. Kun "suurempi" tietotyyppi sijoitetaan pienempään, tyyppimuunnoksen yhteydessä saattaa hukkua informaatiota, kuten aiemmassa esimerkissä nähtiin. Kun liukuluku sijoitetaan kokonaislukuun, mahdolliset kokonaisluvun murto-osat hukataan. C-kääntäjä varoittaa yleensä tällaisessa tilanteessa, mutta kääntää silti ohjelman.

Tyyppimuunnoksen voi pakottaa tekemällä eksplisiittisen tyyppimuunnoksen. Tämä ilmaistaan lausekkeessa antamalla haluttu tyyppi sulkeiden sisällä osana lauseketta. Seuraavassa esimerkissa valoitetaan tätä toimintaa esimerkiksi liukulukujen yhteydessä.

1
2
3
float f = 1.5;
int a = f + f;
int b = (int) f + (int) f;

Yllä oleva ohjelma antaa a:n arvoksi 3, mutta b:n arvoksi tulee 2. Laskutoimitus f + f tehdään liukuluvuilla, jolloin lopputulos on oikea. Laskutoimitus (int) f + (int) f tarkoittaa, että f:n arvo muutetaan kokonaisluvuksi ennen yhteenlaskua. Näin tuloskin on eri. Eksplisiittisiä tyyppimuunnoksia tulisi välttää aina kun mahdollista, mutta joskus niitä saattaa "joutua" käyttämään, ja joskus niitä näkee osana muuta koodia.

Task 02-intro-2: Tyypit kuntoon (1 pts)

Tavoite: Ensituntuma C:n perustietotyyppien oikeaan käyttöön.

Tehtäväpohja sisältää funktion fix_types, joka suorittaa kolme laskutoimitusta ja tulostaa niiden tulokset. Tarkoituksena olisi tulostaa ensimmäisen laskun tulos yhden desimaalin tarkkuudella, ja kahden jälkimmäisen laskutoimituksen tulos kokonaislukuna, mutta funktio palauttaa virheellisiä tuloksia.

Korjaa funktiota siten, että se palauttaa oikeat lopputulokset. Odotettu tuloste on:

5.3 8000000 66666

Älä koske printf - riviin, vaan korjaa muuttujille määritellyt tietotyypit.

Funktiot

C-ohjelmat muodostuvat funktioista. Funktio sisältää yksittäisen loogisen osan ohjelmasta, ja sitä voidaan kutsua ohjelman muista osista (eli toisista funktioista). Funktio voi kutsua myös itse itseään -- tätä kutsutaan rekursioksi. Hyvin suunnitellussa ohjelmassa sama logiikka ei toistu moneen kertaan, vaan se on eristetty omaksi funktiokseen jota kutsutaan aina tarvittaessa. Hyvällä funktiolla on myös selkeä ja hyvin määritelty toiminta.

Funktiolla on neljä pääosaa: nimi, paluuarvo, parametrilista ja funktion runko, jossa funktion toiminta määritellään käyttäen C-kielen lauseita ja kontrollirakenteita. Alla on esimerkki kahdesta lyhyestä funktiosta: square() kertoo saamansa parametrin base itsellään ja palauttaa paluuarvonaan tämän tuloksen. Funktio on määritelty riveillä 1-5.

Ohjelmassa on lisäksi main() - funktio, josta ohjelman suoritus alkaa. Kaikissa C-ohjelmissa täytyy olla yksi (ja vain yksi) main-funktio. Tässä tapauksessa main-funktio vai esittelee kolme muuttujaa, ja alustaa ne square - funktiokutsuista saaduilla paluuarvoilla. Funktiokutsu voi olla osa lauseketta, ja funktion parameterit voivat koostua lausekkeista, kuten alla nähdään. Myös funktiokutsu itsessään voi olla osa lauseketta jota käytetään (ulomman) funktiokutsun parametrinä.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int square(int base)
{
    int res = base * base;
    return res;
}

int main(void)
{
    int val = square(3);  // val becomes 9
    int val2 = square(val * 2);  // val2 becomes 18 * 18
    int val3 = square(square(val));  // val3 becomes (9*9) * (9*9)
    return 0;
}

Riviltä 1 käy ilmi kuinka funktiomäärittely rakentuu. Ensiksi kerrotaan funktion paluuarvon tietotyyppi. Kuten muuttujilla, se voi olla yksi C:n perustietotyypeistä, tai käyttäjän itse määrittelemä tyyppi (joka opitaan tekemään myöhemmin). On mahdollista että funktiolla ei ole lainkaan paluuarvoa. Tällöin tietotyypin paikalle kirjoitetaan void.

Tämän jälkeen annetaan funktion nimi. Funktion nimeä koskevat samat säännöt kuin muuttujienkin nimiä: kirjaimia ja numeroita voi antaa alaviivan lisäksi, mutta nimi ei saa alkaa numerolla. Isot ja pienet kirjaimet tulkitaan eri merkeiksi.

Nimen jälkeen seuraa sulkujen sisässä lista funktion parametreja. Parametrien avulla funktion kutsuja säätelee funktion sisäistä toimintaa. Kullekin parametrilla määritellään tietotyyppi ja parametrin nimi (jälleen samat nimeämissäännöt pätevät). Jos funktiolla on useita parametreja, ne erotetaan pilkulla. Tässä tapauksessa square - funktiolla on vain yksi parametri. On myös mahdollista että funktiolla ei ole parametreja lainkaan. Tällöin parametrilistan paikalle kirjoitetaan void. Näin tehdään yllä olevassa esimerkissä main-funktion määrittelyssä.

Funktion parametrit käyttäytyvät kuten muuttujat funktion määrittelyn sisällä. Niihin voi viitata lauseissa ja lausekkeita, ja niiden sisältöä voi muuttaa. Toisin kuin paikallisesti määritellyillä muuttujilla, funktioparametreilla on alkuarvo, joka määrittyy sen perusteella miten funktiota on kutsuttu.

Parametrit ja muut mahdollisesti määritellyt muuttujat ovat näkyvissä vain funktion ohjelmalohkon sisällä, joka erotellaan aaltosuluin. Jos näihin yritetään viitata aaltosulkujen ulkopuolelta, kääntäjä antaa virheen, eikä tuota ajettavaa ohjelmaa.

Funktion suoritus loppuu joko return - lauseeseen tai kun ohjelman suoritus tulee funktion loppuun. return - lauseen jälkeen annetaan lauseke (tai esimerkiksi vakio tai muuttuja), joka funktio palauttaa paluuarvonaan. Funktiossa voi olla useita return - lauseita, esimerkiksi ohjelman eri ehtohaaroissa. Mikäli funktiolla on määritelty paluuarvo, mutta suoritus loppuu funktion lopussa törmäämättä return - lauseeseen, funktion paluuarvo on määrittelemätön. Kääntäjä varoittaa tästä, mutta yrittää silti kääntää ohjelman. Tällainen tilanne tulisi tietysti korjata, ja funktion tulisi tällöin aina loppua return - lauseeseen.

Task 04_func: Vektorifunktio (1 pts)

Tavoite: perehtyä funktion kirjoittamiseen ja toisten kirjastofunktioiden kutsumiseen.

Toteuta funktio nimeltään vectorlength joka laskee annetun kolmiulotteisen vektorin pituuden. Funktio saa kolme parametria, jotka esittävät vektorin alkioita eri ulottuvuuksissa. Funktio palauttaa vektorin pituuden. Kaikki luvut tulee käsitellä double-tyyppisinä liukulukuesityksinä.

Mikäli vektorilaskut ovat ehtineet unohtua, esimerkiksi Wikipediassa kerrotaan laskukaava. Tarvitset siis esimerkiksi neliöjuurilaskua, joka ei sisälly C-kielen perusoperaattoreihin, vaan sitä varten on sqrt - funktio matematiikkakirjastossa, jota sinun tulee kutsua. pow - funktiolla voi laskea potenssilaskuja. Katso tarkemmat tiedot funktioista linkitetyiltä manuaalisivuilta.

Toteuta funktio source.c - tiedostoon. Tiedostossa on annettu valmiiksi viittaus math.h - otsaketiedostoon jossa matematiikkafunktiot on määritelty, mutta tässä tehtävässä joudut kirjoittelemaan kaiken muun itse. Ohjelma ei aluksi edes käänny, ennenkuin olet toteuttanut vähintäänkin funktion rungon.

Muotoiltu syöte ja tuloste

Muotoiltu tuloste

C-ohjelma voi tulostaa tekstiä käyttäjän ruudulle kutsumalla järjestelmän mukana tulevassa C-standardikirjastossa määriteltyä printf - funktiota. printf - funktiossa on vähintään yksi parametri, lainausmerkeillä rajattu merkkijono, joka kertoo mitä tulostetaan. Lisäksi voi olla vaihteleva määrä muita parametreja, joita halutaan sisällyttävän tulosteeseen.

printf - funktiossa annettu merkkijono voi sisältää muotoilumääreitä, jotka korvataan parametrilistassa annetun lausekkeen arvolla. Muotoilumääreet alkavat aina % - merkillä. Alla yksinkertainen esimerkki.

1
2
3
4
5
6
7
#include <stdio.h>

int main(void)
{
    int number = 50;
    printf("The number is %d\n", number);
}

Huomaamme, että ohjelmassa määritellään kokonaislukumuuttuja number, jonka arvoksi asetetaan 50. Sitten kutsutaan printf - funktiota, jolle annetaan kaksi parametriä: merkkijono, sekä lauseke, joka sisältää pelkästään muuttujan number. Jälkimmäinen parametri voisi olla mikä tahansa lauseke (esimerkiksi funktiokutsu), joka palauttaa kokonaisluvun. Ensimmäinen parametri on aina merkkijono. Tässä tapauksessa %d:n paikalle korvautuu muuttujan number sen hetkinen arvo.

Ohjelman ensimmäisellä rivillä sisällytetään "stdio.h" - otsaketiedostossa annetut määritelmät osaksi ohjelmaa. stdio.h sisältää C-standardikirjaston syöte- ja tulostusvirtaa koskevan määritelmät ja funktiorajapinnat. Myös printf - funktio on määritelty siellä.

Ohjelma tulostaa ruudulle:

The number is 50

ja siirtää kursorin seuravalle riville. Uusi rivi aloitetaan vain silloin kun tulosteessa esiintyvät merkit \n. Mikäli tätä merkkiä ei esiinny, uutta riviä ei aloiteta, vaikka erillisiä printf - kutsuja olisi useita.

Muotoilumääreitä tulee olla yhtä monta, kuin muotoilumerkkijonoa seuraavia parametreja printf-kutsussa. Muotoilumääreitä on erilaisia, riippuen tulostettavan arvon tietotyypistä ja halutusta esitysmuodosta. Muotoilumääreen tyypin tulee vastata parametrin tuottamaa tietotyyppiä. Esimerkiksi seuraavia on käytössä:

  • %d: (int) -- etumerkillinen kokonaisluku desimaaliesityksenä
  • %u: (unsigned int) -- etumerkitön kokonaisluku desimaaliesityksenä
  • %o: (unsigned int) -- kokonaisluku oktaaliesityksenä
  • %x, %X: (unsigned int) -- kokonaisluku heksadesimaaliesityksenä, kirjaimet A-F joko pienillä (ensimmäinen muoto) tai isoilla (jälkimmäinen) esitettynä.
  • %c: (int) -- yksittäinen merkki perustuen ASCII-koodaukseen (kts. edellä).
  • %s: (char*) -- merkkijono. Näistä enemmän asiaa seuraavassa modulissa.
  • %f: (double) -- liukuluku muodossa "n.nnnnnn". Oletusarvoisesti tulostetaan kuusi desimaalia.
  • %e, %E: (double) -- liukuluku eksponenttimuodossa ("n.nnnnnnE+-xx)
  • %g, %G: (double) -- joko %f tai %e - muoto, riippuen eksponentin suuruudesta.

Mikäli tulostettavan merkkijonon halutaan sisältävän %-merkin, pitää sijoittaa %% merkkijonoon, jotta voidaan erottaa se muista muotoilumääreistä.

Muotoilumääreeseen voidaan lisäksi sisällyttää lisämääreitä, joilla voidaan säädellä tulosteen ulkoasua. Esimerkiksi seuraavia voidaan käyttää:

  • numero (esim: %4d): kertoo että korvatun tulosteen tulisi aina käyttää vähintään annettu määrä merkkejä. Tätä voidaan käyttää tulosteen parempaan muotoiluun, esimerkiksi taulukoksi, jossa sarakkeet osuvat nätisti samalle kohdalle.
  • miinusmerkki numeron edellä (esim: %-4d): kun tuloste vaatii vähemmän merkkejä kuin annettu, tulostaa sen annetun kentän vasempaan reunaan. Oletusarvoisesti tuloste tasataan oikeaan reunaan.
  • plussamerkki numeron edellä (%+4d): numeerisissä tulosteissa sisällyttää aina etumerkin ennen numeroarvoa, myös positiivisten lukujen tapauksessa.
  • nollamerkki numeron edellä (%04d): numeerisissä tulosteissa täyttää käyttämättömät merkit nollilla numeron edessä, kun tuloste muuten käyttäisi vähemmän tilaa kuin annettu määrä merkkejä.
  • piste, jonka perässä numero (%4.1f): määrittelee kuinka monta desimaalia näytetään liukuluvuissa. Pisteen edellä oleva numero kertoo edelleen kuinka monta merkkiä tulosteen tulee kokonaisuudessaan (vähintään) käyttää, ja sitä ei ole pakko antaa.
  • h tai l merkit (esim: %ld): Oletusarvoisesti kokonaisluvun tyypiksi oletetaan int ja liukuluvun double. h ilmaisee, että tulostus odottaa lausekkeen tyypiksi lyhyen tietotyypin (short kokonaisluvulle tai float liukuluvule). l ilmaisee vastaavasti että pitkä tietotyyppi on käytössä (long tai long double).

Lisää yksityiskohtia löytyy esimerkiksi K&R - kirjasta.

Alla joitain esimerkkejä muotoillusta tulosteesta. Hakasuluilla ei tulosteessa ole mitään erityismerkitystä, mutta niitä käytetään korostamaan sitä, kuinka monta merkkiä tuloste käyttää.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>

int main(void)
{
    int numA = 10;
    float numB = 2.54;
    float numC = 0.000001;
    printf("At least five characters long: [%5d]\n", numA);
    printf("Length is six, one decimal shown: [%6.1f]\n", numB);
    printf("Float number, aligned left: [%-10.2e]\n", numC);
    printf("Number with leading zeros: [%05d]\n", ++numA);
}

Tämä tulostaa:

1
2
3
4
The following field is at least five characters long: [   10]
The length is six, but just one decimal shown: [   2.5]
Another floating point number, aligned left: [1.00e-06  ]
Number with leading zeros: [00011]

Esimerkissä ei liene mitään kovin yllättävää, mutta rivillä 11 esitellään vielä kerran kuinka unäärinen "lisää yksi" - operaattori toimii, kun se on annettu ennen muuttujaa. Lisäksi voi huomioida \n käytön jokaisen rivin lopussa. Mikäli sitä ei otettaisi mukaan, kaikki tulosteet ilmestyisivät samalle riville. \n vittaa erityiseen rivinvaihtomerkkiin (ASCII-koodi 10), joka aiheuttaa tulosteen siirtymisen seuraavalle riville. Ilman tällaista erikoiskoodia rivinvaihtomerkkiä olisi vaikea sisällyttää ohjelmakoodiin, mutta muitakin keinoja siihen löytyy (esim: printf("%c", 10);)

Vastaavia erikoismerkkejä on muitakin:

  • '\t': sarkain -- siirtää tulostetta yhden sarkainvälin eteenpäin
  • '\': tulostaa yhden kenoviivan
  • '\"': tulostaa hipsut (")
  • '\'': tulostaa yksinkertaisen lainausmerkin

Lisää erikoismerkkejä löytyy jälleen kirjallisuudesta tai verkosta.

Muotoiltu syöte

Käyttäjältä voi lukea syötettä käyttäen scanf - funktiota. Sekin on määritelty standardikirjastossa, ja vaatii toimiakseen "stdio.h" - otsaketiedoston sisällyttämisen ohjelmaan. Käyttäjän syöte parsitaan käyttäen samanlaisia muotoilumääreitä kuin printf - funktion yhteydessä, riippuen siitä odotetaanko numeroarvoja jossain tietyssä lukujärjestelmässä, merkkejä, vai jotain muuta.

Alla esimerkki scanf - funktion käytöstä:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <stdio.h>

int main(void)
{
    int a;
    float b, c;
    int ret_a, ret_b;

    ret_a = scanf("%d", &a);
    ret_b = scanf("%f,%f", &b, &c);
}

scanf - funktio lukee käyttäjältä merkkejä, ja parsii ne annettuihin muuttujiin. Nämä muuttujat pitää ensin esitellä, kuten riveillä 5-7 tehdään. Muuttujilla ei ole määriteltyä alkuarvoa, mutta tässä tapauksessa se ei haittaa.

Rivillä 9 luetaan käyttäjältä etumerkillinen kokonaisluku muuttujaan a. Jälleen muotoilumääreen ja annetun muuttujan tyypin tulee vastata toisiaan. scanf-funktion paluuarvo sijoitetaan muuttujaan ret_a. Paluuarvo on tärkeä, koska se kertoo, kuinka monta muotoilumäärettä onnistuttiin onnistuneesti parsimaan. Jos tässä tapauksessa funktio palauttaa 0, parsiminen ei onnistunut, esimerkiksi koska käyttäjä ei ole syöttänyt numeerisiä merkkejä. Mikäli käyttäjän syöte ei vastaa odotettua muotoilumäärettä, scanf-funktio palaa välittömästi ja lopettaa parsimisen siihen paikkaan.

Rivi 10 odottaa käyttäjältä kahta liukulukua, joiden välillä on pilkku. Funktio palauttaa 0, mikäli ensimmäistä liukulukua ei onnistuttu lukemaan, 1 mikäli ensimmäisen liukuluvun lukeminen onnistui, mutta toisen ei, tai 2, mikäli koko syöte onnistuttiin lukemaan.

Koska scanf-funktio keskeyttää suorituksen heti kun syötteen lukeminen ei onnistu, on mahdollista että annettuja muuttujia ei asetetakaan ja esimerkiksi edellisessä esimerkissä niiden arvo jää tuntemattomaksi. Lisäksi on hyvä huomioida, että seuraava scanf-kutsu jatkaa lukemista siitä mihin edellinen jäi. Tämä saattaa joskus yllättää, jos edellisen kutsun parsiminen ei onnistunutkaan, ja parsiminen siksi keskeytetään.

scanf-funktio ohittaa tyhjät merkit, esimerkiksi välilyönnin, tabulaattorimerkin (\t) tai rivinvaihdon (\n). Poikkeuksena silloin, kun luetaan yksittäistä merkkiä (%c - muotoilumääre), jolloin näidenkin merkkien parsinta onnistuu.

Tässä vaiheessa & - merkin käyttö muuttujien edellä kummastuttaa. Tämä liittyy osoittimien toimintaan, joista lisää seuraavassa modulissa. Toistaiseksi voit vain olettaa, että tuo merkki tulee aina lisätä muuttujan nimen eteen, kun scanf-funktiota käytetään.

Task 05-calc-1: Summalasku (1 pts)

Tavoite: harjoittele muuttujien tulostamista ja syöttämistä.

Toteuta funktio simple_sum, joka kysyy käyttäjältä kaksi kokonaislukua ja laskee niiden summan. Lopuksi funktio tulostaa summan seuraavassa muodossa:

1 + 2 = 3

Tulosteen lopussa tulee olla rivinlopetus ('\n'). Seuravassa esimerkki käyttäjän syötteestä (punaisella) ja sitä seuraavasta ohjelman tulosteesta (mustalla):

4 5
4 + 5 = 9

Vinkki: Koska scanf ohittaa kaikki tyhjät merkit muotoiluohjeiden välissä, ohjelma saa hyväksyä syötteet, joissa kaksi kokonaislukua ovat eri riveillä, tai ne on erotettu useilla välilyönneillä.

Ehtorakenteet

Lauseet ja ohjelmalohkot

Funktion runko koostuu lauseista, jotka loppuvat aina puolipisteeseen. Koostettu lause kokoaa yhteen ryhmän lauseita omaksi lohkokseen. Lohkon alku ja loppu merkitään aaltosuluilla { ja }. Tällainen lohko voidaan ajatella itsessään omaksi lauseekseen ohjelman parsimisen näkökulmasta. Määrittelyt (esimerkiksi muuttujat), jotka tehdään lohkon sisällä näkyvät vain lohkon loppuun asti. Sisäkkäisiä lohkoja voi olla ohjelmassa useita. Yleensä lohkoja käytetään yhdessä muiden ohjelman kontrollirakenteiden kanssa, kuten ehtolauseissa tai toistosilmukoissa, mutta niitä voi määritellä muutenkin keskellä funktion toteutusta.

Alla oleva esimerkki esittelee lohkon toimintaa, ja sitä kuinka paikalliset muuttujat ovat käytettävissä:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int main(void)
{
    int a = 1;
    a = a + 1;
    {
        int b = 6;
        b = b + 1;
    }
    a = a + b; /* Virhe! b:hen ei voi tässä kohtaa enää viitata */
}

Yllä olevassa esimerkissä muuttuja a on näkyvillä koko funktiossa, mutta koska b määritellään vasta sisemmässä ohjelmalohkossa, se ei näy rivillä 9, koska siinä kohtaa sisempi lohko on jo suljettu. Siksi kääntäjä tekee käännösvirheen, eikä tuota ajettavaa ohjelmaa.

Vertailuoperaattorit ja loogiset operaattorit

Loogiset operaattorit, jollaisia vertailutkin ovat, tuottavat arvokseen aina 1 tai 0. Nämä ovat normaalia kokonaislukutietotyyppiä (int). Toisin kuin monissa muissa ohjelmointikielissä, C-kielessä ei ole totuusarvotyppiä, vaan totuusarvoja esitetään kokonaislukujen avulla: "epätosi" ilmaistaan arvolla 0, "tosi" millä tahansa muulla kokonaisluvulla.

Vertailuoperaattorit ovat (ei niin yllättäen) seuraavat:

  • < -- pienempi kuin
  • <= -- pienempi tai yhtäsuuri kuin
  • > -- suurempi kuin
  • >= -- suurempi tai yhtäsuuri kuin
  • == -- yhtäsuuri (Tärkeää: Huomaa ero sijoitusoperaattoriin '=')
  • != -- erisuuri kuin

Seuraava esimerkki näyttää kuinka sijoitusoperaattorit toimivat:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int main(void)
{
    int a;
    int ret = scanf("%d", &a);
    if (ret > 0) {
        int a_res = a < 5;
        printf("a less than 5: %d\n", a_res);
        printf("a equal to 5: %d\n", a == 5);
    }
}

Mikäli käyttäjä syöttää kokonaisluvun, se luetaan muuttujaan "a". Muuttuja "a_res" kertoo mikäli syötetty luku oli pienempi kuin 5: tässä tapauksessa muuttujan arvoksi sijoitetaan 1, muutoin 0. Vertailuoperaattoria voi käyttää normaalisti osana mitä tahansa muuta lauseketta, kuten on tehty rivillä 8 osana printf-funktiokutsua. Mikäli käyttäjä ei syöttänyt kokonaislukua, muuttujan "ret" arvo on 0, eikä edellämainittuja vertailuja tai tulosteita tehdä laisinkaan.

Käyttäjän syöte (punaisella) ja ohjelman tuloste (mustalla) voisi näyttää esimerkiksi seuraavalta:

5
a less than 5: 0
a equal to 5: 1

Lisäksi voidaan käyttää loogisia operaattoreita JA, TAI ja EI:

  • JA-operaattori on &&: esimerkiksi lauseke (a < 5 && b > 6) on tosi (1) jos a on pienempi kuin 5 ja b on suurempi kuin 6.
  • TAI-operaattori on ||: esimerkiksi lauseke (a < 5 || b > 6) on tosi jos joko a on pienempi kuin 5 tai b on suurempi kuin 6.
  • EI operaattori viittaa vain yhteen lausekkeeseen. ! lausekkeen edessä kääntää sen totuusarvon toiseksi. Esimerkiksi !(a < 5) kertoo etti "a ei ole pienempi kuin 5", eli se on suurempi tai yhtäsuuri kuin 5.

Näitä operaattoreita ja vertailuoperaattoreita voi tietysti yhdistellä mielin määrin. Sulkeiden kanssa kannattaa tällöin olla tarkkana, jotta loogiset ehdot testaavat varmasti oikeaa asiaa.

Yleinen aloittelijan virhe C-kielessä on sekoittaa loogiset operaattorit && ja || bittioperaattoreihin & ja |. Kääntäjä ei huomauta asiasta mitään, koska lausekkeessa käytettynä molemmat tapaukset voidaan laskea -- ne vain tuottavat täysin eri lopputuloksen. Samanlainen virheen mahdollisuus on yhtäsuuruusvertailun (==) ja sijoituksen (=) kanssa. Tarkkana!

Ehtorakenteet

Ehtorakenteet ovat merkittävä osa mitä tahansa tietokoneohjelmaa. C:ssä ehtolauseet noudattavat rakennetta:

if (lauseke)
  lause-1
else
  lause-2

Jos "lauseke" on tosi (eli lausekkeen tuottaman kokonaisluvun arvo on jotain muuta kuin 0), "lause-1" suoritetaan. Jos "lauseke" ei ole tosi (eli kokonaisluku on 0), "lause-2" suoritetaan.

"lauseke" voi olla mikä tahansa C-kielinen lauseke. Se voi esimerkiksi sisältää funktiokutsun, jolloin funktion paluuarvo määrittelee ehtolauseen lopputuloksen. Myös sijoitusta voidaan käyttää if-lausekkeessa, koska lauseke käyttää sijoituksen lopputulosta osana laskentaa. Myös yksittäinen numerovakio on hyväksyttävä lauseke: if (1) suorittaisi aina "lause-1":n. Usein käytetty muoto on myös if (a), joka testaa onko muuttujan a arvo jotain muuta kuin 0.

lause-1 ja lause-2 voivat olla ohjelmalohkoja, eli koostua tällöin useista lauseista, tai ne voivat sisältää yksittäisen lauseen, jolloin aaltosulkuja ei esiinny. Jälkimmäisessä tapauksessa täytyy muistaa päättää lause puolipisteeseen.

Tässä jälleen esimerkki, joka valottaa edellä kerrottua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int days, years; // useita muuttujia voidaan esitellä pilkulla erotettuna
int ret;
ret = scanf("%d %d", &days, &years);
if (ret >= 2) {
    if (days > 365) {
        years++;  // tai: years = years + 1;
        days -= 365;  // tai: days = days - 365;
    }
    else
        printf("%d days remaining until the next year\n", 365 - days);

    /* Ei aaltosulkuja else-haarassa --
       seuraava rivi suoritetaan kummassakin tapauksessa */
    printf("days: %d  years: %d\n", days, years);
} else {
    printf("Invalid input!\n");
}

Käyttäjän syöte (punainen) ja ohjelman tuloste voisivat esimerkiksi olla:

400 2
days: 35  years: 3

Yllä oleva esimerkki käyttää lohkoa if ehtojen jälkeen, mutta pelkkää yksittäistä lausetta sisemmässä else-haarassa (rivi 10). Tämä on teknisesti ottaen hyväksyttävää C:tä, mutta ei välttämättä hyvää ja johdonmukaista ohjelmointityyliä. Koodin lukija ei voi aina tietää onko aaltosulut jätetty pois tarkoituksella, vai kenties unohtuneet vahingossa. Siksi suositeltavaa on käyttää aaltosulkeita aina, mikä parantaa ohjelman luettavuutta. On ok, mikäli ohjelmalohko koostuu vain yhdestä lauseesta, kuten onkin tehty ulommassa else-haarassa rivillä 16.

Edellä olevasta esimerkistä nähdään myös, kuinka tärkeää on sisentää koodi johdonmukaisesti ohjelmalohkojen mukaisesti. C-kääntäjä hyväksyisi ohjelman, jossa kaikki rivit alkaisivat vasemmasta reunasta, tai vaikka ohjelman joka olisi sullottu kokonaan yhdelle riville, mutta sellaisen lukeminen olisi hyvin työlästä.

else-haaran voi jättää kokonaan pois, mikäli sille ei ole käyttöä.

Ehtorakenteessa voi olla useampia osia kuin kaksi. Tällöin käytetään "else if" muotoa osana ehtorakennetta, tähän tyyliin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int a = 0;
scanf("%d", &a);  // jos ei numero, a:n arvoksi jää 0

if (a == 1)
    printf("one\n");
else if (a == 2)
    printf("two\n");
else if (a == 3)
    printf("three\n");
else
    printf("some other number\n");

Switch

switch - lause on toinen tapa tehdä moniosaisia ehtorakenteita, silloin kun vaihtoehdot ovat kokonaislukuvakioita. swich-lause evaluoi saamansa lausekkeen, ja vertailee sitä annettuihin vakioihin, jotka listataan case - sanalla seuraavaan tyyliin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
char a = 0;
scanf("%c", &a);  // lue yksi merkki käyttäjältä

switch(a) {
case '1':  // ASCII '1' on sama kuin kokonaisluku 49
    printf("user typed one\n");
    break;

case '2':
    printf("user typed two\n");
    break;

case 'a':
case 'b':
case 'c':
    printf("user typed a, b or c\n");
    break;

default:
   printf("user typed something else\n");
   break;
}

Ohjelma siis lukee käyttäjältä merkin rivillä 2. Muuttuja 'a' on 8-bittinen kokonaisluku (char), johon tallennetaan käyttäjän kirjoittamaa merkkiä vastaava ASCII-koodi (Huomaa %c scanf:ssä, eikä esim. %d). Ohjelman tulkitsemisen kannalta voimme kuitenkin vain ajatella, että muuttujassa on tallessa jokin näppäimistöltä syötetty merkki.

switch-lausekkeessa rivillä 4 arvioidaan pelkästään muuttujan a sisältöä. Myös switch:iä tulee seurata ohjelmalohko, jossa on listattu joukko vaihtoehtoja (useat case rivit, jotka päättyvät kaksoispisteeseen). Ohjelman suorituksen voidaan ajatella "hyppäävän" sopivaa vaihtoehtoa esittävän case-lauseen kohdalle, ja siten kunkin case:n perässä voi seurata useita lauseita ilman että niitä on erotettu ohjelmalohkokseen. Aaltosulkujen käyttö on kuitenkin sallittua myös tässä tilanteessa. break - komento keskeyttää switch-lohkon suorittamisen, ja hyppää suoraan lohkon loppuun. Mikäli break unohtuu, suoritus jatkuu suoraan eteenpäin, vaikka välissä olisi joku toinen case-haara. Joskus tämä voidaan tehdä tarkoituksella, mutta usein myös break unohtuu, ja ohjelma toimii väärin, vaikka kääntäjä on siihen tyytyväinen.

Edellä olevassa esimerkissä on tärkeää huomioida merkkivakioiden käyttäminen case-haaroissa, eli yksinkertaisten lainausmerkkien käyttö. Koska käyttäjältä luettiin merkki (%c), eikä kokonaislukua (%d), tämä on oleellinen ero.

switch-rakenteen sijaan voitaisiin myös käyttää pitkää if..else if.. - rakennetta.

Task 05-calc-2: Laskin (1 pts)

Tavoite: Harjoittele ehtorakenteita yhdessä syötteen ja tulosteen kanssa.

Toteuta funktio simple_math, joka kysyy kolmiosaisen syötteen käyttäjältä: numeron, operaattorin, ja numeron. Operaattorin tulee olla yksi seuraavista merkeistä: '+', '-', '*' tai '/'. Jos jotain muuta merkkiä yritetään käyttää operaattorina, funktion tulee tulostaa merkkijono ERR. Numerot ovat liukulukuja.

Mikäli käyttäjän syöte ei noudata oikeanlaista numero-operaattori-numero - rakennetta, funktion tulee niinikään tulostaa ERR. Kun hyväksytty syöte on annettu, funktion tulee tulostaa annetun laskutoimituksen tulos yhden desimaalin tarkkuudella.

Seuraavassa esimerkki ohjelman mahdollisesta syötteestä ja tulosteesta:

8 - 2
6.0

8.3 / 5.1
1.6

-3.456 - 2.31
-5.8

Vinkki: Kiinnitä huomiota merkkivakioiden käyttöön, ja siihen kuinka yksittäisiä merkkejä käytetään esimerkiksi scanf - funktion yhteydessä.

Toistorakenteet

While ja do-while

while lause toistaa sitä seuraavaa lausetta tai ohjelmalohkoa niin kauan kuin while-lauseessa annettu ehto on tosi (eli lausekkeen arvo on erisuuri kuin 0). Esimerkiksi seuraava ohjelmanpätkä toistaa yksinkertaista silmukkaa kunnes muuttujan a arvo on 10 tai suurempi.

1
2
3
int a = 0;  // muuttujan alustaminen tärkeää erityisesti tässä esimerkissä
while (a < 10)
    a++;

while - lauseen yhteydessä annettu lopetusehto testataan ennenkuin seuraava lause suoritetaan. Mikäli while:n lopetusehto on epätosi heti alussa, while:n "sisällä" olevaa lausetta tai ohjelmalohkoa ei suoriteta lainkaan.

Tässä vielä esimerkki ohjelmalohkon käytöstä while:n yhteydessä.

1
2
3
4
5
6
int a = 0;
while (a < 10)
{
    printf("value of a is now %d\n", a);
    a++;
}

Mikäli halutaan että lopetusehto testataan vasta lauseen tai ohjelmalohkon päätteeksi, voidaan käyttää do-while - rakennetta seuraavaan tapaan:

1
2
3
4
5
int a = 20;
do {
    printf("value of a is now %d\n", a);
    a++;
} while (a < 10);

Edellisessä esimerkissä muuttujan a arvo alustetaan 20:ksi. Siksi silmukasta hypätään ulos heti kun lopetusehto tarkistetaan. Koska do..while - rakenteessa tämä tapahtuu vasta lohkon jälkeen, ohjelma ehtii tulostaa yhden rivin ennen silmukasta poistumista.

For

Toinen tapa rakentaa toistoja ja ohjelmasilmukoita on käyttää for - rakennetta. Se on ilmaisuvoimaltaan samanlainen, kuin while, mutta joissain tilanteissa kenties kätevämpi. for-rakenne on seuraavanlainen:

for (lauseke_1; lauseke_2; lauseke_3)
      lause

Yllä olevan for-rakenteen voi toteuttaa while:ä käyttämällä seuraavasti:

lauseke_1;
while (lauseke_2) {
     lause
     lauseke_3;
}
  • lauseke_1 tekee tarvittavat alustukset ennenkuin silmukan suoritus aloitetaan.

  • lauseke_2 sisältää toistoehdon, joka testataan ennen kunkin kierroksen alkua, myös heti alustusten jälkeen. Jos toistoehto on epätosi, silmukasta poistutaan.

  • lauseke_3 sisältää toimenpiteet jotka kunkin iteraation päätteeksi tehdään. Tyypillisesti siinä esimerkiksi muutetaan toistoehdossa käytetyn muuttujan arvoa.

C:lle tyypilliseen tapaan mitä tahansa lausekkeita voidaan käyttää yllä olevissa kohdissa. On siis mahdollista, että lauseke_2 päivittää jotain muuttujaa samalla kun sitä käytetään toistoehdon tarkistukseen.

Esimerkiksi äskeinen while - esimerkki voitaisiin kirjoittaa for:ia käyttäen seuraavaan tapaan.

1
2
3
4
int a;
for (a = 0; a < 10; a++) {
    printf("value of a is now %d\n", a);
}

Mitkä tahansa kolmesta lausekkeesta voivat olla tyhjiä. Esimerkiksi seuraavakin ohjelma vastaisi täysin yllä olevaa.

1
2
3
4
5
int a = 0;
for ( ; a < 10; ) {
    printf("value of a is now %d\n", a);
    a++;
}

Mikäli lopetusehto (eli lauseke_2) jätetään tyhjäksi, sen oletetaan olevan aina tosi. Tällä tavoin voidaan tehdä päättymättömiä silmukoita. Tällaisenkin silmukan voi keskeyttää käyttämällä esimerkiksi return - lausetta, joka poistuu funktiosta, tai break - lauseella.

Useita silmukoita voi luonnollisesti suorittaa sisäkkäin.

C99-standardista alkaen muuttujia voi esitellä osana for-lausetta, lauseke_1:ssä, esimerkiksi näin:

1
2
3
for (int a = 0; a < 10; a++) {
    printf("value of a is now %d\n", a);
}

Tällainen muuttuja on käytettävissä vain for-silmukan sisällä.

break ja continue

break - lauseella voidaan keskeytää toistorakenne vaikka annettu toistoehto olisi vielä tosi. Esimerkiksi seuraava esimerkki ei koskaan pääse 10:een asti, vaan keskeyttää 5:n kohdalla.

1
2
3
4
5
6
int a;
for (a = 0; a < 10; a++) {
    printf("value of a is now %d\n", a);
    if (a == 5)
        break;
}

continue - lause keskeyttää nykyisen iteraation suorituksen ja palaa silmukan alkuun saman tien. for - rakenteessa suoritetaan kuitenkin lauseke_3 myös continue:n yhteydessä, ennen kuin siirrytään uudestaan alkuun. Seuraava esimerkki tulostaa vain parilliset numerot (a % 2 laskee 2:n jakojäännöksen muuttujasta a):

1
2
3
4
5
6
int a;
for (a = 0; a < 10; a++) {
    if (a % 2 == 1)
        continue;
    printf("value of a is now %d\n", a);
}

Task 07-geometry-1: Kertotaulu (1 pts)

Tavoite: Totuttele sisäisiin silmukoihin ja tulostuksen muotoiluun

Toteuta funktio multi_table joka tulostaa määrätyn kokoisen kertotaulun taulukkomuodossa. Kertotaulun vaakakoko annetaan parametrissa xsize ja pystykoko parametrissa ysize. Kertotaulun vasen yläkulma alkaa luvusta 1. Kunkin luvun tulee käyttää neljän merkin verran tilaa ruudulta, ja numerot tulee tasata oikealle. Jokaisen rivin (mukaanlukien viimeinen rivi) tulee päättyä rivinvaihtoon ('\n'). Esimerkiksi funktiokutsun multi_table(4,5) tulisi saada aikaan seuraavanlainen tuloste:

   1   2   3   4
   2   4   6   8
   3   6   9  12
   4   8  12  16
   5  10  15  20

Task 07-geometry-2: Kolmio (1 pts)

Tavoite: Lisää harjoittelua sisäkkäisillä silmukoilla, sekä tutustumista ehtolausekkeisiin.

Toteuta funktio draw_triangle joka piirtää neliömäisen ASCII-laatikon, jonka sisällä on kolmio.

Laatikon tulee olla size merkkiä levä ja korkea, ja se tulee jakaa vasemman alakulman ja oikean yläkulman välillä siten, että vasemman yläkulman puoli täytetään merkeillä '.' (piste) ja oikean alakulman puoli merkeillä '#' (risuaita). Ensimmäisellä rivillä tulee olla yksi risuaita oikeassa yläkulmassa ja viimeisen rivin tulee täyttyä risuaidoista. Kaikkien rivien (mukaanlukien viimeinen rivi) tulee päättyä rivinvaihtomerkkiin ('\n').

Kun kutsutaan draw_triangle(5), tulisi tulla tällainen tuloste:

....#
...##
..###
.####
#####

Task 07-geometry-3: Pallo (1 pts)

Tavoite: Jatketaan edelleen samalla teemalla, mutta tällä kertaa tulosteen määrittelyyn tarvitaan erillistä funktiota.

Toteuta funktio void draw_ball(unsigned int radius) joka tulostaa ASCII-neliön, jonka sisällä tähtimerkeistä ('*') muodostettu täytetty ympyrä.

Laatikon pituus ja leveus on (2 * radius + 1), missä radius on funktion saama parametri. Toisinsanoen laatikko on juuri riittävän suuri sisältääkseen ympyrän, jonka säde on radius - parametrissa annettu arvo.

Tehtäväpohjassa on apufunktio distance jota voit käytää hyväksi ympyrän piirtämiseen. Funktio palauttaa koordinaattien (x,y) etäisyyden origosta, eli kun distance(x,y) <= radius, koordinaatti (x,y) on ympyrän sisällä, kun ympyrän keskipiste on (0,0).

Mikäli ruutu on ympyrän sisällä, tulosta merkki '*'. Mikäli ruutu on ympyrän ulkopuolella, tulosta merkki '.'.

Esimerkiksi kun kutsutaan draw_ball(3), ruudulle pitäisi tulla:

...*...
.*****.
.*****.
*******
.*****.
.*****.
...*...

Vinkki: for-silmukan ei aina tarvitse alkaa nollasta, vaan iteroitava muuttuja voi sisältää myös negatiivisia arvoja (mikäli tietotyyppi sallii sen)

Task 08-characters-1: ASCII-taulu (1 pts)

Tavoite: Perehdytään ASCII-taulukkoon printf-tulosteiden eri muotoilumääreiden avulla.

Toteuta funktio void ascii_chart(char min, char max) joka tulostaa annetun osan ASCII-taulukosta. Funktion tulee käydä läpi luvut min:stä max:iin, sekä jokaiselle lukuarvolle tulostaa seuraavasti:

  • kolmen merkkiä leveä kenttä, joka tulostaa kyseisen luvun desimaalimuodossa. Jos numeron on alle 100, se tulee tasata oikealle.

  • yksi välimerkki, jonka jälkeen neljä merkkiä leveä kenttä johon tulostetaan sama lukuarvo heksadesimaalimuodossa. Kukin heksaluku vie kaksi merkkiä, ja sen eteen tulee tulostaa merkit '0x'. Mikäli heksaluku vie vain yhden merkin, eteen tulee sijoittaa '0' siten jokaisessa luvussa on aina kaksi merkkiä.

  • yksi välimerkki, jonka perään kyseistä lukua vastaava ASCII-merkki. Tämä vie aina vain yhden merkin verran tilaa. Jotkut merkkiarvot eivät ole tulostettavissa, eli niille ei ole määritelty näkyvää tulostetta. Tällaisten merkkien tilalle tulee tulostaa kysymysmerkki ('?'). Voi käyttää kirjastofunktiota int isprint(int c) (man-sivu) selvittääksesi onko merkki c tulostettavissa vai ei. Jos funktio palauttaa 0, merkki ei ole tulostettavissa.

  • Lopuksi tulosta yksi tabulaattorimerkki ('\t') ennen seuraavaa merkkiarvoa, paitsi jos rivillä on jo neljän luvun tiedot. Neljännelle luvulle sinun tulee vaihtaa riviä, eli tulostaa tabulaattorimerkin sijaan rivinvaihto ('\n')

Sinun tulee siis käydä läpi edellä mainitulla tavalla kaikki lukuarvot annetulla numerovälillä (sisältäen myös arvon max). Esimerkiksi funktiokutsun ascii_chart(28,38) tulisi aiheuttaa seuraavanlainen tuloste:

 28 0x1c ?   29 0x1d ?   30 0x1e ?   31 0x1f ?
 32 0x20     33 0x21 !   34 0x22 "   35 0x23 #
 36 0x24 $   37 0x25 %   38 0x26 &

Task 08-characters-2: Salaviesti (1 pts)

Tavoite: Lisää merkkien pyörittelyä, jotta ASCII-merkistön toiminta tulee tutuksi. Samalla pientä johdattelua merkkijonoihin (jotka tulevat seuraavassa modulissa).

Toteuta funktio void secret_msg(int msg) joka purkaa ja salaa annetun viestin soveltaen yksinkertaista algoritmia. Salaviestit on numeroitu kokonaisluvulla, joka annetaan funktion parametrilla msg.

Voit hakea salaviestin merkkejä yksi kerrallaan käyttäen funktiota char get_character(int msg, unsigned int cc), joka on annettuna harjoituspohjassa (kyseinen funktio on toteutettu käyttäen taulukoita ja merkkijonoja, jotka tulevat vastaan vasta seuraavassa modulissa.). Parametri msg kertoo mistä viestistä on kysymys, ja on sama arvo, jonka olet saanut secret_msg - kutsun mukana. cc on haettavan merkin järjestysluku. Funktio palauttaa paluuarvonaan kyseisen merkin.

Viestin sisältämät merkit on numeroitu nollasta alkaen. Sinun tulee kutsua get_character - funktiota kullekin viestille useamman kerran, kasvattaen aina merkkilaskuria, kunnes funktio palauttaa 0, mikä tarkoittaa että viesti on lopussa.

Kun luet merkkejä, sinun tulee purkaa salaus ja tulostaa kukin merkki ruudulle, kunnes 0-merkki tulee vastaan. 0-merkkiä ei tulosteta.

Salauksenpurku-algoritmi on seuraavanlainen: vähennät saamasi merkkiarvon desimaaliluvusta 158, eli 158 - m, missä m on get_character - funktiolta saamasi merkkiarvo. Tämän laskutoimituksen tulos tulostetaan siis merkkinä (ei esimerkiksi desimaalilukuna).

Voit testata funktiota viesteillä, jotka on numeroitu 0:ksi ja 1:ksi src/main.c:ssä. Jos funktio toimii, näiden salaviestien tulisi muuntua lyhyiksi englanninkielisiksi lauseiksi. TMC-tarkistuksissa käytetään myös muita merkkijonoja.

Task 09-ships: Laivanupotus (4 pts)

Tavoite: Rakenna hieman isompi ohjelma, joka koostuu muutamasta funktiosta.

Tässä tehtävässä toteutetaan yksinkertainen laivanupotuspeli. Pelin toteuttamisessa tarvitaan joitain C:n ominaisuuksia, joita ei ole vielä käyty läpi, joten osa tarvittavista funktioista on annettu valmiina. Sinun täytyy toteuttaa neljä funktiota saadaksesi pelin valmiiksi.

Pelikenttä on 10x10 ruudun kokoinen, ja kukin laiva on 3 ruutua pitkä. Koordinaatit pysty- ja vaakasuuntaan merkataan välillä 0 ja 9: (0,0) on vasen yläruutu, ja (9,9) on oikea alaruutu. Peli päättyy kun kaikki laivat on upotettu.

Pelikoodi on jaettu kahteen erilliseen C-kieliseen lähdetiedostoon, joita molempia tarvitaan pelin kääntämiseen. shiplib.c sisältää apufunktioita joilla käsitellään pelikenttää. Sinun täytyy kutsua näitä funktioita osana omien tehtäväfunkitoidesi toteutusta, mutta tätä tiedostoa ei kannata muuttaa. Lue koodin seassa olevia kommentteja selvittääksesi miten funktiot toimivat. Tiedosto ships.c sisältää funktiot jotka sinun tulee toteuttaa.

Sinun tulee listata seuraavassa kuvatut neljä funktiota. Saat pisteen kunkin funktion toimivasta toteutuksesta.

a) Aseta laivat

Toteuta funktio void set_ships(unsigned int num) joka asettaa num alusta pelikartalle. Asettaaksesi yhden laivan johonkin karttapaikkaan, sinun tulee kutsua funktiota place_ship, jonka parametreiksi annat laivan sijainnin ja suunnan (katso lähdekoodista tarkempi kuvaus). Huomaa, että place_ship - funktio ei onnistu mikäli yrität asettaa alusta toisen päälle, tai ulos kartta-alueelta, joten sinun tulee tarkistaa funktion paluuarvo, jotta voit varmistua funktion onnistumisesta.

Vinkki: Voit käyttää C-kirjaston funktiota rand()** valitaksesi laivalle satunnaisen sijainnin ja suunnan. Funktio palauttaa satunnaisen kokonaisluvun, jonka voit rajoittaa haluamallesi lukualueelle käyttämällä jakojäännös-operaatiota ('%'). Esimerkiksi rand() % 10 tuottaa satunnaislukuja välillä 0 ja 9.

b) Tulosta pelikenttä

Toteuta funktio void print_field(void) joka tulostaa koko pelikentän ruudulle. Mikäli pelaaja ei vielä tunne ruudun sisältöä (eli ei ole ampunut sitä), '?' tulisi tulostaa. Mikäli ruutu on tunnettu, tuloste voi olla yksi kolmesta vaihtoehdosta:

  • '.' jos paikassa ei ole laivaa
  • '+' jos paikassa on laivan osa johon ei ole vielä osunut (tarvitaanko tätä?)
  • '#' jos paikassa on laivan osa johon on osuttu.

Alussa kaikki ruudut ovat näkymättämiä, mutta ruutu muuttuu näkyväksi, kun siihen ammutaan.

Tarvitset kahta funktiota: is_visible(x,y), kertoo onko annettu ruutu näkyvissä, ja is_ship(x,y), joka kertoo mikäli ruudussa on laiva ja onko siihen osunut. Lue shiplib.c - lähdekoodista tarkemmat kuvaukset funktioista.

c) Ammu

Toteuta funktio int shoot(void) joka kysyy kaksi etumerkitöntä kokonaislukua käyttäjältä välilyönnillä eroteltuna. Nämä esittävät koordinaatteja, joihin seuraavaksi ammutaan. Jos käyttäjä antaa virheellisen syötteen, tai koordinaatit eivät ole pelialueen sisässä, funktion tulee palauttaa -1. Jos annetussa sijainnissa on alus, sinun tulee kutsua funktiota hit_ship() merkataksesi sijainnin osutuksi ja palauttaa arvo 1. Mikäli sijainnissa ei ole alusta, sinun tulee palauttaa arvo 0. Molemmissa tapauksissa tulee ruutu merkata näkyväksi funktiota checked() kutsumalla.

d) Pelin päättyminen

Toteuta funktio int game_over(unsigned int num) joka palauttaa 1, mikäli kaikki laivat on upotettu, tai 0, mikäli kentällä on vielä laivan osia joihin ei ole osuttu. Parametri num kertoo kuinka monta laivaa kentällä on. Saat selville kunkin laivan tilanteen käyttämällä is_ship() - funktiota, ja koska tiedät että kukin laiva on 3 ruutua pitkä, tiedät kuinka monesta ruudusta tulee löytyä osuma.