teejuht
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.
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;
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Peale nende on veel kolm (tegelikult on neid neli) AND - &&,
OR - ||, NOT -!, (XOR)
neid tehteid iseloomustab siin toodud tabel:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
A 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!
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;
}
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!
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:
|
omadustega objekt: |
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 |
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
...