Biologie | Chimie | Didactica | Fizica | Geografie | Informatica | |
Istorie | Literatura | Matematica | Psihologie |
FUNCTII SI FISIERE
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.
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.
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.
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()
// file2.c:
extern int a;
int f();
void g()
'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()
// file2.c:
int a;
int g()
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()
// file2.c
static int a = 7;
static int f()
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()
struct s;
// file2.c
const a = 7;
inline int f()
struct 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).
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 point; |
|Declaratii de functii extern int strlen; |
|Definitii de functii inline inline char get(); |
|Declaratii de date extern int a; |
|Definitii de constante const float pi = 3.141593; |
|Enumerari enum bool ; |
|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() |
|Definitii de date int a; |
|Definitii de agregate constante const tbl[] = ; |
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 &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.
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
;
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_string[256];
extern double expr();
extern double term();
extern double prim();
struct name;
extern name* look(char* p, int ins = 0);
inline name* insert(char* 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_string[256];
token_value get_token()
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()
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()
double term()
double expr()
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 table[TBLSZ];
name* look(char* p, int ins)
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)
extern int strlen(const char*);
main(int argc, char* argv[])
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.
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)
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 name;
extern name* look(char* p, int ins = 0);
inline name* insert(char* s)
//table.c : definitiile tabelei de simboluri
#include 'error.h'
#include <string.h>
#include 'table.h'
const TBLSZ = 23;
name* table[TBLSZ];
name* look(char* p, int ins)
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_value;
extern token_value curr_tok;
extern double number_value;
extern char name_string[256];
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_string[256];
token_value get_token()
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()
double term()
double expr()
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* argv[])
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.
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)
//file2.c : 'extern' in fisierul.c
extern int a;
extern const c;
extern f(int);
int g()
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* table[TBLSZ];
name* look(char* p, int ins)
Aceasta va asigura ca toate accesele la table sa se
faca prin look(). Nu este necesar sa se 'ascunda'
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).
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.
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.
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.
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
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.
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 (&6.5), o facilitate pentru transferul neverificat al argumentelor (&6.8) si o facilitate pentru specificarea argumentelor implicite (&6.6). Consideram:
void f(int val, int& ref)
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)
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).
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)
Pot fi mai multe instructiuni return intr-o functie:
int fact(int n)
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()
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()
Din fericire, compilatorul avertizeaza asupra unor astfel de valori returnate. Iata un alt exemplu:
int& f()
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()
Cu alte cuvinte, un argument de tip T[] 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 vec;
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* day[] = ;
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 m[3][4])
}
Cazul dificil apare cind trebuie pasate ambele dimensiuni. 'Solutia evidenta' pur si simplu nu functioneaza:
void print_mij(int m[][], int dim1, int dim2) //eroare
}
In primul rind, argumentul m[][] este ilegal deoarece trebuie sa fie cunoscuta dimensiunea a doua a tabloului pentru a gasi locatia unui element.
In al doilea rind, expresia m[i][j] 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)
}
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; v[i*dim2+j];
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
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:
[1] Cauta o corespondenta exacta si daca exista se utilizeaza functia respectiva;
[2] Cauta o corespondenta utilizind conversii predefinite si utilizeaza o functie gasita in acest fel;
[3] 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()
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)
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.
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, wordn');
printf('My name is %s %sn', first_name, second_name);
printf('%d + %d = %dn', 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 %sn', 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* argv[])
}
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
va_end(ap); //curatirea argumentelor cerr << 'n'; if(n)
exit(n);
}
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.
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) void (*efct)(char*); //pointer spre functie void f()
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()
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_ops[]=; //op. de editare
PF file_ops[]=;//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: (*button2[3])();
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.
*/
}
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 user;
typedef user* Puser;
user heads[]=;
void print_id(Puser v, int n)
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
int cmp2(char* p, char* q) //se compara numerele dept
Programul acesta sorteaza si imprima:
main()
Este posibil sa se ia adresa unei functii inline si de asemenea sa se ia adresa unei functii supraincarcate (&r8.9).
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
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()
}
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).
Exercitii
(*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.
(*1). Ce semnifica linia de mai jos? La ce ar fi buna ea? typedef int(rifii&)(int, int);
(*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.
(*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.
(*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.
(*2). Sa se implementeze sort() (&6.9) utilizind un algoritm de sortare mai eficient.
(*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.
(*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.
(*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.
(*2). Sa se scrie o functie ce inverseaza un tablou bidimensional.
(*2). Sa se scrie un program care citeste din cin si scrie caracterele in cout codificat. Codul lui c poate fi c^key[i], 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.
(*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.
(*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>.
(*1). Cum am alege nume pentru tipuri de pointeri spre functii definite prin typedef?
(*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?
(*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)
(*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.
Politica de confidentialitate |
Copyright © 2024 - Toate drepturile rezervate