C

teejuht
 
 0.
sissejuhatus
1.
muutuja
2.
loogiline tingimus mille väärtus on C-s integer 
3.
for tsükkel
4.
if -if else - else tingimus
5.
massiiv
6.
while
7.
char muutuja tüüp ja printf - scanf funktsioonide formaat
8.
input/output ehk I/O - klaviatuurilt, failist/ekraanile, faili, käsurealt
9.
switch
10.
pointer
11.
string
12.
funktsioon
13.
struktuur
14.
Pointer struktuurielemendile
15.
GNU C Library'te kasutamine
16.
Shared Library'te valmistamine ja kasutamine
                                17. Binary failidega õiendamine

1. Sissejuhatus

Alljärgnevad muu seas rida praktilisi näiteid kuidas rakendada C keelt sisendi ja väljundiga manipuleerimisel. Samuti kuis tegelda stringidega ja miks poinerid võivad kasulikud olla. Autor tunnistab enda mittepädevust kuid, he is doing his best ...
Eeldatud on, et lugejal on PC ja sellel mingi *nix nt Linux Slackware 3.5 ja C keele vahendid; või öelduga ekvivalentne olukord.

Arvuti protsessor on selline aparaat, mis ootab käitumisjuhendit ja selleks käitumisjuhendiks on teatud järjekorda seatud elektrisignaalid. Tegelikult väljastab ka protsessor vaid elektrisignaale, kuid need protsessorist väljuvad signaalid on sellised ja selliselt rakendatud, et midagi toimub: ekraalile ilmuvad tähed, arvuti ootab klaviatuurilt arve ja teeb nendega varem kindlaks määratud tehteid jne. Programmeerimine tähendab praktiliselt nende protsessori käitumisjuhendite loomist.
Need no. käitumisjuhendid ehk programmid asuvad failides ja kujutavad teatud hulka järjestatud baite.
Edasine jutt keerlebki selle ümber kuidas luua neid käitumisjuhendeid ja keelitada protsessorit tegema seda mida me soovime. Siin on praktiliselt kaks etappi:

1. Programmi lähteteksti (nn. source) kirjutamine.
2. Lähteteksti muutmine protsessorile arusaadavaks käitumisjuhendiks ehk programmiks.

Üks lihne näide:

Kõige lihtsam programm millest tavaliselt alustataks kirjutab midagi ekraanile ja ongi kõik. Lähtetekst näeb välja selline:

#include<stdio.h>
main()
{
printf("Tere kaunis maailm\n");
return 0;
}

0. tehke proovimiseks tühi kataloog ja õiedage seal sees.
1. Selle teksti peaks looma nt. joe abil ja
2. salvestama nime alla maailm.c ning siis
3. andma käsurealt korralduse teha arvutile täidetav fail (executable, binary) st. käitumisejuhend ehk programm nimega maailm.b:

cc -o maailm.b maailm.c

(mõnedes perekondades cc asemel gcc)

Ning nüüd ls -i tehes peaks olema näha uus fail maailm.b. Siin antud nimed oleks võinud olla ka teistsugused va. lahteteksti laiend .c, see on vajalik, et cc ehk kompillaator võtaks ta 'omaks'.

Ja lõpeks, püüdes käivitada käsurealt korraldusega maailm.b asjatehtud programmi peaks ta tööle minema ja trükkima ekraanile rea 'Tere kaunis maailm'. Kui ei ,siis proovida käivitada

./maailm.b

kus ./ peaks arvutile ütlema et maailm.b tuleb võtta sellest kataloogist kus parasjagu ollakse.

Veel peaks lisama, et failide nimetamisel oleks hea teha endale mingid reeglid. Siin peaks sümboliseerima maailm.b lõpus olev .b, et tegu on nn. binary -ga erinevalt maailm.c -st mida nimetatakse source-ks. Muide, kui anda käsk cc maailm.c siis ilmub fail a.out mis on samuti käivitatav; Aga nimi on nagu ilmetu :)

Vot, ja kui niipalju on selge, siis juba ongi algus tehtud.

Siin veel yks lõbus romantiline programm: love.c
lahteteks on selline:

#include<stdio.h>
main()
{
printf("She loves me!\n");
}

ja et sellest saada nii ütelda binarit nimega love anda korraldus

make love

siiski vaadake et ei eksi teelt ning ei muuda harjumust binary'te loppu lisada
.b

seega parem:

make love; mv love love.b

Järjekindlamad juhendid

Füüsikas on olemas mingid põhilised tõesed ja kontrollitavad faktid ja need on sõnastatud füüsika seadusteks ning praktiliste küsimuste lahendamiseks pole põhimotteliselt vaja muud teada.

Moraal: et olla praktiline füüsik ja leida nt. rakketide trajektoore tuleb tunda teatud algtõdesid ja neid oma olukorrale vastavalt rakendada.

Inglise keeles ja muidugi ka muudes keeltes on kokku lepitud milliseid asju või nähtusi mingid sõnad (kirjapilt ja hääldus) tähistavad ja samuti on olemas reegid kuidas nend sõnade abil mõtteid väljendada. Kui inimesed tunnevad neid reegleid, siis on suur tõenaosus, et nad saavad 11 -st aru.

Moraal: et olla praktiline keelekasutaja tuleb tunda sõnu ja nende tähendusi ning reegleid kuidas sõnu kombineerida ja teie saate teistest aru ja teised saavad  teist aru. (nt. te peate teadma kuidas kasutada sõnu, et kirjendada minevikus aset leidnud sündmust)

Ja ka programmeerimises ei käituta vastavalt instinkidele nagu suudlemisel vaid tuleb tuleb tunda kokkuleppeid ja reegleid kuidas enda ideid väljendada, et arvuti teist adekvaatselt aru saaks ja nii nagu kavandatud käituma hakkaks.

Järgnevas hakkame uurima programmide loomist kasutades teatud kokkulepete susteemi mida nimetatakse C -keeleks. Selle keele tugevus on nö. sõnade lühidus ja võimalus aruvuti ressussidele kerge vaevaga lähedale saada. Viimane lause omab tähendust ilmselt allest hiljem :) Niisiis jaab üle

1. kasutada nt. tekstiredaktorit joe ja kirjutada seal C -keelne programmi lähtetekst (maailm.c) ning
2. see ara kompileerida (linkimine toimub ühe programmi puhul automaatselt) kompillaatoriga cc .

Tulemuseks saate arvutile mõistetava faili (maailm.b). Kompilaator pole mitte loodusand, vaid selle on loonud targad ameeriklased. Võiks küsida, et miks jamada kompillaatoriga, parem kohe ise kirjutada arvutile täitmiskõlbulik programm (st. maailm.b). See aga osutub väga keeruliseks ülesandeks kuna arvuti ja inimene kasutavad väga erinevaid märkide süsteeme. Viimane lause muutub jällegi tähenduslikuks ehk hiljem; aga te avage joe -s maailm.b ning siis näete milline ta välja naeb :) Kui keegi kogemata tegi

cat maailm.b,

siis võib pilt rikki minna, tagasi saab ^v ^o ja Reavahetusega.
(halvemal juhul ^v Esc c Reavahetus).

See sissejuhatav jutt peaks et põhejendama edasiste targutuste vajadust, no eks näiss.
 

1. Muutuja

Programm deklareerib kaks muutujat a ja b, omistab neile väärtused ning arvutab kolm tehet -,+, *.

1 #include<stdio.h>
2 main()
3 {
4 int a, b;
5 a=5;
6 b=10;
7 printf("a + b = %d,\na - b = %d,\na * b = %d\n", a+b, a-b, a*b);
8 return 0;
9 }

selgitus (ARVUD RIDADE EES TULEB KIRJUTAMATE JÄTTA NAD ON SIIN VAID VIITAMISEKS!)
 
 
1. enamasti see on seal, h -header fail kust kompilaator vaatab, mida teha kohates nt. käsku printf() voi teisi selliseid. 
2. main () peab olema igas programmis
3. { ja } märgi vahele jäävad nn. C - keele laused; kogu programm ise on mõnes mõttes liitlause. Lause esinevad ka edaspidi muude konstruktsioonide juures.
4. näidatakse kompillaatorile, et kasutusele tulevad täisarvulised (int - integer) muutujad a ja b 
muutuja on tegelikult arvuti mälus aadressiga pesa kus on sees muutuja väärtused. Muutuja väärtus võib programmi täitmise ajal muutuda aga aadress jääb samaks. Füüsikaliselt vastavad erinevatele muutuja väärtustele erinevad mälupesa elektrilised või magnetilised olekud. 
5. omistatakse muutujale a täisarvuline väärtus viis (ta võtab mälus enda alla 4 baiti) 
6. omistatakse muutujale b väärtus kümme (ta võtab mälus enda alla 4 baiti)
7. trükib välja erinevatele ridadele tehte ja tulemuse. see on nagu shabloon (öeldakse format
printf("Jutumärkide vahel olev tekst trükitakse"); 
printf("väljundisse saadetakse reavahetus \n ja kaks tabulaatorit \t \t");
printf("Aga %d koha peale vaadatakse muutuja a väärtust siit tagant", a); 
See on tegelikult suur kunst hoida kooskolas %d ja a ja int a; st. seda, et programmi kirjutades te peate ette teadma milliseks võivad kujuneda muutjate väärtused (tähed ehk char'id, string'id, ujukomaarvud, täisarvud) 
8. return 0 on ilus panna programmi lõppu . st et peale programmi lõppu saab koht kust programm käivitati (keskkond) teate selle kohta, et  programm on töö lõpetanud normaalselt. Kui aga programmi täitmise ajal juhtus mõni viga ja protsessor ei jõundud piltlikult üteldes return 0 juurde, siis saab keskkond vastava teate. Kas keskkond alati sellest teatest midagi ka järeldab on teine asi, aga teade on vähemalt antud.
9. vt. 3

 

1a Sama programm ainult natuke teistmoodi

#include<stdio.h>
main()
{
int a, b, summa, vahe, korrutis;
a=5;
b=10;
summa=a+b;
vahe = a-b;
korrutis = a*b;
printf("a + b = %d, a - b = %d, a * b = %d\n", summa, vahe, korrutis);
return 0;
}

Siin on vahet ainult niipalju, et vahepeal on eraldi muutujatele omistatud tehete väärtused.
Kindlasti on tähelepanu äratanud jagamistehte puudumine. See toob sisse väikese nüansi kuivõrd jagatis võib anda tulemuseks ujukomaarvu (st. tavalise komakohtadega arvu). Sellest hiljem.
 

2. Loogilised tingimused - Boolean - tõevaartusetüübi puudumine

C-keelega eripärakse paljude teiste keeltega võrreldes nimetatakse
muude asjade seas seda, et C-s puudub tõeväärtuse ehk boolean muutuja tüüp.

Lihtsalt:

(4 < 6) on tõene ja tema väärtus on C-s üks (1);
(4 > 6) on mittetõene ja väärtus on null (0).

Seda ilmestab alumine näide.

#include<stdio.h>
main()
{
int a=4, c;
c=(a < 5);
printf("tingimus väärtustub: %d\n", c);
printf("tingimus väärtustub: %d\n", a < 2 );
return 0;
}
 
 
suurem
greater
>
väiksem
less
<
võrdne
equal
==
mittevõrdne
not equal
!=
suuremvõrdne
greater equal
>=
väiksemvõrdne
less equal
<=

Peale nende on veel kolm (tegelikult on neid neli) AND - &&, OR - ||, NOT -!, (XOR)
neid tehteid iseloomustab siin toodud tabel:
 
 
operand1
operand2
NOT op1
op1 AND op2
op1 OR op2
op1 XOR op2
0
0
1
0
0
0
1
0
0
0
1
1
0
1
1
0
1
1
1
1
0
1
1
0

Selgitus tabeli lugemiseks:

op1 AND op2
võiks näiteks konkreetselt olla nii:
(a > 5) AND (a < 10)

seega
(a > 5) loogilise tehte tulemus on AND - tehte operand1; kui a on tõesti suurem 5 -st, siis op1 = 1, muidu op1 = 0
(a < 10) loogilise tehte tulemus on AND - tehte operand2; kui a on tõesti väiksem 10 -st, siis op1 = 1, muidu op1 = 0
 

Kõige raskem on ehk mõista XOR-i (exclusive OR). Tavalises keeles põhjustab OR ja XOR eristamatus ka tihti segadusi.

Kui anda kellegile käsk tuua poest fantat või (OR) piima, siis õige käitumine on koju saabumine
fantaga ja ilma piimata,
piimaga ja ilma fantata,
piima ja fantaga.

Kui anda kellegile käsk tuua poest fantat või (XOR) piima, siis õige käitumine on koju saabumine
fantaga ja ilma piimata,
piimaga ja ilma fantata.

XOR -i saab realiseerida AND - i ja OR -i abil ning teda ei pruugigi eraldi olemas olla:

op1 XOR  op2

on sama mis

(NOT (op1 AND op2)) AND (op1 OR op2)

Loogilisi tingimusi kasutatakse mitmetes konstruktsioonides nagu for, if, while switch ..

Ehk veel mõni näide:

#include<stdio.h>
main()
{
printf("tingimus 4 < 5 && 6 < 7 väärtustub: %d\n", 4 < 5 && 6 < 7);
printf("tingimus 1 <  2 || 5 > 2 väärtustub: %d\n", 1 <  2 || 5 > 2);
printf("tingimus 4 < 5 && 6 < 5 väärtustub: %d\n", 4 < 5 && 6 < 5);
printf("tingimus 4 <  2 || 5 > 2 väärtustub: %d\n", 4 <  2 || 5 > 2);
printf("tingimus 4 != 5 && 6 < 7 väärtustub: %d\n", 4 != 5 && 6 >7);
printf("tingimus 3 <= 2 || 5 == 2 väärtustub: %d\n", 3 <= 2 || 5 == 2);
printf("tingimus !(4 != 5 && 6 < 7) väärtustub: %d\n", !(4 != 5 && 6 >7));
printf("tingimus 3 <= 2 || !(5 == 2) väärtustub: %d\n", 3 <= 2 || !(5 == 2));
return 0;
}
 

3. Programm, mis teostab sarnast tegevust mitu korda - for tsükkel
 

1 #include<stdio.h>
2 main()
3 {
4 int lugeja, i;
5 for (lugeja = 0; lugeja < 5; lugeja = lugeja + 1)
6     {
7         printf("lugeja vaartus on sel ringil %d ja kontrolltingimus lugeja < 5 on toene\n", lugeja);
8      }
9 printf("asi tsykilit valjas ja toepoolest, lugeja on saanud %d -ks ja tingimus polnud enam taidetud\n", lugeja);
10 return 0;
11 }

Seglitus:
 
 
a. muutuja "lugeja" saab algväärtuse 0  (lugeja=0) ja kontrollitakse kas lugeja ikka on 5 st väiksem (lugeja < 5) 
b. kui on, siis kirjutatakse tekst ekraanile
c. Peale seda suurendatakse lugejat ühe võrra ja 
d. kontrollitakse taas kas ta on veel 5 väiksem
e. kui on, siis kirjutataks tekst taas ekraanile 

See jätkub kuni lugeja on peale mingit teksti kirjutamist suurendatud viieks ja tingimus  lugeja < 5 enam ei ole tõene ning programmi täitmist jätkatakse realt, mis asub peale for -tsüklit. (antud juhul rida 9).

VIGUR   ka selline for tsükkel on mõeldav

for(i=0; i <= 5; i=i*i - 4)
{
mingi asi mida korratakse;
}

i=i(i), st. mingi i funktsioon mitte pelgalt juurde või maha liitmine.

ja selline lõpmatu tsükkel kah (väljumine realiseeritakse tsükli sees mingit tingimust kontrollide ja tehes break.

for (;;)
{
laused;
if (tingimus)
{break}
}

Teine varant teha lõpmata tsükkel

for(i=0; 1; i=i*i - 4)
{
mingi asi mida korratakse;
}

Loogiline tingimuse kohal on üks ja C on sellega väga rahul :)
 

Ylesanded;

1. kirjutage I eeskujul programm mis leiab 10 cm kyljega kuubi ruumala ja pindala.
Andke sourcele nimeks kuup.c ja binaryle nimeks kuup.b

2. kirjutage II eeskujul programm mis kirjutab uhte tulpa arvud ja korvale tulpa nende ruudud ning kolmandasse tulpa nende kuubid. esimese tulba arvud olgu vahemikus 5 ... 10.
Binary nimeks arv.ruut.kuup.b

3. kirjutage II eeskujul programm mis valjastab read
Mina olen arv 2
Mina olen arv 4
Mina olen arv  8
...
Mina olen arv 512

(vaike abi: lugeja = lugeja * 2)
Binary nimeks kaks.korda.b

4. kirjutage II eeskujul programm mis valjastab read
Mina olen arv 10
Mina olen arv 8
Mina olen arv  6
...
Mina olen arv -4

(vaike abi: lugeja = lugeja - 2)
Binary nimeks miinus.kaks.b
 

4. Programm mis käitub vastavalt tingimustele - if

Programm kirjutab all viie arvud vasemale ja viis ning üle viie arvud paremale.

#include<stdio.h>
main()
{
int i;
for (i=0; i <= 10; i++)
{
    if (i < 5)
        {
            printf("%d\n", i);
         }
    if (i >= 5)
        {
            printf("\t\t%d\n", i);
        }
}
return 0;
}

selgitus

Niisiis, if -i järel olev lause st. { ja } vahele jäävad käsud täidetakse kui if sulgude sees olev avaldis on 1 vastasel korral teda ei täideta.
 

4a Programm mis kaitub vastavalt tingimustele - if - else tingimus
Sama as ainult veidi erinevalt teostatud.

#include<stdio.h>
main()
{
int i;
for (i=0; i <= 10; i++)
{
    if (i < 5)
        {
            printf("%d\n", i);
         }
    else
        {
            printf("\t\t%d\n", i);
        }
}
return 0;
}

Kuigi mõlemad annavad ühte moodi tulemuse on siin põhimotteline erinevus.
esimesel juhul on konstruktsioon

if (tingimus 1)
    {lause 1}

if (tingimus 2)
    {lause 2}

mis tegelikult on kaks järjestikust if tingimust ja võimalik on, et teatud juhul täidetakse nii 'lause 1' kui ka 'lause 2'
NB! kaks tingimust. Ja selliselt võib panna üksteise järele nii palju if tingimusi kui süda lustib; alati ei pruugi see muidugi kõige mõistlikum olla :)

teisel juhul on aga lugu nii;

if (ainus tingimus)
    {lause 1}

else
    {lause2}

kusjuures täidetakse kas lause1 või lause 2 aga mitte mingil juhul mõlemad.
NB! üks tingimus

4b if - else if -else tingimus

#include<stdio.h>
main()
{
int i;
for (i=0; i <= 10; i++)
{
if (i < 1)
        printf ("%d\tAlla ühe\n", i);
else if (i < 2)
        printf ("%d\tAlla kahe\n", i);
else if (i < 3)
        printf ("%d\tAlla kolme\n", i);
else if (i < 4)
        printf ("%d\tAlla nelja\n", i);
else if (i < 5)
        printf ("%d\tAlla viie\n", i);
else
        printf ("%d\tÜle nelja\n", i);
}
}

See on väga üldine if tingimuse kasutamine. Kui selline variant tuleb kasutusse, siis peaks mõtlema, et äkki on otstarbekam kasutada switch'i. Vt. allpoolt.

Aga siiski,
If tingimusi kontrollitakse nende esinemise järjekorras ja esimese õige (see ei pruugi olla ainus) juures vastav lause täidetakse ning
if - else if - else -st väljutakse. Antud juhul minnakse uuele for-i ringile. Viimane else võib ka olla ära jäetud. Siin võib tähele panna, et loogelised sulud võib ühe käsu puhul hoopis ära jätta; samas neid panna ei ole vale.
 

5. Massiivid

Massiivid on otstarbekas võtta kasutusele, kui on terve hulk sarnase tähendusega muutujaid ja neid tuleb omavahel nt. võrrelda ja ümberjärjestada. Piltlikult on massiiv hulk indekseerituid muutujaid:
a0, a1, a2, a3, a4, ..., a9 - C's tähsitatakse neid a[0], a[1], a[2], a[3], a[4], ..., a[9]

Kasutame massiivi ja
1) täidame ta arvudega,
2) trükime välja tagurpidi ja
3) üleühe.

#include<stdio.h>
main()
{
int a[10], i;
for (i=0; i  <= 9; i ++)
    a[i]=i*i;

printf("õigetpidi\n");
for (i=0; i  <= 9; i ++ )
printf("%d ", a[i]);

printf("\ntagurpidi\n");
for (i=0; i  <= 9; i ++)
printf("%d ", a[9-i]);

printf("\nüleühe\n");
for (i=0; i  <= 9; i += 2)
printf("%d ", a[i]);
printf("\nja ongi kõik\n");
return 0;
}

selgitus

massiivide kasutamine peaks olema kaunis selge, aga tähelepanu väärib C-keele võimalus kirjutada asju lühemalt kui muidu võiks loomulik tunduda, nimelt
i=i+1 on sama mis i++
i=i+2 on sama mis i+=2
Siin on veel üks nüanss:
põhimotteliselt peaks korraldus
printf("%d\n", i++);
enne kirjutama ekraanile i vaartuse ja alles siis seda suurendama
aga
printf("%d\n", ++i);
enne suurendama i väärtust ja alles siis  kirjutama ekraanile
Sellega on tihti igavene jama ja vahest on selgem avaldistes mitte kasutada i++ ja ++i konstruktsioone.

6. konstruktsioon while - lauset korratakse kuni tingimus on toene st. =1

Teeme näiteks programmi, mis kirjutab ekraanile 5 arvu.

#include<stdio.h>
main()
{
int a=1;
while (a < 6)
{
printf("arv on %d\n", a++);
}
return 0;
}

Nagu öeldud täidetakse while all olevat {}-d seni kuni tingimus on tõene st. 1.
while (1)
{
laused;
}

on niisiis lõputu kordamine; nagu for -i puhul saab sellest väljuda break abil.

7. Char muutuja tyyp. Printf - käsu formateering

Siiamaani tegelesime valdavalt täisarvude väljatrükkimisega. Nüüd vaatame natuke lähemalt millised võimalusi pakub printf funktsioon või käsk. Samuti vaatame kuidas trükkida välja tähti. Arvuti hoiab kõike enda infot ja tegeleb oma asjadega kui arvudega aga inimestele on rohkem meele järgi tegeleda tähtede ja kirjavahemärkidega. Seepärast on loodud vastavus arvude (nn. ASCII koodide, mis on failides) ja ASCII sümbolite (tähemarkide, mis ilmuvad ekraanile või printerile) vahel.
Peale selle tuleb arvestada sellega, et arvutile ei ole kõige meelepärasemad ka mitte meie decimal (kümnend) süsteemi arvud. Ta armastab binaryt (kahendsüst.) või hexadecimali (kuueteistkümnendsüst) arve.

Kirjutame siia programmi mis peaks neid seoseid illustreerima ja kirjutama kolme tulpa samad täisarvud erinevalt tõlgendatuna:

#include<stdio.h>
main()
{
int a=75;
while (a < 97)
{
printf("decimal: %d hexadecimal: %x HEXADECIMAL: %X  ASCII sümbol: %c\n", a, a, a, a++);
}
return 0;
}

katsetamise abil leidke üles millised ASCII koodivahemikud vastavad a-z, A-Z ja 0-9 le. Niipalju hoiatuseks, et alla 30d (st. decimal süst arvu 30) ei ole ASCII tavalisi tähemärke.

Siin on ASCII tabel neile, kes juba ise proovisid, võrrelge tulemusi:
 

Transfer interrupted!

bs, \b
ASCII kood,  d 
ASCII symbol
ASCII kood, d
ASCII symbol
ASCII kood, d
ASCII symbol
0
NULL
49
1
98
b
1
 
50
2
99
c
2
 
51
3
100
d
3
 
52
4
101
e
4
 
53
5
102
f
5
 
54
6
103
g
6
 
55
7
104
h
7
BEL, \a
56
8
105
i
57
9
106
j
9
tab, \t
58
:
107
k
10
LF, \n
59
;
108
l
11
vt, \v
60
<
109
m
12
FF, \f
61
=
110
n
13
CR, \r
62
>
111
o
14
 
63
?
112
p
15
 
64
@
113
q
16
 
65
A
114
r
17
 
66
B
115
s
18
 
67
C
116
t
19
 
68
D
117
u
20
 
69
E
118
v
21
 
70
F
119
w
22
 
71
G
120
x
23
 
72
H
121
y
24
 
73
I
122
z
25
 
74
J
123
{
26
 
75
K
124
|
27
ESC
76
L
125
}
28
 
77
M
126
~
29
 
78
N
127
 
30
 
79
O
128
 
31
 
80
P
129
 
32
tühik
81
Q
130
 
33
!
82
R
131
 
34
@
83
S
132
 
35
#
84
T
133
 
36
$
85
U
134
 
37
%
86
V
135
 
38
&
87
W
136
 
39
'
88
X
137
 
40
(
89
Y
138
 
41
)
90
Z
139
 
42
*
91
[
140
 
43
+
92
\
141
 
44
,
93
]
141
 
45
-
94
^
142
 
46
.
95
_
143
 
47
/
96
`
144
 
48
0
97
a
145
 

Siin peab ütlema, et esitatud on vaid pool ASCII tabelit kuna esimese hooga ei osanud teise poole sümboleid HTML-s välja võluda ja esimesed 30 on kah hõredad aga praktiliselt pole rohkem vaja kui toodud on. 0 - 127 on pool baiti ehk nelja bitiga esitatavad.

Mis C-s on hea on integeride ja ASCII sümbolite mõnesugune vahetatavus ja samaväärsus. Nt.

printf("%c ja %c on korrutades %d nagu symbolitele vastavate ascii koodide korrutis\n", 'A'*'A');

või

a > b on 0 kuna 97 > 98 on väär.

Sellist seost täisarvude ja tähtede vahel saab kasutada nt. sõnade tähestikulisse järjekorda seadmisel ja ei tea veel kus.

Lahemalt printf'ist.

Toome siin tabeli mis näitab kuidas defineertud muutujaid välja trükkida. See pole ammendav aga alguseks piisav, vast.
 
 
deklareerimine printf-i formaat mis tuleb
int a=65; 
mälus eraldatakse 4 baiti
"%d", a 
"%c", a 
"%u", a 
"%x", a 
"%X", a 
"%o", a
65 

65 
vastav arv hexadec süsteemis 
sama aga suured tähed 
vastav arv octal süsteemis
char a=65 
mälus eraldatakse 1 bait
"%d", a 
"%c", a
65 
A
float a=65; 
mälus eraldatakse 4 baiti
"%f", a 65.000000
int a=65; "%f", a/1.0 65.000000

NB! muutuja tüüp nagu ta deklareeriti peab olema kooskõlas sellega mis kirjutatakse printf -i sisse nii öelda shablooni. Põhimotteliselt on formaatidega õiendamine sama ka scanf-i puhul ainult tavalise muutuja asemel tuleb kasutada tema aadressi nt. &int_tyypi_muutuja_nimi  (scanf("%c", &a); - sellest põhjalikumalt allpool).

8. Väljundisse kirjutaja ja sisendist lugeja

Siiamaani on kõik programmid võtnud andmed nö. programmi koodi seest ja tulemuse väljastanud ekraanile. Tegelikult on selleks järgmised võimalused:

programm võtab sisendi

1. enda seest
2. klaviatuurilt
3. failist
4. käsurealt

programm saadab väljundi

1. ekraanile
2. faili

C keeles käsitletakse klaviatuuri ja ekraani kui erilisi faile nimedega stdin ja stdout.
Linuxi eripära on inputi ja outputiga (I/O ga) kaunis vaba manipuleerimine, mis toob kaasa võimaluse kasutada muidu klaviatuurilt lugemiseks ja ekraanile kirjutamiseks tehtud programmi  nii:

bash# programm < juttsisse > juttvalja

seda nimetatakse I/O ümbersuunamiseks ja tuleb mõista seliselt, et faili juttsisse sisu saadetakse programmi stdin -ga tegelevatele käsudele sisendiks ja programmi stdout-iga tegelevate käskude väljund saadetakse faili juttvalja.

No, ärge ehmuge, see jutt omandab kohe tähenduse kah!
 

Programm, mis kirjutab ekraanile mõned read ja küsib teilt kahte arvu:

teie pikkust cm -des ja teie massi. Vastusena ta annab kui palju kaaluks keskmiselt üks viil kuid teid teha  cm paksusteks viiludeks :) No see on mass/pikkus jagamistehe. f - float, võite sisestada komadega (tegelikult punktidega) arve.

#include<stdio.h>
main()
{
float mass, pikkus;
printf("Miline on teie kaal kg-des?\n");
scanf("%f", &mass);
printf("Miline on teie pikkus cm-tes?\n");
scanf("%f", &pikkus);
printf("Teie keha 1 cm paksune viilakas oleks keskmiselt %f kg raskune.\n", mass/pikkus);
return 0;
 }

selgitus

siin jääb kohe silma uus käsk

scanf("%f", &mass);

ta on printf-le mõnes mõttes vastand,
printf väljastab formateeritud asja, scanf aga võtab vastu formateeritud asja. &mass kohale tuleb alati panna, asi mis näitab aadressi: kas pointer millest tuleb varsti juttu või muutuja aadress nagu praegu. St. 'mass' on komaarvuline muutuja ja '&mass' on tema aadress. Aadressidest on pikemalt juttu allpool ponterite juures. Esialgu leppige sellega, et nii see on. Kui järele moelda, siis te peate vist ka hiljem leppima sellega, et nii see on :)

proovige I/O-ga mängimiseks teha asi veidi ümber
looge stdin -i jaoks fail "andmed" milles on kaks arvu 76 ja 184

bash#cat > andmed
76
184
Ctrl +d
bash#

ning andke korraldus

bash# a.out < andmed > tulemus

vaadake mis on tulemuses sees.

ASCII faili avamine, sinna kirjutamine ja sulgemine

Oluline on ära õppida panna kuidas fail avatakse ja siis suletakse. NB! tähesuurus on C-is oluline. Siin võib olla probleeme ka faili rwx õigustega.

#include<stdio.h>
main()
{
int a;
FILE *f;
f=fopen("/root/datta.xxx", "w");
for (a=0; a < 6; a++);
fprintf(f, "arv: %d ja tema ruut: %d\n", a, a*a);
fclose(f);
return 0;
}
 

Linux'i Input-i ja Output-iga manipuleerimise oskuse kiituseks peab ütlema, et samasuguse faili datta saab luua ka alltoodud programmiga.
ainult programm tuleb käivitada nii:

bash#a.out > datta

#include<stdio.h>
main()
{
int a;
for (a=0; a < 6; a++);
printf("arv: %d ja tema ruut: %d\n", a, a*a);
return 0;
}
 

ASCII failist lugemine

Nüüd vaatame kuidas õnnestub ASCII failist lugeda. Esiteks,
olgu fail "loeme1" lihtsalt selline:
0 1 2 3 45 57

bash#tee
0 1 2 3 45 57
Ctrl+d
 

ja lugeja selline ning tulemust näete ise kui järele proovite:)

#include<stdio.h>
main()
{
FILE *f;
int arv;
f=fopen("/root/loeme1", "r");
while(fscanf(f, "%d", &arv)!=EOF)
printf("arv: %d ja ruut: %d\n", arv, arv*arv);
fclose(f);
return 0;
}

selgitus

Eriti kaval on siin see asi kuidas faili lõpp ära tuntakse: kui fscan võtab failist arve, siis kohates faili lõppu tagastab ta väärtuse EOF (tegelikult -1 enamasti). while () kontrollib kunas fscan tagastab -1 ehk EOF ja kui tagastab, siis jätkab programmi täitmist while -le järgnevalt realt, meil fclose(f).

Uus näide - isetehtud cat

Aga siin loetakse failist char-e, sh. ka reavahetusi. See programm
peaks lihtsalt esitama faili ASCII sisu (misatehes pika, ta on sarnane linuxi cat programmile).

#include<stdio.h>
main()
{
FILE *f;
char tahemark;
f=fopen("/root/loeme1", "r");
while(fscanf(f, "%c", &tahemark)!=EOF)
printf("%c", tahemark);
fclose(f);
return 0;
}

Käsurea argumendid sealhulgas programmi enda nimi

#include<stdio.h>
main(int argc, char *argv[])
{
int i;
for (i=0; i < argc;i++)
{
printf("%s\n", argv[i]);
}
return 0;
}

Siinkohal on põhjust märkida, et nt. gzip ja gunzip on tegelikult hard lingid samale inode'le. inode on seotud programmiga, mis käivitatakse ja ta vaatab kas tema poole pöörduti kui gzip'i või kui gunzip'i poole (kontrollid seda sama argv[0] -i) ja käitub siis vastavalt.  Muidugi oleks tulemus sama kui kasutada võtit: 'gzip -d'  aga miks mitte ka selgemat gunzip'i.

9. switch - võimalus suunata programmi mitme tegevuse vahel

Selle konstruktsiooni saab asendada ka if -if else -else konstruktsioonidega aga mõnes olukorras olla switch abiks.
Programm lihtsal loeb klaviatuurilt täisarvu ja ütleb, kas see oli viiest suurem või vaikse-võrdne.

#include<stdio.h>
main()
{
int a;
scanf("%d", &a);
switch (a > 5)
{
case (0) : printf("väiksem viiest või võrdne või polegi arv :(\n"); break;
case (1) : printf("suurem viiest\n"); break;
  default: printf("no siia ta küll ei jõua\n"); break;
}
return 0;
}

Siin on veel teine switch-iga programm, mis leiab mitu täishäälikut
esines programmi stdio-sse antud tekstis. Kui oleks vaja kogu
tähestikku uurida oleks tegelikult õigem programm teha teist moodi.
Natuke naljakas oleks kasutada 24 case-i. Siis tuleks kasutada tõsiasja,
et iga täht on identifitseeritud arvuti jaoks ASCII koodiga
ja sealt midagi valja mõelda. Aga siin see on:

#include<stdio.h>
main()
{
char taht;
int a=0, e=0, i=0, o=0, u=0, muu=0;
while (scanf("%c", &taht)!=EOF)
{
switch (taht)
{
case 'a': a++; break;
case 'e': e++; break;
case 'i': i++; break;
case 'o': o++; break;
case 'u': u++; break;
 default: muu++; break;
}
}
printf("\tTulemused:\na: %d\ne: %d\ni: %d\no: %d\nu: %d\nmuu: %d\n", a, e, i, o, u, muu);
return 0;
}

Jah, jah - eesti keeles on veel õäöü kah olemas aga let them be ...
Ja samuti on AEIOU ka vokaalid, aga - tehke programm nii ümber, et ta ka neid arvestaks!

10. Pointer

Väike näide pointerist. Pointer on selline muutuja tüüp mis
omandab väärtuseks mäluaadresse. Neid aadresse on võimalik
kasutada otse aadressina või sellel aadressil oleva suurusega
tegelemiseks. Ilma pikema teoreetilise jututa peaks alltoodud
näitest aru saama kuidas pointerit deklareerida ja kasutada.
Muide, pointerit me kasutasime juba scanf() -i ja printf() juures sellest
väga kõnelemata :)
 

#include<stdio.h>
main ()
{
int i, *p, arvud[5];
for (i=0; i < 5; i++)
arvud[i]=i;

printf ("trükime välja lihtsalt massiivi elementide väärtused\npöördudes nende poole massiivi indexitega:\n");
for (i=0; i < 5; i++)
printf("arvud[%d]: %d ja &arvud[%d]: %u\n",i ,arvud[i], i, &arvud[i]);

p=&arvud[1];
*p=10;

printf ("\ntrükime välja uuedmassiivi elementide väärtused\npöördudes nende poole massiivi indexitega:\n");
for (i=0; i < 5; i++)
printf("arvud[%d]: %d ja &arvud[%d]: %u\n", i, arvud[i], i, &arvud[i]);

p=&arvud[0];

printf("\ntrükime välja massivi elemendid\nkasutades nende poole pöödumisel pointerit:\n");
for (i=0; i < 5; i++)
{
printf("arvud[%d]: %d ja &arvud[%d]: %u\n", i, *p, i, p);
p++;
}

return 0;
}
 
 

11. String - ehk järgnevus tähemarke

Vaatame kuidas on lugu stringidega. String on objekt mis on tegelikult
char-ide massiiv, kuid tema viimane element peab kindlasti olema
eriline ASCII sümbol mida kujutatakse '\0'. Stringi praktiline
eelis char massiivi ees on et temaga saab tegelda spetsiaalseid
stringi käske kasutades (stringi printf-imisel, kahe stringi võrdlemisel
etc.)

Alltoodud programm selgitab seda.

#include<stdio.h>
main()
{
char sona[20];
sona[0]='l';
sona[1]='e';
sona[2]='n';
sona[3]='i';
sona[4]='n';
sona[5]='\0';
printf("%s\n", sona);

return 0;
}
 

Ja siin all olev näide peaks andma aimu kuis on seotud char massiiv, string ja
pointerid. Uurige, prooviga ja leidke praktiline rakendus!

#include<stdio.h>
main()
{
char i=0, *p, sona[20];
strcpy(sona, "Tere Lenin");
printf("%s ja seal on tähmärke %d", sona, strlen(sona));

printf("\n------------------\n");

while(sona[i]!='\0')
{
printf("%c", sona[i]);
i++;
}

printf("\n------------------\n");
p=sona;
while(*p!='\0')
{
printf("%c", *p);
p++;
}
printf("\n------------------\n");
return 0;
}
 

12. Funktsioonid

Järgnevas püüame omandada oskuse luua ise funktsioone. Me pole maininud, aga funktsioone oleme kasutanud juba mitmeid:
printf(), scanf(), kui nimetada mõned. Kes on tuttavad muude keeltega, siis teadke, et C keeles on protseduurid ja funktsioonid kõik ühed funktsioonid. Ja kuivõrd nad on funktsioonid, siis neil on arumendid a=printf("Tere maailm!\n") ja väärtus, st. iga funktsioon tagastab peale täitmist ka väärtuse a=printf("Tere maailm!\n"). Esmalt veendume, et see on ikka nii. Muuseas, kui tagastatud väärtus on -1 on tegemist veaga, tavaliselt.

#include<stdio.h>
main()
{
int a;
a=printf("Näis millise väärtuse ta tagastab..\n");
printf("%d\n", a);
return 0;
}

Ja kui tahta väga kindla peale programmeerida (st. kontrollitakse mida funktsioonid tagaastavad ning see peaks vältima nö. programmi kinnijooksmise), siis tehakse midagi sellist:

Püüame luua CD peale faili :)

#include<stdio.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
main()
{
    int f;
    f=creat("/cdrom/nimi", S_IRWXU);
    printf("%d\n", f);
    if (f < 0)
        {
            printf("Juhtus viga ..\n");
           exit(1);
        }
    close(f);
    return 0;
}

See tohutu hulk include'sid on vaja creat'i pärast. Tõsi, see programm lõpetab ka muidu ise töö.

Aga nüüd ise funktsioone tegema!

All on toodud näide, kus ühes samas failis on põhiprogramm millest funktsioon root välja kutsutakse; ja selles failis on ka root'i definitsioon defineeritud.

 1. #include<stdio.h>
 2. int root (int a);

 3. main()
 4. {
 5.     int c=4,r;
 6.     printf("Tere, mina on main, nö. põhiprogramm ehk põhifunktsioon\n");
 7.     r=root(c);
 8.     printf("ja ongi kõik\narv= %d, ruut= %d\n", c, r);
 9.     return 0;
10. }

11. int root (int a)
12. {
13.     printf("terekest siis, mina olen funktsioon\n");
14.     return a*a;
15. }

Selgitus:

2. deklareeritakse, et hakatakse kasutama funktsiooni ruut millel on täisarvuline argument ja mis tagastab täisarvulise väärtuse
3. -10. põhiprogramm
7. pöördutakse funktsiooni poole
11. - 15. defineeritakse funktsioon, kooskõlas 2. reaga
14. määratletakse mida funktsioon tagastab

Deklareerimine näitab muutuja tüübi; defineerimine eraldab mälupesa muutuja väärtuse hoidmiseks!

Programm läheb tööle ka siis, kui 2. rida hoopis ära jätta või kui 5. dale juurde kirjutada 'root (int a)'
Punast värvi muutujate nimede asemel võib kasutada suvalisi nimesid.

Võimalik, et mõni funktsioon ei võtagi argumente:

 1. #include<stdio.h>
 2. int root (void);

 3. main()
 4. {
 5.     int c=4,r;
 6.     printf("Tere, mina on main, nö. põhiprogramm ehk põhifunktsioon\n");
 7.     r=root();
 8.     printf("ja ongi kõik\narv= %d, ruut= %d\n", 2, r);
 9.     return 0;
10. }

11. int root (void)
12. {
13.     printf("terekest siis, mina olen funktsioon\n");
14.     return 2*2;
15. }

void - in.k. tühjus (ka sõna avoid - vältima)

Info vahetamine põhiprogrammi ja funktsiooni vahel.

Selleks on kaks võimalust: external variables ja funktsiooni argumendid. Tegeleme esmalt viimase võimalusega. See tagab programmi parema struktuursuse.

Muutujate kohta tuleb mainida, et neid saab kasutada selle funktsiooni piires, kus nad defineeriti kuigi on olemas ka nn. external muutujaid, mis on ühesugused kõigi failis defineeritud funktsioonide jaoks. Põhiprogrammis võib olla muutuja 'a' ja funktsioonis samuti muutuja 'a', kuid nad nö. ei tea teineteise olemasolust. Need kaks 'a' -d elavad kumbki omas mälupesas.  Nt. kui põhiprogrammis on muutuja 'a' väärtus 5, ja seda kasutatakse funktsiooni poole pöördumisel argumendina ning funktsioon muudab 'a' väärtust, siis põhiprogramm sellest teada ei saa. Tõepoolest, funktsiooni poole pöördumisel kirjutati funktsiooni muutaja 'a' mälupessa põhiprogrammi 'a' mälupesa väärtus ja funktsioon tegeles seejärel oma 'a' mälupesaga.

Teeme näite kah:

#include<stdio.h>
int root(int a);
main()
{
int a=2;
printf("%d on %d ruudus\n", root(a), a);
return 0;
}

int root (int a)
{
a=a*a;
return a;
}

Mõnikord on aga siiski vaja muuta funktsioonil põhiprogrammi muutujatele vastavate mälupesade väärtusi; siin tulevad appi pointerid.

Olgu ülesandeks muuta põhiprogrammi muutuja b=10 väärtus b=5 -ks.

#include<stdio.h>
int root(int *a);
main()
{
int b=2;
printf("enne oli %d .. \n", b);
root(&b);
printf(".. ja nüüd on %d \n", b);
return 0;
}

int root (int *a)
{
*a=(*a)*(*a);
return 0;
}

käivitamisel:

bash# u1
enne oli 2 ..
.. ja nüüd on 4
bash#
 

Selgitused:

int root (int *a);

See tähendab, et root on funktsioon mis tagastab täisarvulise väärtuse ja funktsiooni argumendiks on täisarvu mälupesa aadress (tegelikult küll algusaadress, sest linux'is võtab int 'ga defineeritud täisarv enda ala 4 baiti).

int b=2;
root (&b);

Need read tähendavad, et b on täisarvuline muutuja kusjuures funktsiooni argumendiks saab talle vastava mälupesa aadress. Aga seda just root oma argumeniks ootabki.

*a=(*a)*(*a);

See tähendab, et korrutatakse mälupesa aadressi omava muutuja 'a' poolt viidatavas mälupesas oleva täisarvu väärtus iseendaga ja tulemus kirjutatakse sinna samasse mälupessa. Füüsiliselt tegeldakse sama aadressiga millele viitab ka &b. Kui sellest aru saada on juba suur asi!
 

Tüüpiline rakendus on massiivi järjestamine:

#include<stdio.h>
int swap(int *a, int *b);
main()
{
    int m[5], i=0;
    m[0]=9; m[1]=5; m[2]=9; m[3]=4; m[4]=1;

    while (i < 5)
        {
            printf("%d, ",m[i]);
            i++;
        }
    printf("\n"); i=0;

    while (i < 4)
        {
            if (m[i] > m[i+1])
                {
                    swap(&m[i], &m[i+1]);
                    i=-1;
                }
            i++;
        }

    i=0;
    while (i < 5)
        {
            printf("%d, ",m[i]);
            i++;
        }
    printf("\n"); i=0;
    return 0;
}

    int swap(int *a, int *b)
        {
            int tmp;
                tmp=*a;
                *a=*b;
                *b=tmp;
            return 0;
        }

Eesmärk oli järjestada massiiv arvude kasvamise järjekorras.
Idee on siin lihtne: võrreldakse esimest ja teist arvu - kui teine on esimesest suurem siis võrreldakse teist ja kolmandat; kui kolmas on teisest väiksem, siis vahetatakse teise ja kolmanda kohad ja alustatakse taas algusest arvude võrdlemist. Asi toimub kuni võrreldakse kahte viimast arvu ja neid ei tule ära vahetada. Selline  algoritm küll ei sobi suurte massiivide jaoks, kuid illustratsiooniks sobib, ehk.
 

Kui soovida funktsiooni hoida omaette failis siis võib nii talitada:
pohi.c:

#include<stdio.h>
int root (int a);
main()
{
printf("Tere, mina on main, nö. põhiprogramm ehk põhifunktsioon\n");
teretaja(4);
printf("ja ongi kõik\n");
return 0;
}

teretaja.c:

int root (int a)
{
printf("terekest siis, %d, %d\n", a, a*a);
return a*a;
}

kusjuures kompilleeritakse nii:

bash# cc -o tulemus pohi.c teretaja.c

Muide, viga on selles, et praegu lastakse tal kaks korda ruutu arvutada.
(Kuidas teha ühte programmi kokku mitmest failist, selleks on palju võimalusi, allpoolno toodud teisigi)
Siinkohal on paras aeg rääkida objektidest ja header failidest .. kui ma vaid oskaks!

13. Struktuur

Struktuur on järjekordne võimalus muutujaid deklareerida, ehk massiivi moodi natuke.  Kui massiivis on kõik elemendid ühte tüüpi, nt. täisarvud, siis struktuuris võivad olla nad eri tüüpi. Massiivis võib olla väga palju (100 000) elemente, aga struktuuris tavaliselt kuni paarkümmend. Struktuuri on põhjust kasutada juhul kui andmed moodustavad teatud komplekti; nt. peate 3 andmebaasi isiku andmetest:
 
 
kui_kodanik:
kui füüsiliste/meditsiiniliste 
omadustega objekt:
kui emotsionaalsete/isiku 
omadustega objekt:
kodanik.data med.data emots.data
- eesnimi 
- perekonna nimi 
- isikukood
- elukoht 
- telefon 
- perekonnaseis 
- sugu
- isikukood
- pikkus 
- kaal 
- veregrupp 
- vanus 
- ausus 
- ilu 
- aatelisus 
- ärrituvus 
- isikukood
Muuseas, et nende andmetega korraga tegeleda peab olema neis kõigis esinema mingi väli, antud juhul isikukood. Kui siit teemat arendada võiks välja jõuda asjani mida nimetatakse relatsiooniliseks andmebaasiks (seotud, suhtuvaks andmebaasiks).

Hetkel keskendume aga andmete hoidmisele muutujates. Osutub, et otstarbekas on kasutada selliseid struktuure:

struct stru_kodanik
    {
        char ees_nimi[20];
        char perekonna_nimi[20];
        char isikukood[20];
        char elukoht[20];
        char telefon[20];
        char perekonnaseis[20];
        char sugu[20];
    } kodanik;

struct stru_med
    {
        char isikukood[20];
        int pikkus[20];
        int kaal[20];
        char veregrupp[20];
        int vanus[20];
    } med;

struct stru_isik
    {
        char ausus[20];
        char ilu[20];
        char aatelisus[20];
        char arrituvus[20];
        char isikukood[20];
    } isik;

 Ning lõpuks, kui soovime omistada muutujale väärtust toimub see nii:

strcpy(isik.ausus, "väga aus");
med.pikkus = 189;
med.kaal = 94;

Kusjuures, struktuur võib alla ka massiivi elemendiks. Illustreerime neid kahe teist laadi näitega:

Näide 1:

Programm kirjutab arvude arvu 5 mõned astmed:

#include<stdio.h>
main()
{
struct stru_astmed {

int a1;
int a2;
int a3;
} astmed;
 

astmed.a1=5;
astmed.a2=astmed.a1*astmed.a1;
astmed.a3=astmed.a1*astmed.a1*astmed.a1;

printf ("astmed.a1= %d\nastmed.a2= %d\nastmed.a3= %d\n", astmed.a1 ,astmed.a2, astmed.a3);

return 0;
}

Ja järgmine näide peaks illustreerima massiivi mille elementideks on struktuur:

Programm kirjutab 1 .. 10 arvude mõned astmed:

#include<stdio.h>
main()
{
    int i=0;
    struct stru_astmed {
        int a1;
        int a2;
        int a3;
        } astmed[10];

    printf ("astmed.a1    astmed.a2    astmed.a3 \n");
    while (i < 10)
        {
            astmed[i].a1=i;
            astmed[i].a2=i*i;
            astmed[i].a3=i*i*i;
            printf ("%d\t\t%d\t\t%d\n", astmed[i].a1 ,astmed[i].a2, astmed[i].a3);
            i++;
        }
    return 0;
}

Tunnistagem, et kahte viimast programmi oleks võinud palju lihtsamate vahenditega realiseerida kui mõtelda sellele mida nad teevad. Aga oluline on just see uus technique!

14. Pointer struktuurile ja sturktuurielemendile

See võib näida küll tehniliste võimaluste ümber keerutamisena, kuid järgmine punkt on oluliselt tähtis. See võimaldab hakata teadlikult kasutama GNU C library funktsioone. Niisiis, kannatust ja vaatame ära kuidas struktuuri elementide poole poinerite abil pöörduda.
 

 1. #include<stdio.h>
 2. #include<stdlib.h>
 3. main()
 4. {
 5. struct fyysiline_isik
 6.        {
 7.                int pikkus;
 8.                int kaal;
 9.                int vanus;
10.                int jalanr;
11.                char nimi[20];
12.        };
13.
14. struct fyysiline_isik *imre;
15. imre=malloc(sizeof(*imre));
16. (*imre).pikkus=192;
17. (*imre).kaal=85;
18. imre->vanus=28;
19. imre->jalanr=45;
20. strcpy(imre->nimi, "Imre Oolberg");
21.
22. printf("struktuuri imre alguse mäluaadress: %p\n", imre);
23. printf("struktuuri imre pikkus mälus baitides: %d\n", sizeof(*imre));
24. printf("pikkus %d\n", (*imre).pikkus);
25. printf("kaal %d\n", (*imre).kaal);
26. printf("vanus %d\n", imre->vanus);
27. printf("jalanr %d\n", imre->jalanr);
28. printf("nimi %s\n", imre->nimi);
29.
30. free(imre);
31. return 0;
32. }

Selle programmi moraal on järgmine:

5. - 12. defineerib struktuuri nimega fyysiline_isik
14. deklareerib pointeri imre struktuurile fyysiline_isik
15.  eraldatakse arvuti mälus struktuurile mälu
16. - 20.   kahe erineva süntakiga aga praktiliselt sama tulemusega omistatakse struktuurielementidele väärtused
22. - 28. trükitakse ekraanile mõned suurused ja struktuurielemendid (NB! %p pointeri trükkimisel)
30. vabastatakse mälu imre alt

Selle tulemuse oleks saanud ka tunduvalt lihtasamini ekraanile väljastada, aga tehnika esitus oli oluline!
 

15. GNU C Library'te kasutamine

Me oleme algusest peale kasutanud C library't.  Esmalt kinnitame seda, et meie kasutada (eeldusel, et te tegelete ikka linux'i peal ja kompilaatoriks on standartne GNU C) olev C sisaldab peale kompilaatori suure komplekti tarkade meeste poolt kirjutatud funtsioone. Tuttavad ja need millega oleme tegemist teinud on printf ja scanf, näiteks. Peale funktsioonide on meie kasutada ka nende meeste poolt defineeritud muutujate sh. struktuuri tüübid. Kui oma programmis kasutada olemasolevaid funktsioone, siis tuleb kõige algul nad ka ära deklareerida #include<header faili nimi> ridadel. Header failist saab kompilaator teada millist tüüpi funktsiooni argumendid võivad olla ja kontrollib kas me kasutame neid õieti.

Veel on oluline, et me oskaksime orienteeruda tavaliselt installeeritud programmeerimise 'man paged' des. Nt. funktsioon atoi().
'man atoi' ütleb selle kohta järgmist:

ATOI(3)             Linux Programmer's Manual             ATOI(3)
 

NAME
       atoi - convert a string to an integer.

SYNOPSIS
       #include <stdlib.h>

     int atoi(const char *nptr);

DESCRIPTION
       The  atoi()  function  converts the initial portion of the
       string pointed to by nptr to int.  The  behaviour  is  the
       same as

       atoi() funktsioon teisendab pointer nptr'i poolt osutatud stringi
       esimese osa täisasvuks mille ta tagastab.

              strtol(nptr, (char **)NULL, 10);

       except that atoi() does not detect errors.

RETURN VALUE
       The converted value.

CONFORMING TO
       SVID 3, POSIX, BSD 4.3, ISO 9899

SEE ALSO
       atof(3), atol(3), strtod(3), strtol(3), strtoul(3)
 

Rõhutatud rida peaks teatama, et funktsioon võtab argumendiks pointeri stringile ja tagastab täisarvu. 'const' rõhutab, et string ei muuda argumendi st. stringi väärtust. Mõned funktsioonid (all tim() teemav seda).

programm:

#include<stdio.h>
main()
{
int a;
char *s="15 16";
printf ("%d\n", atoi(s));
return 0;
}
 

Teine näide kirjeldab printf()-i tegemisi, see on keeruline :) Ma kirjutasin selle enne valmis ja siis lihsama näite; ja nüüd ei raatsinud enam maha kah võtta!

Nt. funktsiooni printf() man page algab nii ('man fprintf'):

PRINTF(3)           Linux Programmer's Manual           PRINTF(3)

NAME
       printf,   fprintf,  sprintf,  snprintf,  vprintf,  vfprintf,  vsprintf,
       vsnprintf - formatted output conversion

SYNOPSIS
       #include <stdio.h>

       int printf(const char *format, ...);
       int fprintf(FILE *stream, const char *format, ...);
       int sprintf(char *str, const char *format, ...);
       int snprintf(char *str, size_t size, const char *format, ...);

       #include <stdarg.h>

       int vprintf(const char *format, va_list ap);
       int vfprintf(FILE *stream, const char *format, va_list ap);
       int vsprintf(char *str, const char *format, va_list ap);
       int vsnprintf(char *str, size_t size, const char *format, va_list ap);

DESCRIPTION
       The printf family of functions produces output according to a format as
       described  below. The functions printf and vprintf write output to std-
       out, the standard output stream; fprintf and vfprintf write  output  to
       the  given  output  stream;  sprintf,  snprintf, vsprintf and vsnprintf
       write to the character string str.

       These functions write the output under the control of a  format  string
       that  specifies how subsequent arguments (or arguments accessed via the
       variable-length argument facilities of  stdarg(3))  are  converted  for
       output.

     The format string is composed of  zero  or  more  directives:  ordinary
       characters  (not  %),  which are copied unchanged to the output stream;
       and conversion specifications, each of which results in  fetching  zero
       or  more subsequent arguments.  Each conversion specification is intro-
       duced by the character  %.   The  arguments  must  correspond  properly
       (after type promotion) with the conversion specifier.  After the %, the
       following appear in sequence:

           d, u, f, p ....
       ...

Siit saame olulist meid huvitava funktsiooni printf kohta teada:

int printf(const char * format, ....);

(muuseas, tavaliselt seda käsku tarvitatakse nii:

printf("mina käis %d. klassis\n", i);

kes ei mäletanud)

'const char *format' ütleb, et nn. formateeriva stringi asemel otse võiks ka kasutada pointerit talle, proovime:

#include<stdio.h>
#include<stdlib.h>
main()
{
int i, *j;
char *format_string;
format_string=malloc(50);
strcpy(format_string, "muutuja tüüp double on pikk: %d baiti\n");
printf(format_string, sizeof(double));
free(format_string);
return 0;
}

ja 'const' ütleb, et printf funktsioon ei muuda argumendi väärtust.

No lõpuks algavad ka praktilisemat laadi õpetussõnad. Nimelt vaatame kuidas kasutada GNU C library'eid ja seal defineeritud andmestruktuure. Püstitame sellise probleemi, et on vaja trükkida välja aega. Library pakub selleks vahendid, millele saab ligi kasutades header file time.h:

funktsioone time  ja localtime
andmete hoimiseks muutuja tüüpi time_t (see on tegelikult long int) ja struktuuri tm mis on selline:

struct tm
              {
                      int     tm_sec;         /* seconds */
                      int     tm_min;         /* minutes */
                      int     tm_hour;        /* hours */
                      int     tm_mday;        /* day of the month */
                      int     tm_mon;         /* month */
                      int     tm_year;        /* year */
                      int     tm_wday;        /* day of the week */
                      int     tm_yday;        /* day in the year */
                      int     tm_isdst;       /* daylight saving time */
              };
 

Ajaga tegelev programm on niisugune:

 1. #include<stdio.h>
 2. #include<time.h>
 3. main()
 4. {
 5.     time_t taka;
 6.     struct tm *aeg;
 7.     time(&taka);
 8.     aeg=localtime(&taka);
 9.     printf ("aja osa: %d\n", (*aeg).tm_year);
10.     printf ("aja osa: %d\n", aeg->tm_wday);
11.     return 0;
12. }

Selgitused:

1. .2 inkluudivad library'd
5. dfineeritakse muutuja taka (sisuliselt on see long int - nimelt linux arvestab aega sisemiselt kui sekundeid mis on möödunud 1. jaanuarst 1970 aastal; rikkaliklt on funktsioone mis teisendavad tolle täisarvu vastavaks nädalapäevaks, või minutiks ...)
6. defineerime pointeri library's omakorda defineeritud struktuurile tm
7. funktsioon omistab muutujale väärtuse (vt. alt kommentaari)
8. omistame struktuuri *aeg elementidele väärtused
9. - 10.  trükime ekraanile aastaarvu ja nädalapäeva

Kõigepealt märgime mida 'man localtime' asja kohta ütleb:

       #include <time.h>

       char *asctime(const struct tm *timeptr);

       char *ctime(const time_t *timep);

       struct tm *gmtime(const time_t *timep);

     struct tm *localtime(const time_t *timep);

       time_t mktime(struct tm *timeptr);
 

ning 'man time'

#include <time.h>

       time_t time(time_t *t);

Siit on näha, et 'const' puudub ja seega funktsioon ei kasuta (seda võib ta ka teha, tegelikult)argumendi väärtust vaid omistab talle ise väärtuse.
 
 
 

Shared Library'te valmistamine

Esmalt kirjeldame paari sõnaga seda kuidas OS Linux'i all programmid töötavad.

Joonisel on kujutatudud väga ülekantud tähenduses kolm programmi: mv, tar, cp
Kui järele mõelda siis nad kõik oskavad ka midagi ühist: kui mitte muud siis ekraanile kirjutada (võtme 'v' (verbose) abil.
Aga ju nende programmide kujuteldavates binary'tes on ka muid koodiosi mis kattuvad või on sarnased.
 

Tagrad ameeriklased on välja mõtelnud sellise süsteemi, et programm ei pea olema ühes failis ja seda ta ka enmasti pole. Osa temast
asub nn. library'tes (in. k. raamatukogu) ja teine unikaalsem osa vastavanimelisest nö. programmifailis. Kui Slackware't seostatakse
libc5'ga ja RedHat'i libc6 mida kutsutakse ka glibc-ks, siis tegelikult viidataksegi erinevatele library'tele. Loomulikult saab oskuslikul
kasutamisel nii ühele kui teisele panna peale mistahes library lisaks oolemasolevale. Idee on selles, et kui see nn. programmifaili
binary on ikka tehtud eeldades, et süsteemis on olemas teatud konkreetse versiooni library, siis tavaliselt peab see seal ka olema.

Peale libc on veel ka muid library'id:

ncurses - tegeleb mittegraafilise ekraaniga (a la mc)
Xaw - tegeleb (X windowsiga)
gtk - tegeleb X - alla graafiliste nupukeste jms. eleementide joonistamisga
jt.

Moraal:
kui teie sõbra linuxis on tore programm ytle_mulle_oma_pruudi_nimi.b siis selleks et seada enda arvutis kasutada ei piisa tolle .b faili
kopeerimisest vaid peate vaatama käsuga 'ldd' mis library'te olemasolu see eeldab ja tagama oma masinas nende olemasolu.

bash# ldd /usr/bin/ytle_mulle_oma_pruudi_nimi.b
        libtermcap.so.2 => /lib/libtermcap.so.2 (0x4000b000)
        libgpm.so.1 => /usr/lib/libgpm.so.1 (0x4000e000)
        libc.so.5 => /lib/libc.so.5 (0x40012000)
        libcurses.so.1 => /usr/lib/libcurses.so.1 (0x400d4000)
bash#

Hea küll, te võite küsida, aga kui mul ka arvutis on olemas õutav library, kuidas see binary oskab ta yles leida. Katalooge on ju nii
palju?

Tegelikult on selles suhtes olukord kaunis standartne, või vähemalt võiks/peaks olema. Libraryd peaks asuma kataloogides:

/lib,
/usr/lib
..

aga neid kohti võib ka ise lisada redigeerides faili /etc/ld.so.conf sisu:

/usr/local/lib
/usr/X11R6/lib
/usr/i486-linuxaout/lib
/usr/openwin/lib
/usr/lib
/usr/local/include
/libr

Siia ongi endal lisatud viimane rida.

Seda faili kasutab programm ldconfig mis loob /etc/ld.so.cache mida tegelikult linux library'te leidmisel kasutab. Niisiis, peale selle
kataloogi sisu muutmist või uute libraryte lisamist tuleb käivitada ldconfig. Muuseas selle programmi käivitab linux ka igal boodil.

Nüüd on kõneldud kuidas on asjad valmis binary'te seisukohalt.

Aga kui ise programmeerida ja soovida enda loodud library'eid kasutada?

Enda tehtud valmis binary'te suhe library'tega on samasugune nagu asjakirjeldatud 'tehase' binary'tel. Aga kompilleerimisel tuleb
lisaks tegeleda nn. header failidega. No sellega olete te juba ilmaselt üksjagu tegelenud kah: '#include<stdio.h>' :)

Teadujärgi on '#include' C pre-protsessori käsk mis ütleb talle, et juurde võtta tolle faili sisu millele include viitab. Ja include viitab nn.
header failidele. Need on failid mis jäävad veel kompilleerimata C programmi ja valmis olevate libraryte vahele. Include failis olevad
kirjed peavad põhimõttleiselt kirjeldama (olema kooskõlas) kõnealuse library funktsioonidega. Näite varal on ilmaselt selle süsteemi
selgitamine ja sellest arusaamine lihtasam.

Aga nüüd on jälle see küsimus, et kus kohal hoiab linux neid header faile. Tavaliselt kataloogis:

/usr/include

Sellist cache faili nagu library'te jaoks on /etc/ld.so.cache minuteada header'ite jaoks pole.

Olgu öeldud, et kui header fail asub standartses kohas, siis tuleb kirjutada vastav rida

#include<header.h>

ja kui kuskil mujal siis pika teega nt. nii

#include "/libr/header.h"

Ja lõpuks loodetavast töötav näide:

Järgnevas eeldame, et kogu tegevus toimub kataloogis /proov. Osa vahetulemusi tuleb kopeerida ka mujale kataloogidesse, vajadusel
moodustage kataloog.
Programm arvutab liikuja poolt läbitud teepikkuse teades aega ja kiirust ning kirjutab tulemuse ekraanile; selleks kasutab ta library's
'library.liikuja.so' defineeritud funktsiooni 'teepikkus'.

- nö. programmi osas (s.c) on kirjas see kuidas klaviatuurilt andmeid lugeda ja valmis tulemus ekranile väljastada
- library's (library.liikuja.so) on kirjas kuidas omades aega ja teepikkust kiirus leida
- header failis (liikuja.h) on kirjeldatud kasutatavat funktsiooni nii nagu ta on library's olemas

1.
teeme valmis library.liikuja.so
library.liikuja.c:

int teepikkus(int aeg, int kiirus)
    {
        return aeg * kiirus;
    }

bash# gcc -c library.liikuja.c

tulemuseks on library.liikuja.o; see on nn. objektfail millega pole muud peale hakata kui ainult linkida mõne muuga, või teha temast nn.
library; meie puhul tegelikult shared library (so - shared object)

bash# ld -shared -o library.liikuja.so library.liikuja.o

tulemus: library.liikuja.so

Siin on oluline kus see fail asuma hakkab; olgu järgnev kooskõlas eelnenud libraryte jutuga, st. paneme ta kataloogi /libr ja olgu see
kataloog ka kirjas /etc/ld.so.conf failis. Uuendame ka cache'i käsuga ldconfig.

2.
teeme header faili liikuja.h

extern int teepikkus(int a, int b);

3.
koostame lõpuks ka programmi faili tee.c

#include <stdio.h>
#include "/proov/liikuja.h"
main()
{
int aeg, kiirus, vastus;
printf ("Sisestage liikumise aeg sekundites: ");
scanf("%d", &aeg);
printf ("Sisestage liikuja kiirus m/s: ");
scanf("%d", &kiirus);
vastus = teepikkus (aeg, kiirus);
printf ("Liikuja oli teel %d sekundit\n", vastus);
}

ja kompilleerime ta ära:

bash# gcc  -c   tee.b   tee.c    /libr/library.liikuja.so

ja tulemusena peaks tekkima töötav binary tee.b; proovige seda käivitada.

ja, et veendud selles, et ta tõepoolest kasutab meie tehtud libraryt võite tolle korraks mõne muu nime alla kopeerida või anda käsu

bash# ldd tee.b
        /libr/library.liikumine.so => /libr/library.liikumine.so (0x4000b000)
        libc.so.5 => /lib/libc.so.5 (0x4000d000)
bash#
 

Veel märkusi:

Sellega peaks olema programm valmis, ainult teeme veel mõned tähelepanekud.

1.
Näeme, et meie programmi eeldab nagu arvata võis ka libc5'e olemasolu. Tõele au andes peab tunnistama, et siiski saab teha ka
selliseid binary'eid mis ise sisaldavad kõike vajalikku ja põhimõtteliselt eeldavad vaid linuxi kerneli normaalselt versiooni:

Nende tegemine toimub selliselt:

????

2.
so on tegelikult objekt - fail, see tähendab ta on kompilleeritud aga linkimata.  Siiski saab näha mis funktsione ta toetab, käsk 'nm'

bash# nm /libr/library.liikumine.so
000011c0 A _DYNAMIC
000011b4 A _GLOBAL_OFFSET_TABLE_
000011f0 A __bss_start
000011f0 A _edata
000011f0 A _end
000001b4 A _etext
000001a0 t gcc2_compiled.
000001a0 T teepikkus
bash#

3.
Peale selle on hea näha millised libraryd on cache'is sees käsuga

bash# strings /etc/ld.so.cache | grep liikumine
library.liikumine.so
/libr/library.liikumine.so
bash#
 
 

17. nn. Binary Failide kirjutamine ja lugemine

Nagu märgitud käsud fprint() ja scanf() vastavalt kirjutavad ja loevad ascii faile. Aga vahest on vaja kirjutada otse binary vormis.

Mida siiski nö. ascii ja binary fail tähendavad?

olgu meil kolm täisarvu  1,3 ja 7 ja soov neid faili kirjutada C programmi abil.

nn. ascii faili kirjutab järgnev programm:

#include <stdio.h>
main ()
  {
    char a=0, b=1, c=3, d=7;
    FILE *fh;
    fh=fopen ("arvu_fail", "w");
    fprintf (fh, "%d%d%d%d",a,b,c,d);
    fclose(fh);
  }

see programm avab kirjutamiseks faili "arvu_fail"; seega enne käivitamist tuleb too fail tekitada sobivate õigustega:

bash# touch arvu_fail

ja tulemusena täitub fail nende numbritega milles on lihtne joe'ga veenuda. Aga kas fail ikka täitub nende numbritega? Millised bitid seal kirjas on seda näitab programm 'od'

bash# od -x arvu_fail
0000000 3130 3733
0000004
bash#

-x tähendab, et ta näitaks tulemust hexadecimal formaadis ja järjekord on "ristpistes":

30 h = 48 d = '0' ascii
31 h = 49 d = '1' ascii
37 h = 55 d = '3' ascii
33 h = 51 d = '7' ascii

Niisiis, antud juhul sisaldas fail füüsiliselt sellised bitte:

0011 0000 b
0011 0001 b
0011 0111 b
0011 0011 b

ja tegelikult võib neid tõlgendada väga mitut moodi:

- nn. lühikeste ('char' 1 B) märgita täisarvudena mis puhul on failis talletatud arvud:

nelikümmend kaheks
nelikümmend üheksa
viiskümmenda viis
viiskümmend üks

- ascii tabeli sümbolitena (ascii loob vastavuse kümnendarvude ja tuntud graafiliste märkide vahel; tegelikult mõned helid kah)

'0'   '1'   '3'   '7'

Ülakomad rõhutavad asjaolu, et tegemist on ümbolite mitte arvudena; kuigi tavaliselt neid sümboleid käsitletakse arvudena.

- 2 B ehk 'short' märgita täisarvudena:

0011 0000  0011 0001 b = 4 160 d
0011 0111  0011 0011 b = 5 939 d

- 4 B ehk 'int' märgita täisarvudena

0011 0000  0011 0001   0011 0111  0011 0011 b = ... d

arvutage ise :)

Seega, mis failis kirjas on on paljuski tõlgendamise küsimus.

Probleem tekib seal, kui peab salvestama faili ainult arve. Kas mõistlikum on arve (nt. 0137) salvestada nagu meie programm seda tegi salvestades tegelikult numbritele vastavaid ascii koode või lausa otse meie arvudele vastavaid bitte? Arvatakse, et põhimõtteliselt loomulikum on arve salvestada otse neile vastavate bittidena. See eeldab, et kõik salvestatavad arvud esitatakse nö. võrdse pikusega. St, et iga salvestatave arvu kirjutame kasutades nt. 2 B'd. Mõne arvu puhul me ilmselt raiskame osa bitte ära aga mis teha. Muide, kokkupakkijad seda just ära kasutavadki.
 

Kuidas kirjutada arve otse faili ehk kuidas kirjutada binaarfaile?

Arvud 48, 49, 55, 51 kirjutab 1B kaupa faili selline programm:
(NB! 'touch arvu_fail2')

#include <stdio.h>
main()
        {
        char a=48, b=49, c=55, d=51;
        FILE *fh;
        fh=fopen("arvu_fail2","w");
        fwrite(&a,sizeof(a),1,fh);
        fwrite(&b,sizeof(b),1,fh);
        fwrite(&c,sizeof(c),1,fh);
        fwrite(&d,sizeof(d),1,fh);
        fclose(fh);
        }

Ehk on ka siin huvitav kasutada 'od' d:

bash# od -a arvu_fail2
0000000   0   1   7   3
0000004
bash#

Tegelikult on fwrite ja fread suhteliselt nö. madalal töötavad asjad ja neid tuleb kasutada teadlikumalt kui mina siin näitasin. Aga toodud näited töötavad ja peaksid selgitama põhimõtet.

Ja veel üks näiteprogramm mis oskab ka lugeda.

#include <stdio.h>
struct rec
    {
         char x;
    };

main()
    {
        int i,j;
        FILE *f;
        struct rec r;
        f=fopen("junk","w");
        for (i=0;i<=5; i++)
          {
              r.x=65+i;
              fwrite(&r,sizeof(struct rec),1,f);
          }
        fclose(f);

        f=fopen("junk","r");
        for (i=0;i<=5; i++)
           {
              fread(&r,sizeof(struct rec),1,f);
              printf("%d\n",r.x);
           }

        fclose(f);
        printf("\n");
        }
 
 

Ja esialgu ongi kõik,

kui aega saan ja oskusi juurde tuleb siis oleks loomulik kõnelda teemadest:

1. mitmemõõtmelised massiivid,
2. binary tree
3. rekursioon
...