Toate programele netriviale sint alcatuite din diferite unitati compilate separat
(conventional, numite fisiere). Acest capitol descrie cum se compileaza functiile
separat, cum se pot apela una pe alta, cum functiile compilate separat pot utiliza
date in comun si cum tipurile utilizate in diferite fisiere ale programului
pot fi tinute consistent (necontradictoriu).Functiile se discuta in anumite
detalii; aceasta include transferul de argumente, argumente implicite, nume de functii care se supraincarca,
pointeri spre functii si desigur, declaratii si definitii de functii.In final
sint prezentate macrourile.
4.1. Introducere
A avea un program complet intr-un fisier este de obicei imposibil deoarece
codul pentru bibliotecile standard si de sistem sint in alta parte. Mai mult
decit atit, avind fiecare utilizator codul sau intr-un singur fisier este ceva
care este atit impractic cit si inconvenient. Modul in care este organizat un
program in fisiere poate ajuta cititorul sa inteleaga structura unui program
si sa permita compilatorului sa impuna acea structura. Intrucit unitatea de
compilare este un fisier, tot fisierul trebuie sa fie recompilat ori de cite
ori s-a facut in el o schimbare. p8r7rx
Pentru un program dimensionat chiar moderat, timpul petrecut pentru recompilare
poate fi redus semnificativ partitionind programul in fisiere dimensionate potrivit.
Sa consideram exemplul cu calculatorul de birou. A fost prezentat ca un singur
fisier sursa. Daca il tastam, noi fara indoiala avem niste probleme minore in
obtinerea declaratiilor in ordine corecta si cel putin o declaratie trebuie
utilizata pentru a permite compilatorului sa trateze functiile mutual recursive
expr(), term() si prim(). Textul amintit are patru parti (analizor lexical,
analizor sintactic, tabela de simboluri si un driver), dar aceasta nu se reflecta
in nici un fel in cod. In realitate calculatorul nu a fost scris in acest fel.
Acesta nu este modul de a o face; chiar daca toate consideratiile metodologiei
de programare, mentinere si eficienta compilarii au fost deconsiderate pentru
acest program, autorul totusi va partitiona acest program de 200 de linii in
mai multe fisiere pur si simplu pentru a face sarcina programarii mai placuta.
Un program care consta din mai multe parti compilate separat trebuie sa fie
consistent (necontradictoriu) in utilizarea numelor si tipurilor in exact acelasi
mod ca si un program care consta dintr-un singur fisier sursa. In principiu,
aceasta se poate asigura prin linker. Linkerul este programul care leaga partile
compilate separat. Un linker uneori este numit (gresit) incarcator; linkerul
UNIX-ului se numeste ld. Cu toate acestea linkerul disponibil pe majoritatea
sistemelor este prevazut cu putine facilitati care sa verifice consistenta modulelor
compilate separat.
Programatorul poate compensa lipsa acestor facilitati ale linkerului furnizind
informatii de tip suplimentare (declaratii). Un program poate fi realizat consistent
asigurind ca declaratiile prezentate in compilari separate sa fie consistente.
C++ a fost definit ca un instrument care sa incurajeze astfel de compilari cu
declaratii explicite si este prevazut un linker care sa verifice consistenta
modulelor respective. Un astfel de linker se spune ca face o linkare explicita.
In cazul limbajului C nu se realizeaza o linkare explicita ci numai una implicita
si ea este adesea saraca in testarea consistentei modulelor linkate.
4.2. Link-editare
Daca nu se stabileste altfel, un nume care nu este local la o functie sau clasa
trebuie sa refere acelasi tip, valoare, functie sau obiect in orice parte compilata
separat a programului. Deci exista numai un tip, valoare, functie sau obiect
nelocal atasat la un nume intr-un program. De exemplu, consideram doua fisiere:
// file1.c: int a = 1; int f()A/* face ceva */S
// file2.c: extern int a; int f(); void g()Aa = f();S
'a' si f() utilizati in file2.c sint cele definite in file1.c. Cuvintul cheie
extern indica faptul ca declaratia lui a in file2.c este (chiar) o declaratie
si nu o definitie. Daca 'a' ar fi fost initializata, extern ar fi fost pur si
simplu ignorata deoarece o declaratie cu initializator este totdeauna o definitie.
Un obiect trebuie sa fie definit exact odata intr-un program. Poate fi declarat
de mai multe ori, dar tipul trebuie sa coincida exact. De exemplu:
// file1.c: int a = 1; int b = 1; extern int c;
// file2.c: int a; extern double b; extern int c;
Exista trei erori: 'a' este definit de doua ori (int a: este o definitie insemnind
int a = 0); 'b' este declarat de doua ori cu diferite tipuri; 'c' este declarat
de doua ori dar nu este definit. Aceste tipuri de erori (erori de linkare) nu
pot fi detectate cu un compilator care analizeaza odata numai un fisier. Ele
sint, totusi, detectate la linkare.
Programul urmator nu este in C++ (chiar daca el este in C):
// file1.c: int a; int f()Areturn a;S
// file2.c: int a; int g()Areturn f();S
Intii, file2.c nu este C++ deoarece f() nu a fost declarat, asa ca, compilarea
va esua. In al doilea rind programul nu se va putea linka deoarece 'a' este
definit de doua ori.
Un nume poate fi local la un fisier declarindu-l static. De exemplu:
// file1.c static int a = 6; static int f()A/*.......*/S
// file2.c static int a = 7; static int f()A/*.......*/S
Intrucit fiecare 'a' si f() este declarat static, programul rezultat este corect.
Fiecare fisier are pe 'a' si f() propriu. Cind variabilele si functiile sint
declarate static explicit, un fragment de program este mai usor de inteles (nu
trebuie sa ne uitam in alta parte). Utilizind static pentru functii putem avea,
de asemenea, un efect benefic asupra cantitatii de functii utilizate si dind
compilatorului informatii care pot fi utilizate in ideea realizarii unor optimizari.
Consideram aceste doua fisiere:
// file1.c const a = 7; inline int f()A/*.......*/S struct sAint a, b;S;
// file2.c const a = 7; inline int f()A/*........*/S struct sAint a, b;S;
Daca se aplica regula a "exact unei definitii" la constante, functii
inline si definitii de tip in acelasi mod in care se aplica la functii si variabile,
file1.c si file2.c nu pot fi parte ale aceluiasi program C++. Dardaca este asa,
cum pot doua fisiere sa utilizeze aceleasi tipuri si constante? Raspunsul scurt
este ca tipurile, constantele, etc. pot fi definite de atitea ori de cit este
de necesar cu conditia ca ele sa fie definite identic.
Raspunsul complet este intr-o anumita masura mai complicat (asa cum se explica
in sectiunea urmatoare).
4.3. Fisiere antet
Tipurile in toate declaratiile aceluiasi obiect trebuie sa fie consistente.
Un mod de a atinge acest lucru ar fi de a furniza facilitatile de verificare
de tip linkerului, dar deoarece multe linkere au fost proiectate in 1950 ele
nu pot fi schimbate din motive practice. Este usor a schimba un linker, dar
facind aceasta si scriind un program care depinde de imbunatatirile facute,
cum mai poate fi acest program transferat portabil pe alte calculatoare ?
O alta conceptie este de a asigura ca,codul supus compilarii sa fie sau consistent
sau sa contina chei care sa permita compilatorului sa detecteze inconvenientele.
O metoda imperfecta dar simpla de a atinge consistenta pentru declaratii in
diferite fisiere este de a include fisiere antet, care sa contina informatii
de interfata din fisierele sursa care contin cod executabil si/sau definitii
de date.
Mecanismul #include este o facilitate extrem de simpla de manipulare a textului
sursa pentru a culege fragmente de programe sursa impreuna intr-o singura unitate
(fisier) pentru compilare. Directiva:
#include "to_be_included" inlocuieste linia in care apare #include cu continutul fisierului "to_be_included".
Continutul ar trebui sa fie text sursa C++ intrucit compilatorul va proceda
la citirea lui. Adesea, incluziunea este gestionata printr-un program separat
numit preprocesor C, apelat de CC pentru a transforma fisierul sursa prezentat
de programator intr-un fisier fara a include directivele inainte de a incepe
compilarea propriuzisa. O alta varianta este ca, compilatorul sa gestioneze
aceste directive pe masura ce ele apar in textul sursa. Daca programatorul vrea
sa vada efectul directivelor include, se poate folosi comanda:
CC -E file.c pentru a prelucra fisierul file.c in acelasi mod ca si cind CC ar fi inainte
de a incepe compilarea propriu-zisa. Pentru a include fisiere standard, se utilizeaza
parantezele unghiulare in locul ghilimelelor. De exemplu:
#include <stream.h> // din directorul include standard
#include "myheader.h" // din directorul curent
Avantajul lui "<", ">" este faptul ca numele real
al directorului standard pentru include nu este construit in program.
Un spatiu este semnificativ intr-o directiva include:
#include < stream.h > // nu va gasi stream.h
Ar fi extravagant sa se recompileze un fisier de fiecare data cind este inclus
undeva, dar timpul necesar pentru a compila un astfel de fisier de obicei nu
difera mult de timpul necesar pentru a citi o anumita forma precompilata a lui.
Motivul este ca textul programului este o reprezentare cit se poate de compacta
a programului si ca fisierele incluse, de obicei, contin numai declaratii si
nu un cod care trebuie sa fie analizat extensiv de cart compilator.
Regula urmatoare despre ce poate si ce nu poate fi plasat intr-un fisier antet
nu este o cerinta a limbajului, ci pur si simplu o sugestie despre un mod rezonabil
de a utiliza mecanismul #include.
Un fisier antet poate contine:
|Definitii de tip struct pointAint x, y;S; |
|Declaratii de functii extern int strlenAconst char*S; |
|Definitii de functii inline inline char get()Areturn *p++;S; |
|Declaratii de date extern int a; |
|Definitii de constante const float pi = 3.141593; |
|Enumerari enum bool Afalse, trueS; |
|Directive #include #include <signal.h> |
|Macro definitii #define Case break; case |
|Comentarii /* check for end of file */ |
Dar niciodata nu contine:
|Definitii de functii ordinare char get()Areturn *p++;S |
|Definitii de date int a; |
|Definitii de agregate constante const tblai = A/*...*/S; |
In sistemul UNIX, fisierele antet sint cu extensia convenabila .h. Fisierele
care contin definitii de functii si date vor avea extensia .c. De aceea ele
sint frecvent referite ca "fisiere.h" si respectiv "fisiere.c".
Macrourile se descriu in &4.7. Sa observam ca macrourile sint pe departe
Mai putin utile in C++ decit in C, deoarece C++ are constructia const in limbaj
pentru a defini constante inline.
Motivul de a admite definirea de constante simple si nu si a agregatelor constante
in fisierele.h este pragmatic. In principiu exista o singura problema in admiterea
copiilor definitiilor de variabile (chiar si definitiile functiilor pot fi copiate).
Cu toate acestea, este foarte dificil pentru un linker vechi sa verifice identitatea
constantelor netriviale si sa elimine duplicatele nenecesare. Mai mult decit
atit, cazurile simple sint pe departe mai frecvente si de aceea mai importante
pentru generarea de cod.
4.3.1. Fisier antet unic
Cea mai simpla solutie la problema partitionarii unui program in diferite fisiere
este de a pune definitiile de functii si date intr-un numar potrivit de fisiere
sursa si de a declara tipurile necesare pentru a comunica, intr-un singur fisier
antet care este inclus de toate celelalte fisiere. Pentru programul calculator
putem folosi fisiere.c : lex.c, sgn.c, table.c, main.c si un fisier antet dc.h,
care contine declaratiile fiecarui nume utilizat in Mai mult decit un fisier.c:
//dc.h declaratii comune pentru programul calculator
#include <stream.h> enum token_value
A
NAME, NUMBER, END, PLUS = '+', MINUS = '-', MUL = '*',
DIV = '/', PRINT = ';', ASSIGN = '=', LP = '(', RP = ')'
S; extern int no_of_errors; extern double error(char* s); extern token_value get_token(); extern token_value curr_tok; extern double number_value; extern char name_stringa256i; extern double expr(); extern double term(); extern double prim(); struct nameA char* string; name* next; double value;
S; extern name* look(char* p, int ins = 0); inline name* insert(char* s)Areturn look(s, 1);S
Codul real al lui lex.c va arata astfel:
//lex.c : analiza de intrare si analiza lexicala
#include "dc.h"
#include <ctype.h> token_value curr_tok; double number_value; char name_stringa256i; token_value get_token() A /* ... */ S
Sa observam ca, utilizind fisierele antet in acest fel se asigura ca fiecare
declaratie a unui obiect definit de utilizator intr-un fisier antet va fi intr-un
anumit punct inclus fisierul in care el este definit. De exemplu, cind compilam
lex.c, compilatorul va intilni: extern token_value get_token();
// ... token_value get_token() A /* ... */ S
Aceasta asigura ca, compilatorul va detecta orice inconsistenta in tipurile
specificate pentru un nume. De exemplu, daca get_token() a fost declarat sa
returneze o valoare de tip token_value, dar este definit sa returneze un int,
atunci compilarea lui lex.c va esua, cu eroare de neconcordanta de tip.
Fisierul sgn.c va arata astfel:
//sgn.c : analiza sintactica si evolutiva
#include "dc.h" double prim() A /* ... */ S double term() A /* ... */ S double expr() A /* ... */ S
Fisierul table.c va arata astfel :
//table.c : tabela de simboluri si lookup
#include "dc.h" extern char* strcmp(const char*, const char*); extern char* strcpy(char*, const char*); extern int strlen(const char*); const TBLSZ = 23; name tableaTBLSZi; name* look(char* p, int ins) A /* ... */ S
Sa observam ca table.c declara el insusi functiile standard de manipulare a
sirurilor, asa ca nu exista modificari de consistenta asupra acestor declaratii.
Este aproape totdeauna mai bine sa se includa un fisier antet decit sa se declare
un nume extern intr-un fisier.c. Aceasta ar putea implica sa se includa "prea
mult", dar aceasta nu afecteaza serios timpul necesar pentru compilare
si de obicei va economisi timp pentru programator. Ca un exemplu al acestui
fapt sa observam cum se redeclara strlen() din nou in main.c (de mai jos). Aceasta
este o sursa potentiala de erori intrucit compilatorul nu poate verifica consistenta
celor doua declaratii. Evident, aceasta problema s-ar putea elimina daca fiecare
declaratie externa s-ar plasa in dc.h.
Aceasta neglijenta a fost lasata in program din cauza ca este foarte frecventa
in programele C si conduce la erori care insa nu sint greu de depistat. In final,
fisierul main.c va arata astfel:
//main.c: initializare ciclu principal si tratarea erorilor
#include "dc.h" int no_of_errors; double error(char* s) A /* ... */ S extern int strlen(const char*); main(int argc, char* argvai)A//...S
Exista un caz important in care dimensiunea fisierelor antet devine o pacoste
serioasa. Un set de fisiere antet si o biblioteca pot fi utilizate pentru a
extinde limbajul cu un set de tipuri generale si specifice aplicatiei (vezi
capitolele 5-8). In astfel de cazuri, nu este iesit din comun sa gasim mii de
linii ale fisierelor antet la inceputul fiecarui fisier care se compileaza.
Continutul acelor fisiere este de obicei "inghetat" si se schimba
foarte rar.
O tehnica pentru a incepe compilarea cu continutul acestor fisiere antet poate
fi de mare utilitate. Intr-un sens, se poate crea un anumit limbaj cu un anumit
sens special cu ajutorul compilatorului existent. Nu exista proceduri standard
pentru a crea un astfel de sistem de compilare.
4.3.2 Fisiere antet multiple
Stilul unui singur fisier antet pentru un program partitionat este mult mai
util cind programul este mic si partile lui nu se intentioneaza sa se utilizeze
separat. Apoi, nu este o situatie serioasa faptul ca nu este posibil sa se determine
care declaratii se plaseaza in fisierul antet si pentru ce motiv. Comentariile
pot fi de ajutor. O alternativa este sa lasam ca fiecare parte a unui program
sa aiba fisierul antet propriu care defineste facilitatile pe care le furnizeaza
el. Fiecare fisier.c are atunci un fisier.h corespunzator si fiecare fisier.c include fisierul.h propriu (care specifica
ce furnizeaza el) si de asemenea pot fi si alte fisiere.h (care specifica de
ce are el nevoie).
Considerind aceasta organizare pentru calculator, noi observam ca error() este
utilizata exact ca fiecare functie din program si ea insasi utilizeaza numai
<stream.h>. Aceasta este tipic pentru functiile error() si implica faptul
ca error() ar trebui sa fie separata de main():
//error.h: trateaza erorile extern int no_errors; extern double error(char* s);
//error.c
#include <stream.h>
#include "error.h" int no_of_errors; double error(char* s) A /* ... */ S
In acest stil de utilizare a fisierelor antet, un fisier.h si un fisierul.c
pot fi vazute ca un modul in care fisierul.h specifica o interfata si fisierul.c
specifica implementarea.
Tabela de simboluri este independenta de restul, exceptind utilizarea functiei
error(). Aceasta se poate face acum explicit:
//table.h : declaratiile tabelei de simboluri struct nameA char* string; name* next; double value;
S;
extern name* look(char* p, int ins = 0); inline name* insert(char* s)Areturn look(s, 1);S
//table.c : definitiile tabelei de simboluri
#include "error.h"
#include <string.h>
#include "table.h" const TBLSZ = 23; name* tableaTBLSZi; name* look(char* p, int ins) A /* ... */ S
Sa observam ca declaratiile functiilor de manipulare a sirurilor sint incluse
in <string.h>. Aceasta elimina o alta sursa potentiala de erori.
//lex.h: declaratii pentru intrare si analiza lexicala enum token_valueA
NAME, NUMBER, END, PLUS = '+', MINUS = '-', MUL = '*',
DIV = '/', PRINT = ';', ASSIGN = '=', LP = '(', RP = ')'
S; extern token_value curr_tok; extern double number_value; extern char name_stringa256i; extern token_value get_token();
Aceasta interfata cu analizorul lexical este cit se poate de incurcata. Lipsa
unui tip propriu de lexic arata necesitatea de a prezenta utilizatorului pe
get_token() cu bufferele de lexicuri reale number_value si name_string.
//lex.c : definitiile pentru intrare si analiza lexicala
#include <stream.h>
#include <ctype.h>
#include "error.h"
#include "lex.h" token_value curr_tok; double number_value; char name_stringa256i; token_value get_token() A /* ... */ S
Interfata cu analizorul sintactic este curata:
//syn.h : declaratii pentru analiza sintactica si evoluare
#include "error.h"
#include "lex.h"
#include "syn.h" double prim() A /* ... */ S double term() A /* ... */ S double expr() A /* ... */ S
Programul principal este pe cit de uzual pe atit de trivial:
#include <stream.h>
#include <lex.h>
#include <syn.h>
#include <table.h>
#include <string.h> main(int argc, char* argvai) A /* ... */ S
Cit de multe fisiere antet sa se utilizeze intr-un program depinde de multi
factori. Multi dintre acestia au de a face mai mult cu modul de tratare al fisierelor
pe sistemul dumneavoastra, decit cu C++. De exemplu, daca editorul nu are facilitati
de a cauta in acelasi timp in mai multe fisiere, utilizarea multor fisiere antet
devine mai putin atractiva. Analog, daca deschiderea si citirea a 10 fisiere
de 50 de linii fiecare este substantial mai costisitor decit citirea unui singur
fisier de 500 de linii. Noi trebuie sa gidim de doua ori inainte de a folosi
stilul fisierelor antet multiple pentru un program mic. Un sfat: un set de 10
fisiere antet plus fisierele standard antet este de obicei ceva normal de gestionat.
Totusi, daca partitionati declaratiile unui program mare in fisiere antet de
dimensiuni logic minime (punind fiecare declaratie de structura intr-un fisier
propriu, etc.), atunci ve-ti ajunge usor la sute de fisiere greu de gestionat.
4.3.3 Ascunderea datelor
Utilizind fisierele antet, un utilizator poate defini explicit interfetele pentru
a asigura utilizarea consistenta a tipurilor dintr-un program. Cu toate acestea,
un utilizator poate ocoli interfata furnizata printr-un fisier antet inserind
declaratiile externe in fisierele.c.
Sa observam ca stilul urmator de legatura nu este recomandat:
//file1.c : "extern" nu se utilizeaza int a = 7; const c = 8; void f(long) A /* ... */ S
//file2.c : "extern" in fisierul.c extern int a; extern const c; extern f(int); int g()A return f(a+c); S
Intrucit declaratiile extern din file2.c nu sint incluse cu definitiile din
file1.c compilatorul nu poate verifica consistenta acestui program. In consecinta,
daca incarcatorul nu este mai destept decit de obicei, cele doua erori din acest
program va trebui sa le gaseasca programatorul. Un utilizator poate proteja
un fisier impotriva unei astfel de legaturi indisciplinate declarind ca static
acele nume care nu se intentioneaza sa se utilizeze global. Astfel, ele au ca
dome- niu fisierul respectiv si sint interzise pentru alte parti din program.
De exemplu:
//table.c : definitia tabelei de simboluri
#include "error.h"
#include <string.h>
#include "table.h" const TBLSZ = 23; static name* tableaTBLSZi; name* look(char* p, int ins) A /* ... */ S
Aceasta va asigura ca toate accesele la table sa se faca prin look(). Nu este
necesar sa se "ascunda" constanta TBLSZ.
4.4 Fisiere si Module
In sectiunea precedenta fisierele.c si .h definesc impreuna o parte a programului.
Fisierul.h este interfata utilizata de alte parti ale programului; fisierul.c
specifica implementarea.
O astfel de entitate este numita, adesea, modul. Numai numele de care are nevoie
sa le cunoasca utilizatorul se fac disponibile iar restul sint ascunse. Aceasta
proprietate se numeste adesea ascunderea datelor, chiar daca data este numai
unul din lucrurile ce se pot ascunde. Acest tip de modul furnizeaza o flexibilitate
mare. De exemplu, o implementare poate consta din unul sau Mai multe fisiere.c
si diferite interfete ce pot fi furnizate sub forma de fisiere.h. Informatia
pe care un utilizator nu este necesar sa o cunoasca este ascunsa in fisierul.c.
Daca se considera ca utilizatorul nu trebuie sa stie exact ce contine fisierul.c,
atunci el nu trebuie sa fie disponibil in sursa. Fisierele de tip .obj sint
suficiente.
Este uneori o problema ca aceasta flexibilitate sa fie atinsa fara o structura
formala. Limbajul insusi nu recunoaste un astfel de modul ca o entitate si nu
exista nici o cale ca, compilatorul sa faca distinctie intre fisierele.h care
definesc nume ce sa fie utilizate de alte module (exportate) de fisierele.h
folosite pentru a declara nume din alte module (importate). Alta data, poate
fi o problema ca un modul sa defineasca un set de obiecte si nu un nou tip.
De exemplu, modulul table defineste o tabela; daca noi dorim doua tabele, nu
exista un mod trivial de a furniza celalalt tabel utilizind aceasta idee de
module. Capitolul 5 prezinta o solutie a acestei probleme.
Fiecare obiect alocat static este implicit initializat cu zero, iar alte valori
(constante) pot fi specificate de programator. Aceasta este doar o forma primitiva
de initializare. Din fericire, utilizind clasele, se poate specifica un cod
care sa fie executat pentru initializare inainte de a face orice utilizare a
modulului si de asemenea se poate executa cod pentru anulare (curatire) dupa
ultima utilizare a modulului. (vezi &5.5.2).
4.5 Cum se construieste o biblioteca
Fraze de genul "pune in biblioteca" si "gaseste intr-o anumita
biblioteca" se utilizeaza des (in aceasta carte si in alta parte), dar
ce inseamna acest lucru pentru un program C++ ?
Din nefericire, raspunsul depinde de sistemul de operare utilizat. Aceasta sectiune
explica cum se face si se utilizeaza o biblioteca in versiunea 8 a sistemului
UNIX. Alte sisteme furni zeaza facilitati similare.
O biblioteca, in principiu, este o multime de fisiere.o obtinute prin compilarea
unui set de fisiere.c. De obicei exista unul sau mai multe fisiere.h care contin
declaratii necesare pentru a utiliza acele fisiere.o. Ca un exemplu, sa consideram
ca avem de furnizat (in mod convenabil) un set de functii matematice pentru
o multime nespecificata de utilizatori. Fisierul antet ar putea arata astfel: extern double sqrt(double); //subset al lui <math.h> extern double cos(double); extern double exp(double); extern double log(double); iar definitiile acestor functii vor fi memorate in fisierele sqrt.c, sin.c,
cos.c, exp.c si respectiv log.c.
O biblioteca numita math.a poate fi facuta astfel:
$cc -c math.c sin.c cos.c exp.c log.c
$ar cr math.a sqrt.o sin.o cos.o exp.o log.o
$ranlib math.a
Fisierele sursa se compileaza intii obtinindu-se fisiere obiect echivalente.
Se utilizeaza apoi comanda ar pentru a face o arhiva numita math.a. In final
arhiva respectiva este indexata pentru un acces mai rapid. Daca sistemul dumneavoastra
nu are comanda ranlib, atunci probabil ca nu aveti nevoie de ea; sa va uitati
in manualul de operare pentru detalii. Biblioteca poate fi utilizata astfel:
$cc myprog.c math.a
Acum, care este avantajul utilizarii lui math.a in loc de a utiliza direct fisierele.o?
De exemplu:
$ myprog.c sqrt.o sin.o cos.o exp.o log.o
Pentru majoritatea programelor, gasirea setului corect de fisiere.o nu este
un lucru trivial. In exemplul de mai sus, ele au fost toate incluse, dar daca
functiile din myprog.c apeleaza numai functiile sqrt() si cos() atunci pare
ca ar fi suficient:
$cc myprog.c sqrt.o cos.o
Acest lucru nu este tocmai asa deoarece cos.c utilizeaza sin.c. Linkerul apelat
de comanda cc ca sa foloseasca un fisier.a (in acest caz math.a) stie sa extraga
numai fisierele.o necesare, din multimea care a fost utilizata pentru a crea
fisierul.a.
Cu alte cuvinte, folosind o biblioteca, se pot include multe definitii folosind
un singur nume (inclusiv definitii de functii si variabile utilizate de functii
interne pe care utilizatorul nu le-a vazut niciodata) si in acelasi timp se
asigura numai un numar minim de definitii include.
4.6 Functii
Modul tipic de a face ceva intr-un program C++ este de a apela o functie care
sa faca lucrul respectiv. Definirea unei functii este o cale de a specifica
cum sa se faca o operatie. O functie nu poate fi apelata daca ea nu este declarata.
4.6.1 Declaratii de functii
O declaratie de functie da un nume functiei, tipul valorii returnate (daca returneaza
vreuna) de functie, numarul si tipurile argumentelor care trebuie furnizate
in apelul unei functii. De exemplu: extern double sqrt(double); extern elem* next_elem(); extern char* strcpy(char* to, const char* from); extern void exit(int);
Semantica transferului de argumente este identica cu semantica initializarii.
Tipurile argumentelor se verifica si se fac conversii implicite ale tipurilor
argumentelor cind este necesar. De exemplu, dindu-se declaratiile precedente: doublesr2 = sqrt(2); va apela corect functia sqrt() cu valoarea 2.0.
O declaratie de functie poate contine nume de argumente. Acest lucru poate fi
un ajutor pentru cititor, dar compilatorul ignora pur si simplu astfel de nume.
4.6.2 Definitii de functii
Fiecare functie care este apelata intr-un program trebuie sa fie definita undeva
(o singura data). O definitie de functie este o declaratie de functie in care
este prezent corpul functiei. De exemplu: extern void swap(int*, int*); //o declaratie void swap(int* p, int* q) //o definitie
A int t = *p;
*p = *q;
*q = t;
S
O functie poate fi declarata inline pentru a elimina apelul functiei suprapunind-o
peste el (&1.12), iar argumentele pot fi declarate register pentru a furniza
un acces mai rapid la ele (&2.3.11). Ambele caracteristici pot fi eliminate
si ele ar trebui sa fie eliminate ori de cite ori exista dubii in legatura cu
utilitatea folosirii lor.
4.6.3 Transferul argumentelor
Cind se apeleaza o functie se rezerva memorie pentru argumentele formale si
fiecare argument formal se initializeaza prin argumentele efective corespunzatoare.
Semantica transferului de parametri este identica cu semantica initializarii.
In parti- cular se verifica tipul unui argument efectiv cu tipul argumentului
formal corespunzator si se fac toate conversiile de tip standard si definite
de utilizator. Exista reguli speciale pentru transferul vectorilor (&4.6.5),
o facilitate pentru transferul neverificat al argumentelor (&4.6.8) si o
facilitate pentru specificarea argumentelor implicite (&4.6.6). Consideram: void f(int val, int& ref)
A val++; ref++;
S
Cind se apeleaza f(), val++ mareste o copie locala a primului sau argument,
in timp ce ref++ incrementeaza cel de al doilea argument efectiv. De exemplu: int i = 1; int j = 1; f(i, j); va incrementa pe j dar nu si pe i. Primul argument i este pasat prin valoare,
iar cel de al doilea prin referinta. Asa cum s-a mentionat in &2.3.10, folosind
functii care modifica argumentele apelate prin referinta se pot face programe
greu de citit si in general ar trebui eliminate (dar vezi &6.5 si &8.4).
Totusi, este mult mai eficient ca un obiect mare sa fie transferat prin referinta
in loc sa fie transferat prin valoare. In acest caz, argumentul ar putea fi
declarat const pentru a indica faptul ca referinta se utilizeaza numai din motive
de eficienta iar functia apelata nu poate schimba valoarea obiectului: void f(const large& arg)
A //valoarea lui arg nu poate fi schimbata S
Analog, declarind un argument pointer const, cititorul este avertizat ca valoarea
obiectului spre care pointeaza acel argument nu se schimba prin functia respectiva.
De exemplu : extern int strlen(const char*); //din <string.h> extern char* strcpy(char*
to, const char* from); extern int strcmp(const char*, const char*);
Importanta acestei practici creste cu dimensiunea programului. Sa observam ca
semantica transferului de argumente este diferita de semantica asignarii. Acest
lucru este important pentru argumentele const, pentru argumentele referinta
si pentru argumentele unor tipuri definite de utilizator (&6.6).
4.6.4 Valoarea returnata
O valoare poate fi (si trebuie) returnata dintr-o functie care nu este declarata
void. Valoarea returnata se specifica printr-o instructiune return. De exemplu: int fact(int n)
A return (n>1) ? n*fact(n-1) : 1;
S
Pot fi mai multe instructiuni return intr-o functie: int fact(int n)
A if(n > 1) return n*fact(n-1); else return 1;
S
Ca si semantica transferului de argumente, semantica valorii returnate de o
functie este identica cu semantica initializarii. O instructiune return se considera
ca initializeaza o variabila de tipul returnat. Tipul expresiei returnate se
verifica cu tipul valorii returnate de functie si la nevoie se fac toate conversiile
de tip standard sau definite de utilizator. De exemplu: double f()
A // ... return 1; //se converteste spre double(1)
S
De fiecare data cind se apeleaza o functie se creaza o copie noua pentru argumentele
si variabilele automatice ale ei. Memoria este eliberata la revenirea din functie,
asa ca nu este indicat sa se returneze un pointer spree o variabila locala.
Continutul locatiei spre care se face pointarea se va schimba imprevizibil: int* f()
A int local = 1;
// ... return &local; //nu se face asa ceva
S
Din fericire, compilatorul avertizeaza asupra unor astfel de valori returnate.
Iata un alt exemplu: int& f()
A return 1; //nu se face asa ceva
S
4.6.5 Argumente vector
Daca se utilizeaza un vector ca un argument de functie, se transfera un pointer
spre primul sau element. De exemplu: int strlen(const char*); void f()
A char vai = "a vector"; strlen(v); strlen("Nicholas");
S
Cu alte cuvinte, un argument de tip Tai va fi convertit spre T* cind este transferat.
Rezulta ca o asignare la un element al argumentului vector schimba valoarea
elementului argumentului respectiv. Cu alte cuvinte, vectorii difera de alte
tipuri prin aceea ca vectorul nu este pasat prin valoare (si nici nu poate fi
pasat prin valoare). Dimensiunea unui vector nu este disponibila in functia
apelata. Aceasta poate fi o pacoste, dar exista dife- rite moduri de tratare
a acestei probleme. Sirurile se termina prin zero, asa ca dimensiunea lor se
poate calcula usor. Pentru alte tipuri de vectori se poate transfera un al doilea
argument care contine dimensiunea sau un tip care contine un pointer si un indicator
de lungime in locul vectorului (&11.11). De exemplu: void compute1(int* vec_ptr, int vec_size); //un mod struct vecA //un alt mod int* ptr; int size;
S; void compute2(vec v);
Tablourile multidimensionale sint mai ciudate, dar adesea pot fi utilizati vectori
de pointeri in locul lor si nu au nevoie de o tratare speciala. De exemplu: char* dayai = A"mon","tue","wed","thu","fri","sat","sun"S;
Cu toate acestea consideram definirea unei functii care manipuleaza o matrice
bidimensionala. Daca dimensiunile sint cunoscute la compilare, nu exista nici
o problema: void print_m34(int ma3ia4i)
A for(int i=0; i<3; i++)
A for(int j=0; j<4; j++) cout << " " << maiiaji; cout << "\n";
S
S
Cazul dificil apare cind trebuie pasate ambele dimensiuni. "Solutia evidenta"
pur si simplu nu functioneaza: void print_mij(int maiai, int dim1, int dim2) //eroare
A for(int i=0; i<dim1; i++)
A for(int j=0; j<dim2; j++) cout << " " << maiiaji; //surpriza cout << "\n";
S
S
In primul rind, argumentul maiai este ilegal deoarece trebuie sa fie cunoscuta
dimensiunea a doua a tabloului pentru a gasi locatia unui element.
In al doilea rind, expresia maiiaji este corect interpretata ca *(*(m+i)+j),
dar aceasta este improbabil ca este ce a dorit programatorul. O solutie corecta
este: void print_mij(int** m, int dim1, int dim2)
A for(int i=0; i<dim1; i++)
A for(int j=0; j<dim2; j++) cout << " " << ((int*)m)ai*dim2+ji; //obscur cout << "\n";
S
S
Expresia utilizata pentru a face acces la elementele tabloului este echivalenta
cu cea generata de compilator cind cunoaste ultima dimensiune. Se poate introduce
o variabila auxiliara pentru a face codul mai putin obscur: int* v = (int*)m; vai*dim2+ji;
4.6.6 Argumente implicite
O functie necesita adesea mai multe argumente in general, decit este nevoie
in cazul cel mai simplu sau in cazul cel mai frecvent. De exemplu, biblioteca
stream are o functie hex() care produce un sir ce contine reprezentarea hexazecimala
a unui intreg. Un al doilea intreg se foloseste pentru a specifica numarul de
caractere disponibile pentru reprezentarea primului argument. Daca numarul de
caractere este prea mic pentru a reprezenta intregul, apare trunchierea; daca
este prea mare, sirul este completat cu spatii. Adesea, programatorul nu se
intereseaza despre numarul de caractere necesare pentru a reprezenta intregul
atita timp cit exista spatiu suficient, asa ca argumentul al doilea este 0 pentru
a indica faptul ca la conversie sa se utilizeze "exact atitea caractere
cite sint necesare". Pentru a elimina apelurile de forma hex(i, 0), functia
se declara astfel: extern char* hex(long, int = 0);
Initializarea pentru cel de al doilea parametru inseamna ca acesta este un parametru
implicit. Adica, daca numai un argument este prezent intr-un apel, cel de al
doilea este utilizat impli- cit. De exemplu: cout << "**" << hex(31) << hex(32, 3) << "**"; se interpreteaza astfel: cout << "**" << hex(31, 0) << hex(32, 3) <<
"**"; si va imprima:
**1f 20**
Un argument implicit se verifica din punct de vedere al tipului in momentul
declararii functiei si este evaluat in momentul apelului. Este posibil sa se
furnizeze argumente implicite numai pentru argumente din ultimele pozitii, asa
ca: int f(int, int = 0, char* = 0); //ok int g(int = 0, int = 0, char*); //error int h(int = 0, int, char* = 0); //error
Sa observam ca in acest caz spatiul dintre * si = este semnificativ (*= este
operatorul de asignare): int nasty(char *= 0); //syntax error
4.6.7 Nume de functii supraincarcate
Adesea este o idee buna de a da la diferite functii nume diferite, dar cind
niste functii fac acelasi lucru asupra obiectelor de tipuri diferite, poate
fi mai convenabil sa le dam acelasi nume. Utilizarea aceluiasi nume pentru operatii
diferite pentru tipuri diferite se numeste supraincarcare. Tehnica este deja
utilizata pentru operatii de baza in C++; exista un singur nume pentru adunare
(+), dar el poate fi utilizat pentru a aduna valori de tipuri intregi, in flotant
si pointeri. Aceasta idee se extinde simplu pentru a trata operatii definite
de programator, adica functii. Pentru a proteja programatorul de reutilizarea
accidentala a unui nume, un nume poate fi utilizat pentru mai multe functii
numai daca este declarat la inceput ca fiind supraincarcat. De exemplu: overload print; void print(int); void print(char*);
La compilare singurul lucru pe care functiile il au in comun este numele. Probabil
ca intr-un anumit sens functiile sint similare, dar limbajul nu are restrictii
asupra lor. Astfel numele supraincarcat al functiilor sint in primul rind o
conventie de notatie. Aceasta conventie este semnificativa pentru functii cu
nume conventionale, cum ar fi sqrt, print si open. Cind un nume este semantic
semnificativ, cum ar fi operatorii +, * si << (&6.2) si in cazul constructorilor
(&5.2.4 si &6.3.1), aceasta facilitate devine esentiala. Cind este apelata
o functie f() supraincarcata, compilatorul trebuie sa stie care functie este
apelata dintre cele cu numele f. Aceasta se face prin compararea tipurilor argumentelor
efective cu tipurile argumentelor formale a tuturor functiilor numite f. Gasirea
functiei care sa fie apelata se face in trei pasi separati:
a1i Cauta o corespondenta exacta si daca exista se utilizeaza functia respectiva;
a2i Cauta o corespondenta utilizind conversii predefinite si utilizeaza o functie
gasita in acest fel;
a3i Cauta o corespondenta folosind conversiile definite de utilizator (&6.3)
si daca exista un set de conversii unic, se utilizeaza functia gasita. De exemplu: overload print(double), print(int); void f()
A print(1); print(1.0);
S
Regula de corespondenta exacta va face ca f() sa scrie pe 1 ca un intreg, iar
pe 1.0 ca un numar flotant. Zero, char sau short sint fiecare o corespondenta
exacta pentru un argument int. Analog, un float este o corespondenta exacta
pentru double.
Pentru argumentele functiilor cu nume supraincarcate, regulile de conversie
standard (&r.6.6) nu se aplica complet. Conversiile care pot distruge informatie
nu se aplica, raminind int spre long, int spre double, zero spre long, zero
spre double si conversia de pointeri; zero spre pointer, pointer spre void*
si pointer spre clasa derivata pentru a pointa spre baza clasei (&7.2.4).
Iata un exemplu in care este necesara conversia: overload print(double), print(long); void f(int a)Aprint(a);S
Aici a poate fi imprimat sau ca double sau ca long. Ambiguitatea poate fi rezolvata
utilizind tipul de conversie explicita (sau print(long(a)) sau print(double(a))).
Dindu-se aceste reguli, se poate asigura ca cel mai simplu algoritm (functie)
va fi utilizat, cind eficienta sau precizia calcului difera semnificativ pentru
tipurile implicite. De exemplu: overload pow; int pow(int, int); double pow(double, double); //din <math.h>
complex pow(double, complex); //din <complex.h> complex pow(complex, int);
complex pow(complex, double); complex pow(complex, complex);
Procesul de gasire a corespondentei ignora unsigned si const.
4.6.8 Numar nespecificat de argumente
Pentru anumite functii nu este posibil sa se specifice numarul si tipul tuturor
argumentelor asteptate intr-un apel. O astfel de functie se declara terminind
lista argumentelor din declaratie prin trei puncte (...) care inseamna ca "
pot fi mai multe argumente". De exemplu: int printf(char* ...);
Aceasta specifica faptul ca un apel a lui printf trebuie sa aiba cel putin un
argument de tip char* si poate sa aiba sau nu si altele. De exemplu: printf("Hello, word\n"); printf("My name is %s %s\n", first_name, second_name); printf("%d + %d = %d\n", 2, 3, 5);
O astfel de functie trebuie sa se refere la o informatie care nu este disponibila
compilatorului cind se interpreteaza lista de argumente. In cazul functiei printf(),
primul argument este un sir de format care contine o succesiune de caractere
speciale care permite ca printf() sa trateze corect celelalte argumente: %s
inseamna "se asteapta un argument de tip char*" iar %d inseamna "asteapta
un argument int". Cu toate acestea, compilatorul nu stie aceasta, asa ca
el nu se poate asigura ca argumentele asteptate sa existe in realitate sau ca
un argument este un tip propriu. De exemplu: printf("My name is %s %s\n", 2); se va compila si in cel mai bun caz se va scrie la executie ceva straniu. Evident
daca un argument nu a fost declarat, compilatorul nu are informatia necesara
pentru a face verificarea standard de tip si de a face eventual o conversie
de tip. In acest caz, char sau short se transfera ca int, iar float ca double.
Aceasta nu este in mod necesar ceea ce a vrut utilizatorul.
Utilizarea la extrema a celor trei puncte conduce la imposibilitatea de a verifica
argumentele, lasind programatorului deschisa problema aceasta. Un program bine
proiectat necesita cel putin citeva functii pentru care tipurile argumentelor
nu sint specificate complet. Functiile supraincarcate si functiile care utilizeaza
argumente implicite pot fi utilizate avind grija ca verificarea tipului sa se
faca ori de cite ori se utilizeaza argumente de tip nespecificat. Numai cind
atit numarul de argu mente cit si tipul argumentelor variaza este necesar sa
se foloseasca trei puncte. Cea mai frecventa utilizare a celor trei puncte este
de a specifica o interfata cu functiile de biblioteca ale lui C care sint definite
fara a fi disponibile alternativele posibile: extern int fprintf(FILE*, char* ...); din <stdin.h> extern int execl(char* ...); din <system.h> extern int abort(...); din <libc.h>
Un set de macrouri standard disponibile pentru a avea acces la argumente nespecificate
in astfel de functii pot fi gasite in <stdargs.h>. Sa consideram scrierea
unei functii eroare care are un argument intreg ce indica nivelul de eroare,
urmat de un numar arbitrar de siruri. Ideea este de a compune mesajul de eroare
pasind fiecare cuvint ca un argument de tip sir separat: void error(int ...); main(int argc, char* argvai)
Aswitch(argc)
Acase 1: error(0, argva0i, 0); break; case 2: error(0, argva0i, argva1i, 0); break; default: error(1, argva0i, "with", dec(argc-1),
"arguments", 0);
S
S
Functia eroare ar putea fi definita astfel:
#include <stdargs.h> void error(int n ...)
// "n" urmat de o lista de char* s terminata prin zero
A va_list ap; va_start(ap, n); //arg startup for(;;)
A char* p = va_arg(ap, char*); if(p == 0) break; cerr << p << " ";
S va_end(ap); //curatirea argumentelor cerr << "\n"; if(n) exit(n);
S
Intii se defineste va_list care este initializata prin apelul lui va_start().
Macroul va_start ia numele lui va_list si numele ultimului argument formal ca
argumente. Macroul va_arg() se utilizeaza pentru a alege argumentul nedenumit
in ordine. La fiecare apel programatorul trebuie sa furnizeze un tip; va_arg()
presupune ca argumentul efectiv de acel tip a fost pasat, dar de obicei nu exista
o cale de a asigura aceasta. Inainte de a reveni dintr-o functie in care s-a
utilizat va_start(), trebuie apelata va_end(). Motivul este ca va_start() poate
modifica stiva in asa fel ca revenirea nu se va Mai realiza cu succes: va_end()
reface stiva la forma necesara revenirii corecte.
4.6.9 Pointer spre functie
Exista numai doua lucruri care pot fi facute cu o functie: apelul ei si sa
se ia adresa ei. Pointerul obtinut functiei poate fi apoi utilizat pentru a
apela functia. De exemplu: void error(char* p)A/*...*/S void (*efct)(char*); //pointer spre functie void
f()
Aefct = &error; //efct pointeaza spre error
(*efct)("error"); //apelul lui error prin efct
S
Pentru a apela o functie printr-un pointer (de exemplu efct) intii trebuie
sa i se atribuie pointerului adresa functiei res- pective. Intrucit operatorul
() de apel de functie are prioritate mai mare decit operatorul *, nu se poate
scrie apelul prin *efct("error") caci aceasta inseamna *(efct("error")),
ceea ce este o eroare de tip. Acelasi lucru se aplica la sintaxa declaratiei
(vezi de asemenea &7.3.4).
Sa observam ca pointerii spre functii au tipurile argumentelor declarate ca
si functiile insasi. In asignarea de pointeri, tipul functiei trebuie sa corespunda
exact. De exemplu: void (*pf)(char*); //pointer spre void(char*); void f1(char*); //void(char*); int f2(char*); //int(char*); void f3(int*); //void(int*);
void f()
A pf = &f1; //ok pf = &f2; //eroare: tipul valorii returnate
// este eronat pf = &f3; //eroare: argument de tip eronat
(*pf)("asdf"); //ok
(*pf)(1); //eroare: tip de argument eronat int i = (*pf)("qwer"); //eroare: void se asigneaza la int
S
Regulile pentru pasarea argumentelor sint aceleasi atit pentru apelurile directe
la o functie cit si pentru apelurile la o functie printr-un parametru. Adesea
este convenabil sa se defineasca un nume pentru tipul unui pointer spre o functie
pentru a elimina utilizarea tot timpul a unei sintaxe neevidente. De exemplu: typedef int (*SIG_TYP)(); //din <signal.h> typedef void (*SIG_ARG_TYP)();
SIG_TYP signal(int, SIG_ARG_TYP);
Adesea este util un vector de pointeri spre functii. De exemplu, sistemul de
meniuri pentru editorul bazat pe "mouse" se implementeaza utilizind
vectori de pointeri spre functii ce reprezinta operatii. Sistemul nu poate fi
descris aici in detaliu dar ideea generala este aceasta: typedef void (*PF)();
PF edit_opsai=Acut, paste, snarf, searchS; //op. de editare
PF file_opsai=Aopen, reshape, close, writeS;//tratarea fis.
Definirea si initializarea pointerilor care definesc actiunile selectate dintr-un
meniu asociat cu butoanele mouse-ului:
PF* button2 = edit_ops;
PF* button3 = file_ops;
Intr-o implementare completa, este necesara mai multa informatie pentru a defini
fiecare element. De exemplu, un sir care specifica textul de afisat trebuie
sa fie pastrat undeva. Pe masura ce se utilizeaza sistemul, sensul butoanelor
mouse se schimba frecvent cu contextul. Astfel de schimbari se realizeaza (partial)
schimbind valoarea pointerilor de butoane. Cind un utilizator selecteaza un
meniu, cum ar fi elementul 3 pentru butonul 2, se executa operatia asociata:
(*button2a3i)();
Un mod de a cistiga o apreciere a puterii expresive a pointerilor spree functii
este incercarea de a scrie cod fara ele. Un meniu poate fi modificat la executie
inserind functii noi intr-o tabela operator. Este de asemenea usor sa se construiasca
meniuri noi la executie.
Pointerii spre functii pot fi utilizati sa furnizeze rutine care pot fi aplicate
la obiecte de tipuri diferite: typedef int (*CFT)(char*, char*); int sort(char* base, unsigned n, int sz, CFT
cmp)
/* Sorteaza cele n elemente ale vectorului "base" in ordine crescatoare
utilizind functia de comparare spre care pointeaza "cmp". Elementele
sint de dimensiune "sz".
Algoritm foarte ineficient: bubble sort.
*/
A for(int i = 0; i < n-1; i++) for(int j = n-1; i < j; j--)
A char* pj = base+j*sz; //baji char* pj1 = pj-sz; //baj-1i if((*cmp)(pj, pj1) < 0) //swap baji and baj-1i for(int k = 0; k < sz; k++)
A char temp = pjaki; pjaki = pj1aki; pj1aki = temp;
S
S
S
Rutina de sortare nu cunoaste tipul obiectelor pe care le sorteaza, ci numai
numarul de elemente (dimensiunea vectorului), dimensiunea fiecarui element si
functia de apelat pentru a face compararea. Tipul lui sort() ales este acelasi
cu tipul rutinei qsort() din biblioteca C standard. Programele reale utilizeaza
qsort(). Intrucit sort() nu returneaza o valoare, ar trebui declarata cu void,
dar tipul void nu a fost introdus in C cind a fost definit qsort(). Analog,
ar fi mai onest sa se foloseasca void* in loc de char* ca tip de argument. O
astfel de functie sort() ar putea fi utilizata pentru a sorta o tabela de forma: struct userAchar* name; char* id; int dept;
S; typedef user* Puser; user headsai=A"McIlroy M.D.", "doug", 11271,
"Aho A.V.", "ava", 11272,
"Weinberger P.J.", "pjw", 11273,
"Schryer N.L.", "nls", 11274,
"Schryer N.L.", "nls", 11275,
"Kernighan B.W.", "bwk", 11276
S;
void print_id(Puser v, int n)
Afor(int i = 0; i < n; i++) cout << vaii.name << "\t" << vaii.id << "\t"
<< vaii.dept << "\n";
S
Pentru a putea face sortarea, intii trebuie sa definim functia de comparare
potrivita. O functie de comparare trebuie sa returneze o valoare negativa daca
primul ei argument este mai mic decit al doilea, zero daca sint egale si un
numar pozitiv altfel: int cmp1(char* p, char* q) //se compara sirurile nume
A return strcmp(Puser(p)->name, Puser(q)->name);
S
int cmp2(char* p, char* q) //se compara numerele dept
A return Puser(p)->dept - Puser(q)->dept;
S
Programul acesta sorteaza si imprima: main()
A sort((char*)heads, 6, sizeof(user), cmp1); print_id(heads, 6) //in ordine alfabetica
cout << "\n"; sort((char*)heads, 6, sizeof(user), cmp2); print_id(heads, 6); //in ordinea numerelor de departament
S
Este posibil sa se ia adresa unei functii inline si de asemenea sa se ia adresa
unei functii supraincarcate (&r8.9).
4.7 Macrouri
Macrourile se definesc in &r11. Ele sint foarte importante in C, dar sint
pe departe mai putin utilizate in C++. Prima regula despre ele este: sa nu fie
utilizate daca nu trebuie. S-a observat ca aproape fiecare macro demonstreaza
o fisura fie in limbajul de programare, fie in program. Daca doriti sa folositi
macrouri va rog sa cititi foarte atent manualul de referinta pentru implementarea
preprocesorului C pe care il folositi. Un macro simplu se defineste astfel:
#define name restul liniei
Cind name se intilneste ca o unitate lexicala, el este inlocuit prin restul
liniei. De exemplu: named = name va fi expandat prin: named = restul liniei
Un macro poate fi definit, de asemenea, prin argumente. De exemplu:
#define mac(a, b) argunent1: a argument2: b
Cind se utilizeaza mac, cele doua siruri de argumente trebuie sa fie prezente.
Ele vor inlocui pe a si b cind se expandeaza mac(). De exemplu: expanded = mac(foo bar, yuc yuk) va fi expandat in: expanded = argument1: foo bar argument2: yuk yuk
Macrourile manipuleaza siruri si stiu putin despre sintaxa lui C++ si nimic
despre tipurile si regulile de existenta ale lui C++. Compilatorul vede numai
formele expandate ale unui macro, asa ca o eroare intr-un macro va fi propagata
cind macroul se expandeaza. Aceasta conduce la mesaje de eroare obscure, ele
nefiind descoperite in definitia macroului. Iata citeva macrouri plauzibile:
#define case break;case
#define nl <<"\n"
#define forever for(;;)
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
Iata citeva macrouri complete necesare:
#define PI 3.141593
#define BEGIN A
#define END S
Iata citeva macrouri periculoase:
#define SQUARE(a) a*a
#define INCR_xx (xx)++
#define DISP = 4
Pentru a vedea de ce sint periculoase, sa incercam sa expandam: int xx = 0; //numarator global void f()
A int xx = 0; //variabila locala xx = SQUARE(xx+2); //xx=xx+2*xx+2
INCR_xx; //incrementeaza localul xx if(a-DISP == b) //a-= 4==b
A
//....
S
S
Daca noi dorim sa utilizam un macro trebuie sa utilizam operatorul de rezolutie
a domeniului "::" cind dorim sa facem referinte la nume globale (&2.1.1)
si sa includem in paranteze aparitiile numelor argumente ale macrourilor (vezi
MIN de mai sus).
Sa se observe diferenta efectelor de expandare a acestor doua macrouri:
#define m1(a) something(a) // comentariu serios
#define m2(a) something(a) /* comentariu serios */
De exemplu: int a = m1(1) + 2; int b = m2(1) + 2;
se vor expanda in int a = something(1) // comentariu serios + 2 ; int b = something(1) /* comentariu
serios */ + 2;
Utilizind macrouri, noi putem proiecta limbajul nostru propriu; el va fi probabil
mult mai incomprehensibil decit altele. Mai mult decit atit, preprocesorul C
este un macroprocesor foarte simplu. Cind noi incercam sa facem ceva netrivial,
noi probabil gasim sau ca este imposibil sau ceva nejustificat de greu de realizat
(dar vezi &7.3.5).
4.8 Exercitii
1. (*1). Sa se scrie declaratii pentru: o functie care are ca argumente un
pointer spre caractere si referinta la un intreg si nu returneaza nici o valoare;
un pointer spre o astfel de functie; o functie care are un astfel de pointer
ca argument; o functie care returneaza un astfel de pointer. Sa se scrie definitia
unei functii care are un astfel de pointer ca argument si returneaza argumentul
ei ca valoare. Sa se utilizeze typedef.
2. (*1). Ce semnifica linia de mai jos? La ce ar fi buna ea? typedef int(rifii&)(int,
int);
3. (*1.5). Sa se scrie un program ca "Hello, world" care ia un nume
din linia de comanda si care scrie "Hello, numele respectiv". Sa se
modifice acest program pentru a lua oricite nume ca argumente si sa se scrie
Hello la fiecare.
4. (*1.5). Sa se scrie un program care citeste un numar arbitrar de fisiere
a caror nume se dau ca argumente in linia de comanda si le scrie unul dupa altul
in cout. Acest program se poate numi cat deoarece concateneaza fisierele respective.
5. (*2). Sa se converteasca un program mic C intr-un program C++. Sa se modifice
fisierele antet pentru a declara toate fun- ctiile apelate si sa declare tipul
fiecarui argument. Sa se inlo- cuiasca #define prin enum, const sau inline unde
este posibil. Sa se elimine declaratiile extern din fisierele C si sa se converteasca
in sintaxa definitiilor de functii din C++. Sa se inlocuiasca apelurile lui
malloc() si free() cu new si delete. Sa se elimine conversiile de tip explicit
necesare.
6. (*2). Sa se implementeze sort() (&4.6.9) utilizind un algoritm de sortare
mai eficient.
7. (*2). Sa consideram definitia lui struct tnode din &r8.5. Sa se scrie
functia pentru introducerea unui cuvint nou intr-un arbore de tnode noduri.
Sa se scrie o functie care listeaza arborele de tnode noduri. Sa se scrie o
functie care listeaza arborele respectiv in ordine alfabetica a cuvintelor pe
care le contine. Sa se modifice tnode astfel incit sa contina numai un pointer
spre un cuvint de lungime arbitrara memorat in memoria libera folosind new.
Sa se modifice functiile pentru a putea utiliza noua definitie a lui tnode.
8. (*2). Sa se scrie un modul care implementeaza o stiva. Fisierul.h trebuie
sa declare functiile push(), pop() si orice alte functii potrivite. Un fisier.c
defineste functiile si datele necesare de a fi pastrate pe stiva.
9. (*2). Sa cunoasteti fisierele antet standard. Sa se listeze fisierele din
/usr/include si /usr/include/cc (sau orice alte fisiere antet standard pastrate
de sistemul d-voastra). Cititi tot ce pare a fi interesant.
10. (*2). Sa se scrie o functie ce inverseaza un tablou bidimensional.
11. (*2). Sa se scrie un program care citeste din cin si scrie caracterele in
cout codificat. Codul lui c poate fi c^keyaii, unde key este un sir pasat ca
argument al liniei de comanda. Programul utilizeaza caracterele din key intr-o
maniera ciclica pina cind au fost citite toate caracterele de la intrare. Recodificarea
textului cu aceeasi cheie produce textul original. Daca nu exista nici o cheie
(s-a pasat sirul vid), atunci nu se face nici o codificare.
12. (*3). Sa se scrie un program care ajuta la descifrarea mesajelor codificate
cu metoda descrisa mai sus fara a cunoaste cheia. A se consulta David Kahn:
The code-breakers, Macmillan, 1967, New York, pp 207-213.
13. (*3). Sa se scrie o functie error care are un format asemanator cu printf,
continind %s, %c si %d si un numar arbitrar de argumente. Sa nu se foloseasca
printf(). A se consulta &8.2.4 daca nu se cunoaste sensul lui %s etc. Sa
se utilizeze <stdargs.h>.
14. (*1). Cum am alege nume pentru tipuri de pointeri spre functii definite
prin typedef?
15. (*2). Analizati niste programe pentru a avea o idee despre diversitatea
stilurilor numelor utilizate in realitate. Cum se utilizeaza literele mari?
Cum se utilizeaza sublinierea? Cind se utilizeaza nume scurte ca x si i?
16. (*1). Ce este gresit in macrodefinitiile de mai jos?
#define PI = 3.141593;
#define MAX(a, b) a > B ? a : b
#define fac(a) (a) * fac((a) - 1)
17. (*3). Sa se scrie un macroprocesor care defineste si expandeaza macrouri
simple (asa cum face macroprocesorul C). Sa citeasca din cin si sa scrie in
cout. La inceput sa nu se incerce sa se trateze macrouri cu argumente. Calculatorul
de birou (&3.1) contine o tabela de simboluri si un analizor lexical pe
care noi l-am putea modifica.