18. Ierarhii de clase pentru operaţii de intrare/ieşire
18.1. Streamuri
În secţiunea 15.1 am prezentat nişte cunoştinţe de bază referitoare la ierarhiile de clase existente în C++ pentru realizarea operaţiilor de intrare/ieşire. Am văzut că nu există instrucţiuni de intrare/ieşire în limbajul C, şi nici în C++. În schimb în limbajul C s-au definit funcţii standard de bibliotecă, iar în C++ ierarhii de clase pentru operaţii de intrare/ieşire. În continuare ne vom ocupa de cele două ierarhii de clase definite în C++ în vederea efectuării operaţiilor de intrare/ieşire.
Operaţiile de intrare/ieşire sunt realizate de către cele două ierarhii de clase cu ajutorul noţiunii de stream. Printr-un stream vom înţelege un flux de date de la mulţimea datelor sursă (tastatură, fişier sau zonă de memorie) la mulţimea datelor destinaţie (monitor, fişier sau zonă de memorie). Cele două ierarhii de clase sunt declarate în fişierul iostream.h, deci acest fişier va trebui inclus de fiecare dată, când se lucrează cu ierarhiile de clase pentru intrare/ieşire. Prima ierarhie de clase este:
Figura 4. Ierarhia de clase cu rădăcina în streambuf
Clasa streambuf se poate folosi pentru gestionarea zonelor tampon şi pentru operaţii de intrare/ieşire simple. A doua ierarhie de clase este mai complicată. Prezentăm în continuare o parte a ei.
Figura 5. Ierarhia de clase cu rădăcina în ios
Legătura dintre cele două ierarhii de clase s-a realizat printr-o dată membru a clasei ios, care este un pointer către clasa streambuf. Clasa ios este clasă de bază virtuală atât pentru clasa istream, cât şi pentru ostream. Astfel elementele definite în clasa ios vor fi prezente numai într-un singur exemplar în clasa iostream.
Clasa istream realizează o conversie din caracterele unui obiect de tip streambuf, conform unui format specificat. Folosind clasa ostream se poate efectua o conversie conform unui format specificat, în caractere memorate într un obiect de tip streambuf, iar clasa iostream permite conversii în ambele direcţii.
Clasele istream_withassign, ostream_withassign şi iostream_withassign sunt clase derivate, având clasele de bază istream, ostream respectiv iostream. În plus operatorul de atribuire (=) este supraîncărcat în două moduri, de aceste clase.
Clasele derivate din clasa istream sau ostream se vor numi clase stream, iar obiectele claselor derivate din clasa ios se vor numi streamuri. Există următoarele patru streamuri standard definite în fişierul iostream.h:
StreamObiect al claseiCorespunde fişierului standardcinistream_withassignstdincoutostream_withassignstdoutcerrostream_withassignstderr (fără zone tampon)clogostream_withassignstderr (cu zone tampon)
Tabelul 3. Streamuri standard
În continuare ne vom ocupa de folosirea streamurilor standard, pentru realizarea operaţiilor de intrare/ieşire conform unui format specificat.
18.2. Ieşiri formatate
18.2.1. Operatorul de inserare
Operaţiile de scriere pe dispozitivul standard de ieşire, într-un fişier, sau într-o zonă de memorie se pot efectua cu ajutorul operatorului <<, care în acest caz se va numi operator de inserare.
Operandul din partea stângă al operatorului << trebuie să fie un obiect al clasei ostream (bineînţeles şi obiectele claselor derivate din clasa ostream se consideră obiecte ale clasei ostream). Pentru operaţiile de scriere pe dispozitivul standard de ieşire se va folosi obiectul cout.
Operandul din partea dreaptă al operatorului << poate fi o expresie. Pentru tipul corespunzător expresiei, trebuie să fie supraîncărcat operatorul <<. În cazul tipurilor standard operatorul de inserare este supraîncărcat cu o funcţie membru de forma:
ostream& operator << (nume_tip_standard);
Menţionăm că pentru tipurile abstracte programatorul poate supraîncărca operatorul de inserare. Să considerăm acum din nou exemplul prezentat în secţiunea 15.1, într-o formă mai detaliată, pentru a ilustra asemănarea cu utilizarea funcţiei printf. Fişierul stream1.cpp:
După executarea programului se obţine:
Observăm că rezultatul afişării şirurilor de caractere şi datelor de tip int, double şi char este acelaşi. În cazul afişării unui pointer, valoarea afişată este identică, dar formatul diferă (dacă se scrie cu printf se folosesc litere mari şi nu se afişează baza, iar în cazul scrierii cu cout se folosesc litere mici şi se afişează baza).
Deoarece operatorul << returnează o referinţă către clasa curentă, operatorul se poate aplica în mod înlănţuit. Acest lucru este prezentat în exemplul următor. Fişierul stream2.cpp:
În afară de faptul că operatorul de inserare se poate aplica în mod înlănţuit, se observă că evaluarea expresiilor afişate s-a făcut în ordine inversă comparativ cu afişarea. Numai în acest fel se poate explica faptul că valoarea afişată mai repede este deja incrementată, iar cea afişată mai târziu are valoarea iniţială. De fapt şi pentru funcţia printf este valabilă acelaşi lucru.
Ordinea de evaluare a expresiilor, care se afişează folosind operatorul de inserare în mod înlănţuit, este ilustrată mai explicit de următorul exemplu. Fişierul stream3.cpp:
După executarea programului obţinem:
Deci evaluarea funcţiei f2 s-a făcut înainte de evaluarea funcţiei f1.
18.2.2. Funcţia membru setf
În limbajul C funcţia printf ne permite afişarea datelor conform unui format specificat de programator. Acest lucru se poate realiza şi cu ajutorul ierarhiilor de clase definite în limbajul C++. În clasa ios s-a declarat o dată membru x_flags, care se referă la formatul cu care se vor efectua operaţiile de intrare/ieşire. Tot în clasa ios s-a definit un tip enumerare cu care se pot face referiri la biţii datei membru x_flags. Tipul enumerare este următorul:
class ios {
public:
...
enum {
skipws = 0x0001, // se face salt peste caractere albe la citire
left = 0x0002, // la scriere se cadrează la stânga
right = 0x0004, // la scriere se cadrează la dreapta
internal = 0x0008, // caracterele de umplere vor fi după
// semn, sau după bază
dec = 0x0010, // se face conversie în zecimal
oct = 0x0020, // se face conversie în octal
hex = 0x0040, // se face conversie în hexazecimal
showbase = 0x0080, // se afişează şi baza
showpoint = 0x0100, // apare şi punctul zecimal în cazul
// numerelor reale
uppercase = 0x0200, // în hexazecimal se afişează litere mari
showpos = 0x0400, // numerele întregi pozitive sunt afişate
// cu semnul în faţă
scientific= 0x0800, // afişare numere reale cu exponent
fixed = 0x1000, // afişare numere reale fără exponent
unitbuf = 0x2000, // se videază zonele tampon după scriere
stdio = 0x4000 // după scriere se videază stdout şi stderr
};
...
};
Data membru x_flags are o valoare implicită pentru fiecare tip standard. Astfel rezultatul afişării datelor care aparţin tipurilor standard, va fi în mod implicit identic cu rezultatul afişării cu funcţia printf, folosind specificatori de format care corespund tipurilor respective. Prezentăm această legătură între tipuri şi specificatori de format, în tabelul 4.
Dacă se modifică biţii corespunzători datei membru x_flags, atunci şi rezultatul afişării se va schimba, conform formatului specificat. Acest lucru se poate realiza cu ajutorul unor funcţii membru.
TipSpecificator de format corespunzătorint%dlong%ldunsigned%ulong unsigned%lufloat%gdouble%lglong double%Lgchar%cşir de caractere%s
Tabelul 4. Legătura dintre tipuri şi specificatori de format
Pentru a putea lucra mai uşor cu biţii corespunzători formatului, s-au determinat trei grupe ale biţilor datei membru x_flags. Fiecare grupă are un nume, care este de fapt numele unei constante statice de tip long declarate în clasa ios. Aceste grupe sunt:
-
adjustfield (right, left şi internal), pentru modul de cadrare;
-
basefield (dec, oct şi hex), pentru determinarea bazei;
-
floatfield (scientific şi fixed), pentru scrierea numerelor reale.
Fiecare grupă are proprietatea că numai un singur bit poate fi setat în cadrul grupei. Biţii datei membru x_flags pot fi setaţi cu ajutorul funcţiei membru setf al clasei ios. Funcţia membru setf are două forme:
long setf(long format);
şi
long setf(long setbit, long grupa);
Prima variantă a funcţiei membru setf setează biţii corespunzător parametrului de tip long: format. Dacă un bit din format este egal cu unu, atunci bitul corespunzător din x_flags va fi unu, iar dacă bitul din format este egal cu zero, atunci bitul corepunzător din x_flags rămâne neschimbat.
A doua variantă a funcţiei membru setf setează un bit din una dintre cele trei grupe adjustfield, basefield sau floatfield. Cu ajutorul parametrului setbit se determină bitul care se va seta. În locul unde se află bitul trebuie să fie unu, iar în rest zero. În parametrul al doilea trebuie specificat numele grupei. În acest caz se anulează biţii corespunzători grupei după care se setează biţii din setbit. Ambele variante ale funcţiei setf returnează valoarea datei membru x_flags înainte de modificare.
Referirea la biţii datei membru x_flags se face cu numele clasei ios, urmată de operatorul de rezoluţie şi numele bitului din tipul enumerare. Referirea la numele unei grupe se face în mod analog, înlocuid numele din tipul enumerare cu numele grupei. Utilizarea funcţiei membru setf este ilustrată de următorul exemplu. Fişierul stream4.cpp:
Prin executarea programului obţinem:
Funcţia afisare, din exemplul de mai sus, afişează mai întâi biţii datei membru x_flags, după care se scriu numele biţilor cu valoarea egală cu unu. Mai întâi s-a setat bitul oct şi s-a afişat valoarea constantei de tip întreg 36, folosind o conversie în octal. Astfel s-a obţinut valoarea 44. După aceea s-au setat biţii hex şi showbase, şi s-a afişat valoarea constantei încă o dată, astfel obţinând valoarea 0x24. Observăm că folosind a doua variantă a funcţiei membru setf pentru setarea bitului hex, s-a obţinut şi anularea bitului oct, aşa cum am dorit.
Menţionăm că numele şi valoarea tuturor biţilor tipului enumerare s-ar fi putut afişa de exemplu cu următoarea funcţie:
void nume_bit( char* s[], long x )
{
for( int i = 0; i < 15; i++)
printf("%-11s:%2d\n", s[i], (x >> i)& 1);
}
O altă observaţie este următoarea. Pentru afişarea biţilor datei membru x_flags s-a folosit funcţia printf şi nu ierarhia de clase declarată în fişierul iostream.h. Deşi în general o funcţie de forma
void binar_stream( long x )
{
for( int i = 8*sizeof(long)-1; i >= 0; i-- )
cout << ((x >> i)& 1) << (i%8 ? "": " ");
cout << '\n';
}
ar afişa corect biţii datei membru x_flags, vor apare erori în cazul în care este setat bitul showbase şi unul dintre biţii oct sau hex. În aceste cazuri se va afişa şi baza setată, deci numerele întregi vor fi precedate de zero în cazul conversiei în octal, şi de 0x sau 0X în cazul conversiei în hexazecimal.
18.2.3. Funcţiile membru width, fill şi precision
Ca şi în cazul funcţiei printf, se poate determina lungimea minimă a câmpului în care se va afişa data respectivă. Această valoare este memorată în data membru x_width a clasei ios. Valoarea implicită a datei membru x_width este zero, ceea ce înseamnă că afişarea se va face pe atâtea caractere câte sunt necesare.
Valoarea datei membru x_width poate fi determinată sau modificată cu funcţia membru width a clasei ios. Ea are următoarele două forme:
int width();
şi
int width( int lungime );
Prima formă a funcţiei membru width returnează valoarea datei membru x_width. A doua variantă modifică valoarea datei membru x_width la valoarea determinată de lungime, şi returnează vechea valoare a lui x_width.
Este foarte important de menţionat că după orice operaţie de intare/ieşire valoarea datei membru x_width se va reseta la valoarea zero. Deci dacă nu se determină o lungime a câmpului înainte de o operaţie de inserare, atunci se va folosi valoarea implicită.
Dacă lungimea câmpului, în care se face afişarea, este mai mare decât numărul de caractere, care vor fi afişate, atunci cadrarea se va face în mod implicit la dreapta, şi spaţiul rămas se va completa cu caractere de umplere. În mod implicit caracterele de umplere sunt spaţii, dar ele pot fi modificate cu ajutorul funcţiei membru fill a clasei ios. Clasa ios are o dată membru x_fill în care se memorează caracterul de umplere. Funcţia membru fill are următoarele două forme:
char fill();
şi
char fill( char car );
Prima variantă returnează caracterul de umplere curent. A doua formă a funcţiei membru fill modifică data membru x_fill la caracterul car, şi returnează vechea valoare a caracterului de umplere.
În cazul în care se afişează valoarea unor numere reale, se poate determina precizia, adică numărul de zecimale, care se va folosi la scrierea datelor. Clasa ios are o dată membru x_precision, care are valoarea implicită egală cu zero. În mod implicit datele de tip real se vor afişa cu şase zecimale. Funcţia membru precision are următoarele două forme:
int precision();
şi
int precision( int p );
Prima variantă a funcţiei membru precision returnează valoarea curentă a datei membru x_precision. A doua variantă atribuie valoarea parametrului p datei membru x_precision şi returnează valoarea anterioară. Funcţia membru precision s-a folosit în fişierul sup_fun1.cpp pentru a determina numărul de cifre care vor fi afişate în cazul calculării numărului π. Prezentăm în continuare un alt exemplu, în care se vor folosi funcţiile membru din acest paragraf. Fişierul stream5.cpp:
După executarea programului obţinem:
Caracterele '*' s-au afişat pentru a evidenţia câmpul în care se face scrierea datelor. Observăm că într-adevăr valoarea implicită a datelor membru x_width şi x_precision este zero, deci afişarea se va face cu şase zecimale.
După modificarea acestor date membru la valorile x_width=7 respectiv x_precision=2, afişarea se face în câmpul de şapte caractere, cu două zecimale, numărul fiind cadrat la dreapta. După operaţia de scriere valoarea datei membru x_width devine zero, dar valoarea datei membru x_precision nu se modifică.
Folosind funcţia membru width, se atribuie datei membru x_width din nou valoarea şapte. În continuare se foloseşte funcţia membru fill pentru a determina un caracter de umplere diferit de caracterul blanc.
Menţionăm că dacă în loc de apelarea funcţiei printf, s-ar fi folosit ierarhia de clase declarată în fişierul iostream.h, atunci nu s-ar fi obţinut ceea ce s-a dorit. De exemplu, dacă în loc de funcţiile scrie_width_precision_c şi afisare_pi_c s ar fi folosit funcţiile:
void scrie_width_precision_stream()
{
cout << "x_width: " << cout.width() << '\n';
cout << "x_precision: " << cout.precision() << '\n';
}
void afisare_pi_stream()
{
cout << "*" << pi << "*\n";
}
atunci s-ar fi obţinut:
În acest caz, de fiecare dată, valoarea lui π se afişează pe un câmp de lungime egală cu numărul de caractere afişate. Explicaţia este că datei membru x_width după prima operaţie de inserare s-a atribuit valoarea zero, prima operaţie fiind afişarea şirului de caractere "x_width: ", şi nu scrierea valorii lui π.
Există o legătură strânsă între funcţiile membru prezentate în acest paragraf şi formatările din cadrul funcţiei printf. Acest lucru este ilustrat de următorul exemplu. Fişierul stream6.cpp:
În exemplul de mai sus s-a afişat valoarea constantei de tip real x în două moduri: cu ajutorul streamurilor, şi folosind funcţia printf. Putem să constatăm că s-a obţinut acelaşi rezultat. Deşi în acest caz scrierea cu funcţia printf este mai compactă, afişarea cu streamuri este mai generală, deoarece caracterul de umplere poate fi orice alt caracter, nu numai '0'.
18.2.4. Manipulatori
Biţii datei membru x_flags, care corespund conversiei, pot fi setaţi şi într un alt mod, folosind funcţii membru speciale, numite manipulatori. Avantajul manipulatorilor este că ei returnează o referinţă la un stream, deci apelurile acestor funcţii membru pot fi înlănţuite.
O parte a manipulatorilor este declarată în fişierul iostream.h, iar celelalte în fişierul iomanip.h. Manipulatorii declaraţi în fişierul iostream.h sunt:
endltrecere la linie nouă şi vidarea zonei tampon corespunzătoare streamuluiendsinserarea caracterului '\0'flushvidarea zonei tampon a unui obiect al clasei ostreamdecconversie în zecimalhexconversie în hexazecimaloctconversie în octalwssetarea bitului skipws
Tabelul 5. Manipulatorii declaraţi în fişierul iostream.h
Manipulatorii declaraţi în fişierul iomanip.h sunt:
setbase(int b)setarea bazei sistemului de numeraţie corespunzătoare conversiei, la valoarea bє{0,8,10,16}resetiosflags(long x)ştergerea biţilor specificaţi în parametrul x, din data membru x_flagssetiosflags(long x)setarea biţilor din data membru x_flags, specificaţi în parametrul xsetfill(int f)datei membru x_fill i se atribuie valoarea parametrului fsetprecision(int p)datei membru x_precision i se atribuie valoarea parametrului psetw(int w)datei membru x_width i se atribuie valoarea parametrului w
Tabelul 6. Manipulatorii declaraţi în fişierul iomanip.h
Folosind manipulatori exemplul din fişierul stream6.cpp se poate transcrie în următorul mod. Fişierul stream7.cpp:
Rezultatul obţinut este identic cu cel al programului anterior. Manipulatorii s au apelat în mod înlănţuit, deci programul devine mai simplu. În următorul exemplu se va afişa valoarea datei membru x_flags în binar, hexazecimal, octal şi zecimal. Fişierul stream8.cpp:
Rezultatul obţinut este următorul:
În acest caz nu a fost necesară includerea fişierului iomanip.h, deoarece manipulatorii hex, oct şi dec sunt declarate în fişierul iostream.h.
Dacă se schimbă modul de conversie cu un manipulator, ea rămâne valabilă în continuare, până la o nouă modificare a conversiei.
18.2.5. Supraîncărcarea operatorului de inserare
În paragrafele de mai sus ne-am ocupat de modul de folosire a operatorului de inserare pentru afişarea datelor care aparţin tipurilor standard. Ar fi de dorit ca operatorul << să fie utilizabil şi în cazul tipurilor abstracte de date. De exemplu în cazul clasei numerelor raţionale
class Fractie{
int numarator;
int numitor;
public:
Fractie( int a, int b) { numarator = a; numitor = b;}
...
};
afişarea unei fracţii ar trebui să se efectueze în forma
cout << f;
unde f este un obiect al clasei fracţie.
Acest lucru se poate realiza, dacă se supraîncarcă operatorul de inserare pentru afişarea obiectelor de tip Fractie.
Deoarece operandul din stânga al operatorului << este un stream, supraîncărcarea operatorului se poate efectua cu o funcţie prieten a clasei Fractie. Pentru o clasă oareacare cu numele Clasa, operatorul de inserare se poate supraîncărca cu următoarea funcţie prieten:
class Clasa {
...
friend ostream& operator<<(ostream&, Clasa);
...
};
Pentru clasa Fractie supraîncărcarea operatorului de inserare printr-o funcţie prieten se poate face în felul următor. Fişierul fractie2.cpp:
Dacă se execută programul se obţine:
Deci se afişează numărul raţional, aşa cum am dorit. Apelarea operatorului de inserare se poate aplica în mod înlănţuit, deoarece funcţia prieten a clasei Fractie returnează o referinţă la streamul curent.
Deşi metoda de mai sus este simplă şi dă rezultat corect, ea are dezavantajul că utilizarea unei funcţii prieten micşorează gradul de protecţie a datelor. Deci datele membru protejate pot fi modificate în interiorul funcţiei prieten, care supraîncarcă operatorul de inserare. În continuare prezentăm o modalitate de supraîncărcare a operatorului <<, fără a introduce o funcţie prieten. Pentru o clasă oarecare cu numele Clasa, acest lucru poate fi realizat cu ajutorul unei funcţii membru afisare, care se va apela de către funcţia care supraîncarcă operatorul de inserare. Deci:
class Clasa {
...
public:
ostream& afisare(ostream& s);
...
};
ostream& Clasa::afisare(ostream& s)
{
...
return s;
}
ostream& operator<<(ostream& s, Clasa c1)
{
return c1.afisare(s);
}
În cazul clasei Fractie supraîncărcarea operatorului de inserare fără utilizarea unei funcţii prieten se poate efectua în modul următor. Fişierul fractie3.cpp:
Rezultatul obţinut prin executarea acestui program este identic cu cel al fişierului fractie2.cpp, dar în acest caz nu s-a folosit funcţia prieten.
Dostları ilə paylaş: |