C-kielen alkeisopas
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
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.

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.

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/

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

Pinomuisti (Stack)

Pinomuisti on alue RAM-muistissa, jota käytetään funktioiden kutsumiseen ja paikallisten muuttujien säilyttämiseen. Kun funktio kutsutaan, sen tiedot "pinotaan" muistiin, ja kun funktio päättyy, nämä tiedot poistetaan automaattisesti.

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 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 toinen RAM-muistin osa, jota käytetään dynaamiseen muistinvaraukseen ohjelman ajon aikana. Tiedot kekomuistissa säilyvät niin kauan kuin ohjelmoija itse vapauttaa ne. Tämä on hyödyllistä esimerkiksi silloin, kun ei tiedetä etukäteen kuinka paljon muistia tarvitaan.

#include <stdlib.h>

int* dyn = (int*)malloc(100 * sizeof(int)); // varataan 100 intin taulukko

free(dyn); // vapautetaan varattu muisti

Kekomuistin käyttö ei ole rajattu samalla tavalla kuin pinomuistin, ja sitä voidaan varata tarpeen mukaan. Kuitenkin, esimerkiksi C-kielessä, ohjelmoijan vastuulla on itse vapauttaa varattu muisti. Jos tämä unohtuu, seurauksena voi olla muistivuoto (memory leak), joka heikentää suorituskykyä ja voi lopulta johtaa muistin loppumiseen. Joissakin muissa ohjelmointikielissä, kuten Java ja Python, käytössä on roskienkeruu (garbage collection), joka huolehtii dynaamisen muistin vapauttamisesta automaattisesti.

Dynaamisesta muistinvarauksesta kerrotaan lisää kohdassa Dynaaminen muistinvaraus.

Swappaus

Swappaus (tai sivutus) on muistinhallintatekniikka, jossa käyttöjärjestelmä siirtää osan keskusmuistin sisällöstä väliaikaisesti levymuistille, kun fyysinen RAM-muisti ei riitä. Tämän avulla voidaan suorittaa suurempia ohjelmia tai useampia prosesseja samanaikaisesti, mutta suorituskyky voi heikentyä, koska levymuisti on RAM-muistia huomattavasti hitaampaa.

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.

C-kielinen ohjelmointi

C-ohjelmointikieli kehitettiin 1970-luvulla, ja se on edelleen yksi laajimmin käytetyistä ohjelmointikielistä. Myöhemmin C-kielestä kehitettiin laajennettu ohjelmointikieli C++, johon lisättiin muun muassa olio-ohjelmoinnin ominaisuuksia.

C-kielisiä ohjelmia voidaan kehittää eri käyttöjärjestelmissä, kuten Windowsissa, Linuxissa ja macOS:ssä. Näissä ympäristöissä käytetään yleensä kyseiselle käyttöjärjestelmälle sopivia C-kääntäjiä, jotka muuntavat ohjelmakoodin tietokoneen suoritettavaksi konekieleksi.

Lähdekoodista sovellukseksi

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-kieliselle lähdekooditiedostolle käytetään yleensä tarkenninta .c (.cpp on yleensä C++-koodia sisältävä tiedosto). Header-tiedostojen tarkennin on .h ja niissä on kuvattu ohjelmassa käytettävien funktioiden rakenne. Funktioiden varsinainen koodi on kuitenkin .c-tiedostoissa.

Windowsissa suoritettavien sovellusten tarkennin on .exe. Jos siis kirjoitetaan suoritettava sovellus nimeltään myApp, on C-kielisen kääntäjän tuottama suoritettava tiedosto nimeltään myApp.exe. Linuxissa ja macOS:ssä suoritettava tiedosto on yleensä ilman päätettä eli se olisi myApp.

Yksinkertaisesti voidaan kuvata C-sovelluksen luontia näin:

  1. Ns. esikääntäjä lukee .c-tiedostoa, etsii sen alussa olevia #include-rivejä ja korvaa ne kyseisten otsikkotiedostojen (.h) sisällöllä.
  2. C-kääntäjä ottaa esikäsitellyn lähdekoodin ja kääntää sen konekielelle, luoden objektitiedoston (.o tai .obj).
  3. Linkkeri (linker) yhdistää mahdolliset useat objektitiedostot sekä kirjastot, ja tuottaa lopullisen suoritettavan tiedoston, esim. .exe Windowsissa.
Header-tiedostot

Header-tiedostot (pääte .h) ovat olennainen osa C-ohjelmointia. Ne sisältävät tyypillisesti funktioiden esijulistuksia, makromäärittelyjä, tietorakenteiden määrittelyjä sekä muita vakioita, joita voidaan käyttää useassa lähdekooditiedostossa.

Header-tiedoston käyttö

Header-tiedosto sisällytetään lähdekoodiin #include-komennolla:

#include "omaheader.h"
#include <stdio.h>

Kulmasulkeita (<>) käytetään järjestelmän vakioheaderien kanssa, kun taas lainausmerkkejä ("") käytetään omien tiedostojen kohdalla.

Include-vahdit

Jotta header-tiedoston sisältö ei tulisi sisällytetyksi useita kertoja, käytetään ns. include-vahtia:

#ifndef OMAHEADER_H
#define OMAHEADER_H

// Sisältöä

#endif

Tällainen rakenne estää kaksoismäärittelyt ja mahdolliset käännösvirheet.

Mitä header-tiedosto EI sisällä?

Header-tiedostossa ei tule toteuttaa funktioita (paitsi mahdollisesti inline-funktioita). Funktioiden toteutukset kuuluvat .c-tiedostoihin.

Yhteenveto

  • Headerit helpottavat koodin uudelleenkäyttöä ja organisointia
  • Sisältävät tyypillisesti esijulistuksia, makroja ja tyyppimäärittelyjä
  • Include-vahdit estävät moninkertaisen sisällyttämisen
Kirjastot (library)

Kirjasto on valmiiksi tehtyä ohjelmakoodia, jota oma ohjelma voi käyttää. Ohjelmoija voi käyttää valmiita, toisten tekemiä kirjastoja, mutta hän voi myös tehdä kirjastoja itse. Kirjastossa voi olla esimerkiksi valmiita funktioita, joita ei tarvitse kirjoittaa aina uudelleen. C-ohjelmissa käytetään kirjastoja esimerkiksi tulostamiseen, tiedostojen käsittelyyn ja matemaattisiin laskuihin.

Kirjasto ei yleensä ole itsenäinen ohjelma, vaan se liitetään osaksi omaa ohjelmaa käännöksen tai ohjelman käynnistyksen yhteydessä. Tässä vaiheessa riittää ymmärtää, että kirjasto tuo ohjelmaan valmista toiminnallisuutta.

  • Staattinen kirjasto liitetään yleensä osaksi valmista ohjelmatiedostoa käännöksen yhteydessä. Sen tiedostopäätteitä ovat esimerkiksi .a ja .lib.
  • Dynaaminen kirjasto pidetään erillisenä tiedostona, jota ohjelma käyttää ajon aikana. Sen tiedostopäätteitä ovat esimerkiksi .dll Windowsissa ja .so Linuxissa.
Tiedostopäätteitä
Tiedostopääte Tiedostotyyppi
.a static library file
.c C language file
.cpp C++ language file
.dll Dynamic Linked Library file (Windows)
.h C Header file
.lib Static library or import library file (Windows)
.o Object file (Unix/Linux)
.obj Object file (Windows)
.so Dynamic shared library file (Linux/Unix)



Toggle Menu