Versiunea 2.0
Manual de utilizare
STRUCTURA GENERALĂ A UNUI PROGRAM C
1.1. ISTORIC, CONCEPŢIE, EVOLUŢIE
Limbajul C a fost finalizat în 1972 de Dennis M. Ritchie i Brian W.
Kernighan de la firma americană Bell Laboratories.
Prima versiune a limbajului se numeşte BCPL apoi următoarele poartă
numele de A, B şi C. Cei doi autori au dezvoltat aceste prime versiuni
în jurul sistemului de operare UNIX.
La vremea respectivă din aproximativ 13000 linii sursă ale UNIX-ului
doar 200 de linii sursă nu erau scrise în limbajul C. De acest fapt
se leagă detractorii limbajului care spun că limbajul C nu este un
limbaj deosebit ci doar un fel de limbaj “oficial” al sistemului de
operare UNIX.
În anul 1978 apare manualul The C Programming Language care este de
fapt şi prima standardizare a limbajului. Cei doi autori intră astfel
în istorie...
După anul 1980 odată cu dezvoltarea hardware apar şi primele PC-uri
iar acestea implică şi produse software adecvate. Principalele firme
producătoare de sofware -MICROSOFT şi BORLAND - au dezvoltat unelte
adecvate pentru programarea şi utilizarea limbajului C. Deocamdată
firma BORLAND deţine supremaãţia prin versiunile mediului BORLAND C.
Cele mai folosite sunt versiunile 2.0, 3.1, 4.0. În ultimii doi ani au
apărut aşa numitele medii “visuale”: VISUAL C versiunile 4.5 şi
5.0 care sunt de fapt extensii ale mediului BORLAND C adaptate
programării orientate obiect şi interfeţei grafice WINDOWS 95.
Mediile de programare BORLANDC pot compila 3 tipuri de programe sursă
C:
fişiere cu extensia .C (fişiere cu prg. standard C);
fişiere cu extensia .CP (fişiere cu prg. C+,);
fişiere cu extensia .CPP (fişiere cu programe C++).
Menţionăm că limbajul C++ a fost elaborat de Bjarne Stroustrup de la
AT&T. El este un superset al limbajului C şi permite principalele
concepte ale programării prin abstractizarea datelor şi programării
orientate spre obiecte.
Limbajul C este un limbaj hibrid având facilităţi caracteristice
limbajelor de asamblare cât şi facilităţi ale limbajelor de înalt
nivel.
Câteva dintre principalele caracteristici ale limbajului C sunt:
• portabilitate: chiar dacă acest concept nu-i definit foarte
riguros spunem că un program este portabil dacă el poate fi
transferat uşor de la un tip de calculator la altul; limbajul C este
un astfel de limbaj;
• flexibilitate: compilatorul face un număr mai redus de controale
(face multe conversii implicite);
• programare structurată: limbajul are principalele structuri ale
programării structurate: structura secvenţială, structura iterativă
şi structura de selecţie;
• compactizare: unele instrucţiuni sunt scrise foarte compact; de
exemplu i:=i+1 se poate scrie mai scurt ca i++;
• lucrul pe biţi şi calcule cu adrese.
CONCEPTUL DE FUNCŢIE
Un program C se compune din una sau mai multe funcţii. Funcţia este o
unitate lexicală de program compilabilă independent.
Una dintre funcţii este funcţie principală, numele ei este
predefinit şi anume main.
Execuţia programului începe cu prima instrucţiune din funcţia
principală.
Dăm în continuare 2 exemple de structuri de program (fişiere
sursă):
[directive de preprocesare]
[declaraţii de date globale]
[declaraţie prototip funcţia f1
. . .
declaraţie prototip funcţia fn]
[Void main (void)
{ declaraţii
instrucţiuni instrucţiuni
}]
[Implementare funcţia f1]
. . .
[Implementare funcţia fn]
[directive de preprocesare]
[declaraţii de date globale]
[declaraţie prototip funcţia f1
. . .
declaraţie prototip funcţia fn]
[Void main (void)
{ declaraţii
instrucţiuni instrucţiuni
}]
[implementare funcţia f1]
. . .
[implementare funcţia fn]
Funcţia principală main este obligatorie pentru orice program
celelalte elemente fiind optionale. Pentru ca o anumită funcţie să
poată fi apelată e necesar ca ea să aibă declarat prototipul dacă
implementarea (definiţia) ei se află după funcţia main (exemplul
1). Dacă funcţia principală se află la sfârşitul fişierului
atunci nu mai e necesar prototipul funcţiei apelate ci doar
implementarea ei (exemplul 2). Comparând structura unui program C cu
structura unui program PASCAL se observă nivelul de imbricare diferit.
În PASCAL apare o imbricare a procedurilor şi funcţiilor pe când
în C nu există o astfel de imbricare (rămâne la nivelul fiecărei
funcţii dacă e cazul).
PASCAL C
...
1.2.1. Definiţia unei funcţii
Definiţia unei funcţii în limbajul C se compune din antet şi corp.
O funcţie poate fi apelată dacă este precedată de definiţia sau de
prototipul ei. Aceste lucruri care sunt valabile în limbajul C se
regăsesc şi în limbajul C++.
Antet şi prototip
Antetul simplificat al unei funcţii în C are formatul:
tip nume_funcţie (lista_parametrilor_formali)
unde: tip - reprezintă tipul valorii returnate de funcţie sau dacă
funcţia nu
returnează nici o valoare se pune cuvântul cheie void;
nume_funcţie - reprezintă un identificator clasic format dintr-un
mixaj
de litere şi cifre, primul caracter fiind obligatoriu litera;
printre litere se numără şi liniuţa de subliniere;
lista_parametrilor_formali – nume de variabile separate prin virgule.
Exemple:
1) double radical (double x) // calculeaza radacina patrata din x si
returneaza valoarea gasita
2) double radical_n (double x, int n) // calculeaza radacina de ordinul
n din x
Prototipul unei funcţii este asemănător antetului dar la sfârşit
se pune caracterul “;”
1.2.3. Corpul unei funcţii
Corpul unei funcţii C se compune din declaraţii de variabile locale
şi instrucţiuni scrise între acolade conform figurii de mai jos.
{ declaraţii
instrucţiuni
}
Şi pentru că autorii limbajului C consideră că un limbaj de
programare se învaţă mai repede scriind şi executând programe căt
mai timpuriu vom da un mic exemplu de funcţie.
int modul (int i) // determina valoarea absoluta a intregului i si
retruneaza aceasta valoare
{ if (i < 0) return –i;
if (i = = 0) return 0;
else return i;
}
1.3. CONSTRUCŢIILE DE BAZĂ ALE LIMBAJULUI
1.3.1. Caractere
Limbajul C foloseşte setul de caractere al codului ASCII care se
codifică prin numere întregi din intervalul [0,127], adică 128 de
coduri. Un astfel de întreg se păstrează pe un BYTE (OCTET) adică
pe 8 biţi.
Mulţimea caracterelor se împarte în trei grupe:
- caractere negrafice (coduri cuprinse între 00=NUL şi 31) şi
127=DEL;
- spaţiu (codul 32);
- caractere grafice (coduri cuprinse între 33 şi 126).
Caracterele grafice se împart la rândul lor în:
litere mari (coduri între 65 şi 90);
litere mici (coduri între 97 şi 122);
cifre (coduri între 48 şi 59);
caractere speciale (celelalte coduri).
Caracterele negrafice au diferite funcţii. Astfel codul 10 semnifică
LF (Line Feed), adică deplasează cursorul pe linia următoare în
coloana 1, iar codul 13 semnifică CR (Carriage Return) adică
deplasează cursorul în coloana 1 aceeaşi linie. În limbajul C
combinaţia celor două caractere se notează prin:
\n
şi are semnificaţia trecerii cursorului la linie nouă şi coloana 1
(newline).
Tabulatorul orizontal (deplasarea cursorului peste un anumit număr de
poziţii) se precizează prin notaţia:
\t
Să mai precizăm că următoarele 3 caractere: spaţiu, newline şi
tabulator orizontal se mai numesc spaţii albe (white spaces).
1.3.2. Nume
În limbajul C, un nume este o succesiune de litere şi eventual
cifre, care începe cu o literă. Ca lungime un nume poate fi oricât
de lung dar numai primele 32 de caractere se iau în considerare.
Folosind notaţia BNF (vezi anexa) un nume se poate defini recursiv, ca
mai jos:
<nume> ::= <litera> | <nume><litera> | <nume><cifra>
<litera> ::= A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Z|
a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|z|_
<cifra> ::= 0|1|2|3|4|5|6|7|8|9|
Numele se folosesc pentru denumirea variabilelor, tablourilor,
funcţiilor, etc.
Exemple:
A, _start, a_, matrice, matrice_patratica.
Dăm şi câteva contraxemple:
&B - conţine caracterul &;
x+y - conţine caracterul +;
1.3.3. Cuvinte cheie
Un cuvânt cheie este un cuvânt împrumutat din limba engleză, care
are un înţeles predefinit. Aceste cuvinte se scriu cu litere mici. Un
cuvânt cheie nu poate avea altă utilizare într-un program C decât
cea care i-a fost predefinită. Fiind o succesiune de litere, un
cuvânt cheie este un nume. Lista cuvintelor cheie se dă în anexa de
la sfârşitul cărţii. Sensul fiecărui cuvânt cheie va rezulta la
definirea construcţiei în care se utilizează.
Exemple:
for, do, if, while, else, break, return, int, long, double, static,
extern.
1.3.4. Tipuri de bază
În limbajul C distingem câteva tipuri predefinte de date care se mai
numesc tipuri de bază. În general un tip de dată are trei atribute
bine conturate:
mulţimea valorilor;
reprezentarea (internă şi externă)
comportamentul (adică operaţiile ce se pot face cu acel tip).
În tabelul de mai jos dăm cuvântul cheie, mulţimea valorilor,
reprezentarea internă.
Reprezentarea internă
Cuvânt cheie Mulţimea valorilor Lungime (biţi) Formatul intern
int întregii din [-215; 215-1] 16 reprezentare binară
short întregii din [-215; 215-1] 16 reprezentare binară
long întregii din [-231; 231 - 1] 32 reprezentare binară
unsigned întregii din [0; 216-1] 16 reprezentare binară
char 8 cod ASCII
float [-3.4*10-38; 3.4*1038] 32 virgulă flotantă simplă precizie
double [-1.7*10-308; 1.7*10308] 64 virgulă flotantă dublă
precizie
Facem precizarea că se poate folosi şi combinaţia unsigned long
pentru întregii fără semn în dublă precizie.
Cu tipurile de bază se pot defini şi tipuri utilizator folosind
instrucţiunea struct.
1.3.5. Constante
O constantă are un tip şi o valoare. Atât tipul cât şi valoarea
unei constante se definesc prin caracterele care compun constanta
respectivă.
Constantă întreagă zecimală
O constantă întreagă este un şir de cifre care eventual este
prefixat de un semn (+ sau -) şi postfixat de litera L sau l. O
constantă întreagă se reprezintă în binar pe 16 sau 32 de biţi.
Sufixul L sau l forţează reprezentarea pe 32 de biţi.
Constantă întreagă octală
O constantă întreagă octală este un şir de cifre octale (cifre
cuprinse între 0 şi 7) precedat de un zero nesemnificativ.
Constantă întreagă hexazecimală
O constantă întreagă hexazecimală este un şir de cifre
hexazecimale prefixat cu 0x sau 0X.
Exemple:
Reprezentare Interpretare
12345 întreg zecimal reprezentat în binar pe 16 biţi
-12345 întreg zecimal reprezentat în binar pe 16 biţi
12345L întreg zecimal reprezentat în binar pe 32 biţi
012345 întreg octal reprezentat în binar pe 16 biţi (o cifră
octală pe 3 biţi)
0xabcd întreg hexa reprezentat în binar pe 16 biţi (o cifră pe 4
biţi)
12345678 întreg zecimal reprezentat pe 32 de biţi
Constantă flotantă
O constantă flotantă este un număr raţional care se compune din
următoarele elemente:
un semn (+ sau -) care poate lipsi pentru numerele pozitive;
o parte întreagă care poate fi şi vidă;
o parte fracţionară care poate fi şi vidă;
un exponent care poate fi şi vid.
Prezenţa părţii fracţionare este suficientă pentru ca numărul
respectiv să reprezinte o constantă flotantă. Absenţa părţii
fracţionare implică prezenţa părţii întregi şi a exponentului.
Partea întreagă reprezintă o succesiune de cifre zecimale. Partea
fracţionară se compune din caracterul punct urmat de o succesiune de
cifre zecimale, care poate fi şi vidă. Exponentul se compune din
litera e sau E urmată de un + sau -, opţional, şi un şir de cifre
zecimale.
Constantele flotante se păstrează în format flotant dublă
precizie.
Exemple:
3.14; 0.314e1; 3.1415926; 2.71828; 2.; 271828E-5.
Constantă caracter
O constantă caracter reprezintă un caracter şi are ca valoare codul
ASCII al caracterului respectiv. O constantă caracter grafic se poate
scrie incluzând caracterul respectiv între caractere apostrof.
Exemple:
Constantă caracter valoare cod ASCII
‘A’ 65
‘B’ 66
‘a’ 97
‘0’ 48
‘9’ 58
‘*’ 77
Anumite caractere negrafice au notaţii speciale la scrierea cărora
se utilizează caracterul backslash (\). Dăm în continuare un tabel
cu cele mai folosite caractere negrafice:
Caracter Reprezentare ca şi constantă caracter Valoare cod ASCII
Backspace ‘\b’ 8
Retur de car ‘\r’ 13
Newline ‘\n’ 10
Apostrof ‘\’’ 39
Backslash ‘\\’ 92
Tabulator vertical ‘\v’ 11
Salt pagină imprimantă ‘\f’ 12
Carcterul NUL ‘\0’ 0
Constantă şir de caractere
O constantă şir de caractere este o succesiune de zero sau mai multe
caractere delimitate prin ghilimele. Ghilimelele nu fac parte din
şirul de caractere.
Exemple:
“123”; “Limbajul C”; “sir de caractere”; “sir\””;
“”(şirul vid).
Constanta şir de caractere se reprezintă în memoria calculatorului
printr-o succesiune de octeţi în care se păstrează codurile ASCII
ale caracterelor şirului, iar ultimul octet conţine caracterul NUL
care marchează sfârşitul şirului de caractere.
După cum se observă din exemplele date putem să folosim şi
convenţia cu backslash.
De reţinut că ‘X’ şi “X” reprezintă construcţii diferite.
1.3.6. Variabile simple
Prin variabilă înţelegem o dată a cărei valoare se poate schimba
în timpul execuţiei programului care o conţine. Unei variabile i se
ataşează un nume prin intermediul căruia o putem referi sau
modifica. Totodată valorile pe care le poate lua o variabilă trebuie
să aparţină aceluiaşi tip. Corespondenţa dintre numele şi tipul
unei variabile se realizează printr-o construcţie specială numită
declaraţie.
După modul de grupare datele sunt:
date izolate denumite şi variabile simple;
date grupate:
- mulţimi ordonate de date de acelaşi tip la care ne referim cu
indici,
- structuri în care date de tipuri diferite se grupează.
Declaraţia de variabilă simplă are formatul general:
tip listă_de_nume;
unde listă_de_nume este formată fie dintr-un singur nume fie din mai
multe, separate prin virgule.
În limbajul C trebuie declarate toate variabilele înainte de a fi
utilizate.
Exemple:
int a,b,c; // a, b, c sunt variabile de tip int reprezentate in binar
pe 2 octeţi;
float x,y,z; // x, y, z sunt variabile de tip float in format flotant
simpla precizie pe 32 de biţi;
char car; // car este o variabila de tip char pe 1 octeti poate
conţine coduri ASCII;
long i,j,k; // i, j, k sunt variabile de tip long in format binar pe
32 de biţi.
Tablouri
Un tablou ca orice variabilă simplă trebuie declarat înainte de a fi
utilizat. Dacă referirea la elementele tabloului se face cu un singur
indice se spune că tabloul este unidimensional (se mai numeşte
vector); dacă referirea se face cu doi indici tabloul se numeşte
bidimensional(matrice); iar cu n indici tabloul este n-dimensional.
Declaraţia unui tablou în forma cea mai simplă este:
tip nume[l1][l2]...[ln];
unde:
l1, l2, ... ln sunt expresii constante care au valori întregi ce pot
fi evaluate de compilator la întâlnirea lor.
Evident că se pot declara mai multe tablouri deodată şi atunci
numele de tablouri se pot înşirui într-o listă, fiecare separat
prin virgulă.
Exemple:
int t[5]; // s-a declarat un tablou unidimensional de 5 componente;
float a[5][3]; // s-a declarat un tablou bidimensional de 5*3=15
componente;
La elementele unui tablou ne referim prin variabile cu indici. O
variabilă cu indici se compune din numele tabloului urmat de unul sau
mai mulţi indici, fiecare indice fiind inclus în paranteze drepte.
Numărul indicilor defineşte dimensiunea tabloului. Indicii sunt
expresii care au valori întregi. Limita inferioară a indicilor este
zero. Astfel dacă t este tabloul de tip int în exemplul de mai sus,
elementele lui sunt:
t[0], t[1], t[2], t[3], t[4].
În cazul matricii a declarate mai sus elementele ei vor fi:
prima linie: a[0][0], a[0][1], a[0][2];
a doua linie: a[1][0], a[1][1], a[1][2];
...
a cincea linie: a[4][0], a[4][1], a[4][2];
Deci pentru dimensiunea k indicele variază între 0 şi lk-1.
Repartizarea memoriei pentru un tablou se face astfel: pentru fiecare
element al tabloului se repartizează o zonă de memorie necesară în
conformitate cu tipul tabloului respectiv (pentru tipul int câte 2
octeţi, pentru tipul float câte 4 octeţi, etc). Numele unui tablou
este un pointer, el poate fi utilizat în diferite construcţii şi el
are ca valoare adresa primului său element.
În desenul următor se indică repartizarea memoriei pentru tabloul t
din exemplul de mai înainte:
t
adresa lui
t[0]
t[0] t[1] t[2] t[3] t[4]
Comentariu
În limbajul C un comentariu se scrie între /* şi */ pe oricâte
rânduri. între /* şi */ se poate scrie o succesiune arbitrară de
caractere, care însă nu poate să conţină secvenţa de terminare
(adică */). Comentariul poate fi inserat într-un program în orice
poziţie în care este legal să apară un caracter alb. El constituie
o explicaţie pentru programator sau pentru utilizator. Compilatorul nu
interpretează comentariul în nici un mod. Comentariul se recomandă a
fi inserat în punctele în care programatorul poate lămuri prin
explicaţii, anumite aspecte ale procesului de calcul sau ale datelor
utilizate. În mediile BORLAND C există şi o altă convenţie de a
marca un comentariu la nivelul unui rând cu ajutorul a două caractere
slash (//), convenţie pe care am folosit-o de altfel în exemplele
anterioare.
1.4. PREPROCESARE
Un program sursă C poate fi prelucrat înainte de a fi compilat. O
astfel de prelucrare se numeşte preprocesare sau precompilare. Acest
lucru se realizează printr-un program special numit preprocesor.
Acesta este apelat automat înainte de a începe compilarea.
Preprocesorul limbajului C realizează următoarele:
includeri de alte fişiere (de obicei fişiere sursă);
definiţii şi apeluri de macrouri simple;
compilare condiţionată.
1.4.1. Includerea unui fişier sursă
Fişierele sursă pot fi incluse cu ajutorul directivei include.
Două formate se folosesc pentru a include un fişier sursă în locul
în care apare directiva (de obicei se pune la începutul programului):
#include “specificator_de_fisier”
sau
#include <specificator_de_fisier>
unde: specificator_de_fişier trebuie să fie un nume de fişier valid
din punct de vedere al sistemului de operare DOS şi de obicei are
extensia “.h” sau “.c”.
Prima variantă este folosită de obicei când fişierele de inclus
sunt furnizate de utilizator; dacă nu este indicată calea atunci
fişierele sunt căutate în directorul curent şi apoi în
directoarele setate pentru directiva include.
A doua variantă este folosită pentru încorporarea unui fişier
standard care se caută în directoarele setate pentru directiva
include (de obicei astfel de fişiere standard sunt livrate în
biblioteci ataşate mediului de programare C).
Odată localizat fişierul dintr-o directivă include se înlocuieşte
aceasta prin textul fişierului sursă. Deci compilatorul nu va mai
întâlni directiva include ci textul fişierului inclus de
preprocesor.
Includerile de fişiere se fac de obicei la început pentru ca textele
fişierelor sursă (date şi funcţii) să poată fi utilizate în tot
fişierul sursă de lucru. De aceea, la începutul fişierelor sursă
vom întâlni mai multe includeri de fişiere standard: stdio.h,
stdlib.h, etc. Textul unui fişier inclus poate la rândul său să
conţină directiva include. Fişierul stdio.h (prescurtarea de la
standard input output header) conţine printre altele şi funcţiile
standard de intrare-ieşire printf şi scanf. Fişierele cu extensia
“.h” se mai numesc şi fişiere header (fişiere care se pun la
începutul fişierului sursă). Un alt exemplu de fişier header este
iostream.h folosit în mediul BORLAND C care conţine funcţiile cin
(console input) şi cout (console output).
1.4.2. Constante simbolice
Cu directiva define se pot defini constante simbolice şi macrouri.
Constantele simbolice se definesc astfel:
#define nume succesiune_de_caractere
Preprocesorul substituie nume cu succesiune_de_caractere peste tot în
fişierul sursă care urmează poziţiei directivei define. Dacă
succesiune_de_caractere nu încape pe un rând atunci se poate continua
pe rândul următor scriind caracterul “\” la sfârşitul primului
rând.
Numele nume definit ca mai sus se spune că este o constantă
simbolică. Se recomandă ca nume să fie scris cu litere majuscule
pentru a scoate în evidenţă că este o constantă simbolică.
Construcţia succesiune_de_caractere folosită pentru a defini o
constantă simbolică poate la rândul ei să conţină alte constante
simbolice.
O constantă simbolică poate fi redefinită (tot cu define) sau poate
fi anihilată cu undef (#undef nume).
Exemple:
1) #define PROCENT 10 // din acest punct al fisierului sursa se
substituie
// PROCENT cu 10
. . .
#define PROCENT 15 // de aici PROCENT se substituie cu 15
. . .
#undef PROCENT // din acest punct constanta simbolica PROCENT
// isi inceteaza existenta
. . .
#define DIM 100 // s-au definit doua constante simbolice DIM
#define DOI_PI (2*3.1415926) // si DOI_PI
. . .
int vector[DIM]; // DIM va fi inlocuit cu 100
. . .
x=DOI_PI;
. . .
LECŢIA 2.
CLASE DE VARIABILE (DE MEMORIE)
Compilatorul C alocă memorie variabilelor din program de dimensiune
corespunzătoare tipului fiecăreia.
Memoria se alocă în 2 moduri:
static, repartizată într-o zonă specială asociată programului;
dinamic, repartizată într-o zonă specială numită stivă (se
comportă ca o listă LIFO).
În funcţie de modul cum se alocă memorie, vom distinge mai multe
clase de variabile.
O primă clasă de variabile este aceea a variabilelor globale cărora
li se alocă memorie pe toată durata execuţiei programului şi ele
pot fi utilizate în orice funcţie a programului. Altă clasă de
variabile este clasa variabilelor locale, aceste variabile au o
utilizare locală la nivelul unei funcţii.
2.1. VARIABILE GLOBALE
O variabilă globală are o definiţie şi atâtea declaraţii de
variabilă externă câte sunt necesare.
Definiţia unei variabile globale coincide sintactic cu o declaraţie
obişnuită, dar care este scrisă în afara oricărei funcţii a
programului (fişierului sursă). Implicit, definiţia unei variabile
globale determină ca variabila respectivă să fie definită
începând din punctul scrierii ei şi până la sfârşitul
fişierului sursă respectiv. De aceea se recomandă ca definiţiile
variabilelor globale să fie scrise la începutul fişierului sursă,
pentru a fi accesate în orice funcţie a fişierului.
Pentru a utiliza variabilele globale şi în alte funcţii situate în
alte fişiere sursă decât în cel în care sunt definite, ele trebuie
declarate ca externe în funcţiile respective.
O declaraţie de variabilă externă coincide cu o declaraţie
obişnuită doar că începe cu cuvântul cheie extern.
Exemplu:
fişierul în care sunt declarate ca variabile globale fişierul în
care sunt folosite ca variabile externe
int i;
float f; void functie(. . .)
void main(void) { extern int i;
{ i = 10; extern double f;
. . . . . .
f = 3.14; f = f*i;
. . . . . .
} }
Variabilele i şi f sunt declarate în afara funcţiei main şi în
afara oricărei funcţii, deci sunt variabile globale. Ele pot fi
folosite în toate funcţiile din fişierul sursă care conţine
definiţiile lor. Pentru a le utiliza în funcţii situate în alte
fişiere sursă decât în cel în care sunt definite ele sunt
declarate ca externe. Deci variabilele i şi f sunt declarate ca
externe în funcţia functie din al doilea fişier sursă. Cele două
fişiere sursă care pot fi scrise în momente şi de persoane diferite
se pot asambla într-un singur program cu ajutorul directivei de
preprocesare include.
2.2. VARIABILE LOCALE
Variabilele locale nu sunt valabile în tot programul. Ele au o
utilizare locală în două feluri:
ca şi variabile automatice (alocate pe stivă) la nivelul unei
funcţii;
ca şi variabile statice (alocate în zona programului) la nivel de
fişier (eventual şi la nivelul unei funcţii).
Variabilele declarate în interiorul unei funcţii şi care nu sunt
declarate ca externe sunt variabile locale. Lor li se alocă memorie pe
stivă la intrarea în funcţia în care sunt declarate. La ieşirea
din funcţie, stiva se reface la conţinutul dinaintea apelului şi
variabilele locale pierd alocarea. Deci ori de câte ori se apelează o
funcţie, variabilele locale acesteia (denumite şi variabile
automatice) se alocă (primesc memorie) pe stivă şi la ieşirea din
funcţie variabilele locale sunt şterse din stivă.
Variabilele locale pot să nu fie alocate pe stivă, ci într-o zonă
de memorie destinată acestui scop. O astfel de variabilă locală se
numeşte variabilă statică. O variabilă locală statică se declară
printr-o declaraţie obişnuită, precedată de cuvântul cheie static.
O variabilă statică poate fi declarată atât în corpul unei
funcţii cât şi în afara oricărei funcţii. În cazul unei
variabile statice declarată în interiorul unei funcţii alocarea nu
se face pe stivă ci într-o zonă de memorie destinată acestui scop,
aceasta fiind deosebirea esenţială faţă de variabilele automatice.
În cazul în care o variabilă statică este declarată în afara
funcţiilor, ea este definită din punctul în care a fost declarată
şi până la sfârşitul fişierului sursă care conţine declaraţia
ei. Spre deosebire de variabilele globale, o variabilă statică nu
poate fi declarată ca externă şi deci nu poate fi utilizată în
funcţiile din alte fişiere sursă diferite de cel în care a fost
declarată.
Se recomandă ca tablourile mari să fie declarate statice, deoarece
dacă sunt automatice pot depăşi capacitatea stivei (care are
implicit 4K octeţi).
Exemple:
Fişierul fisier1.c este un fişier sursă care conţine 2 variabile
globale i şi d , o variabilă statică x şi două funcţii f şi
main. Funcţia main conţine variabila statică a iar funcţia f
conţine variabila statică b.
int i; // definiţia variabilei globale i
double d; // definiţia variabilei globale d
static int x; // definiţia variabilei statice x, locala fisierului
fisier1.c
void main (void)
{ static char a; // definiţia variabilei statice a, locala funcţiei
main
float c; // definiţia variabilei automatice c, locala funcţiei
main
/* in acest moment se pot folosi variabilele i,d,x,a si c */
. . .
}
{ int p; // definiia variabilei automatice p, locala funciei f
static float b; // definiia variabilei statice b, locala
funciei f
/* in acest moment se pot folosi variabilele i,d,x, p si b */
. . .
}
Variabilele a şi c fiind locale funcţiei main nu pot fi utilizate
în funcţia f. Analog, variabilele p şi b sunt locale în funcţia f,
nu pot fi utilizate în funcţia main.
Fişierul fisier2.c conţine funcţiile f1 şi f2 care intră în
componenţa aceluiaşi program ca şi funcţiile main şi f din
fişierul fisier1.c
static unsigned t; // definitia variabilei statice t, locala
fisierului fisier2.c
void f1(...)
{ extern int i; // declaratie externa pentru i
extern double d; // declaratie externa pentru d
static int k; // definitia variabilei statice k, locala functiei f1
/* in acest moment se pot folosi varibilele i,d, k si t */
. . .
}
void f2(...)
{ extern int i; // declaratie externa pentru i
static double s; // definitia variabilei statice s, locala functiei
f2
/* se pot folosi variabilele i, s si t */
. . .
}
Variabila statică x definită în fişierul fisier1.c nu poate fi
utilizată în fişierul fisier2.c. De asemenea, variabila statică t
nu poate fi utilizată în fişierul fisier1.c. Variabila globală d nu
poate fi utilizată în funcţia f2, ea nefiind declarată ca şi
externă.
Observaţii:
1o. Variabilele globale constituie un mijloc simplu de interfaţă
între funcţiile unui program. Se recomandă a fi folosite când dorim
transferuri de valori între două sau mai multe funcţii în aşa fel
încât modificările realizate de o funcţie să fie accesibile pentru
toate funcţiile programului. Nu trebuie să se facă abuz în
utilizarea variabilelor globale deoarece constituie şi o sursă
potenţială de erori, pentru că accesul unui mare număr de funcţii
la anumite date globale conduce deseori la modificări nedorite şi
greu evidenţiabile.
2o. Funcţiile sunt considerate cu atributul implicit extern. Dacă
într-un program există mai multe fişiere sursă atunci o funcţie
poate fi apelată oriunde, bine înţeles respectând convenţia
definirii ei sau a prototipului ei înainte de a fi folosită. Putem
să limităm definind funcţiile cu atributul static precedând antetul
funcţiei prin cuvântul cheie static. Astfel funcţia respectivă
devine locală şi deci apelabilă doar în fişierul în care este
definită.
2.3. VARIABILE REGISTRU
Limbajul C oferă posibilitatea de a aloca anumite variabile în
regiştri microprocesorului. Deoarece regiştri constituie un tip de
memorie ultrarapidă în anumite cazuri se poate economisi atât timp
de execuţie cât şi memorie. Se pot aloca în regiştri numai
parametrii funcţiilor şi variabilele automatice de tip int, char şi
de tip pointer. O variabilă registru se declară în mod obişnuit,
precedând declaraţia ei prin cuvântul rezervat register.
Alocarea într-un registru a unei variabile se face numai dacă
există un registru disponibil. În caz contrar, variabila registru se
alocă pe stivă exact ca o variabilă automatică. Alocarea se face
în ordinea declarării variabilelor registru.
Exemplu:
void f (register int i)
{ register char c;
register int j;
. . .
}
Mai întâi se alocă parametrul i într-un registru, apoi se alocă c
şi j în alţi doi regiştri.
Se recomandă alocarea în regiştri a variabilelor care au o
utilizare mare, de exemplu a indicilor de tablouri.
2.4. INIŢIALIZARE
Variabilelor li se pot atribui valori iniţiale. Pentru variabilele
globale valorile iniţiale se atribuie prin definiţiile lor, iar în
cazul celorlalte variabile se pot atribui valori prin declaraţiile
lor. Pentru că de multe ori am folosit cuvintele definiţia
variabilelor sau declaraţiile varibilelor precizăm că ele au
înţeles distinct în anumite contexte. O variabilă globală se
defineşte o singură dată şi se poate declara ori de câte ori e
necesară utilizarea ei în alte fişiere (evident declarată
externă). În cazul celorlalte tipuri de variabile definiţiile şi
declaraţiile coincid. Totodată definiţia şi declaraţia
(prototipul) unei funcţii nu coincid.
O variabilă simplă se poate iniţializa printr-o declaraţie de
forma:
tip nume=expresie;
Variabilelor globale şi statice li se atribuie valori iniţiale la
lansarea programului. Expresia utilizată în cazul acestor variabile
trebuie să fie o expresie constantă care să poată fi evaluată de
compilator la întâlnirea ei. Aceasta, deoarece variabilele globale
şi statice se iniţializează prin valori definite la compilare.
Variabilele automatice se iniţializează la execuţie, de fiecare
dată când se activează funcţia în care sunt declarate. Din
această cauză, nu mai este necesar ca expresia să fie o expresie
constantă. Totuşi, la întâlnirea ei, trebuie să fie definiţi
operanzii expresiei de iniţializare.
Exemplu:
void f(int n)
{ int i=10;
int k=i+n;
. . .
}
La întâlnirea expresiei i+n sunt deja definiţi ambii operanzi:
i a fost în prealabil iniţializat cu valoarea 10;
n are o valoare care e transferată la apel.
Variabilele globale şi statice neiniţializate au implicit valoarea
egală cu zero, iar varibilele automatice neiniţializate au o valoare
iniţială imprevizibilă.
Tablourile se pot iniţializa printr-o listă de expresii incluse
între acolade. Astfel un tablou unidimensional se poate iniţializa
folosind următorul format:
tip nume[n] = { exp1, exp2, . . . expn }
La iniţializarea tablourilor se pot utiliza numai expresii constante.
Numărul expresiilor poate fi mai mic decât numărul elementelor
tabloului. Elementele neiniţializate au valoarea zero în cazul
tablourilor globale şi statice. Dacă se iniţializează fiecare
element al tabloului atunci numărul n al elementelor acestuia nu mai
este obligatoriu în declaraţia tabloului respectiv. Deci se poate
scrie astfel:
tip nume[ ] = { exp1, exp2, . . . expn};
Numărul elementelor tabloului se consideră că este egal cu cel al
expresiilor care realizează iniţializarea lor.
Pentru un tablou bidimensional vom folosi următoarea structură:
tip nume [n][m] = { {exp11, exp12, . . . exp1m}, // pentru linia
întâi
{exp21, exp22, . . . exp2m}, // pentru linia a doua
. . .
{expn1, expn2, . . . expnm}, // pentru ultima linie
};
Numărul expresiilor poate fi mai mic decât m în oricare din
acoladele corespunzătoare celor n linii ale tabloului bidimensional.
În acest caz se poate omite doar numărul n (al liniilor tablourilor),
m fiind obligatoriu. Formatul de iniţializare a tablourilor
bidimensionale se extinde imediat pentru tablouri cu mai multe
dimensiuni.
Tablourile de tip caracter pot fi iniţializate folosind un format mai
simplu decât cel indicat anterior. Astfel. un tablou de tip caracter
se poate iniţializa printr-o declaraţie de forma:
char sir[n] = şir_de_caractere;
Evident, marginea superioară n poate fi omisă şi în acest caz.
Totodată compilatorul pune automat caracterul NUL după ultimul
caracter al şirului utilizat în iniţializare.
Exemple:
1) int itab[] = {1,2,3,4,5} // tabloul de tip intreg are 5 elemente
itab[0] = 1,. . . itab[4] = 5
2) int m[3][3] = {{-1,0,1},{-1},{0,1}}; // tabloul are 3 linii si 3
coloane.
Elementele primei linii sunt iniţializate astfel:
m[0][0] = –1;
m[0][1] = 0;
m[0][2] = 1;
În linia a doua se iniţializează numai m[1][0] = -1; Dacă tabloul
ar fi declarat global sau static atunci m[1][1] şi m[1][2] ar avea
valoarea zero. Altfel ele au o valoare imprevizibilă.
Elementele ultimei linii se iniţializează astfel:
m[2][0] = 0;
m[2][1] = 1;
declaraţiile de mai jos sunt identice:
char sir [ ] =
{‘L’,’I’,’M’,’B’,’A’,’J’,’U’,’L’, ‘
‘,‘C’};
char sir [ ] = {“LIMBAJUL C”};
LECŢIA 3.
EXPRESII, OPERANZI, OPERATORI
3.1. EXPRESII
O expresie în limbajul C este formată fie dintr-un operand fie din
mai mulţi operanzi legaţi între ei prin operatori. O expresie are o
valoare şi un tip care se determină aplicând operatorii conform
priorităţilor şi asociativităţii acestora.
În limbajul C operatorii se asociază de la stânga la dreapta,
exceptând operatorii unari şi de atribuire, care se asociază de la
dreapta la stânga.. Totodată pot fi folosite parantezele rotunde
pentru a impune o anumită ordine în executarea operaţiilor.
3.2. OPERANZI
Un operand în limbajul C poate fi una din următoarele elemente:
o constantă;
o constantă simbolică;
numele unei variabile simple;
numele unui tablou;
numele unei structuri;
numele unei funcţii;
referirea la elementul unui tablou (variabilă cu indici);
referirea la elementul unei structuri;
apelul unei funcţii;
o expresie inclusă în paranteze rotunde.
Unele dintre elementele de mai sus nu au fost încă definite, ele se
vor prezenta în lecţiile viitoare.
Exemple: 9876 - constantă întreagă;
x - variabilă simplă;
t[i][3] - variabilă cu indici;
0xabcd - constantă hexazecimală;
t - nume de tablou;
(expresie) - expresie inclusă în paranteze rotunde.
f1 - numele unei funcţii
3.3. OPERATORI
Operatorii limbajului C pot fi grupaţi în mai multe clase, dar oricum
ei pot fi folosiţi împreună într-o aceeaşi expresie. Operatorii au
arităţi diferite: unari, binari, ternari şi totodată o anumită
prioritate implicită care e redată în tabelul de mai jos. Operatorii
de aceeaşi prioritate se află trecuţi în aceeaşi linie. Liniile
tabelulul conţin operatorii limbajului C în ordinea descrescătoare a
priorităţilor. Astfel în prima linie se află operatorii de
prioritate maximă, iar în ultima linie operatorul virgulă cu
prioritatea cea mai mică. Cu excepţia operatorilor “.”,
“->”,”&”,”*”, a parantezelor rotunde (folosite la
definiţia şi apelul funcţiilor) şi a parantezelor drepte (folosite
la variabilele cu indici) ceilalţi operatori vor fi explicaţi în
această lecţie.
( ) [ ] . ->
- (unar) +(unar) *(unar) &(unar) ! ~ ++ -- (tip) sizeof
* / %
+ -
<< >>
< <= >= >
= = !=
&
^
|
&&
| |
? : (ternar)
= op= op poate fi: *(binar) / % +(binar) –(binar) << >> & ^ |
,
3.3.1. Operatori aritmetici
Lista operatorilor aritmetici este redată mai jos:
- (minus unar);
+ (plus unar);
* / % operatori binari multiplicativi; (înmulţire, împărţire,
restul împărţirii întregi);
+ - operatori binari aditivi (adunare şi scădere).
Operatorii de pe aceeaşi linie au aceeaşi prioritate. Cei unari au
prioritate mai mare decât cei binari. Operatorii multiplicativi au
prioritate mai mare decât cei aditivi.
Exemple:
int i,j,k;
float x,y;
double t[10];
// se dau cateva exemple de expresii folosind operatorii aritmetici
i*x+t[5];
-y+k;
i%j; // daca i=9 si j=4 atunci i%j are valoarea 1
i/j; // daca i=9 si j=4 atunci i/j are valoarea 2
x*-y; // - este operatorul unar deci avem x*(-y)
3.3.2. Operatori relaţionali
Lista operatorilor relaţionali este redată astfel:
< (mai mic)
<= (mai mic sau egal; cele două caractere ce compun operatorul sunt
concatenate)
> (mai mare)
>= (mai mare sau egal; cele două caractere ce compun operatorul sunt concatenate)
Toţi operatorii relaţionali au aceeaşi prioritate. Ea este mai
mică decât prioritatea operatorilor aditivi. Rezultatul aplicării
unui operator relaţional este 1 sau 0, după cum operanzii se află
în relaţia definită de operatorul respectiv sau nu.
Exemple:
a= 4 şi b= -5
atunci a>0 are valoarea 1;
a<=0 are valoarea 0;
a+b>0 are valoarea 0;
a>=b are valoarea 1;
a<0 are valoarea 1;
a+b>=b-a are valoarea 1;
a+b<=(b-a)*(b-a) are valoarea 0;
3.3.3. Operatori de egalitate
Lista operatorilor de egalitate este redată mai jos:
= = (egal; două semne “=” concatenate)
!= (diferit; semnele sunt concatenate).
Operatorii de egalitate au ambii aceeaşi prioritate şi este imediat
mai mică decât a operatorilor relaţionali. Operatorul “= =”
testează egalitatea a doi operanzi. Dacă operanzii sunt egali atunci
rezultatul operaţiei “= =” este 1, în caz contrar este 0.
Operatorul “!=” furnizează rezultatul 1 când cei doi operanzi
sunt diferiţi şi 0 când sunt egali.
Exemple:
a= 2 şi b=-1
atunci
a= =b are valoarea 0;
a!=b are valoarea 1;
a*b!=a+b are valoarea 1;
3.3.4. Operatori logici
Lista operatorilor logici este redată mai jos:
! (negaţia logică - operator unar);
&& (ŞI logic);
|| (SAU logic).
Operatorul “!” are aceeaşi prioritate cu operatorii unari “+”
şi “-“. Operatorul “&&” este mai prioritar decât operatorul
“||”, dar are o prioritate mai mică decât operatorii de
egalitate.
În limbajul C nu există valori logice speciale. Valoarea fals se
reprezintă prin zero. Orice valoare diferită de zero reprezintă
valoarea adevărat.
Dacă operatorul “!” se aplică la un operand a cărui valoare
este zero, atunci rezultatul este 1. Dacă acelaşi operator se aplică
la un operand a cărui valoare este diferită de zero, atunci
rezultatul este 0.
Dăm în continuare tabelele operatorilor logici binari aplicate
valorilor 0 şi 1.
&& 0 1 || 0 1 sau exclusiv 0 1
0 0 0 0 0 1 0 0 1
1 0 1 1 1 1 1 1 0
Chiar dacă pentru “sau exclusiv” nu există operator el se poate
realiza prin expresia următoare aplicată operanzilor a şi b:
!a&&b||!b&&a sau folosind parantezele rotunde ((!a) &&b)||((!b)&&a).
Operatorii logici se evaluează de la stânga la dreapta. Dacă la
evaluarea unei expresii se ajunge într-un punct în care se cunoaşte
valoarea întregii expresii, atunci restul expresiei nu se mai
evaluează.
Dacă a=0 şi b=1 atunci expresia ! a||b are valoarea 1 pentru că !a
are deja valoarea 1.
3.3.5. Operatori logici pe biţi
Lista operatorilor logici pe biţi este redată mai jos în ordinea
descrecătoare a priorităţilor:
~ (operator unar; complement faţă de 1)
>> << (deplasări la dreapta, respectiv la stânga)
& (ŞI pe biţi)
^ (SAU-EXCLUSIV pe biţi)
| (SAU pe biţi)
Operatorul “~”, fiind unar, are aceeaşi prioritate ca şi
ceilalţi operatori unari ai limbajului C. El schimbă fiecare bit 1 al
operandului în 0 şi invers.
Operatorul “>>” realizează deplasarea la dreapta care este
echivalentă cu o împărţire întreagă cu puteri a lui 2; a >> 3
este echivalentă cu [a/23].
Operatorul “<<” realizează deplasarea la stânga care este
echivalentă cu o înmulţire cu puteri a lui 2; a << 3 este
echivalentă cu a*8.
Pentru operatorii &, |, ^ dăm în continuare tabelele operaţiilor:
& 0 1 | 0 1 ^ 0 1
0 0 0 0 0 1 0 0 1
1 0 1 1 1 1 1 1 0
Observaţii:
1o. Operanzii care nu ocupă un cuvânt (16 biţi) se extind la un
cuvânt. De exemplu expresia ~0 are ca rezultat un cuvânt cu toţi
biţi egali cu 1.
2o. Operatorii logici pe biţi se execută bit cu bit spre deosebire de
operatorii logici care se evaluează global. De exemplu dacă x=2 i
y=1 sunt variabile de tip int atunci:
x&&y are valoarea 1 pentru că ambii operanzi sunt diferiţi de 0.
x&y are valoarea 0 conform schemei de mai jos
0000 0000 0000 0010
0000 0000 0000 0001
& 0000 0000 0000 0000
3o. Operatorul & se foloseşte frecvent pentru a anula biţi din
configuraţia unui cuvânt, iar operatorul | pentru a seta (pune) biţi
într-un anumit mod;
4o. Operanzii trebuie să fie întregi (de tipul int sau long).
5o. Atenţie la deplasări nu se modifică valoarea operandului; deci
trebuie să facem o atribuire; de exemplu a = a << 3 va modifica
valoarea lui a pe când a << 3 nu modifică valoarea lui a.
Exemple:
Fie declaraţia:
int i;
atunci expresia i >> 8 & 255 are ca rezultat valoarea celui mai
semnificativ octet a lui i; i >> 8 deplasează octetul mai semnificativ
al lui i în poziţia mai puţin semnificativă; se face apoi un ŞI
logic pe biţi cu masca 255 care păstrează octetul mai puţin
semnificativ.
2) Fie expresia: (x >> 6) & ~(~ 0 << 3)
Să presupunem că x are valoarea în biţi 1010 1011 1000 1101.
Atunci x>>6 are valoarea 1111 1110 1010 1110
Al doilea operand pregăteşte o mască astfel:
~0 1111 1111 1111 1111
~0<<3 1111 1111 1111 1000
~(~0<<3) 0000 0000 0000 0110
Rezultatul final este dat de:
0000 0000 0000 0111
1111 1110 1010 1110
0000 0000 0000 0110
Practic s-a obţinut valoarea biţilor 8,7,6 a lui x (numerotaţi de la
dreapta la stânga începând cu 0).
3.3.6. Operatori de atribuire
În forma cea mai simplă operatorul de atribuire se notează cu
“=” şi se utilizează în construcţii de forma:
v=expresie;
(v este fie o variabilă simplă, fie variabilă cu indici sau un
element de structură).
Această construcţie se mai numeşte expresie de atribuire. Ea este
considerată ca fiind un caz particular de expresie. Tipul ei coincide
cu tipul lui v, iar valoarea întregii expresii este chiar valoarea
atribuită lui v.
O expresie de forma:
v1=(v=expresie);
este şi ea legală şi se efectuează în felul următor :
se evaluează expresia expresie şi valoarea ei se atribuie lui v;
valoarea lui v se atribuie apoi şi lui v1.
Deoarece operatorii de atribuire se asociază de la dreapta la stânga,
expresia de mai sus se poate scrie şi fără paranteze:
v1=v=expresie;
În general, putem realiza atribuiri multiple printr-o expresie de
forma:
vn =. . . =v1=v=expresie
Dacă expresia din dreapta semnului egal are un tip diferit de cel al
variabilei v, atunci întâi valoarea ei se converteşte spre tipul
variabilei v şi pe urmă se realizează atribuirea,
Pentru operaţia de atribuire, în afara semnului egal se mai poate
folosi şi succesiunea :
op=
unde prin op se înţelege unul din operatorii binari aritmetici sau
logici pe biţi, adică unul din următorii:
% / * - + & ^ | << >>
Acest mod de construcţie se foloseşte pentru a compacta un anumit tip
de atribuire. Astfel expresia:
v op = expresie;
este identică cu expresia de atribuire:
v = op expresie;
Exemple:
int i, j;
double x, y;
int v[10];
i=5;
j=10;
x=y=10.01;
i +=1; // echivalenta cu i=i+1 si cu i++
x*=3; // echivalenta cu x=x*3
j<<=10; // echivalenta cu j=j<<10
v[i]*=i // echivalenta cu v[i]=v[i]*i
x /= x-y // echivalenta cu x = x/(x-y)
3.3.7. Operatori de incrementare şi decrementare
Aceşti operatori sunt unari şi au aceeaşi prioritate cu ceilalţi
operatori unari ai limbajului C. Operatorul de incrementare se notează
prin “++” şi măreşte valoarea operandului cu unu, iar operatorul
de decrementare se notează prin “- -“ şi micşorează valoarea
operandului cu unu. Operatorii sunt folosiţi prefixat şi postfixat.
Astfel operatorii prefixaţi au notaţia:
++operand;
- - operand;
Ei se aplică mai întâi şi apoi se foloseşte valoarea lor.
Astfel operatorii postfixaţi au notaţia:
operand++;
operand - -;
Se foloseşte valoarea operanzilor şi apoi se aplică incrementarea
sau decrementarea.
Menţionăm că aceşti operatori se pot aplica numai la următorii
operanzi:
variabilă simplă;
variabilă cu indici;
referire la elementul unei structuri.
Exemple:
int i,j;
double x,y;
int vector [5];
j=i++; // este echivalent cu j=i si i=i+1;
y=--x; // este echivalent cu x=x-1 si y=x;
i=++vector[j] // este echivalent cu vector[j]=vector[j]+1 si
i=vector[j]
3.3.8. Operatorul de conversie explicită (expresie cast)
Pentru forţarea tipului unui operand se foloseşte o construcţie de
forma:
(tip) operand
Prin aceasta valoarea operandului se converteşte spre tipul indicat
în paranteze.
Exemplu:
int i,j;
double y;
i=8; j=5;
y=i/j; // y are valoarea 1, pentru ca se face impartirea intreaga i/j
Dacă vom converti operanzii i şi j spre tipul double se va obţine
rezultatul corect adică 1.6.
Deci:
int i,j;
double y;
i=8; j=5;
y=(double) i / (double) j; // y are valoarea 1.6,
Construcţia (tip) este un operator unar prin care se explicitează
conversia dorită. Are aceeaşi prioritate ca restul operatorilor
unari.
3.3.9. Operatorul dimensiune (sizeof)
Pentru a determina lungimea în octeţi a unei date se poate folosi
construcţia:
sizeof (data)
unde data poate fi:
numele unei variabile simple;
numele unui tablou;
numele unei structuri;
numele unui tip;
referirea la elementul unui tablou sau structură.
Exemple:
int i;
long l;
float f;
double d;
char c;
int itablou[5];
double dtablou[5];
sizeof (i) // are valoarea 2;
sizeof (l) // are valoarea 4;
sizeof (f) // are valoarea 4;
sizeof (d) // are valoarea 8;
sizeof (c) // are valoarea 1;
sizeof (itablou[1]) // are valoarea 2;
sizeof (dtablou[1]) // are valoarea 8;
sizeof (itablou) // are valoarea 10;
sizeof (dtablou) // are valoarea 40;
Regula conversiilor implicite
În general o expresie C conţine operanzi de tipuri diferite. Pentru
operatorii binari există situaţii când operanzii nu sunt de acelaşi
tip şi trebuie executate conversii astfel încât operatorii să se
aplice pentru operanzi de acelaşi tip. Aceste conversii le face
automat compilatorul. Există o regulă a conversiilor implicite care
are următorii paşi:
fiecare operand de tip char se converteşte spre tipul int şi fiecare
operand de tipul float se converteşte spre double;
dacă unul dintre operanzi este de tip double atunci şi celălalt se
converteşte spre tipul double şi rezultatul va avea tipul double;
dacă unul dintre operanzi este de tip long, atunci şi celălalt se
converteşte spre tipul long şi rezultatul va avea tipul long;
dacă unul dintre operanzi este de tip unsigned, atunci şi celălalt
se converteşte spre tipul unsigned şi rezultatul va fi de tipul
unsigned;
la acest pas se ajunge numai dacă ambii operanzi sunt de tip int şi
deci operaţia se execută cu operanzii respectivi, iar rezultatul va
fi de tip int.
Aplicând regula de mai sus pas cu pas (la fiecare operator în
momentul efectuării lui), se ajunge în final la evaluarea întregii
expresii şi prin acesta se determină tipul expresiei. Regula
conversiilor implicite nu se aplică pentru operatorul de atribuire
(valoarea expresiei din partea dreaptă a semnului de atribuire se
converteşte spre tipul variabilei din stânga semnului egal).
Exemple:
int i, j, k;
float a, b;
double x, y;
unsigned p;
long r;
char c;
expresii conversii tipul expresiei
i-j/k nu int
a/b a spre double
b spre double double
x+y nu double
i+a a spre double
i spre double double
i-3.14 i spre double double
i+3 nu int
i+x i spre double double
i-c c spre int int
x+10 10 spre double double
p-10 10 spre unsigned unsigned
r*5 5 spre long long
(double)(i/j) se realizează împărţirea întreagă între
i şi j şi rezultatul se converteşte spre double double
Dacă rezultatul unei operaţii depăşeşte domeniul de valori ce
corespunde tipului rezultatului, valoarea respectivă se trunchiază
şi rezultatul este eronat.
3.3.11. Operatori condiţionali
Operatorii condiţionali sunt ? şi : şi se folosesc împreună în
construcţii de forma:
exp1 ? exp2 : exp3
Evaluarea se face astfel:
se evaluează expresia exp1;
dacă exp1 este diferită de zero, atunci valoarea şi tipul expresiei
condiţionale sunt egale cu valoarea şi tipul expresiei exp2; altfel
cu expresia exp3.
Exemplu: procesul de determinare a maximului a două numere a şi b
este:
dacă a>b atunci max=a
altfel max=b
sfdacă
În limbajul C se poate realiza acest proces cu ajutorul operatorilor
condiţionali astfel:
max= a>b ? a : b
Dacă a>b atunci expresia condiţională are valoarea şi tipul lui a
altfel expresia condiţională are valoarea şi tipul lui b.
Operatorul virgulă
Operatorul “,” este folosit pentru gruparea mai multor expresii
într-una singură.
Cu ajutorul acestui operator (care are prioritatea cea mai mică) se
construiesc expresii de forma:
exp1, exp2,. . ., expn
Această expresie are valoarea şi tipul ultimei expresii (deci a lui
expn).
Exemplu:
k= (i=10, j=j+5; i+j)
Se execută pe rând cele două atribuiri de la stânga la dreapta din
parantezele rotunde apoi se face suma i+j şi în final se atribuie
această sumă lui k,
LECŢIA 4.
INTRĂRI / IEŞIRI STANDARD
Limbajul C nu posedă instrucţiuni de intrare / ieşire. Aceste
operaţii se realizează prin apeluri de funcţii din bibliotecile
standard ale mediului de programare. De obicei astfel de funcţii
asigură interfaţa programului cu terminalul de la care s-a lansat, cu
imprimanta, etc. Se numesc intrări standard şi ieşiri standard
intrările respectiv ieşirile de la terminalul de la care s-a lansat
programul. Totodată se presupune că datele de intrare / ieşire sunt
organizate în fişiere.
Unui program C i se ataşează în mod implicit următoarele fişiere:
- stdin intrare standard;
- stdout ieşire standard;
- stderr ieşire standard pentru erori;
- stdprn ieşire pe imprimantă;
- stdaux intrare / ieşire serială.
4.1. FUNCŢIA STANDARD printf
Funcţia printf realizează ieşiri cu format la ieşirea standard
stdout, deci afişare la terminalul la care care s-a lansat programul.
Funcţia printf se apelează printr-o instrucţiune cu formatul:
int printf (control, lista_expresii);
unde control este un şir de caractere care conţine:
texte de scris;
specificatori de format pentru datele care se scriu din lista_expresii.
lista_expresii conţine expresii; valoarea fiecărei expresii se scrie
conform unui specificator de format corespondent din parametrul
control.
Parametrul control este inclus între ghilimele, iar numărul
specificatorilor de format coincide cu numărul expresiilor din
lista_expresii. Dacă dorim să scriem numai un text atunci parametrul
de control nu conţine nici un specificator de format iar
lista_expresii este vidă (practic este absentă).
Un specificator de format are formatul BNF următor:
%[-][d..d][.d..d][l1]l2
După cum se observă un specificator de format începe întotdeauna cu
caracterul procent (“%”). După acest caracter poate urma una din
construcţiile următoare:
- un caracter “-“ opţional; prezenţa acestui caracter indică
cadrarea la stânga a datei în câmpul în care se scrie (implicit
data se scrie cadrată la dreapta);
-un şir de cifre zecimale opţional, care defineşte lungimea minimă
a câmpului în care se scrie data corespunzătoare din lista_expresii;
data se scrie astfel:
în cazul în care necesită o lungime mai mică se scrie cadrată la
dreapta sau stânga (în funcţie de absenţa sau prezenţa semnului
“-“)
în cazul în care necesită o lungime mai mare se va scrie pe atâtea
poziţii câte îi sunt necesare;
-un punct urmat de un şir de cifre zecimale (după cum se observă
opţional); acest element indică precizia datei care se scrie:
dacă data se scrie în virgulă flotantă, precizia defineşte
numărul de cifre aflate după marca zecimală (deci câte zecimale);
dacă data este un şir de caractere atunci indică câte caractere se
scriu.
-una sau două litere, care definesc tipul de conversie din formatul
intern în formatul extern:
prima litera poate fi “l”, ceea ce semnifică conversia din
formatul intern long în formatul extern definit de specificator;
a doua literă este obligatorie întotdeauna şi are semnificaţiile de
mai jos:
litera tipul de conversie realizat
d - din int intern în zecimal extern
o - din int intern în octal extern
x - din int intern în hexazecimal extern (litere mici pentru
cifrele mai mari ca 9 deci a,b,c,d,e,f,)
X - din int intern în hexazecimal extern (litere mici pentru
cifrele mai mari ca 9 deci A,B,C,D,E,F)
u - din unsigned intern în zecimal extern fără semn
c - din binar intern (cod ASCII) în caracterul corespunzător
s - din şir de coduri ASCII într-un şir de caractere (atenţie
ultimul cod este NUL (adică ‘\0’)
f - din float sau double intern în d...d.d...d (implicit 6 cifre
zecimale la partea fracţionară dacă nu e prezentă precizia)
e - din float sau double intern în d.d...de±ddd (implicit 6
cifre zecimale la partea fracţionară dacă nu e prezentă
precizia)
E - din float sau double intern în d.d...dE±ddd (implicit 6
cifre zecimale la partea fracţionară dacă nu e prezentă
precizia)
g - se aplică una din conversiile definite de literele f şi e
alegându-se aceea care are un număr minim de poziţii
G - se aplică una din conversiile definite de literele f şi E
alegându-se aceea care are un număr minim de poziţii
Literele d, o, x, u pot fi precedate de litera l conversia
realizându-se din formatul intern long în loc de int.
Observaţie.
1o. Dacă caracterul % nu este urmat de una din construcţiile de mai
sus atunci nu reprezintă un specificator de format.
Funcţia printf întoarce lungimea totală în octeţi a datelor
scrise la terminal sau valoarea simbolică EOF în caz de eroare.
Precizăm că EOF este definită în fişierul header stdio.h astfel:
#define EOF –1.
Totodată funcţia printf poate fi testată şi astfel:
EOF= = printf (control, lista_expresii)
Dacă are valoarea adevărat atunci la scrierea datelor s-a produs o
eroare.
Exemple:
1)
#include<stdio.h>
#include<math.h>
void main(void)
{ int i=10; long j=11;
float a=1.2, b=1.3;
double A=1.4; B=1.5;
clrscr(); // inceputul instructiunilor executabile; se sterge
ecranul
printf ("\ni*j = %d",i*j); // incep afisarile
printf ("\ni*j = %5d",i*j);
printf ("\ni*j = %-5d",i*j);
printf ("\ni*j = %5.5d",i*j);
printf ("\ni*j = %05d",i*j);
printf ("\na*b = %10.1f",a*b);
printf ("\nA*B = %10.5lf",A*B);
printf ("\nradical(a*b) = %lf",sqrt((double) a*b));
printf ("\nradical(A*B) = %15.10lf",sqrt(A*B));
printf ("\nradical(A*B) = %25.17lf",sqrt(A*B));
printf ("\nradical(A*B) = %25.19lf",sqrt(A*B));
getche(); // asteapta citirea unui caracter de la terminal
}
Rezultatele execuiei programului sunt:
i*j = 110
i*j = 110
i*j = 110
i*j = 00110
i*j = 00110
a*b = 1.6
A*B = 2.10000
radical(a*b) = 1.249000
radical(A*B) = 1.4491376746
radical(A*B) = 1.44913767461894372
radical(A*B) = 1.4491376746189437200
2)
#define sir “PC WORLD”
void main (void)
{ printf(“*%10s*“,sir);
printf(“*%-10s*“,sir);
printf(“*%10.5s*“,sir);
printf(“*%-10.5s*“,sir);
}
Rezultatele execuţiei programului sunt:
* PC WORLD*
*PC WORLD *
* PC WO*
*PC WO *
4.2. FUNCŢIA STANDARD scanf
Funcţia de bibliotecă scanf realizează intrări cu format de la
intrarea standard stdin (intrare de la terminalul de la care s-a lansat
programul) şi poate fi apelată printr-o instrucţiune de apel de
forma:
scanf (control, lista_adrese);
Ca şi în cazul funcţiei printf, parametrul control este delimitat
de ghilimele şi poate conţine texte şi specificatori de format.
Caracterele albe din parametrul control sunt neglijate. Celelalte
caractere care nu fac parte dintr-un specificator de format, trebuie
să existe la intrare în poziţii corespunzătoare. Ele se folosesc
în scopul realizării unor verificări asupra datelor care se citesc.
Un specificator de format începe şi în acest caz prin caracterul
procent, apoi:
un caracter “*” opţional;
un şir de cifre, opţional, care defineşte lungimea maximă a
câmpului din care se citeşte data de sub controlul specificatorului
de format;
una sau două litere care definesc tipul conversiei din formatul extern
în formatul intern.
Câmpul controlat de un specificator de format începe cu primul
caracter care nu este alb şi se termină:
fie la caracterul după care urmează un caracter alb;
fie la caracterul după care urmează un caracter care nu corespunde
specificatorului de format care controlează acel câmp;
fie la caracterul prin care se ajunge la lungimea maximă indicată în
specificatorul de format.
Condiţia c) este absentă în definirea câmpului dacă în
specificatorul de format nu este indicată lungimea maximă a
câmpului.
Literele care termină un specificator de format sunt asemănătoare
cu cele utilizate la funcţia printf. În acest caz este realizată
conversia inversă faţă de cea realizată de funcţia printf.
Litera Tipul conversiei realizate
d - data din câmpul de intrare este un şir de cifre zecimale,
precedat eventual de un semn şi se converteşte din zecimal extern în
binar de tip int
- ca şi în cazul literei d, dar din octal extern în binar de tip
int.
x - ca şi în cazul literei d, dar din hexazecimal extern în binar de
tip int; se utilizează literele mici a-f sau mari A-F pentru cifrele
mai mari ca 9.
X - ca şi în cazul literei x;
u - data este un şir de cifre zecimale care formează un număr
întreg fără semn şi se converteşte din zecimal extern în binar
tipul unsigned.
c - data se consideră formată din caracterul curent de la intrare şi
parametrului corespunzător i se atribuie codul ASCII al acestui
caracter; în acest caz nu se face avans peste caracterele albe, ca şi
în cazul celorlalţi specificatori.
s - data se consideră că este şir de caractere; şirul începe,
conform regulii generale, cu primul caracter care nu este alb şi se
termină la caracterul după care urmează un caracter alb sau când
s-au citit atâtea caractere câte indică dimensiunea maximă din
specificatorul de format;
f - data de intrare reprezintă un număr flotant; deci conversie din
flotant extern în virgulă flotantă simplă precizie; data care se
citeşte conţine un punct sau un exponent, sau atât punct cât şi
exponent.
Literele d, o, x şi u pot fi precedate de litera l şi în acest caz
conversia se realizează spre long, în loc de int.
De asemenea, litera f va fi precedată de litera l pentru a face
conversii spre formatul virgulă flotantă dublă precizie.
Lista_adrese conţine adresele zonelor receptoare ale datelor citite
prin intermediul funcţiei scanf. Fiecare dintre parametri reprezintă
adresa unei zone receptoare sub forma:
&nume
Ea determină adresa zonei de memorie rezervată variabilei nume.
Caracterul “&” din construcţia de mai sus reprezintă un operator
unar, numit operatorul adresă. El are aceeaşi prioritate ca şi
ceilalţi operatori unari din limbajul C.
Exemplu:
void main (void)
{ int i;
long n;
float x;
scanf (“ %d %ld %f ”,&i,&n,&x); // citeste datele din stdin si le
atribuie lui i,n,x.
scanf(“ %d %*ld %f “, &i,&x); // caracterul * indica faptul ca
valoarea pentru variabila n
// nu se citeste
}
Funcţia scanf returnează numărul de câmpuri citite corect din
fişierul stdin. Dacă apare o eroare la citire (din diverse motive de
exemplu neconcordanţă dintre specificatorul de format şi datele din
fişierul stdin) atunci funcţia nu va returna numărul total de
câmpuri; citirea se va întrerupe în locul detectării erorii şi
scanf va returna numărul de câmpuri citite până în acel moment.
Deci de multe ori pentru a valida formal intrarea datelor (atenţie nu
e o validare calitativă) se va folosi o construcţie de forma:
nr=scanf(...)
Prin construcţia de mai sus se poate pune în evidenţă sfârşitul
de fişier, deoarece în acest caz scanf returnează valoarea EOF.
Sfârşitul de fişier se poate genera de la tastatură acţionând în
acelaşi timp tastele CTRL şi Z (deci CTRL / Z). Deorece funcţia
scanf citeşte datele de la intrarea standard prin intermediul unei
zone de memorie intermediare (numită zonă tampon sau buffer), ea are
acces la caracterele din zona tampon numai după acţionarea tastei
ENTER (RETURN). De aceea după acţionarea combinaţiei CTRL / Z se va
tasta şi ENTER. Totodată această intermediere face posibilă
eventuale corecturi înainte de a acţiona tasta ENTER.
Exemplu:
Vom citi un întreg de cinci cifre de la intrarea standard şi vom
afişa cifrele respective precedate fiecare de două spaţii.
void main (void)
{ int c1,c2,c3,c4,c5; // c1,c2,c3,c4,c5 sunt cifrele intregului citit
scanf(“%1d %1d %1d %1d %1d”, &c1,&c2,&c3,&c4,&c5);
// se citesc cifrele intregului in cele 5 variabile cu %1d
printf(“%3d%3d%3d%3d%3d”,c1,c2,c3,c4,c5);
}
Pentru a citi şiruri de caractere se va folosi funcţia scanf fără
a mai pune operatorul de adresă în faţa numelui şirului de
caractere, deoarece numele unui tablou reprezintă un pointer (deci
conţine o adresă şi anume adresa primului element de tablou).
Exemplu:
Vom citi numele şi prenumele unei persoane şi le afişăm pe ecran.
#define MAX 20
void main(void)
{ char nume[MAX+1], prenume[MAX+1]; //declararea tablourilor de
caractere
scanf (“%20s %20s”,nume, prenume); //nu s-a mai pus operatorul de
adresa
printf (“\nnumele: %s, prenumele: %s”,nume,prenume);
}
4.3. FUNCŢIA STANDARD putchar
Funcţia standard putchar se poate utiliza pentru a scrie un caracter
în fişierul standard de ieşire stdout, în poziţia curentă a
cursorului. Ea se apelează folosind o instrucţiune de apel de forma:
putchar (expresie);
unde expresie este codul ASCII al caracterului care se scrie la
ieşirea standard.
Practic putchar nu este o funcţie în sensul definiţiei ce am dat-o
în lecţia 1, ci este un macrou definit în fişierul header stdio.h,
care foloseşte o funcţie specială destinată prelucrării
fişierelor, funcţia putc, astfel:
#define putchar(c) putc(c,stdout)
Exemplu:
void main (void)
{ putchar (‘A’); // scrie caracterul A in fisierul de iesire in
poziia curenta a cursorului
putchar (‘A’+2) // scrie caracterul de cod ASCII
‘A’+2=65+2=67, adica litera C
putchar (‘\n’); // scrie caracterul newline; deci inceput de
linie nouă
}
4.4. FUNCŢIA STANDARD getchar
Această funcţie citeşte de la intrarea standard, stdin, caracterul
curent şi returnează codul ASCII al caracterului citit. Tipul valorii
returnate este int. La întâlnirea sfârşitului de fişier (CTRL/Z)
se returnează valoarea EOF (adică -1). Ca şi putchar, getchar este
un macrou definit în fişierul header stdio.h, cu ajutorul unei
funcţii speciale, getc destinate prelucrării fişierelor, astfel:
#define getchar() getc(stdin)
De obicei getchar se foloseşte în expresii de atribuire de forma:
c=getchar();
După citirea caracterului curent de la intrarea standard se atribuie
variabilei c codul ASCII al caracterului cititi sau EOF la întâlnirea
sfârşitului de fişier.
Exemple:
1)
#include <stdio.h>
void main (void)
{ putchar(getchar() – ‘A’ + ‘a’); // citeste o litera mare si
o rescrie ca litera mica }
2) exemplul următor testează dacă s-a citit o literă mare şi numai
în acest caz aplică transformarea ei în literă mică. În cazul în
care la intrare nu se află o literă mare se rescrie caracterul
respectiv.
#include <stdio.h>
void main (void)
{ intc c;
putchar(((c = getchar() )>= ‘A’ && c<= ‘Z’) ?
c-‘A’+’a’ :c);
}
4.5. FUNCŢIILE STANDARD getche ŞI getch
Funcţia getche citeşte de la intrarea standard caracterul curent şi
returnează codul ASCII al caracterului citit. Această funcţie are
acces direct la caracter după ce a fost tastat şi are forma:
getche();
Funcţia getch este similară cu getche cu singura condiţie că
citirea se face fără ecou (deci caracterul tastat nu se reafişează
pe terminal). De fapt prezenţa/absenţa sufixului e precizează că
funcţia e sau nu cu ecou. Cele două funcţii fac citiri fără
intermedierea zonei tampon.
LECŢIA 5.
INSTRUCŢIUNI
5.1. SCURT ISTORIC AL METODELOR DE PROGRAMARE
Vom prezenta în continuare câteva metode de programare dar nu
exhaustiv, nefiind aici cadrul adecvat (eventual într-un curs de
Software Engineering). O clasificare cronologică a ceea ce vrem să
prezentăm ar fi următoarea:
programarea artizanală;
programarea procedurală;
programarea modulară;
programarea structurată;
programarea prin abstractizarea datelor;
programarea orientată spre obiecte.
5.1.1. Programare artizanală
Această metodă de fapt nu este o metodă propriu-zisă ci este prima
modalitate de programare odată cu apariţia calculatoarelor. Intuiţia
şi experienţa programatorului joacă un rol important. Fiecare
programator îşi are propriile reguli de programare. Programele sunt
monolitice (un singur corp de instrucţiuni), lungi şi greu de
înţeles de alt programator. Însuşi cel ce a elaborat un astfel de
program întâmpină dificultăţi de înţelegere a propriului program
după un timp oarecare.
5.1.2. Programare procedurală
Odată cu apariţia primelor limbaje de înalt nivel se utilizează
programarea procedurală. Necesitatea ca anumite secvenţe de program
să fie folosite de mai multe ori duce la organizarea acestora în
unităţi distincte de program numite în diverse limbaje subprograme,
subrutine, proceduri, etc. De multe ori procedurile trebuie să fie
generale deci procesarea să facă abstractizare de valorile datelor.
De exemplu o procedură de calcul al radicalului de ordinul 2 trebuie
să calculeze acest lucru din orice număr real pozitiv iar pentru cele
negative să semnaleze eroare. Procedurile trebuie deci parametrizate
cu anumite variabile numite parametri formali. Valorile de la apel ale
parametrilor formali se numesc parametri efectivi. Programarea
procedurală are la bază deci utilizarea procedurilor, iar acestea
realizează o abstractizare prin parametri. La apelare o procedură
funcţionează după principiul cutiei negre (black box): se cunosc
intrările şi ieşirile rezultate din acestea dar nu şi modul de
transformare care nu e important în acest moment.
În majoritatea limbajelor procedurale de programare se consideră 2
categorii de proceduri:
proceduri care definesc o valoare de revenire (denumite şi funcţii);
proceduri care nu definesc o valoare de revenire.
În limbajele C şi C++ procedurile de ambele categorii se numesc
funcţii.
5.1.3. Programarea modulară
Pe măsură ce complexitatea aplicaţiilor a crescut, a apărut ideea
de a descompune problemele în subprobleme mai simple care la rândul
lor pot fi descompuse în altele mai simple şi aşa mai departe. În
felul acesta se ajunge la o descompunere arborescentă a problemei date
în subprobleme mai simple. Programarea subproblemelor devine o
problemă mai simplă şi fiecare subproblemă are o anumită
independenţă faţă de celelalte subprobleme. De asemenea, interfaţa
ei cu celelalte subprobleme este limitată şi bine precizată prin
procesul de descompunere a problemei iniţiale. De obicei, programarea
unei subprobleme, componentă a descompunerii arborescente a problemei
iniţiale, conduce la realizarea unui număr relativ mic de proceduri
(funcţii). Aceste funcţii pot prelucra în comun anumite date. Unele
dintre ele sunt independente de funcţiile realizate pentru alte
subprobleme componente ale descompunerii arborescente. Altele
realizează chiar interfaţa cu subproblemele învecinate.
Despre funcţiile obţinute în urma programării unei subprobleme se
obişnuieşte să se spună că sunt înrudite. De obicei, aceste
funcţii, împreună cu datele pe care le prelucrează, se păstrează
într-un fişier şi se compilează independent.
O colecţie de funcţii înrudite, împreună cu datele pe care le
prelucrează în comun formează un modul. În felul acesta, problema
iniţială se realizează printr-un program alcătuit din module.
Programarea modulară are la bază elaborarea programelor pe module.
O parte din datele utilizate în comun de funcţiile modulului, sau
chiar toate datele modulului, nu sunt necesare şi în alte module.
Aceste date pot fi protejate sau cum se mai spune, ascunse în modul.
Limbajul C şi C++, permite ascunderea datelor în modul folosind date
care au clasa de memorie static. Mai mult, pot fi declarate şi
funcţiile ca statice şi atunci ele vor fi ascunse în modul (nu pot
fi apelate din afara modului). Ascunderea funcţiilor în modul se face
pentru acele funcţii care nu se utilizează la realizarea interfeţei
modulului cu celelalte module. Ascunderea datelor şi funcţiilor în
module permite protejarea datelor şi preîntâmpină utilizarea
eronată a funcţiilor.
5.1.4. Programarea structurată
Descompunerea unei probleme în subprobleme mai simple se poate face
succesiv în mai multe etape, până când subproblemele sunt direct
programabile sub forma unor proceduri sau module. Această descompunere
succesivă se mai numeşte rafinare pas cu pas (stepwise refinement)..
Evident că se obţine o descompunere arborescentă. Procedurile se pot
organiza sau nu în module. În cadrul procedurilor se folosesc anumite
structuri de control a execuţiei. Aceasta impune o anumită
disciplină a programării. Structurile de control de sunt:
secvenţa;
iteraţia (pretestată, posttestată, cu număr prestabilit de
ciclări);
alternativa (simplă, completă, generalizată).
Instrucţiunea de bază (primitivă) în cadrul acestor structuri de
control este instrucţiunea de atribuire.
Această abordare a programării s-a născut din necesitatea
eliminării instrucţiunii de control GO TO care face saltul
necondiţionat la o instrucţiune precizată, alta decât
instrucţiunea următoare ei. Profesorul Dijsktra de la Universitatea
din Eindhoven spunea, prin anul 1965, “calitatea unui programator
este invers proporţională cu numărul de instrucţiuni GO TO
folosite” şi a impus D-structurile de control:
secvenţa;
iteraţia pretestată;
alternativa simplă.
D-structurile se regăsesc în toate limbajele procedurale.
Corespondenţa ar fi:
secvenţa este echivalentă cu execuţia instrucţiunilor în ordinea
scrierii lor în programul sursă;
b) iteraţia pretestată echivalentă cu WHILE ... DO;
c) alternativa simplă echivalentă cu IF ... THEN.
O ilustrare grafică a celor trei D-structuri se dă în continuare.
da
S1 C
nu da
S2 S C
S nu
.
Sn
S-a demonstrat ulterior (Bohm şi Jacopini) că orice algoritm se poate
descrie doar cu D-structurile dar pentru o mai bună lizibilitate şi
înţelegere a programelor sursă s-au adăugat şi iteraţia
postestată (REPEAT ... UNTIL), iteraţia cu număr prestabilit de
ciclări (FOR ... DO), alternativa completă (IF ... THEN ... ELSE) şi
alternativa generalizată (CASE ... OF).
În unele limbaje se folosesc şi alte structuri pe lângă cele de mai
sus pentru o cât mai fidelă reflectare a algoritmului.
5.1.5. Programarea prin abstractizarea datelor
În toate aceste tehnologii anterioare se urmăreşte mai mult
organizarea programului şi mai puţin rezolvarea cât mai naturală a
problemei. Programarea prin abstractizarea datelor şi programarea
orientată spre obiecte propun metodologii în care conceptele deduse
din analiza problemei să poată fi reflectate cât mai fidel în
program şi să se poată manevra cu instanţieri ale acestor concepte
cât mai natural. Se realizează o mai mare fidelitate a programului
faţă de problemă. De exemplu dacă într-o problemă în care se
procesează numere complexe e nevoie să se lucreze într-o formă cât
mai apropiată cu forma matematică se poate introduce tipul COMPLEX
(tip care nu există în limbajele de programare) şi apoi se pot
declara variabile de acest tip. Mai mult ar trebui să se poată face
toate operaţiile matematice asupra datelor de tip COMPLEX. În general
un TAD (Tip Abstract de Date) are două componente fundamentale:
datele membru (reflectă reprezentarea tipului);
funcţiile membru (reflectă comportamentul tipului).
5.1.6. Programarea orientată spre obiecte
Un neajuns al programării prin abstractizarea datelor este faptul că
nu permite exprimarea legăturilor dintre diferite concepte (TAD-uri).
Singura legătură dintre concepte care se poate exprima, este aceea
că datele membru ale unei clase pot fi obiecte ale unei alte clase.
Acest lucru nu este suficient în cazul în care conceptele sunt
strâns dependente între ele formând structuri ierarhice. Exprimarea
ierarhiilor conduce la atribute suplimentare cum sunt cele de
moştenire. Aceste atribute conduc la un nou model de programare pe
care îl numim programare orientată obiect.
În vârful unei ierarhii se află fenomenul sau forma de existenţă
care are trăsături comune pentru toate celelalte componente ale
ierarhiei respective. Pe nivelul următor al ierarhiei se află
componentele care pe lângă trăsăturile comune de pe nivelul
superior, mai au şi trăsături suplimentare, specifice. O ierarhie,
de obicei, are mai multe nivele, iar situarea unui element pe un nivel
sau altul al ierarhiei este uneori o problemă deosebit de complexă.
Dăm în exemplul următor o ierarhie arborescentă pentru conceptele
de paralelogram, dreptunghi, romb şi pătrat.
Paralelogram
Dreptunghi Romb
Pătrat
Dacă paralelogramul se află în vârful ierarhiei atunci pe nivelul
imediat inferior se aşează dreptunghiul (paralelogramul cu un unghi
drept) dar şi rombul (paralelgramul cu 2 laturi alăturate
congruente). Apoi pătratul se poate defini fie ca un dreptunghi cu
laturile congruente fie ca un romb cu un unghi drept. Conceptul de pe
fiecare nivel se observă că moşteneşte proprietăţile conceptului
imediat superior din care este derivat.
La ora actuală, toate ramurile cunoaşterii ştiinţfice sunt pline
de ierarhii rezultate în urma clasificării cunoştinţelor acumulate
în perioada lungă de observare a fenomenelor şi formelor de
existenţă a lumii materiale şi spirituale. Clasificările ierarhice
ale cunoştinţelor pot fi întâlnite atât în domeniile care pleacă
de la cele mai concrete forme ale lumii materiale, cum sunt botanica,
zoologia, biologia, etc cât şi în domenii care studiază concepte
dintre cele mai abstracte, cum ar fi matematica sau filozofia.
Aceste ierarhii sunt rezultatul definirii conceptelor după regula
includerii “genul proxim şi diferenţa specifică”.
Limbajul C dispune de un set bogat de instrucţiuni care permit
scrierea de:
programe structurate,
programe flexibile,
programe compacte.
Totodată limbajul C permite aplicarea metodelor de programare
procedurală, programare modulară şi programare structurată. Pe
lângă aceste metodologii limbajul C++ permite şi programarea prin
abstractizarea datelor şi programarea orientată spre obiecte.
Vom descrie în continuare instrucţiunile limbajului C. Ca o
caracteristică sintactică toate instrucţiunile limbajului se
termină prin caracterul “;”, excepţie făcând instrucţiunile
care se termină cu acolada închisă.
Limbajul C dispune de un set bogat de instrucţiuni care permit
scrierea de:
programe structurate,
programe flexibile,
programe compacte.
Totodată limbajul C permite aplicarea metodelor de programare
procedurală, programare modulară şi programare structurată. Pe
lângă aceste metodologii limbajul C++ permite şi programarea prin
abstractizarea datelor şi programarea orientată spre obiecte.
Vom descrie în continuare instrucţiunile limbajului C. Ca o
caracteristică sintactică toate instrucţiunile limbajului se
termină prin caracterul “;”, excepţie făcând instrucţiunile
care se termină cu acolada închisă.
5.2. INSTRUCŢIUNEA VIDĂ
Instrucţiunea vidă se reduce la caracterul “;”. Ea nu are nici un
efect. Adesea este nevoie de ea la construcţii în care se cere
prezenţa unei instrucţiuni, dar nu este necesar să se execute nimic
în punctul respectiv.
5.3. INSTRUCŢIUNEA EXPRESIE
Instrucţiunea expresie se obţine scriind punct şi virgulă după o
expresie, deci:
expresie;
Există cazuri particulare ale instrucţiunii expresie:
expresia de atribuire, care de altfel este cel mai important caz
particular al instrucţiunii expresie:
expresie;
apelul unei funcţii:
nume_funcţie (par1, par2, . . . parn);
incrementările şi decrementările pre şi post fixate:
variabilă++; ++variabilă;
variabilă- -; - - variabilă;
Exemplu:
void main (void)
{ int i;
float f;
double d;
i=10; // instructiune de atribuire
i++; // i se mareste cu 1
f=i; // instructiune de atribuire (valoarea lui i se converteste in
float)
d=++f; // incrementare lui f si atribuirea valorii lui d
putchar(‘a’); // instructiune de apel
}
5.4. INSTRUCŢIUNEA COMPUSĂ
Instrucţiunea compusă este o succesiune de declaraţii urmate de
instrucţiuni, succesiune inclusă între acolade:
{
declaraţii
instrucţiuni
}
Pot lipsi declaraţiile sau instrucţiunle dar nu în acelaşi timp.
Dacă delaraţiile sunt prezente, atunci ele definesc variabile care
sunt valabile numai în instrucţiunea compusă respectivă.
Exemplu:
. . .
{ int i=100; // variabila i este definita in aceasta instructiune
compusa
i++; // i are valabilitate doar intre acolade; dupa acolada inchisa
i isi
printf (“i=%d\n”,i); // pierde valabilitatea
}
Observaţii:
1o. După acolada inchisă a unei instrucţiuni compuse nu se pune
”;”.
2o. Corpul unei funcţii are aceeaşi structură ca şi instrucţiunea
compusă, deci o funcţie are formatul:
antetul funcţiei
instrucţiune compusă
5.5. INSTRUCŢIUNEA if
Instrucţiunea if permite să realizăm o ramificare a execuţiei în
funcţie de valoarea unei expresii. Ea are două formate ce permit
aplicarea structurii de alternativă simplă şi compusă.
Formatul 1:
if (expresie) instructiune;
Efectul:
se evaluează expresia din paranteze;
dacă valoarea expresiei este diferită de zero (deci conform
convenţiei are valoarea adevărat), atunci se execută instructiune,
altfel se trece la instrucţiunea următoare.
Formatul 2:
if (expresie) instructiune_1;
else instructiune_2;
Efectul:
se evaluează expresia din paranteze;
dacă valoarea expresiei este diferită de zero (deci conform
convenţiei are valoarea adevărat), atunci se execută instructiune_1,
altfel se execută instructiune_2; apoi în ambele cazuri se trece la
instrucţiunea următoare.
Observaţii:
1o. Se pot folosi instrucţiuni if imbricate, nivelul de imbricare
fiind oarecare (deci nu există o limitare a numărului de imbricări).
2o. Pentru mai multe imbricări se foloseşte regula asocierii if-lui
cu else astfel:
un else se pune în corespondenţă cu primul if care se află
înaintea lui în textul sursă şi nu este inclus în instrucţiunea
care îl precede pe el şi nici nu îi corespunde deja un else.
Exemple
void main (void)
{ float x,y,a;
x=-5;
y=10;
if (x<0) // ultimul else se asociaza cu primul if iar
if (y<0) a=1; // penultimul else se asociaza cu cel de-al doilea if
else a=2;
else a=0;
}
void main (void)
{ float x,y,a;
x=-5;
y=10;
if (x<0) // ultimul else se asociaza cu primul if deoarece cel de-al
{ if (y<0) a=1; } // de-al doilea if este inclus in instructiunea
compusa care
else a=0; // il precede pe if
}
void main (void) // citeste trei intregi si scrie minimul dintre ei
{int i,j,k,min;
scanf (“\ndati i=%d”, &i);
scanf (“\ndati j=%d”, &j);
scanf (“\ndati k=%d”, &k);
if (i>j) min=j;
else min=i;
if (k<min) min=k;
printf (“min(%d,%d,%d)= %d\n”,i,j,k,,min);
}
5.6. INSTRUCŢIUNEA while
Instrucţiunea while are următorul format:
while (expresie) instructiune;
Cu ajutorul instrucţiunii while se realizează structura repetitivă
pretestată (condiţionată anterior).
Efectul:
se evaluează valoarea expresiei din paranteze;
dacă expresia are valoarea diferită de zero, atunci se execută
instructiune şi se reia punctul 1), altfel se trece la instrucţiunea
următoare instrucţiunii while.
Deci instructiune se execută repetat atâta timp cât expresia din
paranteză este diferită de zero. Se observă că dacă expresia are
valoarea zero de la început, atunci instructiune nu se execută
niciodată.
Antetul ciclului while este construcţia while (expresie) iar
instructiune formează corpul ciclului. În cazul în care este necesar
să se execute repetat mai multe instrucţiuni, se utilizează o
instrucţiune compusă formată din instrucţiunile respective.
Exemplu:
Vom crea un program care citeşte un întreg n şi scrie n!. Algoritmul
în pseudocod este următorul:
Citeste n
f=1
i=2
CâtTimp i<=n execută
f=f*i;
i=i+1
SfârşitCâtTimp
Scrie n,f
Programul în C este:
#include<stdio.h>
void main (void)
{ int n,i;
double f;
f=1.0;
i=2;
printf(“\n dati n= “);
scanf(“%d”,&n);
while (i<=n)
{ f=f*i;
i++;
}
printf(“\nn=%d, iar n!=%g\n”,n,f);
}
Corpul ciclului while se poate scrie mai compact astfel:
while (i<=n) f*=i++;
5.7. INSTRUCŢIUNEA for
Instrucţiunea for, ca şi instrucţiunea while, se utilizează pentru
a realiza o structură repetitivă pretestată. Formatul instrucţiunii
este:
for(exp1; exp2; exp3) instructiune;
Antetul ciclului este definit de for(exp1; exp2; exp3) iar
instructiune formează corpul ciclului. Prima expresie exp1 constituie
partea de iniţializare a ciclului, iar exp3 este partea de
reiniţializare a ciclului. Condiţia de continuare a ciclului este
exp2. De obicei exp1 şi exp3 reprezintă atribuiri.
Efectul:
se execută secvenţa de iniţializare definită de expresia exp1;
se evaluează exp2; dacă exp2 are valoarea zero, atunci se iese din
ciclu, adică se trece la instrucţiunea următoare instrucţiunii for,
altfel se execută instrucţiunea din corpul ciclului;
se execută apoi secvenţa de reiniţializare definită de exp3, apoi
se reia secvenţa de la punctul 2).
Observaţii:
1o. Ca şi în cazul instrucţiunii while, instrucţiunea din corpul
ciclului for poate să nu se execute niciodată dacă exp2 are valoarea
zero chiar la prima evaluare.
2o. Expresiile din antetul instrucţiunii for pot fi şi vide; totuşi
caracterele “;” vor fi întotdeauna prezente.
3o. Comparând instrucţiunile for şi while observăm că
instrucţiunea for cu formatul anterior se poate realiza cu secvenţa
următoare folosind while:
exp1;
while (exp2)
{ instructiune;
exp3;
}
Invers, o instrucţiune while de forma: while (exp) instructiune este
echivalentă cu următoarea instrucţiune for:
for(; exp; ) instructiune.
Autorii limbajului C propun ca instrucţiunea for să se folosească cu
prioritate pentru ciclurile care au pas.
Exemple:
Vom da o secvenţă de instrucţiuni care însumează elementele unui
tablou:
s=0;
for(i=0; i<n; i++) s=s+tab[i];
sau scrisă mai compact:
for (s=0, i=0; i<n; i++) s+=tab[i];
În continuare vom da un mic program care afişează numărul
caracterelor citite de la intrarea standard stdin.
#include <stdio.h>
void main(void)
{ long n;
for (n=0; getchar()!=EOF; n++);
printf (“\nnumarul caracterelor citite =%ld\n”,n);
}
sau scrisă cu instrucţiunea while
#include <stdio.h>
void main(void)
{ long n=0;
while (getchar()!=EOF) n++;
printf (“\nnumarul caracterelor citite =%ld\n”,n);
}
5.8. INSTRUCŢIUNEA do-while
Această instrucţiune realizează structura repetitivă
condiţionată posterior (posttestată) dar modificată faţă de
REPEAT .. UNTIL. Această instrucţiune s-a introdus pentru o mai mare
flexibilitate în scrierea programelor. Formatul ei este: do
instructiune;
while (exp);
Efectul:
se execută instrucţiunea instructiune;
se evaluează expresia exp din paranteze;
dacă valoarea expresiei este zero se trece la instrucţiunea
următoare instrucţiunii do-while; altfel se revine şi se execută
din nou instructiune.
Observaţii:
1o. Structura realizată de instrucţiunea do-while poate fi realizată
printr-o secvenţă în care se foloseşte instrucţiunea while astfel:
instructiune;
while (exp) instructiune;
2o. Se observă că în cazul instrucţiunii do-while, corpul ciclului
se execută cel puţin odată, spre deosebire de ciclurile while şi
for unde corpul ciclului poate să nu se execute niciodată.
Exemplu:
Vom da un program care calculează rădăcina pătrată dintr-un număr
real a>=0.
#include<stdio.h>
#define EPS 1e-10
void main (void)
{ double x1,x2,y,a;
clrscr(); // sterge ecranul
printf(“\ndati un numar real pozitiv a=”);
if (scanf(“%lf”,&a) !=1 || a<0) printf (“numarul citit nu este
pozitiv\n”);
else {
x2 = 1.0;
do { x1 = x2;
x2 = 0.5 *(x1+a/x1); // formula de iteratie
if ((y=x2-x1) < 0) y = -y;
}
while (y >= EPS);
printf (“radacina patrata din:%g este: %.2lf\n”,a,x2); // 2
zecimale
} //sfirsit else
}
5.9. INSTRUCTIUNEA switch
Instrucţiunea switch permite realizarea structurii alternativa
generalizată. Ea este echivalentă cu o imbricare de structuri de
alternativă simple. Utilizarea instrucţiunii switch face în schimb
programul mult mai clar.
Formatul instrucţiunii switch este următorul:
switch (exp)
{ case c1: sir1
break;
case c2: sir2
break;
. . .
case cn: sirn
break;
default: sir
}
unde: c1,. . . cn sunt constante sau constante simbolice;
sir1, . . . ,sirn, sir sunt şiruri de instrucţiuni.
Efectul:
se evaluează expresia din paranteză;
se compară pe rând valoarea expresiei cu valorile constantelor c1, .
. . , cn;
dacă valoarea expresiei coincide cu valoarea lui ck, se execută
secvenţa de instrucţiuni definită prin sirk; în cazul în care
valoarea expresiei nu coincide cu nici una din constantele c1, . . . ,
cn, se execută secvenţa de instrucţiuni definită prin sir;
după execuţia secvenţei sirk sau sir se trece la instrucţiunea
următoare instrucţiunii switch, adică la prima instrucţiune aflată
după acolada închisă care termină instrucţiunea switch
respectivă; evident, acest lucru are loc dacă şirul care se execută
nu impune, el insuşi, un alt mod de continuare a execuţiei, de
exemplu o revenire din funcţia respectivă, un salt la o anumită
instrucţiune, etc.
Observaţii:
1o. Ramura default nu este obligatorie. În lipsa ei, dacă valoarea
expresiei nu coincide cu nici una din constantele c1,. . . , cn,
instrucţiunea switch respectivă nu are nici un efect.
2o.Construcţia break reprezintă o instrucţiune. Ea termină fiecare
ramură de instrucţiuni sir1, . . . , sirn, provocând saltul la
instrucţiunea următoare instrucţiunii switch sau, cum se mai spune,
realizează ieşirea din instrucţiunea switch.
3o. Instrucţiunea break nu este obligatorie. În cazul în care este
absentă, se execută secvenţial următoarea ramură. De exemplu dacă
avem secvenţa:
switch (exp)
{ case c1: sir1
case c2: sir2
}
ea se execută în felul următor:
dacă valoarea expresiei este egală cu c1 se execută sir1 şi apoi
sir2;
dacă valoarea expresiei este egală cu c2 se execută sir2;
daca valoarea expresiei difera de valorile c1 şi c2 instrucţiunea
switch de mai sus nu este efectivă, se trece la instrucţiunea
următoare care urmează după switch.
secvenţa de mai sus se putea realiza şi astfel:
if (exp = = c1)
{ sir1
sir2
}else if (exp = = c2) sir2
Exemplu:
Vom citi din fişierul de intrare construcţii de forma: op1 operator
op2, unde op1 şi op2 sunt numere întregi (operanzi întregi) iar
operator este un operator aritmetic {“+”, “-“, “*”,
“/”}. La ieşire se va scrie valoarea expresiei citite. De exemplu
dacă se citeşte secvenţa 100/3 se va afişa rezultatul 33. Programul
permite citirea şi evaluarea mai multor astfel de expresii, până la
întâlnirea sfârşitului de fişier.
#include <stdio.h>
void main (void)
{ int op1,op2,operator,rezultat,i;
while (( i=scanf(“%d %c %d”, &op1,&operator, &op2)) != EOF)
if (i = = 3 ) // ramura adevarat inseamna ca s-au citit 3 campuri
corecte
{ switch (operator)
{ case ‘+’: rezultat = op1 + op2 ; // avem adunare
break;
case ‘-‘ : rezultat = op1 – op2; // avem scadere
break;
case ‘*’ : rezultat = op1 * op2; // avem inmultire
break;
case ‘/’ : // avem impartire intreaga
if (op2 = = 0)
{ printf (“divizor nul\n”);
rezultat = 0;
} else rezultat = op1 / op2;
break;
default : printf (“operator eronat\n”);
rezultat = 0;
} // sfarsit switch
printf (“%d %c %d %d\n”, op1, operator, op2, rezultat);
} else
printf (“expresie eronat\n”); // sfarsit if si while
}
5.10. INSTRUCŢIUNEA break
Formatul instrucţiunii este următorul:
break;
De obicei instrucţiunea break se foloseşte pentru a ieşi dintr-un
ciclu. Dacă există mai multe cicluri imbricate instrucţiunea break
va trece controlul la ciclul de nivel imediat superior (deci imbricarea
rămâne, nu se iese din toate ciclurile). O altă utilizare este în
instrucţiunea switch, după cum am observat în paragraful anterior.
Un alt exemplu de utilizare frecventă este ieşirea dintr-un ciclu
infinit de forma:
for ( ; ; )
{. . .
if (exp) break;
. . .
}
5.11. INSTRUCŢIUNEA continue
Formatul instrucţiunii este următorul:
continue;
Efectul:
în ciclurile while şi do-while ea realizează saltul la evaluarea
expresiei care decide asupra continuării ciclului;
în ciclul for ea realizează saltul la pasul de reiniţializare.
Observaţie:
1o. Instrucţiunea continue se utilizează numai în corpul unui ciclu,
permiţând, după caz, să se treacă la pasul următor al ciclului
sau să se iasă din ciclu.
5.12. INSTRUCŢIUNEA goto
Conform principiilor programării structurate instrucţiunea goto nu
ar fi necesară. Dar ea a fost introdusă în limbaj, deoarece, în
anumite cazuri, se dovedeşte a fi utilă, asigurând o flexibilitate
mai mare în programare. De multe ori ieşirea dintr-un ciclu imbricat
în alte cicluri se realizează mai simplu cu ajutorul instrucţiunii
goto. În lipsa ei ar trebui să folosim mai mulţi indicatori şi
teste asupra valorilor acestora pentru ieşirea din ciclu. Saltul
făcut de goto se face la o instrucţiune care este prefixată de o
etichetă.
Prin etichetă vom înţelege un nume urmat de caracterul “:”.
Etichetele sunt locale unei funcţii.
Instrucţiunea goto are următorul format:
goto eticheta;
Efectul:
se realizează saltul la instrucţiunea prefixată de eticheta al
cărei nume se află scris după cuvântul cheie goto.
5.13. APELUL ŞI REVENIREA DINTR-O FUNCŢIE
5.13.1. Apelul unei funcţii
În limbajul C funcţiile sunt de două tipuri:
funcţii care returnează o valoare la revenirea din ele;
funcţii care nu returnează nici o valoare la revenirea din ele.
O funcţie care nu returnează nici o valoare la revenirea din ea se
apelează printr-o instrucţiune de apel. Ea are următorul format:
nume (lista_parametrilor_efectivi); (*)
unde:
nume este numele funcţiei;
lista_parametrilor_efectivi este fie vidă, fie se compune din una sau
mai multe expresii separate prin virgulă.
Instrucţiunea de apel este un caz particular al instrucţiunii
expresie. Parametrii efectivi (de la apel) trebuie să corespundă cu
cei formali (de la definirea funcţiei) prin ordine, tip şi număr.
În cazul în care o funcţie returnează o valoare, ea poate fi
apelată fie printr-o instrucţiune de apel, fie sub forma unui operand
al unei expresii.
Observaţii:
1o. Dacă nu dorim să utilizăm valoarea returnată de funcţia
respectivă, apelul se face printr-o instrucţiune de apel.
2o. Dacă dorim să utilizăm valoarea returnată de funcţie, vom
folosi apelul funcţiei drept operand într-o expresie, operandul
având formatul (*).
Exemple de apeluri de funcţii folosite până acum sunt apelurile
funcţiilor standard printf, scanf, getchar şi putchar. Funcţiile
printf şi putchar au fost apelate prin instrucţiuni de apel, valorile
returnate de ele nefiind utilizate. În schimb funcţiile scanf şi
getchar au fost apelate în ambele moduri, atât prin instrucţiuni de
apel, cât şi ca operanzi în diferite expresii.
5.13.2. Prototipul unei funcţii
O funcţie poate fi apelată dacă ea este definită în fişierul
sursă înainte de a fi apelată. După cum am prezentat în lecţia 1
nu întotdeauna este posibil acest lucru şi în astfel de cazuri
apelul funcţiei trebuie să fie precedat de prototipul ei.
Prototipul unei funcţii are ca scop să informeze compilatorul
despre:
tipul valorii returnate de funcţie;
tipurile parametrilor.
În felul acesta, la apelul unei funcţii, compilatorul poate face
teste cu privire la tipul expresiilor care reprezintă parametrii
efectivi, precum şi unele conversii necesare asupra valorii returnate
de funcţie.
Observaţii:
1o. Tipurile parametrilor pot să lipsească. În acest caz,
compilatorul nu controlează tipurile parametrilor efectivi, singura
informaţie conţinută de prototip fiind tipul valorii returnate de
funcţia respectivă.
2o. Absenţa atât a prototipului unei funcţii, cât şi a definiţiei
funcţiei înainte de a fi apelată este posibilă; în acest caz se
presupune că funcţia returnează o valoare de tip int.
3o. În practică se recomandă utilizarea prototipurilor pentru toate
funcţiile înainte de a fi apelate. În acest scop, ele vor fi scrise
la începutul fişierelor sursă.
Formatele posibile ale unui prototip sunt:
formatul 1: tip nume (lista_declaratiilor_de_parametri);
formatul 2: tip nume (lista_ tipurilor_parametrilor);
formatul 3: tip nume (void);
formatul 4: tip nume ();
Formatul 2 este cel mai utilizat. Formatul 3 se poate folosi pentru
orice funcţie care nu are parametri. Formatul 4 se poate folosi pentru
orice funcţie la al cărei apel nu se doresc teste referitoare la
tipul parametrilor efectivi.
Funcţiile din biblioteca standard a limbajului C au prototipurile
definite în fişierele de tip .h.
5.13.3. Apel prin valoare şi apel prin referinţă
La apelul unei funcţii, fiecărui parametru formal i se atribuie
valoarea parametrului efectiv care-i corespunde. Deci, la apelul unei
funcţii se transferă valorile parametrilor efectivi. Din această
cauză se spune că apelul este prin valoare (call by value). În
anumite limbaje de programare, la apel nu se transferă valorile
parametrilor efectivi ci adresele acestora. În acest caz se spune că
apelul este prin referinţă (call by refference).
Între cele două tipuri de apeluri există o diferenţă esenţială
şi anume: în cazul apelului prin valoare funcţia apelată nu poate
modifica parametrii efectivi din funcţia apelantă, neavând acces la
ei. În cazul apelului prin referinţă, funcţia apelată, dispunând
de adresele parametrilor efectivi, îi poate modifica.
În limbajul C apelul se realizează implicit prin valoare. În cazul
că un parametru este numele unui tablou atunci transferul se
realizează prin referinţă deoarece numele unui tablou este un
pointer şi conţine adresa primului element al tabloului. Transferul
prin referinţă se realizează cu ajutorul variabilelor de tip pointer
şi cu ajutorul operatorului de adresă (&).
5.13.4. Revenirea dintr-o funcţie
Revenirea dintr-o funcţie se poate face în două moduri:
la întâlnirea instrucţiunii return;
după execuţia ultimei sale instrucţiuni, adică a instrucţiunii
care precede acolada închisă ce termină corpul funcţiei respective.
Instrucţiunea return are două formate:
return; sau return expresie;
Primul format se utilizează când funcţia nu returnează o valoare,
iar cel de-al doilea când funcţia returnează o valoare. În acest
ultim caz, funcţia returnează valoarea expresiei specificate.
Observaţie:
1o. Când revenirea se face după execuţia ultimei instrucţiuni a
funcţiei nu se returnează o valoare; revenirea în acest caz, se face
ca şi cum acolada închisă de la sfârşitul corpului funcţiei ar fi
precedată de instrucţiunea return.
Exemplu: vom da un exemplu de apel al funcţiei care determină
rădacina pătratică dintr-un număr nenegativ.
#include<stdio.h>
double radacina_2 (double) // prototipul functiei
void main (void) // functia principala care citeste d
// si afiseaza radacina patrata din d
{ double d;
clrscr(); // sterge ecranul
if (scanf (“%lf”,&d) != || d<0)
printf (“numarul dat este eronat\n”);
else
printf (“d=%f, radacina patrata = %.10g\n”, d, radacina_2(d));
#define EPS 1e-10
double radacina_2 (double x)
{ double x1,x2,y;
x2 = 1.0;
do { x1 = x2;
x2 = 0.5 *(x1+x/x1); // formula de iteratie
if ((y=x2-x1) < 0) y = -y;
}
while (y >= EPS);
return x2;
}
Observaţie:
1o. Limbajul C dispune de o bibliotecă matematică în care sunt
incluse o serie de funcţii pentru calculul valorilor funcţiilor
elementare. Există o funcţie numită sqrt (cu prototipul double sqrt
(double);). Fişierul care conţine biblioteca matematică se numeşte
math.h şi trebuie inclus în fişierul sursă de lucru dacă se
doreşte utilizarea funcţiilor definite în el.
LECŢIA 6.
POINTERI
Un pointer este o variabilă care are ca valori adrese. Pointerii se
folosesc pentru a face referire la date cunoscute prin adresele lor.
Astfel, dacă p este o variabilă de tip pointer care are ca valoare
adresa zonei de memorie alocată pentru variabila întreagă x atunci
construcţia *p reprezintă chiar valoarea variabilei x.
În construcţia de mai sus, *p, caracterul * se consideră ca fiind
un operator unar care furnizează valoarea din zona de memorie a cărei
adresă este conţinută în p. Operatorul unar * are aceeaşi
prioritate ca şi ceilalţi operatori unari din limbajul C.
Dacă p conţine adresa zonei de memorie alocată variabilei x, vom
spune că p pointează spre x sau că p conţine adresa lui x.
Pentru a atribui unui pointer adresa unei variabile, putem folosi
operatorul unar &. Astfel, dacă dorim ca p să pointeze spre x, putem
utiliza construcţia:
p = &x;
În limba română se utilizează şi alte denumiri pentru noţiunea
de pointer: referinţă, localizator; reper; indicator de adresă.
6.1. DECLARAŢIA DE POINTER
Un pointer se declară ca orice variabilă cu deosebirea că numele
pointerului este precedat de caracterul *. Astfel, dacă, de exemplu,
dorim să declarăm variabila p utilizată anterior pentru a păstra
adresa variabilei întregi x, vom folosi declaraţia următoare:
int *p;
Tipul int stabileşte în acest caz faptul că p conţine adrese de
zone de memorie alocate datelor de tip int. Declaraţia lui p se poate
interpreta în felul următor: *p reprezintă conţinutul zonei de
memorie spre care pointează p, iar acest conţinut are tipul int.
În general, un pointer se declară prin:
tip *nume;
ceea ce înseamnă că nume este un pointer care pointează spre o
zonă de memorie ce conţine o dată de tipul tip.
Comparând declaraţia de pointer anterioară cu una obişnuită:
tip nume;
putem considera că:
tip *
dintr-o declaraţie de pointer reprezintă tip dintr-o declaraţie
obişnuită. De aceea, construcţia
tip *
se spune că reprezintă un tip nou, tipul pointer.
Există cazuri în care dorim ca un pointer să fie utilizat cu mai
multe tipuri de date. În acest caz, la declararea lui nu dorim să
specificăm un tip anume. Aceasta se realizează folosind cuvântul
cheie void:
void *nume;
Exemple:
1)
void main (void)
{ int x,y;
int *p;
y=x+10; // aceast atribuire este echivalenta cu secventa urmatoare
p=&x;
y=*p+100;
x=y; // este echivalenta cu secventa
p=&x;
(*p)++;
}
2) funcţia permutare de mai jos realizează transferul parametrilor
prin adresă:
void permutare (int *x, int *y) // x si y sunt pointeri
{ int temp;
temp = *x; // temp ia valoarea ce se afla la adresa continuta in x
*x=*y; // in zona a carei adresa se afla in x se transfera
continutul
// zonei a carei adresa se afla in y
*y=temp; // in zona a carei adresa se afla in y se transfera valoarea
// lui temp
}
Apelul funcţiei permutare se face astfel:
permutare (&a, &b);
pentru a schimba valorile lui a cu b.
6.2. LEGĂTURA DINTRE POINTERI ŞI TABLOURI
Numele unui tablou este un pointer deoarece el are ca valoare adresa
primului său element. Totuşi există o diferenţă între numele unui
tablou şi o variabilă de tip pointer, şi anume unui nume de tablou
nu i se poate atribui altă adresă. Deci numele unui tablou trebuie
considerat ca fiind un pointer constant.
Dacă x este un parametru formal ce corespunde unui parametru efectiv
care este un nume de tablou, x poate fi declarat fie ca tablou fie ca
pointer spre tipul tabloului.
Exemplu:
Fie funcţia cu antetul următor:
unsigned lungime (char x[ ]);
Să presupunem că această funcţie determină lungimea unui şir de
caractere şi se poate apela prin:
l=lungime(tablou);
unde tablou este de tip caracter.
Antetul funcţiei lungime poate fi schimbat în felul următor:
unsigned lungime (char *x);
Cele două declaraţii sunt identice deoarece declaraţia:
char x[ ];
defineşte pe x ca numele unui tablou de tip caracter; dar atunci el
este un pointer spre caractere deci se poate declara prin:
char *x;
6.3. OPERAŢII CU POINTERI
Asupra pointerilor se pot face diferite operaţii. Deoarece ei conţin
adrese atunci operaţiile se realizează cu adrese.
6.3.1. Incrementare şi decrementare
Operatorii de incrementare şi decrementare se pot aplica variabilelor
de tip pointer.
Efectul:
operatorul de incrementare aplicat asupra unui operand de tip pointer
spre tipul tip măreşte adresa conţinută de operand cu numărul de
octeţi necesari pentru a păstra o dată de tipul tip.
operatorul de decrementare se execută în mod analog, cu singura
diferenţă că în loc să se mărească adresa, ea se micşorează cu
numărul corespunzător de octeţi.
De obicei decrementările şi incrementările adreselor sunt mai rapide
ca execuţie când se au în vedere prelucrări de tablouri.
Exemplu:
int tab[10];
int *p;
int i=0;
p=&tab[i];
p++; // p contine adresa lui tab[1]
// cu p se pot face referiri la orice element de tablou
6.3.2. Adunarea şi scăderea unui întreg dintr-un pointer
Dacă p este un pointer, sunt corecte expresiile de forma:
p+n şi p-n
unde n este de tip întreg.
Efectul:
expresia p+n măreşte valoarea lui p cu n*nr_tip, unde nr_tip este
numărul de octeţi necesari pentru a memora o dată de tipul tip spre
care pointează p;
analog expresia p-n micşorează valoarea lui p cu n*nr_tip.
Dacă x este un tablou de tipul tip, atunci x este pointer, deci o
expresie de forma:
x+n;
este corectă şi deoarece x este un pointer spre primul său element
x[0], x+n va fi un pointer spre elementul x[n]. Rezultă că valoarea
elementului x[n] se poate reprezenta prin expresia:
*(x+n);
Astfel variabilele cu indici se pot înlocui prin expresii cu
pointeri. Aceasta permite ca la tratarea tablourilor să se folosească
expresii cu pointeri în locul variabilelor cu indici. Versiunile cu
pointeri sunt de obicei optime în raport cu cele realizate prin
intermediul indicilor.
6.3.3. Compararea a doi pointeri
Doi pointeri care pointează spre elementele aceluiaşi tablou pot fi
comparaţi folosind operatorii de relaţie şi de egalitate. Astfel,
dacă p şi q sunt doi pointeri care pointează spre elementele tab[i],
respectiv tab[j] ale tabloului tab, expresiile următoare au sens:
p<q p!=j p= =q.
Observaţii:
1o. Pointerii nu pot fi comparaţi decât în condiţiile amintite mai
sus (deci dacă sunt pointeri spre elementele aceluiaşi tablou).
2o. Operatorii = = şi != permit compararea unui pointer şi cu o
constantă simbolică specială având numele NULL. Aceste comparaţii
permit să stabilim dacă un pointer conţine o adresă sau nu. Astfel,
dacă expresia:
p= = NULL
este adevărată, p nu conţine o adresă. Dacă expresia respectivă
are valoarea fals atunci p conţine o adresă. Constanta simbolică
NULL este definită în fişierul stdio.h
.
6.3.4. Diferenţa a doi pointeri
Doi pointeri care pointează spre elementele aceluiaşi tablou pot fi
scăzuţi. Rezultatul diferenţei a doi pointeri este definit astfel:
fie t un tablou de un tip oarecare şi p şi q doi pointeri, p conţine
adresa elementului t[i] iar q conţine adresa elementului t[i+n].
Atunci diferenţa q-p are valoarea n.
6.3.5. Exemple
Vom da câteva funcţii asupra şirurilor de caractere:
funcţia lungime
unsigned lungime (char*x)
{ int i;
for (i=0; *x != ‘\0’; i++) x++; // sau for (i=0; *x++; i++);
return i;
}
funcţia copiază
void copiaza(char *x, char *y) // copiaza din zona de adresa y
// in zona de adresa x
{ while(*x++ = = *y++); }
funcţia concateneaza
void concateneaza (char *x, char *y)
// concateneaza sirul de adresa y la sfarsitul sirului
// de adresa x
{ while (*x) x++; // avans de adresa pana la sfarsitul sirului x
while (*x++= *y++);
}
funcţia compara
int compara (char *x, char *y)
{ while (*x= = *y)
{ if (*x= = ‘\0’) return 0;
x++;
y++;
return *x - *y; // daca diferenta caracterelor este
// negativa atunci x<y altfel x>y
}
}
6.4. ALOCAREA DINAMICĂ A MEMORIEI
Biblioteca standard a limbajului C pune la dispoziţia utilizatorului
funcţii care permit alocarea de zone de memorie în timpul execuţiei
programului. O astfel de zonă de memorie poate fi utilizată pentru a
păstra date temporare. Zona respectivă poate fi eliberată în
momentul în care nu mai sunt necesare datele care au fost păstrate
în ea. Alocarea de zone de memorie şi eliberarea lor în timpul
execuţiei programelor permite gestionarea optimă a memoriei de către
programator. Un astfel de mijloc de gestionare a memoriei îl vom numi
alocare dinamică a memoriei.
Vom indica două funcţii din bibloteca limbajului C utilizate
frecvent în alocarea dinamică a memoriei. Prototipurile lor se află
în fişierele standard alloc.h şi stdlib.h, deci pentru a le utiliza
vom include unul din aceste fişiere.
Funcţia malloc permite alocarea unui bloc de memorie a cărui
dimensiune se specifică în octeţi. Funcţia returnează un pointer
spre începutul zonei alocate. Întrucât acest pointer trebuie să
permită memorarea oricărui tip de dată în zona alocată, el este de
tip void *.
Prototipul funcţiei este:
void *malloc (unsigned n);
unde n este numărul de octeţi al zonei de memorie care se alocă. În
cazul în care n este prea mare, funcţia returnează pointerul NULL.
Funcţia free eliberează o zonă de memorie alocată prin malloc.
Prototipul ei este:
void free (void *p);
unde p este pointerul returnat de malloc la alocare, deci este
pointerul spre începutul zonei care se eliberează.
Exemplu:
Funcţia memchar memorează un şir de caractere într-o zonă de
memorie alocată prin funcţia malloc. Ea returnează adresa de
început a zonei în care s-a salvat şirul de caractere, deci
returnează un pointer spre tipul char.
#include <stdio.h>
#include <alloc.h>
#include <string.h>
char *memchar (char *s)
{ char *p;
if ((p=(char *)malloc(strlen(s)+1) ) != NULL
{ strcpy (p,s);
return p;
} else
return NULL;
}
Observaţii:
1o. În fişierul stdio.h există definiţia constantei NULL.
2o. Fişierul alloc.h s-a inclus deoarece conţine prototipul funcţiei
malloc.
3o. Fişierul string.h conţine prototipurile funcţiilor strlen şi
strcpy.
4o. Funcţia malloc se apelează pentru a rezerva strlen(s)+1 octeţi;
strlen returnează numărul de octeţi cuplaţi de caracterele proprii
ale lui s (fără caracterul NULL). Cum în zona de memorie rezervată
prin malloc se păstrează şi caracterul NULL, lungimea returnată de
funcţia strlen s-a mărit cu 1.
5o. Pointerul returnat de malloc a fost convertit spre char *, deoarece
el este de tip void *. Acest pointer se atribuie lui p, deci p
pointează spre începutul zonei de memorie alocate prin apelul
funcţiei malloc. Se testează dacă acest pointer este diferit de NULL
(deci dacă s-a putut aloca memoria de dimensiunea cerută). În caz
afirmativ, se transferă şirul prin apelul funcţiei strcpy,
returnându-se apoi valoarea pointerului p.
6.5. POINTERI SPRE FUNCŢII
Numele unei funcţii este un pointer spre funcţia respectivă. El
poate fi folosit ca parametru efectiv la apeluri de funcţii. În felul
acesta, o funcţie poate transfera funcţiei apelate un pointer spre o
funcţie. Aceasta, la rândul ei, poate apela funcţia care i-a fost
transferată în acest fel.
Exemplu:
Un exemplu matematic în care este nevoie de un astfel de transfer este
cel cu privire la calculul aproximativ al integralelor definite. Să
presupunem că dorim să calculăm integrala definită din funcţia
f(x), între limitele a şi b, folosind formula trapezului:
I= h((f(a)+f(b))/2 +f(a+h)+f(a+2h)+. . . +f(a+(n-1)h)
unde
h=(b-a)/h.
În continuare construim o funcţie care calculează partea dreaptă a
acestei relaţii. Numim aria_trapez această funcţie.
Observaţii:
1o. Deoarece funcţia f(x) din relaţia de mai sus nu este definită
în acest moment, ea trebuie să figureze printre parametrii funcţiei
aria_trapez, împreună cu limitele de integrare şi valoarea lui n.
2o. Funcţia aria_trapez returnează valoarea aproximativă a
integralei şi ea se va apela printr-o expresie de atribuire, de
exemplu:
aria=aria_trapez (a, b, n, f);
3o. Funcţia aria_trapez returnează o valoare flotantă în dublă
precizie. De asemenea, şi funcţia f(x) returnează o valoare
flotantă în dublă precizie. De aici rezultă că prototipul
funcţiei aria_trapez este următorul:
double aria_trapez (double a, double b, int n, double (*f)());
sau
double aria_trapez (double, double, int , double (*)());
4o. Este necesar ca înaintea apelului funcţiei aria_trapez funcţia
f(x) să fie definită sau să fie prezent prototipul ei , de exemplu:
double f();
5o. Construcţia double (*f) () se interpretează în felul următor:
- *f înseamnă că f este un pointer;
- (*f)() înseamnă că f este un pointer spre o funcţie;
- double (*f) () înseamnă că f este un pointer spre o funcţie care
returnează o valoare flotantă în dublă precizie.
6o. Trebuie să se includă *f între paranteze, deoarece construcţia
double *f(); este corectă, dar înseamnă altceva, parantezele rotunde
fiind prioritare operatorului unar *. În acest caz, se declară f ca o
funcţie ce returnează un pointer spre o valoare flotantă în dublă
precizie.
7o. Ultimul parametru formal al funcţiei aria_trapez corespunde
parametrului efectiv f şi deci el trebuie declarat ca şi pointer spre
o funcţie ce returnează o valoare flotantă în dublă precizie.
Conform observaţiei 5), dacă p este numele parametrului formal ce
corespunde parametrului efectiv f, atunci p se declară astfel:
double (*p)();
8o. În corpul funcţiei aria_trapez va trebui să apelăm funcţia
f(x) pentru a calcula valorile:
f(a), f(b), f(a+h), . . . , f(a+(n-1)h).
În momentul programării funcţiei aria_trapez, nu se cunoaşte numele
funcţiei concrete, ci numai pointerul p spre ea. De aceea, vom
înlocui numele funcţiei prin *p, deci vom folosi apelurile:
(*p)(a), (*p)(b), (*p)(a+h), . . . ,(*p)(a+(n-1)h)
double aria_trapez(double x, double y, int m, double(*p)());
{ double h,s;
int i;
h=(y-x)/m;
for (i=1, s=0.0; i<m; i++) s+=(*p)(x+i*h);
s+=((*p)(x) + (*p)(y))/2;
s=h*s;
return s;
}
Vom utiliza funcţia aria_trapez pentru a calcula integrala definită
din funcţia sin(x2) pe intervalul [0,1], cu o eroare mai mică decât
10-8. Vom nota cu In următoare sumă:
In= h((f(a)+f(b))/2 +f(a+h)+f(a+2h)+. . . +f(a+(n-1)h)
Paşii algoritmului sunt următorii:
Pasul 1. Se alege o valoare iniţială pentru n, de exemplu 10.
Pasul 2. Se calculează In.
Pasul 3. Se calculează I2n prin dublarea lui n.
Pasul 4. Dacă |In-I2n| < 10-8, algoritmul se întrerupe şi valoarea
integralei, cu precizia admisă, este I2n; altfel se dublează n, se
pune In=I2n; n, şi se trece la pasul 3.
#define A 0.0
#define B 1.0
#define N 10
#define EPS 1e-8
#include <stdio.h>
#include <math.h>
double sinxp(double); // prototipul functiei sin(x*x)
double aria_trapez(double, double, int, double (*)());
void main (void) // functia principala
{ int n=N;
double in, i2n, vabs;
in=aria_trapez (A, B, n, sinxp);
do {
n=n*2;
i2n=aria_trapez(A, B, n, sinxp);
if ((vabs= in-i2n) < 0) vabs = -vabs;
in=i2n;
} while (vabs >= EPS);
printf (“valoarea integralei este : %g.10\n”,i2n);
}
double aria_trapez(double x, double y, int m, double(*p)());
{ double h,s;
int i;
h=(y-x)/m;
for (i=1, s=0.0; i<m; i++) s+=(*p)(x+i*h);
s+=((*p)(x) + (*p)(y))/2;
s=h*s;
return s;
}
double sinxp (double x)
{ return sin (x*x); }
6.6. TRATAREA PARAMETRILOR DIN LINIA DE COMANDĂ
În linia de comandă folosită la apelul execuţiei unui program se
pot utiliza diferiţi parametri. Aceşti parametri pot fi utilizaţi
folosind parametrii argc şi argv ai funcţiei principale.
Parametrul argc este de tip întreg şi indică numărul de parametri
din linia de comandă.
Parametrul argv este un tablou de pointeri spre zonele în care sunt
păstraţi parametrii liniei de comandă. Aceştia se consideră
şiruri de caractere.
Astfel antetul funcţiei principale va fi :
main (int argc, char *argv[ ])
Exemplu:
Considerăm că la lansarea programului prog s-au furnizat parametrii:
MARTIE 1956
În acest caz argc=4, iar tabloul argv conţine pointerii:
- argv[0] - pointer spre numele programului (calea, numele şi
extensia .EXE
- argv[1] - pointer spre şirul “31”;
- argv[2] - pointer spre şirul “MARTIE”;
- argv[3] - pointer spre şirul “1991”.
Observaţii:
1o. Lansarea unui program se face cu prima instrucţiune a funcţiei
principale. Deci parametrii argc şi argv au deja în acest moment
valorile indicate mai sus, putând fi analizaţi chiar cu prima
instrucţiune executabilă.
2o. În mod frecvent, aceşti parametrii reprezintă diferite opţiuni
ale programului, date calendaristice, nume de fişiere, etc.
3o. argv[0] este întotdeauna pointerul spre numele fişierului cu
imaginea executabilă a programului.
void main ( int argc, char *argv[]) // va afisa parametrii din linia
de comanda
{ int i;
for (i=0; i<argc; i++;) printf (“%s\n”,argv[i]);
}
6.7. MODIFICATORUL const
Am văzut anterior că o constantă se defineşte prin caracterele
care intră în compunerea ei. De asemenea, în acelaşi capitol s-a
arătat că putem atribui un nume unei constante printr-o construcţie
#define. Un astfel de nume se spune că este o constantă simbolică
şi el se substituie prin şirul de caractere care şi corespunde, în
faza de preprocesare.
Un alt mod de a defini o constantă este acela de a folosi
modificatorul const într-o declaraţie. Printr-o astfel de
declaraţie, unui nume i se poate atribui o valoare constantă. În
acest caz, numele respectiv nu mai este tratat de preprocesor şi el
poate fi folosit în program în mod analog cu numele variabilelor.
Unui astfel de nume declarat cu ajutorul modificatorului const nu i se
poate schimba valoarea printr-o expresie de atribuire, ca şi unei
variabile obişnuite.
Formatele declaraţiei cu modificatorul const sunt următoarele:
tip const nume = valoare;
const tip nume = valoare;
tip const nume;
const tip nume;
const nume = valoare;
const nume;
Exemplu:
void main (void)
{ const i=10; // i devine egal cu 10; nu este posibila o atribuire
i=0
const pi = 3.1415926
char *const s=”martie”; // s este un pointer constant spre zona in
care este
// pastrat sirul de caractere “martie”. Valoarea lui s
// nu poate fi schimbata dar continutul zonei spre
// care pointeaza s poate fi schimbat
*s= ‘1’; // schimba litera m cu 1
*(s+1)=’2’ // schimba litera a cu 2
char const *s=”aprilie”; // s este un pointer spre o zona
constanta.
// valoare lui s poate schimbata dar sirul “aprilie”
// nu poate fi modificat
const char *s=”aprilie” // este identica cu declaratia de mai
sus.
}
Modificatorul const se foloseşte frecvent la declararea parametrilor
formali de tip pointer. O astfel de declaraţie are formatul:
const tip *nume_parametru_formal;
Un parametru formal declarat prin construcţia :
tip *nume_parametru_formal;
corespunde unui parametru efectiv a cărui valoare este o adresă. La
apel, valoarea parametrului formal devine egală cu această adresă.
Datorită acestui fapt, funcţia apelată poate să modifice data
aflată la adresa respectivă. Dacă se foloseşte modificatorul const
utilizat la declararea unui astfel de parametru formal atunci se
interzice funcţiei apelate să modifice data de la adresa
recepţionată la apel de către parametrul formal corespunzător.
Acest mecanism este folosit frecvent în cazul funcţiilor de tratare a
şirurilor de caractere.
De exemplu funcţia strlen din biblioteca standard a limbajului C are
prototipul:
unsigned strlen (const char *s);
Ea se apelează prin expresii de atribuire de forma:
i=strlen(x);
unde x este un pointer spre o zonă de memorie în care se află un
şir de caractere.
Funcţia strlen determină lungimea şirului aflat la adresa
recepţionată de către parametrul s. Ea nu are voie să modifice
şirul respectiv şi din această cauză parametrul s se declară
folosind modificatorul const.
6.8. STIVA
Prin stivă (stack în engleză) înţelegem o mulţime ordonată de
elemente la care accesul se realizează conform principiului ultimul
venit primul servit. În engleză stiva se mai numeşte şi listă LIFO
(Last In First Out).
O modalitate simplă de a implementa o stivă este păstrarea
elementelor ei într-un tablou unidimensional. În acest tablou se vor
păstra elementele stivei unul după altul. De asemenea, ele se pot
scoate din tablou în ordinea inversă păstrării lor. La un moment
dat se poate scoate ultimul element pus pe stivă şi numai acesta.
Despre ultinul element pus în stivă se spune că este vârful
stivei, iar despre primul element că este baza stivei.
Accesul este pemis doar la vârful stivei:
un element se poate pune pe stivă numai după elementul aflat în
vârful stivei şi după această operaţie el ajunge vârful stivei;
se poate scoate de pe stivă numai elementul aflat în vârful stivei
şi după această operaţie în vârful stivei rămâne elementul care
a fost pus pe stivă înaintea lui.
Vom numi stack tablou de tip int afectat stivei şi next variabila care
indică prima poziţie liberă din stivă. Deci stack[0] este baza
stivei iar stack[n] va fi vârful stivei. Vom defini mai multe funcţii
asociate tabloului stack:
- push funcţia care pune un element în stivă;
- pop funcţia care scoate un element din stivă;
- clear funcţia de iniţializare a stivei; după apelul ei stiva
devine vidă;
#define MAX 1000
static int stack[1000];
static next = 0; // indicele pentru baza stivei
void push(int x) // pune pe stiva valoarea lui x
{ if (next < MAX)
stack [next++]=x;
else
printf (“stiva este depasita\n”);
}
int pop() // scoate elementul din varful stivei si returneaza
valoarea lui
{ if (next >0)
return stack [--next];
else { printf (“stiva este vida\n”);
return 0;
}
}
void clear(void) // videaza stiva
{ next=0;
}
LECŢIA 7.
RECURSIVITATE
Spunem că o funcţie C este recursivă dacă ea se autoapelează
înainte de a se reveni din ea. Funcţia se poate reapela fie direct,
fie indirect (prin intermediul altor funcţii).
La fiecare apel al unei funcţii, parametrii şi variabilele locale se
alocă pe stivă într-o zonă independentă. De asemenea, orice apel
recursiv al unei funcţii va conduce la o revenire din funcţie la
instrucţiunea următoare apelului respectiv. La revenirea dintr-o
funcţie se realizează curăţarea stivei, adică zona de pe stivă
afectată la apel parametrilor şi variabilelor automatice se
eliberează.
Un exemplu simplu de funcţie apelata recursiv este funcţia de calcul
al factorialului. Putem defini recursiv funcţia factorial astfel:
factorial(n)= 1, dacă n=0
factorial(n)=n*factorial(n-1), dacă n>0
În limbajul C avem :
double factorial (int)
{ if (n= = 0) return 1.0;
else return n*factorial(n-1);
}
Observaţii:
1o. În general, o funcţie recursivă se poate realiza şi nerecursiv,
adică fără să se autoapeleze.
2o. De obicei, recursivitatea nu conduce nici la economie de memorie
şi nici la execuţia mai rapidă a programelor. Ea permite însă o
descriere mai compactă şi mai clară a funcţiilor. Acest lucru
rezultă şi din exemplul de mai sus de calcul al factorialului.
3o. În general, funcţiile recursive sunt de preferat pentru procese
care se definesc recursiv. Există şi excepţii. De exemplu algoritmul
de generare a permutărilor de n obiecte poate fi descris recursiv
astfel: având în memorie toate cele (n-1)! permutări, atunci
permutările de n obiecte se generează înserând pe n în toate
poziţiile posibile ale fiecărei permutări de n-1 obiecte. Dar ne
aducem aminte că 10!=3628800 şi capacitatea stivei se depăşeşte
repede.
Exemple:
Programul determină recursiv cmmdc (algoritmul lui Euclid) a două
numere întregi (de tip long):
cmmdc (a,b) = b, dacă a%b =0 (restul împărţirii lui a la b e
zero)
cmmdc (a,b) = cmmdc (b,a%b), în caz contrar.
#include <iostream.h> #include <conio.h>
long cmmdc(long a, long b) { if (!(a % b)) return b;
else return cmmdc(b, a % b);
}
void main(void)
{ long x,y;
clrscr();
cout << "dati un numar natural=";
cin >> x;
cout << "dati alt numar natural=";
cin >> y;
cout << "cmmdc(" << x << "," << y << ")=" << cmmdc (x,y);
}
Am folosit funcţiile de intrare / ieşire cin şi cout, imediat se
observă modul lor de utilizare.
Programul determină recursiv suma unor elemente de tablou
unidimensional
a[1]+a[2]+ . . . + a[n]
#include <iostream.h> #define MAX 100 int a[MAX];
// suma(n)= 0, daca n=0
// suma(n)=suma(n-1) + a[n] daca n>0
int suma(int n) { if (!n) return 0; else return a[n]+suma(n-1); }
void main(void) {int n,i; cout << "dati n= "; cin >> n; for (i=1;
i<=n; i++) { cout << "a[" << i << "]= ";
cin >> a[i];
}
cout << "suma numerelor este " << suma(n);
}
Programul determină recursiv termenul al n-lea din şirul lui
Fibonacci definit după cum urmează:
fibonacci[0]=0
fibonacci[1]=1
fibonacci[n]=fibonacci[n-1]+fibonacci[n-2], dacă n>1
#include<iostream.h>
long fibonacci (long n) {if (!n) return 0; else if (n==1) return 1;
else return fibonacci(n-1) + fibonacci(n-2); }
void main (void) { long n; cout << "dati n = "; cin >> n; cout <<
"fibo(" << n << ") =" << fibonacci (n);
}
Programul determina maximul dintr-un vector de numere astfel:
M(n)= a[1] dacă n=1 M(n)= max { M(n-1),a[n] } dacă n>1
#include<iostream.h> #define MAX(x,y) (x > y ? x : y) int a[100]; int
M(int n) { if (n= =1) return a[1];
else return MAX (M(n-1), a[n]); } void main(void) {int n,i; cout <<
"dati n="; cin >> n; for (i=1; i<=n; i++) { cout << "a[" << i << "]=
"; cin >> a[i]; } cout << "maximul este " << M(n); }
5) Programul afisează un şir de caractere în mod recursiv, caracter
cu caracter, considerând că şirul de caractere este format din
primul caracter(capul) + restul şirului de caractere (coada).
#include <iostream.h>
#include <conio.h>
#define max 100
char sir [max];
int n;
void afis (int m)
{ if (m = = n+1) return;
else { cout << sir[m];
afis(m+1);
}
}
void main (void)
{int i;
do { cout << "\ndati lungimea sirului =";
cin >> n;
}
while ( (n< 0) || (n > max));
for(i=1; i<=n; i++)
{ cout << "sir[" << i << "]=";
cin >> sir[i];
}
afis(1);
getch();
}
6) Programul ce urmează e oarecum asemănător cu exemplul anterior
doar că afişează şirul de caractere de la sfârşit spre început.
#include <iostream.h>
#include <conio.h>
#define max 100
char sir [max];
void afis (int m)
{ if (m==0) return;
else { cout << sir[m];
afis(m-1);
}
}
void main (void)
{int n,i;
do {cout << "\ndati lungimea sirului ="), cin >> n;}
while ( (n< 0) || (n > max));
for(i=1; i<=n; i++)
{ cout << "sir[" << i << "]=";
cin >> sir[i];
}
afis(n);
getch();
}
7) Programul sortează prin metoda quicksort un vector de numere
întregi:
#define dim 50
#include <stdio.h>
#include <conio.h>
int x[dim+1],i,n;
void tipsir ()
{ for (i=1; i<=n; i++)
{ printf("%3d",x[i]);
if (!(i % 20)) printf ("\n");
}
}
void quik(int st, int dr)
{int i,j,y;
i=st; j=dr; y=x[i];
do { while ((x[j] >= y) && (i<j)) j - -;
x[i]=x[j];
while ((x[i] <= y) && (i<j)) i++;
x[j]=x[i];
}
while (i != j);
x[i]=y;
if (st < i-1) quik(st,i-1);
if (i+1 < dr) quik(i+1,dr);
x[j]=x[i];
}
void citire (void)
{ int cod = 0;
n = dim+1;
while ( n <= 0 || n > dim || ! cod )
{ printf ("\ndati dim. sir:");
cod=scanf ("%d",&n);
}
i = 1;
while (i<=n)
{ printf ("x[%2d]=",i);
scanf ("%d", &x[i]);
i++;
}
}
void main(void)
{ clrscr();
citire();
clrscr();
printf ("\n\nsir initial\n");
tipsir();
quik(1,n);
printf ("\n\nsir sortat\n");
tipsir();
getche();
}
LECŢIA 8.
STRUCTURI, TIPURI UTILIZATOR
După cum am văzut datele de acelaşi tip se pot grupa în tablouri.
Limbajul C permite gruparea unor date de tipuri diferite sub alte forme
de organizare numite structuri.
Tablourile au un tip şi anume tipul comun elementelor lor. Astfel,
distingem tablouri de tip întreg, de tip caracter, de tip flotant,
etc. În cazul structurilor, nu mai avem un tip comun. Fiecare
structură reprezintă un nou tip de date, tip care se introduce prin
declaraţia structurii respective.
Un exemplu simplu de structură este data calendaristică, cu
componentele următoare:
ziua;
luna;
anul.
unde: ziua şi anul sunt date de tip întreg iar luna este un tablou de
caractere.
Structura ca şi tabloul, este o mulţine ordonată de elemente. În
exemplul de mai sus se consideră că ziua este primul ei element, luna
este al doilea iar anul este ultimul ei element. Trebuie să precizăm
că referirea la componentele unei structuri nu se mai face cu ajutorul
indicilor ci prin calificare.
8.1. DECLARAŢIA DE STRUCTURĂ
O structură se poate declara în mai multe feluri, astfel:
Formatul 1:
struct nume_structura
{ lista_declaratii
};
Cu ajutorul acestui format se introduce un nou tip de dată cu numele
nume_structură. Lista de declaraţii este formată din declaraţii
obişnuite. Tipul data_calendaristica îl putem introduce astfel:
struct data_calendaristica
{ int ziua;
char luna[11];
int anul;
};
O astfel de declaraţie se numeşte declaraţie de tip. Să reţinem
că unui nou tip de date nu i se alocă memorie, el este doar
contabilizat ca un nou tip utilizator pe lângă tipurile predefinite
ale limbajului C.
Formatul 2:
struct nume_structura
{ lista_declaratii
}lista_variabile;
Un astfel de format introduce tipul utilizator nume_structura şi
declară o listă de varibile în care fiecare element din listă are
tipul nume_structură. Prin exemplu următor se introduc variabilele
dc1 şi dc2 ca date elementare de tipul data_calendaristica şi tabloul
dc de 13 componente.
struct data_calendaristica
{ int ziua;
char luna[11];
int anul;
} dc1, dc2, dc[13];
Formatul 3:
struct { lista_declaraţii
} lista_variabile;
Acest format se foloseste dacă nu vrem sa dăm nume noului tip
structurat şi totodată dacă nu mai vrem să-l folosim. Deci nu vom
mai pute declara alte date de tipul structurat nou introdus pentru că
tipul nu are nume.
Exemplu:
struct { int ziua;
char luna[11];
int anul;
} dc1, dc2, dc[13];
S-au declarat varibilele dc1, dc2 şi tabloul dc având noul tip
structurat utilizator dar nu se mai doreşte să declarăm alte date de
acest tip.
Observaţii:
1o. Dacă se foloseşte formatul 1 atunci pentru a declara date de
tipul utilizator nou introdus se foloseşte o construcţie de forma:
struct nume_ structura lista_variabile;
Compilatorul alocă memorie varibilelor din lista de variabile,
tratând această construcţie ca şi declaraţiile obişnuite.
2o. Componentele unei structuri pot fi ele însele date structurate. O
componentă care nu este structurată se numeşte componentă
elementară.
3o. Ca şi în cazul celorlalte tipuri de variabile se pot defini
structuri globale, statice sau automatice. Structurile statice se
declară precedând declaraţiile lor prin cuvântul static, iar cele
externe prin cuvântul cheie extern.
4o. Elementele unei date de tip structură pot fi iniţializate după
modelul iniţializării variabilelor care au tipuri predefinite.
Exemple:
1) Introducem tipul utilizator data_calendaristica astfel:
struct data_calendaristica
{ int ziua;
char luna[11];
int anul;
};
pentru a iniţializa o dată de tipul data_calendaristică vom scrie:
struct data_calendaristica dc1={31, “martie”, 1956};
2) Dacă declarăm un nou tip date_personale şi în care vrem să
folosim tipul data_calendaristica, vom scrie:
struct date_personale
{ char nume[30];
char adresa[50];
struct data_calendaristica data_nasterii, data_angajarii;
};
8.2. ACCESUL LA ELEMENTELE UNEI STRUCTURI
Pentru a avea acces la componentele unei date structurate va trebui
să folosim o calificare de forma:
nume_data_structurata.nume_componenta
Astfel dacă avem tipul structurat data_calendaristica introdus in
exemplele anterioare şi declarăm data dc astfel:
struct data_calendaristica dc;
atunci pentru a ne referi la componentele datei dc vom folosi
construcţiile:
dc.ziua
dc.anul
dc.luna (atenţie este pointer spre caractere)
Dacă avem declarat un tablou astfel:
struct data_calendaristica tdc[10];
atunci pentru fiecare componentă i ne vom referi astfel:
tdc[i].ziua
tdc[i].anul
tdc[i].luna (este pointer)
tdc[i].luna[0], tdc[i].luna[1], . . . , tdc[i].luna[11]
Ca şi tablourile structurile se pot transfera prin parametrii,
transferând un pointer spre data structurată respectivă, adică
adresa de început a zonei alocate structurii. Deci, printr-un apel de
forma:
functie(&data_structurata);
se transferă funcţiei functie adresa de început a zonei alocate
structurii data_structurata. Dacă data_structurata este o structura de
tipul tip, atunci antetul funcţiei functie este următorul:
void functie(tip *p)
unde p este pointer spre tipul structurat tip.
Pentru data structurată dc de tipul data_calendaristica antetul
funcţiei functie este:
void functie(struct data_calendaristica *p)
iar apelul pentru data dc se face
functie(&dc);
Printr-un astfel de apel, funcţia apelată nu are acces la numele
datei structurate transferate, ci numai la pointerul spre ea. De aceea
se pune problema accesului la componentele datei structurate prin
pointerul la ea. În acest caz numele datei structurate se va înlocui
prin *p. Deci, în cazul datei structurate dc, transferate ca şi mai
sus, în locul construcţiei
dc.zi
vom scrie:
(*p).zi
înlocuind numele datei structurate dc prin *p, unde p este un pointer
spre dc.
Observaţie:
1o. Parantezele rotunde din construcţia de mai sus sunt obligatorii,
deoarece punctul este un operator prioritar operatorului unar *.
2o. Construcţia de mai sus poate fi înlocuită prin p->zi care este
identică cu ea. Simbolul -> se compune din caracterele ‘-‘ şi
‘>’ scrise unul după celălalt fără spaţiu între ele. El se
numeşte săgeată şi este considerat a fi un operator cu aceeaşi
prioritate ca şi punctul, deci de prioritate maximă.
8.3. ATRIBUIRI DE NUME PENTRU TIPURI DE DATE
După cum ştim tipurile de bază ale limbajului C, numite şi tipuri
predefinite se identifică printr-un cuvânt cheie (int, char, float,
etc). Totodată prin instrucţiunea struct, programatorul poate să
introducă un tip nou. Programatorul poate să atribuie un nume unui
tip (predefinit sau utilizator) cu ajutorul construcţiei:
typedef tip nume_nou_tip;
unde:
tip este numele unui tip predefinit sau al unui tip utilizator
(introdus cu struct);
nume_nou_tip este noul nume atribuit tipului respectiv.
După ce s-a atribuit un nou nume unui tip, numele respectiv poate fi
utilizat pentru a declara date de acel tip, la fel cum se utilizează
în declaraţii cuvintele cheie int, char, float, etc.
Observaţii:
1o. De obicei numele atribuit unui tip se scrie cu litere mari.
2o. Un exemplu de astfel de nume există în fişierul stdio.h pentru
tipul fişier, căruia i s-a atribuit numele FILE.
Exemple:
Fie declaraţiile:
typedef int INTREG;
typedef float REAL;
În continuare, denumirile INTREG şi REAL se pot folosi la fel ca şi
cuvintele cheie int şi float. Cu alte cuvinte, declaraţia:
INTREG i, j, tablou[10];
este identică cu declaraţia următoare:
int i, j, tablou[10];
Analog:
REAL x, y, z;
este identică cu declaraţia:
float x, y, z;
typedef struct data_calendaristica
{ int ziua;
char luna[11];
int anul;
} DC;
Prin această declaraţie se atribuie denumirea DC tipului structurat
data_calendaristica. În continuare putem declara date de tip DC:
DC data_nasterii, data_angajarii;
DC data_curenta ={31,”august”,1998};
typedef int *PI;
Prin această declaraţie se introduce un sinonim pentru tipul pointer
spre întregi: int *.
Putem să declarăm în continuare pointeri spre întregi astfel:
PI p;
care este echivalentă cu:
int *p;
4) Declaraţia typdef struct
{ double real;
double imaginar;
} COMPLEX;
introduce numele COMPLEX pentru datele de tip complex.
Funcţia următoare returnează modulul unui număr complex:
typedef struct
{ double real;
double imaginar;
} COMPLEX;
#include <math.h>
double modul (COMPLEX *x) // returneaza modulul numarului
// spre care pointeaza x
{ return sqrt (x->real * x->real + x->imaginar * x->imaginar);
}
8.4. UNIUNE
Limbajul C oferă utilizatorului posibilitatea de a folosi aceeaşi
zonă de memorie pentru a păstra date de tipuri diferite în momente
diferite ale execuţiei programului. Astfel, de exemplu, putem utiliza
o zonă de memorie pentru a păstra la un moment dat o dată flotantă,
iar ulterior să reutilizăm aceeaşi zonă pentru o dată întreagă
sau de tip pointer. Reutilizările zonelor de memorie conduc la
utilizarea mai eficientă a acesteia, uneori putându-se obţine o
economie substanţială a spaţiului de memorie alocat programului.
O uniune se declară printr-o construcţie asemănătoare declaraţiei
de structură. Deosebirea constă în înlocuirea cuvântului struct
prin union:
union nume
{ tip_membru_1;
. . .
tip_membru_2;
}
Exemplu:
union u
{ int i;
float f;
double d;
};
Prin această declaraţie s-a definit tipul de date u. În continuare,
putem declara date de tipul u printr-o declaraţie de forma:
union u u1;
unde u1 este o dată de tip u căreia i se alocă o zonă de memorie
care poate fi utilizată pentru a păstra date de tipurile int, float
sau double. Deoarece tipul double necesită memoria cea mai mare se
alocă 8 octeţi. Astfel zona u1 poate păstra pe oricare din celelalte
componente ale uniunii dar în momente diferite ale execuţiei
programului.
Accesul la componentele unei uniuni se face la fel ca şi în cazul
structurilor. Astfel, pentru a ne referi la componenta i a uniunii u1
definită în exemplul anterior folosim construcţia:
u1.i
sau dacă p este pointer spre tipul u declarat prin
union u *p;
atunci construcţia
p -> i
permite accesul la componenta i a uniunii spre care pointează p.
Pentru a evita erorile legate de evidenţa în fiecare moment a datei
care se prelucrează se ataşează unei uniuni o dată menită să
indice componenta curentă. Această dată este specificată pentru
fiecare uniune. De exemplu pentru uniunea u1 definită anterior este
important să se ştie dacă zona de memorie conţine un întreg, un
flotant în simplă precizie sau un flotant în dublă precizie. Se
definesc trei constante simbolice:
#define INTREG 1
#define F_SIMPLU 2
#define F_DUBLU 3
Modificăm tipul u ataşând data tip_curent de tip int astfel:
struct u
{int tip_curent;
union { int i;
float f;
double d;
} uu;
};
Declarăm structura us astfel:
struct u us;
În acest caz, în momentul în care se păstrează o dată în zona
rezervată uniunii, se atribuie componentei tip_curent una din
constantele definite anterior:
INTREG, dacă se păstrează un întreg;
F_SIMPLU dacă se păstrează un flotant în simplă precizie;
F_DUBLU dacă se păstrează un flotant în dublă precizie.
Astfel când se foloseşte componenta de tip int se va asocia
atribuirea:
us.tip_curent=INTREG;
Analog se vor folosi ţi atribuirile următoare când se vor folosi
componentele de tip float sau de tip double:
us.tip_curent=F_SIMPLU;
respectiv:
us.tip_curent=F_DUBLU;
În felul acesta, se poate testa, în fiecare moment, tipul de dată
prezent în zona rezervată. Aceasta se poate face printr-o secvenţă
de instrucţiuni if sau prin intermediul instrucţiunii switch.
if (us.tip_curent = = INTREG) // se foloseste us.uu.i
else if (us.tip_curent = = FSIMPLU) // se foloseste us.uu.f
else if (us.tip_curent = = FDUBLU) // se foloseste us.uu.d
else eroare
sau folosind switch avem o construcţie mai clară de forma:
switch (us.tip_curent)
{ case INTREG:
// se foloseste us.uu.i
break;
case FSIMPLU:
// se foloseste us.uu.f
break;
case FDUBLU
// se foloseste us.uu.d
break;
default:
// eroare
}
Programul următor calculează ariile pentru următoarele figuri
geometrice:
cerc;
dreptunghi;
pătrat;
triunghi.
Programul citeşte datele pentru o figură geometrică, calculează
aria figurii respective şi scrie rezultatul:
La intrare se folosesc următoarele formate:
- pentru cerc C raza;
- pentru dreptunghi D lungime laţime;
- pentru pătrat P latură;
- pentru triunghi T latură latură latură;
- sfârşit fişier EOF.
În cazul triunghiului, se utilizează formula lui HERON pentru
calculul ariei:
aria = sqrt (p*(p-a)(p-b)(p-b))
unde p este semiperimetrul, iar a, b, c sunt cele 3 laturi.
#include <stdio.h> #include <math.h> #define PI 3.14159265
#define EROARE -1
#define CERC 1 #define PATRAT 2 #define DREPT 3 #define TRIUNGHI 4
typedef struct
{ int tip; // tipul figurii union
{ double raza // cerc double lp ; // patrat double ld[2] ;
// dreptunghi double lt[3] ; // triunghi } fig; }FIG;
void main (void) // calculeaza arii { double aria,p;
int i;
char car[2];
FIG zfig;
for(; ;) // citeste primul caracter,el defineste tipul figurii
geometrice { printf(“se cere o litera mare\n”); if ((i =
scanf("%1s",car)) == EOF) break; if (i != 1) { printf (" se cere o
litera mare\n");
continue;
}
zfig.tip = EROARE;
switch(car[0])
{case 'C': // cerc
printf("se cere raza cercului in flotanta\n"); i = scanf("%lf",
&zfig.fig.raza);
if(i != 1)
{ printf("se cere raza cercului in flotanta\n");
break;
}
zfig.tip = CERC; // se pastreaza tipul figurii break;
case 'P': // patrat
printf("se cere latura patratului in flotanta\n");
i = scanf("%lf",&zfig.fig.lp);
if( i !=1)
{ printf("se cere latura patratului in flotanta\n");
break;
}
zfig.tip = PATRAT;
break;
case 'D': // dreptunghi
printf("se cer laturile dreptunghiului in flotanta\n");
i = scanf("%lf %lf",&zfig.fig.ld[0],&zfig.fig.ld[1]);
if(i != 2)
{ printf("se cer laturile dreptunghiului in flotanta\n");
break;
}
zfig.tip = DREPT;
break;
case 'T': // triunghi
printf("se cer laturile triunghiului in flotanta\n");
i = scanf("%lf %lf %lf", &zfig.fig.lt[0],
&zfig.fig.lt[1],&zfig.fig.lt[2]);
if(i != 3)
{ printf("se cer laturile triunghiului in flotanta\n");
break;
}
zfig.tip =TRI;
break;
printf("laturile nu formeaza un triunghi\n");
break;
default:
printf("se cere una din literele urmatoare\n");
printf("C pentru cerc\n");
printf("P pentru patrat\n");
printf("D pentru dreptunghi\n");
printf("T pentru triunghi\n");
} // sfarsit switch
switch (zfig.tip)
{case CERC: // aria cercului
printf("raza=%g aria=%g\n", zfig.fig.raza,
PI*zfig.fig.raza*zfig.fig.raza);
break;
case PATRAT: // aria patratului
printf("latura =%g aria=%g\n",zfig.fig.lp, zfig.fig.lp*zfig.fig.lp);
break;
case DREPT: // aria dreptunghiului
printf("lungimea =%g latimea =%g\n", zfig.fig.ld[0], zfig.fig.ld[1]);
printf("aria=%g\n", zfig.fig.ld[0]*zfig.fig.ld[1]);
break;
case TRIUNGHI: // aria triunghiului
p=(zfig.fig.lt[0] + zfig.fig.lt[1] + zfig.fig.lt[2])/2;
if(p>zfig.fig.lt[0] && p>zfig.fig.lt[1] && p>zfig.fig.lt[2])
{p=p*(p-zfig.fig.lt[0])*(p-zfig.fig.lt[1])* (p-zfig.fig.lt[2]);
printf("a=%g b=%g c=%g\n", zfig.fig.lt[0], zfig.fig.lt[1],
zfig.fig.lt[2]);
printf("aria = %g\n",sqrt(p));
}
else { printf (“ laturile nu formeaza un triunghi”);
break;
}
default : // avans pana la newline sau EOF
while ((i = getchar()) != ‘\n’ && i != EOF);
} // sfarsit switch
if (i = = EOF) break;
} // sfarsit for
} // sfarsit main
8.5. CAMP
Limbajul C permite utilizatorului definirea şi prelucrarea datelor pe
biţi. Utilizarea datelor pe biţi este legată de folosirea
indicatorilor care de obicei sunt date care iau numai două valori 0
sau 1.
Nu este justificat ca un astfel de indicator să fie păstrat ca un
întreg pe 16 biţi şi nici măcar pe un octet. Indicatorul poate fi
păstrat pe un singur bit. În acest scop, limbajul C oferă
posibilitatea de a declara date care să se aloce pe biţi (unul sau
mai mulţi biţi). Acest lucru îşi găseşte aplicare în programele
de sistem. Astfel, de exemplu, atributele variabilelor dintr-o tabelă
de simboluri pot fi păstrate pe biţi, ceea ce conduce la o
economisire substanţială a memoriei ocupate de tabela respectivă.
Prin camp înţelegem un şir de biţi adiacenţi conţinuţi într-un
cuvânt calculator. Câmpurile se grupează formând o structură.
Un câmp se declară ca şi o componentă a unei structuri şi el are
tipul unsigned (întreg fără semn). Totodată în declaraţia
câmpului se indică şi dimensiunea lui în biţi.
În general, o structură cu componente câmpuri are forma:
struct
{ camp1;
. . .
campn;
} nume;
unde campi (i=1,...,n) are unul din formatele de mai jos:
unsigned nume : lungime_în_biţi
sau : lungime_în_biţi
Exemplu:
struct
{ unsigned a:1;
unsigned b:1;
unsigned c:2;
unsigned d:2;
unsigned e:3;
} indicatori;
Data indicatori se alocă într-un cuvânt calculator, adică pe 16
biţi. Componentele ei sunt:
a un bit;
b un bit;
c doi biţi;
d doi biţi;
e trei biţi.
La câmpuri ne putem referi la fel ca şi la componentele oricărei
structuri. Deci la indicatorii de mai sus ne putem referi prin
următoarele construcţii:
indicatori.a
indicatori.b
indicatori.c
indicatori.d
indicatori.e
Alocarea biţilor este dependentă de calculator. De obicei biţii se
alocă de la dreapta spre stânga ca în figura de mai jos:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
a
b
c
d
e
Observaţii:
1o. Dacă un câmp nu poate fi alocat în limitele unui cuvânt, el se
alocă în întregime în cuvântul următor.
2o. Nici un câmp nu poate avea o dimensiune mai mare decât 16 biţi.
3o. Formatul fără nume (al doilea format) pentru câmp se foloseşte
pentru cadraje. Acest lucru este util atunci când sunt zone de biţi
neutilizate în cadrul unui cuvânt. De asemenea, utilizarea formatului
cu lungime egală cu zero permite ca alocarea câmpurilor următoare
lui să se facă în cuvântul următor.
4o. O structură care are şi componente câmpuri poate avea şi
componente obişnuite.
5o. Nu se pot defini tablouri de câmpuri.
6o. Unui câmp nu i se poate aplica operatorul adresă.
Câmpurile se utilizează frecvent la scrierea unor programe de sistem,
cum ar fi : drivere pentru periferice, compilatoare, etc.
Utilizarea câmpurilor poate conduce la programe cu o portabilitate
redusă. Totodată, accesul la date pe biţi conduce la creşterea
numărului de operaţii, fiind necesare deplasări şi operaţii pe
biţi suplimentare, fapt ce poate conduce atât la creşterea timpului
de execuţie a programelor, cât şi la creşterea memoriei utilizate.
Ori datele pe biţi se folosesc chiar în ideea de a economisi memorie.
8.6. TIPUL ENUMERAT
Tipul enumerat permite utilizatorului să folosească în program nume
sugestive în locul unor valori numerice. De exemplu, în locul
numărului unei luni calendaristice, se poate folosi denumirea ei:
ian
feb
mar
În locul valorilor 0 şi 1 se pot folosi cuvintele FALS şi
ADEVRAT.
Prin aceasta, se introduce o mai mare claritate în programe, deoarece
valorile numerice sunt înlocuite prin diferite sensuri atribuite lor.
Un tip enumerat se introduce printr-o declaraţie de forma:
enum nume {nume0, nume1, . . . , numen};
Prin această declaraţie se defineşte tipul enumerat nume, iar numei
are valoarea i. O formă mai generală a declaraţiei de mai sus
permite programatorului să forţeze valorile numelor din acoladă. În
acest scop, se pot folosi construcţii de forma:
numei= eci
unde eci este o expresie constantă de tip int.
Cu alte cuvinte, unui nume i se poate atribui o valoare sau valoarea
lui coincide cu a numelui precedent mărită cu 1. Dacă primului nume
din acoladă nu i se atribuie o valoare, el are valoarea 0. Numele
nume0, nume1,. . . , numen trebuie să fie nume diferite. Ele sunt
constante şi valoarea lor se stabileşte prin declaraţia în care au
fost scrise. Domeniul lor de valabilitate este definit de domeniul de
valabilitate al declaraţiei prin care se definesc:
instrucţiunea compusă care conţine declaraţia;
fişierul sursă în care este scrisă declaraţia, dacă este externă
oricărei funcţii.
Valorile atribuite lui nume0, nume1, . . . , numen sunt de obicei
diferite, dar unele pot să şi coincidă.
După ce s-a introdus un tip enumerat, se pot declara date de tipul
respectiv printr-o declaraţie de forma:
enum nume lista_de_variabile;
Datele de tip enumerat se consideră de tip int şi se pot utiliza în
program oriunde este legal să apară o dată de tip int.
Observaţii:
1o. Se pot utiliza, ca şi în cazul structurilor, construcţii de
forma:
enum nume {nume0, nume1,. . . , numen} lista_de_variabile;
sau
enum { nume0, nume1,. . . , numen} lista_de_variabile;
2o. De asemenea, se poate utiliza construcţia typedef pentru a atribui
un nume unui tip enumerat:
typedef enum nume {nume0, nume1,. . . , numen} NUME;
În continuare se pot declara date de tipul NUME, astfel:
NUME lista_de_variabile;
Exemple:
enum luna{ian=1,feb,mar,apr,mai,iun,iul,aug,sep,oct,nov,dec};
enum luna luna_calendaristica
Prin prima declaraţie se introduce tipul enumerat luna. Mulţimea de
valori asociate acestui tip este formată din numerele întregi 1,2, .
. . , 12. Se pot utiliza denumirile:
ian ia valoarea 1
feb ia valoarea 2
. . .
dec ia valoarea 12
A doua construcţie declară data luna_calendaristica de tipul luna. Ei
i se pot atribui valori prin expresii de atribuire de forma:
luna_calendaristica = mar sau
luna_calendaristica = mai + 4
typedef enum {luni, marti,miercuri,joi,vineri,sambata,duminica} ZI;
ZI z;
Variabila z este de tip ZI. Se poate utiliza în expresii de forma:
z=marti;
if(z<sambata) // trateaza ziua de lucru
else // trateaza zi de odihna
LECŢIA 9.
LISTE
9.1. DATE STRUCTURATE DEFINITE RECURSIV
Limbajul C permite definirea de tipuri structurate recursiv
(autoreferenţiate). Acest lucru se face cu ajutorul pointerilor, şi
anume un element al structurii poate să fie un pointer spre tipul de
dată introdus prin structura respectivă:
struct nume
{ declaratii
struct nume *p;
declaratii
};
Un tip definit ca mai sus se spune că este un tip autoreferit sau
recursiv. O dată structurată declarată printr-un astfel de tip se
spune că este autoreferită sau recursivă. Datele structurate
recursive au numeroase aplicaţii în prelucrarea listelor
înlănţuite şi arborescente.
Două tipuri structurate t1 şi t2 pot să conţină fiecare un
pointer spre celalalt. În acest caz se va proceda ca mai jos:
struct t1; // o declaratie inainte fara de care nu se poate
// declara tipul t2
struct t2
{ declaratii
struct t1 *pt1;
declaratii
};
struct t1
{ declaratii
struct t2 *pt2;
declaratii
};
9.2. LISTE ÎNLĂNŢUITE
Datele structurate se pot organiza în tablouri sau în structuri
recursive introducând în tipul structurat unul sau mai mulţi
pointeri spre tipul structurat respectiv. Astfel se stabileşte o
relaţie de ordine (uneori chiar mai multe) între elementele mulţimii
de date structurate; de asemenea, mulţimea rescpectivă se poate
organiza în mod dinamic, adăugând elemente noi sau suprimându-le pe
cele care nu mai sunt necesare.
Definiţie O mulţime dinamică de structuri recursive de acelaşi tip
şi care satisfac una sau mai multe relaţii de ordine introduse prin
pointeri se numeşte listă înlănţuită. Elementele listei se mai
numesc noduri.
Cele mai utilizate tipuri de listă sunt:
lista simplu înlănţuită;
lista circulară simplu înlănţuită;
lista dublu înlănţuită;
lista circulară dublu înlănţuită;.
Cele patru tipuri de liste sunt exemplificate grafic astfel:
capul
listă liniară simplu înlănţuită;
capul
listă liniară circulară simplu înlănţuită;
capul
listă liniară dublu înlănţuită;
capul
listă liniară circulară dublu înlănţuită;
9.3. LISTA LINIARĂ SIMPLU ÎNLĂNŢUITĂ
O listă simplu înlănţuită; este o listă înlănţuită; ale
cărei noduri satisfac o singură relaţie de ordine introdusă prin
pointeri.
Tipul unui nod dintr-o listă simplu înlănţuită; se poate declara
în două moduri:
struct tnod
{ declaratii
struct tnod *urmator;
declaratii
};
typedef struct tnod
{ declaratii
struct tnod *urmtor;
declaratii
} TNOD;
Observaţii
1o. Varianta b) este mai folosită.
2o. Pointerul următor introduce o relaţie de ordine între nodurile
de tip TNOD.
3o. Ultimul nod al listei va avea pointerul urmator = NULL.
4o. Pentru nodurile interioare ale listei pointerul urmator va avea
valori adrese; dacă urmator din nodul a pointează spre nodul b,
spunem că nodul b este succesorul lui a.
Operaţiile ce se pot efectua asupra unei liste simplu înlănţuită;
crearea listei;
accesul la un nod al listei;
inserarea unui nod înlănţuită;
ştergerea unui nod dintr-o listă;
ştergerea unei liste.
Memorarea listelor se poate face:
dinamic (în memoria internă);
static (în fişiere).
Pentru modul dinamic se va folosi funcţia malloc la crearea listei
cât şi la inserarea de noduri, iar la ştergerea de noduri funcţia
free.
9.4. CREAREA ŞI AFIŞAREA UNEI LISTE
Vom crea o listă ce conţine în noduri informaţii despre numere
întregi şi pătratele lor. Avem două funcţii: una de creare care
întoarce adresa capului listei şi o funcţie de afişare a
informaţiei din noduri. Vom comenta instrucţiunile importante din
program.
#include <stdio.h> #include <alloc.h> typedef struct nod // definirea
tipului NOD
{ int nr;
int patrat;
struct nod* leg;
} NOD;
NOD *creaza(void) // functia de creare, intoarce adresa capului
{ NOD *cap,*p,*pc;
int i,lung;
printf("\n\n\n\ creare lista simplu inlantuita\n\n");
lung = sizeof(NOD);
pc=(NOD *)malloc(lung); // pc este un pointer curent, in el se vor
pune adresel noi
cap=pc;
printf("dati informatia elementului : ");
scanf ("%d",&i);
while (i>0) // crearea listei se termina la numar negativ
{ p=pc; // pointer ce pastreaza adresa noua
p->nr=i; // incarcarea informatiei
p->patrat=i*i;
pc=(NOD *)malloc(lung); // se cere o noua adresa de memorie
p->leg=pc; // se leaga pointerul leg la noua adresa
printf("dati informatia elementului : ");
scanf ("%d",&i);
}
p->leg=NULL; // ultimul nod al listei are pointerul leg = NULL
free(pc); // eliberarea ultimei adrese care de fapt nu face parte
din lista
return cap; // returneaza adresa capului listei
}
void afisare(NOD *p) // functia de afisare a listei { while (p !=
NULL) // cat timp n-am ajuns la ultimul nod { printf ("\n numarul %d
si patratul sau %d", p->nr,p->patrat); p=p->leg; // trecerea la
urmatorul nod al listei
}
}
void main (void) // functia principala
{ NOD *capul;
clrscr();
capul=creaza(); // lista e cunoscuta prin adresa capului
afisare(capul);
getch();
}
LECŢIA 10.
PRELUCRAREA FIŞIERELOR
10.1. FIŞIERE
În general, prin fişier înţelegem o colecţie ordonată de
elemente numite înregistrări, care sunt păstrate pe diferite
suporturi de memorie externă. Suportul de memorie externă cel mai
folosit este suportul magnetic (de obicei discuri sub forma de flopy
şi hardiscuri sau bandă magnetică care e din ce în ce mai rar
folosită). Suportul magnetic este reutilizabil deoarece zona
utilizată pentru a păstra înregistrările unui fişier poate fi
ulterior reutilizată pentru a păstra înregistrările altui fişier.
Datele introduse de la un terminal se consideră că formează un
fişier de intrare. Înregistrarea în acest caz, de obicei, este
formată din datele tastate la terminal pe un rând deci caracterul de
rând nou (newline) este terminator de înregistrare. În mod analog,
datele care se afişează pe terminal formează un fişier de ieşire.
Înregistrarea poate fi formată din caracterele unui rând.
Un fişier are o înregistrare care marchează sfârşitul de fişier.
În cazul fişierelor de intrare de la tastatură sfârşitul de
fişier se generează prin:
CTRL/Z
El poate fi pus în evidenţă folosind constanta simbolică EOF
definită în fişierul stdio.h.
Prelucrarea fişierelor implică un număr de operaţii specifice
acestora. Două operaţii sunt absolut necesare la prelucrarea
oricărui fişier:
deschiderea fişierului;
închiderea fişierului.
Aceste operaţii de deschidere şi închidere a unui fişier se pot
realiza prin intermediul unor funcţii speciale din biblioteca standard
a limbajului C. Alte operaţii privind prelucrarea fişierelor sunt:
crearea unui fişier;
consultarea unui fişier;
actualizarea unui fişier;
adăugarea de înregistrări într-un fişier;
poziţionarea într-un fişier;
ştergerea unui fişier.
Prelucrarea fişierelor se poate face la două nivele. Primul nivel
face apel direct la sistemul de operare şi se numeşte nivelul
inferior de prelucrare al fişierelor. Cel de-al doilea nivel de
prelucrare se realizează prin utilizarea unor proceduri specializate
în prelucrarea fişierelor care, printre altele, pot rezerva şi
gestiona automat zone tampon necesare realizării operaţiilor de
intrare/ieşire, şi se numeşte nivelul superior de prelucrare al
fişierelor
10.2. NIVELUL INFERIOR DE PRELUCRARE AL FIŞIERELOR
La acest nivel de prelucrare se folosesc 5 funcţii:
open (creat)- pentru deschiderea fişierelor;
read - pentru citirea din fişier;
write - pentru citirea din fişier;
lseek - pentru poziţionarea în fişier;
close - pentru închiderea fişierului.
10.2.1. Deschiderea unui fişier
Orice fişier înainte de a fi prelucrat trebuie deschis. Această
operaţie se realizează prin intermediul funcţiei open al cărui
prototip este următorul:
int open (const char *cale, int acces);
unde:
cale este un pointer spre un şir de caractere care defineşte calea
spre fişierul care se deschide (în cea mai simplă formă este numele
fişierului dacă se află în directorul curent)
acces este o variabilă de tip întreg care poate lua una din valorile:
- O_RDONLY - fişierul se deschide numai în citire (consultare);
- O_WRONLY - fişierul se deschide numai în scriere (creare);
(sau O_CREAT)
- O_RDWR - fişierul se deschide în citire/scriere;
- O_APPEND - fişierul se deschide la sfârşit pentru adăugare;
- O_BINARY - fişierul se prelucrează binar;
- O_TEXT - fişierul este de tip text.
Unele valori din cele de mai sus se pot combina cu ajutorul
operatorului |. De exemplu O_RDWR | O_BINARY pentru deschiderea
fişierului în scriere/citire binară.
Observaţii:
1o. Funcţia open întoarce descriptorul de fişier care este o valoare
intreagă ce va identifica fişierul în toate celelate operaţii care
se vor realiza asupra lui. Dacă deschiderea unui fişier nu reuşeşte
(de obicei unul din parametrii este eronat) atunci funcţia open
returnează valoarea –1.
2o. Pentru a putea utiliza funcţia open trebuie incluse fişierele
header io.h şi fcntl.h.
3o. Pentru a crea un fişier nou se va folosi funcţia creat în locul
funcţiei open cu prototipul:
int creat (const char *cale, int mod);
unde:
- cale are aceeaşi semnificaţie ca şi la funcţia open;
- mod este un întreg care poate fi definit prin constantele simbolice
de mai jos:
S_IREAD - se poate citi fişierul;
S_IWRITE - se poate scrie în fişier;
S_IEXEC - se poate executa programul conţinut în fişier.
Utilizarea funcţiei presupune includerea fişierelor io.h şi stat.h
4o. Implicit fişierul se consideră că este de tip text.
Exemple:
char nume_fisier[ ]=”fis1.dat”;
int df;
df = open (nume_fisier, O_RDONLY);
Prin apelul de mai sus se deschide în citire fişierul fis1.dat din
directorul curent.
int df;
df = open (“c:\\borlandc\\help.txt”,O_APPEND);
Se deschide în adăugare fişierul help.txt din directorul borlandc de
pe discul C.
10.2.2. Citirea dintr-un fişier (consultare)
Funcţia folosită pentru operaţia de citire dintr-un fişier în
memorie se numeşte read şi are prototipul următor:
int read (int df, void *buf, unsigned lung);
unde:
- df este descriptorul de fişier a cărui valoare a fost definită la
deschidere;
buf este pointerul spre zona de memorie în care se recepţionează
înregistrarea care se citeşte;
lung este lungimea în octeţi a inregistrării citite.
Observaţii:
1o. La fiecare apel funcţia returnează înregistrarea curentă. La
primul apel se citeşte prima înregistrare din fişier, la al doilea
apel se citeşte a doua, etc. Ordinea înregistrărilor în fişier
este cea definită la crearea fişierului.
2o. La un apel al funcţiei read se citesc cel mult lung octeţi
înregistrarea având definită lungimea la scrierea în fişier.
Funcţia reîntoarce numărul de octeţi citiţi, 0(zero) la sfârşit
de fişier, sau –1 la eroare. De obicei se folosesc frecvent
înregistrări de 512 octeţi sau chiar mai mari.
3o. Funcţia read poate fi folosită pentru a citi de la intrarea
standard. În acest caz, descriptorul de fişier este 0 (stdin are 0,
stdout are 1, stderr are 2 stdprn are 3 stdaux are 4). Programatorul nu
trebuie să deschidă fişierele standard deoarece ele sunt deschise
automat la lansarea în execuţie a programului.
4o. Utilizarea funcţiei read, presupune includerea fişierului io.h.
10.2.3. Scrierea într-un fişier (creare, actualizare, adăugare)
Pentru a scrie într-un fişier se foloseşte funcţia write. Se
presupune că fişierul este deschis în prealabil prin funcţia creat
sau open. Ea este asemănătoare cu funcţia read, doar că realizează
transferul invers, adică din memorie în fişier şi are prototipul:
int write (int df, void *buf, unsigned lung);
Observaţii:
1o. Funcţia returnează numărul octeţilor scrişi în fişier.
Acesta este egal cu lung şi defineşte lungimea înregistrării scrise
în fişier. În cazul în care numărul returnat de funcţia write
diferă de parametrul lung scrierea a fost eronată şi se reîntoarce
valoarea –1.
2o. Utilizarea funcţiei write implică includerea fişierlui io.h.
10.2.4. Poziţionarea într-un fişier
Pentru a avea acces aleator la înregistrările unui fişier se
foloseşte o funcţie de poziţionare în fişier pe anumite
înregistrări dorite. Pe fişierele care au suporturi magnetice este
posibilă poziţionarea cu ajutorul funcţiei lseek care are prototipul
următor:
long lseek (int df, long deplasament, int origine)
unde:
df este descriptorul de fişier;
deplasament defineşte numărul de octeţi peste care se va deplasa
capul de
scriere/citire al discului;
origine are una din valorile:
0 deplasamentul se consideră de la începutul fişierului;
1 deplasamentul se consideră din poziţia curentă a capului de
scriere/citire;
2 deplasamentul se consideră de la sfârşitul fişierului.
Observaţii:
1o. Funcţia returnează poziţia capului de citire/scriere faţă de
începutul fişierului în număr de octeţi sau –1L la eroare.
2o. Funcţia nu realizează nici un transfer de informaţie ci doar
poziţionează capul de citire/scriere în fişier. Deci pentru
transfer e nevoie de funcţiile read sau write.
3o. Utilizarea funcţiei presupune includerea fişierului io.h.
4o. Apelul lseek (df, 0L, 0) permite o poziţionare la început de
fişier
iar apelul lseek (df, 0L, 2) permite o poziţionare la sfârşit de
fişier.
10.2.5. Închiderea unui fişier
La sfârşitul prelucrării unui fişier acesta trebuie închis. Acest
lucru se realizează automat dacă programul se termină prin apelul
funcţiei exit. Programatorul poate închide un fişier folosind
funcţia close. Se recomandă închiderea unui fişier de îndată ce
s-a terminat prelucrarea lui. Aceasta din cauză că numărul
fişierelor ce pot fi deschise simultan este limitat. Limita este
dependentă de sistemul de operare şi ea variază, de obicei, în
intervalul 15-25. De obicei numărul de buffere (zone tampon) asociate
fişierelor se precizează în fişierul autoexec.bat. Menţionăm că
fişierele standard din limbajul C nu se închid de programator.
Funcţia close are prototipul următor:
int close (int df);
unde
df este descriptorul fişierului care se închide.
Observaţii:
1o. La o închidere normală, funcţia returnează valoarea 0 şi –1
în caz de incident.
2o. Utilizarea funcţiei close implică includerea fişierului io.h.
Exemple
Vom deschide fişierul “fis1.dat” în creare şi vom scrie în el
două înregistrări.
#include<io.h>
#include<fcntl.h>
void main (void)
{ int df,i;
df = open("fis1.dat", O_CREAT); // se deschide fisierul fis1.dat in
creare
if (df != -1) // se testeaza daca fiserul s-a deschis corect
{ write (df,"cioban vasyle\n", 14); // se scriu doua inregistrari
write (df,"cioban andrei\n", 14);
} else printf (“nu s-a deschis fisierul);
close (df); // se inchide fisierul
}
Se afişează conţinutul fişierului “fis1.dat”
#include<io.h>
#include<fcntl.h>
#include <stdio.h>
#include <conio.h>
void main (void)
{int df;
char s[14];
df = open("fis1.dat", O_RDONLY); // se deschide fisierul în citire
if (df != -1) // se testeaza daca deschiderea e corecta
{ read (df, s, 14);
printf ("%.14s",s);
read (df, s, 14);
printf ("%.14s",s);
} else printf (“nu s-a deschis fisierul);
close (df);
getch(); // se asteapta un caracter de la tastatura
}
programul următor copiază intrarea standard la ieşierea standard
folosind o zonă tampon de 80 de caractere:
#define LZT 80
#include <io.h>
void main (void) // copiaza intrarea standard la iesirea standard
{ char zona_tampon[LZT];
int i;
while ((i = read (0, zona_tampon, LZT)) > 0) write (1, zt, i);
}
10.3. NIVELUL SUPERIOR DE PRELUCRARE A FIŞIERELOR
După cum am amintit, la acest nivel fişierele se prelucrează cu
ajutorul unor proceduri specializate.
Deschiderea unui fişier
Funcţia fopen se utilizează pentru deschiderea unui fişier. Ea
returnează un pointer spre tipul FILE (tipul fişier), tip definit în
fişierul stdio.h. Tipul FILE este un tip structurat şi el depinde de
sistemul de operare. În caz de eroare, funcţia fopen returnează
pointerul NULL. Prototipul funcţiei fopen este următorul:
FILE *fopen (const char *cale, const char *mod);
unde:
cale are aceeaşi semnificaţie ca şi în cazul funcţiilor open şi
creat.
mod este un pointer spre un şir de caractere care defineşte modul de
prelucrare al fişierului după deschidere. Acest şir de caractere se
defineşte în felul următor:
- “r” - deschidere în citire (read);
- “w” - deschidere în scriere (write);
- “a” - deschidere pentru adăugare;
- “r+” - deschidere pentru modificare (citire sau scriere);
- “rb” - citire binară;
- “wb” - scriere binară;
- “r+b” - citire/scriere binară.
Observaţii:
1o. Dacă se deschide un fişier inexistent cu modul “w” sau
“a”, atunci el este deschis în creare.
2o. Dacă se deschide un fişier existent cu modul “w”, atunci
conţinutul vechi al fişierului se pierde şi se va crea unul nou cu
acelaşi nume.
3o. Menţionăm că, stdin, stdout, stderr, stdaux şi stdprn sunt
pointeri spre tipul FILE şi permit ca funcţiile de nivel superior de
prelucrare a fişierelor să poată trata intrarea standard şi
ieşirile standard pe terminal şi imprimantă la fel ca şi fişierele
pe celelalte suporturi. Singura deosebire constă în aceea că aceste
fişiere nu se deschid şi nici nu se închid de către programator.
Ele sunt deschise automat la lansarea în execuţie a programului şi
se închid la apelul funcţiei exit.
4o. Apelul funcţiei se realizează prin construcţia:
FILE *pf;
pf = fopen (“FIS1.DAT”,”w”);
10.3.2. Prelucrarea pe caractere a unui fişier
Fişierele pot fi scrise şi citite caracter cu caracter, folosind
două funcţii simple:
putc pentru scriere;
getc pentru citire.
Funcţia putc are prototipul:
int putc (int c, FILE *pf);
unde:
c este codul ASCII al caracterului care se scrie în fişier;
pf este pointerul spre tipul FILE a cărui valoare a fost returnată de
funcţia fopen la deschiderea fişierului în care se scrie; pf poate
fi şi stdout, sdterr, stdaux, stdprn.
Funcţia putc returnează valoarea lui c respectiv –1 în caz de
eroare.
Funcţia getc are prototipul:
int getc (FILE *pf);
unde:
pf este pointerul spre tipul FILE a cărui valoare a fost returnată de
funcţia fopen la deschiderea fişierului; în particular pf poate fi
stdin.
Funcţia getc returnează codul ASCII al caracterului citit sau EOF la
sfârşit de fişier sau eroare.
Închiderea unui fişier
Închiderea unui fişier se realizează cu ajutorul funcţiei fclose
care are prototipul:
int fclose (FILE *pf);
unde:
pf este pointerul spre tipul FILE a cărui valoare a fost definită la
deschiderea fişierului prin intermediul funcţiei fopen.
Funcţia fclose returnează:
0 la închiderea normală a fişierului;
1 în caz de eroare.
Exemple:
Programul următor copiază intrarea standard la ieşirea standard
stdout, folosind funcţiile getc şi putc.
#include <stdio.h>
void main (void)
{ int c;
while (( c = getc (stdin)) != EOF) putc (c, stdout);
}
Programul următor copiază intrarea standard la imprimantă.
#include <stdio.h>
void main (void)
{ int c;
while (( c = getc (stdin)) != EOF) putc (c, stdprn);
}
Programul următor scrie la ieşirea stdout caracterele unui fişier a
cărui cale este argumentul din linia de comandă. Dacă nu există un
argument în linia de comandă, atunci se citeşte de la intrarea
standard.
#include <stdio.h>
void main (int argc, char *argv[ ] )
{ FILE *pf;
int c;
if (argc = = 1)
pf = stdin; // nu exista argument in linia de comanda
else // se deschide fisierul a carui cale se afla in argv[1]
if (( pf = fopen (*++argv,”r”)) = = NULL)
{ printf (“nu se poate deschide fisierul %s\n”,*argv);
exit (1);
}
while (( c = getc (pf)) != EOF) putchar(c);
exit (0);
}
Operaţiile de intrare-ieşire cu format
Biblioteca standard a limbajului C conţine funcţii care permit
realizarea operaţiilor de intrare/ieşire cu format. Astfel se pot
utiliza funcţiile fscanf şi fprintf, prima pentru citire cu format
dintr-un fişier, iar a doua pentru scriere cu format într-un fişier.
Funcţia fscanf este asemănătoare cu funcţia scanf. Ea are un
parametru în plus faţă de scanf. Acest parametru este un pointer
spre tipul FILE şi el defineşte fişierul din care se face citirea.
Acest pointer este primul parametru al funcţiei fscanf. Funcţia poate
fi apelată printr-o expresie de atribuire de forma:
nr = fscanf (pf, control, lista_de_parametrii );
unde :
pf este un pointer spre tipul FILE şi valoarea lui a fost definită
prin apelul funcţiei fopen; defineşte fişierul din care se face
citirea;
ceilalţi parametri sunt identici cu cei utilizaţi la apelul funcţiei
scanf.
Funcţia fscanf, ca şi funcţia scanf, returnează numărul
câmpurilor citite din fişier. La întâlnirea sfârşitului de
fişier se returnează valoarea EOF definită în fişierul stdio.h.
Pentru pf = stdin, funcţia fscanf este identică cu scanf.
Funcţia fprintf , ca şi funcţia printf, returnează numărul
caracterelor scrise în fişier sau –1 în caz de eroare. Pentru pf =
stdout, funcţia fprintf este identică cu printf. De asemenea, se pot
utiliza pointerii standard obisnuiţi: stderr, stdaux, stdprn.
Exemplu:
Vom scrie un program cu ajutorul căruia se va crea un fişier cu
numele "FIS.DAT" şi care conţine înregistrări cu numele, prenumele
şi adresa unor persoane.
#include <stdio.h>
void main(void)
{
int n=0, i=1; // n este numarul de înregistrari ce se va scrie in
fisier
char nume[25], prenume[30], adresa[50];
FILE *pf;
printf("\n Dati numarul de inregistrari n= ");
scanf("%d",&n);
pf=fopen("FIS.DAT","w");
if (pf = = NULL)
{ printf ("Eroare la deschidere");
return;
}
do
{
printf("\n Nume : ");
scanf("%s",nume);
printf("\n Prenume : ");
scanf("%s",prenume);
printf("\n Adresa : ");
scanf("%s",adresa);
fprintf(pf,"%s %s %s", nume, prenume, adresa);
i++;
} while (i<=n);
fclose(pf);
}
10.3. 5. Intrări-ieşiri de şiruri de caractere
Biblioteca standard a limbajului C conţine funcţiile fgets şi fputs
care permit citirea respectiv scrierea într-un fişier ale cărui
înregistrări sunt şiruri de caractere.
Funcţia fgets are prototipul:
char *fgets (char *s, int n, FILE *pf);
unde:
s este pointerul spre zona în care se face citirea caracterelor;
n-1 este numărul maxim de caractere care se citesc;
pf este pointerul spre tipul FILE care defineşte fişierul din care se
face citirea.
De obicei s este numele unui tablou de tip char de dimensiune cel
puţin n. Şirul se termină cu ‘\0’ (caracterul NUL). La
întâlnirea caracterului ‘\n’, citirea se opreşte. În acest caz,
în zona receptoare se transferă caracterul ‘\n’ şi apoi
caracterul NUL (‘\0’).
În mod normal, funcţia returnează valoarea pointerului s. La
întâlnirea sfârşitului de fişier se returnează valoarea NULL.
Funcţia fputs scrie într-un fişier un şir de caractere care se
termină prin ‘\0’. Ea are prototipul:
int fputs (const char *s, FILE *pf);
unde:
s este pointerul spre zona care conţine şirul de caractere care se
scrie;
pf este pointerul spre zona care conţine şirul de caractere care se
scrie.
Funcţia fputs returnează codul ASCII al ultimului caracter scris sau
–1 în caz de eroare.
Aceste funcţii sunt realizate folosind funcţia getc pentru fgets şi
putc pentru fputs.
Pentru a citi de la intrarea standard stdin, se poate folosi funcţia
gets, care nu mai are parametrii pf şi n. Parametrul pf este implicit
stdin. Funcţia gets citeşte caracterele de la intrarea standard
până la întâlnirea caracterului ‘\n’ care nu mai este păstrat
în zona spre care pointează s. Şirul de caractere citit se termină
şi în acest caz prin ‘\0’.
În mod analog, pentru a scrie la ieşirea standard stdout se poate
folosi funcţia puts, care nu mai are parametrul pf, acesta fiind
implicit stdout. În rest, funcţia puts este la fel ca şi funcţia
fputs.
10.3.6. Poziţionarea într-un fişier
Cu ajutorul funcţiei fseek se poate deplasa capul de citire/scriere
al discului în vederea prelucrării înregistrărilor fişierului
într-o ordine oarecare, diferită de cea secvenţială (acces
aleator). Această funcţie este asemănătoare cu funcţia lseek. Ea
are prototipul următor:
int fseek (FILE *pf, long deplasament, int origine);
unde:
pf este pointerul spre tipul FILE care defineşte fişierul în care se
face poziţionarea capului de citire/scriere;
deplasament şi origine au aceeaşi semnificaţie ca şi în cazul
funcţiei lseek.
Funcţia fseek returnează valoarea zero la poziţionare corectă şi o
valoare diferită de zero în caz de eroare.
Funcţia ftell indică poziţia capului de citire în fişier. Ea are
prototipul:
long ftell (FILE *pf);
unde:
pf este pointerul spre tipul FILE care defineşte fişierul în cauză.
Funcţia returnează o valoare de tip long care defineşte poziţia
curentă a capului de citire/scriere, şi anume reprezintă
deplasamentul în octeţi a poziţiei capului faţă de începutul
fişierului.
10.3.7. Prelucrarea fişierelor binare
Fişierele organizate ca date binare (octeţii nu sunt consideraţi ca
fiind coduri de caractere) pot fi prelucrate la acest nivel folosind
funcţiile fread şi fwrite. În acest caz se consideră că
înregistrarea este o colecţie de date structurate numite articole. La
o citire, se transferă într-o zonă specială, numită zonă tampon,
un număr de articole care se presupune că au o lungime fixă. În mod
analog, la scriere se transferă din zona tampon un număr de articole
de lungime fixă. Cele două funcţii au prototipurile de mai jos:
unsigned fread (void *ptr, unsigned dim, unsigned nrart, FILE *pf);
unde:
- ptr este pointerul spre zona tampon ce conţine articolele citite
(înregistrarea citită);
- dim este un întreg ce reprezintă lungimea unui articol;
- nrart este un întreg ce reprezintă numărul articolelor care se
transferă;
- pf este un pointer spre tipul FILE care defineşte fişierul din care
se face citirea.
unsigned fwrite (const void *ptr, unsigned dim, unsigned nrart, FILE
*pf);
Parametrii funcţiei fwrite au aceeaşi semnificaţie ca şi parametrii
funcţiei fread.
Ambele funcţii returnează numărul articolelor transferate sau –1
în caz de eroare.
Exemplu:
Programul următor citeşte de la intrarea standard stdin datele ale
căror formate sunt definite mai jos şi le scrie în fişierul
miscari.dat din directorul curent.
tip denumire um cod pret cantitate
1 TELVIZOR buc 0001 3000000 200
#include <stdio.h> #define MAX 60
typedef struct { char tip[2];
char den[MAX];
int val;
char unit[3];
long cod;
float pret;
float cant;
} ARTICOL;
// 6 articole in zona tampon
union {
ARTICOL a[6];
char zt[6*sizeof(ARTICOL)];
}buf;
int cit (ARTICOL *str) // citeste datele de la intrarea standard si
le scrie in
{ // structura spre care pointeaza str
int nr,c;
float x,y;
while ((nr = scanf("%1s %60s %3d %2s %ld", str -> tip, str ->den,
&str ->val, str -> unit,&str -> cod)) != 5|| scanf("%f %f", &x,&y) !=
2)
{
if(nr = = EOF) return (EOF);
printf ("rind eronat ; se reia\n");
// avans pina la newline
while ((c = getchar ()) != '\n' && c != EOF);
if (c == EOF) return (EOF);
} // sfarsit while
str -> pret = x;
str -> cant = y;
return (nr);
} // sfarsit cit
void main (void) // creaza fisierul miscari.dat cu datele citite de la
intrarea standard
{
FILE *pf;
ARTICOL a;
int i,n;
if((pf = fopen("miscari.dat", "wb")) == NULL)
{
printf("nu se poate deschide in creare fisierul miscari.dat\n");
exit(1);
}
for ( ; ; )
{ // se umple zona tampon a fisierului
for (i = 0 ;i<6 ; i++)
{
if((n=cit(&a)) == EOF) break;
buf.a[i]=a;
}
// se scrie zona tampon daca nu este vida
if(i !=0 )
if(i!=fwrite(buf.zt, sizeof(ARTICOL), i, pf))
{ printf("eroare la scrierea in fisier\n");
exit(1);
}
if(n = = EOF) break;
}
fclose(pf);
}
10.4. ŞTERGEREA UNUI FIŞIER
Un fişier poate fi şters apelând funcţia unlink. Aceasta are
prototipul:
int unlink (const char *cale);
unde:
- cale este un pointer spre un şir de caractere identic cu cel
utilizat la crearea fişierului în funcţia creat sau fopen.
Funcţia unlink returnează valoarea zero la o ştergere reuşită,
respectiv -1 în caz de eroare.
LECŢIA 11.
FUNCŢII STANDARD
Vom descrie câteva clase de funcţii, numite clase de funcţii
standard aflate în bibliotecile mediului BORLAND C. Funcţiile şi
macrourile utilizate frecvent în majoritatea aplicaţiilor se pot
grupa în următoarele clase:
funcţii de prelucrare a fişierelor;
funcţii de alocare dinamică a memoriei;
macrouri de clasificare;
macrouri de transformare a simbolurilor;
funcţii care realizează conversii;
funcţii de prelucrare a şirurilor de caractere;
funcţii de calcul;
funcţii pentru controlul proceselor;
funcţii de gestiune a datei şi a orei;
funcţii de gestiune a ecranului.
Funcţiile de la punctele a) şi b) au fost descrise în lecţii
anterioare.
11.1. MACROURI DE CLASIFICARE
În această clasă distingem un număr de macrouri simple care au o
utilizare largă în prelucrarea simbolurilor. Definiţiile acestor
macrouri se află în fişierul ctype.h
Unul dintre macrouri este denumit isascii şi are prototipul:
int isascii (int c);
Macroul returnează o valoare diferită de zero dacă valoarea lui c
aparţine intervalului de numere întregi [0,127] şi zero în caz
contrar. Acest macrou permite să se testeze dacă valoarea
parametrului său reprezintă un cod ASCII sau nu.
Celelalte macrouri au prototipul următor:
int nume (int c);
unde nume este unul din următoarele:
isalpha dacă c este codul unei litere;
isalnum dacă c este codul unei litere sau cifre;
isdigit dacă c este codul unei cifre;
isgraph dacă c este codul unui caracter imprimabil inclusiv spaţiul;
islower dacă c este codul unei litere mici;
isprint dacă c este codul unui caracter imprimabil inclusiv spaţiu;
isspace dacă c reprezintă spaţiu, tabulator, retur de car, rând
nou,
tabulator vertical, salt la început de pagină de imprimantă.
isupper dacă c este codul unei litere mari;
isxdigit dacă c este codul unei cifre hexazecimale.
Exemplu:
Programul următor citeşte un fişier şi îl rescrie schimbând
literele mari cu litere mici. Căile spre cele două fişiere (sursă
şi destinaţie) sunt argumente în linia de comandă:
argv[1] este un pointer spre fişierul sursă;
argv[2] este un pointer spre fişierul destinaţie.
#include <stdio.h>
#include <ctype.h>
void main ( int argc, char *argv[ ] )
{ FILE *pf1, *pf2;
int c;
if (argc != 3)
{ printf (“linia de comanda eronata\n”);
exit(1);
}
if (( pf1 = fopen (argv[1],”r”) ) = = NULL
{ printf (“nu se poate deschide fisierul”%s\n”, argv[1]);
exit(1);
}
if (( pf2 = fopen (argv[2],”w”) ) = = NULL
{ printf (“nu se poate deschide fisierul”%s\n”, argv[2]);
exit(1);
}
while (( c = getc (pf1)) != EOF)
if (isascii(c) && isupper (c))
putc (c - ‘A’ + ‘a’, pf2); // c este codul unei litere mari
else
putc(c, pf2); // c este codul unei litere mici
fclose (pf1);
fclose (pf2);
// afisarea fisierului destinatie
if (( pf2 = fopen (argv[2], “r”) = = NULL)
{ printf (“nu se poate deschide in citire:%s\n”, argv[2]);
exit(1);
}
while ((c = getc(pf2)) != EOF) putchar (c);
fclose (pf2);
}
11.2. MACROURI DE TRANSFORMARE A SIMBOLURILOR
În această clasă distingem macrouri definite tot în fişierul
ctype.h. Prototipurile acestor macrouri sunt:
int toascii (int c); returnează ultimii 7 biţi ai lui c (care
reprezintă un
cod ASCII);
int tolower (int c); transformă pe c din literă mare în literă
mică;
int toupper (int c); transformă pe c din literă mică în literă
mare.
11.3. CONVERSII
O dată are un format extern şi un format intern. Prin conversie
înţelegem o transformare a unei date dintr-un format al ei în
celălalt. Conversiile se pot face sub controlul unui format sau fără
format. Dintre funcţiile care realizează conversii sub controlul
formatelor amintim:
printf;
fprintf;
scanf;
fscanf;
Aceste funcţii au fost descrise în lecţiile anterioare. Vom da în
continuare câteva funcţii care realizează conversii fără format
şi care sunt utilizate mai frecvent. Aceste funcţii au prototipurile
în fişierul stdlib.h.
Funcţia atoi are prototipul:
int atoi (const char *ptr);
unde:
ptr este un pointer spre o zonă de tip caracter ce conţine cifre
zecimale care
sunt, eventual, precedate de semnul minus;
Efectul:
şirul de cifre spre care pointează ptr este convertit din întreg
zecimal în întreg binar de tip int.
Observaţie:
1o. Funcţia returnează rezultatul acestei conversii.
Funcţia atol are prototipul:
long atol (const char *ptr);
unde:
ptr este un pointer spre o zonă de tip caracter ce conţine cifre
zecimale care
sunt, eventual, precedate de semnul minus;
Efectul:
şirul de cifre spre care pointează ptr este convertit din întreg
zecimal în întreg binar de tip long.
Observaţie:
1o. Funcţia returnează rezultatul acestei conversii.
Funcţia atof are prototipul:
double atof (const char *ptr);
unde:
ptr este un pointer spre o zonă de tip caracter ce conţine cifre
zecimale care
sunt, eventual, precedate de semnul minus (poate conţine marca
zecimală);
Efectul:
şirul de cifre spre care pointează ptr este convertit în virgulă
flotantă dublă precizie.
Observaţie:
1o. Funcţia returnează rezultatul acestei conversii.
Funcţia itoa are prototipul:
char *itoa (int val, char *sir, int baza)
Efectul:
valoarea parametrului val se converteşte din întreg binar de tip int
în baza de numeraţie definită de parametrul baza şi se păstrează
în zona spre care pointează sir.
Observaţie:
1o. Funcţia returnează pointerul sir.
Funcţia ltoa are prototipul:
char *ltoa (long val, char *sir, int baza)
Efectul:
valoarea parametrului val se converteşte din întreg binar de tip long
în baza de numeraţie definită de parametrul baza şi se păstrează
în zona spre care pointează sir.
Observaţie:
1o. Funcţia returnează pointerul sir.
11.4. FUNCŢII DE PRELUCRARE A ŞIRURILOR DE CARACTERE
Funcţiile din această clasă implică includerea fişierului
string.h. Indicăm mai jos funcţiile din această clasă, utilizate
mai frecvent. O parte din aceste funcţii au mai fost utilizate în
diferite exemple din lecţiile anterioare.
Funcţii de copiere:
char *strcpy (char *dest, const char *sursa);
char *strncpy (char *dest, const char *sursa, unsigned n);
prima funcţie copiază şirul de caractere spre care pointează sursa
în zona spre care pointează dest;
a doua funcţie realizează acelaşi lucru, dar copiază cel mult
primii n octeţi din sursă;
ambele funcţii returnează valoarea pointerului dest.
Funcţii de concatenare:
int strcmp (const char *dest, const char *sursa);
char *strncat (const char *dest, const char *sursa, unsigned n);
prima funcţie copiază şirul spre care pointează sursa la
sfârşitul şirului din zona spre care pointează dest;
a doua funcţie realizează acelaşi lucru, dar se copiază cel mult
primii n octeţi din zona spre care pointează sursa;
ambele funcţii returnează valoarea pointerului dest.
Funcţii de comparare:
int strcmp (const char *sir1, const char *sir2);
int stricmp (const char *sir1, const char *sir2);
int strncmp (const char *sir1, const char *sir2, unsigned n);
int strnicmp (const char *sir1, const char *sir2, unsigned n);
aceste funcţii compară şirurile de caractere din zonele spre care
pointează pointerii sir1 şi sir2;
ele returnează o valoare:
negativă, dacă şirul spre care pointează sir1 este mai mic decât
cel spre care pointează sir2;
zero, dacă cele două şiruri sunt egale;
pozitivă, dacă şirul spre care pointează sir1, este mai mare decât
cel spre care pointează sir2;
prezenţa literei i (ignore) în numele funcţiei înseamnă că nu se
face distincţie între literele mari şi mici;
prezenţa literei n în numele funcţiei înseamnă că se realizează
comparaţia pe cel mult n octeţi.
Observaţie:
1o. Fie şirurile s1 şi s2 de lungime l1 şi l2. Atunci cele două
şiruri sunt egale dacă:
l1=l2 ( au aceeaşi lungime);
s1[k] = s2 [k] pentru k=0,1,...,l1
2o. Şirul s1 este mai mic decât şirul s2 dacă există un j, j 0
şi j min (l1, l2), astfel încât:
s1[j] < s2[j];
s1[k] = s2[k], pentru k=0,1, . . . , j-1.
3o. Şirul s1 este mai mare decât şirul s2 dacă există un j, j
0 şi j (l1, l2), astfel incât:
s1[j] > s2[j];
s1[k] = s2[k], pentru k=0,1, . . . , j-1.
Funcţia lungime:
unsigned strlen (const char *sir);
returnează lungimea şirului de caractere spre care pointează sir;
caracterul NUL care termină şirul nu este numărat.
11.5. FUNCŢII DE CALCUL
Majoritatea funcţiilor matematice au prototipurile în fişierul
math.h. Multe dintre acestea se utilizează la calculul valorilor
funcţiilor elementare şi au prototipul:
double nume (double x);
unde nume este unul din următoarele:
acos -arccos;
asin -arcsin;
atan -arctg;
cos -cos;
sin -sin;
exp -ex;
log -ln;
log10 -lg;
sqrt -rădăcina pătrată;
ceil -returnează cel mai mare întreg mai mare sau egal cu x (partea
întreagă);
floor -returnează cel mai mare întreg mai mic sau egal cu x;
fabs -valoarea absolută;
sinh -sinus hiperbolic;
cosh -cosinus hiperbolic;
tanh -tangentă hiperbolică;
Alte funcţii:
double atan2 (double y, double x); - returnează arctg(y/x);
double pow (double x, double y); - returnează xy;
double cabs (struct complex z); - returnează modulul numărului
complex;
double poly (double x, int n, double c[ ] ) - returnează valoarea
polinomului
de grad n în punctul x, coeficienţii
sunt c[0], . . . c[n].
Funcţiile care urmează au prototipul în fişierele stdlib.h şi
math.h:
int abs (int n); - returnează valoarea absolută din întregul n;
long labs (long n); - returnează valoarea absolută din întregul
long n;
11.6. FUNCŢII PENTRU CONTROLUL PROCESELOR
Aceste funcţii au prototipurile în fişierul stdlib.h şi în
process.h şi realizează controale asupra programelor:
void abort (void); - termină un program în caz de eroare;
void exit (int stare) - termină un program şi returnează o stare;
stare este
0 pentru terminare normală şi diferită de zero
pentru o terminare anormală; videază buferele
fisierelor, închide toate fişierele;
int system (const char *comanda) - execută o comandă DOS definită
prin sirul
de caractere spre care pointează comanda;
returnează 0 la succes si –1 la eroare.
Aceste funcţii au prototipurile în stdlib.h şi în process.h.
11.7. FUNCŢII PENTRU GESTIUNEA DATEI ŞI OREI
Dăm mai jos prototipurile a patru funcţii pentru citirea/setarea
datei şi orei. Ele implică includerea fişierului dos.h.
void getdate(struct date *d); - încarcă structura de tip date spre
care pointează d
cu datele corespunzătoare furnizate de sistemul de
operare;
void gettime(struct time *t); - încarcă structura de tip time spre
care pointează t
cu datele corespunzătoare furnizate de sistemul de
operare;
void setdate (struct date *t); - setează data curentă în
conformitate cu datele de
tip date;
void settime (struct time *t); - setează ora curentă în conformitate
cu datele de tip
time spre care pointează t.
Tipurile date şi time sunt definite în fişierul dos.h. astfel:
struct date {
int da_year;
int da_day;
int da_mon;
};
struct time {
unsigned char ti_min;
unsigned char ti_hour;
unsigned char ti_hund;
unsigned char ti_sec;
};
Exemplu:
void main (void) // afiseaza data si ora
{ struct date d;
struct time t;
getdate (&d);
gettime (&t);
printf (“\n\t%02d/%02d/%04d”,d.da_day, d.da_mon, d.da_year);
printf (“\tora %02d:%02:%02\n”, t.ti_hour, t.ti_min, t.ti_sec);
}
11.8. ALTE FUNCŢII DIVERSE DE UZ GENERAL
void clrscr (void); - şterge fereastra activă sau tot ecranul;
prototipul în conio.h
void delay(unsigned i); - suspendă execuţia programului pentru o
perioadă de i
milisecunde;
void sleep(unsigned i); - suspendă execuţia programului pentru o
perioadă de i
secunde;
void nosound (void); - opreşte difuzorul calculatorului;
void sound(unsigned h); - porneşte difuzorul calculatorului cu un ton
egal cu h Hz.
LECŢIA 12.
GESTIUNEA ECRANULUI ÎN MOD TEXT
Biblioteca standard a limbajelor C şi C++ conţine funcţii pentru
gestiunea ecranului. Acesta poate fi gestionat în 2 moduri:
modul text şi
modul grafic.
Modul text presupune că ecranul este format dintr-un număr de linii
şi coloane. De obicei există două variante:
25 de linii x 80 de coloane = 2000 de carctere sau
25 de linii x 40 de coloane = 1000 de caractere.
Poziţia pe ecran a unui caracter se defineşte printr-un sistem de
coordonate întregi (x,y)
unde:
x - reprezintă numărul coloanei în care este situat caracterul;
y - reprezintă numărul liniei în care este situat caracterul.
Colţul din stânga sus are coordonatele (1,1) iar colţul din dreapta
jos (80,25) sau (40,25).
În mod implicit funcţiile de gestiune a ecranului în mod text au
acces la tot ecranul. Accesul poate fi limitat la o parte din ecran
utilizând aşa numitele ferestre. Fereastra este un dreptunghi care
este o parte a ecranului şi care poate fi gestionată independent de
restul ecranului.
Un caracter de pe ecran, pe lângă coordonate, mai are şi
următoarele atribute:
culoarea caracterului afişat;
culoarea fondului;
clipirea caracterului.
Aceste atribute sunt dependente de adaptorul grafic utilizat. Cele mai
utilizate adaptoare sunt:
placa MDA, care este un adaptor monocrom;
placa HERCULES, care este un adaptor color;
placa CGA, care este un adaptor color;
placa EGA, care este un adaptor color;
placa VGA, care este un adaptor color de mare performanţă.
Pentru adaptoarele de mai sus se pot utiliza 8 culori de fond şi 16
culori pentru afişarea caracterelor.
Atributul unui caracter se defineşte cu ajutorul formulei:
atribut = 16 * culoare_fond + culoare_caracter + clipire (*)
unde:
culoare_fond (background) = cifră între 0 şi 7; (8 culori)
culoare_caracter (foreground) = întreg între 0 şi 15; (16 culori)
clipire = 128 (clipire) sau 0 (fără clipire)
Tabel cu numele culorilor:
Culoare Constantă simbolică Valoare
negru BLACK 0
albastru BLUE 1
verde GREEN 2
turcoaz CYAN 3
roşu RED 4
purpuriu MAGENTA 5
maro BROWN 6
gri deschis LIGHTGRAY 7
gri închis DARKGRAY 8
albastru deschis LIGHTBLUE 9
verde deschis LIGHTGREEN 10
turcoaz deschis LIGHTCYAN 11
roşu dechis LIGHTRED 12
purpuriu magenta LIGHTMAGENTA 13
galben YELLOW 14
alb WHITE 15
clipire BLINK 128
12.1. SETAREA ECRANULUI ÎN MOD TEXT
Setarea (punerea) se realizează cu ajutorul funcţiei textmode care
are prototipul:
void textmode (int modtext);
unde
modtext poate fi exprimat numeric sau simbolic în felul următor:
Modul text activat Constantă simbolică Valoare
alb/negru 40 coloane BW40 0
color 40 coloane C40 1
alb/negru 80 coloane BW80 2
color 80 coloane C80 3
monocrom MONO 7
color 43 linii pentru EGA
şi 50 linii pentru VGA C4350 64
modul precedent LASTMODE -1
Modul MONO se poate seta pe un adaptor monocolor. Celelalte moduri se
pot seta pe adaptoare color.
12.2. DEFINIREA UNEI FERESTRE
Dacă dorim să partajăm ecranul în zone care să poată fi
gestionate independent trebuie să definim ferestre.
O fereastră este o zonă dreptunghilară de pe ecran care se poate
defini cu funcţia window al cărei prototip este:
void window (int stanga, int sus, int dreapta, int jos);
unde:
-(stanga, sus) = coordonatele colţului din stânga al ferestrei;
-(dreapta, jos) = coordonatele colţului din dreapta jos.
La un moment dat o singură fereastră este activă şi anume aceea
definită de ultimul apel al funcţiei window. Funcţiile de gestionare
a ecranului în mod text acţionează întotdeauna asupra ferestrei
active. După setarea modului text cu ajutorul funcţiei textmode este
activ tot ecranul.
Menţionăm că funcţia window nu are nici un efect dacă parametrii
de la apel sunt eronaţi.
12.3. ŞTERGEREA UNEI FERESTRE
Fereastra activă se poate şterge cu ajutorul funcţiei clrscr cu
prototipul:
void clrscr(void);
fereastra activă (sau tot ecranul) devine activă; fondul are culoarea
definită prin culoarea de fond curentă;
clrscr poziţionează cursorul în poziţia (1,1) a fiecărei ferestre.
12.4. GESTIUNEA CURSORULUI
Programatorul poate plasa cursorul pe un caracter al ferestrei
folosind funcţia gotoxy al cărei prototip este:
void gotoxy (int coloana, int linie);
unde:
(coloana, linie) sunt coordonate relative la fereastra activă, dacă
sunt înafara ferestrei active apelul este ignorat;
Poziţia cursorului se poate determina cu ajutorul funcţiilor wherex
şi wherey care au prototipurile:
int wherex (void); -returnează numărul coloanei în care se află
cursorul
int wherey (void); -returnează numărul liniei în care se află
cursorul.
Există cazuri când se doreşte ascunderea cursorului. Acest lucru se
poate realiza printr-o secvenţă specială în care se utilizează
funcţia geninterrupt.
void ascundcursor (void) // face invizibil cursorul
{ _AH = 1;
_CH = 0x20;
geninterrupt (0x10);
}
Cursorul poate fi reafişat apelând funcţia de mai jos:
void afiscursor (void) // face vizibil cursorul
{ _AH = 1;
_CH = 6;
_CL = 7;
geinterrupt (0x10);
}
AH, CH, CL reprezintă unii din regiştrii microprocesorului.
12.5. DETERMINAREA PARAMETRILOR ECRANULUI
Utilizatorul are posibilitatea să obţină parametrii curenţi ai
ecranului prin apelarea funcţiei gettextinfo al cărui prototip este:
void gettextinfo (struct text_info *p);
unde structura text_info este definită în fişierul conio.h astfel:
struct text_info
{ unsigned char winleft; // amplasarea colturilor ferestrei
unsigned char wintop;
unsigned char winright;
unsigned char winbottom;
unsigned char attribute; // culoarea fondului, a caracterelor si
unsigned char normattr; // clipirea
unsigned char currmode;
unsigned char screenheight; // dimensiunea ecranului
unsigned char screenwidth;
unsigned char curx; // pozitia cursorului
unsigned char cury;
};
12.6. MODURILE VIDEO ALB/NEGRU
Există două moduri (intens şi normal) care se pot activa cu
funcţiile highvideo şi lowvideo ale căror prototipuri sunt:
void highvideo (void);
void lowvideo (void);
Intensitatea iniţială este de obicei cea normală. Se poate reveni
la intensitatea normală dacă se apelează funcţia normvideo cu
acelaşi prototip ca al celorlalte două funcţii.
Exemplu:
Vom scrie un program care afişează parametrii ecranului:
#include <stdio.h>
#include <conio.h>
void main (void)
{ struct text_info par_ecran;
gettextinfo (&par_ecran) ;
printf (“\nstanga: %u sus: %u dreapta: %u jos: %u\n”,
par_ecran.winleft,
par_ecran.wintop,
par_ecran.winright,
par_ecran.winbottom);
printf (“atribut: %d mod curent: %d\n”,
par_ecran.normattr, par_ecran.currmode);
printf (“inaltimea ecranului: %d latimea ecrnului: %d\n”,
par_ecran.screenheight, par_ecran.screenwidth);
printf (“coloana cursorului: %d linia cursorului: %d\n”,
par_ecran.curx, par_ecran.cury);
}
12.7. SETAREA CULORILOR
Culoarea fondului se setează cu ajutorul funcţiei textbackground cu
prototipul următor:
void textbackground (int culoare);
unde:
culoare este un întreg cuprins între 0 şi 7 cu semnificaţia din
tabelul anterior al culorilor.
Culoarea caracterelor se setează cu ajutorul funcţiei textcolor cu
prototipul următor:
void textattr (int culoare);
unde:
culoare este un întreg între 0 şi 15.
Se pot seta ambele culori, precum şi clipirea caracterului folosind
funcţia textattr de prototip:
void textattr (int atribut);
unde:
atribut e definit cu ajutorul relaţiei (*) de la începutul lecţiei.
12.8. GESTIUNEA TEXTELOR
Pentru afişarea caracterelor colorate în conformitate cu atributele
definite prin relaţia:
atribut = 16 * culoare_fond + culoare_caracter + clipire
se pot folosi funcţiile:
putch - pentru afişarea unui caracter;
cputs - pentru afişarea color a unui şir de caractere (acelaşi
prototip ca puts);
cprintf - pentru afişarea color sub controlul formatelor.
Alte prototipuri de funcţii:
void insline (void); - inserează o linie cu spaţii în fereastră,
liniile de sub
poziţia cursorului se deplasează în jos cu o poziţie;
void clreol (void) - şterge sfârşitul liniei începând cu poziţia
cursorului;
void delline (void) - şterge toată linia pe care este poziţionat
cursorul;
int movetext ( int stanga, int sus, int dreapta, int jos,
int stanga_dest, int dreapta_dest );
- copiază un text dintr-o poziţie în alta;
returnează: 1 dacă textul s-a copiat cu succes şi o în caz de
eroare.
Textele dintr-o zonă dreptunghiulară a ecranului pot fi salvate sau
citite dintr-o zonă de memorie cu ajutorul funcţiilor puttext şi
gettext şi au prototipurile:
int gettext (int stanga, int sus, int dreapta, int jos, void
*destinatie);
unde
primii patru parametrii definesc fereastra unde se află textul de
salvat;
destinatie este pointerul spre zona de memorie în care se salvează
textul.
şi
int puttext (int stanga, int sus, int dreapta, int jos, void *sursa);
unde
primii patru parametrii definesc fereastra unde se va scrie pe ecran
textul preluat din memorie;
sursa este pointerul spre zona de memorie din care se transferă
textul.
Ele returnează:
1 la copiere cu succes;
0 la eroare.
Observaţie:
1o. Fiecare caracter de pe ecran se păstrează pe doi octeţi:
pe un octet codul caracterului;
pe octetul următor atributul caracterului.
Exemple:
Programul următor setează o fereastră şi modurile video alb/negru.
#include <conio.h>
void main (void)
{ textmode (BW80);
window (10,4,60,4);
clrscr ();
lowvideo ();
cputs(”lowvideo”);
normvideo ();
cputs (“normvideo”);
textmode (LASTMODE);
cprintf (“\n\r Acionai o tast pentru a continua:”);
getch ();
}
Programul următor afişează toate combinaţiile de culori posibile
pentru fond şi caractere (adaptor EGA/VGA).
#include <conio.h>
#include <stdio.h>
void main (void)
{ static char *tculoare [ ] = {“ 0 BLACK negru”,
“ 1 BLUE albastru”,
“ 2 GREEN verde”,
“ 3 CYAN turcoaz”,
“ 4 RED rosu”,
“ 5 MAGENTA purpuriu”,
“ 6 BROWN maro”,
“ 7 LIGHTGRAY gri deschis”,
“ 8 DARKGRAY gri inchis”,
“ 9 LIGHTBLUE albastru deschis”,
“10 LIGHTGREEN verde deschis”,
“11 LIGHTCYAN turcoaz deschis”,
“12 LIGHTRED rosu dechis”,
“13 LIGHTMAGENTA purpuriu magenta”,
“14 YELLOW galben “,
“15 WHITE alb”};
int i,j,k;
struct text_info atribut;
gettextinfo (&atribut);
for (i = 0; i < 8; i++ ) // i alege culoarea fondului
{ window (3,2,60,20);
k=2;
textbackground (i);
clrscr();
for (j=0; j <10; j++, k++) // j alege culoarea caracterului
{ textcolor (j);
gotoxy (2,k);
if (i = = j) continue;
cputs (tculoare[j]);
}
gotoxy (1,18);
printf (“actionati o tasta pentru contiuare\n”);
getch();
}
window (atribut.winleft, atribut.wintop, atribut.winright,
atribut.winbottom);
textattr (atribut. attribute);
clrscr();
}
LECŢIA 13.
GESTIUNEA ECRANULUI ÎN MOD GRAFIC
Modul grafic presupune că ecranul este format din “puncte
luminoase” (pixeli). Numărul acestora depinde de adaptorul grafic
şi se numeşte rezoluţie. O rezoluţie este cu atât mai bună cu
cât este mai mare. Adaptorul CGA are o rezoluţie de 200 rânduri x
640 de coloane iar EGA de 350 de rânduri x 640 de coloane. Adaptorul
VGA şi SVGA oferă rezoluţii în funcţie de mărimea ecranului.
Astfel adaptorul VGA şi SVGA oferă rezoluţii de până la 768 de
rânduri x 1024 de coloane pentru ecranul de 14” (inches).
Pentru gestiunea ecranului în mod grafic se pot utiliza peste 60 de
funcţii standard aflate în biblioteca sistemului. Aceste funcţii au
prototiopurile în fişierul graphics.h. Totodată ele folosesc
pointeri de tip far (de 32 de biţi).
13.1. SETAREA MODULUI GRAFIC
Modul grafic se setează cu ajutorul funcţiei initgraph. Această
funcţie poate fi folosită singură sau împreună cu o altă funcţie
numită detectgraph care determină parametrii adaptorului grafic.
Prototipul ei este:
void far detectgraph (int far *graphdriver, int far *graphmode);
unde:
în zona spre care pointează graphdriver se păstrează una din
valorile:
valoare simbol
CGA
MCGA
EGA
EGA64
EGAMONO
IBM8514
HERCMONO
ATT400
VGA
PC3270
în zona spre care pointează graphmode se memorează una din valorile:
pentru CGA
valoare simbol
CGAC0
CGAC1
CGAC2
CGAC3
corespunzând toate pentru o rezoluţie de 320*200 de pixeli şi permit
4 culori
CGAHI
are o rezoluţie de 640*200 puncte şi lucrează numai alb/negru.
pentru EGA
valoare simbol
EGALO are 640*200 i 16 culori
EGAHI are 640*350
pentru VGA
valoare simbol
0 VGALO are 640*200
1 VGAMED are 640*350
2 VGAHI are 640*480
Valorile spre care pointeză graphdriver definesc nişte funcţii
standard corespunzătoare adaptorului grafic. Aceste funcţii se numesc
drivere. Ele se află în subdirectorul BGI.
Funcţia detectgraph detectează adaptorul grafic prezent la calculator
şi păstrează valoarea corespunzătoare acestuia în zona spre care
pointează graphdriver. Modul grafic se defineşte în aşa fel încât
el să fie cel mai performant pentru adaptorul grafic curent. Cele mai
utilizate adaptoare sunt de tip EGA (calculatoare mai vechi) şi VGA
şi SVGA (calculatoarele mai noi).
Apelul funcţiei detectgraph trebuie să fie urmat de apelul funcţiei
initgraph. Aceasta setează modul grafic în conformitate cu parametrii
stabiliţi de apelul prealabil al funcţiei detectgraph.
Funcţia initgraph are prototipul:
void far initgraph (int far * graphdriver, int far *graphmode, char far
*cale);
unde:
graphdriver şi graphmode sunt pointeri cu aceeaşi semnificaţie ca
în cazul funcţiei detectgraph;
cale este un pointer spre şirul de caractere care definesc calea
subdirectorului BGI (Borland Graphic Interface), de exemplu:
“C:\\BOLANDC\\BGI”
Exemplu:
Setarea în mod implicit a modului grafic:
int driver, mod_grafic;
. . .
detectgraph (&driver, &mod_grafic);
initgraph(&driver, &mod_grafic, “C:\\BORLANDC\\BGI”);
După apelul funcţiei initgraph se pot utiliza celelalte funcţii
standard de gestiune grafică a ecranului.
Din modul grafic se poate ieşi apelând funcţia closegraph care are
prototipul:
void closegraph (void);
Funcţia initgraph mai poate fi apelată şi folosind constanta
simbolică DETECT astfel:
int driver, mod_grafic;
. . .
driver = DETECT;
initgraph (&driver, &mod_grafic, “C:\\BORLANDC\\BGI”);
Constanta simbolică DETECT este definită în fişierul graphics.h,
alături de celelalte constante simbolice care definesc driverul.
Aceasta are valoarea zero.
Prin apelul de mai sus, funcţia initgraph apelează funcţia
detectgraph pentru a defini parametrii impliciţi ai adaptorului
grafic.
Utilizatorul poate defini el însuşi parametri pentru iniţializarea
modului grafic.
13.2. GESTIUNEA CULORILOR
Adaptoarele grafice sunt prevăzute cu o zonă de memorie în care se
păstrează date specifice gestiunii ecranului. Această zonă de
memorie poartă denumirea de memorie video.
În mod grafic, ecranul se consideră format, după cum am precizat,
din puncte luminoase numite pixeli. Poziţia pe ecran a unui pixel se
defineşte printr-un sistem de coordonate (x,y) cu x coloana şi y
linia în care este afişat pixelul.
În cazul adaptoarelor color, unui pixel îi corespunde o culoare.
Culoarea pixelilor se păstrează pe biţi în memoria video. Memoria
video necesară pentru a păstra starea ecranului setat în mod grafic,
se numeşte pagină video. Adaptoarele pot conţine mai multe pagini
video. Gestiunea culorilor este dependentă de tipul de adaptor grafic
existent la microprocesor.
Numărul maxim de culori pentru adaptoarele EGA este de 64 (numerotate
de la 0 la 63). Cele 64 de culori nu pot fi afişate simultan pe ecran.
În cazul adaptorului EGA se pot afişa simultan 16 culori. Mulţimea
culorilor care pot fi afişate simultan pe ecran se numeşte paletă.
Culorile din componenţa unei palete pot fi modificate de utilizator
prin intermediul funcţiilor standard. La iniţializarea modului grafic
se setează o paletă implicită.
Pentru adaptorul EGA există un tablou de 64 de culori (cu coduri
ntre 0 şi 63) din care se selectează cele 16 culori pentru
paletă. Există şi constante simbolice foarte sugestive cu numele
culorilor în engleză. Funcţiile de gestiune a culorilor pot avea ca
parametri nu numai codurille culorilor ci şi aceste constante
simbolice.
Culoarea fondului este întotdeauna cea corespunzătoare indicelui 0
din paletă. Culoarea pentru desenare este cea corespunzătoare
indicelui 15.
Culoarea de fond poate fi modificată cu ajutorul funcţiei setbkcolor
care are prototipul:
void far setbkcolor (int culoare);
unde:
culoare = index în tabloul care defineşte paleta.
Exemplu: setbkcolor (BLUE); setează culoarea de fond pe albastru.
Pentru a cunoaşte culoarea de fond curentă se poate apela funcţia
getbkcolor de prototip:
int far getbkcolor (void);
Ea returnează indexul în tabloul care definete paleta pentru
culoarea de fond.
Culoarea pentru desenare poate fi modificată folosind funcţia
setcolor de prototip:
void far setcolor(int culoare);
unde:
culoare = index în tabloul care defineşte paleta.
Exemplu: setcolor (YELLOW); setează culoarea pentru desenare în
galben.
Culoarea pentru desenare se poate determina apelând funcţia getcolor
de prototip:
int far getcolor (void);
Ea returnează indexul în tabloul care defineşte paleta relativ la
culoarea pentru desenare.
Paleta curentă poate fi modificată cu funcţiile setpalette şi
setallpalette. Prima funcţie se foloseşte pentru a modifica o culoare
din paleta curentă şi are prototipul:
void far setpalette (int index, int cod);
unde:
index este un întreg din {0,. . . , 15} şi reprezintă indexul în
tabloul care defineşte paleta pentru culoarea care se modică;
cod este un întreg din intervalul {0, 1,. . . , 63} şi reprezintă
codul culorii care o înlocuieşte în paletă pe cea veche.
Exemplu: setpalette (DARKGRAY, 45); modifică culoarea
corespunzătoare
indicelui DARKGRAY (adică 8) prin
culoarea de cod 45.
Funcţia setallpalette permite modificarea simultană a mai multor
culori din compunerea paletei şi are prototipul:
void far setallpalette (struct palettetype far *paleta);
unde:
palettetype este un tip definit în fişierul graphics.h astfel
struct palettetype
{ unsigned char size;
unsigned char colors [MAXCOLOR+1];
};
cu - size dimensiunea paletei
colors tablou ale cărui elemente au ca valori codurile componente ale
paletei care se defineşte.
Modificarea paletei curente cu ajutorul funcţiilor setpalette şi
setallpalette conduce la schimbarea corespunzătoare a culorilor
afişate pe ecran în momentul apelului funcţiilor respective.
Pentru a determina codurile culorilor componente ale paletei curente
se va folosi funcţia getpalette de prototip:
void far getpalette (struct palettetype far *paleta);
Paleta implicită poate fi determinată folosind funcţia
getdefaultpalette de prototip:
struct palettetype *far getdefaultpalette(void);
Numărul culorilor dintr-o paletă poate fi obţinut apelând funcţia
getmaxcolor de prototip:
int far getmaxcolor (void);
Funcţia returnează numărul maxim de culori diminuat cu 1. Deci în
cazul adaptorului EGA funcţia returnează valoarea 15.
O altă funcţie care determină dimensiunea paletei este funcţia
getpalettesize cu prototipul:
int far getpalettesize (void);
Funcţia returnează numărul culorilor componente ale paletei. În
cazul adaptorului EGA funcţia returnează valoarea 16.
Exemplu:
Programul următor afişează codurile culorilor pentru paleta
implicită:
#include <graphics.h>
#include <stdlib.h>
#include <stdio.h>
#include <conio.h>
void main (void)
{ int gd = DETECT, gm, i;
struct palettetype far *pal = (void *) 0;
initgraph (&gd, &gm, “C:\\BORLANDC\\BGI”);
pal = getdefaultpalette ();
for (i=0; i<16; i++)
{ printf (“colors[%d]=%d\n”, i, pal -> colors[i]);
getch ();
}
closegraph();
}
13.3. STAREA ECRANULUI
În mod grafic ecranul se compune din n*m pixeli. Pixelul din
stânga-sus are coordonatele (0,0), iar pixelul din dreapta-jos are
coordonatele (n-1,m-1).
În BGI există mai multe funcţii care permit utilizatorului să
obţină informaţii despre:
coordonata maximă pe orizontală;
coordonata maximă pe verticală;
poziţia curentă (pixelul curent);
etc.
int far getmaxx (void); returnează abscisa maximă;
int far getmaxy (void); returnează ordonata maximă;
int far getx (void); returnează poziţia pe orizontală (abscisa)
pixelului curent;
int far gety (void); returnează poziţia pe verticală (ordonata)
pixelului curent.
Exemplu:
#include <graphics.h>
#include <stdlib.h>
#include <stdio.h>
#include <conio.h>
void main (void)
{ int gd = DETECT, gm, cul_fond, cul_desen, curent_x,curent_y, maxx,
maxy;
initgraph (&gd, &gm, “C:\\BORLANDC\\BGI”);
cul_fond = getbkcolor();
cul_desen = getcolor();
maxx = getmaxx();
maxy = getmaxy();
curent_x = getx();
curent_y = gety();
closegraph();
printf (“culoarea fondului = %d\n”, cul_fond);
printf (“culoare desenare = %d\n”, cul_desen);
printf (“abscisa maxima = %d\n”, maxx);
printf (“ordonata maxima = %d\n”, maxy);
printf (“abscisa curenta = %d\n”, curent_x);
printf (“ordonata curenta = %d\n”, curent_y);
printf (“acionai o tasta pentru terminare”);
getch();
}
LECŢIA 14.
PROBLEME DIVERSE
De multe ori suntem puşi în faţa unor probleme pe care le
înţelegem uşor dar nu ştim cum să le rezolvăm cât mai simplu şi
elegant. Vă propunem câteva metode care bine însuşite pot duce,
uneori, la o rapidă rezolvare a problemelor dificile. Evident că, nu
toate problemele pot fi încadrate în aceste tipare propuse dar
fiecare programator poate să-şi formeze un astfel de "portofoliu" de
metode cu care să poate aborda orice problemă. Metodele prezentate
în continuare pot constitui un început.
14.1. GENERAREA COMBINĂRILOR
Fie o mulţime oarecare de n elemente care poate fi pusă într-o
corespondenţă biunivocă cu mulţimea A={1,...,n}. Ne propunem să
determinăm toate m-combinările (mn) ale mulţimii A (submulţimi
de m elemente ale mulţimii A). Vom rezolva problema, nerecursiv,
pornind de la m-combinarea V=(1,2,...,m) determinând succesiv toate
m-combinările ordonate lexicografic crescător.
Fie V=(v1,v2,...,vm) o m-combinare oarecare, atunci m-combinarea
următoare în ordine lexicografică crescătoare se obţine astfel:
se determină cel mai mare indice i satisfăcând relaţiile:
vi<n-m+i, vi+1=n-m+i+1,..., vm-1=n-1, vm=n. (1)
se trece la vectorul următor:
(v1,...,vi-1,vi+1,vi+2,...,vi+n-i+1);
dacă nu există un astfel de indice i (care să satisfacă relaţiile
(1)) înseamnă că vectorul V conţine ultima m-combinare şi anume:
(n-m+1,n-m+2, ...,n).
Dăm în continuare o funcţie C care generează o m-combinare cu n
elemente având ca parametru cod o variabilă booleană care pentru
valoarea 0 generează prima m-combinare iar pentru valoarea 1
generează următoarea m-combinare. Funcţia comb reîntoarce valoarea
1 dacă s-a generat o m-combinare oarecare şi valoarea 0 dacă s-a
generat ultima m-combinare (în acest caz cod are valoarea 0). Se va
folosi un vector global v în care se generează m-combinările.
#define dim 50
#include <stdio.h>
int v[dim+1], n, m;
// functia generatoare a m-combinarilor
int comb(cod)
int cod;
{
int i,j;
// generarea primei m-combinari 1,...,m
if (cod == 0)
{
for (i=1; i<=m; v[i]=i++);
return (1);
}
i=m+1;
// cautarea indicelui i pentru satisfacerea relaiilor (1)
do { i }
while (v[i] >= n m+i);
if (i)
{
v[i]=v[i]+1;
for (j=i+1; j<=m; v[j]=v[j 1]+1,j++);
return (1);
}
else return (0);
}
void main(void)
{
int kod,i;
printf("\ndati n: ");
scanf ("%d",&n);
printf("\ndati m: ");
scanf ("%d",&m);
comb(0);
kod=1;
while (kod)
{ printf("\n");
for (i=1;i<=m;printf ("%3d",v[i++]));
kod = comb(kod);
}
getche();
}
14.2. METODA GREEDY
Se aplică problemelor în care se dă o mulţime A conţinând n date
(de orice natură şi structură) de intrare cerându-se să se
determine o submulţime B (BA) care să îndeplinească anumite
condiţii pentru a fi acceptată. Cum, în general, există mai multe
astfel de submulţimi (numite soluţii posibile) se mai dă şi un
criteriu conform căruia dintre aceste submulţimi să alegem una
singură (numită soluţie optimă) ca rezultat final. Foarte multe
probleme de căutare se înscriu în acest tip.
Menţionăm că, în general, soluţiile posibile au următoarele
proprietăţi:
- se presupune că este soluţie posibilă;
- dacă B este soluţie posibilăţi CB atunci şi C este soluţie
posibilă;
Vom da în continuare o variantă a tehnicii greedy (denumire care în
traducere înseamnnă lăcomie, înghiţire) în care se porneşte de
la mulţimea vidă. Apoi se alege pe rând, într-un anumit fel, un
element din A neales încă. Dacă adăugarea lui la soluţia parţial
anterior construită conduce la o soluţie posibilă, atunci se
adaugă, altfel se alege un nou element. Tot acest procedeu se repetă
pentru toate elementele din A. Dăm în continuare în pseudocod o
procedură:
procedure GREEDY (n,A,B)
B:=;
for i=1,n do
call ALEGE (A,i,x);
call POSIBIL (B,x,cod);
if cod=1 then
call ADAUG (B,x);
endif;
endfor;
return;
end procedure
Despre procedurile apelate din GREEDY precizăm următoarele:
procedura ALEGE furnizează în x un element din A aj{ai,...,an} şi
interschimbă ai cu aj; dacă la fiecare pas se cercetează ai atunci
procedura se simplifică;
procedura POSIBIL verifică dacă elementul x poate fi adăugat sau nu
mulţimii parţiale construită până la pasul curent furnizând o
valoare booleană cod cu semnificaţia:
cod = 1, dacă B U {x}, este soluţie posibilă
cod = 0, altfel
procedura ADAUG înlocuieşte pe B cu B{x}.
Obs. Procedurile de mai sus nu sunt necesare întotdeauna, acest fapt
depinzând de complexitatea problemei. Oricum trebuie reţinut cadrul
de rezolvare al acestui tip de problemă.
Problemă rezolvată
Se dă o mulţime de valori reale X={x1, . . .,xn}. Se cere
submulţimea YX astfel ca y /yY, să fie maximă.
Evident că problema este foarte simplă (în Y trebuie introduse
elementele strict pozitive din X; evităm să mai construim procedurile
ALEGE, POSIBIL, ADAUG) şi vom da rezolvarea ei în pseudocod:
procedure SUMA (n,X,Y,k)
integer n,X(n),Y(n),k
k:=0;
for i=1,n do
if x(i) > 0 then k:=k+1;
y(k):=x(i)
endif;
endfor;
return;
end procedure
Probleme propuse
14.2.1.
Se dau n şiruri S1,S2,...,Sn ordonate crescător, de lungimi L1,L2,
...,Ln. Să se obţină un şir S de lungime L1+L2+...+Ln cu toate
elementele celor n şiruri ordonate crescător (problemă de
interclasare).
Indicaţie: Vom interclasa succesiv câte două şiruri în final
obţinând şirul ordonat crescător. Complexitatea interclasării a
două şiruri A şi B de lungimi a şi b depinde direct proporţional
de a+b (pentru că se fac a+b deplasări de elemente). Se pune problema
găsirii ordinii optime în care trebuie efectuate aceste interclasări
astfel ca numărul total de deplasări să fie minim.
Vom da un exemplu pentru a lămuri mai bine lucrurile:
fie 3 şiruri de lungimi (90,40,10). Interclasăm şirul S1 cu S2 apoi
şirul rezultat cu S3; putem nota acest lucru formal prin (S1+S2)+S3.
Se obţin (90+40) + (130+10)=270 deplasări. Dacă vom interclasa
şirurile în ordinea (S2+S3)+S1 vom obţine (40+10)+ (50+90)=190 de
deplasări. De aici concluzia că întotdeauna vom interclasa şirurile
de lungimi minime din şirurile rămase.
14.2.2.
Găsiţi tripletele de numere pitagorice din Nn x Nn x Nn (prin Nn
notat mulţimea {0,1,2,...,n}). Încercaţi optimizarea timpului de
execuţie a programului.
14.2.3.
Fie mulţimea m-combinărilor luate din n elemente şi fie k<m un
număr natural. Să se dea un algoritm şi apoi să se scrie un program
C astfel încât să se determine o submulţime de m-combinări cu
proprietatea că oricare două m-combinări au cel mult k elemente
comune.
14.2.4.
Să se determine mulţimile interior stabile (MIS) ale unui graf
oarecare dat prin matricea sa de adiacenţă.
14.3. METODA BACKTRACKING (CĂUTARE CU REVENIRE)
Această metodă se aplică problemelor ce pot fi reprezentate sub
forma unui arbore finit iar căutarea soluţiei presupune parcurgerea
arborelui în adâncime (DF=Depth First).
Problemele de acest tip au în general soluţia de forma x=(x1, . . .
,xn) S1x . . . xSn, fiecare Sk fiind o mulţime finită. Mai facem
câteva precizări preliminare:
a) pentru fiecare problemă sunt date anumite relaţii între
componentele x1, . . . ,xn ale lui
x numite condiţii interne;
b) produsul cartezian S=S1x...xSn se mai numeşte spaţiul soluţiilor
posibile, iar soluţiile
posibile care satisfac condiţiile interne se numesc soluţii
rezultat;
c) în general se cer două lucruri în astfel de probleme:
- determinarea tuturor soluţiilor rezultat;
- determinarea doar a acelor soluţii care optimizează o funcţie
obiectiv dată.
Cum rezolvăm astfel de probleme? Există două modalităţi de
rezolvare:
1) tehnica directă (numită şi tehnica forţei brute) prin care se
generează toate elementele spaţiului de soluţii posibile şi apoi se
verifică fiecare dacă este sau nu o soluţie rezultat; această
tehnică necesită un timp prohibitiv (dacă fiecare Si are doar 2
componente complexitatea este O(2n); totodată ar fi necesară
imbricarea a n cicluri cu n aprioric necunoscut).
2) backtracking care evită generarea tuturor soluţiilor posibile.
Să dăm în continuare câteva repere ale rezolvării:
soluţia este construită progresiv, componentă cu componentă;
lui xk i se atribuie o valoare (evident că numai din Sk) dacă şi
numai dacă x1, . . . ,xk-1 au deja valori;
se verifică condiţiile de continuare (strâns legate de condiţiile
interne) dacă are sens trecerea la xk+1;
dacă nu are sens (adică condiţiile de continuare nu sunt
îndeplinite atunci facem o nouă alegere pentru xk sau dacă am
epuizat valorile din Sk atunci k:=k-1 i se face o nouă alegere
pentru xk-1; ş.a.m.d.
2. dacă are sens atunci k:=k+1 şi se testează dacă k>n:
21) dacă inegalitatea este adevărată atunci se afişează soluţia
astfel determinată
şi k:=k-1 continuând procesul de la punctul 1;
22) dacă inegalitatea este falsă se continuă procesul de la
punctul 1.
Procedura rezolvării unor astfel de probleme este:
procedure BT(n,x)
integer n;
array x(n);
k:=1;
while k>0 do
cod:=0;
while ([mai exist o valoare α din Sk netestat] and cod=0)
x(k):=α;
if k(x(1),...,x(k)) then cod:=1;
endif;
endwhile;
if cod=0 then
k:=k-1
else
if k=n then write (x(1),...,x(n))
else k:=k+1
endif
endif;
endwhile;
return;
end procedure
Vom face câteva precizări:
1o. Predicatul k(x1, . . . , xk) reprezintă condiţiile de
continuare pentru x1, . . . , xk;
2o. Cod este o valoare ce indică îndeplinirea/neîndeplinirea
condiţiilor de continuare;
3o. Dacă predicatul k(x1, . . . , xk) este adevărat k
{1,...,n} atunci se vor afla toţi vectorii din S;
4o. Backtracking poate fi uşor reprezentat pe un arbore construit
astfel:
- nivelul 1 conţine rădăcina;
- din orice nod de pe nivelul k pleacă sk muchii spre nivelul k+1,
etichetate cu cele
sk elemente ale lui Sk;
- nivelul n+1 va conţine s1*s2*. . .* sn noduri frunză;
- pentru fiecare nod de pe nivelul n+1 etichetele muchiilor conţinute
în drumul ce
leagă rădăcina de acest nod reprezintă o soluţie posibilă;
Dacă mulţimile Sk reprezintă progresii aritmetice atunci algoritmul
general se modifică astfel:
procedure BTA(n,a,b,h)
integer n;
array primul(n),ultimul(n),ratia(n),x(n);
k:=1;
x(1):=primul(1)-ratia(1);
while k>0 do
cod:=0;
while ( x(k)+ratia(k) ultimul(k) and cod=0 )
x(k):=x(k)+ratia(k);
if k(x(1),...,x(k)) then cod:=1 endif;
endwhile;
if cod=0 then
k:=k-1
else
if k=n then write (x(1),...,x(n))
else k:=k+1
x(k):=a(k)-h(k)
endif
endif;
endwhile;
return;
end procedure
unde:
- primul(n) reprezintă vectorul primilor termeni ai progresiilor
aritmetice;
- ultimul(n) reprezintă vectorul ultimilor termeni ai progresiilor
aritmetice;
- ratia(n) reprezintă vectorul raţiilor progresiilor aritmetice;
De reţinut cele două avantaje ale acestui procedeu:
evitarea imbricării unui număr oarecare de cicluri aprioric variabil
(în algoritmul propus se imbrică doar două cicluri pretestate
while);
evitarea construirii spaţiului tuturor soluţiilor posibile S1xS2x . .
. xSn.
Problemă rezolvată
În câte moduri se pot aranja 8 dame pe tabla de şah astfel încât
să nu se "bată" reciproc. Să se folosească al doilea algoritm
dintre cei menţionaţi anterior.
Prima variantă
Acest program respectă algoritmul anterior cu unele mici modificări.
Facem precizarea că vectorul x conţine în componenta x[i] numărul
coloanei de pe tabla de şah pe care se va afla dama de pe linia i.
Tipărirea va reprezenta o permutare (din cele 8! soluţii posibile).
Se vor afla 92 de soluţii. Lăsăm pe seama cititorului să deducă
analogiile şi diferenţele între algoritm şi program.
#include <stdio.h>
#include <math.h>
void main (void)
{ int x[9],cod,k,i,nr;
k=1; x[1]=0;nr=0;
while (k>0)
{ cod=0;
while (((x[k]+1)<=8)&&(cod= =0))
{ x[k]++;
if ((k= =1) && (x[k]= =1)) cod=1;
else { i=1; cod=1;
while ((cod==1)&&(i<k))
{ if ((x[i]==x[k])||(abs(x[i] x[k])==k i)) cod=0;
i++;
}
}
}
if (cod= =0) k ;
else {if (k= =8)
{ printf("\n%3d. ",++nr);
for (i=1;i<9;printf("%d ",x[i++]));
}}
else x[++k]=0;
}
}
}
A doua variantă:
#include <stdio.h>
#include <math.h>
#define n 100
int x[100],cod,k,nc,nsol;
int Verifica(void)
{ int i,cod1; // cod1=1 conditiile de continuare
cod1=1; // sunt verificate
if (k>1) // cod1=0 in caz contrar;
for(i=1; i<= (k 1); i++)
{ if (x[k]= =x[i]) cod1=0;
if (abs(x[k] x[i]) = = k i) cod1=0; // (*)
}
return cod1;
}
void ScrieSolutie(void)
{ int i;
printf("\n%3d. ",++nsol);
for (i=1; i<=nc; printf("%3d",x[i++]));
}
void CitesteDate(void)
{ int i;
nsol=0;
clrscr();
nc=n+1;
while ((nc>n) || (nc<0))
{ printf ("Dati n = ");
scanf ("%d",&nc);
};
}
void main (void)
{ CitesteDate();
k=1; x[1]=0;
while (k>0)
{ cod=0;
while (((x[k]+1) <= nc) && (cod= =0))
{x[k]++;
cod=Verifica();
}
if (cod= =0) k ;
else {if (k==nc) ScrieSolutie();
else x[++k]=0;
}
}
}
A doua variantă este modulară, mai uşor de înţeles şi
generalizează problema damelor până la tabla de şah de 100x100.
Lăsăm pe seama cititorului modificarea funcţiei ScrieSolutie pentru
a afla în mod grafic tabla de şah.
Dacă în funcţia Verifica se şterge instrucţiunea notat cu (*)
atunci se obţin toate permutările de n obiecte.
Probleme propuse
14.3.1
Să se rezolve problema turelor de şah după al doilea algoritm. În
câte moduri se pot aranja n turnuri pe tabla de şah astfel încât
să nu se "bată" reciproc.
14.3.2.
Să se afişeze poziţiile succesive ale unui cal pe tabla de şah,
pornind dintr-o poziţie dată, astfel încât să fie atinse toate
căsuţele tablei de şah.
14.3.3.
Având un fişier cu o mulţime de cuvinte din limba română de
aceeaşi lungime k să se scrie un program C care afişează toate
careurile rebusiste fără puncte negre. ( Problema e fascinantă
implicând şi cunoştinţe gramaticale dar şi cunoscând faptul că
nu s-au construit careuri de 10x10 fără puncte negre manual şi nici
cu ajutorul calculatorului; se poate încerca apoi şi cu k:=11,12, . .
.).
14.3.4.
Un intreprinzător dispune de un capital C şi are n variante de
investiţii. Pentru fiecare investiţie i cunoaşte fondurile de
investiţie fi precum şi beneficiile bi. Se cere un program care să
deducă toate variantele posibile de investiţii al
intreprinzătorului. Se mai dau şi condiţiile Cci i {1, . .
. ,n}.
14.3.5.
Având un graf neorientat caracterizat prin matricea costurilor să se
determine prin bactraking circuitul de cost minim pornind dintr-un
vârf dat.
14.3.6.
Având un graf neorientat caracterizat prin matricea de incidenţă a
vârfurilor să se determine prin bactraking mulţimile interior
stabile maximale.
14.3.7.
Să se determine toate cuvintele binare de lungime 10 care conţin
exact 5 cifre de 1.
14.3.8.
Să se determine toate cuvintele binare de lungime 10 care conţin cel
mult 5 cifre de 1.
14.3.9.
Să se determine toate cuvintele din {a,b,c}* (mulţimea tuturor
cuvintelor peste alfabetul Σ se notează cu Σ* ) de lungime 10 care
conţin exact 2 simboluri 'a'; 3 simboluri 'b' şi 5 simboluri 'c'.
14.3.10.
Să se determine toate cuvintele din {a,b,c}* de lungime n care conţin
exact na simboluri 'a'; nb simboluri 'b' şi nc simboluri 'c' (cu
condiţia n=na+nb+nc).
14.3.11.
Să se determine toate tripletele (x1,x2,x3) de numere astfel ca:
x1+x2+x3suma
x1*x2*x3produs
cu valorile suma şi produs date iar x1S1, x2S2 şi x3S3 ; S1,
S2 şi S3 fiind trei progresii aritmetice date deasemenea.
14.3.12.
Să se determine toate variantele de pronosport cu 13 rezultate din
{1,x,2} care conţin exact n1 simboluri '1'; nx simboluri 'x' şi n2
simboluri '2' (cu condiţia n1+nx+n2=13).
14.3.13.
Să se determine toate variantele de pronosport cu 13 rezultate din
{1,x,2} care conţin cel mult n1 simboluri '1'; cel mult nx simboluri
'x' şi simboluri '2' în rest (cu condiţia n1+nx13).
14.4. METODA DIVIDE ET IMPERA (DIVIDE ŞI STĂPÂNEŞTE)
Această modalitate de elaborare a programelor constă în
împărţirea repetată a unei probleme de dimensiune mai mare în
două sau mai multe subprobleme de acelaşi tip urmată de combinarea
soluţiilor subproblemelor rezolvate pentru a obţine soluţia
problemei iniţiale.
Se dă un vector A=(a1,...,an) şi trebuie efectuată o prelucrare
oarecare asupra elementelor sale.
Presupunem că:
p,q{1,...,n} cu 1 p < q m{p,p+1,...,q-1} a.î.
prelucrarea secvenţei {ap,...,aq} se poate face prelucrând
subsecvenţele:
{ap,...,am} şi {am+1,...,aq} şi apoi combinând rezultatele pentru a
obţine prelucrarea întregii secvenţe {ap,...,aq}.
Dacă se reuşeşte o astfel de formalizare a problemei atunci ea poate
fi rezolvată cu ajutorul acestei metode.
Vom da în continuare o procedură recursivă în pseudocod:
procedure DI (p,q,α)
if q-p eps then
call PREL (p,q,α)
else
call IMPARTE (p,q,m) ;
call DI (p,m,β);
call DI (m+1,q,γ);
call COMB (β,γ,α);
endif;
return;
end procedure
Câteva precizări se impun:
procedura trebuie apelată prin call DI (1,n,α) în α obţinându-se
rezultatul final;
eps este lungimea maxim a unei secvene {ap,...,aq} notată prin (p,q)
pentru care se face prelucrarea directă fără a mai fi necesară
împărţirea în subprobleme;
procedura PREL realizează prelucrarea directă a secvenţelor (p,q);
procedura COMB realizează combinarea rezultatelor β şi γ ale
prelucrării a două secvenţe vecine (p,m) şi (m+1,q) obţinând
rezultatul α al prelucrării întregii secvenţe (p,q);
prin procedura IMPARTE se obţine valoarea lui m.
Vom da ca exemplu problema sortării crescătoare a unui şir de
întregi prin interclasare.
deoarece secvenţele (i,i+1) sunt uşor de ordonat acestea vor
constitui secvenţele ce se vor prelucra, deci eps = 1;
- m se va calcula ca (p+q)/2, deci nu mai e nevoie de procedura
specială IMPARTE;
procedura COMB va interclasa întotdeauna două secvenţe (p,m) şi
(m+1,q) ordonate crescător;
vom folosi un vector x drept structură globală şi vom face toate
prelucrările pe elementele sale nemaiavând nevoie de zonele α,β;
pentru zona γ vom folosi un vector local y în procedura COMB acesta
conţinând elementele corespondente din x dar ordonate crescător; tot
în procedura COMB se vor copia apoi elementele lui y din porţiunea
(p,q) în x;
evident că procedurile din schema generală a algoritmului sunt
funcţii C cititorul făcând analogiile necesare.
#include <stdio.h>
#include <conio.h>
#define nrelem 100
int x[nrelem];
int n;
void PREL (int p, int q)
{int aux;
if ((p<q) && (x[p] > x[q])) {
aux=x[p];
x[p]=x[q];
x[q]=aux;
}
}
void COMB (int inf, int mijloc, int sup)
{int i,j,k,l;
int y[nrelem];
i=k=inf;
j=mijloc+1;
while (i<=mijloc && j<=sup)
{ if (x[i] <= x[j])
y[k++]=x[i++];
else
y[k++]=x[j++];
}
for(l=i; l<=mijloc; y[k++]=x[l++]);
for(l=j; l<=sup; y[k++]=x[l++]);
for(k=inf; k<=sup; x[k++]=y[k]);
}
void DI (int p, int q)
{ int m;
if ((q p) <= 1)
PREL (p,q);
else
{m=(p+q)/2;
DI (p,m);
DI (m+1,q);
COMB (p,m,q);
}
return;
}
void main(void)
{int i;
clrscr();
printf ("dati nr de elemente\n");
scanf ("%d",&n);
for (i=1; i<=n; i++)
{printf("x[%d]=",i);
scanf("%d",&x[i]);
}
DI (1,n);
printf("\nsirul sortat crescator este:\n");
for (i=1; i<=n; i++) printf("x[%d]=%d\n",i,x[i]);
getch();
}
14.5. METODA PROGRAMĂRII DINAMICE
Această metodă este aplicabilă problemelor de optim în care
soluţia poate fi privită ca rezultatul unui şir de decizii d1, . . .
,dn. Fiecare decizie depinde de deciziile deja luate şi nu este unic
determinată (spre deosebire de tehnica greedy unde fiecare decizie
care se ia trebuie să fie unică). Totodată este necesar să fie
satisfăcută una din variantele principiului optimalităţii pentru a
putea fi aplicată această metodă.
Vom formaliza acest principiu al optimalităţii:
fie d1,...,dn un şir optim de decizii (SOD) care transformă starea so
în starea sn, trecând prin stările intermediare s1, . . . ,sn-1; vom
nota acest fapt prin (d1,dn) este SOD pentru perechea de stări
(so,sn);
grafic procesul este descris ca în figura următoare:
d1 d2 dn
*----->*---->*------>* . . . *------>*
so s1 s2 sn-1 sn
Vom da acum mai multe variante ale principiului optimalităţii:
dacă (d1,dn) este SOD pentru (so,sn) atunci (d2,dn) este SOD pentru
(s1,sn);
2) dacă (d1,dn) este SOD pentru (so,sn) atunci i din {1, . . .
,n-1} avem
a) (d1,di) este SOD pentru (so,si) SAU
b) (di+1,dn) este SOD pentru (si,sn).
3) dacă (d1,dn) este SOD pentru (so,sn) atunci i din {1, . . .
,n-1} avem
a) (d1,di) este SOD pentru (so,si) ŞI
b) (di+1,dn) este SOD pentru (si,sn).
Ultima variantă este cea mai generală şi mai completă. Odată
verificat o formă a principiului optimalităţii, metoda programării
dinamice constă în a scrie relaţiile de recurenţă şi apoi de a le
rezolva. În general relaţiile de recurenţă sunt de 2 tipuri :
fiecare decizie di depinde de di+1,...,dn - relaţii de recurenţă
înainte, deciziile vor fi luate în ordinea dn, dn-1, . . . ,d1;
fiecare decizie di depinde de d1, . . . ,di-1 - relaţii de recurenţă
înapoi, deciziile vor fi luate în ordinea d1, d2, . . . , dn.
Problemă rezolvată
Fie G=(X,Γ) un 1-graf orientat căruia îi ataşăm matricea
costurilor, (fiecare arc (i,j)Γ este etichetat cu o valoare reală
strict pozitivă). Se pune problema găsirii drumului de valoare minim
(DVM) între oricare 2 vârfuri i şi j.
Rezolvare
Verificăm prima variantă a principiului optimalităţii:
- fie i, i1, i2, . . . , im, j DVM între i şi j atunci evident că
i1, . . . , im, j este DVM între i1 şi
j;
- notăm prin L(i,k) lungimea DVM dintre i şi k, kX;
- notăm deasemenea dkj valoarea arcului (k,j);
- ecuaţiile de recurenţă sunt:
L(i,j) = min {L(i,k) + dkj)}
(k,j)Γ
#include<stdio.h>
#include<values.h>
#define nn 10
int d[nn+1][nn+1],i,j,n;
int L(int i,int j)
{
int m=MAXINT;
int k,x=0;
if (i= =j) m=0;
else
for (k=1;k<=n;k++)
if (d[k][j] < MAXINT)
{ x=L(i,k)+d[k][j];
if (x<m) m=x;
}
return m;
}
void citestematrice(void)
{int i,j;
printf("\ndati dimensiunea matricii distantelor : ");
scanf("%d",&n);
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
{ printf("d[%d,%d]= ",i,j);
scanf ("%d",&d[i][j]);
}
for(i=1;i<=n;i++)
for(j=1;j<=n;j++) if (d[i][j]= = 1) d[i][j]=MAXINT;
}
void citeste(void)
{ printf("\ndati varful initial : ");
scanf("%d",&i);
printf("\ndati varful final : ");
scanf("%d",&j);
}
void afiseaza(int val)
{ printf("\nvdlm intre varful %d si %d este %d",i,j,val);
}
void main(void)
{ int vdlm;
clrscr();
citestematrice();
citeste();
vdlm=L(i,j);
afiseaza(vdlm);
getch();
}
Probleme propuse
14.5.1.
Se dă o matrice A=(aij), i=1,...,n; j=1,...,m cu elementele din
mulţimea {0,1,2}. Două elemente din A aij şi akl se numesc 4-vecine
dacă i-k+j-l = 1.
Notăm cu So, S1 şi S2 submulţimile formate din elementele matricii
egale cu 0, 1 respectiv 2. Submulţimea S1 se împarte în grupuri
astfel: aij şi akl fac parte din acelaşi grup dacă sunt 4-vecine sau
dacă apq S1 : aij şi apq sunt 4-vecine iar apk şi akl sunt
din acelasi grup. Un element akl So îl vom numi spion al grupului
G S1 dac aij G a.î. akl şi aij s fie vecine. Un spion
este perfect dac are toţi vecinii din G.
Se cere:
a) cel mai numeros grup (S1) care are un singur spion (dacă
există).
b) toate grupurile care au cel puţin doi spioni perfecţi.
14.5.2.
Se dă o matrice cu elemente care au valori din {d,o}, care reprezintă
un teren cu drumuri {d} şi obstacole {o}. În acest teren se află un
tanc şi o ţintă. Acest tanc poate primi comenzi pentru rotirea
ţevii (cu 90o în ambele sensuri), pentru deplasare (în direcţia
ţevii cu o linie sau cu o coloană) sau pentru tragere (în direcţia
ţevii pentru a nimeri ţinta) în cazul în care între tanc şi
ţintă nu există nici un obstacol. Considerând că deplasarea nu se
poate efectua prin obstacole, se cere cel mai scurt drum necesar
tancului pentru a putea distruge ţinta şi şirul comenzilor efectuate
în cazul în care exist un astfel de drum.
14.5.3.
Se d o matrice cu elemente din {0,1,2,3}, reprezentând o pădure cu
capcane (0) şi cărări (1). În această pădure se află o vulpe (2)
şi mai mulţi lupi (3). Fiecare lup încearcă să se apropie de vulpe
fără a şti unde se află capcanele, iar în cazul în care cade
într-o capcană, moare. Vulpea încearcă să se îndepărteze de cel
mai apropiat lup, având însă posibilitatea să descopere şi
capcanele şi să le ocolească. Atât vulpea cât şi lupii îi pot
modifica poziţia doar cu o linie sau cu o coloană în fiecare moment.
Să se spună dacă vulpea reuşeşte să scape de lupi.
14.5.4.
Se consideră un teren dreptunghiular sub forma unei matrici A de m
linii şi n coloane. Elementele aij ale matricii conţin cotele
(înălţimile) diferitelor porţiuni astfel obţinute. Se presupune
că o bilă se găseşte pe o porţiune (io,jo) având cota a(io,jo).
Se cere un program care să precizeze toate traseele (începând cu
(io,jo) ) posibile pe care le poate urma bila spre a ieşi din teren,
ştiind că bila se poate deplasa în orice porţiune de teren
4-învecinat cu o cotă strict inferioară cotei terenului pe care se
găseşte bila.
14.5.5.
Se cere un program care rezolvă problema labirintului (nerecursiv).
O matrice de m linii şi n coloane reprezintă un labirint dacă:
a(i,j) = o - semnificând culoar;
a(i,j) = 1 - semnificând zid.
Un traseu de ieşire pornind de la o porţiune (io,jo) trebuie să fie
o succesiune de perechi (io, jo), (i1, j1) . . . (ik, jk) astfel
încât 2 perechi învecinate să fie 4-vecine şi a(ip,jp)=0
p=0, . . . ,k.
ANEXĂ
UN MEMENTO AL SINTAXEI LIMBAJULUI C
SINTAXA LIMBAJULUI C
În descrierea sintaxei limbajului vom folosi următoarele notaţii:
a) ::= cu semnificaţia "prin definiţie este";
b) pentru a separa alternativele unei definiţii sintactice;
c) < > pentru definirea unor metavariabile;
d) [ ] pentru delimitarea elementelor opţionale;
e) .. pentru a indica elementele care se pot repeta.
Exemplu: Pentru a defini un "întreg_zecimal" ca o succesiune de cifre
semnificative vom scrie:
<Întreg_zecimal> ::= <Cifră_nenulă> [<Cifră>] ..
A. Atomi lexicali
<Cuvinte_cheie> ::=
autobreakcasecharconstcontinuedefaultdodouble|
elseenumexternfloatforgotoifint|longregister
|
returnshortsignedsizeofstatic|struct
switchtypedef
unionunsignedvoidvolatilewhile
<Nume> ::= <Literă>[<Literă><Cifră>] ..
<Literă> ::=
abcdefghijklmnopqrstuvwxyz
BCDEFGHIJKLMNOPQRS
TUVWXYZ_
<Cifră> ::= 0123456789
<Constantă> ::= <Constantă_întreagă><Constantă_reală>
<Constantă_enumerare><Constantă_caracter>
<Constantă_întreagă> ::= <Număr_întreg>[<Sufix_întreg>
<Număr_întreg> ::=
<Întreg_zecimal><Întreg_hexazecimal><Întreg_octal>
<Întreg_zecimal> ::= <Cifră_nenulă>[<Cifră>] ..
<Cifră_nenulă> ::= 123456789
<Întreg_hexazecimal> ::= 0x<Cifră_hexa> ..
<Cifră_hexa> ::=
<Cifră>abcdefABCDEF
<Întreg_octal> ::= 0[<Cifră_octală>] ..
<Cifră_octal_> ::= 01234567
<Sufix_întreg> ::=
<Sufix_unsigned>[<Sufix_long>]<Sufix_long>[<Sufix_unsigned>]
<Sufix_unsigned> ::= Uu
<Sufix_long> ::= Ll
<Constantă_reală> ::= <Constantă_fract>[<Exponent>][<Sufix_real>]
<Cifră> .. <Exponent>[<Sufix_real>]
<Constantă_fract> ::= [<Cifră>] .. . <Cifră>..<Cifră>.. .
<Exponent> ::= Ee[+-]<Cifră> ..
<Sufix_real> ::= FfLl
<Constantă_enumerare> ::= <Nume>
<Constantă_caracter> ::= '<Caracter_C> ..'L'<Caracter_C>'
<Caracter_C> ::= <Orice_caracter_tipăribil_cu excepţia: ' i \>
<Secvenţă_escape>
<Secvenţă_escape> ::=
\"\'\?\\\a\b\f\n\r\t\v
\<Cifră_octală>[<Cifră_octală>[<Cifră_octală>]]
\x<Cifră_hexa> ..
<Şir_de_caractere> ::= "[<Caracter_S> ..]"
<Caracter_S> ::= <Orice_caracter_tipăribil_cu excepţia: ' şi
\><Secvenţa_escape>
<Operatori_şi_semne_de_punctuaţie
>::= +-*/%^&~!=->+=-=*=
/=%=^=&=!=<<>><<=>>== =<=
>=<>&&!!++--,()[]{};?:...
B. Declaraţii
<Unitate_de_compilare> ::= <O_declaraţie> ..
<O_declaraţie> ::= <Def_funcţie><Declaraţie>;
<Declaraţie> ::= [<Specificatori>][<Listă_declaratori>]
<Specificatori> ::= <Specificator> ..
<Specificator> ::= <Clasă_memorare><Tip>typedef
<Clasă_memorare> ::= autoregisterstaticextern
<Tip> ::= <Tip_simplu><Nume_typedef><Calificator>
<Descriere_tip_enumerare><Descriere_tip_neomogen>
<Tip_simplu> ::=
charshortintlongsignedunsignedfloatdouble void
<Nume_typedef> ::= <Nume>
<Descriere_tip_enumerare> ::= enum<Nume>enum[<Nume>]{<Lista_enum>}
<Lista_enum> ::= <Enumerator>[,<Enumerator>] ..
<Enumerator> ::= <Nume>[=<Expr_constant_>{]
<Descriere_tip_neomogen> ::= <Tip_neomogen><Nume>
<Tip_neomogen>[<Nume>]{<List__câmpuri>}
<Tip_neomogen> ::= structunion
<Listă_câmpuri> ::= <Decl_câmpuri>;[<Decl_câmpuri>;] ..
<Decl_câmpuri> ::= [<Specificatori>][<Câmp>[,<Câmp>] ..]
<Câmp> ::= <Declarator>[<Nume>]:<Expr_constantă>
<Listă_declaratori> ::= <Decl_init>[,<Decl_init>] ..
<Decl_init> ::= <Declarator>[<Iniializare>]
<Declarator> ::= [<Indirect>..]<Denumire>[<Decl_F_T>]
<Indirect> ::= *[<Calificator> ..]
<Denumire> ::= <Nume>(<Declarator>)
<Decl_F_T> ::= ([<L_declar_par>])<Decl_tablou> ..
<Decl_tablou> ::= [][<Expr_constantă>]
<L_declar_par> ::= <Decl_par>[,<Decl_par>] .. [, ..]
<Decl_par> ::= <Tip>..<Declarator><Decl_tip>
<Decl_tip> ::= <Tip>..[<Indirect>..][(<Decl_tip>)][<Decl_F_T>]
<Iniţializare> ::= =<Expr> ={<Listă_init>[,]}
<Listă_init>::=<Expr>[,<Listă_init>
..{<Listă_init>}[,<Listă_init>] ..
<Def_funcţie> ::= [<Specif_funcţie> ..]<Declarator>[<Declaraţie>;]
..<Bloc>
<Specif_funcţie> ::= externstatic<Tip>
C. Expresii
<Expr> ::= <Expresie>[,<Expresie>] ..
<Expresie> ::=
<Expr_condiţională>|<Expr_unară><Oper_atribuire><Expresie>
<Oper_atribuire> ::=
+*=/=%=+=-=<<=>>=&=^=!=
<Expr_condiţională> ::= <Expr_SAU_logic>
<Expr_SAU_logic> ? <Expr> : <Expr_condiională>
<Expr_SAU_logic> ::= <Expr_ŞI_logic><Expr_SAU_logic> !!
<Expr_ŞI_logic>
<Expr_ŞI_logic> ::= <Expr_SAU><Expr_ŞI_logic> && <Expr_SAU>
<Expr_SAU> ::= <Expr_SAU_exclusiv><Expr_SAU><Expr_SAU_exclusiv>
<Expr_SAU_exclusiv> ::= <Expr_ŞI><Expr_SAU_exclusiv> ^ <Expr_ŞI>
<Expr_ŞI> ::= <Expr_egalitate><Expr_ŞI> & <Expr_egalitate>
<Expr_egalitate> ::=
<Expr_relaională><Expr_egalitate>==Expr_relaţională>
<Expr_egalitate> != <Expr_relaţională>
<Expr_relaţională> ::= <Expr_deplasare> <Expr_relaţională> <
<Expr_deplasare>
<Expr_relaţională> > <Expr_deplasare>
<Expr_relaţională> <= <Expr_deplasare>
<Expr_relaţională> >= <Expr_deplasare>
<Expr_deplasare> ::= <Expr_aditivă> <Expr_deplasare> <<
<Expr_aditivă>
<Expr_deplasare> >> <Expr_aditivă>
<Expr_aditivă> ::= <Expr_multiplicativă><Expr_aditivă> +
<Expr_multiplicativă>
<Expr_aditivă> - <Expr_multiplicativă>
<Expr_multiplicativă>::= <Expr_multiplicativă> *
<Expr_prefixată>
<Expr_multiplicativă> / <Expr_prefixată>
<Expr_multiplicativă> % <Expr_prefixată>
<Expr_prefixată> ::= <Expr_unară>(<Decl_tip>)<Expr_prefixată>
<Expr_unară> ::= <Expr_postfixată><Op_unar><Expr_prefixată>
++<Expr_unară> --<Expr_unară>sizeof<Expr_unară>
sizeof(<Decl_tip>)
<Op_unar> ::= &*+-~!
<Expr_postfixată> ::= <Termen><Expr_postfixată>[<Expr>]
<Expr_postfixată>(<Listă_Expresii>) <Expr_postfixată> .
<Nume>
<Expr_postfixată> -> <Nume><Expr_postfixată> ++
<Expr_postfixată>--
<Termen> ::= <Nume><Constantă><Şir_de_caractere>(<Expr>)
<Listă_Expresii> ::= [<Expr>] ..
<Expr_constantă> ::= <Expr_condiţională>
D. Instrucţiuni
<Instr>::= <Instr_etichetată><Instr_compusă>
<Instr_expresie>Instr_de_selecţie>
<Instr_de_ciclare><Instr_de_salt>
<Instr_etichetată> ::= <Nume>:<Instr>case <Expr_constantă {
:<Instr>
default : <Instr>
<Instr_compusă> ::= <Bloc>
<Bloc> ::= {[<Declaraţie>;] .. [<Instr>] ..}
<Instr_expresie> ::= [<Expr>];
<Instr_de_selecţie> ::= if (<Expr>) <Instr>if (<Expr>) <Instr> else
<Instr>
switch (<Expr>) <Instr>
<Instr_de_ciclare> ::= while (<Expr>)<Instr>;
do <Instr> while (<Expr>);
for ( [<Expr>];[<Expr>];[<Expr>] ) [<Instr>];
<Instr_de_salt> ::= goto <Nume>;continue;break;return
[<Expr>];
CUPRINS
BIBLIOGRAFIE
Brian W.Kerninghan, Dennis M. Ritchie, The C Programming Language, INC.
Englewood Cliffs, New Jersey, 1978.
Liviu Negrescu, Limbajul C, Editura Libris, Cluj-Napoca, 1997.
G. Moldovan, M. Lupea, V.Cioban, Probleme pentru programare în
limbajul C, Litografia Universităţii Babeş-Bolyai, Cluj-Napoca, 1995.