C-ohjelmoinnin perusteet
Johdanto

Tässä oppaassa tutustutaan C-kielen alkeisiin ja mm. seuraaviin asioihin:

  • Mitä on ohjelmointi
  • C-kielisen ohjelman rakenne
  • Muuttujien käyttö
  • Funktioiden käyttö
  • Ehtorakenteet
  • Toistorakenteet

Tässä oppaassa keskitytään tekstipohjaisiin C-kielisiin konsoliohjelmiin, joita suoritetaan terminaalissa. C-kielellä voidaan tehdä myös graafisia sovelluksia esimerkiksi erillisten käyttöliittymäkirjastojen tai käyttöjärjestelmän tarjoamien rajapintojen avulla, mutta graafisten käyttöliittymien toteuttaminen ei kuulu tämän oppaan sisältöön.

Mitä on ohjelmointi?

Ohjelmointi tarkoittaa tietokoneohjelman kirjoittamista. Tietokone suorittaa vain konekielisiä ohjelmia. Konekieli on ihmiselle vaikeaselkoista, joten ihmiset eivät kirjoita ohjelmia konekielellä vaan jollakin ohjelmointikielellä. Tätä ihmisen kirjoittamaa "tekstiä" kutsutaan lähdekoodiksi (tai koodiksi). Tämän vuoksi ohjelman kirjoittamista kutsutaan myös koodaamiseksi.

Kääntäjä ja tulkki

Kuten sanottu tietokone ei siis osaa suorittaa ihmisen kirjoittamaa lähdekoodia, vaan se on ensin muutettava konekieliseksi. Muuttaminen voi tapahtua joko kääntäjän tai tulkin avulla. Kääntäminen tarkoittaa sitä, että kääntäjäksi kutsuttu tietokone-ohjelma lukee lähdekoodin sisältävän tiedoston ja muodostaa sen perusteella uuden tiedoston, joka on konekielinen. Tämä konekielinen tiedosto voidaan nyt suorittaa tietokoneella, eikä sen suorittamiseksi tarvita enää lähdekoodia, eikä kääntäjää. Kääntäjän muodostama konekielinen tiedosto voidaan siirtää toiseen koneeseen ja suorittaa siinä vaikkei tässä koneessa ole mainittua kääntäjää eikä lähdekoodia.

C-kielen kääntäjiä ovat esimerkiksi GCC:n C-kääntäjä (komento gcc), Clang (komento clang) ja Microsoftin MSVC (komento cl). GCC ja Clang ovat käytettävissä useissa käyttöjärjestelmissä, ja MSVC:tä käytetään Windowsissa esimerkiksi Visual Studion yhteydessä. Kääntäjä on eri asia kuin koodieditori: editorilla kirjoitetaan lähdekoodi, ja kääntäjä muuntaa sen konekieleksi.

Tulkitseminen tarkoittaa sitä, että uutta erillistä tiedostoa (konekielistä) ei muodosteta, vaan aina kun ohjelma suoritetaan tulkki lukee lähdekoodin ja antaa konekielisen koodin tietokoneen suoritettavaksi ikään kuin lennosta. Tällä tavalla luodun sovelluksen voi suorittaa vain koneessa, jossa on mainittu tulkki.

Osa ohjelmointikielistä on käännettäviä ja osa tulkittavia. Käännettäviä ohjelmointikieliä ovat esimerkiksi C ja C++. Java ja C# käännetään yleensä välikieleksi, joka suoritetaan ajonaikaisessa ympäristössä. Tulkittavia kieliä ovat esimerkiksi Python ja PHP. JavaScript suoritetaan yleensä selaimen tai muun ajonaikaisen ympäristön avulla.

Lähdekoodi

Lähdekoodi on ihmisen kirjoittamaa ohjelmakoodia, joka on kirjoitettu jollain ohjelmointikielellä, kuten C, Python tai Java. Se sisältää ohjelman ohjeet ja logiikan ja se täytyy kääntää tai tulkata, jotta tietokone voi suorittaa sen.

C-ohjelman muodostamisesta lähdekoodista suoritettavaksi sovellukseksi kerrotaan tarkemmin sivulla C-ohjelmoinnin aloittaminen.

Tietokoneohjelman toiminta

Tietokoneohjelman eli sovelluksen kannalta tietokoneen keskeisimmät osat ovat prosessori, kiintolevy ja keskusmuisti (RAM). Käyttäjän kirjoittama lähdekoodi ja kääntäjän tuottama konekielinen tiedosto tallennetaan kiintolevylle, jossa tiedostot säilyvät myös koneen sammuttamisen jälkeen. Kun ohjelma käynnistetään, se tai osa siitä ladataan keskusmuistiin. Keskusmuisti on jaettu tavun (8 bittiä) kokoisiin lohkoihin eli muistipaikkoihin. Suoritin suorittaa ohjelmaa lukemalla peräkkäisiä muistipaikkoja alueelta, johon ohjelmakoodi on tallennettu, ja tulkitsemalla lukemansa bittijonot konekielisiksi käskyiksi. Kullakin muistipaikalla on osoite, jota tarvitaan kertomaan suorittimelle, mistä muistipaikasta sen tulee kulloinkin lukea tietoa.

Oheinen kuva havainnollistaa keskusmuistin rakennetta ja muistipaikkojen osoitteita. Kuhunkin muistipaikkaan voi siis tallentaa yhden tavun eli 8 bittiä.
Keskusmuistin muistipaikkoja ja osoitteita

Seuraavassa esimerkissä luodaan merkkityyppinen muuttuja nimeltään myVariable ja sen arvoksi sijoitetaan a-kirjain. Sitten tulostetaan:

  • muuttujan arvo
  • muuttujan osoite
  • muuttujan arvo desimaalimuodossa

Alla on esimerkin lähdekoodi:

#include <stdio.h>

int main()
{
    char myVariable='a';
    printf("Muuttujan myVariable arvo = %c\n",myVariable);
    printf("Muuttujan myVariable osoite = %p\n",&myVariable);
    printf("Muuttujan myVariable arvo desimaalimuodossa = %d\n",myVariable);

    return 0;
}

Ja ohjelma tulostaa seuraavat rivit:

Muuttujan myVariable arvo = a
Muuttujan myVariable osoite = 00000065eb7ff92f
Muuttujan myVariable arvo desimaalimuodossa = 97

Nyt tietokoneen muistissa on seuraavanlainen data
Muuttujan arvo muistissa
Binääriluku 0110 0001 on desimaalimuodossa luku 97. ASCII-merkkikoodauksessa se vastaa merkkiä a. ASCII-merkit löytyvät esimerkiksi sivulta https://www.asciitable.com/

1. Lähdekoodi
char myVariable = 'a';
→
2. Merkkivakio
'a'
→
3. Koodiarvo
ASCII / UTF-8: 97
→
4. Muistiin tallentuu 1 tavu
0110 0001
Paina painiketta, niin näet miten merkki a päätyy muuttujan muistipaikkaan.

Merkkikoodaus määräytyy kahdessa kohdassa: lähdekooditiedosto on tallennettu jollakin koodauksella (esimerkiksi ASCII tai UTF-8), ja kääntäjä muuntaa merkkivakion ohjelman käyttämään suoritusaikaiseen merkistöön. Tavallisilla C-kääntäjillä tätä voi ohjata asetuksilla, esimerkiksi GCC/Clang: -finput-charset ja -fexec-charset, tai MSVC: /source-charset, /execution-charset ja /utf-8.

Merkki a on helppo esimerkki, koska sen arvo on sekä ASCII:ssa että UTF-8:ssa sama tavu: desimaalina 97 ja binäärinä 0110 0001.

Huomaa:

  • Desimaaliluku arkikielessä tarkoittaa usein lukua, jossa on desimaaliosa, kuten 3,14.
  • Desimaalimuoto tietotekniikassa tarkoittaa kymmenjärjestelmän mukaista esitystä.
  • Edellä 00000065eb7ff92f on heksadesimaaliluku.

Tämän kurssin kannalta heksadesimaalilukujen ymmärtäminen ei ole tärkeää, mutta voit lukea niistä sivulta https://fi.wikipedia.org/wiki/Heksadesimaalij%C3%A4rjestelm%C3%A4

Tietokoneen RAM-muisti

RAM-muisti (Random Access Memory) eli keskusmuisti on tietokoneen nopea työmuisti, jossa säilytetään käynnissä olevien ohjelmien käskyjä ja niiden käsittelemää tietoa. Sen sisältö katoaa, kun virta katkaistaan, joten pysyvästi säilytettävät tiedot tallennetaan esimerkiksi SSD-levylle.

Ohjelman käyttämään muistiin kuuluu muun muassa pinomuisti ja kekomuisti. Niiden lisäksi muistissa on esimerkiksi ohjelmakoodia sekä globaaleja ja staattisia muuttujia. Nykyaikaisissa käyttöjärjestelmissä ohjelma käyttää yleensä virtuaalimuistia, jonka käyttöjärjestelmä yhdistää fyysiseen RAM-muistiin.

Pinomuisti (Stack)

Pinomuisti on ohjelman muistialue, jota käytetään tavallisesti funktiokutsujen tietojen ja automaattisten paikallisten muuttujien säilyttämiseen. Kun funktio päättyy, sen käyttämä pinotila vapautuu automaattisesti uudelleen käytettäväksi. Kaikki paikalliset muuttujat eivät kuitenkaan sijaitse pinossa: esimerkiksi kääntäjä voi sijoittaa muuttujan rekisteriin, ja staattisilla paikallisilla muuttujilla on oma tallennusalueensa.

Pino toimii LIFO-periaatteella (Last In, First Out), eli viimeksi lisätty tieto poistetaan ensimmäisenä.

Pinomuistin koko on rajallinen, ja liiallinen käyttö (esimerkiksi liian syvä rekursio tai suurien taulukoiden luominen pinomuistiin) voi johtaa stack overflow -virheeseen. Ohjelmoijan tulee siksi olla tietoinen pinon käytöstä ja välttää sen loppumista.

Rekursio tarkoittaa ohjelmointitekniikkaa, jossa funktio kutsuu itseään suorittaakseen tehtävän osissa. Jokainen uusi kutsu varaa yleensä tilaa pinomuistista, ja jos kutsuja kertyy liikaa (esimerkiksi jos lopetusehto puuttuu tai on virheellinen), pinomuisti voi täyttyä ja aiheuttaa stack overflow -virheen. Rekursio on hyödyllinen mm. tietorakenteiden läpikäynnissä ja matemaattisissa ongelmissa, mutta sitä tulee käyttää harkiten.

Kekomuisti (Heap)

Kekomuisti on ohjelman muistialue, jota käytetään dynaamiseen muistinvaraukseen ohjelman ajon aikana. C-kielessä varattu muisti pysyy varattuna, kunnes se vapautetaan esimerkiksi free-funktiolla; varausta tehneen funktion päättyminen ei vapauta sitä automaattisesti. Ohjelman päättyessä käyttöjärjestelmä vapauttaa sen muistin. Dynaaminen muistinvaraus on hyödyllistä esimerkiksi silloin, kun ei tiedetä etukäteen kuinka paljon muistia tarvitaan.

#include <stdlib.h>

int *dyn = malloc(100 * sizeof *dyn); // varataan tilaa 100 int-arvolle
if (dyn == NULL) {
    // Muistinvaraus epäonnistui: lopetetaan ohjelma.
    exit(EXIT_FAILURE);
}

// Varattu muisti on alustettava ennen sen arvojen lukemista.
free(dyn); // vapautetaan varattu muisti

Myös kekomuistin määrä on rajallinen: varausta rajoittavat esimerkiksi käytettävissä oleva muisti ja prosessin muistirajat. Muistinvaraus voi epäonnistua, jolloin malloc palauttaa NULL-osoittimen. C-kielessä ohjelmoijan vastuulla on vapauttaa varattu muisti, kun sitä ei enää tarvita. Jos tämä unohtuu, seurauksena voi olla muistivuoto (memory leak), joka voi lopulta johtaa muistin loppumiseen. Joissakin muissa ohjelmointikielissä, kuten Javassa ja Pythonissa, käytössä on automaattinen muistinhallinta, joka vapauttaa muistia, kun siihen tallennettuja olioita ei enää tarvita.

Dynaamisesta muistinvarauksesta kerrotaan lisää kohdassa Dynaaminen muistinvaraus.

Swappaus

Swappauksessa käyttöjärjestelmä siirtää muistissa olevaa tietoa väliaikaisesti levyllä olevaan swap-tilaan tai sivutustiedostoon ja palauttaa sen RAM-muistiin tarvittaessa. Näin RAM-muistia vapautuu muuhun käyttöön. Sivutus puolestaan tarkoittaa muistin hallintaa kiinteänkokoisina sivuina, eikä se itsessään edellytä tietojen siirtämistä levylle. Levylle siirtäminen voi auttaa, kun RAM-muisti käy vähiin, mutta runsas swappaus hidastaa ohjelmia, koska levy on RAM-muistia huomattavasti hitaampi.

Swap-tilan käyttö näkyy käyttöjärjestelmän resurssienhallinnassa, ja sen määrää voidaan yleensä säätää erikseen järjestelmän asetuksista.



Toggle Menu