10. C:n taulukot

Alunperin julkaistu: 11.2.2017

Viimeksi muokattu: torstai 11.6.2020

Taulukko lienee tuttu elementti jokaiselle, joka tätäkin opasta on tähän asti lukenut. Taulukoita määritellään oikeassa elämässä siten, että taulukolle annetaan tietty määrä rivejä ja sarakkeita, mihin tieto kirjataan ylös. Excel-taulukko on tästä yksi esimerkki.

Taulukoiden hyödyllisyys on siinä, että ne sisältävät yleensä jonkin verran tai paljon tietoa, mitä ohjelman pitää hyödyntää, että sovellus toimii oikein.

C-kielessä taulukoita määritellään periaatteessa samalla tavalla, mutta että taulukon varaaminen onnistuu ongelmattomasti, täytyy c:n kielioppi tuntea sitä varten.

Taulukoiden määrittelemisessä on kuitenkin muutama asia, joihin kannattaa kiinnittää huomiota, kuten kuinka suuri taulukko itse asiassa on, miten taulukko on alustettu eli mitkä ovat sen alkuarvot ja miten taulukkoa luetaan tai käytetään. Nämä asiat vaikuttavat olennaisesti siihen, kuinka asiat kannattaa c-kielessä tehdä, vaikka yhtä ainoata oikeaa tapaa ei välttämättä ole ja jokaisella on oma tyylinsä tehdä asiat ohjelmassa.

Taulukoita voi olla montaa eri tyyppistä. Yksiulotteinen taulukko on pienin mahdollinen taulukko ja moniulotteinen taulukko voi olla jopa 3D taulukko, eli mihin on tallennettu vaikkapa X,Y ja Z -koordinaatit. Lisäksi tietotyyppi täytyy taulukon jokaisessa solussa (sanotaan tietueeksi) olla sama. Seuraavassa käydään läpi muutamia perusasioita taulukoista ja niiden käytöstä. Huomaathan, että jatkossa käytän taulukon yksittäisestä elementistä nimitystä solu. Se on mielestäni helpompi ymmärtää kuin tietue, joka on teknisesti korrektimpi termi, mutta ei niin aloittelijaystävällinen (tietysti makuasia, mutta tämä on minun mielipiteeni).

Yksiulotteinen taulukko

Yksiulotteinen taulukko on muistialue, joka koostuu tietyn pituisesta jonosta merkkejä, lukuja tai muita symboleita. Taulukon yksittäisen tietueen eli solun tallennuskapasiteetin (eli montako bittiä voidaan yhteen soluun tallentaa) määrittelee kääntäjä taulukon luonnin yhteydessä tietotyypin perusteella. Jos tietotyypiksi on valittu char tai unsigned char, on tiedon yksittäisessä solussa 8-bittiä. Jos taas käytetään tietotyyppiä int, niin käytössä on 16-bittiä. Jos käytetäänlong tai float tietotyyppejä, on käytössämme kokonaiset 32-bittiä. Olipa käytössä mikä tietotyyppi tahansa, on taulukon yhden solun koko aina vähintään 8 bittiä eli yksi tavu.

Tässä tuleekin yksi suorituskykyyn vaikuttava tekijä. MSP430 sarjalaiset mikrokontrollerit ovat 16-bittisiä, joten ne parhaiten käsittelevät 16-bittisiä lukuja. Jos käytössä on jatkuvasti esimerkiksi 32-bittiset luvut ja runsaasti laskentaoperaatioita, niin mikrokontrollerilla saattaa suorituskyky loppua kesken (jos vaatimuksena esimerkiksi on laskea vaikkapa tuhat kertolaskua ajassa X). Mutta takaisin taulukoihin.

Yksiulotteinen 8-bittinen taulukko voidaan määrittää hyvinkin helposti. Se tapahtuu näin:

char taulukko[5];

Tässä varattiin 5 solua sisältävä taulukko. Varaaminen (tai esittely/luominen) tapahtuu samaan tapaan kuin muuttujankin luominen, tässä käytetään lisäksi kuitenkin hakasulkuja jonka sisällä taulukon koon kertova numero. Tämä kertoo kääntäjälle, että varataan muistista taulukko ja tietty määrä taulukon soluja. Tässä kohtaa taulukon arvoksi ei ole vielä asetettu mitään, joten muistista varatun taulukon soluissa voi olla satunnaista dataa. Alla oleva esimerkki näyttää, miten taulukko varataan pääohjelman lohkossa.

#include <msp430g2231.h>

void main(void)
{
     WDTCTL = WDTPW + WDTHOLD; // watchdog pysäytys 
     char taulukko[5]; 
     // taulukko jonka pituus on 5, solujen arvot ovat todennäköisesti mitä sattuu

     while(1)
     { 
          // tyhjä pääsilmukka 
     }
}

Jos käännät ohjelman ja katsot debuggerin avulla taulukon sisältöä, voit huomata että soluissa on joitain arvoja. On toki mahdollista että solut sattuvat olemaan tyhjinä, eli arvossa 0, mutta usein alustamattomat taulukot sisältävät satunnaista dataa. Sen vuoksi on suositeltavaa, että taulukko alustetaan ennen kuin sitä käytetään. Taulukon alustamisesta on alla esimerkki.

#include <msp430g2231.h>

void main(void)
{
     WDTCTL = WDTPW + WDTHOLD; // watchdog pysäytys 
     char taulukko[5] = {0, 0, 0, 0, 0};  // taulukko jonka solut on alustettu nollaan

     while(1)
     { 
          // tyhjä pääsilmukka 
     }
}

Kuten yllä olevasta voidaan huomata, on nyt taulukon kaikki viisi solua asetettu nollaan. Nollien tilalla voisi olla myös muita arvoja, mikäli ohjelmoijalla on tiedossa arvot, jotka lähtötilanteessa täytyy ohjelmalle antaa. Taulukko olisi voitu luoda myös alla olevalla koodirivillä, joka tekee saman asian kuin yllä oleva esimerkki, mutta hakasulkujen sisään ei tällöin tarvitse kirjoittaa taulukon kokoa:

char taulukko[5] = {0, 0, 0, 0, 0};  
//numeroiden sarjaa voisi jatkaa niin kauan kuin muistia riittää

Taulukoiden alustamiseen on myös funktioita, mutta asioiden yksinkertaisuuden vuoksi ei niihin nyt paneuduta. Sen sijaan alle on tehty vielä yksi esimerkki while -silmukan avulla tehtävästä taulukon alustuksesta taikka arvojen kopioinnista.

#include <msp430g2231.h>

void main(void)
{
     WDTCTL = WDTPW + WDTHOLD; // watchdog pysäytys 
     char taulukko[5];  // taulukko jonka pituus on 5
     char indeksi = 0;

     while(indeksi < 5)
     {
          taulukko[indeksi] = 0xAB;
          // asetetaan jokaisen solun arvoksi AB heksana = 10101011 bin.
          // kasvatetaan indeksiä, jolloin seuraavalla suorituskerralla
          // AB-arvon asetus kohdistuu taulukon seuraavaan soluun
          indeksi++;
     }
}

Koodista voidaan huomata, että while -silmukan suoritus lopetetaan, kun indeksi on saanut arvon 4, koska 4 on pienempi kuin 5. Mutta miksi juuri 4, vaikka taulukon koko on 5? Tämä johtuu siitä, että muistissa taulukon ensimmäisen solun numero taikka indeksi on aina 0, aivan kuten osassa 2 kerrottiin.

Yksiulotteinen taulukko

Käytännössä yllä oleva koodi ei tee mitään järkevää, joten kirjoitetaanpa toinen sovellus, joka tekee jotain järkevää. Lasketaan vaikka käyttäjän napin painalluksia ja tallennetaan napin painallukseen kulunut aika. Tähän tarvittaisiin oikeasti hieman enemmän työtä, mutta tehdään yksi helpottava oletus esimerkin yksinkertaistamiseksi:

1) Oletetaan, että aiemmin tekemämme Viive -aliohjelma kuluttaa aikaa sata millisekuntia kun sille annetaan arvoksi 1000.

Annetaan ohjelman toimia lisäksi siten, että napin painallukset tallennetaan edellisen esimerkin mukaiseen taulukkoon, missä on viisi muistipaikkaa eli solua. Tallennetaan jokaisella kerralla napin painallukseen kulunut aika sen jälkeen kun nappi on päästetty irti.

#include <msp430g2231.h>

#define EI 0 // sanat helpompi ymmärtää kuin numerot 
#define KYLLA 1

void main(void)
{
     WDTCTL = WDTPW + WDTHOLD; // watchdog pysäytys 
     char taulukko[5]; // taulukko jonka pituus on 5 merkkiä tai 8-bit. arvoa 
     char indeksi=0; // muuttuja indeksi, jota tarvitaan taulukon käsittelyssä 
     char saa_tallentaa = EI; // muuttuja joka kertoo ohjelmalle milloin tiedon saa tallentaa 
     char aika_100ms = 0; // alustetaan 100 ms mittausmuuttuja nollaan 
     int viive = 5600; // noin 100 ms tällä ohjelmalla 

     P1DIR |= BIT0; // LED pun. out 
     P1DIR &= ~BIT3; // nappi input
     while(1)
     {
          while(!(P1IN & BIT3)) // nappia painettu? pyöritään niin kauan kuin on
          {
               P1OUT ^= BIT0; // vilkutetaan lediä
               Viive(viive);
               aika_100ms++;
               saa_tallentaa = KYLLA; // kerrotaan loppuohjelmalle että saa tallentaa
          }

          if(KYLLA == saa_tallentaa) // tarkastetaan saako tallentaa
          {
               saa_tallentaa = EI; // ei saa tallentaa ennen kuin mittaus on tehty uudestaan
               // jaetaan mittaustulos kahdella että tulos on oikein.
               taulukko[indeksi] = aika_100ms / 2; // tallennus 
               aika_100ms = 0; // nollataan mittausmuuttuja
               indeksi++; // kasvatetaan taulukon solun ilmaisevaa muuttujaa, indeksiä                               

               if(indeksi > 4) // tarkastetaan onko viimeinen solu kyseessä 
                    indeksi = 0; // nollataan jos on, ettei ylikirjoiteta muita muuttujia
          } 
     }
}
void Viive(unsigned int aika)
{ 
     unsigned int x=0; 
     for(x=0;x<aika;x++);
}

Huomataan, että koodia tuli tällä kertaa hieman enemmän ja rakenne on hieman monimutkaisempi. Tärkeintä kuitenkin tämän kappaleen puitteissa on se, miten taulukkoa on tässä ohjelmassa käytetty. If -lauseen ehdossa tarkistetaan ns. lippu, joka kertoo voidaanko mitattu aika tallentaa taulukkoon. Jos näin on, niin taulukko[indeksi] -paikkaan tallennetaan saatu tieto. Aika on jaettu kahdella, jotta mittaustulos saadaan suurinpiirtein oikein satoina millisekunteina talteen.

Yksinkertaistaen lisää yllä esitettyä, olen seuraavaksi kirjoittanut pari selventävää riviä miten taulukkoon kirjoitetaan ja luetaan:

// tallennetaan taulukon indeksin 0 paikkaan arvo 3. Oletetaan että indeksi = 0
taulukko[indeksi] = 3;

​// luetaan muuttujaan arvo taulukosta
muuttuja = taulukko[indeksi]; 

// voidaan lukea myös haluttu solu suoraan
muuttuja = taulukko[2];

// voidaan myös tallentaa suoraan taulukon soluun näin
taulukko[2] = arvo;

// tallennetaan taulukon soluun jonka jälkeen indeksinumeroa kasvatetaan yhdellä
taulukko[indeksi++] = arvo;

// tallennetaan taulukon soluun jonka jälkeen indeksinumeroa pienennetään yhdellä
taulukko[indeksi--] = arvo;

Kuten huomaat, ei yksiulotteisen taulukon käyttö juuri poikkea muuttujien käytöstä. Jos taulukko on luotu oikein ja muistia on riittävästi, kyseiset koodirivit toimivat varmasti.

Useampiulotteinen taulukko

Joskus ohjelmoinnissa tarvitaan kaksi- tai useampi ulotteisia taulukoita. Esimerkiksi 2D ja 3D taulukot ovat tietyissä tapauksissa yleisiä. Esimerkiksi koordinaateissa tai bittikartoissa tällaiset taulukot ovat sopivia käyttää.

Käytännössä 2D tai 3D taulukko ei eroa yksiulotteisesta taulukosta muistivarauksen suhteen, mutta C-kielessä niiden käyttäminen voi tuntua aluksi hankalalta. Muistissa yksiulotteinen taulukko varataan perättäisistä muistipaikoista ja esimerkiksi edellä mainittu 5 soluinen taulukko vie muistista 5 tavua tilaa. 2D taulukko, kuten myös muutkin taulukot varataan muistista samalla tavalla, usein perättäisistä muistialueista. Tästä ei ohjelmoijan välttämättä tarvitse välittää, mutta sulautetuissa ja resurssirajoitteisissa laitteissa on hyvä tietää kuinka muisti toimii.

Jotta turhalta liirum-laarumilta vältytään, otetaan suoraan käytännön esimerkki. Oletetaan, että halutaan varata muistista tilaa vaikkapa kuudelle viestille, joilla on seuraavat tietokentät:

1) nopeus (esim. 0 - 100 -> mahtuu yhteen tavuun)
2) suuntatieto (1 = eteen, 0 = taakse)
3) liikkeen sallinta (1 = sallittu, 0 = ei sallittu)

Tämä viesti voisi olla vaikka moottorinohjaukseen tai vastaavaan tarkoitettu viesti, joka kertoo ohjaavalle mikrokontrollerille miten sen täytyy laitteita ohjata. Oletetaan, että varataan kaikille tiedoille yksi tavu muistia, eli taulukon pituudeksi tulee 3.

Kun tallennettavaa tietoa on kuuden viestin verran, tarvitaan 6 x 3 = 18 tavua muistia. Tämä varataan muistista seuraavasti:

char moottoriohjaus[6][3];  
// kaksiulotteinen taulukko, missä varattu kuusi riviä ja 3 solua kultakin riviltä

Yllä oleva taulukko sisältää samaan tapaan satunnaista dataa kuin aiemminkin, minkä vuoksi on suositeltavaa alustaa kyseinen taulukko. Sen voi alustaa esimerkiksi taulukon luontivaiheessa näin:

char moottoriohjaus[6][3] =
{ { 0, 0, 0}, // 1. viesti
{0, 0, 0},    // 2. viesti
{ 0, 0, 0},   // 3. viesti
{0, 0, 0},    // 4. viesti
{ 0, 0, 0},   // 5. viesti
{0, 0, 0}     // 6. viesti
}; // huomaa puolipiste ja kaarisulku

Yllä olevassa 2D taulukossa on nyt alustettuna kaikki 18 tavua arvoon 0. Kuten yksiulotteista taulukkoa, 2D taulukkoa voitaisiin käsitellä myös suoraan indeksien kautta näin (tämä sen jälkeen kun taulukko on luotu):
moottoriohjaus[5][2] = 55;  // kirjoitetaan viimeiseen soluun 0
Yllä oleva koodirivi kirjoittaisi arvon 55 taulukon viimeiseen soluun. Tässä voit jälleen huomata että taulukkoa käsiteltäessä solujen numeroihin pitää kiinnittää huomiota. Kuten yksiulotteisessa taulukossa, ensimmäisen solun (tai rivin/sarakkeen) indeksinumero alkaa aina nollasta, näin ollen viimeinen mahdollinen muistipaikka on [5][2].

Myös ohjelmallisia taulukon käsittelyitä on mahdollista tehdä. Alla olevassa esimerkissä alustetaan luotu 2D taulukko nollaan ohjelman suorituksen aikana.

#include <msp430g2231.h>
void main(void)
{
     WDTCTL = WDTPW + WDTHOLD; // watchdog pysäytys 
     char moottoriohjaus[6][3]; // 2D taulukko 
     char rivi_Y=0; // tarvitaan taulukon käsittelyssä 
     char solu_X=0;

     for(rivi_Y = 0; rivi_Y < 6; rivi_Y++)
     {
          // tässä silmukassa käydään läpi jokainen rivi. Rivejä on 6.
          for(solu_X = 0; solu_X < 3; solu_X++)
          {
               // tässä silmukassa käydään läpi jokaisen rivin
               // yksittäinen solu. Yhdellä rivillä on 3 solua. 
               moottoriohjaus[rivi_Y][solu_X] = 0; // asetetaan solu nollaan 
          }
     }
}

Kuten jälleen voidaan huomata, ei useampiulotteisenkaan taulukon käsittely ole tämän vaikeampaa. Niin kauan kuin pysytään taulukon indeksirajojen sisällä, ei ongelmia pitäisi tulla. Koska C-kieli kuitenkin mahdollistaa taulukoiden vääränlaisen käsittelyn ohjelman suorituksen aikana, täytyy näiden kanssa olla tarkkana.

3D taulukossa taulukkoelementtejä tulisi jälleen yksi lisää, jolloin taulukkomuuttujan muistinvarauskoodi menisi esimerkiksi näin:

char moottoriohjaus[6][3][2];  // kolmiulotteinen taulukko

Yllä olevassa 3D taulukossa olisi näin ollen rivi ja sarakemäärä pysyisi samana, mutta näiden rinnalle tulisi toinen "ulottuvuus", kuten D-kirjainkin antaa ymmärtää  (D tulee engl. sanasta dimension). Alla olevassa kuvassa on vielä esitetty graafisesti, kuinka eri ulotteiset taulukot eroavat toisistaan.

Eri taulukoiden varaus olisi kuvan tapauksessa:
char taulukko_1D[3];  // taulukko, 1D
char taulukko_2D[3][3];  // taulukko, 2D
char taulukko_1D[3][3][2];  // taulukko, 3D

Taulukoiden avulla voidaan siis tallentaa tietoa hyvin erilaisista asioista. Toivottavasti taulukoiden käyttäminen tuli hieman tutummaksi, sillä niiden käyttäminen on ohjelmoinnissa aika tavallista. Launchpadilla voit turvallisesti tutkia miten erilaiset muistin varaukset toimivat, ja kehotankin kokeilemaan ja muokkailemaan eri koodeja ja seuraamaan ohjelman etenemistä debuggerin avulla.

Näin on käyty läpi ensimmäiset kymmenen osaa Launchpad oppaassani. Kaikkia asioita ei näin kapeisiin aihealueisiin saa millään mahtumaan, eikä ole mahdollista selittää kaikkia yksityiskohtia näistäkään osa-alueista. Tarkoituksena on ollut antaa enemmänkin yleiskäsitystä siitä, mitä kaikkea ohjelmointi pitää sisällään ja miten yksinkertaisia asioita saadaan C-kielellä ja Launchpadilla tehtyä.

Seuraavaksi siirrytään Launchpadin käytössä eteenpäin ja aletaan tutkimaan mitä muita ominaisuuksia Launchpadissä on kuin ledin vilkutus ja näppäimen painallus.