Documente Academic
Documente Profesional
Documente Cultură
Capitolul 1:
Programele sunt scrise pentru a instrui masinile sa lucreze cu taskuri specifice sau sa rezolve
probleme specifice. O procedura (descrisa pas cu pas) asociata unui task se
numeste algoritm. Programarea este activitatea de comunicare (codificare) a algoritmilor in
calculatoare. Procesul de programare are (in general) patru pasi:
1. Specificarea task-ului;
2. Descoperirea unui algoritm pentru solutia sa;
3. Codificarea algoritmului in C;
4. Testarea codului.
1. tastatura;
2. discheta;
3. banda;
4. CD-ROM.
1. ecranul terminalului;
2. imprimanta;
3. discheta;
4. banda;
5. CD-ROM.
Sistemul de operare consta intr-o colectie de programe speciale si are doua scopuri principale:
cc ex1.c
Daca nu sunt erori in ex1.c, atunci aceasta comanda produce fisierul executabil asociat
(ex1.exe). Acum acesta poate fi rulat (executat) cu numele sau (ex1 sau ex1.exe).
In continuare, vom preciza trei dintre trasaturile procesului de compilare (mentionam ca
asupra acestor notiuni, vom reveni cu detalii interesante intr-un capitol viitor):
1. invocarea preprocesorului;
2. invocarea compilatorului;
3. invocarea incarcatorului.
Preprocesorul modifica o copie a codului sursa prin includerea altor fisiere si facand alte
schimbari. Compilatorul traduce aceasta in cod obiect folosit de incarcator pentru producerea
fisierului executabil final. Fisierul care contine codul obiect se numeste fisier obiect. Fisierele
obiect, spre deosebire de fisierele sursa, nu se pot intelege asa usor. Cand spunem deci
compilare, de fapt invocam preprocesorul, compilatorul si apoi incarcatorul.
Dupa ce scriem un program, acesta trebuie compilat si testat. Daca sunt necesare modificari,
atunci codul sursa trebuie editat din nou. Asadar, partea proceselor de programare consta din
ciclul:
#include
main()
{
printf("azi am inceput laboratoarele de C\n");
}
Folosind un editor de texte, presupunem ca am scris si salvat acest fisier numit "ex1.c". Cand
programul este compilat si rulat atunci va apare pe ecran sirul:
Explicatii:
1. #include
Liniile care incep cu "#" se numesc directive de preprocesare (precompilare). Acestea
comunica cu preprocesorul. Aceasta directiva "#include" determina preprocesorul sa includa
o copie a fisierului header "stdio.h" in acest punct al codului. Parantezele unghiulare din ""
indica ca acest fisier se gaseste in biblioteca C (pentru compilatorul Borland 3.1 pentru MS-
DOS, acesta se gaseste in subdirectorul BC31/INCLUDE). Am inclus acest fisier deoarece
acesta contine informatii despre functia "printf()".
2. main()
Fiecare program are o functie numita "main", care se executa intai. Parantezele ce urmeaza
dupa "main" indica compilatorului ca aceasta este o functie.
3. {
Acolada stanga incepe corpul fiecarei functii. O acolada dreapta corespunzatoare trebuie sa
fie la sfarsitul functiei.
4. printf()
Un sir constant in C consta dintr-un numar de caractere incadrate intre ghilimele. Acest sir
este un argument al functiei "printf()". Caracterele \n de la sfarsitul sirului (se citesc
"backslash n"), reprezinta, de fapt, un singur caracter numit "newline".
#include
main()
{
printf("azi am inceput ");
printf("laboratoarele de C\n");
}
Observatii:
In urmatorul exemplu vom ilustra folosirea variabilelor pentru manipularea valorilor intregi.
Variabilele sunt folosite sa memoreze valori. Din moment ce diferite tipuri de variabile sunt
folosite sa memoreze diferite tipuri de date, tipul fiecarei variabile trebuie specificat.
Pentru a ilustra aceasta idee vom calcula cate ore si minute contin un anumit numar de zile.
Algoritmul ar fi:
#include
main()
{
int zile, ore, minute;
zile=7;
ore=24*zile;
minute=60*ore;
printf("O saptamana are %d ore, %d minute.\n",ore, minute);
}
Explicatii:
Reprezinta o declaratie de variabile. Variabilele zile, ore, minute sunt declarate de tip "int",
unul dintre cele mai importante tipuri din C. O variabila de tip "int" poate lua o valoare
intreaga intre -32678 si 32677. Toate variabilele dintrr-un program trebuie declarate inainte
de a fi utilizate. Declaratiile, la fel ca si instructiunile, au la sfarsit ";".
2. Linia: zile=7:
Reprezinta o instructiune de atribuire (sau asignare). Semnul "=" este operatorul de asignare
de baza in C. Valoarea expresiei din partea dreapta a simbolului "=" este atribuita variabilei
din partea stanga.
Este similara celei prezentate in exemplul precedent, dar are trei argumente. Primul argument,
intotdeauna un sir de caractere, se numeste sir de control. Aici, intalnim specificarea de
conversie (care se mai numeste format) "%d". Formatele "%d" determina tiparirea valorilor
expresiilor corespunzatoare (al doilea si al treilea argument) in formatul intregilor zecimali.
Asadar, primul format "%d" corespunde cu valoarea variabilei "ore", iar cel de-al doilea
format "%d" cu valoarea variabilei "minute".
In C, toate variabilele trebuie declarate inainte de a fi utilizate in expresii si instructiuni.
Forma generala a unui program simplu este:
Directive de precompilare:
main()
{
declaratii
instructiuni
}
Operatorii binari: + - * / %
Exemple: 5 % 2 = 1 si 7 % 4 = 3.
Evident, in expresia a % b, b nu trebuie sa fie zero, din moment ce nu se poate face impartirea
cu zero.
#include
main()
{
float x, y;
x = 1.0;
y = 2.0;
printf("Suma dintre x si y este %f.\n", x+y);
}
Pe ecran se va afisa:
Observatii:
1. Linia: float x, y;
Semnifica declararea variabilelor x si y de tip "float" (deci de tip real). In realitate sunt
numere rationale din intervalul
[-10^{308},-10^{-308}] U [10^{-308},10^{308}].
Initializarea variabilelor se poate face si cand se declara acestea. De exemplu, putem scrie:
char c='A';
int i=1;
Observatii:
1. De obicei, pentru initializarea unei variabile se folosesc constante sau expresii constante.
Se pot insa folosi si variabile care au deja valoarea precizata. De exemplu, urmatoarea
secventa de program este corecta:
int zile=7, ore=zile * 24, minute=ore * 60;
Observatii:
Deoarece identificatorul PI se va inlocui peste tot (cu exceptia sirurilor constante) in 3.14159,
atunci acesta se va numi constanta simbolica.
2. Directiva #define poate aparea oriunde in program, dar ea afecteaza numai liniile care
urmeaza acesteia.
1. Lizibilitate marita. Se refera la citirea si intelegerea rapida a fisierului sursa (PI stim ce
inseamna si ne amintim ca este 3.ceva, deci nu trebuie sa scriem de fiecare data valoarea sa);
2. Schimbarile ulterioare ale unor valori constante se face foarte usor. De exemplu, vrem sa
modificam valoarea lui LIMIT la 10000.
In locul liniei:
Reamintim ca formatul %d este folosit pentru scrierea valorii unei expresii ca un intreg
zecimal. In mod similar:
printf("Multime de argumente: %s %d %f %c
%c\n","one",2,2.33,'G','O');
Argumentele lui "printf()" sunt separate de virgula, deci avem sase argumente. Primul
argument este sirul de control. Obtinem corespondenta:
%s <---> "one"
%d <---> 2
%f <---> 2.33
%c <---> 'G'
%c <---> 'O'
Observatii:
1. Daca instructiunea "printf()" contine prea multe caractere, atunci se poate scrie aceasta pe
mai multe linii, separate prin virgula. De exemplu, putem scrie:
printf("%s%s\n",
c - ca un caracter
d - ca un intreg zecimal
e - ca un numar in virgula flotanta in notatia stiintifica
f - ca un numar in virgula flotanta
g - in format e sau f (alegand cel mai scurt dintre ele)
s - ca un sir
Cand un argument este tiparit, locul unde este tiparit se numeste campul sau, iar numarul de
caractere ale acestui camp se numeste lungimea campului. Aceasta lungime poate fi
specificata intr-un format ca un intreg plasat intre caracterul % si caracterul de conversie. De
exemplu, instructiunea:
printf("%c%3c%7c\n", 'A', 'B', 'C');
va tipari:
A B C
Pentru numerele in virgula flotanta, putem controla precizia (numarul de cifre zecimale), la
fel ca lungimea campului. Forma generala a formatului este:
%m.nf
si semnifica ca m este lungimea campului, iar n precizia. Formatul %mf specifica doar
lungimea campului, iar formatul %.nf numai precizia. De exemplu, instructiunile:
printf("Numere1: %.1f %.2f %.3f\n", 1.0, 2.0, 3.0);
printf("Numere2: %7.1f %7.2f %7.3f\n", 4.0, 5.0, 6.0);
vor avea ca efect afisarea:
Numere1: 1.0 2.00 3.000
Numere2: 4.0 5.00 6.000
Functia "scanf()" este asemanatoare cu "printf()", dar este folosita pentru intrari in loc de
iesiri. Primul sau argument este un sir de control care are formatele corespunzatoare cu
variatele moduri de interpretare a sirurilor de intrare. Dupa sirul de control urmeaza adresele
variabilelor. Adresa unei variabile este locul din memorie unde este memorata variabila (vom
reveni in capitolele viitoare). Simbolul "&" reprezinta operatorul de adresa. De exemplu,
scanf("%d", &x);
c - la un caracter
d - la un intreg zecimal
f - la un numar in virgula flotanta (float)
lf - la un numar in virgula flotanta (double)
Lf - la un numar in virgula flotanta (long double)
s - la un sir
#include
#define PI 3.141592653589793
main()
{
double raza;
printf("\n%s\n\n%s",
"Acest program calculeaza aria cercului",
"Dati raza:");
scanf("%lf", &raza);
printf("\n%s\n%s%.2f%s%.2f%s%.2f\n%s%.5f\n\n",
"Aria = PI * raza * raza",
" = ", PI, " * ", raza, " * ", raza,
" = ", PI * raza * raza);
}
Presupunem ca la executia programului introducem raza egala cu 2.333. Atunci vor apare pe
ecran:
Acest program calculeaza aria cercului
Daca am calcula separat (pe hartie), am obtine Aria = 3.14 * 2.33 * 2.33 = 17.046746, numar
care nu coincide cu cel furnizat de calculator. Justificarea este aceea ca PI si raza sunt tiparite
doar cu doua zecimale, pe cand valorile lor sunt pastrate in memorie cu precizie mai mare.
Instructiunea while face parte din categoria actiunilor repetitive. Pentru a intelege aceasta
instructiune, vom face un exemplu de adunare a numerelor de la 1 la 10.
Exemplu:
#include
main()
{
int i=1, suma=0;
while (i<=10)
{
suma = suma + i;
i = i + 1;
}
printf("Suma primelor 10 numere este %d\n",suma);
}
Explicatii:
2. Constructia:
while (i<=10)
{
suma = suma + i;
i = i + 1;
}
Reprezinta o instructiune while (sau iteratie while). Mai intai, se evalueaza expresia i<=10.
Cum valoarea initiala a lui i este egala cu 1, rezulta ca se vor executa instructiunile dintre
acolade. Astfel, variabila suma va fi asignata cu vechea valoare a lui suma la care se adauga
valoarea lui i. Deci, suma se evalueaza la 1. Apoi, variabila I se evalueaza la suma dintre
vechea valoare a lui i (i=1) si 1, deci este egala cu 2. In acest moment, executia revine la
inceput adica evaluam expresia i<=10. Cum valoarea lui i este 2, rezulta ca se va executa iar
corpul lui while. La sfarsitul acestei iteratii, suma este evaluata la 1+2, iar i la 3. Se observa
usor ca i<=10 este tot adevarata, deci se va executa din nou corpul lui while. La sfarsitul celei
de-a treia iteratii, sum este evaluata la 1+2+3, iar i la 4. Procesul continua pana cand valoarea
lui i este 11, care implica falsitatea expresiei i<=10. Astfel se iese din bucla while.
3. Instructiunea:
printf("Suma primelor 10 numere este %d\n", suma);
va afisa mesajul Suma primelor 10 numere este 55.
Dorim sa citim mai multe numere (fara a sti aprioric numarul lor si care sunt acestea) si dorim
sa afisam suma lor.
Instructiunea while este una din cele trei constructii existente in C menite sa descrie actiuni
repetitive. In solutia noastra, utilizam valoarea returnata de functia "scanf()" pentru a controla
instructiunea while. Consideram urmatorul cod C:
#include
main()
{
int contor = 0;
1. scanf("%f", &x)==1
Simbolul == reprezinta operatorul de egalitate. Expresia a==b intoarce true daca valoarea lui
a este egala cu valoarea lui b. De exemplu, 1==1 intoarce true, 2==3 intoarce false. Functia
"scanf()" are rolul de a citi caractere scrise de utilizator, sa le converteasca la float, si sa
plaseze aceasta valoare la adresa lui x. Daca totul se desfasoara cu succes, atunci scanf()
intoarce valoarea 1, adica true. Daca din anumite motive, procesul de conversie esueaza,
atunci se intoarce valoarea 0 (deci false). Daca nu mai introducem nici o data (Control^z in
MD-DOS, CR urmat de Control^d in UNIX), atunci scanf() va intoarce valoarea -1 (deci tot
false).
Observatii:
1. Daca numaram spatiile, observam ca valoarea lui contor a fost tiparita pe un camp de 5
caractere, iar suma pe 12 caractere. Aceasta este cauzata de formatele "%5d" si "%12f".
Retineti ca tiparirea zecimalelor pentru suma este gresita de la a treia zecimala.
Un stil bun de scriere a codului este esential pentru arta programarii. Aceasta faciliteaza
citirea, scrierea si intretinerea programelor. Un stil bun foloseste:
1. spatii goale si comentarii, astfel incat codul este usor de citit si de inteles;
2. utilizarea indentarii este cruciala, care indica cu precizie structurile de control. De
exemplu,
in constructia
while (expresie)
instructiune
indentarea instructiunii indica ca executia acesteia este sub controlul iteratiei while;
3. alegerea de nume sugestive pentru variabile;
4. corespondenta dintre acolade. De exemplu, urmatorul program este scris in stilul "Bell
Labs
industrial programming style" (#,{,},m pe prima coloana).
#include
#include
#define GO "Start"
main()
{
Observatii:
1. Programatorii incepatori uneori cred ca vor "sparge" piata cu stilul lor propriu de redactare
a programelor. Atentie ! Utilizati strategia care este deja in uz.
Mai ales la inceputul invatarii programarii pe calculator, se fac multe erori simple, cum ar fi:
Exemplu:
#include
main()
{
char ch;
a_{n+2}=a_{n+1}+a_n, n>=1
Capitolul 2
Ca si alte limbaje, C are un alfabet si reguli pentru scrierea programelor corecte folosind
semne de punctuatie. Aceste reguli formeaza sintaxa limbajului C. Compilatorul C are rolul
de a testa daca un program C este corect. Daca sunt erori, atunci va afisa o lista de mesaje de
eroare si se va opri. Daca nu sunt erori, atunci compilatorul va "traduce" acest program in cod
obiect, folosit de incarcator pentru producerea codului executabil.
Mai intai compilatorul imparte multimea caracterelor (programul sursa) in atomi lexicali, care
reprezinta vocabularul de baza al limbajului. In ANSI C (ANSI = American National
Standards Institute) sunt sase tipuri de atomi lexicali (care se mai numesc si elemente lexicale
sau unitati lexicale):
Atentie ! Pentru a verifica aceasta, puteti citi lungimea unui cod executabil (fara
comentarii) si apoi sa comparati lungimea ccodului executabil obtinut dupa o noua
compilare (cu comentarii).
Exemple de comentarii:
1. /* un comentariu */
2. /** al doilea comentariu **/
3. /*****/
4. /*
* Al patrulea
* comentariu
*/
5. /**************
* Al cincilea *
* comentariu *
**************/
Cuvintele rezervate (cheie) au un inteles strict insemnand un atom individual. Ele nu pot fi
redefinite sau utilizate in alte contexte. Iata lista lor:
Anumite implementari pot contine si alte cuvinte rezervate: asm cdecl far huge interrupt
near pascal
Comparativ cu alte limbaje de programare, C are un numar mic de cuvinte rezervate. Ada, de
exemplu, are 62 cuvinte rezervate. Aceasta este o caracteristica a limbajului C de a avea doar
cateva simboluri speciale si cuvinte rezervate.
2.4 Identificatori
Un identificator este un atom lexical compus din secventa de litere, cifre sau underscore ("_")
cu restrictia ca primul caracter este o litera sau underscore. In multe implementari C, se face
distinctie dintre litere mici si mari. In general, se obisnuieste ca identificatorii sa fie scrisi cu
nume sugestive care sa usureze citirea si documentarea programului.
Exemple:
Identificatorii sunt creati pentru a da nume unice pentru diverse obiecte dintr-un program.
Cuvintele rezervate pot fi privite ca fiind identificatori. Identificatori precum "printf()" sau
"scanf()" sunt deja cunoscuti sistemului C ca fiind functii de intrare/iesire.
O diferenta majora dintre sistemele de operare si sistemele C o reprezinta lungimea admisa
pentru numele identificatorilor. Astfel, pentru unele sisteme vechi, este acceptat un
identificator al carui nume are mai mult de 8 caractere, dar numai primele 8 sunt
semnificative. De exemplu, identificatorul _23456781 este privit la fel ca _23456782.
In ANSI C, primele 31 de caractere sunt luate in considerare.
Atentie !
2.5 Constante
O secventa de caractere incadrate intre ghilimele, de exemplu "abc", este un sir constant. Este
inteles de compilator ca fiind un singur atom lexical. In capitolele ulterioare, vom vedea ca de
fapt sirurile constante se memoreaza ca siruri de caractere. Sirurile constante sunt tratate
mereu diferit fata de constantele de tip caracter. De exemplu, "a" nu este totuna cu 'a'.
De mentionat ca ghilimeaua " reprezinta un singur caracter, nu doua. De aceea, daca dorim sa
apara intr-un sir constant, atunci ea trebuie precedata de \ (backslash). Daca dorim ca intr-un
sir sa apara \, atunci trebuie sa-l precedam tot cu \ (devenind astfel \\).
Exemple:
1. "sir text"
2. "" /* sirul vid */
3. " " /* sir de spatii */
4. " a = b + c " /* nu se executa nimic */
5. " /* acesta nu este un comantariu */ "
6. " un sir ce contine ghilimea \" "
7. " un sir ce contine backslash \\ "
8. /* "gresit" */ /* nu este un sir */
9. "gresit
doi" /* nici asta nu este sir */
Doua siruri constante care sunt separate doar printr-un spatiu vor fi concatenate de compilator
intr-unul singur. De exemplu,
"abc" "def" este echivalent cu "abcdef".
Aceasta este o trasatura a limbajului ANSI C, nefiind disponibil in C traditional. Sirurile
constante sunt tratate de compilator ca atomi lexicali. Ca si alte constante, compilatorul va
rezerva spatiu in memorie pentru pastrarea sirurilor constante.
Operatori Asociativitate
() ++ (postfix) -- (postfix) de la stanga la dreapta
+(unar) -(unar) ++(prefix) --(prefix) de la dreapta la stanga
*/% de la stanga la dreapta
+- de la stanga la dreapta
= += -= *= /= etc. de la dreapta la stanga
Toti operatorii de pe o linie (de exemplu, *, /, %) au aceeasi prioritate intre ei, dar au
prioritate mai mare decat cei ce apar in liniile de mai jos.
Operatorii + si - pot fi si binari si unari. De remarcat ca cel unar are prioritate mai mare. De
exemplu, in expresia
- a * b - c
primul operator - este unar, pe cand al doilea binar. Folosind regulile de precedenta, se vede
ca aceasta este echivalenta cu
((- a) * b) - c
Operatorii de incrementare si de decrementare (++, --) au o prioritate foarte mare (dupa cum
se poate vedea in tabelul de mai sus) si se pot asocia atat de la dreapta la stanga, cat se de la
stanga la dreapta. Operatorii ++ si -- se pot aplica variabilelor, dar nu si constantelor. Mai
mult, ei pot apare ca notatie prefixata, cat si postfixata. De exemplu, putem avea ++i si
contor++, dar nu putem avea 167++ sau ++(a * b - 1).
Fiecare din expresiile ++i si i++ au o valoare; mai mult fiecare cauzeaza incrementarea valorii
variabilei i cu o unitate.
Diferenta este:
1. expresia ++i va implica intai incrementarea lui i, dupa care expresia va fi evaluata la
noua valoare a lui i;
2. expresia i++ va implica evaluarea sa la valoarea lui i, dupa care se va incrementa i.
Exemplu:
int a, b, c = 0;
a = ++c;
b = c++;
printf("a=%d b=%d c=%d ++c=%d\n", a, b, c, ++c);
Intr-un mod similar, --i va implica decrementarea valorii lui i cu 1, dupa care expresia --i va
avea noua valoare a lui i, pe cand i-- se va evalua la valoarea lui i, dupa care i se va
decrementa cu 1.
Retineti deci ca, spre deosebire de + si -, operatorii ++ si -- vor determina schimbarea
valorii variabilei i din memorie. Se mai spune ca operatorii ++ si -- au efect lateral (side
effect).
Daca nu folosim valoarea lui ++i sau a lui i++, atunci acestea sunt echivalente. Mai precis, +
+i; si i++; sunt echivalente cu i = i + 1;
Exemple:
a * b / c (a * b) / c 0 a * b % c + 1 ((a * b) % c) + 1 3 ++ a
* b - c -- ((++ a) * b) - (c --) 1 7 - - b * ++ d 7 - ((- b) *
(++ d)) 17
Pentru schimbarea valorii unei variabile, am utilizat deja instructiunea de asignare (atribuire),
cum ar fi
a = b + c;
Spre deosebire de celelalte limbaje, C trateaza = ca un operator. Precedenta sa este cea mai
mica dintre toti operatorii si asociativitatea sa este de la dreapta la stanga. O expresie de
asignare simpla are forma:
variabila = parte_dreapta unde "parte_dreapta" este o expresie. Daca punem ";" la sfarsitul
expresiei de asignare, atunci vom obtine instructiune de asignare. Operatorul = are doua
argumente, "variabila" si "parte_dreapta". Valoarea expresiei "parte_dreapta" este asignata
pentru "variabila" si aceasta valoare se returneaza de catre expresia de asignare (ca un tot
unitar).
b = 2;
c = 3;
a = b + c;
unde toate variabilele sunt de tipul int. Folosind faptul ca = este un operator, putem condensa
aceasta la
a = (b = 2) + (c = 3);
Explicatia este ca expresia de asignare b = 2 atribuie valoarea 2 atat variabilei b, cat si
instructiunii intregi.
Daca exemplul de mai sus pare artificial, atunci o situatie frecvent intalnita este asignarea
multipla. De exemplu, instructiunea
a = b = c = 0;
este echivalenta cu (folosind asociativitatea de la dreapta la stanga)
a = (b = (c = 0));
Relativ la =, mai exista inca doi operatori. Este vorba de += si -=. Expresia k = k + 2 va
aduna 2 la vechea valoare a lui k si va asigna rezultatul lui k si intregii expresii. Expresia k
+= 2 face acelasi lucru.
Exemplu:
Expresia de asignare
j *= k + 3
este echivalenta cu
j = j * (k + 3)
si nu cu
j = j * k + 3
Fie declaratia:
int i = 1, j = 2, k = 3, m = 4;
Consideram urmatoarele exemple de evaluari ale expresiilor: Expresie Expresie echivalenta
Expresie echivalenta Valoare:
i += j + k i += (j + k) i = (i + (j + k)) 6
j *= k = m + 5 j *= (k = (m + 5)) j = (j * (k = (m + 5))) 18
#include
main()
{
int i = 0, power = 1;
2.11 Sistemul C
Exemplu:
#include
#include
main()
{
int i, n;
printf("\n%s\n%s",
"Vom afisa niste intregi aleatori.",
"Cati doriti sa vedeti ? ");
scanf("%d", &n);
for (i = 0; i < n; ++i)
{
if (i % 6 == 0)
printf("\n");
printf("%12d", rand());
}
printf("\n");
}
Daca de exemplu, tastam numarul 11, atunci pe ecran vor apare 11 numere intregi aleatoare.
Observatii:
Controlul instructiunilor
Operatori Asociativitate
() ++ (postfix) -- (postfix) de la stanga la dreapta
+ (unar) - (unar) ++ (prefix) -- (prefix) de la dreapta la stanga
*/% de la stanga la dreapta
+- de la stanga la dreapta
< <= > >= de la stanga la dreapta
== != de la stanga la dreapta
&& de la stanga la dreapta
|| de la stanga la dreapta
?: de la dreapta la stanga
= += -= *= /= etc de la dreapta la stanga
, (operatorul virgula) de la stanga la dreapta
Operatorul ! este unar, spre deosebire de toti operatori (relationali, de egalitate si logici) care
sunt binari. Toti operatorii vor fi prezenti in expresii ce pot lua valoarea intreaga 1 sau 0.
Motivul este ca C reprezinta "false" orice expresie egala cu zero, si "true" orice expresie
diferita de zero.
Am vazut ca operatorii <, >, <=, >= sunt toti binari. Expresiile ce contin acesti operatori pot
lua valoarea 0 sau 1.
1. a < 3
2. a > b
3. -1.1 >= (2.2 * x + 3.3)
4. a < b < c (corecta, dar confuza)
5. a =< b
6. a < = b
7. a >> b
Fie expresia relationala "a < b". Daca valoarea lui a este mai mica decat valoarea lui b, atunci
expresia va avea valoarea 1, pe care o gandim ca fiind "true". Daca valoarea lui a este mai
mare decat valoarea lui b, atunci expresia va avea valoarea 0, pe care o gandim ca fiind
"false". Observam ca valoarea lui "a < b" este aceeasi cu valoarea lui "a - b < 0". Folosind
precedenta operatorilor aritmetici, aceasta este deci echivalenta cu "(a - b) < 0". De altfel, pe
multe masini, expresii cum sunt "a < b" sunt implementate ca fiind "a - b < 0".
1. c == 'A'
2. k != -2
3. x + y == 2 * x - 5
4. a = b
5. a = = b - 1
6. (x + y) =! 44
Intuitiv, o expresie de egalitate cum ar fi a == b este sau "true" sau "false". Mai precis, daca a
este egal cu b, atunci a == b intoarce valoarea 1 (true); altfel, aceasta intoarce valoarea 0
(false). O expresie echivalenta este a - b == 0 (aceasta este ceea ce se implementeaza la nivel
masina).
Expresia "a != b" ilustreaza folosirea operatorului "diferit de" (sau "nu este egal cu").
Operatorul logic ! este unar, iar && si || sunt binari. Expresiile ce contin acesti operatori
intorc valoarea 0 sau 1. Negarea logica poate fi aplicata unei expresii aritmetice sau unui tip
pointer. Daca o expresie are valoarea 0, atunci expresia negata are valoarea 1. Daca expresia
are o valoare diferita de 0, atunci expresia negata intoarce valoarea 1.
1. !a
2. !(x + 7.7)
3. !(a < b || c < d)
4. a!
5. a != b (este corecta, dar se refera la operatorul "diferit")
Operatorii logici binari && si || pot fi folositi in expresii care intorc 0 sau 1.
1. a && b
2. a || b
3. !(a < b) && c
4. 3 && (-2 * a + 7)
5. a &&
6. a | | b
7. a & b (corecta, dar se refera la operatii peste biti)
8. &b (corecta, dar se refera la adresa lui b)
Pentru expresiile ce contin && sau ||, evaluarea are loc cand s-a stabilit deja valoarea
expresiei, eventual fara parcurgerea intregii expresii. Astfel, presupunem ca "expr1" se
evalueaza la 0 (false). Atunci expresia expr1 && expr2 se va evalua la 0, fara a se mai
face evaluarea expresiei "expr2".
Alt exemplu, daca "expr1" se evalueaza la 1 (true), atunci expresia expr1 || expr2 se va
evalua la true fara a se mai evalua expresia "expr2".
Uneori se mai spune ca operatorii && si || sunt lazy (adica le este lene sa mai evalueze toti
operanzii din expresie).
Exemplu:
{
a = 1;
{
b = 2;
c = 3;
}
}
a = b;
a + b + c;
;
printf("%d\n", a);
Exemplu:
Instructiunea "if" de mai jos va testa daca se poate face impartirea cu y (ce trebuie sa fie
diferit de 0):
if (y != 0.0)
x /= y;
Urmatoarele doua instructiuni
if (j < k)
min = j;
if (j < k)
printf("j este mai mic decat k\n");
se pot scrie intr-una singura
if (j < k)
{
min = j;
printf("j este mai mic decat k\n");
}
Instructiunea "if-else" de mai jos este foarte apropiata de instructiunea "if". Aceasta are forma
generala:
if (expresie)
instructiune1
else
instructiune2
Semantica intuitiva este de asemenea clara. Daca valoarea expresiei este diferita de zero,
atunci se executa instructiune1 si "se sare" peste instructiune2. Daca valoarea expresiei este
zero, atunci "se sare" instructiune1, si se executa instructiune2.
Exemplu:
if (x < y)
min = x;
else
min = y;
printf("Valoarea minima = %d\n", min);
"While", "for" si "do" sunt cele trei instructiuni repetitive din limbajul C. Consideram
urmatorul format general al instructiunii "while" (iteratia sau bucla "while").
while (expresie)
instructiune
instructiune_urmatoare
Mai intai se evalueaza expresie. Daca aceasta nu este zero (deci este "true"), atunci se executa
instructiunea, si control trece la inceputul buclei "while". Astfel, corpul buclei se executa de
cate ori expresie se evalueaza la "true". Terminarea buclei are loc cand expresie ia valoarea
zero (adica "false"). In acest punct, controlul se paseaza catre "instructiune_urmatoare".
Exemplu:
expresie1;
while (expresie2)
{
instructiune;
expresie3;
}
instructiune_urmatoare;
Deci, se va evalua expresie1. De obicei, aceasta se foloseste pentru initializarea buclei. Apoi,
se evalueaza expresie2. Daca aceasta nu este zero ("true"), atunci se executa instructiune, se
evalueaza expresie3, si controlul buclei se "paseaza" la inceputul buclei (cu deosebirea ca nu
se mai evalueaza expresie1). De obicei, expresie2 este o expresie logica care controleaza
bucla. Acest proces continua pana cand expresie2 este 0 (false), punct in care se plaseaza
controlul catre instructiune_urmatoare.
factorial=1;
for (i = 1; i <= n; i++)
factorial *= i;
Orice sau toate expresiile dintr-o instructiune "for" pot lipsi, dar nu poate lipsi ;
i = 1;
suma = 0;
for ( ; i <= 10; ++i)
suma += i;
Acesta se poate scrie echivalent:
i = 1;
suma = 0;
for ( ; i <= 10; )
suma += i++;
Daca, in schimb, lipseste expresie2, atunci obtinem o bucla infinita.
Operatorul "," are cea mai mica prioritate dintre toti operatorii din C. Este un operator binar
ce are ca operanzi drept expresii si se asociaza de la stanga la dreapta. Intr-o expresie de
forma:
expresie1 , expresie2
se evalueaza mai intai expresie1, apoi expresie2. Expresia "," intoarce valoarea si
tipul operandului din dreapta.
a = 0, b = 1
intoarce valoarea 1 de tipul int.
Operatorul "," este deseori folosit in instructiunea "for".
Exemplu: Revenim asupra unui exemplu precedent (suma primelor N numere naturale)
suma += i cu ++i
Exemplu:
suma = i = 0;
do
{
suma += i;
scanf("%d", &i);
}
while (i > 0);
Instructiunea "goto" (salt neconditionat) este considerata opusa programarii structurate. Sfatul
general valabil este evitarea acestei instructiuni. Totusi, in unele cazuri se poate folosi (cand
simplifica controlul, cand face codul mai eficient). O instructiune de etichetare are
forma: eticheta : instructiune unde eticheta este un identificator.
Exemple:
bye: exit(1);
eticheta1: a = b + c;
Exemple:
while (1)
{
scanf("%lf", &x);
if (x < 0.0)
break; /* iesim cand x este negativ */
printf("%lf\n", sqrt(x));
}
while (contor < n)
{
scanf("%lf", &x);
if (x > -0.01 && x < =0.01)
continue; /* valorile mici nu se iau in considerare */
++contor;
suma += x;
}
Exemplu:
switch (val)
{
case 1:
++contor_a;
break;
case 2:
case 3:
++contor_b;
break;
default:
++contor_c;
}
Operatorul "?:" este mai putin obisnuit deoarece este ternar (cu trei argumente). Forma
generala este: expresie1 ? expresie2 : expresie3
Mai intai, se evalueaza expresie1. Daca aceasta este diferita de 0 (true), atunci se evalueaza
expresie2, si aceasta va fi valoarea returnata de intreaga expresie conditionala. Daca
expresie1 este 0 (false), atunci se evalueaza expresie3, si aceasta va fi valoarea intregii
expresii conditionale.
Exemplu: Instructiunea
if (y < z)
x = y;
else
x = z;
este echivalenta cu
x = (y < z) ? y : z;
Un program este compus din una sau mai multe functii, printre care si "main()". Intotdeauna
executia unui program incepe cu "main()". Cand o functie este apelata (sau invocata) atunci
controlul programului este pasat functiei apelate. Dupa ce aceasta isi termina executia, atunci
se paseaza inapoi controlul catre program.
Codul C care descrie ce face o functie se numeste "definitia functiei". Aceasta are urmatoarea
forma generala:
tip nume_functie (lista_parametri)
{
declaratii
instructiuni
}
Primul rand se numeste "header-ul" (antetul) functiei, iar ceea ce este inclus intre acolade se
numeste corpul functiei. Daca in antet nu precizam parametri, atunci se va scrie "void"
(cuvant rezervat pentru lista vida). Daca functia nu intoarce nici o valoare, atunci se va scrie
ca tip intors tot "void". Tipul intors de functie este cel precizat in "return" (ce va fi indata
explicat). Parametrii din antetul functiei sunt dati printr-o lista cu argumente separate prin
virgula. Aceste argumente sunt date de tipul argumentului urmat de un identificator ce
apartine acelui tip. Se mai spune ca acel identificator este "parametru formal".
Exemplu:
#include
void tipareste_mesaj(int k)
{
int i;
printf("Iti urez:\n");
for (i = 0; i < k; ++i)
printf(" O zi buna ! \n");
}
main()
{
int n;
printf("Dati un numar natural mic: ");
scanf("%d", &n);
tipareste_mesaj(n);
}
Instructiunea "return" este folosita pentru doua scopuri. Cand se executa o instructiune
"return", controlul programului este pasat inapoi programului apelant. In plus, daca exista o
expresie dupa acest "return", atunci se va returna valoarea acestei expresii. Instructiunea
"return" poate avea formele:
return;
sau
return expresie;
#include
int min(int x, int y)
{
if (x < y)
return x;
else
return y
}
main()
{
int j, k, m;
In C, apelul unei functii poate apare inaintea declararii ei. Functia poate fi definita mai tarziu
in acelasi fisier, sau in alt fisier sau dintr-o biblioteca standard. In ANSI C, prototipul functiei
remediaza problema punand la dispozitie numarul si tipul argumentelor functiei. Prototipul
specifica, de asemenea, si tipul returnat de functie. Sintaxa prototipului unei functii este:
tip nume_functie (lista_tipuri_parametri);
In lista de parametri putem specifica chiar si parametrul, dar asta este optional. Daca functia
nu are parametri, atunci se foloseste "void".
#include
main()
{
int n;
void tipareste_mesaj(int);
void tipareste_mesaj(k)
{
int i;
printf("Iti urez:\n");
for (i = 0; i < k; ++i)
printf(" O zi buna ! \n");
}
Prototipul unei functii poate fi plasat in corpul altei functii, sau de regula, se scriu la inceputul
programelor dupa directivele #include si #define.
Presupunem ca avem de citit cativa intregi si trebuie sa-i afisam in ordine pe coloane (in
capatul de sus al coloanelor trebuie sa scriem numele campului), sa le afisam suma lor
partiala, minimul si maximul lor. Pentru scrierea unui program C ce face acest lucru, vom
utiliza proiectarea (descrierea) "top-down".
Toti acesti trei pasi vor fi descrisi in cate o functie ce se apeleaza din "main()". Obtinem, un
prim cod:
#include
main()
{
void tipareste_antet(void);
void scrie_campurile(void);
void citeste_scrie_coloanele(void);
tipareste_antet();
scrie_campurile();
citeste_scrie_coloanele();
}
Aceasta reprezinta intr-un mod foarte simplu descrierea "top-down". Daca o problema este
prea grea, atunci o descompunem in subprobleme, si apoi le rezolvam pe acestea. Beneficiul
suplimentar al acestei metode este claritatea sa.
void tipareste_antet(void)
{
printf("\n%s%s%s\n",
"**************************************************\n",
"* Calculul sumelor, minimului si maximului *\n",
"**************************************************\n");
}
if (scanf("%d", &articol) == 1)
{
++contor;
suma = minim = maxim = articol;
printf("%5d%12d%12d%12d%12d\n\n",
contor, articol, suma, minim, maxim);
while (scanf("%d", &articol) == 1)
{
++contor;
suma += articol;
minim = min(articol, minim);
maxim = max(articol, maxim);
printf("%5d%12d%12d%12d%12d\n\n",
contor, articol, suma, minim, maxim);
}
}
else
printf("Nici o data nu a fost citita.\n\n");
}
Daca datele se introduc de la tastatura, atunci tabelul se va afisa "intrerupt" de citirile ce au
loc de la tastatura. Astfel, se prefera citirea dintr-un fisier extern. Presupunem ca fisierul
nostru executabil (asociat fisierului sursa scris in C) se numeste "numere.exe" si am creat un
fisier numit "fisier.int" ce contine urmatoarele numere:
19 23 -7 29 -11 17
Dand comanda numere < fisier.int vom obtine un tabel ce contine toate datele dorite.
O functie este invocata prin scrierea numelui sau impreuna cu lista sa de argumente intre
paranteze. De obicei, numarul si tipul acestor argumente se "potriveste" cu parametrii din lista
de parametri prezenti in definitia functiei. Toate argumentele sunt apelate prin valoare ("call-
by-value"). Asta inseamna ca fiecare argument este evaluat si valoarea sa este folosita ca
valoare pentru parametrul formal corespunzator. De aceea, daca o variabila (argument) este
folosita la transmiterea unei valori, atunci valoarea ei nu se schimba.
Exemplu:
#include
main()
{
int n=3, suma, calculeaza_suma(int);
Daca se folosesc in "main()", atunci acestea sunt echivalente, dar in orice alta functie efectul
lor este diferit (in ANSI C, functia "main()" intoarce o valoare de tip int). Un apel al lui exit()
in orice alta functie va implica terminarea executiei programului si returnarea valorii catre
mediul apelant (sistemul de operare sau mediul de programare C). Valoarea returnata se
numeste stare de iesire ("exit status"). Prin conventie, starea de iesire zero indica terminare cu
succes, pe cand iesire cu un numar diferit de zero indica o situatie anormala.
Capitolul 5
Procesarea caracterelor
Este unul dintre tipurile fundamentale din limbajul C. Constantele si variabilele de acest tip
sunt folosite pentru reprezentarea caracterelor. Fiecare caracter este memorat pe 1 byte
(octet), care (in general) este compus din 8 biti. Un octet compus din 8 biti poate pastra
2^8=256 valori distincte.
Cand memoram un caracter intr-un octet, continutul acestuia poate fi gandit ca un caracter sau
un intreg mic (intre 0 si 255). Desi putem memora 256 valori distincte, doar o parte din ele
sunt tiparibile (litere mici, mari, cifre, semne de punctuatie, spatiu, tab, caractere speciale +,
*, %). Exemple de caractere netiparibile: newline, bell.
O constanta caracter se scrie intre apostroafe, cum ar fi: 'a', 'b'. O declaratie obisnuita a unei
variabile de tip caracter este:
char c;
Variabilele caracter se pot initializa astfel:
char c1 = 'A', c2 = '*';
Un caracter este pastrat in memorie pe un octet dupa o codificare specifica. Multe masini
folosesc codurile de caractere ASCII sau EBCDIC. Ne vom referi numai la codul ASCII.
Astfel, vom preciza constanta caracter si valoarea corespunzatoare a sa:
- de la 2^5+2^4 pana la 57, in ordine: '0', '1', ..., '9'
- de la 2^6+2^0 pana la 90, in ordine: 'A', 'B', ..., 'Z'
- de la 2^6+2^5+2^0 pana la 112, in ordine: 'a', 'b', ..., 'z'
De exemplu, se observa ca pentru a obtine litere mici din cele mari, schimbam doar un bit.
Astfel, caracterul 'A' are codul 65 care inseamna numarul 01000001 in baza 2, iar caracterul
'a' are codul 01100001. Se observa ca difera doar bitul cu numarul 3.
Exemple:
1. printf("\"ABC\"");
2. printf("'ABC'");
Un alt mod de a scrie o constanta caracter este folosind una, doua sau trei cifre octale ca
secvente escape, cum ar fi '\007'. Acest '\007' este de fapt caracterul "alert" (sau clopotel). El
mai poate fi scris '\07' sau '\7' sau \a.
Aceste functii sunt folosite pentru citirea si scrierea caracterelor si sunt definite in . Astfel
pentru citirea unui caracter de la tastatura se foloseste "getchar()", iar pentru scrierea unui
caracter pe ecran "putchar()".
Bineinteles ca daca dorim sa afisam un sir de caractere mai mare, este mai elegant cu functia
"printf()".
Exemplu:
Urmatorul program citeste un caracter din intrare (tastatura) si il atribuie unei varibile de tip
char, apoi il afiseaza pe ecran.
#include
main()
{
char c;
while (1)
{
c=getchar();
putchar(c);
}
}
Singurul mod de a opri acest program este sa apasam CTRL^C. Putem reface acest program
folosind constanta EOF.
#include
main()
{
int c;
1. In biblioteca , exista o linie in care se declara #define EOF (-1). Denumirea lui EOF
provine de la "end-of-file".
2. Variabila c trebuie sa fie declarata de tip int si nu de tip char. Am vazut ca sfarsitul unui
fisier este codificat cu -1, si nu cu un caracter.
3. Subexpresia c=getchar() citeste o valoare de la tastatura si o asigneaza variabilei c.
5.3 Biblioteca
Sistemul C pune la dispozitie fisierul header care contine o multime de macro-uri (definitii)
folosite pentru testarea
caracterelor si o multime de prototipuri de functii ce sunt folosite pentru conversia
caracterelor.
In tabelul de mai jos prezentam o lista de macro-uri folosite la testarea caracterelor. Aceste
macro-uri iau ca argument o variabila de tip int si returneaza o valoare de tip int (zero=false,
diferit de zero=true).
In tabelul urmator, vom scrie functiile "toupper()" si "tolower()", care sunt din biblioteca
standard si macro-ul "toascii()". Macro-ul si prototipurile pentru cele doua functii sunt in .
Acestea au ca argument o variabila de tip int si returneaza tipul int.
- toupper(c) schimba c din litera mica in majuscula
- tolower(c) schimba c din majuscula in litera mica
- toascii(c) schimba c cu codul ASCII
Vrem sa numaram cate cuvinte sunt introduse de la tastatura. Ele sunt separate prin spatiu.
Pentru scrierea programului vom utiliza tot strategia "top-down".
#include
#include
main()
{
int numar_cuvinte = 0;
int gaseste_urmatorul_cuvant(void);
while (gaseste_urmatorul_cuvant() == 1)
++ numar_cuvinte;
printf("Numarul de cuvinte = %d\n\n", numar_cuvinte);
}
int gaseste_urmatorul_cuvant(void)
{
int c;
Capitolul 6
| 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
7 6 5 4 3 2 1 0
Fiecare celula reprezinta un bit si fiecare bit este numerotat (incepand cu cel mai putin
semnificativ). Bitii care formeaza un octet sunt fie "on", fie "off", aceste stari fiind
reprezentate prin 1 si 0 respectiv. Acesta ne conduce sa gandim fiecare octet din memorie ca
un sir de 8 cifre binare (se mai numesc siruri de biti).
Astfel variabila "c" poate fi gandita ca sirul de biti 01100001.
Mai general, fiecare cuvant masina poate fi gandit ca un sir de cifre binare grupate in octeti.
Un sir de cifre binare poate fi deci gandit ca un numar binar (adica in baza 2). Fara a intra in
detalii matematice (teorema bazei de numeratie) vom face doar un exemplu:
Exemplu:
Valoarea lui "c" este numarul 01100001 (in baza 2)= 1 x 2^6 + 1 x 2^5 + 0
x 2^4 + 0 x 2^3 + 0 x 2^2 + 0 x 2^1 + 1 x 2^0 care inseamna 64 + 32 +
1 = 97 in notatia zecimala (in baza 10). ANSI C pune la dispozitie trei tipuri referitoare la
caractere:
char signed char unsigned char.
De obicei, tipul "char" este echivalent cu "signed char" sau "unsigned char", depinzand de
compilator. Fiecare din aceste trei tipuri se memoreaza pe un octet (deci poate "tine" 256
valori distincte). Pentru "signed char", valorile sunt intre -128 si 127, iar pentru "unsigned
char" intre 0 si 255.
Exemple:
De obicei, tipul "short" se memoreaza pe doi octeti si tipul "long" pe patru octeti. Astfel, pe
masinile in care cuvintele au patru octeti, lungimea tipului "int" este aceeasi cu lungimea
tipului "long", iar pe masinile in care cuvintele au doi octeti, lungimea tipului "int" este egala
cu lungimea tipului "short". Constantele predefinite MAXSHORT si MAXLONG (in unele
implementari LONG_MAX) caracterizeaza lungimea acestor tipuri. De obicei,
MAXSHORT=2^{15} si MAXLONG=2^{31}. Astfel, daca "s" este o variabila de tip
"short", atunci - MAXSHORT <= s <= MAXSHORT-1.
Daca "l" este o variabila de tip "long", atunci - MAXLONG <= s <= MAXLONG-1.
In ceea ce priveste tipul "unsigned", acesta este memorat pe acelasi numar de octeti ca si tipul
"int". Daca "u" este o variabila de tip "unsigned", atunci 0 <= u <= 2*MAXINT-1
ANSI C contine trei tipuri reale: "float", "double" si "long double". Variabilele de acest tip
vor putea tine valori reale, cum ar fi: 0.001 2.0 3.14159.
Aceasta notatie se numeste notatie zecimala, deoarece contine punctul zecimal. Mai exista si
notatia exponentiala. De exemplu, 1.234567e5 corespunde cu 1.234567 x
10^5=123456.7
Pe majoritatea masinilor, tipul "float" se memoreaza pe 4 octeti, iar tipul "double" pe 8 octeti.
Asta inseamna ca o variabila de tipul "float" poate avea 6 zecimale, iar o variabila de tipul
"double" poate avea 15 zecimale. Astfel, o variabila de tipul "float" are forma:
0,d_1 d_2 d_3 d_4 d_5 d_6 x 10^{n} unde -38 <= n <= 38.
Asemanator, o variabila de tipul "double" are forma
0,d_1 d_2 ... d_{15} x 10^{n} unde -308 <= n <= 308.
Astfel, instructiunea
x = 123.45123451234512345; /* 20 cifre semnificative */
va implica atribuirea lui x a valorii
0.123451234512345 x 10^3 (15 cifre semnificative)
In ANSI C, pentru varibilele de tip "long double" se aloca mai multa memorie. Insa sunt
compilatoare care trateaza acest exact tip exact ca si "double".
#include
main()
{
printf("Lungimea catorva tipuri fundamentale.\n\n");
printf(" char:%3d octeti \n", sizeof(char));
printf(" short:%3d octeti \n", sizeof(short));
printf(" int:%3d octeti \n", sizeof(int));
printf(" long:%3d octeti \n", sizeof(long));
printf(" unsigned:%3d octeti \n", sizeof(unsigned));
printf(" float:%3d octeti \n", sizeof(float));
printf(" double:%3d octeti \n", sizeof(double));
printf("long double:%3d octeti \n", sizeof(long double));
}
Din moment ce limbajul C este flexibil in ceea ce priveste necesarul de memorie pentru
tipurile fundamentale, situatiile pot sa difere de la o masina la alta. Totusi, aceasta garanteaza
ca:
- sizeof(char) = 1
- sizeof(short) <= sizeof(int) <= sizeof(long)
- sizeof(signed) <= sizeof(unsigned) <= sizeof(int)
- sizeof(float) <= sizeof(double) <= sizeof(long double)
"sizeof()" nu este o functie (chiar daca contine paranteze atunci cand ne referim la tipuri), ci
este un operator. De Exemplu:
sizeof(a + b + 7.7) este echivalent cu sizeof a + b + 7.7
O expresie aritmetica, cum ar fi "x + y", are si valoare si tip. De exemplu, daca "x" si "y" au
tipul "int", atunci expresia "x + y" are tipul "int". Dar, daca "x" si "y" au ambele tipul "short",
atunci "x + y" este de tip "int", si nu "short". Aceasta se intampla deoarece in orice expresie,
"short" se converteste la "int".
Un "char" sau "short", ori "signed" sau "unsigned", ori un tip enumerare (vom reveni) poate fi
folosit in orice expresie unde poate fi folosit "int" sau "unsigned int". Daca toate valorile
tipului original pot fi reprezentate de un "int", atunci valoarea acesteia se va converti la "int";
altfel se va converti la "unsigned int". Aceasta se numeste "conversie la intreg".
Exemplu:
char c = 'A';
printf("%c\n", c);
Variabila "c" apare ca argument al functiei "printf()". Cu toate acestea, deoarece are loc
conversia la intreg, tipul expresiei "c" este "int", si nu "char".
Conversiile aritmetice pot apare cand sunt evaluati operanzii unui operator binar.
Exemplu:
Presupunem ca "i" este "int" si "f" este un "float". In expresia "i + f", operandul "i" se
converteste la "float" si deci expresia "i + f" va intoarce tipul "float".
Aceste reguli se numesc "conversii aritmetice uzuale". Iata urmatorul "algoritm":
- daca un operand este de tip "long double" atunci si celalalt operand va fi convertit la tipul
"long double" altfel daca un operand este de tip "double" atunci si celalalt operand va fi
convertit la tipul "double" altfel daca un operand este de tip "float" atunci si celalalt operand
va fi convertit la tipul "float" altfel
/***** au loc conversiile la intreg *****/
- daca un operand este de tip "unsigned long" atunci si celalalt operand va fi convertit la tipul
"unsigned long" altfel daca un operand are tipul "long" si celalalt "unsigned" atunci:
- daca un "long" poate reprezenta toate valorile "unsigned" atunci operandul de tip
"unsigned" se va converti la "long"
- daca un "long" nu poate reprezenta toate valorile "unsigned" atunci ambii operanzi se vor
converti la "unsigned long" altfel
- daca un operand are tipul "long" atunci celalalt operand se converteste la "long" altfel daca
un operator are tipul "unsigned" atunci celalalt operand va fi convertit la "unsigned" altfel
ambii operanzi vor avea tipul "int".
- char c;
- short s;
- int i;
- unsigned u;
- unsigned long ul;
- float f;
- double d;
- long double ld;
Atunci avem urmatoarele valori pentru tipurile expresiilor de mai jos:
Daca "i" este de tip "int", atunci (double) i va converti valoarea lui "i" astfel incat expresia sa
aiba tipul "double". Variabila "i" ramane neschimbata. Conversiile se pot aplica si expresiilor.
Exemple:
Exemplu:
Exemplu:
Capitolul 7
Exemplu:
Exemplu:
Exemplu:
Exemplu:
Exemplu: typedef int culoare; culoare rosu, verde, albastru. Acesta defineste tipul "culoare"
ca fiind un sinonim al lui "int". Apoi declaram trei variabile de tipul "culoare".
Exemplu:
Vom ilustra folosirea lui "typedef" pentru un tip enumerare (creand o functie ce returneaza
ziua urmatoare).
zi gaseste_ziua_urmatoare(zi z)
{
zi ziua_urmatoare;
switch(z)
{
case duminica:
ziua_urmatoare = luni;
break;
case luni:
ziua_urmatoare = marti;
break;
...
case sambata:
ziua_urmatoare = duminica;
break;
}
return ziua_urmatoare;
}
O alta versiune (mai succinta) folosind "cast" este:
zi gaseste_urmatoarea_zi(zi z)
{
return ((zi)(((int) z + 1) % 7));
}
Operatorii pe biti lucreaza cu expresii integrale reprezentate ca siruri de cifre binare. Acesti
operatori sunt dependenti de sistem.
Operatorii pe biti sunt:
Operatori logici
Operatori de deplasare
int a = 5171;
Reprezentarea binara a lui a este:
00010100 00110011
Expresia ~a este:
11101011 11001100
Aceasta are valoarea intreaga - 5172.
Reprezentarea complementului fata de doi a unui numar natural este un sir de biti obtinut prin
complementarierea scrierii lui n in baza 2. Considerand complementul pe biti al lui n la care
adunam 1, obtinem reprezentarea complementului fata de doi a lui -n.
O masina care utilizeaza reprezentarea complementului fata de doi ca reprezentare binara in
memorie pentru valori integrale se numeste masina complement fata de doi. Reprezentarile
complement fata de doi ale lui 0 si -1 sunt speciale. Astfel valoarea 0 are toti bitii "off", pe
cand valoarea -1 are toti bitii "on". Numerele negative sunt caracterizate prin aceea ca au bitul
cel mai semnificativ 1. Pe masinile care utilizeaza complementul fata de doi, hard-ul permite
implementarea scaderii ca o adunare si un complement pe biti. Operatia a - b este aceeasi cu a
+ (-b), unde -b se obtine considerand complementul pe biti al lui b la care adunam 1.
Cei trei operatori & (si), ^ (sau exclusiv) si | (sau inclusiv) sunt binari si au operanzi integrali.
Operanzii sunt operati bit cu bit. Tabelul de mai jos contine semantica lor:
a b a&b a^b a|b
0 0 0 0 0
1 0 0 1 1
0 1 0 1 1
1 1 1 0 1
Cei doi operanzi ai unui operator de deplasare trebuie sa fie expresii integrale. Tipul returnat
de expresie este dat de operandul din stanga. O expresie de forma expresie1 <<
expresie2 implica reprezentarea pe bit a lui "expresie1" sa fie deplasata catre stanga cu un
numar de pozitii specificat de "expresie2". In capatul din dreapta, vor fi adaugate 0-uri.
Chiar daca valoarea lui "c" se memoreaza pe un octet, intr-o expresie aceasta ia tipul "int".
Deci valoarea expresiilor "c << 1", "c << 4" si "c << 15" se memoreaza pe doi octeti.
Operatorul de deplasare la dreapta ">>" nu este chiar simetric cu "<<". Pentru expresiile
integrale fara semn, din stanga se va deplasa 0, iar pentru cele cu semn 1.
Exemplu:
Ca valoare intreaga, a >> 3 este -4096, iar b >> 3 este 4096, lucru care este in concordanta cu
notiunea de numar negativ si pozitiv din matematica.
Daca operandul din dreapta a operatorului de deplasare este negativ sau are o valoare care
este egala sau depaseste numarul de biti folositi pentru reprezentarea operandului din stanga,
atunci comportarea este nedefinita.
Tabelul de mai jos ilustreaza regulile de precedenta (+ este mai prioritar) si asociativitate (de
la stanga la dreapta) referitoare la operatorii de deplasare.
7.8 Masti
O "masca" este o constanta sau variabila folosita pentru extragerea bitilor doriti dintr-o alta
variabila sau expresie. Din moment ce constanta "int" 1 are reprezentarea pe biti:
00000000 00000001
aceasta poate fi folosita pentru determinarea bitului cel mai nesemnificativ dintr-o expresie
"int". Codul de mai jos foloseste aceasta masca si tipareste o secventa alternativa de 0 si 1:
int i, masca = 1;
for (i = 0; i < 10; ++ i)
printf("%d", i & masca);
Daca dorim sa gasim valoarea unui anume bit dintr-o expresie, putem folosi o masca care este
1 in acea pozitie si 0 in rest.
Exemplu:
Putem folosi expresia 1 << 2 pentru a vedea al treilea bit (numarand de la dreapta). Expresia
(v & (1 << 2)) ? 1 : 0 are valoarea 1 sau 0 dupa cum este al treilea bit din "v".
Alt exemplu de masca este valoarea constanta 255 (adica 2^8 -1). Ea are reprezentarea
00000000 11111111
Deoarece doar octetul din dreapta este plasat pe "on", atunci expresia v & 255 va intoarce o
valoare ce are reprezentare pe biti cu toti bitii din octetul din stanga "off" si cel din dreapta
identic cu octetul din dreapta a lui "v". Spunem ca 255 este o masca pentru octetul din
dreapta.
#include
#include
#include
void bit_print(int a)
{
int i;
int n = sizeof(int) * CHAR_BIT;
int masca = 1 << (n-1);
masca <<= n;
return((p & masca) >> n);
}
void main()
{
clrscr();
bit_print(65);
printf("\nab = ");
bit_print(pack('a', 'b'));
putchar('\n');
getch();
printf("Numarul 24930 este impachetarea caracterelor %c si
%c",
unpack(24930,1), unpack(24930,0));
getch();
}
Reluam un exemplu dintr-un capitol precedent si anume transformarea literelor mari in mici
folosind operatori de deplasare.
#include
#include
#include
void main()
{
clrscr();
int c;
while ((c = getchar()) != EOF)
{
if (isupper(c)) // sau (c>='A' && c<='Z')
putchar(c | (1 << 5));
else
putchar(c);
}
}
Capitolul 8
Va amintiti ca daca o expresie este transmisa ca argument pentru o functie, atunci se creeaza
o copie a valorii expresiei care se transmite. Acest mecanism este cunoscut sub numele de
apel prin valoare ("call-by-value") si se foloseste in limbajul C. Presupunem ca avem o
variabila v si o functie f(). Daca scriem v = f(v); atunci valoarea returnata de functia f va
schimba valoarea lui v, altfel nu. In interiorul functiei f, nu se modifica valoarea lui v.
Aceasta se datoreaza faptului ca se transmite doar o copie a lui v catre f. In alte limbaje de
programare, un apel de functie poate schimba valoarea lui v din memorie. Acest mecanism se
mai numeste apel prin referinta ("call-by-reference"). Noi vom simula apelul prin referinta
transmitand adresele variabilelor ca argumente in apelul functiei.
Pointerii sunt folositi in programe pentru accesarea memoriei si manipularea adreselor. Deja
ne-am intalnit cu adresele variabilelor ca argumente ale functiei "scanf()". De exemplu, putem
avea:
scanf("%d\n", &n);
Daca v este o variabila, atunci &v este o adresa (sau locatie) din memorie. Operatorul de
adresa & este unar si are aceeasi precedenta si asociativitate de la dreapta la stanga ca si
ceilalti operatori unari. Variabilele pointer pot fi declarate in programe si apoi folosite pentru
a lua valori adrese din memorie.
Exemplu: Declaratia
int i, *p;
defineste i de tip "int" si p "pointer catre int". Domeniul legal de valori pentru orice pointer
cuprinde adresa speciala 0 si o multime de numere naturale care sunt interpretate ca fiind
adrese masina ale sistemului C. De obicei, constanta simbolica NULL este 0 (definita in ).
Exemple:
Am vazut ca operatorul de adresa & se aplica unei variabile si intoarce valoarea adresei sale
din memorie. Operatorul de indirectare (sau de dereferentiere) se aplica unui pointer si
returneaza valoarea scrisa in memorie la adresa data de pointer. Intr-un anumit sens, acesti
doi operatori sunt inversi unul altuia. Pentru a intelege mai bine aceste notiuni, sa vedem pe
un exemplu ce se intampla in memorie:
Mentionam ca adresa unei variabile este dependenta de sistem (C aloca memorie acolo unde
poate).
Exemplu:
float x, y, *p;
p = &x;
y = *p;
Mai intai "p" se asigneaza cu adresa lui "x". Apoi, "y" se asigneaza cu valoarea unui obiect la
care pointeaza p (adica *p). Aceste doua instructiuni de asignare se pot scrie:
y = *&x;
care este echivalent cu
y = x;
Am vazut mai sus ca un pointer se poate initializa in timpul declararii sale. Trebuie sa avem
totusi grija ca variabilele din membrul drept sa fie deja declarate.
Spre deosebire de C traditional, in ANSI C, singura valoare intreaga care poate fi asignata
unui pointer este 0 (sau constanta NULL). Pentru asignarea oricarei alte valori, trebuie facuta
o conversie explicita (cast).
In cele ce urmeaza, vom scrie un program care ilustreaza legatura dintre valoarea unui pointer
si adresa lui.
Exemplu:
#include
#include
void main()
{
int i = 777, *p = &i;
clrscr();
Exemplu:
#include
void main()
{
int a = 3, b = 7;
tmp = *p;
*p = *q;
*q = tmp;
}
Efectul apelului prin adresa este realizat prin:
1. Declararea parametrului functiei ca fiind un pointer;
2. Folosirea unui pointer de indirectare in corpul functiei;
3. Transmiterea adresei unui argument cand functia este apelata.
Domeniul unui identificator este partea din textul unui program unde identificatorul este
cunoscut sau accesibil. Aceasta idee depinde de notiunea de "bloc", care este o instructiune
compusa cu declaratii.
Regula de baza in stabilirea domeniului este aceea ca identificatorii sunt accesibili numai in
blocul unde sunt declarati si necunoscuti in afara granitelor blocului. Unii programatori
folosesc acelasi nume de identificatori prezenti in anumite blocuri.
Exemplu:
{
int a = 2;
printf("%d\n", a);
{
int a = 7;
printf("%d\n", a);
}
printf("%d\n", ++a);
}
Un program echivalent ar fi:
{
int a_afara = 2;
printf("%d\n", a_afara);
{
int a_inauntru = 7;
printf("%d\n", a_inauntru);
}
printf("%d\n", ++a_afara);
}
Variabilele declarate in interiorul functiilor sunt implicit automate. De aceea, clasa "auto" este
cea mai cunoscuta dintre toate. Daca o instructiune compusa (bloc) incepe cu declararea unor
variabile, atunci aceste variabile sunt in domeniu in timpul acestei instructiuni compuse (pana
la intalnirea semnului }).
Exemplu:
auto int a, b, c;
auto float f;
Declaratiile variabilelor in blocuri sunt implicit automate.
La executie, cand se intra intr-un bloc, se aloca memorie pentru variabilele automate.
Variabilele sunt considerate locale acestui bloc. Cand se iese din acest bloc, sistemul
elibereaza zona de memorie ocupata de acestea si deci valorile acestor variabile se pierd.
Daca intram din nou in acest bloc, atunci se aloca din nou memorie pentru aceste variabile,
dar vechile valori sunt necunoscute.
Exemplu:
#include
int a = 1, b = 2, c = 3;
int f(void);
void main()
{
printf("%3d\n", f());
printf("%3d%3d%3d\n", a, b, c);
}
int f(void)
{
int b, c; /* b si c sunt locale, deci b, c globale sunt
mascate */
Deci, cuvantul rezervat "extern" spune compilatorului "cauta peste tot, chiar si in alte
fisiere !". Astfel, programul precedent se poate rescrie:
in fisierul "fisier1.c":
#include
void main()
{
printf("%3d\n", f());
printf("%3d%3d%3d\n", a, b, c);
}
in fisierul "fisier2.c":
int f(void)
{
extern int a; /* cauta-l peste tot */
int b, c;
Deci, putem conchide ca informatiile se pot transmite prin variabile globale (declarate cu
extern) sau folosind transmiterea parametrilor. De obicei se prefera al doilea procedeu.
Toate functiile au clasa de memorare externa. De exemplu,
extern double sin(double);
este un prototip de functie valid pentru functia "sin()", iar pentru definitia functiei, putem
scrie:
extern double sin(double x)
{
..
}
Exemplu:
{
register int i;
for (i = 0; i < LIMIT; ++i)
{
. . . . .
}
} /* la iesirea din bloc, se va elibera registrul i */
Declaratia
register i;
este echivalenta cu
register int i;
Daca lipseste tipul variabilei declarata intr-o clasa de memorare de tip "register", atunci tipul
se considera implicit "int".
Exemplu:
void f(void)
{
static int contor = 0;
++contor;
if (contor % 2 == 0)
. . . . .
else
. . . . .
}
Prima data cand functia este apelata, "contor" se initializeaza cu 0. Cand se paraseste functia,
valoarea lui "contor" se pastreaza in memorie. Cand se va apela din nou functia "f()", "contor"
nu se va mai initializa, ba mai mult, va avea valoarea care s-a pastrat in memorie la
precedentul apel. Declararea lui "contor" ca un "static int" in functia "f()" il pastreaza privat
in "f()" (adica numai aici i se poate modifica valoarea). Daca ar fi fost declarat in afara acestei
functii, atunci si alte il puteau accesa.
Ne vom referi acum la folosirea lui "static" ca declaratie externa. Aceasta pune la dispozitie
un mecanism de "izolare" foarte important pentru modularitatea programelor. Prin "izolare"
intelegem vizibilitatea sau restrictiile de domeniu.
Deosebirea dintre variabile externe si cele externe static este ca acestea din urma sunt
variabile externe cu restrictii de domeniu. Domeniul este fisierul sursa in care ele sunt
declarate. Astfel, acestea sunt inaccesibile pentru functiile definite anterior in fisier sau
definite in alte fisiere, chiar daca functiile folosesc clasa de memorare "extern".
Exemplu:
void f(void)
{
. . . . . /* v nu este accesibil aici */
}
void g(void)
{
. . . . . /* v poate fi folosit aici */
}
Exemplu:
double probability(void)
{
seed = (MULTIPLIER * seed + INCREMENT) % MODULUS;
return (seed / FLOATING_MODULUS);
}
Functia "random()" produce o secventa aleatoare (aparenta) de numere intregi situate intre 0
si MODULUS. Functia "probability()" produce o secventa aleatoare (aparenta) de valori reale
intre 0 si 1.
Observam ca un apel al functiei "random()" sau "probability()" produce o noua valoare a
variabilei "seed" care depinde de cea veche. Din moment ce "seed" este o variabila externa
statica, aceasta este locala acestui fisier si valoarea sa se pastreaza de la un apel la altul.
Putem acum crea functii in alte fisiere care apeleaza aceste numere aleatoare fara sa avem
grija efectelor laterale.
Prezentam, in continuare, un ultim exemplu de utilizare a lui "static" ca specificator de clasa
de memorare pentru functii. Functiile declarate "static" sunt vizibile doar in fisierul unde au
fost declarate.
Exemplu:
void f(int a)
{
. . . . . /* g() este disponibil aici, dar nu si in alte
fisiere */
}
In C, variabilele externe si statice care nu sunt explicit initializate de catre programator, sunt
initializate de catre sistem cu 0. Aceasta include siruri, siruri de caractere, pointeri, structuri si
inregistrari (union). Pentru siruri (de caractere), aceasta inseamna ca fiecare element se
initializeaza cu 0, iar pentru structuri si "union" fiecare membru se initializeaza tot cu 0. In
contrast cu aceasta, variabilele "registru" si "auto" nu se initializeaza de catre sistem, ci
pornesc cu valori "garbage" (adica cu ce se gaseste la momentul executiei la acea adresa).
Exemplu: Procesarea caracterelor
O functie care utilizeaza "return" poate returna o singura valoare. Daca dorim sa trasmitem
mai multe valori pentru mediul apelant, atunci trebuie sa transmitem adresele unor variabile.
Vrem sa procesam un sir de caractere (in stilul "top-down") astfel:
- citeste caractere de la intrare pana cand avem EOF;
- schimba litere mici in litere mari;
- scrie pe fiecare linie trei cuvinte separate de un singur spatiu;
- numara caracterele si literele de la intrare.
#include
#include
#define NR_CUVINTE 3
int procesare(int *, int *, int *);
void main()
{
int c, numar_caractere = 0, numar_litere = 0;
Comitetul ANSI a adaugat cuvintele rezervate "const" si "volatile" pentru limbajul C (acestea
nu sunt disponibile in limbajul C traditional). De obicei, "const" este plasat intre clasa de
memorare si tipul variabilei.
Exemplu:
Citim aceasta "k este o constanta de tip int cu clasa de memorare static". Deoarece "k" are
tipul "const", atunci putem initializa "k", dar nu mai poate fi reasignat (incrementat sau
decrementat). Chiar daca variabila este calificata ca fiind "const", aceasta nu se poate folosi
pentru precizarea lungimii unui sir.
Exemplu:
const int n = 3;
int v[n]; /* gresit */
Deci o variabila calificata "const" nu este echivalenta cu o constanta simbolica.
Un pointer necalificat nu poate fi asignat cu adresa unei variabile calificata "const".
Exemplu:
const int a = 7;
int *p = &a; /* gresit */
Motivul este ca "p" este un pointer obisnuit catre "int" si l-am putea folosi mai tarziu in
expresii de genul "++*p". Totusi, utilizand pointeri, putem schimba valoarea lui a (ceea ce
contravine conceptului de constanta).
Exemplu:
const int a = 7;
const int *p = &a;
Nu vom putea modifica valoarea lui "a", utilizand "*p". Pointerul "p" nu este constant (putem
face p++).
Presupunem ca vrem ca "p" sa fie constant, si nu "a". Consideram declaratiile:
int a;
int * const p = &a;
Ultima declaratie spune ca "p este un pointer constant catre int, si valoarea sa initiala este
adresa lui a". Apoi, nu mai putem asigna o valoare lui p, dar putem da valori lui "*p".
Consideram acum un exemplu si mai interesant:
Exemplu:
const int a = 7;
const int * const p = &a;
Ultima declaratie spune ca p este un pointer constant catre o constanta intreaga. Nici "p", nici
"*p", nu mai pot fi reasignate. In contrast cu "const", calificatorul "volatile" este rar folosit.
Un obiect "volatile" este unul ce poate fi modificat intr-un mod nespecificat de catre hard.
Capitolul 9
Siruri si pointeri
Un sir (se mai spune si vector) este o secventa de date ce contine articole de acelasi tip,
indexate si memorate contiguu. De obicei, sirurile se folosesc pentru reprezentarea unui
numar mare de valori omogene (in capitolul urmator vom studia sirurile de caractere). O
declaratie obisnuita de sir aloca memorie incepand de la adresa de baza. Numele sirului este
un pointer constant la aceasta adresa de baza.
O alta notiune pe care o vom explica este transmiterea sirurilor ca argumente in functii.
Exemplu:
Exemplu:
Vom scrie un mic program care initializeaza un sir, tipareste valorile sale si insumeaza
elementele sirului.
#include
#define N 5
void main()
{
int a[N]; /* aloca spatiu de memorie pentru a[0], a[1], a[2],
a[3] si a[4] */
int i, suma = 0;
Deci vectorul (sirul) "a" se va memora incepand de la adresa 3A38:0FF6. Deci "a = &a[0]".
Se recomanda definirea lungimii unui sir ca o constanta simbolica (folosind directiva
"#define").
Sirurile pot apartine claselor de memorare "auto", "extern", "static" sau "constant", dar nu pot
fi "register". Ca si variabilele simple, sirurile pot fi initializate in timpul declararii lor.
Initializarea sirurilor se face folosind acolade si virgule.
Exemplu:
Exemplu:
Declaratiile
int a[] = {3, 4, 5, 6}; si int a[4] = {3, 4, 5, 6};
sunt echivalente.
Exemplu:
#include
#include
void main()
{
int c, i, litera[26];
Am vazut ca numele unui sir (de exemplu "a") este o adresa, deci poate fi privit ca valoare a
unui pointer. Deci sirurile si pointerii pot fi priviti oarecum la fel in ceea ce priveste modul
cum sunt folositi pentru accesarea memoriei. Cu toate acestea, sunt cateva diferente (subtile si
importante). Cum numele unui sir este o adresa fixa (particulara), atunci aceasta o putem
gandi ca un pointer constant. Cand este declarat un sir, compilatorul trebuie sa aloce o adresa
de baza si un spatiu suficient de memorie care trebuie sa contina toate elementele sirului.
Adresa de baza a unui sir este locatia initiala din memorie unde sirul este memorat; aceasta
coincide cu adresa primului element (de index 0) al sirului.
#define N 100
int a[N], *p;
Atunci sistemul va rezerva octetii (sa zicem) numerotati 300, 302, ..., 498 ca fiind adresele
elementelor a[0], a[1], ..., a[99]
Instructiunile
p = a; si p = &a[0];
sunt echivalente si vor asigna lui "p" valoarea 300 (ca adresa de memorie). Aritmetica
pointerilor pune la dispozitie o alternativa pentru indexarea sirurilor.
Instructiunile p = a + 1; si p = &a[1]; sunt echivalente si va asigna lui "p" valoarea 302
(adresa, bineinteles).
Exemplu: Presupunem ca avem un sir ale carui elemente au deja valori. Pentru a face suma
elementelor, putem folosi pointeri.
suma = 0;
for (p = a; p < &a[N]; ++p)
suma += *p;
Pointerii aritmetici reprezinta una din trasaturile puternice ale limbajului C. Daca variabila
"p" este pointer catre un tip particular, atunci expresia "p + 1" reprezinta adresa masina pentru
memorarea sau accesarea urmatoarei variabile de acest tip. In mod similar, expresiile p + i
++p p += i au sens. Daca "p" si "q" sunt pointeri catre elemente de tip vector, atunci "p -
q" intoarce valoarea "int" si reprezinta numarul de elemente dintre "p" si "q". Chiar daca
expresiile pointer si expresiile aritmetice seamana, exista diferente mari intre cele doua tipuri
de expresii.
Exemplu:
void main()
{
double a[2], *p, *q;
p = &a[0]; /* pointeaza catre baza sirului */
q = p + 1; /* echivalent cu q = &a[1]; */
printf("%d\n", q - p); /* se va tipari 1 */
printf("%d\n", (int) q - (int) p)); /* se va tipari 8 */
}
Intr-o definitie de functie, un parametru formal care este declarat ca un sir este de fapt un
pointer. Cand este trimis un sir, atunci se trimite de fapt adresa de baza (evident prin "call-by-
value"). Elementele vectorului nu sunt copiate. Ca o conventie de notatie, compilatorul
permite folosirea parantezelor patrate ([,]) in declararea pointerilor ca parametri.
Exemplu:
Exemplu:
Algoritmii eficienti de sortare au, de obicei, O(n*log n) operatii. Metoda sortarii cu bule este
ineficienta din acest punct de vedere deoarece are O(n^2) operatii. Totusi, pentru siruri de
lungime mica, numarul de operatii este acceptabil. Un cod "elegant" ar fi:
void interschimba(int *, int *);
Limbajul C permite siruri de orice tip, inclusiv siruri de siruri. Putem obtine siruri de
dimensiune 2, 3, ... .
Exemple:
Exemplu:
Presupunem ca sunt date elementele vectorului "a". Functia de mai jos se poate folosi pentru
suma elementelor unui sir. Atentie ! Trebuie specificat numarul de coloane.
int suma(int a[][5])
{
int i, j, suma = 0;
Vectorii de dimensiune mai mare decat 3 lucreaza intr-un mod similar. Daca avem
declaratia int a[7][9][2];
atunci compilatorul va aloca spatiu pentru 7*9*2 intregi. Adresa de baza a sirului este "&a[0]
[0][0]", iar functia de corespondenta in memorie este specificata de a[i][j][k] care este
echivalent cu *(&a[0][0][0] + 9*2*i + 2*j + k)
int a[2][2][3] = {
{{1, 1, 0}, {2, 0, 0}},
{{3, 0, 0}, {4, 4, 0}}
};
O initializare echivalenta poate fi data si astfel:
int a[][2][3] = {{{1, 1}, {2}}, {{3}, {4, 4}}};
De obicei, daca un sir declarat "auto" nu este explicit initializat, atunci elementele sirului vor
contine valori "garbage".
Sirurile "static" si "external" sunt initializate implicit cu 0. Iata un mod simplu de a initializa
toate valorile unui vector cu 0:
int a[2][2][3] = {0};
C pune la dispozitie pentru alocarea memoriei functiile "calloc()" si "malloc()" din biblioteca
standard. Aceste functii au prototipul declarat in . Acest lucru va permite rezervarea memoriei
pentru un vector (de exemplu) in care ii aflam dimensiunea abia la rularea in executie (pana
acum declararam dimensiunea unui vector cu #define). Un apel de tipul calloc(n,
dimensiune_tip) va returna un pointer catre un spatiu din memorie necesar pentru memorarea
a "n" obiecte, fiecare pe "dimensiune_tip" octeti. Daca sistemul nu poate aloca spatiul cerut,
atunci acesta va returna valoarea NULL.
In ANSI C, tipul "size_t" este dat ca "typedef" in . De obicei, tipul este "unsigned". Definitia
tipului este folosita in prototipurile functiilor "calloc()" si "malloc()":
void *calloc(size_t, size_t);
void *malloc(size_t);
Deoarece pointerul returnat de aceste functii are tipul "void", acesta poate fi asignat altor
pointeri fara conversie explicita (cast). Totusi unele sisteme nu accepta aceasta conversie,
deci ea trebuie facuta explicit. Octetii rezervati de "calloc()" sunt automat initializati cu 0, pe
cand cei rezervati cu "malloc()" nu sunt initializati (deci vor avea valori "garbage"). Numele
"calloc", respectiv "malloc", provine de la "contiguous allocation", respectiv "memory
allocation".
#include
#include
void main()
{
int *a, i, n, suma = 0;
ptr=&a[0][0];
printf("\n\nIntroduceti elementele matricii:\n");
Capitolul 10
Prin conventie, un sir de caractere se termina prin marcatorul (santinela, delimitator) \0, sau
caracterul nul. De exemplu, sirul "abc" este memorat pe 4 caractere, ultimul fiind \0. Deci
numarul de elemente al sirului este 3, iar dimensiunea 4.
Exemplu:
Exemplu: Utilizarea sirurilor de caractere (ca vectori). Citim o linie de caractere dintr-un sir,
le tiparim in ordine inversa si adunam literele din sir.
#include
#include
#define MAXSTRING 100
main()
{
char c, name[MAXSTRING];
int i, sum = 0;
Vom discuta despre folosirea pointerilor pentru procesarea unui sir si cum se pot folosi
acestea pentru a fi transmise ca parametri unei functii. Vom scrie un exemplu de program
interactiv care citeste intr-un sir o linie de caractere introdusa de utilizator. Programul va crea
un nou sir si-l va tipari.
Exemplu:
#include
#define MAXLINE 100
void main()
{
char linie[MAXLINE], *schimba(char *);
void citeste_in(char *);
printf("\nDati un sir:");
citeste_in(linie);
printf("\n%s\n\n%s\n\n",
"Asa arata sirul dupa schimbare:", schimba(linie));
}
*p++ = '\t';
for ( ; *s != '\0'; ++s)
if (*s == 'e')
*p++ = 'E';
else
if (*s == ' ')
{
*p++ = '\n';
*p++ = '\t';
}
else
*p++ = *s;
*p = '\0';
return sir_nou;
}
Deoarece numele "sir_nou" este tratat ca un pointer catre adresa de baza a sirului. Fiind
declarat "static", acesta se pastreaza in memorie si dupa ce se iese din functia "schimba()".
Acest lucru nu s-ar fi intamplat si daca, de exemplu, sirul ar fi fost declarat "auto".
#include
C pune la dispozitie siruri de orice tip, inclusiv siruri de pointeri. Pentru scrierea de programe
care folosesc argumente in linia de comanda, trebuie sa folosim siruri de pointeri catre
caractere. Pentru aceasta, functia "main()" foloseste doua argumente, numite generic "argc" si
"argv".
Exemplu:
#include
Biblioteca standard contine multe functii utile pentru lucrul cu siruri de caractere. Sirurile ce
sunt argumente trebuie terminate cu '\0' si toate returneaza un intreg sau o valoare a unui
pointer catre "char".
Cateva functii utile pentru lucrul cu siruri de caractere
Aceste functii sunt scrise in C si sunt foarte scurte. Variabilele din ele sunt de obicei declarate
"register" pentru a face executia mai rapida.
1. char s[14];
strcpy(s, "Ce mai faci ?\n");
2. char s[14];
scanf("%s", &s);
Capitolul 11
Exemple:
Exemplu:
#define EQ ==
Exemplu:
#define do /* spatiu */
De exemplu, acum putem simula instructiunea "while" din C ca un "while do" din Pascal sau
Algol.
De exemplu, daca avem definitiile de sintaxa "dulce" de mai sus, putem spune ca
instructiunile
while (i EQ 1) do
{
. . . . .
}
si
while (i == 1)
{
. . . . .
}
sunt echivalente.
Exemplu:
Macrourile sunt folosite de obicei pentru a inlocui apelurile functiilor cu cod liniar (scurte si
fara variabile suplimentare).
Exemple:
C pune la dispozitie facilitatea "typedef" pentru a asocia (redenumi) un tip cu unul specific.
Declaratia de mai sus face tipul "uppercase" sinonim cu "char". De exemplu, declaratiile de
mai jos sunt valide:
uppercase c, u[100];
Fisierul header contine cateva definitii de tip:
typedef int ptrdiff_t; /* tip intors de diferenta pointerilor */
typedef short wchar_t; /* tip caracter mare */
typedef unsigned size_t; /* tipul sizeof */
Tipul "ptrdiff_t" spune care este tipul returnat de o expresie implicata in diferenta a doi
pointeri. In MS-DOS, acesta depinde de modelul de memorie ales (tiny, short, large, far,
huge), pe cand in UNIX, tipul folosit este "int".
Tipul "wchar_t" se foloseste pentru acele caractere care nu se pot reprezenta pe un octet (char
-> int).
Reamintim ca operatorul "sizeof" este folosit pentru determinarea lungimii unui tip sau a unei
expresii. De exemplu, "sizeof(double) = 8". Tipul "size_t" este returnat de operatorul
"sizeof".
Un macrou definit in este
#define NULL 0
Exemplu:
Vom relua problema de mai sus, dar vom folosi macrouri cu argumente. Vom scrie programul
in doua fisiere, un fisier header "sort.h" si un fisier "sort.c". Fisierul header va contine
directive de precompilare (#include, #define), precum si prototipuri pentru functiile noastre.
Fisierul "sort.h" este:
#include
#include
#include
#include
#define M 32
#define N 11
#define parte_fractionara(x) (x - (int) x)
#define caracter_aleator() (rand() % 26 + 'a')
#define real_aleator() (rand() % 100 / 10.0)
#define INIT(array, sz, type) \
if (strcmp(type, "char") == 0) \
for (i = 0; i < sz; ++i) \
array[i] = caracter_aleator(); \
else \
for (i = 0; i < sz; ++i) \
array[i] = real_aleator();
Acum, vom scrie restul codului pentru programul nostru, si anume fisierul "sort.c".
#include "sort.h"
void main()
{
char a[M];
float b[N];
int i;
srand(time(NULL));
INIT(a, M, "char");
PRINT(a, M, "%-2c");
qsort(a, M, sizeof(char), lexico);
PRINT(a, M, "%-2c");
printf("---\n");
INIT(b, N, "float");
PRINT(b, N, "%-6.1f");
qsort(b, N, sizeof(float), compara_partea_fractionara);
PRINT(b, N, "%-6.1f");
}
Preprocesorul are directive pentru compilare conditionata. Acestea pot fi folosite pentru
dezvoltarea programelor si pentru scrierea codului mai portabil de la o masina la alta. Fiecare
directiva de forma
#if expresie_integrala_constanta
#ifdef identificator
#ifndef identificator implica compilarea conditionata a codului care urmeaza pana la
directiva de precompilare#endif
Pentru compilarea codului de mai sus, in cazul lui #if trebuie ca expresia constanta sa fie
diferita de zero (true), in cazul lui
#ifdef sau #ifdefined numele identificatorului trebuie sa fie definit anterior intr-o linie
#define, fara interventia directivei
#undef identificator
In cazul lui #ifndef, numele identificatorului trebuie sa nu fie curent definit.
Expresia constanta integrala folosita intr-o directiva de precompilare nu poate contine
operatorul "sizeof" sau un cast. Poate
insa, folosi operatorul de precompilare "defined" (valabil in ANSI C, dar nu si C traditional).
Expresia defined identificator
este echivalenta cu defined(identificator).
Acesta se evalueaza la 1 daca identificatorul este definit, si 0 in caz contrar.
Exemplu:
11.11 Operatorii # si ##
#define X(i) x ## i
X(1) = X(2) = X(3);
va deveni dupa preprocesare
x1 = x2 = x3;
ANSI C pune la dispozitie macroul "assert()" din biblioteca standard "assert.h". Acest macrou
poate fi folosit cand vrem sa ne asiguram ca o expresie are o anumita valoare. Vrem sa scriem
o functie ale carei argumente satisfaca niste conditii.
Exemplu:
#include
void f(char *p, int n)
{
. . . . .
assert(P != NULL);
assert(n > 0 && n < 5);
. . . . .
}
Daca vreo asertiune esueaza, atunci sistemul va tipari un mesaj si va opri executia
programului. Iata o implementare posibila a lui "assert()".
#if defined(NDEBUG)
#define assert(ignore) ((void) 0) /* ignorare */
#else
#define assert(expr)
if (!(expr)) \
{ \
printf("\n%s%s\n%s%s\n%s%d\n\n, \
"Assertion failed: ", #expr, \
"in file ", __FILE__, \
"al line ", __LINE__); \
}
#endif
De remarcat ca daca NDEBUG este definit, atunci sunt ignorate toate asertiunile. Aceasta
permite programatorului in timpul scrierii programului sa verifice pas cu pas executia
programului. Functia "abort()" se gaseste in biblioteca standard.
11.13 Folosirea lui #error si #pragma
Exemplu:
Capitolul 12
Recursie
O functie este recursiva daca se autoapeleaza, direct sau indirect. In C toate functiile se pot
defini recursiv.
Exemplu:
#include
void numara(int n);
void main()
{
numara(10);
}
void numara(int n)
{
if (n)
{
printf("%d ! ", n);
numara(n - 1);
}
else
printf("Gata !\n");
}
int suma(int n)
{
if (n <= 1)
return n;
else
return (n + suma(n - 1));
}
De obicei, functiile recursive urmeaza un "pattern" standard:
- exista un caz de baza (sau mai multe);
- caz recursiv general (in care, in general, un intreg este trimis ca argument al apelului
recursiv);
Recursia este un procedeu foarte puternic de rezolvare a problemelor. Secretul este
identificarea cazului general.
Pentru exemplul precedent, cand se trimite n catre functia "suma()", recursia activeaza n copii
ale functiei inaintea intoarcerii pas cu pas catre primul apel recursiv (se mai spune ca in
momentul apelului recursiv, variabilele locale "ingheata", ele "dezghetandu-se" la intoarcerea
din recursie). Multe functii recursive se pot scrie intr-o forma iterativa (folosind structuri de
tip "while", se mai spune "derecursivare"). Recursia se recomanda cand problema se poate
rezolva foarte usor folosind recursie si cand nu se cere o eficienta sporita in timpul executiei
programului. Uneori, se recomanda recursia finala (adica dupa apelul recursiv nu mai sunt
alte instructiuni si nu exista variabile locale).
Exemplu: Citeste o linie si o afiseaza in ordine inversa, apoi lasa doua randuri goale.
#include
void tipareste(void);
void main()
{
printf("Introduceti o linie: ");
tipareste();
printf("\n\n");
}
void tipareste(void)
{
char c;
if ((c = getchar()) != '\n')
tipareste();
putchar(c);
}
Iata o rulare in executie:
Introduceti o linie: iepurasu usa rupei
iepur asu usarupei
Observati in exemplul precedent ca la fiecare apel recursiv, se memoreaza in stiva caracterul
"c" legat la o valoare, care se va afisa la intoarcerea din recursie. Deci practic, sunt "n" copii
ale lui "c", unde "n" reprezinta lungimea liniei.
Exemplu:
Putem complica putin exemplul precedent, in sensul ca afisam aceleasi cuvinte, dar in ordine
inversa.
#include
#include
void tipareste_cuvinte(void);
void citeste_cuvant(char *);
void main()
{
printf("Introduceti o linie: ");
tipareste_cuvinte();
printf("\n\n");
}
void tipareste_cuvinte(void)
{
char w[MAXWORD];
citeste_cuvant(w);
if (w[0] != '\n')
tipareste_cuvant();
printf("%s ", w);
}
if (c == '\n')
*s++ = c;
else
while (!isspace(c = getchar()))
*s++ = c;
*s = '\0';
}
Daca, la executie, utilizatorul scrie:
Introduceti o linie: noi invatam C
atunci pe ecran, va apare:
C invatam noi
Variabila "c" avand clasa de memorare "static", rezulta ca valoarea ei se pastreaza de la un
apel la altul. De altfel, initializarea lui "c" se face o singura data (cand se intra prima data in
aceasta functie). Daca "c" ar fi fost de tip "auto", atunci chiar daca aveam la sfarsitul sirului
'\n', la urmatorul apel, acesta nu ar fi fost cunoscut, deci practic nu mai aveam conditie de
oprire.
Exemplu:
void main()
{
display(SYMBOL, OFFSET, LENGTH);
}
* * * * * * * * * * * * * * * * * * *
* * * * * * * * * * * * * * *
* * * * * * * * * * *
* * * * * * *
* * *
Un sir consta dintr-un numar de caractere consecutive, terminate prin caracterul '\0'. De fapt,
putem gandi un sir ca fiind sirul nul (care consta doar din caracterul '\0') sau un caracter urmat
de un sir. Aceasta definitie a sirului este o structura de date recursiva.
Eleganta acestei formulari recursive este "platita" de o pierdere in timpul executiei. Daca sirul
are lungimea k, calcularea lungimii sale necesita k + 1 apeluri recursive (un compilator
optimizat poate evita aceasta pierdere).
12.2 Metodologia "divide-et-impera"
if (n == 2)
if (a[0] < a[1])
{
*min_ptr = a[0];
*max_ptr = a[1];
}
else
{
*min_ptr = a[1];
*max_ptr = a[0];
}
else
{
minmax(a, n/2, &min1, &max1);
minmax(a + n/2, n/2, &min2, &max2);
if (min1 < min2)
*min_ptr = min1;
else
*min_ptr = min2;
if (max1 < max2)
*max_ptr = max2;
else
*max_ptr = max1;
}
}
Capitolul 13
Intrari/iesiri si fisiere
In acest capitol vom explica folosirea unor functii de intrare/iesire (printre care si "printf()" si
"scanf()"). De asemenea vom arata modurile de deschidere a fisierelor (pentru procesarea lor)
si cum se foloseste un pointer catre fisier.
Are doua proprietati importante care permit o folosire flexibila la un nivel inalt:
- poate fi tiparita o lista de argumente de lungime arbitrara;
- afisarea este controlata de specificari de conversie simple (sau formate).
Functia "printf()" primeste sirul de caractere din fisierul standard de iesire (stdout), care este
normal conectat la ecran. Lista de argumente a lui "printf()" are doua parti:
sirul_de_control si celelalte_argumente
Caracter
Cum sunt afisate de argumentele corespunzatoare ?
conversie
c ca un caracter
d, i ca un intreg zecimal
u ca un intreg zecimal fara semn
o ca un intreg octal fara semn
x, X ca un intreg hexazecimal fara semn
e ca un numar in virgula mobila; (ex: 7.123000e+00)
E ca un numar in virgula mobila; (ex: 7.123000E+00)
f ca un numar in virgula mobila; (ex: 7.123000)
g in formatul cel mai scurt dintre "e" sau "f"
G in formatul cel mai scurt dintre "E" sau "f"
s ca un sir
argumentul corespunzator este un pointer catre void; valoarea sa se va tipari ca
p
un intreg hexazecimal
argumentul corespunzator este un pointer catre un intreg; argumentul nu este
n
convertit
cu formatul %% se va afisa un singur % catre sirul de iesire; nu avem argumente
%
ce trebuie convertite
Exemplu: Iata un exemplu de folosire a formatului "%n":
#include
void main()
{
int * pi;
printf("Mai multe caractere %n.\n", pi);
printf("Nr.caractere = %d", *pi);
}
Pe ecran se va afisa numarul de caractere afisate pana la aparitia formatului "%n" (in cadrul
instructiunii de afisare "printf()" curente), adica 20. Functia "printf()" intoarce un "int" ce
reprezinta numarul de caractere tiparite dupa inlocuirea corespunzatoare a specificatorilor de
conversie.
Exemplu:
#include
void main()
{
int * pi;
int a = printf("Mai mult de %d caractere %n.\n", 10, pi);
printf("Numarul de caractere intors de functia printf() este
%d\n", a);
printf("Nr.caractere = %d", *pi);
}
In specificarile de conversie pot fi incluse informatii de formatare explicite. Daca ele nu apar,
atunci sunt folosite cele implicite. De exemplu, formatul %f cu argumentul 3.77 va afisa
3.770000. Numarul este afisat pe 6 caractere la dreapta punctului zecimal (implicit). Intre %
(care specifica inceputul unei specificari de conversie) si caracterul de conversie de la sfarsit,
pot apare in ordine:
- zero sau mai multe caractere (flag) care modifica intelesul specificarii de conversie;
- un intreg pozitiv optional care specifica minimul lungimii campului a argumentului
convertit. Locul unde un argument este tiparit se numeste "campul sau", iar numarul de spatii
folosit pentru afisarea sa se numeste "lungimea campului". Daca argumentul convertit are mai
putine caractere decat lungimea campului specificat, atunci acesta se va completa cu spatii (la
stanga sau dreapta). Daca argumentul convertit are mai multe caractere decat lungimea
campului specificat, atunci lungimea campului se va mari cu cat este necesar. Daca intregul
care defineste lungimea campului incepe cu zero si argumentul ce se tipareste este ajustat
dreapta in campul sau, atunci pentru umplerea sa se vor folosi zerouri in loc de spatii;
- o "precizie" optionala, care este specificata de un punct urmat de un intreg
nenegativ. Pentru conversii d, i, o, u, x si X aceasta specifica numarul minim de cifre ce
trebuie afisate. Pentru conversii e, E si f aceasta specifica numarul de cifre de la dreapta
punctului zecimal. Pentru conversii g si G aceasta specifica numarul maxim de cifre
semnificative. Pentru conversie cu s, aceasta specifica numarul maxim de caractere ce trebuie
tiparite dintr-un sir;
- un h sau l optional, care este un modificator "short" sau "long", respectiv. Daca h este
urmat de un d, i, o, u, x sau X, atunci aceasta este o specificare de conversie relativ la "short
int" sau "unsigned short int".Daca un h este urmat de n, atunci argumentul corespunzator este
un pointer catre "short int" sau "unsigned short int". Daca l este urmat de d, i, o, u, x sau X,
atunci specificarea de conversie se aplica unui argument "long int" sau "unsigned long
int". Daca l este urmat de caracterul n, atunci argumentul corespunzator este un pointer catre
"long int" sau "unsigned long int";
- L optional, care este un modificator "lung". Daca L este urmat de e, E, f, g sau G,
specificarea de conversie se aplica unui argument "long double".
Intr-un format, lungimea campului sau precizia (sau ambele), pot fi specificate folosind *, in
loc de un intreg, lucru care indica ca o valoare se obtine dintr-o lista de argumente.
Exemplu:
int m, n;
double x = 333.7777777;
. . . . . . . . . . .
/* citeste m si n (sau le calculam cumva) */
. . . . . . . . . . .
printf("x = %*.*f\n", m, n, x);
Daca argumentul corespunzator lungimii campului are lungime negativa, atunci se considera
"-" ca fiind un "flag" urmat de o valoare pozitiva. Daca argumentul corespunzator preciziei
are valoare negativa, atunci acesta se considera ca lipseste.
Tabelul de mai jos contine formate de caractere si siruri (folosim ghilimele pentru delimitarea
vizuala a campului, ele nefiind afisate).
Declaratii si initializari
Are doua proprietati importante care permit o folosire flexibila la un nivel inalt:
- poate citi o lista de argumente de lungime arbitrara;
- intrarea este controlata de specificari de conversie simple (sau formate).
Functia "scanf()" citeste caractere din fisierul standard "stdin". Lista de argumente a functiei
"scanf()" are doua parti:
sir_de_control si celelalte_argumente
char a, b, c, s[100];
int n;
double x;
scanf("%c%c%c%d%s%lf", &a, &b, &c, &n, s, &x);
Avem:
sir_de_control: "%c%c%c%d%s%lf"
celelalte_argumente: &a, &b, &c, &n, s, &x
Celelalte argumente urmate de un sir de control consta dintr-o lista separata prin virgule de
expresii pointer sau adrese (reamintim ca "s" este insusi o adresa).
Sirul de control din "scanf()" este compus din trei tipuri de "directive":
- caractere ordinare
- spatii goale
- specificari de conversie
Caractere ordinare
Caracterele din sirul de control (diferite de spatiile goale si caracterele din specificarile de
conversie) sunt numite "caractere ordinare". Caracterele ordinare trebuie sa se regaseasca
(potriveasca) cu cele din sirul de la intrare.
Exemplu:
float suma;
scanf("$%f", &suma);
Caracterul $ este ordinar. Deci trebuie sa intalnim $ in sirul de la intrare. Daca are loc o
potrivire cu succes, atunci spatiile goale (daca exista) se vor sari, si caracterele care urmeaza
se vor potrivi la o valoare (in virgula mobila). Valoarea convertita va fi plasata in memorie la
adresa variabilei "suma".
Caracterele spatii goale din sirul de control care nu fac parte dintr-o specificare de conversie
se potrivesc cu orice spatiu liber din sirul de intrare.
Exemplu:
Numerele in virgula mobila din sirul de intrare sunt formatate cu un semn optional (+ sau -)
urmat de un sir de cifre cu un punct zecimal optional, urmat de parte exponentiala optionala.
Partea exponentiala consta din e sau E, urmate de un semn optional (+ sau -), urmat de un sir
de cifre.
Exemple:
77
+7.7e1
770.0E-1
+0.003
O specificare de conversie de forma %[sir] indica ca un sir special poate fi citit. Multimea de
caractere dintre parantezele patrate se numeste "multime de scanare". Daca primul caracter
din multimea de scanare nu este caracterul circumflex "^", atunci sirul trebuie sa fie construit
numai din caractere ce apartin multimii de scanare.
Exemple:
1. Formatul %[abc] va citi orice sir care contine literele "a", "b" si "c" si se va opri daca orice
alt caracter va apare in sirul de intraare, inclusiv un spatiu (ex. scanf("%[abc]", m)).
2. In contrast, formatul %[^abc] va citi orice ce se va termina cu "a", "b" sau "c", dar nu si
spatiu.
3. Fie codul
char m[30];
scanf("%29[AB \t\n]", m);
Aceasta va produce citirea in vectorul de caractere "m" a unui sir de cel mult 29 caractere.
Sirul consta din literele A, B,
spatiu, tab, newline. La sfarsit, se va scrie \0.
4. Programatorii de obicei gandesc o linie ca un sir de caractere, inclusiv spatii si taburi, care
se termina cu un newline.
Un mod (elegant) de a citi o linie in memorie este folosirea unei multimi de scanare potrivita:
char linie[256];
while (scanf(" %[^\n]", linie) == 1)
printf("%s\n", linie);
Cand "scanf()" este apelata, poate apare o greseala la citire. De exemplu, daca nu sunt
caractere in sirul de intrare, atunci "scanf()" va intoarce -1 (EOF). Daca apare o nepotrivire
intre formatele din "scanf()" si sirul de la intrare, atunci "scanf()" va intoarce numarul de
conversii cu succes pana in acel moment. Numarul este zero daca nu apar conversii. Daca
"scanf()" reuseste cu succes, atunci este returnat numarul de conversii cu succes. La fel, acest
numar poate fi zero.
Exemplu:
Exemplu: Tabelul de mai jos contine mai multe exemple de directive de control pentru
functia "scanf()":
Continutul
Directive in Tipul argumentului
sirului de Observatii
sirul de control corespunzator
intrare
ab%2c char * abacus ab se potriveste, ac se converteste
%3hd short * -7733 -77 se converteste
%41i long * +0x66 +0x6 se converteste
-%2u unsigned * -123 - se potriveste, 12 se converteste
+ %lu unsigned long * +-123 + se potriveste, -123 se converteste
+ %lu unsigned long * + -123 + se potriveste, -123 se converteste
+ se potriveste, eroare, (- nu se
+ %lu unsigned long * +- 123
converteste)
%3e float * +7e-2 +7e se converteste
%4f float * 7e+22 7e+22 se converteste
%51f double * -1.2345 -1.23 se converteste
%4Lf long double * 12345 1234 se converteste
dependent poate citi ceea ce
%p void * *
printf(),de sistem cu %p scrie la iesire
Functiile "sprintf()" si "sscanf()" sunt versiuni ce folosesc siruri ale functiei "printf()" si
"scanf()", respectiv. Prototipurile lor,
care se gasesc in "stdio.h", sunt:
int sprintf(char *s, const char *format, ...);
int sscanf(const char *s, const char *format, ...);
Punctele ... indica compilatorului faptul ca functia poate avea un numar variabil de
argumente. O instructiune de forma:
sprintf(sir, sir_de_control, alte_argumente);
scrie rezultatul in sirul de caractere "sir". Intr-o maniera similara, o instructiune de forma:
sscanf(sir, sir_de_control, alte_argumente);
citeste rezultatul din sirul de caractere "sir".
Exemplu:
Exemplu:
#include
void main()
{
int suma = 0, val;
FILE *ifp, *ofp;
ifp = fopen("fis_in", "r"); /* deschis pentru citire */
ofp = fopen("fis_out", "w"); /* deschis pentru scriere */
. . . . . .
}
In acest exemplu, am deschis fisierul "fis_in" pentru citire si "fis_out" pentru scriere. Din
momentul deschiderii fisierului, pointerul catre fisier poate fi folosit exclusiv pentru referirea
la intregul fisier. Daca, de exemplu, presupunem ca fisierul "fis_in" are numere intregi, atunci
iata o modalitate de a face suma lor:
while (fscanf(ifp, "%d", &val) == 1)
sum += val;
fprintf(ofp, "Suma lor este %d.\n", suma);
Ca si "scanf()", functia "fscanf()" intoarce numarul de conversii cu succes. Dupa ce terminam
de exploatat fisierele, putem sa le inchidem:
fclose(ifp);
fclose(ofp);
Un apel de functie de forma "fopen(nume_fisier, mod)" deschide fisierul respectiv intr-un
mod particular si returneaza un pointer catre fisier. Sunt mai multe posibilitati pentru modul
de accesare a fisierului:
Fiecare dintre aceste moduri se poate termina cu "a+". Asta inseamna ca fisierul poate fi
deschis si pentru citire si pentru scriere.
Deschiderea pentru citire a unui fisier care nu exista, sau care nu poate fi citit, va esua si
functia "fopen()" va intoarce pointerul NULL. Deschiderea unui fisier pentru scriere va avea
ca efect crearea unui fisier (daca acesta nu exista) sau se va suprascrie peste unul existent.
Deschiderea unui fisier pentru adaugare va avea ca efect crearea unui fisier (daca acesta nu
exista) sau se va scrie la sfarsitul sau (daca acesta exista).
Daca scriem modul "r+" sau "w+" atunci fisierul se considera ca a fost deschis pentru citire si
scriere. Cu toate acestea citirile nu pot fi urmate de scrieri decat daca s-a ajuns la EOF sau au
intervenit apeluri ale functiilor "fseek()", "fsetpos()" sau "rewind()". De asemeni, scrierile nu
pot fi urmate de citiri decat daca s-a ajuns la EOF sau au intervenit apeluri ale functiilor
"fflush()", "fseek()", "fsetpos()" sau "rewind()".
In plus fata de accesarea unui caracter unul dupa altul intr-un fisier (acces secvential), noi
putem accesa caractere in locuri diferite (acces aleator). In biblioteca C, functiile "fseek()" si
"ftell()" sunt folosite pentru accesarea aleatoare a unui fisier. O expresie de forma:
ftell(pointer_catre_fisier)
returneaza valoarea curenta a indicatorului de pozitie in fisier. Valoarea reprezinta numarul de
octeti pornind de la inceputul fisierului, numarand de la 0. Cand un caracter este citit dintr-un
fisier, sistemul incrementeaza indicatorul de pozitie cu 1. Tehnic vorbind, indicatorul de
pozitie in fisier este un membru a structurii catre care pointeaza "pointer_catre_fisier".
Pointerul catre fisier nu pointeaza catre caractere individuale din fisier (aceasta este o eroare
de conceptie pe care o fac programatorii incepatori).
Functia "fseek()" are trei argumente:
- pointer catre fisier
- offset (intreg)
- un intreg care arata locul fata de care se calculeaza offset-ul
O instructiune de forma
fseek(pointer_catre_fisier, offset, mod);
seteaza indicatorul de pozitie la o valoare care reprezinta "offset" octeti pornind de la "mod".
Valoarea lui "mod" poate fi 0, 1 sau 2, insemnand ca ne referim la inceputul fisierului, pozitia
curenta sau sfarsitul fisierului, respectiv.
Atentie ! Functiile "fseek()" si "ftell()" sunt garantate sa lucreze numai pentru fisiere
binare. In MS-DOS, daca dorim sa lucram cu aceste functii, trebuie sa deschidem acest
fisier in acces binar. In UNIX, din moment ce exista doar un singur mecanism de lucru
cu fisierele, orice mod de deschidere pentru fisier este bun.
Un stil bun de programare va verifica daca functia "fopen()" lucreaza asa cum ne asteptam (in
orice program serios, acest lucru trebuie facut). Iata cum ar trebui sa se faca deschiderea
fisierului "fis1" in acces de citire:
if ((ifp = fopen("fis1", "r")) == NULL)
{
printf("\nNu putem deschide fisierul fis1. Pa!\n\n");
exit(1);
}
Capitolul 14
Instrumente soft
Exista doua tipuri de instrumente soft:
- facilitati generale puse la dispozitie de catre sistemul de operare;
- facilitati specifice desemnate explicit pentru a ajuta programatorul.
Din moment ce comenzile sistemului de operare pot fi executate dintr-un program C, atunci
programatorul poate folosi aceste comenzi ca instrumente de soft pentru indeplinirea
anumitor sarcini. Cateva instrumente sunt disponibile intr-un sistem de operare, dar nu si in
altul. De exemplu, "make" exista in UNIX, iar in MS-DOS este o trasatura ce se poate instala.
Instrumentele de soft variaza cu timpul. Sistemele de depanare par a fi cele mai disponibile.
Compilatorul C insusi poate fi considerat un instrument soft.
In acest capitol vom discuta cum executam o comanda din sistemul de operare dintr-un
program C. Apoi vom discuta cateva instrumente soft importante, cum ar fi:
- compilatorul C
- "make"
- "touch"
- "grep"
- instrumente de depanare
Exemplu: Atat in MS-DOS, cat si in UNIX, exista comanda "date". Daca intr-un program C,
scriem comanda
system("date");
atunci va fi tiparita la ecran data curenta a sistemului.
Sirul trimis ca argument al functiei "system()" este tratat ca o comanda a sistemului de
operare. Cand se executa instructiunea, controlul este trimis catre sistemul de operare, se
executa comanda si apoi controlul este trimis inapoi catre program.
Exemplu: In UNIX, "vi" este o comanda folosita pentru editare. Presupunem ca suntem intr-
un program si vrem sa editam un
fisier al carui nume se citteste de la tastatura. Putem scrie:
char comanda[MAXSTRING];
sprintf(comanda, "vi %s", argv[1]);
printf("Dam comanda vi, deschizand fisierul %s\n", argv[1]);
system(comanda);
Un exemplu similar poate functiona si in MS-DOS inlocuind "vi" cu alt editor de texte.
Exemplu: Consideram ca vrem sa dam comanda MS-DOS "dir" si dorim afisarea doar cu
litere mici. Atunci putem scrie programul:
#include
#include
#include
#define MAXSTRING 100
void main()
{
char comanda[MAXSTRING], *nume_fis_temp;
int c;
FILE *ifp;
nume_fis_temp = tmpnam(NULL);
sprintf(comanda, "dir > %s", nume_fis_temp);
system(comanda);
ifp = fopen(nume_fis_temp, "r");
while ((c = getc(ifp)) != EOF)
putchar(tolower(c));
remove(nume_fis_temp);
}
O varianta ceva mai "protejata" este:
#include
#include
#include
#define MAXSTRING 100
void main()
{
char comanda[MAXSTRING], *nume_fis_temp;
int c;
FILE *ifp;
nume_fis_temp = tmpnam(NULL);
sprintf(comanda, "dir > %s", nume_fis_temp);
if (system("dir *.*") == 0)
{
system(comanda);
if ((ifp = fopen(nume_fis_temp, "r")) != NULL)
while ((c = getc(ifp)) != EOF)
putchar(tolower(c));
remove(nume_fis_temp);
}
}
Atentie ! Se creeaza intai executabilul si apoi se ruleaza dupa ce s-a iesit din
compilatorul C.
- folosim functia "tmpnam()" pentru creearea unui nume de fisier temporar (de obicei
"tmp1.$$$"); daca exista deja fisierul
"tmp1.$$$" in directorul curent, atunci se creeaza fisierul "tmp2.$$$", s.a.m.d.);
- apelam functia "sistem()" pentru redirectarea iesirii comenzii "dir" in acel fisier temporar;
- apoi tiparim continutul fisierului la ecran schimband literele mari in mici;
- in final, stergem din memorie fisierul temporar folosind functia "remove()".
14.2 Variabile de mediu
Variabilele de mediu sunt disponibile atat in UNIX, cat si in MS-DOS. Afisarea lor la ecran
se poate face cu urmatorul program:
#include
Exemplu:
printf("%s%s\n%s%s\n%s%s\n%s%s\n%s%s\n",
"Nume utilizator: ", getenv("USER"),
"Nume login: ", getenv("LOGNAME"),
"Shell: ", getenv("SHELL"),
"Env: ", getenv("ENV"),
"Director Home: ", getenv("HOME"));
In UNIX, anumite variabile de mediu, cum ar fi LOGNAME, SHELL, HOME, sunt puse la
dispozitie de catre sistem (adica sunt nemodificabile). Pentru a initializa altele, scriem
setenv NAME "Abcd efgh"
in fisierul nostru ".login".
14.3 Compilatorul C
In cele ce urmeaza, vom preciza modul de apel si optiunile acestora in UNIX. Multe dintre
ele sunt valabile si in MS-DOS.
Daca avem un program complet intr-un singur fisier, sa zicem "pgm.c", atunci comanda:
cc pgm.c va traduce codul C din "pgm.c" in cod obiect executabil si-l va scrie in fisierul
"a.out" (In MS-DOS, fisierul executabil se numeste "pgm.exe"). Comanda "a.out" executa
programul. Consideram acum comanda:
cc -o pgm pgm.c
Aceasta cauzeaza scrierea codului executabil direct in fisierul "pgm", suprascriind-ul in cazul
in care acesta exista deja (In MS-DOS optiunea similara este -e). Comanda "cc" lucreaza de
fapt in trei faze:
- apelul preprocesorului
- apelul compilatorului
- apelul incarcatorului (editorului de legaturi)
Rolul incarcatorului este de a pune (lega) impreuna bucatile furnizate de compilator pentru a
face fisierul executabil final. Optiunea -c se foloseste numai pentru compilare (pentru apelul
preprocesorului si compilatorului), nu si a incarcatorului. Aceasta optiune este utila daca
avem un program scris in mai multe fisiere. Consideram comanda
cc -c main.c fis1.c fis2.c
Daca nu sunt erori, fisierele obiect corespunzatoare vor fi create si vor avea extensia ".o" (In
MS-DOS, ele au extensia ".obj"). Pentru creearea unui fisier executabil, putem compila
anumite fisiere cu extensia ".c" si ".o" (combinate). Presupunem ca avem o eroare in
"main.c". Dupa corectarea ei, putem da comanda:
cc -o pgm main.c fis1.o fis2.o
Folosirea fisierului cu extensia ".o" in locul celui cu extensia ".c" reduce timpul de compilare.
In plus fata de extensia ".c" si ".o", putem folosi fisiere cu extesia ".s" care sunt create de
asamblor sau de compilator cu optiunea "-S" (cand folosim biblioteci create de arhivator).
Bibliotecile, de obicei, au extensia ".a" (In MS-DOS ele au extensia ".lib").
In UNIX, daca folosim optiunea "-p" pentru compilator, atunci se produce cod suplimentar,
care poate lua locul in fisiere obiect sau fisiere executabile produse de compilator. Cand
programul este apelat, codul suplimentar produce informatii care pot fi folosite pentru
generarea "profilului" unei executii. Informatiile pentru "profile" sunt scrise automat in
fisierul "mon.out". Acest fisier nu poate fi citit de utilizatori. Pentru a obtine informatiile din
"mon.out", programatorul trebuie sa dea comanda prof pgm unde "pgm" este numele
programului.
Multe sisteme de operare pun la dispozitie functii pentru folosirea ceasului intern. Accesul la
ceasul masinii este posibil in ANSI C printr-un numar de functii a caror prototipuri sunt
descrise in . Fisierul header contine de asemenea un numar de alte constructii, printre care si
definitiile lui "clock_t" si "time_t". De obicei, aceste definitii de tipuri sunt date prin:
typedef long clock_t;
typedef long time_t;
si aceste tipuri sunt folosite in prototipurile functiilor. Iata trei functii utile pentru
cronometrarea timpului:
clock_t clock(void);
time_t time(time_t *p);
double difftime(time_t time1, time_t time0);
Cand un program este executat, sistemul de operare tine minte timpul procesorului ce este
folosit. Cand este apelata functia "clock()", valoarea returnata de sistem este cea mai buna
aproximare a timpului folosit de program pana in acel punct. Unitatile (de masura) ceasului
pot varia de la o masina la alta. Macro-ul
#define CLOCKS_PER_SEC 60 /* dependent de masina */
este pus la dispozitie in header-ul . Acesta poate fi folosit pentru conversia valorii returnate de
"clock()" catre secunde.
Functia "time()" intoarce numarul de secunde care au trecut de la 1 ianuarie 1970 (sunt
posibile si alte unitati, aceasta fiind una din ele). O folosire uzuala a acestei functii este:
srand(time(NULL));
Apelul se refera la generatorul de numere aleatoare. Daca trimitem doua valori produse de
"time()" catre functia "difftime()", atunci va fi returnata diferenta exprimata in secunde de tip
"double".
Atat pentru programator, cat si pentru masina, este ineficient si costisitor sa pastram un
program C mare intr-un singur fisier care necesita compilari repetate. O strategie mult mai
buna este scrierea programului in mai multe fisiere cu extensia ".c" si compilarea lor separata.
Utilitarul "make" poate fi folosit pentru a pastra "urmele" fisierelor sursa si de a produce
acces usor la biblioteci si la fisierele header asociate. Aceasta facilitate este prezenta in
UNIX, iar in MS-DOS este o proprietate ce se poate instala.
Utilitarul "touch" este disponibil intotdeauna in UNIX si uneori disponibila sub MS-DOS (de
obicei, este disponibila acolo unde este instalat "make"). Utilitarul "touch" este folosit pentru
a actualiza data unui fisier. Acesta este util cand folosim "make" cand se
compara timpurile fisierelor care trebuie compilate.
touch aaa.h
atunci fisierul "aaa.h" are data cea mai recenta decat toate fisierele ".h", ".c", ".o". Acum,
dand comanda "make" toate fisierele cu extensia ".c" vor fi recompilate si fisierele obiect
linkeditate pentru a crea noul fisier executabil.
Sistemul de operare pune la dispozitie multe instrumente soft pentru programatori. Iata o lista
cu cateva instrumente soft ce se gasesc in UNIX (unele chiar si in MS-DOS):
Comanda Observatii
cb Folosit pentru transformarea codului C in "pretty print"
diff Tipareste liniile care difera in doua fisiere
grep Cauta un "pattern" intr-unul sau mai multe fisiere
indent Alt "pretty printer" cu mai multe optiuni
Numara liniiile, cuvintele si caracterele dintr-un fisier (sau mai
wc
multe)
Utilitarul "cb" citeste din "stdin" si scrie in "stdout". Utilitarul "indent" este mai puternic.
Poate fi gasit pe versiunile UNIX
Berkeley si Sun.
Utilitarele "diff", "grep" si "wc" pot fi folosite de oricine, nu numai de programatori. Cu toate
ca sunt utilitare UNIX, ele sunt de obicei disponibile si in MS-DOS (in special "grep", foarte
folositor programatorilor).
In final, sa mentionam ca C poate fi folosit in conjunctie si cu alte instrumente de nivel inalt
(unele dintre ele limbaje "adevarate"):
Utilitar Observatii
awk Limbaj de procesare si scanare a pattern-urilor
csh Acest "shell" (ca si "sh", "ksh") este programabil
lex Genereaza cod C pentru analiza lexicala
sed Editor de texte care preia comenzile sale dintr-un fisier
yacc "Yet another compiler-compiler", folosit la generarea de cod C
O importanta deosebita o au "lex" si "yacc" (cu versiunile "pclex" si "pcyacc" pentru MS-
DOS). Versiuni mai recente, cum ar fi, "flex" sau "bison", sunt disponibile de la Free
Software Foundation, Inc. Ele lucreaza atat sub UNIX, cat si sub MS-DOS.