Programski jezik C
Izvorni in izvršni program – Minimalni program – Skalarne
spremenljivke – Skalarna polja in tekstovni nizi – Strukturirane
spremenljivke – Logični testi in krmilni stavki – Zunanje
funkcije – Izbrane zunanje funkcije – Lastne funkcije –
Dinamična rezervacija pomnilnika – Standardni vhod in izhod –
Datotečni vhod in izhod – Programski argumenti in stikala –
Optimizacija – Predprocesiranje programa – Program v več
datotekah – Prevajanje in povezovanje – Lastne knjižnice
Izvorni in izvršni program
Izvorni program je zaporedje smiselnih ukazov, ki jih
napišemo z editorjem v tekstovno datoteko. Ukazi morajo
biti taki, da so razumljivi prevajalnemu programu za jezik
C, prevajalniku.
Prevajalnik je program, ki, ko ga poženemo iz ukazne
vrstice, čita izvorni program, iz njega izdela izvršni
program in ga zapiše v binarno datoteko. Izvršni program
vsebuje ukaze, ki so razumljivi računalnikovemu procesorju.
Nekateri teh ukazov so klici na druge izvršne programe v
ustreznih knjižnicah.
Izvršni program zaženemo iz ukazne vrstice. Ukazna lupina,
bash, ga naloži v pomnilnik. Če je potrebno, naloži tudi
ustrezne knjižnice. Potem začne računalnik delati po
naloženem programu.
Minimalni program
Minimalni program je zapisan v eni datoteki:
/* Minimal program */
int main(void)
{
return 0;
}
Vse, kar je med oznakama /* in */, prevajalnik ignorira. To
je komentar, namenjen le bralcu izvornega programa.
Program ima obliko "funkcije", sestavljene iz "glave"
in "telesa" med znakoma { in }.
Glava vsebuje ime funkcije ter njene vhodne in izhodne
argumente. Ime funkcije mora biti main. Vhodni argument void
pomeni, da funkcija main pri zagonu ne pričakuje
ukaznovrstičnih parametrov. Izhodni argument int pomeni, da
bo funkcija ob zaključku dostavila lupini celoštevilčni
indikator, kako je svoje delo opravila.
Telo vsebuje zaporedje ukazov, v zgornjem primeru le enega.
Vsak ukaz se zaključi s podpičjem. Ukaz return pomeni, naj
funkcija konča z delom in v lupinsko spremenljivko ?
zapiše vrednost 0.
Skalarne spremenljivke
Lokacije v prevedenem programu rezerviramo in poimenujemo z
ukazi:
char c;
int i, j;
float x, y, z;
S char deklariramo spremenljivko v dolžini 1 zlog, v katero
bomo spravljali cela števila -127..127. Int deklarira 4
zloge za cela čtevila -32767..32767, float pa 8 zlogov za
realna števila na intervalu +-1e-37..1e37 (z natančnostjo
na 6 cifer). Na nekaterih računalnikih so rezervirane
dolžine daljše.
Ime spremenljivke lahko vsebuje črke in številke. Na prvem
mestu mora biti črka. Ime ne sme presegati 31 znakov.
Prevajalnik razlikuje male in velike črke. Vse
spremenljivke, ki jih bomo v programu uporabljali, morajo
biti predhodno deklarirane.
Vsebina deklarirane spremenljivke je sprva neznana. V
programu vanje shranjujemo vrednosti:
c = 100;
i = 1000;
x = 3.14;
y = 5.6e-5;
Spremenljivke polnimo tudi iz drugih že napolnjenih
spremenljivk ali njihovih matematičnih izrazov;
precedenčna pravila so ista, kot v matematiki: najprej
množenje in deljenje, nato seštevanje in odštevanje.
Drugače uporabimo oklepaje.
j = i;
z = (1+x)*(1-x)/y;
Aritmetični operatorji:
+ seštevanje
- odštevanje
* množenje
/ deljenje
dve celi števili dasta celoštevilčni kvocient
% modulus
dve celi števili dasta celoštevilčni modulus
Vrednost izraza mora biti regularna: deljenje z 0 ni
dovoljeno; vrednost spremenljivke ali izraza mora biti
znotraj veljavnega območja; desna stran mora biti enakega
tipa kot leva, oziroma njegov podtip (float <- int <- char).
Matematične funkcije (glej nadaljevanje) vračajo tip
double, ki je nadtip tipa float z več številkami v mantisi
in eksponentu. Double se pri zapisu v float ustrezno
reducira.
Logični izrazi imajo vrednost 1 (true) ali 0 (false);
i = (x < y);
Relacijski in logični operatorji:
< manjši
<= manjši ali enak
> večji
>= večji ali enak
== enak
!= neenak
&& logični AND
|| logični OR
Relacijski operatorji so nižje prioritete kot aritmetični.
Logična operatorja sta nižje prioritete kot relacijski.
Skalarna polja in tekstovni nizi
Enodimenzionalno polje N skalarjev, recimo float,
rezerviramo in poimenujemo z ukazom:
float a[N];
Posamezni elementi so numerirani od 0 do N-1. Prvi element
je dosegljiv kot a[0] in zadnji kot a[N-1]. Posamezne
elemente uporabljamo prav tako kot navadne skalarne
spremenljivke. Posebno skalarno polje je enodimenzionalni
niz tipa char, tekstovni niz:
char s[N];
Elemente polnimo na poseben način:
s[1] = 'A';
Prevajalnik pretvori črko A po kodu ASCII v 65. Nek element
mora biti enak '\0' in s tem označuje konec teksta v nizu.
Dvodimenzionalno polje z M x N elementov:
float a[M][N];
Numeriranje, dostopnost in uporaba posameznih elementov je
analogna.
Strukturirane spremenljivke
Standardni skalarni tipi spremenljivk so char, int in float.
Iz njih lahko sestavim poljuben nov strukturiran tip, na
primer tip complex:
typedef struct {
float re;
float im;
} complex;
Spremenljivko tipa complex deklariram potem kot
complex z;
Posamičen element spremenljivke z je dostopen, na primer,
kot z.re. Elemente uporabljamo kot navadne skalarne
spremenljivke. Strukturno spremenljivko lahko v celoti
kopiramo v drugo.
Poleg skalarnih sestavnih delov lahko struktura vsebuje tudi
polje ali drugo strukturo. Tvorimo lahko tudi polje
struktur.
Logični testi in krmilni stavki
Ukazi v funkciji se izvajajo zaporedno. Tok lahko uravnavamo
s krmilnimi stavki.
/* N-kratno ponavljanje */
for (i=1; i<=N; i=i+1) {
…
}
Enkrat samkrat, na začetku, se izvede prvi del i=1. Potem
se testira i<=N. Če je true, se izvede telo, sicer
preskoči. Na koncu izvedbe telesa se izvede tretji del
i=i+1 in ponovi test drugega dela.
/* Pogojno izvajanje */
if (BOOL) {
…
}
Testira se logični izraz BOOL, recimo x < 10. Če je true,
se izvede telo, sicer preskoči.
/* Pogojna razvejitev v dve veji */
if (BOOL) {
…
}
else {
…
}
Testira se BOOL. Če je true, se izvede prvo telo, sicer drugo.
/* Pogojna razvejitev v več vej */
if (BOOL1) {
…
}
else if (BOOL2) {
…
}
else {
…
}
Testira se BOOL1. Če je true, se izvede prvo telo, sicer se
testira BOOL2 in ustrezno ravna naprej.
/* Pogojno ponavljanje */
while (BOOL) {
…
}
Testira se BOOL. Če je true, se izvede
telo in ponovno testira izraz BOOL. Če je false, se telo
preskoči in nadaljuje s prvim naslednjim ukazom.
/* Neskončno ponavljanje s prekinitvijo */
for (;;) {
…
if (BOOL) break;
…
}
Zunanje funkcije
Program lahko uporablja že prevedeno kodo, zapisano v
knjižnici libc ali kaki drugi. To so zunanje funkcije.
/* Program with external functions */
double sin(double);
double modf(double, double*);
int strcpy(char*, char*);
int main(void)
{
float x, y, z;
char s[10];
x = 3.14;
y = sin(x); /* Return sine of x */
z = modf(y,&x); /* Return fractional part of y,
store integer part in x */
strcpy(s,"HELLO"); /* Copy HELLO to s */
}
Funkcija prejema od glavnega programa podatke preko svojih
argumentov, zapisanih med ( in ). Glavni program funkciji
posreduje kopijo vsebine skalarne spremenljivke (x) ali njen
dejanski naslov (&x). Funkcija potem s tem računa. Glavnemu
programu vrne svojo vrednost, hkrati pa lahko spremeni tudi
vsebino naslovljene spremenljivke. Funkcije vračajo le
skalarne vrednosti. Glavni program jih lahko shrani ali
ignorira. Skalarne spremenljivke lahko posredujemo funkciji
kot kopije ali kot naslove, druge spremenljivke pa zgolj kot
naslove prvega elementa. Pri tem velja sintaktična
olajšava dvomljive sorte, da namreč naslov vektorske
spremenljivke &s[0] posredujemo kot s, ne kot &s.
Glave vseh klicanih funkcij – funkcijski prototipi – morajo
biti zapisane na začetku programa. Tako prevajalnik pri
vsakem klicu funkcije lahko preveri, če je klic pravilen.
Izbrane zunanje funkcije
Funkcije za delo s sistemom:
system("ls") izvede lupinski ukaz ls
Funkcije za delo z nizi. Argumenta s in t sta niza:
strcat(s,t) doda t na konec s
strcmp(s,t) vrne 0, če enaka
strcpy(s,t) kopira t v s
strlen(s) vrne dolžino s
Matematične funkcije. Vsi argumenti so double, vse funkcije
vračajo double:
sin(x) sinus
cos(x) kosinus
tan(x) tangens
asin(x) arcus sinus
acos(x) arcus kosinus
atan(x) arcus tangens
exp(x) eksponentna funkcija
log(x) logaritem
log10(x) desetiški logaritem
pow(x,y) x^y
sqrt(x) kvadratni koren
fabs(x) absolutna vrednost
Lastne funkcije
Večji program je iz očitnih razlogov sestavljen iz
funkcije main in iz zaporedja drugih funkcij, ki jih main
uporablja (kliče) neposredno ali posredno (iz drugih
funkcij). Vrstni red ni predpisan. Smiselno je postaviti
main na prvo mesto. Izvajati se začne main.
/* Program with internal functions*/
float funct1(float, float);
int funct2(float*, float);
int funct3(float*, int);
int funct4(float**, int, int);
int main(void)
{
int n;
float a, b, c;
float A[10], B[10][10];
n = 10; a = 1.0; b = 1.0;
A[1] = 1.0; B[1][1] = 1.0;
c = funct1(a,b); /* c becomes 2.0 */
funct2(&a,b); /* a becomes 2.0 */
funct3(A,n); /* A[1] becomes 2.0 */
funct4(B,n,n); /* B[1][1] becomes 2.0 */
return 0;
}
float funct1(float x, float y)
{
float z;
z = x+y;
return z;
}
int funct2(float* x, float y)
{
*x = *x+y;
return 0;
}
int funct3(float* X, int N)
{
X[1] = X[1]+1.0;
return 0;
}
int funct4(float** X, int M, int N)
{
X[1][1] = X[1][1]+1.0;
return 0;
}
Glave vseh klicanih funkcij – funkcijski prototipi – morajo
biti zapisane na začetku programa.
Deklaracije spremenljivk znotraj funkcije so poznane le
znotraj funkcije, so lokalne.
Glavni program ob klicu pošlje funkciji funct1 kopijo
vsebine svoje spremenljivke a, ki se zapiše v funkcijino
spremenljivko x. Originalna spremenljivka a funkciji ni
dostopna. Če funkcija spremeni vsebino x, se vsebina a ne
spremeni.
Glavni program funkciji funct2 pošlje naslov svoje
spremenljivke a, in sicer kot &a. Ta naslov je neko
kardinalno število. Funkcija ga shrani v svojo kazalčno
spremenljivko x. Originalna spremenljivka a je sedaj
dostopna funkciji, in sicer kot *x (Wirth bi jo naredil
dostopno kar kot x). Če funkcija spremeni vsebino *x, s tem
spremeni vsebino a.
Funkciji funct3 posredujemo enodimenzionalno skalarno polje
lahko le kot naslov prvega elementa &A[0]. Okrajšamo ga
lahko v A (Wirth bi ga okrajšal v &A). Originalni element
A[i] je dosegljiv kot X[i].
Funkciji funct4 posredujemo dvodimenzionalno skalarno polje
lahko le kot naslov prvega elementa &B[0][0]. Okrajšamo ga
lahko v B. Originalni element B[j][i] je dosegljiv kot
X[j][i].
Funkcija programu main vrača vrednost preko svojega stavka
return. Vrnjene vrednosti lahko ignoriramo. Funkcija je
lahko brez stavka return; tedaj jo deklariramo kot tipa void.
Dinamična rezervacija pomnilnika
Ni treba, da že prevajalniku povemo, koliko pomnilnika naj
rezervira za spremenljivke. To lahko naredimo šele v času
izvajanja programa; prostor rezerviramo, uporabimo in
sprostimo.
float* p;
p = (float*)malloc(sizeof(float));
if (p == NULL) {
printf("Error: Out of memory\n");
exit(1);
}
*p = 1.0;
free(p);
Spremenljivka p je kazalec na spremenljivko tipa float, ki
jo želimo ustvariti. Uporabimo lahko poljuben skalarni tip
ali strukturo. Funkcija malloc rezervira del pomnilnika s
pravšnjo dolžino. Naslov novoustvarjene spremenljivke vrne
v p. Če pomnilnika ni možno rezervirati, ker ga morda ni
na voljo, je vrnjeni naslov NULL (0) in program se prekine.
Na novo ustvarjena sprmenljivka je dostopna kot *p. Na koncu
rezervirani pomnilnik sprostimo s funkcijo free.
Rezerviramo lahko vektor n elementov tipa float ali kakega
drugega skalarnega tipa ali strukture:
float* p;
n = 10;
p = (float*)calloc(n,sizeof(float));
if (p == NULL) {
printf("Error: Out of memory\n");
exit(1);
}
p[1] = 1.0;
free(p);
Standardni vhod in izhod
Posamezen znak pošljemo v zaslonov medpomnilnik (buffer)
kot putchar(c). Tam se znaki nabirajo. Na zaslon se
izpišejo, ko v medpomnilnik pošljemo znak '\n'.
Posamezen znak čitamo iz medpomnilnika tipkovnice kot c =
getchar(). Znaki v medpomnilniku postanejo dostopni, ko
odtipkamo ENTER.
Niz znakov pošljemo na zaslon kot puts(s). Funkcija
avtomatsko doda '\n'.
Niz znakov čitamo s tipkovnice kot gets(s). Prebrani '\n'
se nadomesti z '\0'.
Nize in števila kot nize izpisujemo na zaslon s funkcijo
printf("%s %d %f %e\n",c,s,i,x,y);
Funkcija izpiše niz s, celo število i in realno število
x, vse v standardnem formatu. Med sabo jih loči s
presledki. Na koncu doda '\n'. Formate lahko določimo tudi
bolj natančno:
%5s Minimalna širina polja 5 znakov
%7d Minimalna širina polja 7 znakov
%8.2f Min širina 8, od tega 2 decimalki
%8.2e Min širina 8, od tega 2 decimalki mantise
Vsi izpisi so desno poravnani v svojih poljih. Negativne
formatirne številke, recimo %-8.2f, pomenijo levo
poravnavo.
Nize in števila kot nize čitamo s tipkovnice s funkcijo
scanf("%s %d %f %e",s,&i,&x,&y);
Funkcija čita, dokler zahteva format. Eno vhodno polje je
zaporedje ne-belih znakov (beli znaki so SPACE, TAB, LF, CR,
FF). To pomeni, da funkcija lahko čita vhod tudi v
naslednji vrstici.
Namesto formatnega izpisa na zaslon lahko formatiramo v niz
s in potem niz zapišemo na zaslon:
sprintf(s,FORMAT,VARLIST);
puts(s);
Namesto formatnega čitanja iz tipkovnice lahko čitamo v
niz s in potem tega razčlenjujemo:
gets(s);
sscanf(s,FORMAT,VARLIST);
Datotečni vhod in izhod
FILE* f;
FILE* g;
f = fopen("input.txt","r");
g = fopen("output.txt","w");
…
fclose(f);
fclose(g);
Spremenljivka f je kazalec na vhodno datoteko, g na izhodno.
Prva datoteka je odprta kot tekstovna (znaki 0..127) za
branje, "r". Druga je tekstovna za pisanje, "w". Branje in
pisanje binarne datoteke (znaki -127..127) je možno z "rb"
in "wb". Na koncu datoteki zapremo s fclose.
Branje in pisanje datotek po znakih:
char c;
for (;;) {
c = fgetc(f);
if (c==EOF) break;
fputc(c,g);
}
Čitamo zlog za zlogom s funkcijo fgetc. Vsak zlog sproti
zapišemo na izhodno datoteko s putc. Končamo, ko v vhodnem
toku znakov naletimo na znak EOF (tipično -1).
Branje in pisanje tekstovnih datotek po nizih:
char s[80];
for (;;) {
fgets(s,sizeof(s),f);
if (s==NULL) break;
fputs(s,g);
}
Funkcija fgets prečita niz znakov do vključno prvega LF
ali največ 79 znakov ter zapiše v s (brez LF). Funkcija
fputs prepiše s na izhod in doda LF.
Formatirano branje in pisanje tekstovnih datotek:
int i;
float x;
for (;;) {
i = fscanf(f,"%e\n",&x);
if (i==EOF) break;
fprintf(g,"%e\n",x);
}
Branje in pisanje niza poljubnih elementov:
int i;
complex z[N];
i = fread(z,sizeof(z),N,f);
i = fwrite(z,sizeof(z),N,g);
Funkcija prečita/zapiše N elementov tipa complex in vrne
število prebranih/zapisanih elementov.
Programski argumenti in stikala
Pri klicu programa mu lahko damo parametre, recimo imena
datotek, ki jih naj obdeluje.
int main(int argc, char** argv)
{
…
}
Število argumentov je v argc. Argumenti so nizi. Prvi niz,
argv[0], vsebuje ime programa, ostali njegove
ukaznovrstične parametre.
Optimizacija
Pišemo z berljivostjo kot prvim ciljem. Prireditvene stavke
pišemo posebej in jih ne vključujemo v druge izraze.
Šele če prevedeni program ni dovolj hiter ali je prevelik,
začnemo optimizirati. Najprej optimiziramo najbolj
kritično točko in vidimo, ali zadostuje.
Iz zanke vzamemo vse operacije, ki se ne spreminjajo.
Uporabljamo poceni operacije namesto dragih:
==================================
Operacija Relativna cena
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
printf, scanf 1000
malloc, free 800
trig function 500
float operation 100
integer * / 20
function call 10
integer + - 5
pointer dereference 2
&& || ! 1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Predprocesiranje programa
Tekstovni predprocesor preoblikuje tekst, nakar ga posreduje
prevajalniku. Ukazne vrstice, namenjene predprocesorju, se
začno z znakom #.
Vse nize MAX, ki jih najde v programu, nadomesti z nizom
100:
#define MAX 100
float x[MAX];
Na izbrano mesto vstavi funkcijske prototipe iz zunanje
datoteke myfile.h v lokalnem ali kakem drugem imeniku:
#include "myfile.h"
#include "/home/local/include/myfile.h"
Na izbrano mesto vstavi funkcijske prototipe iz zunanje
datoteke stdio.h v standardnem sistemskem imeniku:
#include <stdio.h>
Program v več datotekah
Večje programe je zaradi preglednosti in hitrejšega
prevajanja smiselno zapisati v več izvornih datotek. V prvo
datoteko gre glavni program, v drugo prototipi njegovih
lastnih funkcij. Podprogrami se zapišejo v eno ali več
dodatnih datotek; v vsaki je po ena ali več funkcij.
Glavni program uvaža (#include) datoteko s prototipi
funkcij. Isto velja za vsako datoteko s funkcijami.
V datoteki prototipov je vsak prototip označen kot
zunanji, na primer
extern float funct1(float, float);
Prevajanje in povezovanje
Program v eni datoteki:
bash> gcc -Wall -ansi -pedantic -lm -g -o EXEFILE SRCFILE
Stikalo -Wall povzroči izpisovanje vseh opozoril pri
prevajanju. Stikali -ansi in -pedantic preverjata, ali je
program skladen s standardom ANSI ter opozorita na
neskladja. Stikalo -l povzroči povezovanje s knjižnico
libm.so. To je potrebno, če izvorni program vsebuje
funkcije iz te knjižnice. Eksplicitno je treba navesti vse
potrebne knjižnice razen osnovne, libc.so. Stikalo -g
ukazuje, naj bodo v izvršnem programu vključeni dodatni
ukazi, potrebni za iskanje in odpravljanje morebitnih napak
(razhroščevaneje).
Program v več datotekah prevajamo po delih in na koncu
povežemo. Najprej prevedemo datoteko(e) s podprogrami:
bash> gcc -c util.c
Naredi se knjižnica util.o. Nato prevedemo datoteko z
glavnim programom:
bash> gcc -c main.c
Naredi se main.o. Na koncu vse skupaj povežemo:
bash> gcc -o main main.o util.o
Naredi se izvršni program main. Če kasneje kaj spremenimo
v eni izmed datotek, je potrebno prevesti zgolj to datoteko
ter vse skupaj znova povezati.
Lastne knjižnice
Včasih je smiselno narediti lastno knjižnico podprogramov.
Naredimo jo iz dveh izvornih datotek; v eni so prototipi
funkcij, v drugi pa funkcije same. Slednja mora tudi
uvažati datoteko s prototipi.
Knjižnico naredimo takole:
bash> gcc -fPIC -Wall -ansi -pedantic -g -c libutil.c
bash> gcc -g -shared -o libutil.so.M.N libutil.o
Najprej iz izvorne datoteke naredimo predmetno. Stikalo
-fPIC naredi kod, ki ga lahko sočasno uporablja več
programov. Nato iz predmetne datoteke naredimo deljeno
izvršno; to pove stikalo -shared. Ime se mora začeti z
lib. Dobljeno knjižnico zapišemo v standardni imenik,
recimo /usr/lib, ustvarimo standardni povezavi nanjo ter
osvežimo seznam razpoložljivih knjižnic:
bash> ln -s libutil.so.M.N libutil.so.M
bash> ln -s libutil.so libutil.so.M
bash> ldconfig
Ko je knjižnica teko narejena in nameščena, jo
uporabljamo kakar vsako drugo knjižnico.