1. Johdatus C-kieleen
Kurssin alkuosassa tutustumme matalan tason ohjelmointiin C-kieltä käyttäen. C-kielessä on paljon samaa kuin muissakin yleisissä ohjelmointikielessä: koodissa käytetään esimerkiksi muuttujia, ehtorakenteita, silmukoita ja funktioita. Aloitamme kielen opiskelun tällaisista tutuista asioista.
Kurssi käsittelee C-kieltä kahdesta näkökulmasta: toisaalta C-standardin näkökulmasta ja toisaalta käytännön ohjelmoinnin näkökulmasta. Kurssin materiaali on laadittu olettaen, että käytössä on 64-bittinen Linux-ympäristö, mutta suuri osa materiaalista soveltuu myös muihin ympäristöihin.
C-kielen yleiskuva
C-kieli on matalan tason ohjelmointikieli, joka soveltuu erityisesti laitteistoa lähellä olevan tehokkaan koodin toteuttamiseen. Kieli sai alkunsa 1970-luvun alussa Unix-käyttöjärjestelmän kehityksen yhteydessä, kun Dennis Ritchie ryhtyi suunnittelemaan seuraajaa aiemmalle B-kielelle.
Monet merkittävät ohjelmistot ja järjestelmät perustuvat C-kieleen, minkä takia kielen osaaminen on hyvää ohjelmoijan yleissivistystä. Esimerkiksi C-kielellä on toteutettu ohjelmointikielten kääntäjiä ja tulkkeja (GCC, CPython), tietokantajärjestelmiä (MySQL, PostgreSQL, SQLite), palvelinohjelmistoja (Apache, Nginx) sekä suurin osa Linux-käyttöjärjestelmän ytimestä.
C-kieli on vaikuttanut paljon myös ohjelmointikielten kehitykseen. C-kielen suora seuraaja on C++, joka laajentaa kieltä esimerkiksi olio-ohjelmointiin liittyvillä ominaisuuksilla. Lisäksi C-kieli on vaikuttanut merkittävästi esimerkiksi C#:n, Javan, JavaScriptin ja PHP:n syntaksiin ja ominaisuuksiin.
Alkuvuosina C-kielen määrittely perustui käytännössä Brian Kernighanin ja Dennis Ritchien kirjaan The C Programming Language, joka tunnetaan myös nimellä K&R kirjoittajien sukunimien alkukirjainten mukaan. Kirjan asemaan vaikutti, että toinen kirjoittaja oli C-kielen alkuperäinen suunnittelija.
Vuonna 1989 julkaistiin ensimmäinen C-standardi (”ANSI C”), joka määritteli tarkasti C-kielen toiminnan ja antoi yhteisen pohjan kielen eri toteutuksille. Myöhemmät standardit ovat laajentaneet C-kieltä, mutta kieli on edelleen lähellä alkuperäistä standardia. Tämä kurssi seuraa pääosin alkuperäistä standardia joillakin lisäyksillä uudemmasta vuoden 1999 standardista (C99).
Miksi käyttää vanhaa C-standardia vuosikymmenten takaa eikä esimerkiksi uusinta C23-standardia?
Kun seuraa vakiintunutta standardia, kielen eri toteutukset tukevat sitä hyvin ja koodi toimii luultavasti luotettavasti eri ympäristöissä. C-kielen yhtenä etuna on, että samaa koodia voi käyttää monenlaisissa ympäristöissä.
Kun uusi standardi julkaistaan, tästä ei seuraa, että kaikki C-toteutusten kehittäjät päivittävät välittömästi toteutuksensa. Päinvastoin saattaa kestää kauankin, ennen kuin kielen uudet ominaisuudet yleistyvät. Esimerkiksi monet C-toteutukset eivät tue vieläkään edes C99-standardia täydellisesti.
C-kielen ominaisuudet
C-kieli on käännetty ohjelmointikieli, mikä tarkoittaa, että ohjelman lähdekoodi käännetään konekieliseksi suoritettavaksi ohjelmaksi. C-kieliset ohjelmat ovat tehokkaita, koska kieli on suunniteltu matalan tason ohjelmoinnin näkökulmasta eikä koodissa ole ylimääräisiä kerroksia ja tarkastuksia, jotka hidastaisivat suoritusta.
C-kieli on perinteinen proseduraalinen ohjelmointikieli, jossa ohjelman suoritus perustuu muuttujiin, ehtoihin, silmukoihin ja funktioihin. Kieli on staattisesti tyypitetty, mikä tarkoittaa, että koodin muuttujilla ja lausekkeilla on kiinteät tyypit, jotka voidaan tarkastaa käännöksen yhteydessä.
Verrattuna moniin muihin ohjelmointikieliin C-kieli on melko pieni kieli. ANSI C -standardi määrittelee, että C-kielessä on seuraavat 32 avainsanaa:
auto double int struct
break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while
Nämä avainsanat muodostavat edelleen C-kielen perustan, vaikka uudemmat standardit ovat tuoneet kieleen lisää avainsanoja.
C-kielen osana on standardikirjasto, jonka funktioiden avulla voi esimerkiksi tulostaa tekstiä, lukea tietoa käyttäjältä, käsitellä merkkijonoja sekä varata muistia. Standardikirjaston sisältö on jaettu eri aiheisiin liittyviin otsikkotiedostoihin. Esimerkiksi otsikkotiedosto string.h liittyy merkkijonojen käsittelyyn.
C-kielen standardikirjaston vaikutus näkyy monella tavalla uudemmissa ohjelmointikielissä. Esimerkiksi Pythonin matemaattiset funktiot ja PHP:n tiedostofunktiot periytyvät C-kielen standardikirjastosta.
JavaScript-ohjelmoija voi miettiä, miksi Date-oliossa kuukausi 0 on tammikuu ja vuosi 0 tarkoittaa vuotta 1900. Tämä johtuu siitä, että JavaScriptin Date-olio perustuu Javan vanhaan Date-toteutukseen, joka puolestaan seuraa C-kielen standardikirjaston tm-rakenteen määritelmää.
Esimerkkiohjelma
Seuraavassa on esimerkkinä C-kielinen ohjelma, joka laskee annetun kokonaisluvun kertoman. Esimerkiksi luvun 5 kertoma on 1 · 2 · 3 · 4 · 5 = 120.
#include <stdio.h>
int main(void) {
printf("Enter a number: ");
int number;
scanf("%d", &number);
long factorial = 1;
for (int x = 1; x <= number; x++) {
factorial *= x;
}
printf("The factorial of %d is %ld\n", number, factorial);
}
Kun koodi on tallennettu tiedostoon test.c, sen voi kääntää suoritettavaksi ohjelmaksi seuraavasti gcc-kääntäjällä Linux-ympäristössä:
$ gcc test.c -o test
Käytämme kurssilla C-koodin kääntämiseen gcc-kääntäjää, joka on osa GCC-kokoelmaa (GNU Compiler Collection). Toinen usein Linuxissa käytettävä C-kääntäjä on clang, joka toimii melko samalla tavalla.
Yllä oleva komento tuottaa binääritiedoston test, jonka voi suorittaa seuraavasti:
$ ./test
Enter a number: 5
The factorial of 5 is 120
Tarkastellaan tarkemmin ohjelman rakennetta:
- Ohjelman alussa rivi
#include <stdio.h>on esikääntäjän direktiivi, joka liittää mukaan standardikirjaston otsikkotiedostonstdio.hsisällön. Tämän ansiosta ohjelmassa voidaan käyttää standardikirjaston funktioitaprintf(tekstin tulostaminen) jascanf(syötteen lukeminen). - C-kielessä ohjelman suoritus alkaa pääfunktiosta, jonka nimi on
main. Määrittelyint main(void)tarkoittaa, että funktio palauttaa kokonaisluvun (int) ja sille ei anneta parametreja (void). - Jokaisen ohjelman komennon perässä on puolipiste (
;). - Ohjelma tulostaa tekstiä funktion
printfavulla. Huomaa, että tulostetun tekstin loppuun ei tule automaattisesti rivinvaihtoa, vaan se tulee lisätä tarvittaessa itse (merkintä\n). - Rivi
int numbermäärittelee muuttujan, jonka nimi onnumberja tyyppi onint(kokonaisluku). Tähän muuttujaan luetaan luku käyttäjältä. - Ohjelma lukee käyttäjältä luvun funktion
scanfavulla. Tässä%dilmaisee, että funktion tulee lukea yksiint-luku. Merkintä&numbertarkoittaa, että funktiolle annetaan parametrina osoitin muuttujaannumber. - Ohjelmassa on for-silmukka, joka laskee luvun kertoman. Kertoma lasketaan muuttujaan
factorial, jonka tyyppinä onlong(suuri kokonaisluku). - Viimeisessä
printf-kutsussa muuttujiennumberjafactorialarvot sijoitetaan vastaaviin kohtiin%d(int-luku) ja%ld(long-luku) merkkijonossa.
C-kielessä kaiken suoritettavan koodin tulee olla funktion sisällä (pääfunktiossa main tai jossain muussa funktiossa). Funktioiden ulkopuolella voi olla vain esikääntäjän direktiivejä ja tiettyjä määrittelyjä, kuten mahdolliset muuttujien ja tietotyyppien määrittelyt.
Koodin muotoilu
C-kielessä tyhjällä tilalla (whitespace) eli esimerkiksi rivinvaihdoilla ja välilyönneillä ei ole useinkaan merkitystä koodissa. Esimerkiksi koodin
printf("Enter a number: ");
int number;
scanf("%d", &number);
voisi kirjoittaa myös seuraavasti:
printf("Enter a number: "); int number; scanf("%d", &number);
Tämä toimii, koska koodissa olevat puolipisteet erottavat komennot eikä rivinvaihdoilla ole merkitystä. Kuitenkin useimmissa tilanteissa on selkeää kirjoittaa jokainen komento omalle rivilleen.
Toisin kuin esimerkiksi Pythonissa, C-kielessä ei ole yhtä vakiintunutta tyyliohjetta koodin muotoiluun, vaan eri projekteissa käytetään eri tapoja. Esimerkiksi GNU-tyyliohje ohjeistaa muotoilemaan silmukan näin:
for (int x = 1; x <= number; x++)
{
factorial *= x;
}
Linux-ytimen tyyliohje käyttää puolestaan seuraavaa muotoilua:
for (int x = 1; x <= number; x++) {
factorial *= x;
}
Kommentointi
C-kielessä on kaksi tapaa kommentoida koodia:
-
Monirivinen kommentti, joka alkaa
/*ja päättyy*/. Tämä on perinteinen ANSI C:n mukainen tapa kommentoida koodia. -
Yksirivinen kommentti, joka alkaa
//ja päättyy rivin loppuun. Tämä tapa on ollut standardissa C99:stä alkaen.
Monirivinen kommentti on kätevä silloin, kun halutaan kommentoida pois tietty osa koodista. Esimerkiksi:
/* long factorial = 1;
for (int x = 1; x <= number; x++) {
factorial *= x;
} */
Yksirivistä kommenttia käytetään yleensä lyhyissä kommenteissa, jotka mahtuvat yhdelle riville:
int secret = 42; // difficult to guess
Muuttujat
C-kielessä muuttuja voidaan määritellä joko koodin päätasolla globaalina muuttujana tai funktion sisällä paikallisena muuttujana. Globaali muuttuja on näkyvissä määrittelynsä jälkeen kaikissa funktioissa, kun taas paikallinen muuttuja on näkyvissä vain funktiossa, jossa se on määritelty.
Esimerkiksi seuraavassa koodissa on globaali muuttuja a, joka on näkyvissä funktioissa test ja main. Kummassakin funktiossa on lisäksi paikallinen muuttuja b, joka on näkyvissä vain funktion sisällä.
int a = 5;
void test(void) {
int b = 2;
printf("%d %d\n", a, b); // 5 2
}
int main(void) {
int b = 1;
test();
printf("%d %d\n", a, b); // 5 1
}
Käytännössä globaaleja muuttujia käytetään harvoin C-kielessä ja useimmat muuttujat ovat paikallisia muuttujia.
Muuttujan määrittely
Muuttujan määrittelyssä ilmoitetaan muuttujan tyyppi, nimi ja mahdollinen alkuarvo. Samalla kertaa voi määritellä useamman saman tyyppisen muuttujan pilkuilla erotettuina. Esimerkiksi:
int length = 100;
int height = 6, width = 7;
long big_value = 12345678987654321L;
double pi = 3.14159;
Tämä koodi määrittelee seuraavat muuttujat:
- Muuttuja
lengthtyyppiäint(kokonaisluku) ja alkuarvona 100 - Muuttujat
heightjawidthtyyppiäintalkuarvoina 6 ja 7 - Muuttuja
big_valuetyyppiälong(suuri kokonaisluku) ja alkuarvona 12345678987654321 (luvun perässäLtarkoittaa, että annettu lukuvakio onlong-tyyppinen) - Muuttuja
pityyppiädouble(liukuluku) ja alkuarvona on 3.14159
C-kielessä koodissa olevissa tunnuksissa (kuten muuttujien ja funktioiden nimet) voi käyttää aakkosia (a–z ja A–Z), numeroita (0–9) sekä alaviivoja (_). Kuitenkin rajoituksena on, että ensimmäinen merkki ei saa olla numero.
Muuttujan voi määritellä myös antamatta sille alkuarvoa:
int number;
Tällöin muuttujan alkuarvo riippuu muuttujan määrittelypaikasta:
-
Jos muuttuja on määritelty globaalisti päätasolla, muuttujan alkuarvoksi tulee automaattisesti 0.
-
Jos muuttuja on määritelty paikallisesti funktiossa, muuttuja on alustamaton ja sen alkuarvona voi olla mikä tahansa lukuarvo, joka on ennestään muistissa muuttujalle varatulla muistialueella.
Esimerkiksi seuraava koodi voi tulostaa minkä tahansa lukuarvon:
int number;
printf("%d\n", number);
Yleinen virhe C-ohjelmoinnissa on jättää muuttuja alustamatta ja olettaa, että sen alkuarvona on esimerkiksi luku 0.
Tämän virheen voi havaita ohjelmaa kääntäessä, kun käännöskomentoon lisää liput -Wall -Wextra, jotka näyttävät kattavasti varoituksia:
$ gcc test.c -o test -Wall -Wextra
Nyt kääntäjä varoittaa seuraavasti, kun äskeinen koodi käännetään:
test.c: In function ‘main’:
test.c:5:5: warning: ‘number’ is used uninitialized [-Wuninitialized]
5 | printf("%d\n", number);
| ^~~~~~~~~~~~~~~~~~~~~~
test.c:4:9: note: ‘number’ was declared here
4 | int number;
| ^~~~~~
Yleensäkin lippujen -Wall -Wextra käyttäminen käännöskomennossa on hyvä idea, koska tämä tuo esille monia yleisiä ohjelmointivirheitä.
Muuttujan tyyppi
C-kielessä jokaisella muuttujalla on kiinteä tyyppi, jota ei ole mahdollista muuttaa muuttujan määrittelyn jälkeen. Esimerkiksi muuttuja
int length = 100;
on pysyvästi int-tyyppinen, eikä siihen ole mahdollista tallentaa myöhemmin esimerkiksi liukulukua.
C-kielessä on useita eri kokoisia tyyppejä lukuarvoja varten. Esimerkiksi tyypit int ja long tarkoittavat eri kokoisia kokonaislukuja. Tyypin koko vaikuttaa siihen, miten suuren tai tarkan luvun tyypillä voi esittää. Toisaalta mitä suurempi tyyppi on, sitä enemmän muuttuja vie tilaa muistissa.
C-standardi ei määrittele tarkasti, kuinka suuria tyypit ovat, vaan tämä riippuu käytetystä ympäristöstä. Esimerkiksi 64-bittisessä Linux-ympäristössä int-tyyppi on 32-bittinen ja long-tyyppi on 64-bittinen. Tämä tarkoittaa käytännössä, että:
-
Tyyppiä
intolevaan muuttujaan voidaan tallentaa kokonaisluku, jonka arvo on välillä –231…231–1 (231 on noin 2 · 109 eli kaksi miljardia). -
Tyyppiä
longolevaan muuttujaan voidaan tallentaa kokonaisluku, jonka arvo on välillä –263…263–1 (263 on noin 9 · 1018).
C-standardi määrittelee, että int-tyypin tulee olla vähintään 16-bittinen ja long-tyypin tulee olla vähintään 32-bittinen. Lisäksi standardi määrittelee, että int-tyyppi ei saa olla suurempi kuin long-tyyppi.
Muuttujan näkyvyysalue
Muuttujan näkyvyysalue (scope) tarkoittaa, missä koodin osassa muuttuja on käytettävissä. C-kielessä paikallisen muuttujan näkyvyysalue on lohko (block) eli aaltosulkujen rajaama alue, jonka sisällä muuttuja on määritelty.
Tarkastellaan esimerkkinä seuraavaa koodia:
int main(void) {
int number;
scanf("%d", &number);
if (number % 2 == 0) {
int half = number / 2;
printf("%d\n", half);
}
}
Tässä muuttujan number näkyvyysalue on koko main-funktio, kun taas muuttujan half näkyvyysalue on if-rakenteen määrittelemä lohko. Tämän takia muuttujaan half ei ole mahdollista viitata if-rakenteen päättymisen jälkeen.
ANSI C:ssä rajoituksena on, että muuttujia voi määritellä vain lohkon alussa. Esimerkiksi seuraava koodi ei ole sallittu, koska muuttuja half määritellään vasta scanf-kutsun jälkeen:
int number;
scanf("%d", &number);
int half = number / 2;
Asian voi korjata määrittelemällä heti alussa myös muuttujan half:
int number, half;
scanf("%d", &number);
half = number / 2;
Kuitenkin C99-standardi muutti asiaa niin, että muuttujan voi määritellä missä tahansa lohkon sisällä. Tätä voi pitää hyvänä muutoksena, koska on luontevaa määritellä muuttuja vasta sitten, kun sitä tarvitaan. Tällä kurssilla sallimme muuttujan määrittelemisen vapaasti lohkon sisällä.
Lohko liittyy usein tiettyyn C-kielen rakenteeseen, kuten ehtolauseeseen tai silmukkaan, mutta lohkon voi määritellä myös itsenäisenä. Tällöin lohkon avulla voi määritellä muuttujalle näkyvyysalueen esimerkiksi seuraavasti:
{
int number = 3;
printf("%d", number);
}
{
int number = 5;
printf("%d", number);
}
Tässä kummankin lohkon sisällä määritellään muuttuja number. Koska muuttujat on määritelty eri lohkoissa, ne ovat erillisiä eivätkä vaikuta toisiinsa.
Vakiomuuttuja
Avainsanan const avulla voidaan määritellä vakiomuuttuja, jonka arvoa ei voi muuttaa määrittelyn jälkeen. Tavallinen käytäntö C-kielessä on kirjoittaa vakiomuuttujan nimi kokonaan suurilla kirjaimilla.
Esimerkiksi seuraava koodi määrittelee kaksi vakiomuuttujaa:
const int MAX_LENGTH = 100;
const double PI = 3.14159;
Toinen tapa määritellä vakio on käyttää esikääntäjän direktiiviä #define:
#define MAX_LENGTH 100
#define PI 3.14159
Erona näissä määrittelyissä on, että const-vakioilla on tyypit ja niitä käsitellään koodissa muuttujien tavoin, kun taas esikääntäjä korvaa #define-vakiot niiden arvoilla ennen varsinaista koodin kääntämistä.
Lause ja lauseke
C-kielen keskeisiä käsitteitä ovat lause (statement) ja lauseke (expression).
-
Lause on suoritettava komento, kuten
x = 2 * y;taiprintf("%d", x);, tai koottu lause, joka esitetään lohkona merkeillä{ja}. -
Lauseke on koodin osa, jolla on jokin arvo. Esimerkiksi
2 * (x + 5)on lauseke, jonka osat2,x,5jax + 5ovat myös lausekkeita.
Lauseen muodostus
C-kielessä lauseita ovat esimerkiksi:
- Yksittäinen komento, jonka perässä on puolipiste
; - Koottu lause, joka muodostuu lauseista ja määrittelyistä merkkien
{ja}sisällä - Ohjausrakenteet, kuten ehtolause
ifja silmukatforjawhile - Tyhjä lause, jonka ilmaisee pelkkä puolipiste
;
Muuttujan määrittely (kuten int number = 42;) ei ole lause, vaikka se näyttää lauseelta ja sitä käytetään monissa tilanteissa lauseen tapaisesti. Määrittely ilmaisee muuttujan olemassaolon, mutta ei varsinaisesti tee muuta.
Esimerkiksi koodi
int number;
scanf("%d", &number);
if (number % 2 == 0) {
int half = number / 2;
printf("%d\n", half);
}
muodostuu seuraavista osista:
- Muuttujan määrittely:
int number; - Lause, joka kutsuu funktiota:
scanf("%d", &number); - Ehtolause, jonka sisällä on koottu lause:
if (number % 2 == 0) { int half = number / 2; printf("%d\n", half); }
Lauseke lauseena
Melko moni asia C-kielessä on pohjimmiltaan lauseke, vaikka se ei näyttäisi siltä päältä päin. Esimerkiksi koodissa
x = 2 * y;
on lauseke x = 2 * y, jonka vaikutuksesta muuttuja x saa arvon 2 * y. Tämän lausekkeen arvona on 2 * y, jota ei tässä tapauksessa käytetä. Kuitenkin lausekkeen arvoa voitaisiin käyttää seuraavasti:
int x = 5, y = 3;
printf("%d", x = 2 * y); // 6
printf("%d", x); // 6
Tässä ensimmäisessä printf-kutsussa oleva lauseke x = 2 * y sekä asettaa muuttujan x arvoksi 2 * y että tuottaa sen arvonaan. Toinen printf-kutsu näyttää, että muuttuja x on todella saanut uuden arvon.
Itse asiassa C-kielessä mistä tahansa lausekkeesta voi tehdä lauseen laittamalla sen perään puolipisteen. Esimerkiksi 2 * y; on sallittu C-kielinen lause, joka ei kuitenkaan vaikuta ohjelman toimintaan, koska lauseke 2 * y ei tee itsenäisenä mitään eikä sen arvoa käytetä.
Laskulausekkeet
C-kielessä laskulausekkeet toteutetaan samalla tavalla kuin yleensäkin ohjelmoinnissa käyttäen laskuoperaattoreita (+, -, *, /, %) ja sulkuja. Esimerkiksi (2 + 5) * 3 on laskulauseke, jonka arvo on 21.
Joissain tilanteissa lausekkeen arvoon vaikuttavat lausekkeessa olevien lukujen tyypit. Esimerkiksi lausekkeen 3 / 2 arvona on 1, koska lasku lasketaan kokonaisluvuilla alaspäin pyöristäen. Lausekkeen 3.0 / 2 arvo puolestaan on 1.5, koska luku 3.0 on liukuluku ja lasku myös lasketaan liukuluvuilla.
Tarvittaessa lausekkeessa voi tehdä tyyppimuunnoksia ()-syntaksilla. Esimerkiksi (double)(a + b) / 2 tarkoittaa, että lausekkeen a + b arvo muutetaan double-tyyppiseksi ja jakolasku lasketaan liukuluvuilla.
Muuttujien käsittely
Sijoitusoperaattori = antaa muuttujalle uuden arvon. Operaattorin vasemmalla puolella on muuttuja ja oikealla puolella on mikä tahansa lauseke. Lausekkeen arvona on muuttujan uusi arvo.
Esimerkiksi a = 5 + x on lauseke, joka antaa muuttujalle a arvon 5 + x. Tämän lausekkeen arvona on 5 + x.
Sijoituksia voi myös ketjuttaa: esimerkiksi lauseke a = b = 5 + x saa aikaan, että muuttujat a ja b saavat arvon 5 + x. Lauseke a = b = 5 + x tarkoittaa samaa kuin lauseke a = (b = 5 + x), ja lausekkeen arvona on 5 + x. Tämän avulla voidaan esimerkiksi kätevästi nollata useampia muuttujia:
a = b = c = 0;
Operaattorit +=, -=, *=, /= ja %= tarjoavat lyhyemmän tavan muuttaa muuttujan arvoa laskutoimituksella. Esimerkiksi lauseke a += b tarkoittaa samaa kuin a = a + b ja lauseke x /= 2 tarkoittaa samaa kuin x = x / 2.
Lauseke x *= 2 + a ei tarkoita lauseketta x = x * 2 + a vaan lauseketta x = x * (2 + a). Tulkinta on siis, että operaattorin vasemmalla puolella oleva muuttuja kerrotaan oikealla puolella olevalla lausekkeella eikä että lausekkeen alkuosa x *= muutetaan suoraan muotoon x = x *.
Operaattorit ++ ja -- kasvattavat ja vähentävät muuttujan arvoa yhdellä. Jos operaattori on muuttujan edellä, lausekkeen arvona on muuttujan arvo muutoksen jälkeen. Jos taas operaattori on muuttujan perässä, lausekkeen arvona on muuttujan arvo ennen muutosta. Esimerkiksi:
int a = 3;
printf("%d\n", ++a); // 4
printf("%d\n", a); // 4
int a = 3;
printf("%d\n", a++); // 3
printf("%d\n", a); // 4
Vertailut ja logiikka
C-kielen vertailu- ja loogiset operaattorit toimivat samalla periaatteella kuin yleensäkin ohjelmoinnissa:
- Vertailuoperaattorit:
==,!=,<,<=,>,>= - Loogiset operaattorit:
&&(and),||(or),!(not)
Esimerkiksi lausekkeen (a >= 1 && a <= 5) || b != 1 arvo on tosi, jos muuttujan a arvo on 1:n ja 5:n välillä tai jos muuttujan b arvo ei ole 1.
C-kielessä lukuarvo 0 tulkitaan epätodeksi ja mikä tahansa muu lukuarvo tulkitaan todeksi. Esimerkiksi lukuarvot 1, 42 ja –5 ovat tosia. Vertailu- ja loogiset operaattorit tuottavat tuloksen 0 (epätosi) tai 1 (tosi). Esimerkiksi:
int a = 3, b = 5;
printf("%d\n", a < b); // 1
printf("%d\n", a > b); // 0
Perinteisesti C-kielessä ei ole ollut erillistä boolean-tyyppiä. C99-standardi määrittelee kuitenkin otsikkotiedostossa stdbool.h tyypin bool ja sen mahdolliset arvot true ja false. Uusimmassa C23-standardissa vastaava tyyppi bool on suoraan käytettävissä.
Muita lausekkeita
Ehtolauseke a ? b : c saa arvon b, jos ehto a on tosi, ja muuten arvon c. Esimerkiksi ehtolauseketta voi käyttää seuraavasti:
int smaller = x < y ? x : y;
Tässä muuttuja smaller saa arvon x, jos ehto x < y pätee, ja muuten arvon y. Toisin sanoen muuttujan smaller arvoksi tulee pienempi arvoista x ja y.
Operaattori , yhdistää useamman lausekkeen yhdeksi lausekkeeksi. Esimerkiksi lauseke a = 2, b = 5 asettaa muuttujan a arvoksi 2 ja muuttujan b arvoksi 5. Näemme myöhemmin for-silmukan yhteydessä, mitä hyötyä tästä voi olla.
Lausekkeen arvon laskeminen
Lausekkeen 3 + 2 * 5 arvo on 13, koska operaattori * sitoo vahvemmin kuin operaattori +. Lauseke 3 + 2 * 5 tarkoittaa siis samaa kuin lauseke 3 + (2 * 5) ja eroaa lausekkeesta (3 + 2) * 5.
C-kielessä operaattoreiden sitovuus on määritelty tarkasti. Sitovuuteen liittyy kaksi käsitettä:
-
Presedenssi: Operaattorin presedenssi tarkoittaa sitovuustasoa. Mitä korkeampi presedenssi on, sitä vahvemmin operaattori sitoo. Esimerkiksi operaattorilla
*on operaattoria+korkeampi presedenssi. -
Assosiatiivisuus: Jos kahdella operaattorilla on sama presedenssi, assosiatiivisuus määrittää, onko ryhmittely vasemmalta oikealle vai oikealta vasemmalle. Esimerkiksi operaattorin
-assosiatiivisuus on vasemmalta oikealle, minkä takia lauseke1 - 2 - 3tulkitaan(1 - 2) - 3 = -4eikä1 - (2 - 3) = 2.
Vaikka sitovuus on määritelty tarkasti, useissa tilanteissa ei ole määritelty, missä järjestyksessä lausekkeen osat lasketaan. Esimerkiksi lausekkeessa f(x) + g(x) ei ole määritelty, lasketaanko ensin f(x) vai g(x).
Operaattoreissa || ja && kuitenkin laskujärjestys on vasemmalta oikealle ja lisäksi laskenta katkaistaan, jos ||-ketjun osana on tosi arvo tai &&-ketjun osana on epätosi arvo. Tämä on kätevää seuraavan tyylisessä koodissa:
if (i >= 0 && i < n && numbers[i] != 0)
Tässä vertailu numbers[i] != 0 suoritetaan vain siinä tapauksessa, että edeltävät vertailut i >= 0 ja i < n ovat tosia eli i on kelvollinen taulukon indeksi.
Ehtoketjun aikainen katkaisu (short-circuit evaluation) on nykyään käytössä C-kielen lisäksi yleisesti ohjelmoinnissa, mutta vanhaan aikaan tilanne oli toinen. Esimerkiksi Pascal-kielessä ehtoketjun kaikki osat lasketaan aina, vaikka niillä ei olisi vaikutusta lausekkeen arvoon.
Joissakin tilanteissa lausekkeen arvo on määrittelemätön, koska lausekkeen osien laskentajärjestystä ei ole määritelty mutta se vaikuttaa lausekkeen arvoon. Esimerkiksi lausekkeen x * (x++) arvo on määrittelemätön, koska lausekkeen arvo voi olla joko x * x tai (x + 1) * x riippuen laskentajärjestyksestä.
Entä mitä tarkoittaa lauseke a---b? Se voisi tarkoittaa (a--) - b tai a - (--b), joilla on eri merkitys. C-kielessä tulkinta on (a--) - b , mikä johtuu maximal munch -periaatteesta koodin käsittelyssä: jos on useita tapoja tulkita koodissa seuraavana oleva operaattori, valitaan pisimmälle menevä vaihtoehto. Lausekkeessa a---b voitaisiin a:n jälkeen valita joko - tai -- ja valinnaksi tulee --.
Lauseke a-----b ei ole kelvollinen C-kielen lauseke, vaikka lausekkeen voisi tulkita (a--) - (--b). Kuitenkin maximal munch -periaatteen mukaisesti lauseke jakaantuu osiin [a, --, --, -, b], jolloin keskellä oleva operaattori -- aiheuttaa syntaksivirheen.
Ohjausrakenteet
C-kielen ehtorakenteet (if ja switch), silmukat (while, do ja for) sekä hyppykomennot (break, continue, goto, return) toimivat suurimmaksi osaksi samalla logiikalla kuin yleensäkin ohjelmoinnissa.
Esimerkiksi seuraava koodi tulostaa Collatzin lukujonon annetusta luvusta lähtien. Jos luku on parillinen, se jaetaan kahdella. Jos taas luku on pariton, se kerrotaan kolmella ja siihen lisätään yksi. Lukujono päättyy, kun luku on yksi.
int n;
scanf("%d", &n);
while (1) {
printf("%d ", n);
if (n == 1) break;
if (n % 2 == 0) {
n = n / 2;
} else {
n = 3 * n + 1;
}
}
printf("\n");
Koodin suoritus voi näyttää esimerkiksi seuraavalta:
7
7 22 11 34 17 52 26 13 40 20 10 5 16 8 4 2 1
Ehtorakenteen ja silmukan sisällä suoritettava koodi voi olla yksittäinen komento tai lohkon sisällä oleva koodi. Jos koodi on yksittäinen komento, sen voi kirjoittaa myös lohkon sisään. Esimerkiksi seuraavat rakenteet tarkoittavat samaa:
if (n == 1) break;
if (n == 1) {
break;
}
Lohkon käyttäminen voi usein olla selkeyden kannalta hyvä idea silloinkin, kun yksittäisen komennon voisi kirjoittaa ilman lohkoa. Tällä kurssilla käytämme yleensä aina lohkoja, poikkeuksena jotkin lyhyet if-rakenteet ilman else-osaa.
Seuraava koodi ei toimi tarkoitetulla tavalla:
if (n % 2 == 0)
printf("%d\n", n);
n = n / 2;
C-kielessä sisennys ei vaikuta koodin tulkintaan. Koska koodissa ei ole lohkoa, koodin tulkinta on seuraava:
if (n % 2 == 0) {
printf("%d\n", n);
}
n = n / 2;
Seuraava koodi ei myöskään toimi tarkoitetulla tavalla:
if (n % 2 == 0);
n = n / 2;
Ensimmäisen rivin lopussa oleva puolipiste tulkitaan tyhjänä lauseena, minkä takia koodin tulkinta on seuraava:
if (n % 2 == 0) {
}
n = n / 2;
If-rakenne
If-rakenteella on kaksi mahdollista muotoa. Lyhyessä muodossa on vain ehtolauseke ja suoritettava lause, kun taas pitkässä muodossa on myös else-osa ja toinen suoritettava lause. Esimerkiksi:
if (count < total) {
count++;
}
if (count < total) {
count++;
} else {
rounds++;
count = 1;
}
Koska else-osan lauseena voi olla myös uusi ehtolause, voimme myös rakentaa monimutkaisemman ehtorakenteen seuraavaan tapaan:
if (result <= 20) {
grade = 1;
} else if (result <= 30) {
grade = 2;
} else {
grade = 3;
}
Ehtolauseke voi olla mikä tahansa lukuarvoinen lauseke, joka voidaan tulkita totuusarvona. Esimerkiksi seuraava koodi tarkastaa, onko luku pariton:
if (number % 2) {
odd_count++;
}
Tässä lauseke number % 2 tulkitaan todeksi eli lauseke saa nollasta eroavan arvon, kun number on pariton.
Klassinen bugi syntyy, kun koodissa on tarkoitus olla vertailuoperaattori == mutta sen sijasta onkin sijoitusoperaattori =:
if (number = 0) {
printf("All your base are belong to us.\n");
}
Tässä lauseke number = 0 asettaa muuttujan number arvoksi nollan. Lausekkeen arvo on nolla eli epätosi, joten muuttujan alkuperäisestä sisällöstä riippumatta koodi nollaa muuttujan eikä tulosta mitään.
Switch-rakenne
Switch-rakenne suorittaa halutun koodin riippuen lausekkeen arvosta. Tavallinen tapa käyttää rakennetta on seuraava:
switch (status) {
case 1:
printf("We get signal.\n");
break;
case 2:
printf("Main screen turn on.\n");
break;
case 5:
printf("You know what you doing.\n");
break;
default:
printf("What happen?\n");
break;
}
Tässä suoritettava koodi valitaan muuttujan status arvon perusteella. Esimerkiksi jos muuttujan arvo on 1, suoritetaan haarassa case 1 oleva koodi. Jos muuttujan arvo ei vastaa mitään case-haaraa, suoritetaan default-haarassa oleva koodi.
Usein switch-rakenne muodostetaan yllä olevalla tavalla niin, että jokaisen haaran lopussa on break-komento. Tällöin jokaisessa haarassa on oma koodinsa, joka suoritetaan tietyllä muuttujan arvolla. Sen sijaan jos haaran lopussa ei ole break-komentoa, koodin suoritus jatkuu eteenpäin seuraavaan haaraan.
Yleisemmin switch-rakenteen toiminta voidaan kuvata niin, että koodin suoritus alkaa alussa olevan lausekkeen arvoon täsmäävästä haarasta (tai mahdollisesta default-haarasta) ja koodin suoritus jatkuu eteenpäin, kunnes vastaan tulee break-komento tai switch-rakenne päättyy.
Esimerkiksi seuraavassa switch-rakenteessa ei ole break-komentoja:
switch (status) {
case 1:
printf("We get signal.\n");
case 2:
printf("Main screen turn on.\n");
case 5:
printf("You know what you doing.\n");
default:
printf("What happen?\n");
}
Tämä koodi tulostaa kaikki rivit alkaen haarasta, joka täsmää muuttujan status arvoon. Esimerkiksi jos status on 2, koodin tulostus on seuraava:
Main screen turn on.
You know what you doing.
What happen?
Tavallinen bugi switch-rakenteen yhteydessä on, että haaran lopusta on unohtunut break-komento ja koodin suoritus jatkuu virheellisesti eteenpäin, jolloin koodi voi toimia hyvin oudolla tavalla.
C#-kielessä switch-rakenteen syntaksi on muuten kuten C-kielessä, mutta tällainen bugi on estetty niin, että jokaisen haaran lopussa tulee olla break tai muu komento, joka poistuu rakenteesta.
Switch-rakenteen rajoituksena on, että jokaisessa case-haarassa voi olla vain yksittäinen kokonaislukuarvo. Toisaalta peräkkäin voi olla useita case-haaroja, koska koodin suoritus jatkuu eteenpäin. Esimerkiksi seuraava koodi määrittää kuukauden päivien määrän kuukauden numeron perusteella:
switch (month) {
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
days = 31;
break;
case 2:
days = 28;
break;
case 4: case 6: case 9: case 11:
days = 30;
break;
}
C-kielen switch-rakenne on melko alkeellinen verrattuna esimerkiksi Pascal-kieleen, jossa vastaavaa rakennetta voidaan käyttää näin:
case month of
1, 3, 5, 7, 8, 10, 12:
days := 31;
2:
days := 28;
4, 6, 9, 11:
days := 30;
end;
Pascalin syntaksi on mukavampi, koska haaraan liittyvät arvot voi antaa listana eikä haaran loppuun tarvitse lisätä break-komentoa.
Tässä on taustalla, että C-kielen rakenne on lähempänä konekielen ajattelutapaa. Siirtyminen case-haaraan voidaan toteuttaa konekielessä hyppykomennolla, joka siirtää koodin suorituksen haluttuun kohtaan.
While- ja do-silmukat
While-silmukka suorittaa koodia niin kauan kuin annettu ehto pätee. Esimerkiksi seuraava koodi puolittaa lukua niin kauan kuin luku on positiivinen:
int number;
scanf("%d", &number);
while (number > 0) {
printf("%d ", number);
number /= 2;
}
printf("\n");
Koodin suoritus voi näyttää seuraavalta:
42
42 21 10 5 2 1
Do-silmukka toimii muuten samalla tavalla, mutta ehto tarkastetaan vasta silmukan kierroksen lopussa ja silmukassa on aina vähintään yksi kierros. Seuraava koodi kysyy käyttäjältä lukuja, kunnes käyttäjä antaa positiivisen luvun:
int number;
do {
scanf("%d", &number);
} while (number <= 0);
Vastaavan koodin voisi toteuttaa seuraavasti while-silmukalla:
int number;
while (1) {
scanf("%d", &number);
if (number > 0) break;
}
Tässä silmukan ehtona on lauseke 1, jonka arvo on aina tosi. Tämä on yleinen tapa toteuttaa ikuinen silmukka, jonka toistamista ei ole rajoitettu.
For-silmukka
C-kielen monipuolisin silmukka on for-silmukka, jota käytetään tyypillisesti lukuvälin läpikäyntiin. Esimerkiksi seuraavasti:
for (int i = 1; i <= 10; i++) {
printf("%d ", i);
}
Koodi tuottaa seuraavan tuloksen:
1 2 3 4 5 6 7 8 9 10
Silmukan toiminnan kuvaus muodostuu osista (start; condition; update):
- Silmukan alussa suoritetaan
start, joka tavallisesti määrittelee jonkin muuttujan ja antaa sille alkuarvon. - Silmukan ehtona on lauseke
condition, joka tarkastetaan ennen silmukan kierroksen suorittamista. - Silmukan kierroksen jälkeen suoritetaan lauseke
update, joka tavallisesti muuttaa jonkin muuttujan arvoa.
ANSI C -standardissa start on aina lauseke, minkä takia muuttuja tulee määritellä erikseen ennen for-silmukkaa:
int i;
for (i = 1; i <= 10; i++) {
C99-standardissa start voi kuitenkin olla myös muuttujan määrittely, mikä helpottaa for-silmukan toteuttamista:
for (int i = 1; i <= 10; i++) {
Kun for-silmukan alussa on muuttujan määrittely, tämän muuttujan näkyvyysalue on kyseisen silmukan alue.
Vaikka for-silmukkaa käytetään yleensä lukuvälin läpikäyntiin, sen avulla voi tehdä paljon muutakin. Seuraavassa on hieman erikoisempi for-silmukka, jossa käytetään kahta muuttujaa:
for (int a = 1, b = 10; a < b; a++, b--) {
printf("%d %d ", a, b);
}
printf("\n");
Koodi tuottaa seuraavan tuloksen:
1 10 2 9 3 8 4 7 5 6
On sallittua, että yksi tai useampi osa for-silmukan toiminnan kuvauksessa (start; condition; update) puuttuu:
- Jos
startpuuttuu, silmukan alussa ei tehdä mitään. - Jos
conditionpuuttuu, silmukan toistamista ei ole rajoitettu. - Jos
updatepuuttuu, silmukan kierroksen jälkeen ei tehdä mitään.
Ääritapauksessa kaikki osat puuttuvat, jolloin tuloksena on ikuinen silmukka for (;;), joka vastaa silmukkaa while (1).
Goto-komento
Goto-komento hyppää koodin toiseen kohtaan, joka on merkitty nimiöllä. Esimerkiksi seuraavassa koodissa on hyppy nimiöön skip:
if (x == 0) goto skip;
printf("We get signal.\n");
printf("Main screen turn on.\n");
skip:
printf("What happen?\n");
Jos ehto x == 0 pätee, koodi hyppää suoraan viimeiseen printf-komentoon. Jos ehto ei päde, koodi suorittaa kaikki printf-komennot.
Goto-komento on ehkä ohjelmoinnin historian kiistellyin komento. Komennon käyttäminen voi johtaa sekavaan koodiin, minkä takia joidenkin mielestä komentoa ei tulisi käyttää missään tilanteessa.
Kuuluisa aiheeseen liittyvä kirjoitus on Edsger Dijkstran Go To Statement Considered Harmful, joka julkaistiin vuonna 1968 tietojenkäsittelytieteen alan lehdessä Communications of the ACM. Dijkstran mukaan ohjelmoijan työn laatu on käänteisesti verrannollinen goto-komentojen määrään koodissa.
Käytännössä goto-komennosta voi olla hyötyä esimerkiksi silloin, kun on tarvetta päästä pois monikerroksisen silmukan sisältä. Tässä tilanteessa ei voi käyttää break-komentoa, koska se lopettaa vain sisimmän silmukan suorituksen.
Esimerkiksi seuraavassa silmukkarakenteessa goto-komento keskeyttää haun, kun halutun ehdon täyttävät muuttujat ovat löytyneet:
int found = 0;
for (int y = 0; y < n; y++) {
for (int x = 0; x < n; x++) {
if (grid[y][x] == 0) {
found = 1;
goto ready;
}
}
}
ready:
if (found) printf("empty cell found\n");
Java-kielessä ei ole goto-komentoa, mutta siinä on kuitenkin yllä olevan kaltainen mahdollisuus keskeyttää monikerroksinen silmukka:
boolean found = false;
search:
for (int y = 0; y < n; y++) {
for (int x = 0; x < n; x++) {
if (grid[y][x] == 0) {
found = true;
break search;
}
}
}
if (found) System.out.println("empty cell found");
Tässä nimiö laitetaan silmukan alkuun ja komento break keskeyttää nimiön osoittaman silmukan suorituksen.
Funktiot
C-kielinen ohjelma muodostuu funktioista, jotka kutsuvat toisiaan. Seuraava ohjelma esittelee funktioiden käyttämistä:
#include <stdio.h>
void print_range(int a, int b) {
for (int i = a; i <= b; i++) {
printf("%d ", i);
}
printf("\n");
}
long factorial(int n) {
long result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
int main(void) {
print_range(1, 5); // 1 2 3 4 5
print_range(3, 4); // 3 4
print_range(2, 8); // 2 3 4 5 6 7 8
printf("%ld\n", factorial(5)); // 120
printf("%ld\n", factorial(3)); // 6
printf("%ld\n", factorial(20)); // 2432902008176640000
}
Tämän ohjelman funktiot ovat:
-
Funktio
print_rangetulostaa annetun lukuvälin luvut. Funktiolla ei ole paluuarvoa (void) ja funktiolle annetaan kaksiint-parametria (ajab), jotka määrittelevät tulostettavan lukuvälin rajat. -
Funktio
factoriallaskee annetun luvun kertoman. Funktion paluuarvon tyyppi onlong, ja funktiolle annetaan yksiint-parametri (n). -
Funktio
mainon ohjelman pääfunktio, joka kutsuu muita funktioita. Ohjelman suoritus alkaa pääfunktion alusta ja päättyy pääfunktion loppuun.
Nykyään ei tunnu mitenkään erikoiselta, että C-kielessä kaikkia aliohjelmia kutsutaan funktioiksi, myös vaikka aliohjelma ei palauttaisi mitään. Näinhän asia on muissakin kielissä, kuten JavaScriptissa, Pythonissa ja Rustissa.
Vanhaan aikaan tilanne oli kuitenkin toinen ja C edusti uutta ajattelua. Esimerkiksi Fortranissa ja Pascalissa sanaa funktio käytetään vain, kun aliohjelmalla on paluuarvo. Jos paluuarvoa ei ole, Fortranissa käytetään sanaa alirutiini ja Pascalissa sanaa proseduuri.
Määrittely ja esittely
C-kielessä funktiota ei voi kutsua ennen kuin koodissa on tuotu esille, että kyseinen funktio on olemassa. Tämä tarkoittaa, että ennen funktion kutsumista koodissa tulee olla funktion määrittely (definition), joka sisältää funktion koko toteutuksen, tai ainakin funktion esittely (declaration), joka kertoo funktion nimen ja kutsutavan.
Funktion esittely on usein sama kuin funktion määrittelyn ensimmäinen rivi, joka kertoo funktion nimen sekä paluuarvon ja parametrien tyypit. Esimerkiksi äskeisen factorial-funktion voisi esitellä seuraavasti:
long factorial(int n);
Koodissa oleva funktion määrittely esittelee samalla funktion.
Esimerkiksi seuraavassa koodissa main-funktio kutsuu factorial-funktiota, joka on määritelty myöhemmin. Koodin alussa on factorial-funktion esittely, jonka ansiosta main-funktio tietää riittävästi factorial-funktiosta kutsuakseen sitä.
long factorial(int n);
int main(void) {
printf("%ld\n", factorial(5));
}
long factorial(int n) {
long result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
Vaatimus funktion esittelystä ennen funktion kutsumista johtuu C-kielen historiasta. C-kieli on alun perin suunniteltu niin, että kooditiedoston pystyy kääntämään käymällä tiedoston rivit vain kerran läpi alusta loppuun. Tämän takia koodissa ei voi viitata funktioon, josta tiedetään vasta tulevaisuudessa.
Yhden läpikäynnin käännöksestä on hyötyä, jos kääntäjän käytettävissä on hyvin rajallinen määrä muistia, kuten oli 1970-luvulla. Modernit kääntäjät käyvät koodin läpi useita kertoja eikä niillä ole pulaa muistista, mutta funktiot tulee edelleen esitellä ennen niiden kutsumista.
Tiedonvälitys funktiossa
C-kielessä funktion parametrit välitetään arvoparametreina eli funktio saa kopion kunkin parametrin sisällöstä. Tämä tarkoittaa, että jos funktion kutsussa parametrina on muuttuja, funktio ei pysty muuttamaan muuttujan arvoa.
Esimerkiksi seuraavassa koodissa funktio muuttaa parametrin x arvoa, mutta tämä muutos ei heijastu kutsukohdan muuttujaan a.
void test(int x) {
x = 2;
printf("%d\n", x); // 2
}
int main(void) {
int a = 1;
test(a);
printf("%d\n", a); // 1
}
Kuitenkin funktion parametrina voi olla myös osoitin, jonka kautta funktio pystyy muuttamaan muuttujan arvoa. Osoittimen arvona on muistiosoite, johon muuttujan sisältö on tallennettu.
Seuraavassa koodissa parametri x on osoitin, jonka kautta funktio pystyy muuttamaan muuttujan a arvoa. Merkintä *x tarkoittaa, että x on osoitin, jonka kautta käsitellään muistin sisältöä. Merkintä &a tarkoittaa, että osoittimen arvoksi tulee muuttujan a muistiosoite.
void test(int *x) {
*x = 2;
printf("%d\n", *x); // 2
}
int main(void) {
int a = 1;
test(&a);
printf("%d\n", a); // 2
}
Esimerkiksi scanf-funktion parametrit ovat osoittimia, koska funktion täytyy pystyä muuttamaan muuttujien arvoja:
int x, y;
scanf("%d %d", &x, &y);
Tutustumme tarkemmin osoittimien käyttämiseen myöhemmin kurssilla.
Pääfunktio
Pääfunktion main paluuarvo tarkoittaa, minkä arvon ohjelma palauttaa käyttöjärjestelmälle suorituksen jälkeen. Esimerkiksi seuraavan ohjelman paluuarvo on 42:
int main(void) {
return 42;
}
Linuxissa paluuarvon pystyy tarkastamaan seuraavasti ohjelman suorituksen jälkeen:
$ ./test
$ echo $?
42
Yleinen käytäntö on, että paluuarvo 0 tarkoittaa ohjelman onnistunutta suoritusta ja muut paluuarvot tarkoittavat virhetilanteita. Jos main-funktiossa ei käytetä return-komentoa, funktion paluuarvoksi tulee automaattisesti 0.
Automaattinen paluuarvo 0 on C99-standardin tuoma erikoistapaus, joka liittyy vain main-funktioon. Alkuperäisessä ANSI C -standardissa main-funktion paluuarvo on määrittelemätön, jos return-komentoa ei käytetä.
Pääfunktion voi toteuttaa myös laajemmassa muodossa niin, että se pystyy käsittelemään komentorivillä annetut parametrit:
#include <stdio.h>
int main(int argc, char **argv) {
printf("%d\n", argc);
for (int i = 0; i < argc; i++) {
printf("%d: %s\n", i, argv[i]);
}
}
Tässä parametri argc kertoo parametrien määrän ja argv on merkkijonotaulukko, joka sisältää parametrit. Ohjelmaa voi käyttää esimerkiksi näin:
$ ./test apina banaani cembalo
4
0: ./test
1: apina
2: banaani
3: cembalo
Ensimmäinen parametri on ohjelman käynnistyskomento (tässä ./test) ja muut parametrit ovat ohjelman varsinaiset parametrit.