POO - Laborator 4 Clase specifice sistemului de intrari/iesiri In C++ exista o ierarhie de clase predefinite, care asigura un suport eficient pentru operatiile de intrare/iesire, atat cu consola cat si cu discul. Conceptul de baza este cel de dispozitiv logic de intrare/iesire (flux de intrare/iesire). Aceste fluxuri sunt denumite stream-uri. Fisierele antet care asigura interfata cu clasele predefinite sunt si . Exista doua clase de baza: streambuf si ios. Clasa streambuf ofera operatii minimale de acces la un dispozitiv de intrare/iesire. Clasa ios contine adresa unui obiect de tip streambuf, o variabila de stare (pentru memorarea erorilor), variabile de control al formatului si o colectie de functii. Din ios este dezvoltata o ierarhie complexa de clase, dintre care principalele folosite sunt: a) Operatii generale -clasa istream (transfer de la un obiect de tip streambuf); obiectele clasei reprezinta dispozitive de intrare; -clasa ostream (transferuri catre un obiect de tip streambuf; obiectele clasei reprezinta dispozitive de iesire; -clasa iostream, derivata din istream si ostream, pentru operatii bidirectionale (dispozitive de intrare si iesire). b) Operatii cu fisiere -clasa ifstream (pentru citire din fisiere); -clasa ofstream (pentru scriere in fisiere); -clasa fstream (pentru operatii de citire/scriere in fisiere). Exista dispozitivele logice (obiectele) predefinite urmatoare: cin: intrare consola (similar cu stdin in C); cout: iesire consola (similar cu stdout in C); cerr: eroare standard (similar cu stderr in C); In clasele istream si ostream sunt supradefiniti operatorii << si >> pentru operatii de citire si scriere. Acesti operatori sunt definiti dupa modelele: ostream& operator<< (tip_de_baza); istream& operator>> (tip_de_baza&); Functiile supradefinite intorc referinte catre obiectul de tip istream sau ostream respectiv, ceea ce permite asocierea mai multor apeluri ale operatorului (de exemplu cout << x << y se interpreteaza ca (cout.operator<<(x)).operator<<(y). Din acest motiv, cei doi operatori se mai numesc si operatori de insertie. Ei sunt supradefiniti pentru toate tipurile de baza (int, float, char * etc.). Acesti operatori pot fi supradefiniti intr-o clasa proprie CLASA dupa modelele: // Definitii (apar in definitia clasei) friend ostream & operator<< (ostream &, CLASA &); friend istream & operator>> (istream &, CLASA &); De exemplu, intr-o clasa stiva, s-a supradefinit operatorul de afisare, prin: ostream& operator<< (ostream& dev, stiva & f) { for(int i = 0; i <= f.sp; i++) dev << f.buf[i] << " "; dev << "\n"; return dev; } Pe linga operatorii << si >>, clasele istream si ostream au, respectiv, functiile elementare get si put (la nivel de octet), care intorc referinte la stream-ul respectiv, avand prototipurile: ostream& put (char c); // Scrie c istream& get (char& c); // Citeste c Aceste functii se apeleaza, dupa modelul (prsupunind ca dispozitiv de lucru consola): char c; cin.get(c); cin.put(c); Functii de citire/modificare a starii int eof(); // specifica daca este sfarsit de fisier int bad(); // specifica daca a fost o eroare la ultima operatie int good(); // specifica daca operatia s-a incheiat cu succes void clear(); // sterge indicatorii de eroare Un stream poate fi testat in raport cu eventuale erori (sau la sfarsit de fisier) si prin testarea numelui sau (in paranteze). O valoare diferita de zero inseamna fara eroare. Un program de copiere cin --> cout poate fi scris: char c; while (cin.get(c)) cout.put(c); Aici se testeaza de fapt cin dupa apelul functiei membre get. Controlul formatului Operatorii << si >> presupun niste formate implicite. Formatul de intrare/iesire poate fi controlat prin manipulatori. a) Manipulatori fara parametri. Se folosesc in operatii de I/O, dupa sintaxa: out << manipulator in >> manipulator operatii care pot fi inserate in secvente de citire/scriere. Efectul este ca se seteaza o serie de parametri interni ai obiectului de tip stream, care fac ca urmatoarele operatii de citire sau scriere sa aibe loc cu un alt tip de conversie. De exemplu, la cin si cout cu variabile intregi, se presupune implicit ca reprezentarea externa a numerelor este in baza 10. O scriere de forma: cout << hex << n; va face ca variabila intreaga n sa fie afisata in hexazecimal. Similar, o citire de forma: cin >> oct >> i; va considera ca i este scris de la consola in baza 8. Manipulatorii fara parametri sunt: -dec (specifica baza 10) -oct (specifica baza 8) -hex (specifica baza 16) -endl (end of line, adica insereaza '\n') -ws (ignora spatiile albe: blank, tab etc.) -ends (end of string, adica insereaza '\0') -flush (goleste bufferul de iesire) Se pot defini si manipulatori proprii (de exemplu pentru mesaje de tip prompt etc.), dupa sintaxa: istream& nume_manipulator (istream& in) { // Operatie specifica return in; } ostream nume_manipulator (ostream& out) { // Operatie specifica return out; } b) Manipulatori cu parametri (definiti in ) -setbase (int n); // Baza de numeratie n (8,10,16) -setprecision (int n) // Precizia pentru reali la n cifre -setw (int n) // Latime camp la n spatii -setfill (int c) // Caracter de umplere c (pozitiile vide // din camp vor fi completate cu c) Exemplu: float x = 12.345; cout << setw(12) << setprecision(7) << x << endl; Operatii cu fisiere Se fac cu clasele ifstream, ofstrem si fstream. Functia de deschidere: void open (const char *nume_fisier, int mod, int acces); -acces = 0 (normal), 1 (read-only), 2 (ascuns), 3 (sistem). -mod = se specifica modul de deschidere. Modurile sunt predefinite in clasa ios si pot fi: ios::in (intrare) ios::out (iesiere) ios::ate (deschidere cu pozitionare la sfarsitul fisierului) ios::app (apend, adaugare la sfarsitul fisierului) ios::binary (deschidere in mod binar; implicit este in mod text) Sunt implicite modurile ios::in pentru ifstream, ios::out pentru ofstream si acces = 0. Modurile pot fi combinate cu sau logic la nivel de bit (|). In urma deschiderii, variabila stream pentru care s-a apelat functia trebuie sa fie diferita de zero. Este indicat sa se testeze acest lucru imediat dupa apelul open. Functia de inchidere: void close(); Exemplu: program de copiere fisiere #include void main(void) { ofstream ofis; ifstream ifis; char c; ofis.open("date1.dat", ios::app | ios::binary); if(!ofis) cout << "Eroare la deschidere date1.dat"; ifis.open("date2.dat", ios::binary); if(!ifis) cout << "Eroare la deschidere date2.dat"; while (ofis && ifis.get(c)) ofis.put(c); ofis.close(); ifis.close(); } Se observa testarea in bucla while a unei eventuale erori la scriere in ofis si a sfarsitului de fisier in ifis. Se pot folosi si constructori pentru clasele ifstream si ofstream: ofstream ofis("date1.dat", ios::app | ios::binary); ifstream ifis("date2.dat", ios::binary); Exista o multime de alte functii de intrare/iesire. De exemplu, putem citi o linie dintr-un fisier text cu metoda: getline(unsigned char *buf, int len, char delim); care citeste cel mult len-1 caractere de la dispozitivul respectiv in buf, sau pina la intilnirea caracterului delim. Al treilea parametru este implicit caracterul '\n'. Totdeauna se pune '\0' la sfirsitul liniei citite. O bucla de citire la nivel de linii dintr-un fisier text si afisare la consola s-ar putea scrie astfel: unsigned char buf [256]; while (!ifis.eof()) { ifis.getline(buf, 256, '\n'); cout << buf << endl; } Functii de control al pozitiei in fisier Functiile de pina acum presupuneau tacit ca citirea si scrierea se face secvential. Ca sa citim al 100-lea octet dintr-un fisier trebuia obligatoriu sa fi citit cei 99 de octeti anteriori (analog la scriere). Uneori dorim sa citim/scriem intr-un fisier existent, intr-o anumita pozitie. Metodele seekg (la citire din ifstream) si seekp (la scriere in ofstream) realizeaza pozitionarea absoluta in fisier. Prototipul lor este: ifstream& seekg(long poz, int reper); ifstream& seekp(long poz, int reper); unde poz este pozitia (in octeti) iar reper boate fi una din constantele: beg, cur, end (fata de inceput, fata de pozitia curenta, fata de sfirsit). De exemplu, daca dorim sa ne deplasam la sfirsitul fisierului f, deschis pentru scriere, dupa ultimul octet din fisier, scriem: f.seekp (0L, ios::end); Prelucrarile de acest gen se fac in modul binar, cu functii de scriere si citire "oarbe" (care nu fac nici un fel de prelucrari), de tipul: istream& read(unsigned char *buf, int n); ostream& write(const unsigned char *buf, int n); care citesc/scriu n octeti din buf in fisierul respectiv. Sa consideram un exemplu ceva mai elaborat. Presupunem o structura de date de tipul: struct Rec { float a[2]; int i1, i2; }; si vrem sa exploatam fisiere binare care constau din asemenea inregistrari. Deoarece functiile read/write de mai sus presupun un buffer la nivel de octeti, suprapunem in memorie o inregistrare Rec cu un buffer de lungime adecvata, prin crearea unei uniuni: union RecBuf { Rec r; unsigned char buf[sizeof(Rec)]; }; Dezvoltam o clasa Record, care sa contina o structura Rec si trei functii de acces, toate publice. Constructorul si functia Update permit definirea si modificarea datelor dintr-un Record. Pentru afisare comoda la consola supradefinim operatorul << in maniera cunoscuta. Importante sun functiile get si put, cu un parametru de tip fisier de intrare, respectiv iesire, si un numar intreg care specifica a cita inregistrare de tip Rec din fisier se citeste sau se scrie. Un apel cu valoarea 0 va citi/scrie prima inregistrare. In ambele functii, se declara o uniune de tip RecBuf pentru a avea acces la date la nivel de secventa de octeti. De observat calculul pozitiei in octeti, dupa relatia: index*sizeof(Rec) unde index este indicele la nivel de inregistrari. Daca operatiile au avut loc corect, get si put intorc 1. In caz de eroare, testat prin metoda fail(), se intoarce 0. class Record { Rec date; public: Record(float x1, float x2, int i1, int i2) { date.a[0] = x1; date.a[1] = x2; date.i1 = i1; date.i2 = i2; } void Update(float x1, float x2, int i1, int i2) { date.a[0] = x1; date.a[1] = x2; date.i1 = i1; date.i2 = i2; } int get(ifstream&, int); int put(ofstream&, int); friend ostream& operator<< (ostream&, Record&); }; ostream& operator<< (ostream& dev, Record& x) { for (int i = 0; i < 2; i++) dev << x.date.a[i] << " ";; dev << x.date.i1 << " " << x.date.i2 << endl; return dev; } int Record::get(ifstream& f, int index) { RecBuf rb; f.seekg(index*sizeof(Rec), ios::beg); if(f.fail()) return 0; f.read((char *) &(rb.buf), (int) sizeof(Rec)); if(f.fail()) return 0; for (int i = 0; i < 2; i++) date.a[i] = rb.r.a[i]; date.i1 = rb.r.i1; date.i2 = rb.r.i2; return 1; } int Record::put(ofstream& f, int index) { RecBuf rb; for (int i = 0; i < 2; i++) rb.r.a[i] = date.a[i]; rb.r.i1 = date.i1; rb.r.i2 = date.i2; f.seekp(index*sizeof(Rec), ios::beg); if(f.fail()) return 0; f.write((char *) &(rb.buf), (int) sizeof(Rec)); if(f.fail()) return 0; return 1; } void main(void) { ifstream ifis; ofstream ofis; Record x(0.0, 0.0, 0, 0); ofis.open("date3.dat", ios::binary); if(!ofis) cerr << "Eroare la deschidere date.dat pentru scriere" << endl; for (int i = 0; i < 10; i++) { x.Update((float) i+1, (float) i+1, i+1, i+1); x.put(ofis, i); } ofis.close(); ifis.open("date3.dat", ios::binary); cout << "Prima inregistrare" << endl; x.get(ifis, 0); cout << x; cout << "A 6-a inregistrare" << endl; x.get(ifis, 5); cout << x; cout << "A 11-a inregistrare" << endl; // // Nu exista a 11-a inregistrare. get va intoarce 0. // assert(x.get(ifis, 10)); cout << x; ifis.close(); }