Biologie | Chimie | Didactica | Fizica | Geografie | Informatica | |
Istorie | Literatura | Matematica | Psihologie |
DECLARATII SI CONSTANTE
Declaratii
Exemple de declaratii:
char ch;
int count = 1;
char* name = 'Bjarne';
struct complex complex cvar; extern complex sqrt(complex); extern int error_number; typedef complex point; float real(complex* p); const double pi = 3.1415926535897932385; struct user;
Majoritatea acestor declaratii sint de asemenea si definitii; adica ele definesc o entitate pentru numele la care se refera. Pentru ch, count si cvar, aceasta entitate este o cantitate corespunzatoare de memorie care sa se utilizeze ca o variabila. Pentru real, entitatea este o functie specifica.
Pentru
printr-o anumita alta declaratie, memoria pentru variabila error_number de tip intreg trebuie sa fie alocata printr-o anumita alta declaratie a lui error_number, iar o anumita alta declaratie a tipului user trebuie sa defineasca cum arata acel tip. Trebuie totdeauna sa fie exact o definitie pentru fiecare nume dintr-un program C++, dar pot fi multe declaratii si toate declaratiile trebuie sa fie compatibile cu tipul entitatii referite, asa ca fragmentul de mai jos are doua erori:
int count;
int count; // error : redefinition
extern int error_number;
extern short error_number; // error : type mismatch
Anumite definitii specifica o 'valoare' pentru entitatile pe care le definesc ele:
struct complex; typedef complex point; float real(complex* p); const double pi=3.1415926535897932385;
Pentru tipuri, functii si constante 'valoarea' este permanenta. Pentru tipuri de date neconstante valoarea initiala poate fi schimbata ulterior:
int count = 1;
char* name = 'Bjarne';
//.
count = 2;
name = 'Marian';
Numai definitia
char ch;
nu specifica o valoare. Orice declaratie ce specifica o valoare este o definitie.
Domeniu
O declaratie introduce un nume intr-un domeniu; adica un nume poate fi utilizat numai intr-o parte specifica a textului programului. Pentru un nume declarat intr-o functie (adesea numit nume local), domeniul lui se extinde din punctul declaratiei pina la sfirsitul blocului in care apare declaratia lui. Pentru un nume care nu este intr-o functie sau intr-o clasa (adesea numit nume global), domeniul se extinde din punctul declaratiei pina la sfirsitul fisierului in care apare declaratia lui. Un nume poate fi redefinit intr-un bloc pentru a referi o entitate diferita in blocul respectiv. Dupa iesirea din bloc numele isi reia intelesul lui precedent. De exemplu:
int x; //global x
f()
x = 3; //asignarea la primul local x
}
int* p = &x; //ia adresa globalului x
Ascunderea numelor este inevitabila cind se scriu programe mari. Totusi, un cititor poate usor sa nu observe ca un nume a fost ascuns si erorile datorate acestui fapt sint foarte dificil de gasit. In consecinta ar trebui minimizat numarul numelor ascunse. Utilizarea numelor de felul lui i si x pentru variabile globale sau locale in functii mari poate sa ne conduca la erori.
Este posibil sa se utilizeze un nume global ascuns utilizind operatorul de rezolutie a domeniului '::'. De exemplu:
int x; f()
Nu exista un mod de a utiliza un nume local ascuns. Domeniul unui nume incepe in punctul declaratiei lui; aceasta inseamna ca un nume poate fi utilizat chiar pentru a specifica valoarea lui initiala. De exemplu:
int x;
f()
Aceasta este legal dar este fara sens. Este posibil sa utilizam un singur nume pentru a ne referi la doua obiecte diferite intr-un bloc fara a utiliza operatorul '::'. De exemplu:
int x = 11;
f()
Variabila y este initializata cu 11, valoarea globalului x, iar apoi i se atribuie valoarea 22 a variabilei locale x. Numele argumentelor unei functii se considera declarate in blocul cel mai exterior functiei, deci
f(int x)
eroare, deoarece x este declarat de doua ori in acelasi domeniu.
Obiecte si Lvalori
Se pot aloca si utiliza 'variabile' ce nu au nume si este posibil sa se faca atribuiri la expresii care arata straniu (de exemplu *p[a+10]=7). In consecinta, exista nevoia de nume pentru 'ceva aflat in memorie'. Iata ghilimelele corespunzatoare pentru a face referire la manualul C++: 'Un obiect este o regiune de memorie; o lvaloare este o expresie care refera un obiect' (&r.5). Cuvintul lvalue original a fost desemnat pentru a insemna 'ceva ce poate fi in partea stinga a unei atribuiri'. Totusi, nu orice lvalue poate fi utilizata in partea stinga a unei atribuiri; se poate avea o lvaloare care
face referire la o
Durata de viata
Daca programatorul nu specifica altfel, un obiect este creat cind definitia lui este intilnita si distrus cind numele lui iese afara din domeniu. Obiectele cu nume globale se creeaza si se initializeaza numai odata si 'traiesc' pina cind se termina programul. Obiectele definite printr-o declaratie cu cuvintul cheie static se comporta la fel. De exemplu, (directiva #include <stream.h> a fost lasata afara din exemplele din acest capitol pentru a face economie de spatiu. Este necesar sa fie prezenta pentru exemplele care produc iesiri):
int a = 1;
void f()
main()
produce iesirea:
a = 1 b = 1 c = 1
a = 2 b = 1 c = 2
a = 3 b = 1 c = 3
O variabila statica care nu este explicit initializata este initializata cu zero (&4.5).
Utilizind operatorii new si delete, programatorul poate crea obiecte a caror durata de viata poate fi controlata direct (&3.4).
Nume
Un nume (identificator) consta dintr-un sir de litere si cifre. Primul caracter trebuie sa fie litera. Caracterul subliniere _ se considera a fi o litera. C++ nu impune limite asupra numarului de caractere dintr-un nume, dar anumite implementari nu sint sub controlul scriitorului de compilatoare (in particular, incarcatorul). Anumite medii de executie sint de asemenea necesare pentru a extinde sau restringe setul de caractere acceptat intr-un identificator; extensiile (de exemplu, cele care admit caracterul $ intr-un nume) produc programe neportabile. Un cuvint cheie C++ (vezi &r.3) nu poate fi utilizat ca un nume. Exemple de nume:
hello this_is_a_most_unusually_long_name
DEFINED fo0 bAr u_name HorseSence
var0 var1 CLASS _class ___
Exemple de siruri de caractere care nu pot fi utilizate ca identificatori:
012 a fool $sys class 3var
pay.dul foo-bar .name if
Literele mari si mici sint distincte, asa ca Count si count sint nume diferite, dar nu este indicat sa se aleaga nume care difera numai putin unul de altul. Numele care incep cu subliniere se utilizeaza de obicei pentru facilitati in mediul de executie si de aceea nu se recomanda sa se utilizeze astfel de nume in programele aplicative.
Cind compilatorul citeste un program, el totdeauna cauta cel mai lung sir care poate forma un sir, asa ca var10 este un singur nume si nu numele var urmat de numarul 10, iar elseif un singur nume, nu cuvintul cheie else urmat de if.
Tipuri
Orice nume (identificator) dintr-un program C++ are un tip asociat cu el. Acest tip determina ce operatii pot fi aplicate asupra numelui (adica la entitatea referita prin nume) si cum se interpreteaza aceste operatii. De exemplu:
int error_number;
float real(complex* p);
Intrucit error_number este declarat sa fie int, lui i se pot face atribuiri, poate fi folosit in expresii aritmetice, etc..
Functia real, pe de alta parte, poate fi aplicata cu adresa unui complex ca parametru al ei. Este posibil sa se ia adresa oricaruia din ei. Anumite nume, cum ar fi int si complex, sint nume de tipuri. Un nume de tip este utilizat pentru a specifica tipul unui alt nume intr-o declaratie. Singura alta operatie asupra unui nume de tip este sizeof (pentru a determina cantitatea de memorie necesara pentru a pastra un obiect de acel tip) si new (pentru alocare de memorie libera pentru obiectele de tipul respectiv). De exemplu:
main()
Un nume de tip poate fi utilizat ca sa specifice explicit conversia de la un tip la altul (&3.4). De exemplu:
float f;
char* p;
long ll = long(p); // converteste p spre long
int i = int(f); // converteste f spre int
Tipuri fundamentale
C++ are un set de tipuri fundamentale ce corespund la cele mai comune unitati de memorie ale calculatoarelor si la cele mai fundamentale moduri de utilizare ale lor.
char
short int
int
long int , pentru a reprezenta intregi de diferite dimensiuni;
float
double ,pentru a reprezenta numere in flotanta;
unsigned char
unsigned short int
unsigned int
unsigned long int ,pentru a reprezenta intregi fara semn, valori
logice, vectori de biti, etc..
Pentru o notatie mai compacta, int poate fi eliminat dintr-o combinatie de multicuvinte (de exemplu short este de fapt short int) fara a schimba intelesul; astfel long inseamna long int iar unsigned inseamna unsigned int. In general, cind un tip este omis intr-o declaratie, se presupune ca s-a omis int. De exemplu:
const a = 1;
static x;
fiecare defineste un obiect de tip int.
Intregul de tip caracter este cel mai potrivit pentru a mentine si manipula caractere pe un calculator dat; acest tip este de obicei pe 8 biti. Dimensiunile obiectelor din C++ sint exprimate in termeni multipli ai dimensiunii lui char, asa ca, prin definitie sizeof(char) = 1. Depinzind de hardware, un char este un intreg cu sau fara semn. Tipul caracter fara semn este sigur totdeauna fara semn (unsigned char) si utilizindu-l produce programe mai portabile, dar poate sa fie mai putin eficient decit daca este
folosit ca tip char obisnuit. Motivul pentru a funiza mai multe tipuri de intregi, mai multe tipuri de intregi fara semn si mai multe tipuri de flotante este pentru a permite
programatorului sa utilizeze avantajos caracteristicile hardware. Pe multe masini exista diferente semnificative in cerintele de memorie, timpul de acces la memorie si viteza de calcul dintre diferite varietati a tipurilor fundamentale. Cunoscind o masina, de obicei este usor a alege, de exemplu, tipul de intreg potrivit pentru o variabila particulara. A scrie cod de nivel inferior portabil cu adevarat este foarte greu. Ceea ce este garantat in legatura cu dimensiunile tipurilor fundamentale este:
sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long)
sizeof(float) <= sizeof(double)
Cu toate acestea, de obicei este rezonabil sa presupunem ca tipul char poate pastra intregi in intervalul 0..127 (el poate totdeauna pastra un caracter din setul de caractere al masinii), ca un short si un int au cel putin 16 biti, ca un int este apropiat de o dimensiune potrivita pentru aritmetica intregilor si ca un long are cel putin 24 de biti. A presupune mai mult este hazardos si chiar aceste reguli implicite nu se aplica universal (o tabela de caracteristici hardware pentru citeva masini se poate vedea in
&r.6).
Tipurile de intregi fara semn sint ideale pentru a utiliza memoria ca un vector pe biti. Utilizarea unui intreg fara semn in locul unui int pentru a cistiga un bit in plus pentru a reprezenta intregi pozitivi aproape totdeauna nu este o idee buna. Incercarea de a ne asigura ca anumite valori sint pozitive prin declararea variabilelor de tip unsigned va fi ignorata prin reguli implicite de conversie. De exemplu:
unsigned surprise = -1;
este legal (dar compilatorul va face un avertisment despre el).
Conversie implicita de tip
Tipurile fundamentale pot fi amestecate liber in expresii. Oricind este posibil, valorile se convertesc asa ca sa nu se piarda informatie (regula exacta poata fi gasita in &r.6.6).
Exista cazuri in care informatia se poate pierde sau chiar distruge. Atribuirea unei valori de un tip la o variabila de un alt tip cu biti mai putini in reprezentarea ei este in mod necesar o sursa potentiala de erori. De exemplu, sa presupunem ca secventa urmatoare se executa pe o masina in care intregii se reprezinta in complement fata de doi si caracterele pe 8 biti:
int i1 = 256 +255;
char ch = i1; //ch == 255
int i2 = ch; //i2 == ?
Un bit (cel mai semnificativ) este pierdut in atribuirea ch = i1 si ch va pastra toti bitii 1 (adica 8 biti de 1); deci nu exista o cale ca acesta sa poata deveni 511 cind se atribuie lui i2! Dar care ar putea fi valoarea lui i2 ? Pe VAX, unde un caracter este cu semn, raspunsul este 255. C++ nu are un mecanism la executie care sa detecteze un astfel de tip de problema, iar detectarea la compilare este prea dificila in general, asa ca programatorul trebuie sa fie atent la acest fapt.
Tipuri derivate
----- ----- -----
Din tipurile fundamentale (si din tipurile definite de utilizator) se pot deriva alte tipuri folosind operatorii de declaratie:
pointer
& adresa
[] vector
() functie
si mecanismul de definitie de structura. De exemplu:
int* a;
float v[10];
char* p[20]; //vector de 20 de pointeri spre caractere
void f(int);
struct str;
Regulile de compunere a tipurilor utilizind acesti operatori se explica in detaliu in &r8.3.4. Ideea de baza este ca declararea unui tip derivat oglindeste utilizarea lui. De exemplu:
int v[10]; //declara un vector
i = v[3]; //utilizeaza un element al vectorului
int* p; //declaratie de pointer
i = *p; //utilizeaza obiectul spre care se pointeaza
Toate problemele in intelegerea notatiei pentru tipuri derivate apar din cauza faptului ca * si & sint operatori prefix iar [] si () sint postfix, asa ca parantezele trebuie sa fie utilizate pentru a exprima tipuri in care precedenta operatorilor este incomoda. De exemplu deoarece [] are o prioritate mai mare decit *:
int *v[10]; //vectori de pointeri
int (*p)[10] //pointer spre vector
Poate fi plicticos sa utilizam o declaratie pentru fiecare nume pe care vrem sa-l introducem intr-un program, mai ales daca tipurile lor sint identice. Este posibil sa declaram diferite nume intr-o singura declaratie; in locul unui singur nume, declaratia pur si simplu contine o lista de nume separate prin virgula. De exemplu, se pot declara doi intregi astfel:
int x, y; //int x; int y;
Cind declaram tipuri derivate, trebuie sa observam ca operatorii se aplica numai la nume individuale (si nu la orice alte nume din aceeasi declaratie). De exemplu:
int* p, y; //int *p; int y; nu int *y;
int x, *p; //int x; int *p;
int v[10], *p; //int v[10]; int *p;
Opinia autorului este ca astfel de constructii fac un program mai putin lizibil si ar trebui eliminate.
Void
Tipul void se comporta sintactic ca un tip fundamental. El poate totusi, sa fie utilizat numai ca parte a unui tip derivat; nu exista obiecte de tip void. Este folosit pentru a specifica ca o functie nu returneaza o valoare sau ca tip de baza pentru pointeri spre obiecte de tip necunoscut.
void f(); //f nu returneaza o valoare
void* pv; //pointer spre un obiect de tip necunoscut
Un pointer spre orice tip poate fi atribuit la o variabila de tip void*.
Pentru inceput acesta nu pare prea util, deoarece un pointer void* nu poate fi indirectat dar aceasta restrictie este exact ceea ce face ca tipul void* sa fie util. El se utilizeaza in primul rind pentru a transfera la functii pointeri despre care nu se poate face presupunere asupra tipului obiectului spre care pointeza si pentru a returna obiecte fara tip dintr-o functie.
Pentru a utiliza un astfel de obiect, trebuie sa se utilizeze conversia explicita de tip. Astfel de functii de obicei exista la cel mai inferior nivel al sistemului unde se manipuleaza resurse hardware reale. De exemplu:
void* allocate(int size); void deallocate(void*); f()
Pointeri
Pentru cele mai multe tipuri T, T* este tipul pointer spre T. Adica o variabila de tipul T* poate pastra adresa unui obiect de tipul T. Pentru pointeri spre vectori si pointeri spre functii exista notatii mai complicate:
int* pi;
char** cpp; //pointer spre pointer spre caractere
int (*vp)[10] //pointer spre vector de 10 elemente
int (*fp)(char,char*) //pointer spre o functie care are ca parametru (char, char*) si //returneaza un int
Operatia fundamentala asupra unui pointer este indirectarea, adica referirea la un obiect pointat printr-un pointer spre el. Operatorul de indirectare este unarul * (prefixat). De exemplu:
char c1 = 'a';
char* p = &c1; //p pastreaza adresa lui c1
char c2 = *p; //c2 = 'a'
Variabila spre care pointeaza p este c1 si valoarea pastrata in c1 este 'a', asa ca valoarea lui *p atribuita lui c2 este 'a'.
Este posibil sa se faca unele operatii aritmetice cu pointerii. Iata de exemplu o functie care calculeaza numarul de caractere dintr-un sir (nesocotind 0 care termina sirul):
int strlen(char* p)
Un alt mod de a gasi lungimea este ca la inceput sa gasim sfirsitul sirului si apoi sa scadem adresa inceputului sirului din adresa sfirsitului:
int strlen(char* p)
Pointerii spre functii pot fi extrem de utili; ei se discuta in (&4.6.7).
Vectori
Pentru un tip T, T[size] este tipul 'vector de size elemente de tip T'.
Elementele sint indexate de la 0 la size-1. De exemplu:
float v[3]; // un vector de 3 flotante: v[0],v[1],v[2]
int a[2][5]; // doi vectori de 5 intregi
char* vpc[32]; // vectori de 32 de pointeri spre caractere
Un ciclu pentru a scrie valori intregi pentru caracterele mici ar putea fi scris astfel:
extern int strlen(char*); char alpha[] = 'abcdefghijklmnopqrstuvwxyz'; main()
}
Functia chr() returneaza reprezentarea sub forma de caracter a unui intreg mic; de exemplu, chr(80) este 'P' pe o masina care utilizeaza setul de caractere ASCII. Functia oct() produce o reprezentare octala a argumentului sau intreg, iar hex() produce o reprezentare hexazecimala a argumentului sau intreg; chr(), oct() si hex() sint declarate in <stream.h>. Functia strlen() a fost utilizata pentru a numara caracterele din alpha (vezi &4.4). Cind se utilizeaza setul de caractere ASCII, iesirea va arata astfel:
'a' = 97 = 0141 = 0x61
'b' = 98 = 0142 = 0x62
'c' = 99 = 0143 = 0x63
Sa observam ca nu este necesar sa se specifice dimensiunea vectorului alpha; compilatorul calculeaza numarul de caractere din sirul de caractere specificat ca initializator. Utilizind un sir ca un initializator pentru un vector de caractere este convenabil, dar din nefericire este unica utilizare a sirurilor. Nu exista o atribuire similara a unui sir la un vector. De exemplu:
char v[9];
v = 'a string'; // error
este o eroare deoarece atribuirea nu este definita pentru vectori.
Evident sirurile sint potrivite numai pentru a initializa vectori de caractere; pentru alte tipuri trebuie sa se utilizeze o notatie mai laborioasa. Aceasta notatie poate fi de asemenea utilizata pentru vectori de caractere. De exemplu:
int v1[] = ;
int v2[] = ;
char v3[] = ;
char v4[] = ;
Observam ca v4 este un vector de 4 (nu 5) caractere; nu este terminat printr-un zero, asa cum cer prin conventie toate rutinele de biblioteca. Aceasta notatie este de asemenea restrin sa la obiecte statice. Tablourile multidimensionale sint reprezentate ca vectori de vectori si notind cu virgula pentru a separa limitele ca in alte limbaje de programare se obtine la compilare o eroare deoarece virgula (,) este un operator de
succesiune (vezi &3.2). De exemplu, sa incercam:
int bad[5,2]; // error
int v[5][2]; //correct
int bad = v[5,2]; // error
int good = v[4][1]; // correct
O declaratie char v[2][5]; declara un vector cu doua elemente; fiecare din ele este un vector de tip char [5]. In exemplul urmator, primul din acei vectori este initializat cu primele 5 litere iar cel de al doilea cu primele 5 cifre:
char v[2][5] = ;
main()
}
va produce:
v[0][0]=a v[0][1]=b v[0][2]=c v[0][3]=d v[0][4]=e
v[1][0]=0 v[1][1]=1 v[1][2]=2 v[1][3]=3 v[1][4]=4
Pointeri si Vectori
In C++, pointerii si vectorii sint foarte strinsi legati. Numele unui vector poate de asemenea, sa fie utilizat ca un pointer spre primul sau element, asa ca exemplul cu alfabetul ar putea fi scris astfel:
char alpha[] = 'abcdefghijklmnopqrstuvwxyz'; char* p = alpha; char ch; while(ch = *p++);
cout << chr(ch) << '=' << ch
<< '=0' << oct(ch) << 'n';
Declaratia lui p ar putea de asemenea sa fie scrisa:
char* p = &alpha[0];
Aceasta echivalenta este utilizata extensiv in apelurile de functii, in care un argument vector este totdeauna pasat ca un pointer la primul element al vectorului; astfel in acest exemplu:
extern int strlen(char*); char v[] = 'Annemarie'; char* p = v; strlen(p); strlen(v);
este transferata aceeasi valoare la strlen in ambele apeluri.
Rezultatul aplicarii operatorilor +, -, ++, -- la pointeri depinde de tipul obiectului spre care pointeaza pointerul. Cind un operator aritmetic se aplica la un pointer spre un tip T, p este presupus ca pointeaza spre un element al vectorului de obiecte de tip T; p+1 inseamna elementul urmator al acelui vector iar p-1 elementul precedent. Aceasta implica faptul ca valoarea lui p+1 va fi cu sizeof(T) mai mare decit valoarea lui p. De exemplu:
main()
va produce:
char* 1
int* 4
deoarece caracterele ocupa un octet fiecare si intregii ocupa fiecare 4 octeti pe masina mea. Valorile pointer au fost convertite spre long inainte de a face scaderea utilizind conversia explicita de tip (&3.5). Ele au fost convertite spre long si nu spre tipul int deoarece exista masini pe care un pointer nu incape intr-un int (adica sizeof(int) < sizeof(char*)).
Scaderea de pointeri este definita numai cind ambii pointeri pointeaza spre elemente ale aceluiasi vector (desi limbajul nu are un mod de a se asigura ca acest lucru este adevarat). Cind se scade un pointer dintr-un altul, rezultatul este numarul de elemente al vectorului dintre cei doi pointeri (un intreg). Se poate adauga un intreg la un pointer sau scadea un intreg dintr-un pointer; in ambele cazuri rezultatul este o valoare pointer. Daca acea valoare nu pointeaza spre un element al aceluiasi vector, ca si vectorul initial, rezultatul utilizarii valorii respective este nedefinit. De exemplu:
int v1[10];
int v2[10];
int i = &v1[5] - &v1[3]; // 2
i = &v1[5] - &v2[3]; // rezultat nedefinit
int* p = v2 + 2; // p==&v2[2]
p = v2 - 2; // *p nedefinit
Structuri
Un vector este un agregat de elemente de un acelasi tip; o structura este un agregat de elemente de orice tip. De exemplu:
struct address;
defineste un tip nou numit address care consta din elementele de care avem nevoie pentru a trimite o scrisoare la cineva (address nu este in general destul pentru a gestiona toate scrisorile, dar este suficient pentru un exemplu). Sa observam punct-virgula de la sfirsit este unul din foarte putinele locuri din C++ unde este necesar sa o avem dupa o acolada inchisa, asa ca lumea este inclinata sa o uite.
Variabilele de tip adresa pot fi declarate exact ca si alte variabile, iar elementele individuale pot fi accesate utilizind operatorul '.'(punct). De exemplu:
address jd;
jd.name = 'Jim Dandy';
jd.number = 61;
Notatia utilizata pentru initializarea vectorilor poate de asemenea sa fie utilizata pentru variabile de tip structura. De exemplu:
address jd = ,7974};
Utilizind un constructor (&5.4) este de obicei mai bine. Sa observam ca jd.state nu poate fi initializat prin sirul 'NJ'. Sirurile sint terminate prin caracterul '0' asa ca 'NJ' are trei caractere, adica unul in plus decit ar incapea in jd.state.
Obiectele structura sint adesea accesate prin pointeri folosind operatorul ->. De exemplu:
void print_addr(address* p)
Obiectele de tip structura pot fi atribuite, pasate ca si argumente la functie si returnate ca rezultat al unei functii. De exemplu:
address current; address set_current(address next)
Alte operatii plauzibile cum ar fi compararea (== si !=) nu sint definite. Cu toate acestea, utilizatorul poate defini astfel de operatori (vezi cap. 6).
Nu este posibil sa se calculeze dimensiunea unui obiect de tip structura pur si simplu insumind membri ei. Motivul pentru aceasta este ca multe masini necesita ca obiecte de un anumit tip sa fie alocate numai la anumite adrese (un exemplu tipic este faptul ca un intreg trebuie sa fie alocat la o adresa de cuvint) sau pur si simplu pentru a trata astfel de obiecte mult mai eficient. Aceasta conduce spre 'goluri' in structuri. De exemplu (pe masina mea): sizeof(address) este 24 si nu 22 cit ne-am astepta.
Sa observam ca numele unui tip devine disponibil pentru utilizare imediat dupa ce el a fost intilnit si nu numai dupa declararea completa. De exemplu:
struct link;
Nu este posibil sa se declare obiecte noi de tip structura pina cind nu s-a terminat complet declaratia, deci struct no_good;
este o eroare (compilatorul nu este in stare sa determine dimensiunea lui no_good). Pentru a permite ca doua (sau mai multe) tipuri structura sa se refere unul la altul, pur si simplu se admite ca sa se declare ca un nume este numele unui tip structura. De exemplu:
struct list; // to be defined later
struct link;
struct list;
Fara prima declaratie a lui list, declaratia lui link ar produce o eroare sintactica.
Echivalenta tipurilor
Doua tipuri structura sint diferite chiar daca ele au aceeasi membri. De exemplu:
struct s1; struct s2;
sint doua tipuri diferite, asa ca
s1 x;
s2 y = x; // error: type mismatch
Tipurile structura sint de asemenea diferite de tipurile fundamentale, asa ca:
s1 x;
int i = x; // error: type mismatch
Exista un mecanism pentru a declara un nume nou pentru un tip, fara a introduce un obiect nou. O declaratie prefixata prin cuvintul cheie typedef declara nu o noua variabila de un tip dat, ci un nume nou pentru tip. De exemplu:
typedef char* pchar; pchar p1,p2;
char* p3 = p1; Aceasta poate fi o prescurtare convenabila.
Referinte
O referinta este un nume pentru un obiect. Prima utilizare a referintelor este aceea de a specifica operatiile pentru tipuri definite de utilizator (ele se discuta in cap. 6). Ele pot fi de asemenea utile ca argumente de functii. Notatia X& inseamna referinta la X. De exemplu:
int i = 1;
int& r = i; // r si i acum se refera la acelasi obiect
int x = r; // x = 1
r = 2; // i = 2
O referinta trebuie sa fie utilizata (trebuie sa fie ceva pentru ce este el nume). Sa observam ca initializarea unei referinte este ceva cit se poate de diferit de atribuirea la ea. In ciuda aparentelor, nici un operator nu opereaza asupra unei referinte.
De exemplu:
int ii = 0;
int& rr = ii;
rr++; // ii se incrementeaza cu 1
este legal, dar r++ nu incrementeaza referinta rr; ++ se aplica la un int, care se intimpla sa fie ii. In consecinta, valoarea referintei nu poate fi schimbata dupa initializare; ea totdeauna se refera la obiectul cu care a fost initializata pentru a-l denumi. Pentru a primi un pointer spre obiectul notat prin referinta rr, se poate scrie &rr. Implementarea unei referinte este un pointer (constant) care este indirectat de fiecare data cind el este utilizat. Aceasta face initializarea unei referinte trivial cind initializatorul este o lvaloare (un obiect la care se poate lua adresa vezi &r5). Cu toate acestea, initializatorul pentru T& este necesar sa nu fie o lvaloare sau chiar de tip T. In astfel de cazuri:
[1] Intii, se aplica conversia de tip daca este necesar (vezi &r6.6.8 si &r8.5.6);
[2] Apoi valoarea rezultat este plasata intr-o variabila temporara;
[3] In final, adresa acestuia se utilizeaza ca valoare a initializatorului.
Consideram declaratia:
double& dr = 1;
Interpretarea acesteia este:
double* drp; // referinta reprezentata printr-un pointer
double temp; temp = double(1); drp = &temp;
O referinta poate fi utilizata pentru a implementa o functie care se presupune ca schimba valoarea argumentelor sale.
int x = 1;
void incr(int& aa)
incr(x); // x = 2;
Semantica transferului de argumente se defineste ca si pentru initializare, asa ca atunci cind este apelata functia de mai sus argumentul aa a lui incr() devine un alt nume pentru x. Cu toate acestea, pentru a avea un program mai lizibil este cel mai bine sa eliminam functiile care isi modifica argumentele. Este adesea preferabil sa se returneze o valoare dintr-o functie in mod explicit sau sa se returneze un pointer spre argument.
int x = 1;
int next(int p)
x = next(x); // x = 2
void inc(int* p)
inc(&x); // x = 3
Referintele pot fi de asemenea utilizate pentru a defini functii care pot fi utilizate atit in parttea stinga cit si in partea dreapta a unei atribuiri. Din nou, multe din cele mai interesante utilizari ale referintei se afla in proiectarea tipurilor netriviale definite de utilizator. Ca de exemplu, sa definim un tablou asociativ simplu. Intii noi definim struct pair prin:
struct pair;
Ideea de baza este ca un sir are o valoare intreaga asociata cu el. Este usor sa se defineasca o functie, find() care mentine o data structurata ce consta dintr-o pereche pentru fiecare sir diferit ce a fost prezentat. O implementare foarte simpla (dar ineficienta) ar fi urmatoarea:
const large = 1024; static pair vec[large+1]; pair* find(char* p)
/* mentinerea unui set de 'pair': se cauta p, se returneaza
'pair'-ul respectiv daca se gaseste, altfel se returneaza un 'pair' neutilizat */
Aceasta functie poate fi utilizata prin functia value() care implementeaza un tablou de intregi indexat prin siruri de caractere:
int& value(char* p)
return res_val;
}
Pentru un parametru sir dat, value() gaseste obiectul intreg respectiv (nu valoarea intregului corespunzator); ea returneaza o referinta la el.
Aceasta s-ar putea utiliza astfel:
const MAX = 256; //mai mare decit cel mai mare cuvint
main() //numara aparitiilor fiecarui cuvint de la intrare
Fiecare pas al ciclului citeste un cuvint de la intrarea standard cin in buf (vezi cap.8), iar apoi se pune la zi contorul asociat cu el prin find(). In final tabela rezultata de cuvinte diferite de la intrare, fiecare cu numarul sau de aparitii, este imprimat. De exemplu, dindu-se intrarea aa bb bb aa aa bb aa aa
programul va produce:
aa : 5
bb : 3
Este usor sa se perfectioneze aceasta intr-un tip de tablou asociativ propriu folosind o clasa cu operatorul de selectie [] (vezi &6.7).
Registrii
Pe orice arhitectura de masina obiectele (mici) pot fi accesate mai rapid cind se plaseaza intr-un registru. Ideal, compilatorul va determina strategia optima pentru a utiliza orice registru disponibil pe masina pe care se compileaza programul. Totusi, acest task nu este trivial, asa ca uneori este util ca programatorul sa dea compilatorului aceasta informatie. Aceasta se face declarind un obiect registru. De exemplu: register int i; register point cursor; register char* p; Declaratiile de registrii ar trebui utilizate numai cind eficienta este intr-adevar importanta. Declarind fiecare variabila ca variabila registru se va ingreuna textul programului si se poate chiar marii dimensiunea codului si timpul de executie (de obicei sint necesare instructiuni de a incarca un obiect si de a memora un obiect dintr-un registru). Nu este posibil sa se ia adresa unui nume declarat ca registru si nici nu poate fi global un astfel de
nume.
Constante
C++ furnizeaza o notatie pentru valorile de tipuri
fundamentale: constante caracter, constatante intregi si constante in virgula
flotanta. In plus, zero (0) poate fi utilizat ca si o
oricarei valori de orice tip i se poate da un nume si sa fie folosita ca o consatnta adaugind cuvintul cheie const la definitia ei;
un set de constante intregi poate fi definit ca o enumerare;
orice
nume de vector sau functie este o
Constante intregi
Constantele intregi pot fi de patru feluri: zecimale, octale, hexazecimale si constante caracter. Constantele zecimale sint cele mai frecvent utilizate si arata asa cum ne asteptam noi:
Tipul unei constante zecimale este int cu conditia ca ea sa incapa intr-un int, altfel ea este long. Compilatorul se cuvine sa avertizeze asupra constantelor care sint prea lungi ca sa fie reprezentate in calculator.
O
Exemple de constante octale:
Exemple de constante in hexazecimal:
0x0 0x2 0x38 0x53
Literele a, b, c, d, e si f sau echivalentele lor in litere mari se utilizeaza pentru a reprezenta 10, 11, 12, 13, 14 si respectiv 15. Notatiile octale si hexazecimale sint mai folosilositoare pentru a exprima structuri pe biti; utilizind aceste notatii pentru a exprima numere adevarate putem ajunge la surprize. De exemplu, pe o masina pe care un int se reprezinta ca un intreg prin complement fata de 2 pe 16 biti, intregul 0xffff este numarul negativ -1; daca s-ar folosi mai multi biti pentru a reprezenta un int, atunci acesta ar fi 65535.
Constante in flotanta
O
1.23 .23 0.23 1. 1.0 1.2e10 1.23e-15
Sa observam ca nu poate apare un spatiu in mijlocul unei constante flotante. De exemplu:
65.43 e-21 nu este o
65.43 e - 21 si va cauza eroare sintactica.
Daca noi dorim o
const float pi8 = 3.14159265;
Constante caracter
Desi C++ nu are un tip caracter separat pentru date,
ci mai degraba un tip intreg care poate pastra un caracter, trebuie sa avem o
notatie speciala si convenabila pentru caractere. O
'b' backspace
'f' formfeed
'n' newline
'r' cariage return
't' horizontal tab
'v' vertical tab
'' backslash
''' simple quote
''' double quote
'0' null, the integer value 0
Acestea sint caractere singulare in ciuda aparentei. Este posibil, de asemenea sa reprezentam un caracter printr-un numar octal de o cifra, doua sau trei ( urmat de cifre octale) sau de un numar hexazecimal de una, doua sau trei cifre(x urmat de cifre hexazecimale). De exemplu:
'6' 'x6' 6 ASCII ack
'60' 'x30' 48 ASCII '0'
'137' 'x05f' 95 ASCII '-'
Aceasta face posibil ca sa se reprezinte fiecare caracter din setul caracterelor masina si in particular pentru a include astfel de caractere in siruri de caractere (vezi sectiunea urmatoare). Utilizind o notatie numerica pentru un caracter, programul respectiv nu mai este portabil pentru masini cu seturi diferite de caractere.
Siruri
Un sir constant este o secventa de caractere inclusa intre ghilimele: 'this is a string'
Orice sir constant contine cu un caracter mai mult decit cele care apar in sir; ele toate se termina prin caracterul nul '0', cu valoarea 0. De exemplu:
sizeof('asdf')==5;
Tipul unui sir este 'vector de un numar corespunzator de caractere', asa ca 'asdf' este de tipul char[5]. Sirul vid se scrie '' (si are tipul char[1]). Sa observam ca pentru orice sir s, strlen(s) == sizeof(s) - 1 deoarece strlen() nu numara zeroul terminal. Conventia backslash pentru reprezentarea caracterelor negrafice pot de asemenea sa fie utilizate intr-un sir: aceasta face posibil sa se reprezinte ghilimelele si insusi caracterul backslash intr-un sir. Cel mai frecvent astfel de caracter este pe de parte caracterul 'n'. De exemplu:
cout << 'beep at end of message007n';
unde 7 este valoarea ASCII a caracterului bel.
Nu este posibil sa avem un caracter newline 'real' intr-un sir:
'this is not a string
but a syntax error'
cu toate acestea, un backslash urmat imediat de un newline poate apare intr-un sir: ambele vor fi ignorate. De exemplu:
cout << 'this is
ok'
va scrie
this is ok
Este posibil ca sa avem caracterul nul intr-un sir, dar majoritatea programelor nu vor suspecta ca dupa el mai sint caractere. De exemplu, sirul 'asdf000hjkl' va fi tratat ca 'asdf' prin functii standard cum ar fi strcpy() si strlen().
Cind se include o
char v1[]='ax0fah0129'; // 'a' 'xfa' 'h' '12' '9'
char v2[]='axfah129'; // 'a' 'xfa' 'h' '12' '9'
char v3[]='axfad127'; // 'a' 'xfad' '127'
Sa observam ca o notatie cu doua cifre hexazecimale nu este suficienta pe masini cu 9 biti pe byte.
Zero
Zero (0) poate fi folosit ca o
Nici un obiect nu este alocat cu adresa zero. Tipul lui zero va fi determinat de context. Toti bitii de o dimensiune potrivita sint zero.
Const
Cuvintul cheie const poate fi adaugat la declaratia
unui obiect pentru a face acel obiect o
const int model = 145;
const int v[] = ;
Deoarece la un astfel de obiect nu i se poate atribui o valoare, el trebuie sa fie initializat. Declarind ceva ca este constant ne asiguram ca valoarea lui nu va fi schimbata in domeniul lui:
model = 165; // error
model++; // error
Sa observam ca const modifica un tip; adica el
restringe modul in care un obiect poate fi utilizat, in loc sa specifice cum se
aloca
const char* peek(int i)
O functie de aceasta forma ar putea fi utilizata pentru a permite cuiva sa citeasca un sir care nu poate fi alterat.
Cu toate acestea, un compilator poate avea avantaje
de pe urma unui obiect care este o
vectorului sint referite in expresii. Pe multe masini, totusi, o implementare eficienta poate fi atinsa chiar in acest caz plasind vectori de constante in memorii read_only.
Cind utilizam un pointer, sint implicate doua obiecte; pointerul insusi si obiectul spre care se face pointarea.
'Prefixind' o declaratie a unui pointer cu
const se construieste obiectul ca o
const
char* pc = 'asdf'; //
pointer spre o
pc[3] = 'a'; // eroare
pc = 'ghjk'; // ok
Pentru a declara ca pointerul insusi este o
char *const cp = 'asdf'; // pointer constant
cp[3] = 'a'; // ok
cp = 'ghjk'; // eroare
Pentru a face ca sa fie constante atit obiectele, cit si pointerul spre ele, trebuie ca ambele sa fie declarate ca si constante. De exemplu:
const char *const cpe = 'asdf'; // pointer constant spre
//
cpc[3] = 'a'; // eroare
cpc = 'ghjk'; // eroare
Un obiect care este o
char* strcpy(char* p,const char* q);//nu poate modifica pe *q
Se poate atribui adresa unei variabile la un pointer
spre o
int a = 1;
const c = 2;
const* p1 = &c; // ok
const* p2 = &a; // ok
int* p3 = &c; // eroare
*p3 = 7; // schimba valoarea lui
De obicei, daca tipul este omis intr-o declaratie, se alege int ca implicit.
Enumerari
O alta posibilitate pentru a defini constante intregi, care este adesea mai convenabil decit utilizind const, este enumerarea. De exemplu:
enum ;
defineste trei constante intregi, numite enumeratori si atribuie valori la acestia. Deoarece valorile enumerator sint atribuite crescator de la zero, aceasta este echivalent cu scrierea:
const ASM = 0;
const AUTO = 1;
const BREAK = 2;
O enumerare poate fi definita. De exemplu:
enum keyword ;
Numele enumerarii devine un sinonim pentru int, nu un nou tip. De exemplu:
keyword key; switch(key)
va conduce la un avertisment deoarece numai doua valori au fost tratate din cele trei.
Valorile pot fi de asemenea date explicit enumeratorilor. De exemplu:
enum int16 ;
Aceste valori nu este necesar sa fie distincte, crescatoare sau pozitive.
Salvarea spatiului
Cind programam aplicatii netriviale, invariabil vine vremea cind dorim mai mult spatiu de memorie decit este disponibil sau ne putem permite. Exista doua moduri de a obtine mai mult spatiu in afara de cel care este disponibil:
[1] Sa se puna mai mult de un obiect mic intr-un octet;
[2] Sa se utilizeze acelasi spatiu pentru a pastra diferite obiecte in momente diferite.
Prima metoda poate fi realizata folosind cimpurile, iar cea de a doua folosind reuniunile. Aceste constructii se descriu in sectiunile urmatoare. Deoarece utilizarea lor tipica este pentru a optimiza pur si simplu un program si deoarece ele sint adesea cele mai neportabile parti ale programului, programatorul trebuie sa gindeasca de doua ori inainte de a le utiliza. Adesea o conceptie mai buna este sa schimbe modul in care se gestioneaza datele; de exemplu, sa se insiste mai mult asupra memoriei alocate dinamic (&3.6) si mai putin asupra memoriei prealocate static.
Cimpuri
Se pare extravagant ca sa se utilizeze un caracter pentru a reprezenta o variabila binara, de exemplu un comutator on/off, dar tipul char este cel mai mic obiect care poate fi alocat independent in C++. Este posibil, totusi, sa se inmanuncheze impreuna diferite astfel de variabile foarte mici ca si cimpuri intr-o structura. Un membru se defineste a fi un cimp specificind numarul de biti pe care ii ocupa, dupa numele lui. Se admit si cimpuri nedenumite; ele nu afecteaza sensul cimpurilor denumite, dar pot fi utilizate pentru a face o aranjare mai buna insa dependenta de masina:
struct sreg;
Aceasta se intimpla sa fie aranjarea bitilor la registru de stare 0 la DEC PDP11/45. Un cimp trebuie sa fie de tip intreg si se utilizeaza ca alti intregi exceptind faptul ca nu este posibil sa se ia adresa unui cimp. In modulul kernel al unui sistem de operare sau in debugger, tipul sreg ar putea fi utilizat astfel:
sreg* sr0 = (sreg*)0777572;
//..
if(sr0->access) //access violation
Cu toate acestea, utilizind cimpuri pentru a putea impacheta diferite variabile intr-un singur octet nu neaparat se salveaza spatiu. Se salveaza spatiu la date, dar dimensiunea codului rezultat din manipularea acestor variabile se mareste pe majoritatea masinilor. Programele se stie ca se scurteaza semnificativ cind variabilele binare se convertesc de la cimpuri binare la caractere! Mai mult decit atit, de obicei este mai rapid sa se faca acces la char sau int decit pentru a face acces la un cimp.
Reuniuni
Sa consideram o tabela de simboluri in care o intrare pastreaza un nume si o valoare, iar valoarea este sau un sir sau un intreg:
struct entry;
void print_entry(entry* p)
}
Deoarece string_value si int_value nu pot fi utilizate in acelasi timp, evident se pierde spatiu. Se poate recupera usor specificind ca ambii ar trebui sa fie membri ai unei reuniuni, ca mai jos:
struct entry;
};
Aceasta lasa tot codul care foloseste pe entry neschimbat, dar asigura faptul ca atunci cind entry se aloca, string_value si int_value sa aiba aceeasi adresa. Aceasta implica, ca toti membri unei reuniuni sa aiba in comun acelasi spatiu care permite pastrarea celui mai mare membru.
Utilizind reuniunea in asa fel ca totdeauna sa folosim membrul care a fost pastrat in ea, se obtine o optimizare pura. Cu toate acestea, in programe mari, nu este usor sa se asigure ca o reuniune se utilizeaza numai in acest mod si se pot introduce erori subtile. Este posibil sa se incapsuleze o reuniune in asa fel incit corespondenta intre tipul cimp si tipurile membrilor unei reuniuni sa fie garantat ca este corecta (&5.4.6).
Reuniunile sint uneori utilizate pentru 'conversie de tip' (aceasta se face in principiu prin programe introdu-se in limbaj in afara facilitatilor de conversie a tipului, unde este necesar sa fie facuta). De exemplu, pe VAX acestea convertesc un int in int* pur si simplu prin echivalenta de biti.
struct fudge;
};
fudge a;
a.i = 4096;
int* p = a.p; //bad usage
Cu toate acestea, aceasta nu este o conversie reala; pe anumite masini un int si un int* nu ocupa acelasi spatiu, iar pe altele nici un intreg nu poate avea o adresa impara. O astfel de utilizare a unei reuniuni nu este portabila si exista un mod explicit si portabil de a specifica aceasta conversie (&3.5).
Reuniunile sint ocazional utilizate in mod deliberat pentru a elimina conversia de tip. Am putea, de exemplu, utiliza un fudge pentru a gasi reprezentarea pointerului 0:
fudge.p = 0;
int i = fudge.i; // i nu este necesar sa fie 0
Este de asemenea posibil sa se dea un nume unei reuniuni; adica ea formeaza un tip in adevaratul sens al lui. De exemplu, fudge ar putea fi declarata astfel:
union fudge;
si folosita exact ca inainte. Reuniunile numite au de asemenea, utilizari proprii (vezi &5.4.6).
Exercitii
(*1). Sa se execute programul 'Hello, world' (&1.1.1).
(*1). Pentru fiecare din declaratiile din (&1) sa se faca urmatoarele: daca o declaratie nu este o definitie, sa se scrie o definitie pentru ea. Daca o declaratie este o definitie, sa se scrie o declaratie pentru ea, care nu este de asemenea o definitie.
(*1). Sa se scrie declaratii pentru urmatoarele: un pointer spre un caracter; un vector de 10 intregi; o referinta spre un vector de 10 intregi; un pointer spre un vector de siruri de caractere; un pointer spre un pointer la un caracter; o constanta intreaga; un pointer spre o constanta intreaga; un pointer constant spre un intreg. Sa se initializeze fiecare din ei.
(*1.5). Sa se scrie un program care imprima dimensiunea tipurilor fundamentale si a pointerului. Sa se utilizeze operatorul sizeof.
(*1.5). Sa se scrie un program care imprima literele 'a'..'z' si cifrele '0'..'9' si valorile lor intregi. Sa se faca acelasi lucru pentru alte caractere imprimabile. Sa se faca acelasi lucru, dar utilizind notatia hexazecimala.
(*1). Sa se imprime bitii care se folosesc pentru a reprezenta pointerul 0 pe sistemul d-voastra (&5.2).
(*1.5). Sa se scrie o functie care imprima exponentul si mantisa unui parametru in dubla precizie.
(*2). Care sint valorile cele mai mari si cele mai mici pe sistemul d-voastra pentru tipurile urmatoare: char, short, int, long, float, double, unsigned, char*, int* si void* ? Exista mai multe restrictii asupra valorilor ? De exemplu, poate int* sa aiba o valoare impara ? Care este cadrajul obiectelor de acele tipuri ? De exemplu poate un int sa aiba o adresa impara ?
(*1). Care este cel mai lung nume local pe care il puteti utiliza intr-un program C++ pe sistemul d-voastra ? Care este cel mai lung nume extern pe care il puteti utiliza intr-un program C++ pe sistemul d-voastra ? Exista vreo restrictie asupra caracterelor pe care le puteti utiliza intr-un nume ?
(*2). Definiti pe unu astfel: const one = 1; Incercati sa schimbati valoarea lui one la doi. Definiti pe num prin: const num[] = ; Incercati sa schimbati valoarea lui num[1] la
(*1). Scrieti o functie care permuta doi intregi. Sa se utilizeze int* ca tip al argumentului. Scrieti o alta functie de permutare care utilizeaza int& ca tip de argument.
(*1). Care este dimensiunea vectorului str in exemplul urmator:
char str[] = 'a short string'; Care este lungimea sirului 'a short string'?
(*1.5). Sa se defineasca o tabela de nume continind numele fiecarei luni din an si numarul de zile din fiecare luna. Sa se scrie tabela. Sa se faca aceasta de doua ori: odata utilizind un vector pentru nume si un vector pentru numarul de zile si odata utilizind un vector de structuri, fiecare structura pastrind numele lunii si numarul de zile din ea.
(*1). Sa se utilizeze typedef pentru a defini tipurile: unsigned char, constant unsigned char, pointer spre intreg, pointer spre pointer spre char, pointer spre vector de caractere, vector de 7 pointeri intregi, pointer spre un vector de 7 pointeri intregi, vector de 8 vectori de 7 pointeri intregi.
Politica de confidentialitate |
Copyright © 2024 - Toate drepturile rezervate