Introducere
In mod traditional, facilitatile de I/O ale unui limbaj de programare s-au marginit la prelucrarea unui numar redus de tipuri primare: date numerice (intregi si reale), caractere si string-uri.
C++ a fost conceput astfel incat sa ofere utilizatorului posibilitatea de a defini tipuri noi pe care sa le manipuleze intr-o maniera similara tipurilor primare. Ca urmare, s-a impus necesitatea ca si operatiile de I/O sa poata fi aplicate in mod natural tipurilor definite de utilizator.
Eforturile depuse in aceasta directie au avut ca rezultat crearea unui model al procesului de I/O care sa permita extinderea functiilor de citire/scriere si asupra obiectelor. Acest model se numeste stream si presupune convertirea obiectelor in secvente de caractere si vice-versa. Sarcina care revine programatorului este aceea de a specifica o corespondenta intre un obiect (adica o entitate tipizata) si un sir de caractere netipizate.
Implementarea modelului stream-urilor
este inclusa in biblioteca limbajului C++, interfata ei cu utilizatorul
fiind descrisa in <iostream.h>.
Operatiile de iesire
Se realizeaza prin intermediul
metodelor clasei ostream. Principala
functie membru a acestei clase este operator<<
si ea este supraincarcata astfel incat sa admita toate tipurile primare
ale limbajului:
| class
ostream : public virtual ios {
//... public: ostream& operator<<(const char*); //stringuri ostream& operator<<(char); //caractere ostream&5; operator<<(int); //nr. intregi //s.a.m.d. pt. tipurile: //short, long, double, //const void* (pt. scrierea pointerilor) }; |
| ostream&
ostream::operator<< (T e)
{ // scrie valoarea 'e' return *this; } |
Executia functiilor operator<<
este invocata, bineinteles, prin intermediul unui obiect de tip ostream.
Obiectul ostream va identifica dispozitivul
spre care se directeaza iesirea. Atunci cand iesirea se realizeaza pe dispozitivele
standard de afisare, respectiv de eroare, se utilizeaza doua obiecte
ostream
predefinite: cout, respectiv
cerr.
|
int i=100;
//... cout << "i=" << i << "\n"; |
Functia operator<<
returneaza intotdeauna o referinta la obiectul pentru care a fost apelata,
de unde rezulta posibilitatea aplicarii in lant a operatorului <<.
Operatiile de iesire aplicate
asupra tipurilor definite de utilizator
Pentru ca obiectele claselor
definite de utilizator sa poata fi "scrise" este necesara supraincarcarea
unei functii operator<< globale, in
modul urmator:
| class
o_clasa {
//... friend ostream& operator<< (ostream& s, const o_clasa& ob) { // s << ob.membri return s; } }; |
| class
complex {
private: double re, im; public: complex(double r=0.0, double i=0.0): re(r), im(i) {} //... friend ostream& operator<<(ostream& s, const complex& z) { s << z.re; if(z.im>=0.0) s << '+'; s << z.im << "*i"; return s; } }; //... complex a(1,2), b(-2,-5); cout << "a=" << a << "\n"; cout << "b=" << b << "\n"; //se vor afisa secventele: b=-2-5*i |
Se poate observa ca definirea
operatiei de iesire asupra claselor utilizatorului, cu ajutorul clasei
ostream
nu implica nici modificarea clasei
ostream
si nici accesul la datele ascunse ale acesteia.
Operatia de intrare
Este similara, din punct
de vedere al modului de definire, operatiei de iesire. Se realizeaza prin
intermediul metodelor clasei istream.
Principala functie membru a acestei clase este operator>>
si ea este supraincarcata astfel incat sa admita toate tipurile primare
"citibile" ale limbajului:
| class
istream : public virtual ios {
//... public: istream& operator>>(char*);//stringuri istream& operator>>(char&);//caractere istream& operator>>(int&);//nr. intregi //s.a.m.d. pt. tipurile: //short&, long&, double&, float& }; |
| istream&
istream::operator>> (T& v)
{ // neglijeaza spatiile albe (blank, tab, CR, LF, FF) // citeste o valoare de tip T in variabila v return *this; } |
Exemplu:
|
int t[N];
//. . . for(int i=0;i<N;i++) cin >> t[i]; |
|
int t[N];
for(int i=0;i<N;i++) { if(cin >> t[i]) continue; //eroare la citire } |
|
istream& istream::get(char&);
istream& istream::get(char *p,int n,char s='\n'); |
A doua varianta citeste un string de maximum n-1 caractere pe care le memoreaza la adresa indicata de p. Citirea se opreste la aparitia unuia din urmatoarele evenimente:
Exemple de utilizare:
|
void copiere_char_by_char()
{ char c; while(cin.get(c)) cout<< c; } |
|
void citire_pe_linii()
{ char buf[100];
while(cin.get(buf,100,'\n')) fa_ceva(buf);
|
Ca regula generala, operatorii >> se folosesc pentru citirea datelor formatate, ei fiind un echivalent al functiilor scanf din C. Functiile get se utilizeaza cel mai adesea pentru citirea liniilor de text care nu au o structura fixa si in care se vor identifica apoi, prin program, diversele segmente de informatie.
Operatiile de intrare
aplicate asupra tipurilor definite de utilizator
Pentru ca obiectele claselor
definite de utilizator sa poata fi "citite" este necesara supraincarcarea
unei functii operator>> globale, in modul
urmator:
| class
o_clasa {
//... friend istream& operator>>(istream& s, o_clasa& ob) { // s >> ob.membri return s; } }; |
| class
complex {
private: double re, im; public: complex(double r=0.0, double i=0.0): re(r), im(i) {} //... friend istream& operator>>(istream& s, complex& z) /* un numar complex se va introduce sub una din formele: (re) (re,im) */ { double re=0.0,im=0.0; char c=0; s >> c; if(c=='(') { s >> re >> c; if(c==',') s >> im >> c; if(c!=')') {/*eroare*/} } else {/*eroare*/} if(s) z=complex(re,im); return s; } }; |
Fiecare stream, fie istream,
fie ostream, are asociata o stare care reflecta
modul in care se desfasoara operatiile de I/O. Starea
unui stream poate fi examinata prin intermediul unor operatii ale clasei
ios
(care este baza pentru clasele istream si
ostream):
| clas
ios {
//... public: int eof() const; /*returneaza non-zero daca s-a intalnit sfarsitul sirului de intrare */ int fail() const; //returneaza non-zero daca ultima operatie I/O a esuat int bad() const; //returneaza non-zero daca streamul este corupt int good() const; //returneaza non-zero daca precedenta operatie I/O a reusit //... }; |
Diferenta dintre starile fail() si bad() consta in principiu in aceea ca starea fail() presupune ca stream-ul implicat nu este corupt si ca nu s-au pierdut caractere. De exemplu, daca se incearca citirea unei valori intregi dintr-un stream de intrare in care la pozitia curenta se gaseste un caracter nenumeric, efectul va fi intrarea in starea fail(). Daca insa in cursul unei operatii I/O intervin defecte fizice ale dispozitivului implicat, este foarte probabil ca rezultatul va fi starea bad().
Valorile utilizate pentru
a reprezenta starea unui stream apartin unui tip enumerare definit tot
in clasa ios:
| class
ios {
//... public: enum io_state { goodbit, eofbit, failbit, badbit }; //... }; |
| switch(cin.rdstate()){
case ios::goodbit: //precedenta operatie a reusit break; case ios::eofbit: //sfarsitul sirului de intrare break; case ios::failbit: //foarte probabil o eroare de formatare break; case ios::badbit: //foarte probabil s-au pierdut caractere break; } |
| while
(cin >> v) fa_ceva(v);
if(cin.rdstate()!=ios::eofbit) trateaza_eroarea_IO(); |
Functia clear
se poate apela si fara parametru, caz in care acesta este considerat ca
avand valoarea implicita ios::goodbit.
Formatarea stream-urilor
In cele expuse pana aici
s-au prezentat operatiile de I/O ca niste functii de citire/scriere avand
un comportament similar functiilor scanf/printf
din C, dar care ar contine in sirul lor de formatare doar specificatii
de tip (de genul %d pentru intregi, %c
pentru caractere, etc). Asa cum un sir de formatare utilizat de functiile
scanf/printf
poate sa cuprinda si alte detalii legate de modul de prezentare a informatiei
citite/scrise (mai ales a celei scrise), si stream-urile pun la dispozitie
facilitati de precizare a unor asemenea detalii, si anume, prin intermediul
unui set de metode ale clasei
ios.
Operatiile I/O realizate
prin intermediul stream-urilor utilizeaza buffer-e pentru stocarea informatiilor.
Fiecare "atom" de informatie poate fi considerat ca un camp din buffer-ul
respectiv, iar metodele clasei ios de care
aminteam mai sus au rolul de a seta/consulta diverse atribute ale unui
asemenea camp. Atributele sunt reprezentate cu ajutorul unor date membru
ale clasei ios.
| class
ios {
//... int x_fill; long x_flags; int x_precision; int x_width; public: static const long adjustfield; static const long basefield; static const long floatfield; int width(int
w); //seteaza latimea campului (x_width)
char fill(char);
//seteaza caracterul de umplere (x_fill)
long flags(long
f);//seteaza flag-urile de control (x_flags)
int precision(int);//seteaza
precizia nr. reale (x_precision)
|
| cout.width(4);
cout << '(' << 12 << ')'; //efect: valoarea 12 se va scrie pe 4 pozitii // ( 12) |
| cout.width(4);
cout << '(' << 12 << "),(" << 12 << ")\n"; //efect: ( 12),(12) //stringurile "),(" si ")\n", precum si al 2-lea '12' //se scriu pe lungimea strict necesara |
| cout.width(4);
cout.fill('#'); cout << '(' << 12 << ')'; //efect: (##12) |
| class
ios {
//... public: enum { skipws, //neglijeaza spatiile albe la citire
left, //aliniere la stanga
dec, //utilizeaza baza 10 pt. scrierea nr.
showbase, //la afisare se pune si baza in fata nr.
|
Functia flags()
seteaza campul x_flags ca valoare long, nu
la nivel de bit. Pentru a specifica ce biti dorim sa setam vom utiliza
valorile tipului enumerare de mai sus intre care se aplica operatorul sau-logic
pe biti:
cout.flags(ios::left | ios::oct | ios::showpoint | ios::fixed)
Functia returneaza intotdeauna
valoarea anterioara a lui x_flags, aceasta
putand fi utilizata pentru o restaurare ulterioara.
| long
new_opt=ios::left | ios::oct | ios::showpoint | ios::fixed;
long old_opt=cout.flags(new_opt); //... cout.flags(old_opt) |
Valoarea returnata de flags()
poate fi utilizata si atunci cand, pe langa bitii deja setati se doreste
setarea a inca unui bit:
cout.flags(cout.flags() | ios::showpos);
Functia
setf()
cu un parametru serveste la setarea doar a unui bit specificat prin intemediul
constantelor tipului enumerare dat anterior, fara
a afecta valoarea celorlalti biti. De exemplu, linia de mai sus este echivalenta
cu:
cout.setf(ios::showpos)
Functia setf() cu 2 argumente serveste pentru specificarea unor parametri de formatare care nu pot fi reprezentati doar cu ajutorul cate unui singur bit, cum este cazul bazei utilizate pentru afisarea numerelor intregi sau a stilului de afisare a numerelor reale (parametri care pot lua cate 3 valori posibile). Al doilea parametru indica la ce optiune de formatare se face referirea si el poate fi una din variabilele statice basefield, adjustfield, respectiv floatfield ale clasei ios. Primul parametru indica valoarea pe care o poate lua optiunea selectata.
Exemplu:
| cout.setf(ios::oct,ios::basefield);
cout << 1234 << '\n'; //efect: 2322 cout.setf(ios::showbase); cout << 1234 << '\n'; //efect : 02322 cout.setf(ios::hex,ios::basefield); cout << 1234 << '\n'; //efect : 0x4d2 |
Optiunea adjustfield
este coroborata cu setarea campului x_width,
ea indicand modul de aliniere in cadrul unei latimi specificate pentru
afisare.
| cout.width(4);
cout << '(' << -12 << ")\n"; //efect : ( -12) cout.width(4); cout.setf(ios::left,ios::adjustfield); cout << '(' << -12 << ")\n"; //efect : (-12 ) cout.width(4); cout.setf(ios::internal,ios::adjustfield); cout << '(' << -12 << ")\n"; //efect : (- 12) |
| cout.precision(4);
cout << 1234.56789 << "\n"; //efect : 1234.5679 |
Manipulatorii
In contextul operatiilor de I/O exista o serie de actiuni pe care programatorul ar dori sa le specifice si sa le efectueze "din mers", si anume in aceeasi fraza care invoca operatiile de I/O. De regula aceste actiuni privesc stabilirea unor parametri ai procesului de I/O.
| cout
<< x;
cout.precision(4); cout << y; |
Claritatea provine din faptul ca se evidentiaza mai bine legatura logica dintre actiunea de specificare a preciziei si operatia de afisare a lui y afectata de aceasta actiune.
Pentru ca scrierea unei secvente de felul celei de mai sus sa fie posibila este nevoie, in primul rand, ca operatorii de I/O (adica << si >>) sa fie supraincarcati astfel incat sa accepte si tipul ce corespunde apelului de functie din secventa. Supraincarcarea se realizeaza diferit, dupa cum functia respectiva are sau nu parametri de alt tip decat clasele istream/ostream. De exemplu, in cazul prezentat mai sus, functia set_precision trebuie sa primeasca un parametru de tip int.
Conceptul de manipulator este cel care sta la baza facilitatii de a insera in mod direct apeluri de functii intr-o lista de operatii I/O.
In cele ce urmeaza, pentru simplificarea expunerii, vom nota cu tip_stream numele claselor istream/ostream si cu 'op_IO' simbolurile >>/<<.
Manipulatori fara parametri
Crearea unor asemenea manipulatori
presupune urmatoarele definitii:
| typedef
tip_stream&
(*pm)(tip_stream&);
/* pm este tipul pointer la o functie care accepta un parametru de tip tip_stream& si returneaza tip_stream& */ tip_stream&
operator'op_IO'(tip_stream& s,pm f) {
tip_stream&
func(tip_stream& s) {
//Cu
definitiile de mai sus putem scrie:
//fraza
de mai sus se interpreteaza ca:
|
Exemplu:
| ostream&
set_hexa_base(ostream& os) {
os.setf(ios::hex,ios::basefield); return os; } //... cout << x << set_hexa_base << y; //efect : y va fi afisat in baza 16 |
Manipulatori
cu parametri
Pentru exemplificare se
considera ca manipulatorii trebuie sa accepte un parametru de tip int.
In acest caz va trebui sa prevedem urmatoarele definitii:
| class
manip_int
{
int i; tip_stream& (*f)(tip_stream&,int) /* f este de tip pointer la o functie care accepta un parametru de tip tip_stream& si unul de tip int si returneaza tip_stream& */ public: manip_int(tip_stream& (*ff)(tip_stream&, int), int ii): f(ff), i(ii) {} friend tip_stream& operator'op_IO'(tip_stream& s, manip_int& m) { return m.f(s,m.i); } }; /*Pentru fiecare tip de stream (istream/ostream) se va defini cate o clasa de genul lui manip_int */ //Fie o functie
//Se
defineste manipulatorul
//Cu
definitiile de mai sus putem scrie:
//fraza
de mai sus se interpreteaza ca:
|
Cu acestea,
referindu-ne la exemplul prezentat la inceputul paragrafului,
si considerand ca numele clasei manip_int
definita pentru stream-urile de tip ostream
este omanip_int, putem scrie:
| ostream&
precision(ostream& os,int i) {
os.precision(i); return os; } omanip_int setprecision(int i) { return omanip_int(precision,i); } //... cout << x << set_precision(4) << y; //efect : y va fi afisat cu precizia de 4 zecimale |
Manipulatorii standard
In biblioteca de clase pentru
lucrul cu stream-uri sunt definiti o serie de manipulatori de uz curent,
numiti manipulatori standard. Ei se pot utiliza
in locul functiilor ios::flags()
si
ios::setf(),
care necesita un efort de programare mai mare. Dam mai jos lista manipulatorilor
standard:
| ios&
oct(ios&);//stabileste baza octala
ios& dec(ios&);//stabileste baza zecimala ios& hex(ios&);//stabileste baza hexa ostream& endl(ostream&);//scrie
'\n' si executa un flush
istream& ws(istream&);//"sari" peste spatiile albe |
| setbase(int
b);//stabileste baza b
setfill(int f);//stabileste caracterul de umplere setprecision(int p);//stabileste precizia setw(int w);//stabileste latimea campului resetiosflags(long b);//pune pe 0 bitii de formatare setiosflags(long b);//pune pe 1 bitii de formatare |
Atentie:
Pentru
a putea lucra cu acesti manipulatori este necesara includerea fisierului
header <iomanip.h>.
Stream-urile si fisierele
Pentru a specifica faptul ca un anumit stream va lucra cu un fisier de pe disc se utilizeaza obiecte ale claselor ifstream (fisiere de intrare), ofstream (fisiere de iesire) sau,respectiv, fstream (fisiere de intrare/iesire), clase derivate din istream si ostream. Operatiile definite in clasele istream si ostream pot fi utilizate pentru obiectele ifstream, ofstream, respectiv fstream. Interfata acestor clase se gaseste in <fstream.h>
In cele ce urmeaza vom lucra mai mult cu fisiere ifstream si ofstream, aratand unde e cazul particularitatile pentru fstream.
Deschiderea unui fisier se realizeaza pur si simplu prin crearea unui obiect de tip ifstream/ofstream. Constructorii acestor clase accepta urmatorii parametri:
| class
ios {
//... public: enum open_mode{ in, //deschidere pt. citire out, // deschidere pt. scriere ate, //deschidere cu pozitionare la sfarsitul fisierului app, //deschidere pt. adaugare trunc, //trunchiaza fisierul la lungime 0 nocreate, //deschiderea esueaza daca fisierul nu exista noreplace //deschiderea esueaza daca fisierul exista }; //... }; |
Valorile reale ale constantelor de mai sus sunt dependente de implementare. Un fisier ofstream va fi deschis implicit in scriere, iar unul ifstream - in citire.
In exemplul de mai jos este
ilustrata utilizarea indicatorilor de deschidere:
| void
o_func() {
ofstream un_fis("un_nume",ios::out | ios::nocreate); if(un_fis.bad()) {/* probabil fisierul nu exista */}; //... fstream alt_fis("alt_nume",ios::in | ios::out); //... } |
Inchiderea unui fisier se realizeaza automat, prin distructorul claselor ifstream/ofstream. Daca se doreste insa ca inchiderea sa aiba loc inainte de disparitia obiectului ifstream/ofstream, se poate invoca metoda close():
un_fisier.close();
Obiectele de tip ifstream/ofstream pot fi utilizate intr-o maniera similara celor de tip istream/ostream deoarece mostenesc operatiile de baza de la acestea din urma.
Pentru fisierele de intrare,
pe langa operatiile clasice ( get si >>),
se pot aplica si metodele urmatoare (cunoscute de altfel, de la lucrul
cu fisiere obisnuite in C):
| class
ios {
//... public: enum seek_dir{ beg, //cauta de la inceputul fisierului cur, //cauta de la pozitia curenta end //cauta de la sfaritul fisierului }; //... }; |
Probleme
1.Se
cere sa se scrie un program care, folosind operatiile cu stream-uri, citeste
dintr-un fisier o secventa de perechi de numere reale din care creaza obiecte
de tip complex. Apoi scrie intr-un alt fisier valoarea obiectelor complex
sub forma: parte_reala+parte_imaginara*i
2. La clasele definite in programul tema de la lucrarea nr. 6 se vor adauga functii de afisare folosind operatiile cu stream-uri (+mecanismul functiilor virtuale) astfel incat forma de afisare a obiectelor sa fie urmatoarea:
Pentru intrebari legate de enuntul acestei probleme trimiteti un mail la [email protected]
Bibliografie
[Str91] B. Stroustrup - "The C++ programming language", AT&T, 1991