Documente online.
Zona de administrare documente. Fisierele tale
Am uitat parola x Creaza cont nou
 HomeExploreaza
upload
Upload




Conceptele fundamentale OOP

c


Conceptele fundamentale OOP



OOP se dezvolta in jurul a citorva concepte fundamentale: tipuri de date abstracte (clasa, obiect, metoda, mesaj), incapsularea datelor si ascunderea detaliilor, mostenirea (ierarhii de clase si subclase), polimorfismul.

Tipul de date abstract este nucleul in jurul caruia este construit orice limbaj OOP. Un tip de date abstract (TDA) este un model ce defineste o structura de dat SI un set de operatii asociate acesteia. Aceste operatii sint definite special PENTRU aceasta structura de date si ii caracterizeaza existenta.

In majoritatea limbajelor de programare OOP descrierea unui TDA se face printr-o definitie de clasa. Aceasta defineste o interfata pentru toate operatiile permise pentru acel tip si totodata specifica detaliile de implementare pentru structurile de date si pentru implementarea efectiva a operatiilor. Operatiile definite pentru o clasa se numesc metode.

Declararea unei "variabile" (unei "instante") dintr-o clasa (o "mostra") inseamna declararea unui obiect. Un obiect incapsuleaza atit structura de date definita pentru clasa respectiva (pentru TDA respectiv), cit si metodele (functiile, operatiile) specifice acesteia. Procesul de apelare a unei metode pentru un obiect particular se numeste trimiterea unui mesaj.

Pornind de la clase deja definite se pot crea clase noi, "imbogatite" cu informatii noi (structuri de date si/sau metode). Se defineste astfel o "subclasa" sau o clasa "derivata" a clasei initiale. Aceasta clasa derivata caracterizeaza existenta unor obiecte ce primesc "trasaturi" de la clasa initiala ("mostenesc" proprietati), dar au si proprietati noi ce nu le are clasa "parinte".

O metoda poate avea acelasi nume in cadrul mai multor clase derivate. Numele si functia se pot pastra, modificindu-se in schimb detaliile de implementare pentru fiecare clasa in parte. Astfel, un acelasi nume este partajat intre mai multe clase. O aceeasi "trasatura" (proprietate) are mai multe moduri de manifestare (mai multe "fete"), ceea ce se numeste polimorfism.

3.1. Conceptul de obiect

Ori de cite ori se gindeste despre lume se gindeste in termeni referitori la obiecte. In afara domeniului calculatoarelor prin obiect se defineste "ceva" ce are "marginile" foarte bine definite. De la obiecte foarte palpabile (persoane, unelte, masini, etc) si pina la lucruri abstracte (dreptate, revolutie, patrat, etc).

Atunci cind se gindeste la un obiect se gindeste la acesta ca la un tot unitar. De exemplu, cind ne gindim la obiectul "iepure" acesta are asociat cu numele sau o multime de date: este mamifer, are blana, etc. In plus, se asociaza cu acest obiect si existenta. Iepurele maninca, fuge de vulpi, etc. Normal, cind ne gindim la un iepure niciodata nu ne va apare ideea de a subdivide (desprinde, diviza) conceptul de "iepure" si sa-i gindim atributele separat de existenta (actiunile) ce le asociem cu acesta.

Pe de alta parte insa, poza (pictura) unui iepure NU este un iepure, ci este doar un simbol pe o suprafata. Dar nu este reprezentata (abstractizata) intr-un numar de componente definite separat unele de altele, componente independente. Toate componentele ramin in relatia lor esentiala cu celelalte si cu intregul. Relatiile dintre componente si intreg si dintre componente sint semnificative doar atita timp cit sint tinute impreuna intr-un invelis, intr-un tot unitar.

Obiectele modeleaza atit caracteristicile cit si comportarea (existenta, actiunile) elementelor lumii reale. Obiectele sint abstractizarea primara a datelor.

Din punct de vedere al limbajelor OPP, un obiect este alcatuit din date private si nu set de operatii ce pot sa acceseze aceste date.

Exemplu

Fie o aplicatie ce trebuie sa monitorizeze o serie de temperaturi preluate de la un proces. Pentru simplificare presupunem ca trebuie doar sa le afiseze, iar preluarea din proces nu se detaliaza.

Definim pentru aceasta o structura de date care sa poata fi asociata cu informatiile despre un termometru:

struct tip_termometru ;

Trebuie sa definim de asemenea si o functie care sa "afiseze" (sa reprezinte) acest termometru (sau eventual numai temperatura curenta ce o indica acesta); fie, de exemplu:

void display_temp (struct tip_termometru* ter)

Ce se observa ? Desi "termometrul" este un obiect bine definit ca structura de date si operatii ce se pot executa "asupra" lui, structura de date este "rupta" de functia scrisa special pentru aceasta structura de date. Desi luate in ansamblu aceste componente alcatuiesc un TDA (tip de date abstract), acum nu pot fi inca "privite (si gestionate) in ansamblu". De aici rezulta majoritatea neajunsurilor discutate in capitolele anterioare.

"Asamblarea" structurilor de date si a codului (functiilor) se poate face in sistemele (si limbajele) OOP, rezultind in acest fel un OBIECT. Datele interne ale unui TDA (obiect) nu mai sint accesate "din exterior" (aleator si arbitrar); acest acces (aceasta utilizare) revine acum unor functii intrinseci TDA (obiectului), care au acelasi grad de "protectie" si de apartenenta la obiect ca si structurile de date.

Exemplu

Definirea unui obiect (de fapt a unui tip de obiecte) se face in C++ astfel (de remarcat "domeniul" cuprins intre acolade ""; deocamdata se poate face abstractie de cuvintele cheie noi aparute in aceasta declaratie):

class tip_termometru ;    // ... si se termina aici !

Acum functia este "asamblata" cu structura de date si nu mai trebuie sa o "primeasca" drept argument ! (a se vedea prototipul functiei). Definirea acesteia se face acum:

void tip_termometru::display_temp (void)

In acest fel, pentru un obiect interfata cu metodele (functiile proprii) ce acceseaza structurile de date interne se specifica separat de implementare. In acest mod se permite separarea proiectarii de sistem de implementarea sistemului.

Folosind obiectele, un program nu se mai divizeaza in date si cod, ci numai in blocuri ce le contin pe amindoua. Fiecare componenta este considerata acum ca un program in miniatura, programul principal cladindu-se pe aceste blocuri si avind doar rolul unui manager ce controleaza aceste mici "programe".

Deci din punct de vedere al limbajului de programare, un obiect este intocmai unui tip de date abstract, definit de utilizator, fiind o colectie de date impreuna cu functiile asociate ce opereaza asupra datelor.

3.2. Conceptul de clasa

Unul dintre conceptele in jurul caruia s-a creat paradigma OOP este "incapsularea si ascunderea datelor". Acest concept este impus de problemele insurmontabile ce apar in limbalele procedurale prin separarea structurilor de date de functiile ce le utilizeaza.

Incapsularea si ascunderea datelor leaga impreuna aceste 2 componente si limiteaza domeniul in care pot fi utilizate datele, precum si vizibilitatea functiilor la o "regiune" foarte bine localizata. Datele si codul devin inseparabile in cadrul unei noi entitati: "obiectul". Acesta incapsuleaza datele si singurele functii ce sint capabile sa actioneze asupra acestora.

Posibilitatea de incapsulare si ascundere a datelor este oferita de constructia (definirea) claselor.

O clasa defineste caracteristicile unui tip de date abstract (TDA), tip definit de programator, si a setului de functii (operatii) permise asupra acestora. O clasa se construieste in mod asemanator unei structuri, avind in plus cuvinte cheie ce permit "ascunderea" datelor.

In general o clasa se defineste (construieste) astfel:

class <identificator_clasa>

unde:

identificator_clasa = numele clasei ce se defineste

class, private, protected, public = cuvinte cheie (rezervate)

Observatie

Dupa acolada inchisa "}" ce marcheaza sfirsitul definirii unei clase trebuie pus obligatoriu caracterul ";" (punct si virgula), altfel se va obtine o eroare de compilare (este unul dintre putinele cazuri cind se pune ";" dupa "}").

Ca si o structura obisnuita o clasa contine 1 sau mai multi membri. In plus insa, o clasa poate defini 3 nivele de "protectie" (ascundere), astfel incit si membrii clasei sint divizati in 3 grupuri: privati, protejati, publici.

Un membru privat poate fi accesat numai de membrii acestei clase; este asemanator unei variabile declarata in interiorul unei functii ce nu este accesibila decit acelei functii.

Un membru public este accesibil oricarui utilizator al clasei in care a fost definit.

Un membru protected are un nivel de "ascundere" intre privat si public; se foloseste atunci cind se deriveaza o clasa din alta utilizind regula: un membru protected este inaccesibil in afara clasei de definitie (este privat pentru exteriorul clasei), dar este accesibil in clasa in care a fost definit si in orice clasa derivata din cea in care a fost definit, derivare "directa" (pe primul nivel) sau indirecta (pe mai multe nivele) .

Exemple

class xxx ;

Aceasta clasa cu numele "xxx" are 3 membri variabile de tip "int". Variabila "a" nu poate fi utilizata decit de membrii clasei "xxx" (eventuale functii-membru), variabila "c" poate fi "vazuta" (deci folosita) si din exterior (intr-o expresie sau de orice alta functie externa). Variabila "b" nu va putea fi folosita decit in clasa "xxx" sau intr-o eventuala clasa derivata din "xxx" (sau care ar avea la "origine" - prin derivari succesive aceasta clasa).

class rectangle ;

Aceasta clasa defineste un dreptunghi cu 2 laturi, variabilele "wd" si "ht" (variabile membru ale clasei) ce sint private; aceste variabile pot fi "prelucrate" de 2 functii proprii (metode): "set_size" si "area", functii ce sint publice (pot fi apelate din exteriorul clasei).

Observatii

1. In cadrul definirii unei clase sectiunile acesteia (public, protected, private) pot apare in orice ordine si de un numar arbitrar de ori, putind chiar lipsi.

2. In locul cuvintului cheie "class" se pot folosi si cuvintele cheie (din C) de constructie a unei structuri: "struct" sau "union", definindu-se de asemenea o clasa, cu deosebirea:

- daca se foloseste "class" si nu apare nici un cuvint cheie pentru a identifica "tipul" sectiunii, implicit toti membrii se considera "private";

- daca se foloseste "struct" sau "union" si nu se folosesc calificatorii nivelului de protectie a datelor, atunci toti membrii se considera implicit "public";

Diferenta intre "struct" si "union" este cea cunoscuta din C si se refera la variabilele-membru: la "struct" pentru fiecare membru se rezerva o zona de memorie individuala (facind insa parte din blocul de memorie a structurii globale); la "union" pentru toti membrii se rezerva aceeasi zona de memorie (de la aceeasi adresa).

Exemplu

Se poate defini urmatoarea "echivalenta":

struct rectangle ;    };

De aceea, in C++ "struct", fiind un constructor de tipuri definite de programator (TDA) nu mai este nevoie sa se foloseasca cuvintul cheie "struct" cind se declara o variabila de un tip definit cu "struct". Astfel:

structura C structura C++ clasa C++

typedef struct rectangle; }; };

3. Intr-o declaratie de clasa, in afara variabilelor-membru (structurile de date proprii) pot apare si definitii de functii (functii-membru, metode). Din punct de vedere al nivelului de "ascundere", metodele pot aprtine si ele acelorasi 3 categorii de membrii: private, protected, public; posibilitatile de utilizare (domeniile de vizibilitate) sint aceleasi ca si la variabilele membre.

3.3. Clase de obiecte; instantieri

In limbajele de programare un loc central il ocupa conceptul de tip de date. Fiecare variabila ce este prelucrata intr-un program este de un "anumit tip" (predefinit sau definit de utilizator). Dintre acestea, tipul de date definit de utilizator sau abstract este mai important. Acesta se defineste ca un model ce determina un tip de date si un set de operatii asociat. Aceste operatii sint definite pentru acest tip de date si ii caracterizeaza existenta.

Declararea unei variabile de un anumit tip se numeste instantiere.

Exemple

"Betsy e o vaca"

Din aceasta declaratie se pot trage doua concluzii:

a) Betsy apartine categoriei (tipului) "vaca".

b) Betsy este o individualitate.

Betsy este in general ca toate vacile dar difera de celelalte prin insasi individualitatea sa. In acest exemplu "vaca" = clasa, "Betsy" = instanta. Instanta (instantierea) este tangibila, clasa este o abstractie (Bety poate mugi, clasa "vaca" nu poate).

var x,y: integer;

Ce se intelege din aceasta declaratie ? Se aloca doua obiecte (variabile, instante) ale tipului integer, care se vor numi "x" si "y".

Limbajele ce suporta o lista fixa de tipuri predefinite tind sa utilizeze denumirea "type" (PASCAL), pe cind cele ce permit programatorului definirea de noi tipuri agreaza cuvintul "class".

Deci obiectele model (sablon) sint denumite clase si obiectele specifice sint denumite instante. Atunci cind exista pericol de confuzie se folosesc notiunile "obiect clasa" si respectiv "obiect instanta".

Crearea unui "obiect instanta" in C++ se face analog cu definirea oricarei alte variabile. Aceasta deoarece in C++ atit structurile standard C ("struct") cit si clasele propriu-zise ("class") sint intr-adevar TDA (tipuri definite de programator).

Exemple

1. Pentru crearea unui OBIECT de tip "rectangle" (variabila care apartine clasei "rectangle") se foloseste declaratia:

rectangle window_brd;

Aici "window_brd" este "instanta" (obiectul instanta, variabila) iar "rectangle" este "clasa" (TDA, tipul de date definit de programator).

2. Pentru o "fereastra ecran" o definitie de tip (de clasa) ar putea fi:

class window ;

Iar crearea "efectiva" a unei ferestre se face prin declararea unei variabile de acest tip (obiect apartinind acestei clase):

window win1; // win1 este obiectul aprtinind clase window

Obiectele pot fi tratate ca orice alta variabila C (C++). Diferenta esentiala este ca asamblind date si functii putem sa "apelam" o functie ce apartine unui obiect.

De exemplu, ca orice variabila C++ si obiectele pot fi facute "constante": se interzic efectuarea operatiilor asupra lor (indiferent daca sint proprii sau externe) ce le pot modifica structura interna de date (datele-membru). Odata creat un obiect constant valoarea variabilelor-membru pentru ACEL obiect este "inghetata" la cea din momentul crearii, pe tot domeniul de vizibilitate (si deci utilizare) al obiectului respectiv.

Exemplu

class point ;

const point origin; // "origin" este obiect constant

Este mult mai natural sa se descrie cum se comporta o clasa si NU un obiect. Ar fi nefiresc sa se dezvolte un program care descrie cum se comporta un obiect (o vaca) si daca ar fi nevoie de un altul procesul sa se reia! Deci se descrie la nivel de clasa atit comportarea (metodele) cit si "infatisarea" (structuri de date), fiecare obiect urmind a individualiza aceste infatisari (particularizeaza structurile de date).

Clasele au pentru atributele ce le caracterizeaza "posibilitati" de atribuire de valori ("sloturi"), fara insa a avea valori specificate (datele nu se asociaza cu clasa). Instantele, pe de alta parte, au valori asociate cu aceste atribute (sloturi) aceasta fiind modalitatea de individualizare.

Toate in schimb au aceleasi metode, folosind deci aceleasi rutine (cod) pentru a putea raspunde la apelul functiilor proprii (la "mesaje").

3.4. Accesarea membrilor unui obiect; domenii de vizibilitate

Accesarea membrilor unei clase se face intr-un mod absolut similar cu accesul membrilor (cimpurilor) unei structuri in C (in C++ structurile sint clase).

Exemplu

1. Fie urmatoarea structura definita in C:

struct list_node ;

struct list_node lst1;

Se stie ca un membru al unei structuri se acceseaza folosind constructia sintactica:

<nume_structura>.<nume_membru>

astfel incit sint valide urmatoarele expresii:

strcpy(lst1.key,"abracadabra");

old_val=lst1.size;

2. In aceeasi maniera (dupa aceeasi regula sintactica) se acceseaza si variabilele-membru "publice" ale unei clase:

class list_node ;

list_node lobj1;

Se pot referi cele 2 variabile-membru "publice" astfel:

strcpy(lobj1.key,"sesam");

old_val=lobj1.size;

Observatie

Aceasta regula sintactica se aplica NUMAI pentru membrii ce pot fi accesati.

Regulile de vizibilitate specifice claselor sint urmatoarele:

a) domeniul unui nume de clasa incepe in punctul in care apare declaratia si se termina la sfirsitul blocului inconjurator; daca o clasa este declarata in afara oricarei functii (sau oricarui bloc) atunci domeniul de vizibilitate este intreg fisierul in care apare declaratia (de aceea se folosesc fisierele antet cu declaratiile claselor si apoi se includ in toate fisierele sursa - de cod - ce utilizeaza clasa respectiva).

Observatie

Declaratia unei clase in afara oricarui bloc NU este obligatorie !

b) numele de clasa "ascunde" orice clasa, obiect, enumerator sau functie cu acelasi nume din domeniul inconjurator

c) celelalte reguli de vizibilitate (si deci posibilitatile de utilizare) ramin aceleasi cu cele ale C-ului. In plus, insa, clasele mai contin functii-membru pentru care se aplica o nou regula de vizibilitate:

"Functiile-membru pot accesa orice orice variabila-membru a clasei respective ca si cum acestea ar fi "globale" pentru functii".

Se poate privi noul domeniu de vizibilitate al variabilelor - membru ca intinzindu-se intre acoladele de definire a clasei. Oriunde altundeva in afara clasei

- variabilele-membru "private" NU sint accesibile;

- variabilele-membru "protected" pot fi accesate din clase derivate (ca si cum ar fi globale pentru acestea);

- variabilele-membru "publice" pot fi accesate direct.

class list_node

; <─────┴────────────────────────

Din punct de vedere al domeniului de vizibilitate al variabilelor raportat la functia "display", este ca si cum intr-un fisier C am avea declaratiile:

Fis1.C */

char key[20];

int size;

void display(void); /* display poate folosi key si size */

Operatorul de rezolutie de domeniu de vizibilitate "::" se foloseste in mod specific pentru clase (in afara celorlalte utilizari obisnuite). Apare in definirea metodelor claselor, intre numele de clasa si numele metodei pentru a identifica relatia de "apartenenta" a membrului la clasa; se poate utiliza si oriunde pot apare confuzii de accesare a membrilor.

3.5. Definirea metodelor unei clase; apelul prin mesaje

Functiile ce apar in definitia unei clase (in interiorul acesteia) se numesc functii-membru sau metode. Acestea, ca si functiile C uzuale, au un antet in care se specifica numele metodei (functiei), argumentele si tipul rezultatului intors de functie, si un corp ce contine instructiunile propriu-zise.

In functie de locul in care apare corpul unei metode se disting 2 tipuri:

- functii-membru (metode) "in-line"

- functii-membru (metode) uzuale (cu definitie externa)

a) Functiile "in-line" sint acelea in care atit antetul cit si corpul apar in definitia clasei.

Exemplu

class rectangle

int area (void)

Acest tip de metode se folosesc in cazul in care corpul este alcatuit dintr-un numar mic de instructiuni simple. Din punct de vedere al apelului sint tratate ca si functiile declarate explicit cu cuvintul cheie "inline".

b) Functiile ce sint definite uzual au corpul definit in exteriorul declaratiei clasei careia ii apartin, in definitia clasei neavind decit prototipul. In exteriorul definitiei clasei careia ii apartin, corpul metodei este identificat prin numele sau precedat de numele clasei si de operatorul de domeniu de vizibilitate "::". In acest caz operatorul "::" indica apartenenta unei metode la o clasa (careia ii apartine de fapt).

Exemplu

class rectangle ;

// definirea metodei "set_size" ce apartine clasei "rectangle"

void rectangle::set_size (int w, int h)

Elementele ce intervin in definirea acestei metode pot fi identificate in figura urmatoare:

tipul returnat de metoda

│ numele clasei

│ │ operatorul de domeniu de vizibilitate

│ │ │

antetul─>void rectangle::set_size(int w,int h)

metodei └───┬────┘ │ │

│ numele metodei

membri privati ai

clasei (pot fi acesati)

Fig. 5

Elementele constitutive ale unei metode

Observatie

Se remarca inca o data modul in care se acceseaza variabilele-membru ale unei clase dintr-o metoda. Chiar daca metoda se defineste in afara declaratiei de clasa, declaratia metodei ramine in INTERIORUL definitiei clasei respective, beneficiind de domeniul de vizibilitate al clasei respective.

In ceea ce priveste "aranjarea" definirii si implementarii metodelor unei clase se poate folosi mecanismul specific C-ului (pastrat si in C++): se definesc metodele unei clase (impreuna cu structurile de date proprii) intr-un fisier "header" (cu extensia ".H" sau ".HPP") si se implementeaza metodele clasei intr-un alt fisier (cu extensia ".C" sau ".CPP"). Se recomanda ca in acest fisier sa se implementeze NUMAI metodele clasei respective (fara alte functii). Este obligatoriu ca acesta sa contina o directiva (#include) pentru includerea fisierului header. Fisierul ce contine implementarea se compileaza separat, rezultind in acest mod un "modul". Programul principal (fisierul ce contine functia "main") ce utilizeaza aceasta clasa include fisierul header (cu #include) al clasei respective; se compileaza si el separat, urmind ca "legatura" dintre cele 2 module (program principal si modulul clasei) sa se faca la link-editare.

Exemplu

Se defineste o clasa "counter" ce implementeaza un contor numeric ce poate lua valori numai in domeniul 0..65535 (de tip unsigned int). Asupra acestui contor se permit operatiile (metodele) urmatoare: incrementare, decrementare (fara a depasi limitele), citirea (afisarea) valorii curente a contorului si aducerea la zero a valorii lui (reset-area).

Conform strategiei discutate anterior se creaza 2 fisiere pentru aceasta clasa:

// Fisierul 1: "count.h"

// Defineste o clasa numita "counter"

class counter ;

// Fisierul 2: "count.cpp"

// Implementarea metodelor clasei "count"

#include "count.h" // pentru a se cunoaste definitia clasei

void counter::reset (void)

void counter::increment (void)

void counter::decrement (void)

unsigned int counter::val (void)

Observatie

In unele cazuri, daca implementarea metodelor este foarte laborioasa, se poate crea un set de fisiere de implementare, fiecare continind implementarea unei metode.

// Fisierul 3: "test.cpp"

// Utilizeaza clasa "counter" definita anterior

#include <stdio.h> // <...> pentru header standard

#include "count.h" // "..." pentru header programator

void main (void)

printf("\nDupa bucla c2 = %u", c2.val());

for (int i=0;i<5;++i)

c2.decrement();

printf("\nLa sfirsit c2 = %u", c2.val());

Apelarea unei metode a unei clase pentru un obiect particular (instanta) se face printr-un mecanism special: trimiterea mesajelor.

Avind in vedere ca un obiect contine atit date cit si metode, pentru accesul la aceste date nu este normal (si nici posibil uneori) sa tratam un obiect ca un operand static. Putem aplica o functie pentru a obtine o actiune, un rezultat asupra unei date statice, dar nu asupra unui obiect. Singura modalitate de a accesa datele intrinseci ale unui obiect este de "a-i cere" obiectului sa faca acest lucru folosind metodele proprii. Obiectele sint structuri active, avind in ele inglobate un set de operatii ce pot fi activate .

Unui obiect i se cere sa execute o operatie prin trimiterea catre acesta a unui mesaj. Obiectele accepta mesaje ca intrari, si daca sint corespunzatoare genereaza la iesire alte mesaje.

Cind un obiect receptioneaza un mesaj se "uita" la colectia sa de metode pentru a selecta operatia ce implementeaza mesajul. In continuare se executa metoda corespunzatoare (selectata), controlul reintorcindu-se catre apelant.

Exemplu

Pentru un obiect din clasa "rectangle" definita anterior selectarea si executia unor metode prin transmiterea de mesaje se face astfel:

rectangle r; // definirea unui obiect din clasa rectangle

int arie; // variabila intrega pt. calculul ariei

r.set_size(10,20);     // se transmite mesajul de setare a dimens.

arie=r.area(); // se transmite mesajul de calcul a ariei

// pt. obiectul r; rezultatul este in "arie"

Exemplu

Se prezinta un exemplu complet de creare (definire) si utilizare a unei clase si a obiectelor individuale din acea clasa. Obiectele sint prelucrate prin intermediul metodelor proprii, activate (apelate) prin transmiterea de mesaje. Clasa va reprezenta (va modela) un ceas electronic, ce pastreaza si afiseaza ora intr-un format simplu "23:59:59". Ceasul poate sa "ticaie" (sa i se incrementeze timpul memorat cu 1 secunda), iar daca este pus sa repete aceasta (intr-o bucla) poate sa si "mearga" (afiseaza continuu, din secunda in secunda timpul):

#include <stdio.h>

// Se defineste clasa "clock"

class clock ;

// Functia-membru "set" : "potrivire" a ceasului

void clock::set (int h, int m, int s)

// Functia-membru "tick" : "ticairea" ceasului

void clock::tick (void)

}

// Functia-membru "display" pt. "afisarea" orei memorata de ceas

void clock::display (void)

// programul principal (de testare)

void main (void)

Observatii

1. Se poate remarca inca o data uniformitatea "tratarii" datelor si codului in cadrul unui obiect si din sintaxa mecanismului de apel al unei metode. Se observa ca se mentine aceeasi sintaxa ca la accesarea memrilor variabile (structurile interne de date).

Deci indiferent daca e cod sau data, un membru al unui obiect se acceseaza dupa sintaxa:

<nume_structura>.<nume_membru>

2. Se pot folosi pointeri pentru a indica obiecte, intocmai ca si pentru orice alta variabila C (C++). De exemplu, declaratia:

clock * big_ben_ptr;

creaza un pointer la un obiect de tip "clock" (din clasa "clock"), ca in figura urmatoare:

┌─────┬──────┬──────┐

clock* big_ben_ptr ─────>│ hr │ min │ sec │}date private

├─────┴──────┴──────┤

│ set() │─┐

├───────────────────┤ │

│ tick() │ │metode publice

├───────────────────┤ │

│ display │─┘

└───────────────────┘

Fig. 6

Pointeri la obiecte

Pointer-ul poate fi utilizat ca oricare alt pointer C++ ce indica (refera) o variabila: se aloca spatiu de memorie si i se atribuie adresa acestuia, se poate folosi in operatii aritmetice permise, etc.), poate sa fie "dereferit" (sa se acceseze continutul locatiei/locatiilor referite de pointer), etc.

Indicarea continutului locatiei indicata de pointer (dereferirea) se face utilizind tot operatorul "->" atit pentru indicarea datelor (variabilelor-membru) cit si pentru metode (functii-membru). Pentru declaratia de mai sus:

big_ben_ptr->hr = ...;

big_ben_ptr->set(11,59,59);

In plus, in C++ este posibil sa avem pointeri la membrii unei clase, ca in exemplul urmator:

class mynum // initializare

void add (int n) // incrementare cu pasul "n"

mynum a, b; // 2 obiecte din clasa mynum

int mynum::*p; // p este un pointer (int*) la un membru

// de tip int al clasei "mynum"

Urmatoarele operatii sint valide in C++:

p=&mynum::num; // acum se stie catre cine indica "p"

a.*p=42; // membrul "num" al obiectului "a" = 42

b.*p=17; // echivalent cu b.num = 17

Se observa selectarea cimpurilor (prin intermediul pointerului) folosind operatorul cunoscut de selectare a unui cimp dintr-o structura: "." (punctul).

Mai mult decit atit, membrii unei clase inseamna si metodele acelei clase, astfel incit se pot crea si pointeri la metodele (functiile-membru) ale unei clase:

void (mynum::*q)(int);

Aici "q" este pointer la o functie a clasei "mynum" ce are ca parametru un "int" si intoarce "void". Iata si citeva exemple de utilizare a pointerilor la functiile-membru:

q=&mynum::set; // q indica acum metoda "set"

a.*q(3); // echivalent cu a.set(3);

b.*q(4); // echivalent cu b.set(4);

q=&mynum::add; // q indica acum metoda "add" a clasei

// "mynum"

a.*q(5); // echivalent cu a.add(5)

Se pot combina pointerii la obiecte cu pointerii la membrii unui obiect, de exemplu astfel:

mynum* y = new(mynum); // se declara "y" un pointer la un obiect

// de clasa "mynum"; i se aloca memorie

int mynum::*p; // "p" este pointer la un membru "int" al

// clasei "mynum"

p=&mynum::num; // p indica variabila "num"

Pentru aceste declaratii, urmatoarea instructiune:

y->*p=25;

va atribui variabilei membru "num" (ce este indicata de pointerul "p") a obiectului alocat dinamic si indicat (referit) de pointerul "y" valoarea 25. In aceasta expresie:

"y->" inseamna obiectul referit de "y"

"*p" inseamna variabila membru referita de "p"

Exemple

Pentru clasa "mynum" definita anterior se prezinta alte exemple de utilizare a pointerilor la obiecte si la membrii unui obiect:

a) mynum* c = new(mynum)

declarare si initializare pointer la obiect din clasa "mynum".

b) int mynum::*p;

se declara (fara a se initializa) un pointer la un membru de tip "int" al unui obiect din clasa "mynum".

c) p=&mynum::num;

se initializeaza pointerul declarat anterior la membrul "num" (de tip "int") al clasei "mynum".

d) void (mynum::*q)(int);

se declara (fara a se initializa) un pointer la o functie-membru a clasei "mynum", functie ce are 1 parametru de tip "int" si returneaza o valoare de tip "void".

e) q=&mynum::add

pointerul q va indica functia "add" a clasei "mynum" (functie- membru ce respecta conditiile anterioare referitoare la argumente si valoarea returnata).

f) c->*p=55;

pentru obiectul alocat dinamic la punctul a) si pointat de "c" se refera membrul "num" indicat de pointerul "p" prin initializarea de la punctul c), caruia i se atribuie valoarea 55.

g) c->*q(4);

pentru obiectul alocat dinamic la punctul a) si pointat de "c" se refera functia-membr "add" indicata de pointerul "q" prin initializarea de la punctul e), care se apeleaza cu argumentul actual "4" (de tip "int").

3. Un tip special de metoda este "metoda constanta". Acestea sint inspirate de mecanismul (deja cunoscut) al variabilelor si obiectelor constante. Ratiunea ce a impus a definirea unor astfel de metode este data de observatia urmatoare: "un obiect declarat constant nu trebuie sa poata sa "apeleze" metode declarate in mod obisnuit.

Exemplu

#include <stdio.h>

#include <math.h>

class point

double dist (void)

void move(double dx, double dy)

void main(void)

Se observa ca apelul metodei "move" modifica valorile variabilelor-membru pentru obiectul "p". Acest lucru nu este permis pentru "p" care este un obiect "constant". Chiar daca metoda "dist" nu modifica structurile proprii de date, apelul acesteia tot nu este permis deoarece compilatorul nu are cum sa faca distinctie intre o metoda ce modifica valorile variabilelor- membru si o metoda ce nu le modifica.

Solutia, pentru folosirea totusi a metodei "dist" (care stim NOI ca nu modifica valorile variabilelor-membru) este aceea de a diferentia metodele unui obiect in:

- metode "uzuale" ce (pot) sa modifice valorile variabilelor proprii clasei din care fac parte;

- metode "constante" ce nu le modifica (si nici nu li se va permite existenta in corpul lor a unor instructiuni ce ar putea conduce la aceasta).

O metoda se declara "constanta" prin adaugarea cuvintului cheie "const" in antetul functiei, dupa lista parametrilor formali ai acesteia, astfel:

class point

void move (double dx, double dy)

In aceste conditii, pentru un obiect "constant" se poate "apela" metoda "dist" dar in continuare NU se poate apela metoda "move", astfel:

// ...

const point p;

p.set(40,12); // initializare, se poate si altfel

printf("Dist. pina la origine = %f\n",p.dist()); // OK!!!

p.move(1,1); // ILEGAL, p=const ?!?!

Observatie

Intr-o metoda constanta compilatorul NU permite nici o operatie care ar putea conduce la modificarea valorilor variabilelor-membru pentru acel obiect.

4. Daca s-ar mai defini mai multe obiecte din clasa "rectangle" definita anterior si li s-ar trimite mesaje similare, de exemplu:

rectangle r, rprim;

r.set_size(10,20);

rprim.set_size(7,8); // etc...

se poate crea impresia ca fiecare dintre obiectele "r" si "rprim" detin cite o copie a metodelor (functiilor) "set_size", "area", etc. In realitate ceea ce detine fiecare obiect (instanta) este un pointer catre o singura o singura functie, ca in figura urmatoare:

Obiectul r ┌─────────────────────────────────────┐

┌──────┬─────┐ ┌─────> │void rectangle::set_size(int w,inth) │

│wd │ht │ │ ┌───>│ │

├────────────┤ │ └─────────────────────────────────────┘

│& area() ├──│──┐

└────────────┘ │ │

Obiectul rprim │ │

┌──────┬──────┐ │ │

│wd │ht │ │ │ ┌───────────────────────────────────┐

│ 7 │ 8 │ │ └───>│int rect::area(void) │

├──────┴──────┤ │ ┌───>│ │

│& area() ├────┘ └───────────────────────────────────┘

└─────────────┘

segmentul de date segmentul de cod

Se observa ca un mesaj este o expresie analoaga cu un apel de functie ce are un "pas" in plus: mecanismul de selectie.

Atit functiile ordinare cit si mesajele intorc valori, la ambele se salveaza contextul pe stiva, la ambele apelantul e blocat pina la terminarea apelatului.

Ce le deosebeste ? Ambele au argumente, dar pe cind o functie ordinara poate sa aiba 0 argumente, un mesaj are intotdeauna cel putin 1 argument: cel ce identifica obiectul ce trebuie sa receptioneze mesajul.

Partea din mesaj ce-i spune obiectului ce sa faca se numeste "selector de mesaje". Corespunde cu numele functiilor dar intre ele exista o deosebire cruciala: numele functiei defineste in mod unic o parte de program (cod) ce se va executa, in timp ce selectorul nu, deoarece codul ce se va executa depinde si de obiectul caruia ii este adresat.

Exemplu

Functia ordinara "sqrt" presupune ca i se transmite ca parametru, de exemplu, un argument de tip real (float).

Mesajul prin selectorul sau identifica direct operatia "logica" (extragerea radacinii patrate), dar efectiv rutina (codul) ce se executa este aleasa de receptorul mesajului, astfel incit daca se transmite mesajul unui tip oarecare, rezultatul depinde in intregime de tipul acelui obiect.

In acest fel una dintre responsabilitatile programatorului a fost transmisa masinii.

In timp ce functia spune "cum sa faci", mesajul spune "ce sa faci", lasind alegerea modului (cum) pina in momentul in care mesajul este executat.

Spunind ca "un obiect stie cum sa faca ceva" inseamna ca atunci cind un mesaj este executat avind ca receptor un obiect mecanismul de selectie alege una dintre operatiile obiectului.

Singura diferenta intr-adevar substantiala intre programarea conventionala si OOP este acest mecanism de selectie. Semnificatia lui fundamentala este aceea ca muta responsabilitatea peste granita producator-consumator, de la consumator la producator.

In programarea conventionala, un consumator (al unui serviciu) este responsabil pentru alegerea functiei care va functiona corect pe datele ce alcatuiesc serviciul respectiv. Mecanismul de selectie muta aceasta responsabilitate spre producator.

Aceasta reprezinta un mecanism (o unealta) pentru constructia sistemelor (system-building tool), permitind producatorului sa prezinte o interfata "curata" si clara in jurul serviciului ce-l ofera.

Pasarea mesajelor, pe de alta parte, permite obtinerea si unor alte avantaje enorme: garanteaza ca modificarea unui obiect nu afecteaza decit ceea ce este continut in obiectul respectiv.

In plus, datorita faptului ca fiecare obiect isi contine datele si obiectele proprii, necomunicind cu "exteriorul" decit prin schimburi de mesaje, se poate crea un program prin concentrarea asupra unui obiect, rezultind o structurare mult mai profunda a programelor decit daca s-ar fi folosit oricare alta metoda din limbajele conventionale.

Un consumator are acces complet la metodele unui obiect ce ofera anumite servicii (prin intermediul mesajelor) dar nu are acces la date. Din punct de vedere al consumatorului un obiect este ca o capsula ce ofera niste functii. Aceasta proprietate se numeste incapsulare.

3.6. Incapsularea si ascunderea datelor in sistemele OOP

Se prezinta in figura urmatoare modalitatea de accesare a obiectelor in sistemele OOP din punctul de vedere al consumatorului (de functionalitate software).

│ │

│ INTERFATA │ ┌───────────────────┐

│ │ │ X, Y, Visible │

│ ┌────┐ │ │ alte date... │

│ │ │ │ │ ┌──────────────┐ │

x──────────┼────┼── │ ├───────── │ │ implementare │ │

│ │ │ │ │ └──────────────┘ │

Consumator │ └────┘ │ │ Show, Hide, │

│ │ │ alte metode... │

│ │ └───────────────────┘

│ │

Fig. 8

Incapsularea si ascunderea datelor

Legarea (cuplarea) structurilor de date ce definesc un TDA cu functiile asociate, utilizate pentru manipularea acestora, se numeste "incapsularea datelor". Unitatea de incapsulare este obiectul.

Incapsularea este procesul prin care sint definite obiecte software individuale. Incapsularea defineste:

1. o margine clara ce determina domeniul codului intern al obiectului;

2. o interfata ce descrie cum interactioneaza obiectele unele cu altele;

3. o protectie a implementarii interne, inaccesibila in exteriorul domeniului definit de obiect.

Astfel un obiect ascunde datele proprii si particularitatile metodelor proprii pentru alte obiecte. Fiecare obiect este ca o capsula sau "cutie neagra".

Inaccesibilitatea structurilor de date interne ale unui TDA (imposibilitatea accesarii din "exterior") se numeste "ascunderea datelor".

Intr-un sens un obiect este un program virtual. Dindu-i-se ceva la intrare, prin intermediul unui mesaj, el aplica metodele proprii corespunzatoare si produce iesiri, tot mesaje.

Exista foarte multe avantaje ce decurg din aceasta "decuplare" a obiectelor de referinte globale. Daca din exterior nu se cunoaste reprezentarea interna a structurilor de date, atunci se poate activa aceasta reprezentare fara ca cel ce "consuma" acel obiect sa simta aceasta. De exemplu un tablou se poate transforma in lista sau arbore.

Separarea in acest fel a definirii unui tip de date abstract (TDA) de implementarea sa reprezinta un salt urias inainte in tehnologia software in general si in software engineering in special.

Unele dintre primele limbaje care au implementat eficient si au utilizat pe scara larga aceste concepte pentru crearea sistemelor software au fost Modula-2 si ADA, fara insa a "trece" de limitarile impuse de utilizarea NUMAI a acestor concepte.

3.7. Argumentul "ascuns" al metodelor: "this"

Se poate pune intrebarea: "Cum se acceseaza variabilele membru ale unui obiect din interiorul unei metode ?", care este de fapt mecanismul ce permite acest acces ?

La apelul unei metode, in afara parametrilor "vizibili" (uzuali) care se transmit unei metode, in mod automat compilatorul genereaza cod pentru transmiterea a inca unui parametru fiecarei metode: este un pointer la obiectul ce receptioneaza mesajul (pentru care se apeleaza metoda). Acest pointer are un nume predefinit (cuvint rezervat C++): "this".

In orice functie (nestatica) a clasei "clasa" acest pointer e declarat implicit ca:

clasa* this;

O paralela intre o metoda C++ si o functie C ar putea fi ca in exemplul:

1. pt C++:

void clock::set(int h, int m, int s)

2. iar in C:

void set (clock*this, int h, int m, int s)

Pointerul "this" e intodeauna transmis ca prim argument. El se poate folosi in orice metoda C++, dar in majoritatea cazurilor (accesarea variabilelor membru) folosirea lui este reduntanta.

Exemplu

In C++ metoda de mai sus se poate scrie:

void clock::set(int h, int m, int s)

O utilizare nereduntanta a pointerului "this" poate fi, de exemplu, crearea unor structuri "inlantuite" de tip liste.

Exemplu

Se creaza o lista simplu inlantuita de obiecte ce au ca informatie utila un nume (de ciine de exemplu):

#include <stdio.h>

#include <string.h>

class dog ;

void dog::set_name(char* dog_name)

char* dog::dog_name(void)

void dog::create_tail(char* name_of_tail)

char*dog::get_name_of_tail(void)

void main(void)

3.8. Membrii statici ai unei clase

Fiecare obiect individual declarat intr-un program are propria "copie" a structurii de date corespunzatoare clasei careia ii apartine, astfel incit chiar daca apartin aceleiasi clase obiectele individuale acceseaza structuri de date (variabile membru) complet diferite. In toate limbajele de programare este posibil sa se declare structuri de date (variabile) "partajate".

In C++ este posibil sa se defineasca structuri de date definite intr-o clasa (variabile membru) care sa fie "partajate" de toate obiectele ce apartin clasei respective (si numai lor !), adica toate obiectele acceseaza aceiasi zona de memorie (aceiasi copie). Acesti membri se numesc membri statici si se declara adaugind cuvintul cheie "static" inainte de declaratia corespunzatoare din clasa.

Exemple

1) Pentru definitia de clasa:

class abcd ;

si obiectele:

abcd A,B,C,D;

putem avea "figura":

┌───────────────┐

│ object A │

│ char ch; │

┌──┼───────────────┼─┐ │

│ object │ ┌┼───────────────┼─┼─┼─────────────────────┐

│ char ch; │ ││ static int s; │ │ │ char ch; object │

│ B │ │└───────────────┘ │ │ D │

│ └──────────────────┼─┼─────────────────────┘

│ char ch; │

│ │

│ │

│ │

│ object C │

│ │

│ │

└────────────────────┘

Fig. 9

Membrii statici/uzuali ai unei clase

2) Membrii statici sint folositori cind diverse obiecte ale aceleiasi clase (de exemplu "ferestre" ce apar pe ecran in diverse pozitii, forme, cu diverse continuturi) trebuie sa acceseze aceiasi structura de date (de exemplu o stiva in care sa fie pastrate informatii despre ferestrele vizibile: ordinea de activare, etc). In acest caz stiva este UNICA pentru toate obiectele din clasa prin declararea ei ca fiind "statica":

class window ;

Pentru 2 obiecte "ferestre" definite astfel:

window edit_win, error_win;

structurile de date pot fi reprezentate ca in figura:

edit_win

│ popup_stack *─┼─────────┐

│ x │ y │ │

│ wd │ ht │ │

│ * save_image │ └───────> │ │───> │ │──>...

┌───────> │ │ │ │

└───────────────┘ │ lista folosita ca stiva

│ (o singura copie in memorie)

error_win │

│ popup_stack *─┼─────────┘

│ x │ y │

│ wd │ ht │

│ * save_image │

Fig. 10

Lista statica (variabila "globala")

Se observa ca toate datele-membru sint individuale (diferite copii) cu exceptia membrilor statici ce sint accesati in comun (partajate) de TOATE obiectele clasei "window".

Observatie

De ce nu se foloseste o variabila globala "veritabila" ? Pentru ca in acest fel variabila respectiva NU ar mai apartine clasei! Fiind o caracteristica a clasei trebuie sa apartina acesteia, si NUMAI acesteia !

Accesarea variabilelor-membru statice se poate face in doua moduri:

a) ca orice variabila membru, prin "intermediul" unui obiect:

A.s=10

(daca este in domeniul de vizibilitate)

b) folosind operatorul "::" (domeniu de vizibilitate); se acceseaza membrul static folosind numele clasei si numele variabilei membru:

abcd::s=0;

Folosind aceasta sintaxa de accesare, un membru static poate fi referit chiar inaintea declararii unui obiect din acel tip (crearii unei instante).

Este posibil sa se defineasca si metode statice in acelasi mod in care se definesc variabile membru statice. Scopul pentru care se definesc metode statice este in principal de a "opera" asupra variabilelor membru statice. Pot fi accesate intocmai ca si variabilele membru statice, prin cele 2 metode: prin "intermediul unui obiect" (mai putin sugestiv) sau folosind operatorul de domeniu "::" pentru o clasa (mult mai aproape de sensul pentru care se definesc).

Exemplu

class abcd ;

abcd A,B,C,D; // declararea obiectelor

A.init(10); // apel metoda statica, nerecomandat

abcd::init(2); // apel cu numele clasei , OK!

Observatie

Deosebirea fundamentala fata de metodele "uzuale" este aceea ca metodele statice nu pot sa acceseze direct variabile membru "uzuale" (in afara celor statice!). Pentru a le accesa trebuie sa primeasca un obiect din acea clasa ca parametru.

Exemplu

void abcd::init(int val)

Aceasta se datoreaza faptului ca metodele statice nu primesc, cum primesc metodele uzuale, un argument "ascuns" (transparent programatorului) care sa indice obiectul asupra caruia sa face referire (argumentul "this"). A se vedea paragraful anterior.

In interiorul unui membru "uzual" al clasei atit variabilele statice cit si metodele statice se acceseaza "normal", ca orice alt membru, fara a fi nevoie de specificarea numelui clasei (calificator de clasa).

3.9. Obiecte membre ale altor obiecte;clase imbricate

Se stie ca in C era posibil sa se declare structuri complexe "imbricate" (structuri in structuri). In multe situatii aceasta conducea la cresterea expresivitatii programului.

Exemplu

Pentru o structura ce memoreaza numele unei persoane si numarul sau de telefon (in structura completa: cod_tara, prefix_intern, numar_propriu-zis) se poate face declaratia:

struct nume_nr telefon;

Folosirea este acum foarte expresiva, deoarece structura are 2 parti importante componente:

- numele (sir de 80 caractere)

- numar de telefon (o alta structura)

Exemplu

struct nume_nr clienti[100];

strcpy(clienti[0].nume,"Ion Iliescu");

strcpy(clienti[0].telefon.cod_tara,"40");

strcpy(clienti[0].telefon.prefix,"90");

strcpy(clienti[0].telefon.nr,"121212");

Avind in vedere ca in C++ stucturile sint doar un caz particular de clase, astfel de constructii complexe sint permise si pentru clase. In acest fel un obiect de un anumit tip (clasa) devine membru al unui alt obiect (din alta clasa).

Aceasta facilitate este foarte folositoare, avind in vedere ca un obiect incapsuleaza atit date cit si functii, in acest fel functiile devenind membre ale obiectului din exterior.

Un posibil exemplu ar fi descrierea unei ferestre intr-o aplicatie grafica, fereastra ce se compune la rindul sau din mai multe obiecte diferite (cu caracteristici - date si comportari - functii proprii bine definite):

class window ;

In acest exemplu fereastra se compune din 3 obiecte: o margine, un cursor si un "buton de scroll". Fiecare este DEZVOLTAT separat si apoi se utilizeaza "gata facut" (structurile de date impreuna cu functiile ce ii caracterizeaza comportarea).

Singura problema ce apare este aceea a domeniului de vizibilitate: se pot crea clase "ascunse" in alte clase ? NU! Orice clasa indiferent unde e definita (in interiorul unei clase sau in exterior) este considerata externa (vizibilitate globala in cadrul fisierului respectiv). Adica, pentru exemplul precedent:

class nume_nr telefon;

aceasta definitie este echivalenta cu:

class nr_telefon ;

class nume_nr ;

3.10. Constructori si destructori

3.10.1. Descriere generala

Limbajul C permite o mare flexibilitate in ceea ce priveste initializarea variabilelor. Aceasta flexibilitate se transmite partial, insa in C++. Astfel, in C orice variabila ce se defineste poate fi initializata sau nu, poate fi complet/partial initializata (in cazul variabilelor stucturate), ca in exem­plul:

struct part_rec ;

part_rec w=;

part_rec tab[2]= ,

};

In C++ aceasta tehnica de initializare are citeva restrictii, si acestea sint fundamentale:

a) nu pot exista mai multi "initializatori" decit date de initializat

char msg[10]="Acesta este un mesaj"; //EROARE

char msg[3]="ABC" ; //EROARE, sirul are 4 componente

b) obiectele ce au membri privati NU pot fi initializate in acest mod.

Astfel, pentru a permite initializarea obiectelor complexe (create din clase) si pentru eliberarea spatiului detinut in memorie de acestea ("stergerea lor") se folosesc functii speciale: constructori si destructori. Acestea sint functii- membru ale claselor (metode) ce specifica instructiunile de initializare ce trebuie sa se execute cind se creaza un obiect (constructorul) si ce trebuie sa se execute cind un obiect nu mai este folosit (destructorul). Find metode acestea sint "impachetate" cu ceilalti membri ai obiectului. Rezulta astfel inca un avantaj major al "variabilelor" C++ asupra variabilelor C: posibilitatea specificarii unor functii definite de programator atasate variabilei, functii ce se executa la alocarea/dealocarea variabilei respective.

Constructorul este apelat ori de cite ori o variabila "intra in domeniul de vizibilitate", adica atunci cind executia unui program "atinge" instructiunea in care aceasta este declarata. Un constructor poate indeplini o mare varietate de sarcini, ca de exemplu initializarea variabilelor interne, alocari dinamice de memorie, etc.

Destructorul este apelat ori de cite ori se "atinge" punctul final al domeniului de vizibilitate al unei variabile, ca de exemplu: intoarcerea dintr-o functie, terminarea unei bucle (pentru variabilele automatice declarate in interiorul unei bucle, etc). Poate realiza o mare varietate de sarcini, dar in mod uzual este folosit pentru a elibera memoria alocata dinamic si asociata cu variabila pentru care e apelat.

"Perioada" dintre momentul crearii unei variabile si momentul distrugerii sale se numeste "durata de viata".

Astfel, durata de viata a diverselor obiecte este:

a) obiectele automatice = sint locale unei functii si de aceea sint pastrate (create si utlizate) pe stiva ! Exista atita timp cit "exista" functia (se creaza in prologul functiei, se distrug in epilogul acesteia).

b) obiectele statice = sint declarate in afara functiilor (sau cu ajutorul calificatorului "static"). Sint create la inceputul programului (prologul functiei "start-up") si exista pe intreaga durata de executie a programului.

c) obiecte dinamice = sint utilizate prin alocare de spatiu din "heap" (o zona de memorie specifica). Sint create cu operatorul "new" si distruse cu "delete".

d) obiecte membre ale altor obiecte = sint create/distruse atunci cind obiectul caruia ii apartin este creat/distrus.

e) obiectele claselor derivate = sint obiecte ce apartin claselor derivate din alte clase (de baza). Mai intii trebuie apelat constructorul clasei de baza (se creaza baza) si apoi constructorul clasei derivate (apoi se creaza obiectul derivat).

3.10.2. Declararea constructorilor si destructorilor

Constructorii si destructorii unei clase se declara respectind urmatoarele reguli sintactice:

3.10.2.1. Declararea/apelarea constructorilor

O functie constructor se declara folosind urmatoarea conventie de nume:

Orice functie-membru (metoda) ce are acelasi nume cu numele clasei careia ii apartine este tratata ca functie-constructor.

Exemplu

class clock;

// definirea constructorului (ca orice alta metoda)

clock::clock(int h, int m, int s)

Din acest moment,la fiecare declarare de variabila (obiect) de tip "clock" se apeleaza constructorul:

clock rolex(6,0,0),orex(10,2,4);

parametrii din paranteza fiind utilizati ca argumente pentru constructor asa cum se arata si in figura urmatoare:

┌───────────────────────┐

│ │ │ clock(int h, int m, int s)

clock orex (10, 2, 4); <──────────┐ │

│ │ └───┬───┘ │ │

│ │ │ │ corp

│ │ │ │ constructor

numele │ │ declararea unui

clasei │ │ obiect ("variabila")

│ │

numele │

obiectului │

argumente pentru

constructor

Fig. 11

"Apelarea" unui constructor

Observatii

1. Similitudinea cu metodele uzuale "merge" pina la intoarcerea unei valori. Un constructor NU intoarce nici un fel de valoare (nici chiar void), adica in constructor NU poate apare instructiunea "return <argument>"! (poate apare in schimb "return" simplu, fara argument).

2. Apelul unui constructor se poate face in 3 moduri:

a) apelul "scurt" (implicit)

clock orex(10,2,4);

cu parametrii de apel in paranteze, dupa numele obiectului.

b) apelul "lung" (explicit)

clock orex=clock(10,2,4);

se specifica dupa semnul = numele constructorului si parametrii actuali de apel (ca o metoda uzuala).

c) apelul "standard"

clock orex=43200; // orex(43200);

Daca are un singur parametru se pot omite numele si parantezele, sintactic semanind foarte mult cu initializarea variabilelor "uzuale" din C.

Exemple

Exista foarte multe situatii cind un constructor nu numai initializeza date interne, dar aloca dinamic si memorie, ca in exemplu:

class string ;

string::string(int sz)

Se pot declara acum obiecte de tip "string" cu lungimi (si deci spatii de memorare rezervate) diferite:

string sir_mic(2), sir_mare(500);

Aceasta caracteristica este utila cind obiectele apar in functii ce au ca parametru (si) lungimea tabloului de caractere:

void functie(int n)

Obtinindu-se in acest fel un sir de dimensiune variabila efectiv, dimensiune determinata de executie.

3.10.2.2. Declararea/apelarea destructorilor

O functie destructor a unei clase se specifica prin atribuirea numelui clasei pentru numele functiei dar precedat de simbolul: "~" (tilda).

Exemplu

Pentru clasa definita mai sus:

class string;

string::~string(void) // definirea destructorului

Observatii

1. Un destructor este apelat implicit ("automat de catre compilator") atunci cind se atinge sfirsitul domeniului de vizibilitate pentru obiectul respectiv (daca este din clasa de variabile "automatic") sau la sfirsitul programului pentru obiectele globale sau statice.

2. La fel ca si constructorul, un destructor NU intoarce nici o valoare (adica nu poate contine instructiunea "return <expresie>" in corpul sau), dar in plus NU poate avea nici parametri, pentru ca destructorii NU pot fi apelati explicit.

3.10.3. Utilizarea constructorilor si destructorilor

Fiind o functie C++, un constructor beneficiaza de toate caracteristicile acesteia: poate fi suprapus si poate avea argumente cu valori implicite. Astfel se permite specificarea mai multor "versiuni" de initializari pentru obiectele ce pot avea diverse reprezentari externe (se specifica diverse tipuri sau liste de argumente pentru constructori, acestia fiind cei apelati la crearea si intializarea obiectelor).

Exemplu

Pentru clasa "clock" definita anterior se pot specifica 2 tipuri de initializare (2 constructori):

- initializare in forma "ore:minute:secunde"

- initializare numar de secunde si apoi acestea se transforma intr-o reprezentare uzuala "ore:minute:secunde".

class clock // constructorul 1 (in-line)

clock (int h,int m,int s); // constructorul 2, forma standard

// etc...

// definirea constructorului 2:

clock::clock(int h,int m,int s)

Astfel daca se declara doua obiecte:

clock orex(13,55,46); // (1) : apel constructor (2)

clock rolex(123345); // (2) : apel constructor (1)

se va obtine pentru variabila interna data :

data=50146; // pentru (1)

data=12345 // pentru (2)

Selectarea constructorului se face dupa tipul si/sau numarul argumentelor folosind regula de la functiile suprapuse uzuale.

Observatii importante

1. Destructorii NU pot fi suprapusi ! Deoarece neavind argumente nu se poate face destinctie intre diferitele "versiuni" de posibili destructori.

2. Intocmai ca functiile-membru (metodele) uzuale si constructorii pot avea valori implicite pentru argumente:

clock::clock(long sec=0) // (1)

clock::clock(int h=0, int m=0, int s=0) // (2)

Dar pot apare probleme pentru declaratii la obiecte de genul:

clock casio; // Eroare, care constructor ?

clock big_ben(21); // Subtil, (2) pentru ca 21 este int si

// NU long

3. Daca intr-un program se apeleaza functia sistem "abort()", destructorii NU mai apelati !!!

Un caz particular, foarte important insa, este acela al crearii unor tablouri de obiecte (prin intermediul constructorilor), respectiv "distrugerii" acestora (prin intermediul destructorilor). Pentru acest caz se vor aplica urmatoarele reguli:

a) Constructorul este aplelat AUTOMAT pentru FIECARE obiect al tabloului (de atitea ori cite elemente sint in tablou).

b) Daca obiectul (ce constituie elementul de baza al tabloului) are (mai multi) constructori, atunci unul dintre ei trebuie sa nu aiba argumente (nici chiar implicite).

c) Destructorul se apeleaza, dar nu neaparat (AUTOMAT) pentru FIECARE obiect al tabloului; destructorul nu are de unde sa cunoasca numarul EFECTIV de elemente din tabloul ce se dealoca din memorie, daca nu i se specifica EXPLICIT (a se vedea sintaxa si modul de utilizare al operatorului "delete").

Observatie importanta

Sintaxa operatorului "delete" ce specifica numarul de obiecte dealocate (delete[n] ...) este necesara NUMAI pentru obiectele ce au destructori!!! Daca se dealoca un tablou de obiecte din tipurile predefinite (int, char, etc.), ce nu pot avea destructori (definiti de programator), atunci NU este nevoie de specificarea numarului de obiecte dealocate.

Exemplu

Se defineste un tablou de obiecte. Un obiect este "asemanator" unui intreg, numai ca la initializare (la creare cu constructorul) i se da valoarea 33. Constructorul si destructorul clasei contin instructiuni de afisare pentru a vizualiza apelurile respective.

class num33

~num33(void) //destructorul

Pentru aceasta clasa se indica 2 functii "main" ("beneficiarele" clasei) ce o utilizeaza in 2 moduri diferite:

1. O functie main ar putea fi:

void main(void)

// aici AUTOMAT se face apel la destructor!

ce are ca "efect", programul va afisa:

S-a apelat constructorul!

S-a apelat constructorul!

S-a apelat destructorul!

S-a apelat destructorul!

Numarul "corect" de apeluri ale destructorului se datoreaza alocarii statice a tabloului, in acest fel compilatorul (care este cel ce apeleaza AUTOMAT destructorul) "stie" de cite ori sa il apeleze.

2. O alta functie main (ce utilizeaza clasa):

void main(void)

Aceasta varianta va "produce" la iesire:

S-a apelat constructorul!

S-a apelat constructorul!

S-a apelat destructorul!

Numarul "incorect" de apeluri ale destructorului (si deci incorecta dealocare a memoriei) se datoreaza alocarii dinamice (cu "new") si nespecificarii pentru operatorul "delete" (de dealocare) a numarului de componente ale tabloului (in acest caz "tablou" este doar un pointer catre un obiect).

Pentru o dealocare corecta instructiunea "delete" trebuie rescrisa conform sintaxei:

delete[2] tablou;

Observatie importanta

Cind pointerii SPRE obiecte (nu din interiorul obiectului) ies din domeniul de definitie NU SE APELEAZA IMPLICIT UN DESTRUCTOR!!! Aceasta inseamna ca trebuie apelat operatorul "delete" pentru a distruge astfel de obiecte (a dealoca memoria ocupata de acestea).

Deci exista 4 tipuri de constructori ce pot fi folositi. Sintetic, sint prezentati in urmatorul tabel:

FORMA GENERALA UTILIZARE

X::X(void) Implicit;daca nu e definit nici un

constructor acesta se creeaza de catre

compilator;

X::X(Arg1,Arg2,..) Se foloseste pt a initializa un obiect

cu un set (dat) de parametri

X::X(X&) Se foloseste pt a initializa un obiect

cu o copie a unui obict de acelasi tip

X::X(T&) Se foloseste pt a "converti" un obiect

de tip T intr-unul de tip X (copiere

de obiecte de tipuri diferite)

Observatie

S-a notat cu "X" o clasa oarecare, iar cu "T" o clasa oarecare diferita de "X". Daca primele 2 tipuri sint foarte clare, celelalte 2 forme se folosesc pentru a initializa un obiect ce se creaza cu "valori provenite" de la un ALT obiect deja "existent", de acelasi tip - forma (3) (se mai numeste constructor de copiere), sau de alt tip - forma (4) (se mai numeste constructor de conversie de tip).

Exemplu

Pentru exemplificare se prezinta clasa "num33" definita anterior, cu mici modificari:

class num33 ;

num33::num33(num33& x)

num33::num33(int& i)

Daca exista o clasa fara constructor de copiere si initializam un obiect din acea clasa cu un alt obiect tot din acea clasa, atunci compilatorul C++ va genera automat un constructor de copiere. Acesta va efectua o copiere membru cu membru a obiectului sursa in cel ce se creeaza.

Exemplu

Daca nu s-ar fi specificat constructor de copiere pentru clasa "clock" definita anterior si s-ar fi declarat:

clock rolex(12,0,0);// constructor definit de programator

clock orex=casio; // constructor creat automat de catre

// compilator

Un astfel de constructor, "generat" automat de compilator, produce cod echivalent atribuirilor:

orex.hr=casio.hr;

orex.min=casio.min;

orex.sec=casio.sec;

Constructorul de copiere se apeleaza implicit si la pasarea argumentelor catre functii prin valoare si la intoarcerea unui obiect de catre o functie.

O problema delicata apare la constructorii claselor ce au ca membri alte clase; atunci cind se creaza un astfel de obiect in mod obligatoriu trebuie create si obiectele "componente".

Fie de exemplu:

class image ;

class galerie ;

Cum se declara acum constructorul "galerie" ce trebuie sa creeze acum si 2 obiecte de tip "image" (numite "picasso" si "vangogh") ?

Din punct de vedere sintactic astfel:

galerie::galerie(image* i)

: picasso(640,480), vangogh(320,200)

Pentru crearea celor 2 obiecte se apeleaza constructorii claselor respective (cu anumiti parametri, aici fiind chiar constante). Apelul lor se plaseaza intre caracterul ":" (doua puncte) si acolada " │

│ └── obiectele membre │

│ ce se initializeaza │

└── separator corpul constructorului

de baza

Fig. 12

Elementele unui constructor pt. obiecte compuse

Apelul constructorului de baza se face INAINTEA apelurilor constructorilor "membri", dar acestia se apeleaza si se executa inaintea CORPULUI (instructiunilor) constructorului de baza, dupa transferul parametrilor.

Daca un constructor membru nu are nevoie de argumente (sau le are implicite) nu este nevoie de apel explicit in lista apelurilor constructorilor membri (va fi apelat "automat").

Ordinea in care apar in lista nu este importanta, pentru ca ORDINEA EFECTIVA in care se apeleaza este ordinea in care apar obiectele membru in DECLARATIA CLASEI.

Exemplu

Fie de exemplu clasa:

class car2d ;

car2d::car2d(int ht,int len,int rsize)

:roata_fata(rsize),caroserie(ht,len),roata_spate(rsize)

Chiar daca sint in ordinea: roata_fata, caroserie, roata_spate, obiectele-membru se creaza in ordinea in care au fost declarate, adica mai intîi "caroseria" si apoi rotile (fata, spate).

Destructorii sint intotdeauna apelati in ordine inversa fata de cea in care au fost apelati constructorii. Rezulta deci doua efecte importante:

1. Destructorii pentru obiectele membre sint apelati dupa apelul destructorilor obiectului principal (si dupa ce acesta a fost executat). Daca obiectul membru este compus la rindul sau din alte obiecte acest proces este recursiv.

2. Destructorii obiectelor membru sint apelati in ordinea inversa celei in care obiectele sint declarate in clasa.

Avind in vedere ca un tip definit de programator este identic tratat ca si un tip de date predefinit se poate initializa un obiect dintr-o clasa ce nu contine obiecte membre si printr-o lista de initializare a membrilor (de tipuri predefinite), ca si cum acestia ar fi "obiecte" efective.

Exemple

struct point ;

point::point(int xi,int yi)

: x(xi), y(yi) //lista de initializare

Aceasta notatie poate fi pusa in legatura si cu apelul "scurt" al constructorilor cu valori pentru initializare pentru membrii obiectului. Argumentele "transmise" pot fi expresii, in general oricit de complicate:

point::point(int xi,int yi)

: x(xi+1), y(yi+1)

Sau se pot folosi constante pentru un constructor implicit:

point::point(void)

:x(0), y(0)

Observatie

Si obiectele statice pot apela constructori si/sau destructori ai clasei careia ii apartin. Diferenta fata de celelalte obiecte este momentul apelului constructorului sau destructorului.

Daca exista obiecte statice ce au constructori, acestia se apeleaza inaintea apelului functiei main(), intocmai ca si cum "compilatorul" ar "initializa" aceste obiecte (ca pe variabilele statice uzuale din C)

Destructorul este apelat la terminarea programului, din functia (standard) "exit()", ce este apelata automat de compilator (epilogul functiei main). Deci intr-un destructor nu este voie sa se apeleze functia "exit()" pentru ca se creaza o bucla infinitå!

3.11. Clase si functii "friend" (prietene)

Se stie cå numai functiile membru (metodele) au acces la datele private ale unui obiect. Acest lucru uneori poate fi "evitat" (dar trebuie ales cu mare atentie procedeul !) prin definirea unui tip special de functie: "friend".

O functie friend este in esenta o functie ce nu este membra a unei clase dar poate accesa datele private ale clasei respective.

Declararea ei se face prin plasarea prototipului functiei respective in declaratia clasei respective, precedat de cuvintul cheie "friend".

Exemplu

class date_salariat ;

Chiar daca aici apare in sectiunea privata (pentru a sublinia accesul la date private) declararea unei functii friend se poate plasa oriunde in definitia clasei. Domeniul de vizibilitate este de fapt TOT MODULUL in care se defineste, de fapt fiind o functie uzuala din acest punct de vedere sau o functie "publica" pentru clasa.

Definirea functiei "friend" se face similar unei functii C:

void sal_net(date_salariat* e) // calc. salariul "net"

Nu apare calificativul de clasa (numele de clasa), cu sintaxa pentru functiile membru "clasa::func" deoarece functia nu este membru! Chiar daca are acces la datele private o functie "friend" nu poate accesa datele membru direct (ca o functie membru), ci trebuie sa i se paseze un (pointer la un) obiect din clasa respectiva (de exemplu). Componentele unei functii "friend" sint prezentate in figura urmatoare:

class date_salariat ; │ │ │ │

│ │ │ │ │

│ │ │ numele │

│ │ │ clasei │

│ │ │ │

│ │ numele │

│ │ functiei │

│ │ │

│ tipul argument de "acces"

│ returnat a datelor private

└── cuvint cheie pt.

functia "friend"

Fig. 13 Elementele unei functii "friend"

Proprietatea "friend" este valabila doar intr-un singur "sens": de la clasa la functie. Clasa este cea care determina care ii sint functiile "friend" (clasa isi alege functiile "friend") si nu functiile determina clasa! Odata declarata o functie "friend" pentru o clasa si compilata clasa respectiva, nu mai exista nici un mijloc de a "adauga" functii-friend clasei respective.

Exemplu

Se prezinta inca un exemplu pentru a face diferenta intre functii membru ale unei clase, functii friend si functii uzuale:

class myclass ;

void myclass::inside(int p)

int prieten(myclass* a)

int outside(myclass* a) //se poate pasa pointer la clasa

Utilizarea functiilor friend se impune atunci cind dorim sa accesam datele private ale unei clase intr-o functie, fara ca acea functie sa fie membru pentru clasa respectiva.

Exemplu

Fie o clasa care reprezinta punctele 3D, si 2 functii ce trebuie sa calculeze distanta unui punct fata de origine si distanta dintre 2 puncte:

class point

double dist_origine(void) //distanta fata de origine

Este evident ca pentru distanta intre doua puncte functia este:

double dist(point* a, point* b)

iar apelul trebuie sa fie de genul: d=dist(p,q);

Avind in vedere ca trebuie sa apelam datele private ale clasei (ce sint foarte corect private, conform principiului OOP de ascundere a datelor) daca functia ar fi membru, atunci apelul ei ar fi de fapt (conform tehnicii OOP) o transmitere de mesaj unui obiect:

p.dist(p,q);

sau

q.dist(p,q);

ceea ce este clar total nefiresc. Astfel incit solutia este declararea functiei "dist" ca fiind functie friend:

class point ;

O alta utilizare obisnuita a conceptului "friend" este de a face o functie membru a unei clase functie "friend" pentru ALTA clasa!

Exista astfel doua posibilitati:

a) sa se specifice numai acea functie ca fiind "friend" pentru clasa respectiva;

b) sa facem intreaga clasa, deci TOATE functiile unei clase, "friend" pentru acea clasa.

Observatie

In ambele situatii trebuie sa avem o declaratie anticipata pentru una dintre clase.

Exemplu

a) Se declara o functie membru a unei clase ca fiind "friend" pentru alta clasa:

class num_frd;    // declaratia anticipata

class num

friend void num_frd::set(int i);

class num_frd

void set(int i) // O.K.

void alt_set(void) // EROARE!!!

b) Nu numai o functie poate fi "friend" pentru o clasa, ci o intreaga clasa poate fi la rindul ei "friend" pentru o alta clasa. Ceea ce se deduce din aceasta "conexiune" intre clase este faptul ca TOATE functiile membru ale clasei friend pot accesa datele private ale clasei "de baza".

Exemplu

Se prezinta 2 clase utilizate pentru a implementa o lista:

class node ;

class list ;

void list::add(int i) // inserarea unui nod in lista

q->next=n; // adaugare la "coada"

node* list::find(int i)

return p;

Deci cele 2 clase folosite:

list = reprezinta lista ca un INTREG; este alcatuita dintr-o multime de noduri legate

node = tipul unui nod; are o informatie utila (aici int) si un pointer de legatura

Datorita faptului ca "next" este un membru privat pentru clasa "node", TOATE functiile clasei list (aici "add" si "find") trebuie sa fie "friend" pentru clasa "node" (trebuie sa-l acceseze). Astfel incit a fost facuta toata clasa friend!

Relatia intre obiectele de clasa "list" si "node" este prezentata in figura urmatoare:

Obiectul

"list" Obiectul

┌─────────┐ "node"

│ * head ─┼────> ┌─────────┐

───┬──── ├─────────┤ │ * next ─┼─────> ┌─────────┐

functii │ add() │ ├─────────┤ │ ──┼───> ...

├─────────┤ │ data │ ├─────────┤

publice │ find() │ └─────────┘ │ │

└─────────┘

│ etc... │

Fig. 14

Relatia intre obiectele "list" - "node"

Observatie

Clasele friend sint utile atunci cind o clasa este "gestionata" de alta clasa.

3.12. Utilizarea operatorilor membri ai unei clase

3.12.1. Generalitati

Se stie ca in C++ numele unei functii poate fi "suprapus" (mai multe definitii-implementari pentru diverse tipuri de argumente). In plus exista o functie "specifica" numita functie opera­tor ce implementeaza operatorii definiti pentru un tip de date. Functiile operator (fiind de fapt functii cu un anumit statut) pot fi si "suprapuse" si declarate ca functii membru ale unei clase particulare.

Functiile operator sint grupate in 2 categorii: unare si binare. Maniera de apel a unei functii operator depinde si de apartenenta la una din aceste clase dar si de apartenenta sau neapartenenta la o anumita clasa (functie operator uzuala sau membru a unei clase). Modalitatea de "traducere" a unui apel de functie operator este data de tabelele urmatoare:

Tabelul 1: Apelul functiilor operator binare

│Functie membru Sintaxa Apelul efectiv │

│Functie membru X Op Y X.operatorOp(Y) │

│Functie nonmembru X Op Y operatorOp(X,Y) │

Tabelul 2: Apelul functiilor operator unare

│Functie membru Sintaxa Apelul efectiv │

│Functie membru Op X X.operatorOp() │

│Functie membru X Op X.operatorOp() │

│Functie nonmembru Op X operatorOp(X) │

│Functie nonmembru X Op operatorOp(X) │

Se observa modalitatea specifica de apel in cazul functiilor operator membru: numarul de argumente este cu unul mai mic decit numarul operanzilor implicati, unul fiind intotdeauna pointerul ascuns "this".

Observatie

Pentru operatorii unari nu se poate face diferentiere, in cazul suprapunerii, intre prefixare sau postfixare ! ("++").

Exemplu

Pentru clasa"string" se poate suprapune operatorul de atribuire "=" astfel:

class string;

void string::operator= (string& s)

void main(void)

Pentru o clasa (sau eventual in cadrul unui program) apare foarte naturala suprapunerea functiilor operator in cadrul ei.

Exemplu

Se descrie o clasa ce implementeaza "numere circulare".

class circular_num

int high_val(void)

int low_val(void)

int add(int v) //!!

int inc(void)

int dec(void)

circular_num::circular_num(int v, int l, int u)

int circular_num::set_val(int v)

Este foarte natural sa (re)definim operatori de incrementare (++) si decrementare (--), dar si sa suprapunem operatorul "+=" pentru a permite:

-adunarea unui intreg la un numar circular

-adunarea unui nr circular la alt numar circular

class circular_num ;

void circular_num::operator+= (int v)

void circular_num::operator+= (circular_num& x)

void main(void)

Tipul referinta pentru parametrul variantei (2) nu este intimplator. Se datoreaza utilizarii "normale" a operatorului la apel (si mult mai natural).

S-ar fi putut scrie functia operator cu parametru transmis prin valoare (ceea ce ar cere insa o copie suplimentara a sursei) sau prin intermediul unui pointer, in acest caz obtinind:

void circular_num::operator+=(circular_num* x);

dar apelul in acest caz ar fi trebuit sa fie:

a+=&b; // ! Atentie &b

ceea ce ar fi fost nenatural.

3.12.2. Utilizarea operatorilor "friend"

Chiar daca imbunatatesc "vizibilitatea" unui program oferind si unelte foarte puternice pentru utilizarea claselor, operatorii membru suprapusi pot crea probleme in anumite cazuri, si anume atunci cind sint folositi pentru a "combina" tipuri predefinite (int, char, etc) cu tipurile (clasele) definite de programator.

Exemplu

Fie pentru clasa anterioara suprapunerea operatorului "+" astfel:

class string;

Declaram obiecte din clasa "string" si le "mixam" cu obiecte din clase (tipuri) predefinite:

string str1,str2;

str2=1+str1; // ????

Conform "schemei de traducere" a apelului unei functii operator aceasta instructiune este echivalenta cu:

str2=1.operator+(str1);

ceea ce este o eroare, deoarece tipul (clasa) "int" nu are definit operatorul"+" pentru argument de tip "string".

Daca se doreste totusi o astfel de operatie (intre un "int" si un "string") solutia se regaseste din Tabel 1: "scoaterea in afara" clasei a operatorului (a se vedea modul de apel atunci!) si pastrarea totusi a accesului la datele interne ale clasei "string", adica declararea operatorului "+" ca fiind un "friend" pentru clasa "string":

class string ;

Folosind "suprapunerea" operatorilor se poate pastra bineinteles si un operator "+" pentru obiecte din clasa "string":

friend string operator+ (string a, string b);

Acum se pot scrie expresii ca:

str2=1+str1;

str3=str1+str2; //etc

Problema care apare acum este aceea ca trebuie sa "oferim" cite un operator pentru FIECARE tip de operand (operanzi) posibili.

Exista insa o metoda ce permite eliminarea acestor multiple definiri, si anume definirea functiilor de conversie sau a operatorilor de modificare a tipului ("type-casting"). Prin folosirea acestora numarul operatorilor se reduce la strictul necesar.

Observatie

Avind in vedere ca pentru operatorii " friend" trebuie sa pasam ca argument obiectul asupra caruia actioneaza, se recomanda (mai ales pentru operatorii unari ca "++") declararea operatorilor ca functii membru si nu "friend".

Exemplu

class clock

// se apeleaza: orex++;

//...

class clock

// se apeleaza: operator++(orex)

//...

3.12.3. Suprapunerea operatorului de asignare "="

Suprapunerea operatorului de asignare "=" este utila in prelucrarea obiectelor din clasele ce utilizeaza, de obicei, alocarea dinamica a memoriei. Avind in vedere ca operatorul predefinit "=" realizeaza o copiere "membru-cu-membru" a unui obiect in alt obiect, daca obiectele din clasa respectiva contin pointeri se va copia doar pointer-ul si nu si continutul ! Acest lucru se repercuteaza esential la apelul destructorilor !!

Exemplu

class employee ;

employee tom;

employee bob("Bob Smith", "1271 Park Avenue", 45, 25.50);

tom=bob;

Rezultatul operatiei de asignare este prezentat in figura urmatoare:

│ x────┼───┘ name │ x────┼─┐ └───┤B│o│b│ │S│m│i│t│h│ │

│ x────┼───┐ adress│ x────┼───┐

│ 45 │ │ age │ 45 │ └───┤1│2│1│1│ │P│a│r│k│ │

│ 2550.50 │ │ salary │ 2550.50 │ │

tom │ bob │

└────────────────────────┘

Fig. 15

Copierea eronata a obiectelor cu pointeri

"Greseala" consta in faptul ca nu se aloca si al doilea buffer pentru obiectul "tom".

Exemplu

char* window_buff1 = new char[4096];

char* window_buff2 = new char[4096];

get_screen(window_buff1); // apel pt. salvarea ecranului

window_buff2=window_buff1; // ?? Atentie!!

//....

delete window_buff1; // inca OK

delete window_buff2; // EROARE!

Eroarea provine din faptul ca se incearca dealocarea aceleiasi zone de memorie (in urma asignarii "uzuale" se copiaza doar pointerii, prima zona NU mai este accesibila). Aceasta se poate corecta astfel:

class window_buffer

~window_buffer (void)

void operator= (window_buffer& v);

void window_buffer::operator= (window_buffer& v)

Acum o asignare de genul:

window_buffer mem_window, base_window;

mem_window=base_window;

are ca efect apelul operatorului "=" astfel:

mem_window.operator=(base_window);

Astfel se pot apela acum in siguranta destructorii pentru dealocarea memoriei, avind in vedere ca fiecare pointeaza acum zone de memorie diferite (chiar daca au acelasi continut).

De remarcat ca operatorul poate fi si "friend" astfel:

//...

friend void operator=( window_buff& a, window_buff& b);

//...

void operator= (window_buff& a, window_buff& b)

Apelul se transforma acum:

operator=(mem_window,base_window);

La suprapunera operatorului de asignare trebuie avuta in vadere diferenta intre atribuire (asignare) si initializare. Chiar daca sintactic sint asemanatoare (cu semnul "=") ele difera fundamental: initializare se realizeaza cu ajutorul constructorului (eventual de copiere), iar copierea cu operatorul de asignare "=".

Exemplu

class ch_array ;

ch_array s(80);

ch_array t=s; // INITIALIZARE

A doua instructiune este o initializare ce se realizeaza cu un constructor de copiere (daca e definit) sau cu unul generat implicit (automat) de compilator pentru copiere membru cu membru.

ch_array s(80), t(80);

s=t; // ATRIBUIRE

Daca ch_array are un operator "=" definit propriu (suprapus) se foloseste (ca aici), daca nu are loc o atribuire membru cu membru, aparind aceleasi probleme referitoare la pointeri. Formal diferenta intre atribuire si initializare e data de :

REGULA

Initializarea declara si tipul obiectului a carui valoare se fixeaza, pe cind atribuirea nu contine si declaratia de tip.

Se poate imbunatati substantial functia-operator "=" pentru o clasa: returnarea unei referinte la obiectul destinatie pentru a permite (ca in C) atribuiri multiple, de genul: "a=b=c".

Exemplu

ch_array sa(80), sb(80), sc(80);

//...

sa=sb=sc; // in varianta anterioara NU se poate!

Se redefineste functia "operator=()" astfel:

ch_array& ch_array::operator= (ch_array& src)

Este necesara o dereferire (indicare a continutului) a pointerului "this" pentru a-l "transforma" intr-un obiect "ch_array", dupa care se construieste o referinta la el.

3.12.4. Suprapunerea operatorului de indiciere "[]"

Scopul pentru care se foloseste operatorul de indiciere este acela de a "ascunde" aritmetica de pointeri.

Exemplu

ch=names[12] <=> ch=*(names+12)

Operatorul "[]" este un operator BINAR. Astfel, intr-o expresie de genul p=x[i], operatorul este "[]" iar argumentele sint "x" si "i". Diferenta fata de operatorii uzuali (ce pot fi eventual suprapusi) consta in faptul ca un astfel de operator trebuie sa fie neaparat membru al clasei careia i se ataseaza. Astfel expresia anterioara este interpretata NUMAI:

x.operator[](i).

Exemplu

Pentru clasa "string" definita anterior se suprapune operatorul de indiciere pentru a detecta eventualii indici eronati.

class string255 //constructorul

char& operator[] (int i); //operatorul de indiciere

char& string255::operator[] (int i)

else

return data[i];

void main(void)

In acest exemplu, instructiunile (1) si (2) pot fi interpretate astfel:

(1'): a.operator[](5)=17; //atentie la val returnata

(2'): c=a.operator[](7);

Datorita faptului ca se intoace o referinta, operatorul poate fi folosit atit in membrul sting al asignarii (lvalue) cit si in membrul drept, ca operand intr-o expresie uzuala.

3.12.5. Functii de conversie si operatori de conversie de tip

Problema esentiala pentru functiile operator membru suprapuse (ca de altfel pentru toate functiile suprapuse) este determinarea "variantei" ce se foloseste intr-un apel (in functie de argumentele efective din apel). Pentru rezolvarea problemelor ce apar la conversia de tipuri se foloseste algoritmul general, oferindu-se in plus 2 metode de conversie de tip:

a) functii de conversie

b) operatori de conversie (type-cast)

a) functiile de conversie sint tipuri speciale de constructori ce contin un singur argument. Argumentul trebuie sa fie un obiect de alt tip decit clasa constructorului.

Exemplu

class clock //constructor de conversie

Pentru declaratiile urmatoare:

clock orex=clock(0,0,0); //apel constructor (1)

clock rolex=clock(30.0); //apel constructor (2)

Se considera apelul (2) ca fiind o conversie de la o valoare "double" la un obiect de tip "clock", de unde si numele lor.

Aceste functii de conversie au insa un dezavantaj: sint membri ai clasei respective si conversii suplimentare (pentru clase deja compilate sau clase standard) nu se pot adauga. In plus acesti constructori de conversie NU se pot folosi cu tipurile predefinite (clase predefinite): int, char, etc. Aceste limitari sint eliminate de:

b) operatori de conversie de tip ("type-cast")

In esenta un astfel de operator este asemanator cu o functie operator ce are ca nume un nume de tip (predefinit sau definit de utilizator). Tinind cont de modalitatea C++ de conversie si pentru tipurile predefinite intre ele, rezulta o maniera uniforma de tratare a conversiilor de tip.

Exemplu

class clock //operator de conversie

Defineste un operator de conversie "clock" -> "double". Astfel se poate scrie:

clock mickey(2,45,15);

double seconds;

seconds=double(mickey); //type-cast

Acest ultim apel poate fi interpretat astfel:

seconds=mickey.double();

Observatii

1. Operatorii de conversie nu au specificat explicit tipul valorii returnate! Acesta este de fapt NUMELE operatorului !!

2. Din punct de vedere sintactic se poate utiliza si metoda C de conversie de tip, dar nu este recomandabila:

seconds=(double) mickey;

Aceste metode ofera posibilitatea conversiei explicite de tip. Este posibil insa sa utilizam si conversii implicite de tip, chiar si pentru functiile/operatorii de conversie suprapusi.

Exemplu

seconds=mickey;

Aceasta instructiune va genera o conversie de tip implicita, pentru ca este detectat de compilator tipul operanzilor, aplicindu-se automat operatorul corespunzator de conversie de tip.

Si functiile de conversie pot fi apelate implicit, de exemplu astfel:

clock t(0,0,0);

t=42.17; //apel de functie de conversie

seconds=3600+mickey;

Aceste apeluri pot apare si atunci cind valorile sint pasate functiilor ca argumente sau sint intoarse dintr-o functie.

Exemplu

double duty (clock t)

double duty2 (double d)

clock duty3 (void)

clock mickey(0,0,0);

double make_time;

make_time=duty(mickey); //conversie la return

make_time=duty2(mickey); //conversie la pasarea arg

mickey=duty3();

Ori de cite ori are loc o conversie de tip se incearca apelul operatorului de conversie "clock::double()"; in cazul "duty3" se apeleaza functia de conversie.



Document Info


Accesari: 5046
Apreciat: hand-up

Comenteaza documentul:

Nu esti inregistrat
Trebuie sa fii utilizator inregistrat pentru a putea comenta


Creaza cont nou

A fost util?

Daca documentul a fost util si crezi ca merita
sa adaugi un link catre el la tine in site


in pagina web a site-ului tau.




eCoduri.com - coduri postale, contabile, CAEN sau bancare

Politica de confidentialitate | Termenii si conditii de utilizare




Copyright © Contact (SCRIGROUP Int. 2024 )