C/Taulukot
Tämä luku kertoo C:n taulukoista.
Yksiulotteiset
muokkaaTaulukko (engl. array) on muuttuja, johon on mahdollista tallentaa joukko arvoja. Yksiulotteinen taulukko luodaan samalla tavalla kuin normaali muuttuja, mutta sen perään laitetaan hakasulkuihin alkioiden lukumäärä. Seuraava esimerkki luo taulukon johon voi tallentaa kymmenen kokonaislukua.
int taulukko[10];
Taulukon alkioiden numerointi alkaa nollasta, ei ykkösestä. 10-alkioisen taulukon ensimmäisellä alkiolla on siis indeksi 0 ja viimeisellä 9. Yllä oleva taulukko näyttäisi siis tältä.
Taulukkoja voi alustaa eri tavoin. Yksi tapa on esitellä taulukko yllä olevalla tavalla ja täyttää jokainen alkio yksi kerrallaan. Arvojen sijoittaminen taulukoiden alkioihin tapahtuu samalla tavalla kuin tavallisiin muuttujiin. Sinun täytyy vain kirjoittaa hakasulkuihin muokattavan alkion järjestysnumero eli indeksi.
int taulukko[3]; taulukko[0] = 1; taulukko[1] = 2; taulukko[2] = 3;
Jos alkioita on monta, tällainen arvojen sijoittaminen on kuitenkin työlästä kirjoittaa. On olemassa toinen tapa alustaa taulukoita, jonka avulla voit alustaa monta alkiota kerralla laittamalla arvot aaltosulkujen sisään pilkuilla eroteltuina. Mikäli aaltosulkeisiin ei kirjoiteta kaikkia arvoja, loput alustetaan nolliksi. Jos aaltosulkuihin laitetaan ainoastaan nolla, kaikki alkiot alustetaan nolliksi.
/* alustetaan alkioihin eri arvot */ int numeroita[3] = { 1, 2, 3 }; /* alustetaan kaikki alkiot nolliksi */ int numeroita[5] = { 0 };
C-kielessä taulukot ja osoittimet ovat hyvin läheistä sukua keskenään, ja itse asiassa taulukon indeksointiin käytettävä lauseke numeroita[3] tekee aivan saman asian kuin *(numeroita + 3) ja itse asiassa myös saman kuin *(3 + numeroita) eli 3[numeroita]. Tällaisten muotojen käyttöä ei voida pitää hyvänä ohjelmointityylinä, mutta taulukon osoitinluonne on hyvä tuntea. Edelliset lausekkeet toimivat, koska taulukko (muuttuja) numeroita muuntuu implisiittisesti int*-tyyppiseksi osoittimiksi, joka osoittaa taulukon ensimmäiseen alkioon. Siis numeroita on sama kuin &numeroita[0] eli &*(numeroita + 0).
Osoitinluonne mahdollistaa myös taulukoiden välittämisen funktioiden argumentteina:
#include <stdio.h> void print(int* array, size_t size) { // Tämä antaa osoittimen koon - taulukon koko ei välity osoittimen mukana, vaan se pitää antaa erikseen printf("sizeof(array) = %d\n", sizeof(array)); printf("%d != %d\n", sizeof(array) / sizeof(array[0]), size); // Tulostetaan taulukon sisältö for (size_t i = 0; i < size; ++i) printf("%d\n", array[i]); } void modify(int* array) { array[2] = -1234; } int main(void) { int numbers[] = { 4, 8, 15, 16, 23, 42 }; // Huom: sizeof(numbers) antaa taulukon koko sisällön koon tavuina ja // sizeof(numbers[0]) yhden alkion koon, joten size on alkioiden lukumäärä int size = sizeof(numbers) / sizeof(numbers[0]); modify(numbers); print(numbers, size); }
Tällaisessa tilanteessa monelle ohjelmoijalle tulee kantapään kautta vastaan se, että osoittimille sizeof antaa osoittimen koon (yleensä 4 tai 8), mutta taulukolle taas sen sisältämän tiedon koon. Tämä on hämäävää, koska C:ssä taulukko muuntuu implisiittisesti osoittimeksi, melkein vahingossa, kuten esim. tässä funktiokutsun yhteydessä.
Myös malloc-funktiolla varattua muistia tai yleensäkin mitä tahansa osoitinta voi käyttää taulukkona, koska indeksointi on todellisuudessa pelkkää osoitinaritmetiikkaa. Tätä tarvitaan jos halutaan esim. varata taulukko funktiossa ja antaa se paluuarvona, jolloin staattisesti varattu taulukko vapautettaisiin automaattisesti funktion päättyessä.
Moniulotteiset
muokkaaMoniulotteiset taulukot ovat taulukoita, joita indeksoidaan useammalla kuin yhdellä muuttujalla, esim. array[15][12][i] (kolmiulotteinen taulukko). Niitä on C:ssä kahta päätyyppiä: staattisia ja dynaamisia, joista jälkimmäisiä kutsutaan myös osoitintaulukoiksi, koska ne on toteutettu osoittimin.
Staattiset moniulotteiset taulukot
muokkaaStaattisessa kaksiulotteisessa taulukossa array[3][4] on 12 alkiota, jotka on sisäisesti tallennettu 12-alkioiseen 1-ulotteiseen taulukkoon, mutta kääntäjä antaa indeksoida taulukkoa ikään kuin se olisi kaksiulotteinen. Kirjoittaessasi array[i][j], hakee kääntäjä taulukostaan alkion [i * 4 + j]. Huomaa, että tässä kääntäjän oli tiedettävä taulukon taulukon koko toisen dimension suhteen. Vastaavasti kolmiulotteisella staattisella taulukolla taulukko[3][4][5], viitattaessa soluun taulukko[i][j][k], indeksi lasketaan [i * 4 * 5 + j * 5 + k] ja niin edelleen. Huomionarvoista tässä on, että taulukon käyttämiseksi kääntäjän täytyy aina tietää sen kaikki dimensiot, ensimmäistä lukuun ottamatta.
Staattiset moniulotteiset taulukot esitellään samoin kuin yksiulotteisetkin ja ne voi myös alustaa vastaavasti (jos ei alusta, ovat taulukon sisältämät arvot satunnaisia). Taulukkoa ei kuitenkaan yksiulotteisten tapaan voi helposti välittää funktiolle, koska funktion määrittelyssä tulee taulukon koon (paitsi ensimmäisen indeksin) olla määritelty jo lähdekoodissa, kuten tässä:
#include <stdio.h> void print(int taulukko[][4]) { for (size_t i = 0; i < 3; ++i) { for (size_t j = 0; j < 4; ++j) printf("%d ", taulukko[i][j]); putchar('\n'); } } int main(void) { int taulukko[3][4] = { { 11, 12, 13, 14 }, { 21, 22, 23, 24 }, { 31, 32, 33, 34 } }; print(taulukko); }
Käytännössä moniulotteista staattista taulukkoa ei kannata koskaan välittää muualle. Jos tarvetta taulukon välittämiseen on, ovat yksiulotteiset taulukot (joita voi kertolaskun avulla käyttää moniulotteisena) ja dynaamiset taulukot parempia vaihtoehtoja.
Dynaamiset moniulotteiset taulukot
muokkaaOsoitintaulukko on huomattavasti staattista taulukkoa joustavampi ratkaisu, mutta myös merkittävästi hankalampi luoda. Taulukko ei myöskään tuhoudu automaattisesti lohkon päätteeksi, vaan se täytyy itse vapauttaa. Staattisesta moniulotteisesta taulukosta poiketen, osoitintaulukko voidaan helposti välittää funktion argumenttina, tietämättä sen kokoa ennalta. Seuraavassa esimerkki täysin dynaamisesta moniulotteisesta taulukosta:
#include <stdio.h> #include <stdlib.h> void print(int** array, size_t dim1, size_t dim2) { for (size_t i = 0; i < dim1; ++i) { for (size_t j = 0; j < dim2; ++j) printf("%d ", array[i][j]); putchar('\n'); } } int** array_construct(size_t dim1, size_t dim2) { // Varataan ulommaisin taulukko (ensimmäinen indeksi), sisältää osoittimia sisätaulukoihin int** array = malloc(dim1 * sizeof(int*)); for (size_t i = 0; i < dim1; ++i) { // Varataan sisätaulukko (toinen indeksi), sisältää numeroita array[i] = malloc(dim2 * sizeof(int)); // Alustetaan taulukon solut vastaavilla arvoilla kuin staattisella taulukolla edellä for (size_t j = 0; j < dim2; ++j) array[i][j] = (i + 1) * 10 + j + 1; } } void array_destruct(int** array) { for (size_t i = 0; i < 3; ++i) free(array[i]); free(array); } int main(void) { int** array = array_construct(3, 4); // ... print(array, 3, 4); // Kerrotaan oikeassa alakulmassa olevan alkion arvo sadalla: array[2][3] *= 100; // ... // Vapautetaan taulukolle varattu muisti array_destruct(array); array = NULL; // Jottei vahingossa käytettäisi jo vapautettua muistia // ... }
Edellä olevassa esimerkkikoodissa mallocin paluuarvoja ei tarkistettu, mutta oikeassa koodissa pitää aina mallocin kutsumisen jälkeen tarkistaa palauttiko kutsu NULL:n ja tällöin suorittaa vaadittavat siivoustoimenpiteet (esim. jo varatun muistin vapautus) ennen funktiosta poistumista.
Täysin dynaamisessa osoitintaulukossa rivien ei tarvitse olla yhtä pitkiä, eli silmukan sisällä oltaisiin voitu tehdä malloc((i + 1) * sizeof(int)), jolloin ensimmäisellä rivillä olisi yksi alkio, toisella kaksi, jne. Rivien kokoa voi muuttaa (realloc) ilman että muulle taulukossa olevalle tiedolle tarvitsee tehdä mitään ja myös rivien lisäys ja poisto hoituu kevyesti riviosoittimia siirtelemällä (sisällä olevaa tietoa ei tarvitse siirtää esim. lisättäessä uusi rivi taulukon alkuun).
Monesti kuitenkin riittää myös kevyempi "puolidynaaminen" osoitintaulukko, jossa tieto on tallennettu isoon yksiulotteiseen taulukkoon (voi olla esim. staattinen moniulotteinen taulukko), mutta sitä indeksoidaan osoitintaulukon avulla. Seuraavassa esimerkki sellaisen luomisesta:
int static_array[3 * 4] = { 11, 12, 13, 14, 21, 22, 23, 24, 31, 32, 33, 34 }; int* array[3]; // Osoitintaulukko for (size_t i = 0; i < 3; ++i) array[i] = static_array + i * 4 + j; print(array, 3, 4);
Taulukko static_array olisi yhtä hyvin voinut olla staattinen kaksiulotteinen taulukko, koska sellaisen sisäinen rakenne on identtinen yksiulotteisen taulukon kanssa. Osoitintaulukko taasen on tällä kertaa toteutettu pinosta varattuna, eikä mallocilla varattuna, joten sitä ei tarvitse erikseen vapauttaa funktion päättyessä. Kuitenkin taulukko voidaan print-funktiolle välittää samoin kuin mikä tahansa osoitintaulukko (huomaa implisiittinen tyyppimuunnos int* array[3]:sta int**:ksi funktiokutsun yhteydessä).
Osoittimet taulukkoihin
muokkaaOsoittimet voivat osoittaa taulukkoihin. Tällöin osoittimen tähtimerkki ja nimi sijoitetaan sulkeiden sisälle:
int* taulukko_osoittimia[3]; /* taulukko, jossa on osoittimia */ int (*osoitin_taulukkoon)[3]; /* osoitin taulukkoon */
Tällaista rakennetta käytetään harvoin, ellei haluta esittää moniulotteista taulukkoa.
C-ohjelmointikieli |
---|
Yksinkertainen C-kielinen ohjelma — Muuttujat — Operaattorit — Kommentit — Ohjausrakenteet — Funktiot — Osoittimet — Dynaaminen muistinvaraus — Taulukot — Merkkijonot — Tietueet — Esikääntäjä — Otsikkotiedostot C-kielen varatut sanat — Standardikirjasto — Aiheesta muualla |