4.Functii si date de tip static
Fiecare obiect al unei clase are propriul sau set de functii si date publice sau private, accesul facīndu-se īn functie de statutul ales (private sau public). Īn anumite situatii este de dorit ca unul sau mai multe cīmpuri sa fie accesibile tuturor obiectelor dintr-o clasa. Un exemplu de asemenea situatie este numele directorului de start īntr-un program care scaneaza recursiv directoarele discului. Un alt exemplu este o variabila 'flag' care sa indice o anumita initializare: numai primul obiect creat al clasei face initializarea, punīnd 'flagul' pe 'yes'. Aceste situatii se īntīlnesc si īn coduri C, unde anumite functii trebuie sa acceseze anumite variabile. Solutia īn C este de a defini toate functiile īn acelasi fisier sursa, iar variabilele de tip static: īn acest fel variabilele sīnt cunoscute doar īn acest fisier. O alta solutie este de a da variabilei īn cauza un nume neuzual (_6DODU), sperīnd ca nici o alta parte a programului nu va utiliza numele din greseala. Ambele solutii sīnt neelegante.
C++ permite utilizarea de functii si date statice, comune tuturor obiectelor unei clase.
5.1 Date statice
O data membru al unei clase poate fi declarata static, indiferent de zona īn care a fost inclusa (publica sau privata). O asemenea data este creata si initializata o singura data, īn contrast cu datele ne-statice, care sīnt create pentru fiecare obiect al clasei. O data static este creata la īnceputul programului, dar face totusi parte din clasa. Datele static declarate public sīnt ca variabilele normale:ele pot fi accesate de īntregul cod folosindu-se numele lor, precedat de numele clasei si de operatorul scope. Exemplu:
class Test
{
public:
static int public_int;
private:
static int private_int;
}
int main ()
{
Test::public_int = 145; // ok
Test::private_int = 12; // gresit, nu poate fi accesata
return (0);
}
Fragmentul de cod nu este pentru un compilator C++: el ilustreaza numai declaratia, nu si definitia datelor membre statice.
Date statice private
Penru ilustrarea utilizarii unei date membru statice declarata private, iata urmatorul exemplu:
class Directory
{
public: // constructori, destructori, etc.
.
.
private: // data membru
static char path [];
};
Data membru path este o variabila private static. Ea exista īntr-un singur exemplar, chiar daca se creeaza mai multe obiecte din clasa Directory. Aceasta data poate fi inspectata sau alterata de constructor, destructor sau orice alta functie membru al clasei Directory.
Deoarece constructorii sīnt apelati pentru fiecare nou obiect al clasei, datele static nu sīnt niciodata initializate de constructori, ci cel mult modificate. Motivul este ca datele static exista īnainte de apelul constructorului primului obiect din acea clasa. Datele static pot fi initializate īn timpul definitiei lor, īn afara tuturor celorlalte functii membru, ca si variabilele globale. De obicei, definitia si initializarea datelor static apar īn fisierul sursa ce contine si definitia clasei. Data membru path poate fi definita si initializata īn fisierul sursa al constructorului:
// data membru static: definitie si initializare
char Directory::path [200] = "/usr/local";
// constructorul implicit
Directory::Directory ()
{
.
.
}
Trebuie retinut ca definitia unei date statice poate apare īn orice fisier sursa (dar numai o singura data). Īn declaratia clasei membrii statici sīnt doar declarati: numai la definitia lor tipul si numele clasei apare explicit. De asemeni, dimensiunea datei poate fi omisa īn declaratie, dar este necesara la definitie.
Un al doilea exemplu de folosire al unei date private static este dat īn continuare. Clasa Graphics defineste comunicarea programului cu un device grafic. Pregatirea initiala al device-ului, care īn cazul nostru ar reprezenta o trecere din modul text īn cel grafic, este o actiune a constructorului si depinde de o variabila statica nobjects (un 'flag'). Variabila respectiva numara obiectele de tip Graphics prezente la un moment dat. Cīnd se creeaza primul obiect, este initializat si modul grafic (de catre constructor). Īn mod similar, destructorul face trecerea de la modul grafic la cel text daca nu mai exista nici un obiect grafic.
class Graphics
{
public: // constructorul, destructorul
Graphics ();
~Graphics ();
//alte functii de interfata
.
.
private: // numara obiectele
static int nobjects;
void setgraphicsmode (); // functie ipotetica de trecere īn modul grafic
void settextmode (); // trecere īnapoi īn modul text
}
// data membru statica
int Graphics::nobjects = 0;
// constructorul
Graphics::Graphics ()
{
if (! nobjects)
setgraphicsmode ();
nobjects++;
}
// destructorul
Graphics::~Graphics ()
{
nobjects--;
if (! nobjects)
settextmode ();
}
Este evident ca daca sīnt definiti mai multi constructori, fiecare va trebui sa incrementeze nobjects si daca e posibil sa initializeze modul grafic.
Date statice public
Datele membru pot fi declarate si īn sectiunea public, desi nu este recomandat (violarea principiului de ascundere a datelor). De exemplu, daca data membru statica path ar fi declarata īn sectiunea public, īntregul cod al programului ar avea acces la aceasta variabila. Si īn acest caz variabila path va trebui definita, iar īn fisierul sursa va trebui sa contina definitia :
char Directory::path [200].
5.2 Functii membru static
Īn afara de datele static, C++ permite si declararea de functii static. Conform conceptului, aceste functii vor putea fi aplicate tuturor obiectelor din acea clasa. Functiile static pot adresa doar date static din acea clasa; cele ne-statice sīnt inaccesibile (daca nu ar fi asa, carui obiect ar apartine ?). Īn acelasi timp, functiile statice nu pot apela functii ne-statice din clasa respectiva. Acest lucru este dat de faptul ca functiile static nu contin pointerul this. Functiile care sīnt statice si care sīnt declarate īn sectiunea public pot fi apelate fara specificarea unui obiect al clasei:
class Directory
{
public: // constructori, destructori
.
.
// functiile static publice
static void setpath (char const *newpath);
private: // stringul static
static char path [];
};
// definitia variabilei static
char Directory::path [199] = "/usr/local";
// functia static
void Directory::setpath (char const *newpath)
{
strncpy (path, newpath, 199);
}
int main ()
{
// Alternativa (1): apelul setpath() fara nici un obiect din clasa Directory
Directory::setpath ("/etc");
// Alternativa (2): cu un object
Directory dir;
dir.setpath ("/etc");
return (0);
}
Īn acest exemplu, functia setpath() este o functie public static. C++ permite si definirea functiilor private static: īn acest caz functia poate fi apelata numai de un obiect al clasei (nu si din afara). De asemeni, functia va putea accesa numai datele statice sau alte functii statice.
Mostenirea
Cīnd programam īn C, īn mod obisnuit vedem solutia problemei sub forma top-down: functiile sīnt definite īn functie de alte sub-functii, si tot astfel pīna ajungem la functiile sistemului. Īn top se gaseste main() care va apela restul functiilor. Īn C++ dependentele īntre cod si date pot fi definite īn termeni de clase dependente de alte clase. Aceasta seamana cu notiunea de compunere, īn care obiectele contineau ca date obiecte din alte clase. Dar relatia descrisa īn continuare este de un tip diferit: o clasa poate fi definita prin intermediul alteia, definita anterior, aceasta ducīnd la situatia īn care noua clasa 'mosteneste' toate functiile clasei vechi, dar īsi adauga propriile functii. Īn locul unei compuneri, unde o clasa contine alta clasa, vom avea o 'derivare', īn care o clasa data devine alta clasa. Un alt sinonim folosit pentru derivare este 'mostenirea': noua clasa mosteneste functionalitatea clasei existente, desi aceasta din urma nu apare ca data specifica īn noua clasa. Vom numi clasa initiala - clasa de baza-, iar noua clasa, - clasa derivata. Derivarea claselor este deseori folosita atunci cīnd se exploateaza la maxim posibilitatile limbajului C++. Asa cum am vazut si īn primul laborator, clasele sīnt identificate īn procesul de analiza al problemei, dupa care obiectele (ale claselor definite) vor reprezenta entitati ale problemei. Clasele sīnt plasate īntr-o ierarhie, īn care cele mai putin functionale sīnt plasate īn vīrf, iar celelalte, prin derivare, capata noi functionalitati.
Sa consideram o clasificare a vehicolelor pentru a construi o ierarhie de clase. Prima clasa va fi Vehicle, care va avea ca functionalitate posibilitatea de a seta sau afla greutatea unui vehicol. Urmatorul nivel īn ierarhie va contine : land-, water- si air-vehicle.
5.3. Tipuri de relatii
Relatia īntre clasele propuse reprezinta diferitele tipuri de vehicole. Clasa Vehicle este deci 'cel mai mare numitor comun' al ierarhiei. Iata un exemplu de implementare:
class Vehicle
{
public: // constructori
Vehicle ();
Vehicle (int wt); // interfata
int getweight () const;
void setweight (int wt);
private: // data
int weight;
}
Pentru a reprezenta vehicole care calatoresc pe uscat, definim o noua clasa ce pastreaza functionalitatea clasei Vehicle, dar adauga si noi informatii. De exemplu sa presupunem ca sīntem interesati de viteza sī de greutate. Relatia īntre Vehicle si Land poate fi reprezentata si printr-o compozitie, dar ar fi gresit: compozitia ar sugera ca vehicolul Land contine un vehicol, pe cīnd relatia este de fapt: vehicolul Land este un tip special de Vehicol. O relatie īn termenii unei compozitii ar introduce si cod īn plus. De exemplu, considerīnd urmatorul cod (īn care este folosita compozitia):
class Land
{
public:
void setweight (int wt);
private:
Vehicle v; // compunere cu Vehicle
};
void Land::setweight (int wt)
{
v.setweight (wt);
}
Folosind compozitia, functia setweight() al clasei Land serveste doar la pasarea argumentului catre Vehicle::setweight(). Deci īn ceea ce priveste greutatea, apare numai un cod redundant. Relatia este mai bine pusa īn evidenta prin derivare: Land este derivata din Vehicle, Vehicle fiind clasa de baza.
class Land: public Vehicle
{
public: // constructori
Land ();
Land (int wt, int sp);
// interfata
void setspeed (int sp);
int getspeed () const;
private: // data
int speed;
};
Prin postfixarea clasei Land īn definitia sa cu public Vehicle se defineste derivarea: clasa Land va contine toate functionalitatile clasei Vehicle plus propriile informatii. Acestea constau īntr-un constructor cu doua argumente si o functie interfata pentru accesarea datei speed. Pentru ilustrarea folosirii acestei clase, iata un exemplu:
Land veh (1200, 145);
int main ()
{
printf ("Vehicle weighs %d\n"
"Speed is %d\n",
veh.getweight (), veh.getspeed ());
return (0);
}
Acest exemplu demonstreaza si doua aspecte ale derivarii. Īn primul rind, getweight() nu este un membru direct al clasei Land, si totusi este apelat cu numele veh.getweight(). Acest membru face parte implicita din clasa, este 'mostenit' de la 'tatal' Vehicle. Īn al doilea rīnd, desi clasa derivata Land contine acum functionalitatile lui Vehicle, partile private ale clasei Vehicle ramīn private (īn sensul ca pot fi accesate doar de functiile membru ale obiectelor Vehicle). Asta īnseamna ca functiile membru din Land trebuie sa utilizeze functiile interfata (getweight(), setweight()) pentru a accesa cīmpul weight. Aceasta restrictie este necesara pentru a pastra conceptul de ascundere a datelor. Clasa Vehicle poate fi, mai tīrziu, recodata; clasa Land ramīne neschimbata. Īn exemplul urmator presupunem ca urmatoarea clasa, Auto, este capabila sa reprezinte greutatea, viteza si numele unei masini. Clasa este derivata din clasa Land:
class Auto: public Land
{
public: // constructori
Auto ();
Auto (int wt, int sp, char const *nm);
Auto (Auto const &other); // constructorul copy
Auto const &operator= (Auto const &other); // asignarea
~Auto (); // destructorul
// interfata
char const *getname () const;
void setname (char const *nm);
private: // data
char const *name;
};
Deoarece clasa Auto este derivata din clasa Land care este derivata din clasa Vehicle, putem vorbi de o derivare īn lant.
5.4. Constructorul unei clase derivate
Asa cum am putut vedea din definitia clasei Land, exista un constructor pentru a seta si greutatea si viteza obiectului. O implementare 'slaba' este urmatoarea:
Land::Land (int wt, int sp)
{
setweight (wt);
setspeed (sp);
}
Implementarea are dezavantaje: compilatorul C++ va genera cod pentru apelul constructorului implicit al clasei de baza la fiecare apel al constructorului clasei derivate (daca nu se specifica expres altfel). Acest lucru este echivalent cu o compozitie de obiecte. Solutia corecta este de a apela direct constructorul clasei Vehicle ce asteapta un argument int. Pentru a obtine acest lucru, sintaxa cere ca apelul constructorului clasei de baza sa urmeze lista de argumente a constructorului clasei derivate.
Land::Land (int wt, int sp)
: Vehicle (wt)
{
setspeed (sp);
}
6.5. Redefinirea functiilor membru
Actiunea tuturor functiilor definite īn clasa de baza poate fi redefinita īn clasa derivata. Sa presupunem, de exemplu, ca sistemul de clasificare a vehicolelor trebuie sa fie capabil sa reprezinte autocamioanele, care constau īn din doua parti: camionul propriu-zis si remorca. Fiecare parte are propria sa greutate, īnsa getweight() trebuie sa returneze greutatea totala. Deci definitia clasei Truck va fi derivata din clasa Auto, dar va avea un cīmp de date suplimentar. Convenim sa reprezentam greutatea camionului īn clasa Auto, cea a remorcii aparīnd distinct īn noua clasa:
class Truck: public Auto
{
public: // constructori
Truck ();
Truck (int engine_wt, int sp, char const *nm,
int trailer_wt);
// interfata: setarea a doua greutati
void setweight (int engine_wt, int trailer_wt);
// returnarea greutatii
int getweight () const;
private: // data
int trailer_weight;
};
// examplu de constructor
Truck::Truck (int engine_wt, int sp, char const *nm,
int trailer_wt)
: Auto (engine_wt, sp, nm)
{
trailer_weight = trailer_wt;
}
Sa observam ca clasa Truck contine doua functii deja prezente īn clasa de baza:
. Functia setweight(). Redefinirea īn Truck nu pune probleme deosebite. Noua definitie o va acoperi cea veche, din Vehicle, deci va putea fi apelata numai cu doua argumente.
. Functia getweight(). Are acelasi numar de argumente ca si functia din clasa Vehicle.Īn acest caz clasa Truck redefineste functia membru.
Urmatorul cod reprezinta redefinirea functiei getweight():
int Truck::getweight () const
{
return ( Auto::getweight () + //suma greutatii camionului
trailer_weight); //si a remorcii
}
Sa observam ca apelul Auto::getweight() selecteaza explicit functia din clasa Auto. O instructiune de forma return (getweight()+trailer_weight); va duce la o recursie infinita.
5.5. Mostenirea multipla
C++ permite sī derivarea unei clase din mai multe clase, creīnd asa numita mostenire multipla (clasa cu mai multi parinti). De exemplu, clasa Engine va contine functii pentru stocarea urmatoarelor informatii: numar de serie, puterea, tipul combustibilului, etc..
class Engine
{
public: // constructori
Engine ();
Engine (char const *serial_nr, int power, char const *fuel_type);
// functii necesare datorita prezentei pointerilor īn definitia clasei
Engine (Engine const &other);
Engine const &operator= (Engine const &other);
~Engine ();
// interfata
void setserial (char const *serial_nr);
char const *getserial () const;
void setpower (int power);
int getpower () const;
void setfueltype (char const *type);
char const *getfueltype () const;
private: // data
char const *serial_number, fuel_type;
int power;
};
Pentru a reprezenta un obiect Auto, dar cu toate informatiile despre motor, se poate deriva o noua clasa, MotorCar, din clasa Auto si din clasa Engine. Functionitatile ambelor clase vor fi mostenite de clasa noua:
class MotorCar: public Auto, public Engine
{
public: // constructori
MotorCar ();
MotorCar (int wt, int sp, char const *nm,
char const *ser, int pow, char const *fuel);
};
MotorCar::MotorCar (int wt, int sp, char const *nm,
char const *ser, int pow, char const *fuel)
: Engine (ser, pow, fuel), Auto (wt, sp, nm)
{
}
Cīteva observatii:
. Cuvīntul cheie public este prezent īn fata ambelor nume ale claselor de baza, aceasta deoarece implicit, īn C++, derivarea este private.
. Noua clasa nu introduce functii noi, ci numai le combina pe cele pre-existente (agregheaza tipuri mai simple īntr-un tip complex - abilitate a C++ des utilizata).
. Constructorul, care asteapta sase argumente, nu contine cod propriu. sarcina lui este numai de a activa constructorii claselor de baza.
Ca observatie asupra sintaxei: dupa lista de argumente sīnt apelati cei doi constructori, fiecare cu propria lista de argumente. Ordinea īn care sīnt apelati este data de ordinea derivarii. Primul constructor apelat este cel al clasei Auto, deoarece MotorCar este mai īntīi derivat din Auto.
Īn final, o regula de recunoastere sintactica: relatia 'este' īnseamna derivare (Motorcar 'este' si un Auto, si un Engine), iar relatia 'are' īnseamna compunere (de exemplu, MotorCar 'are' un Engine (motor), caz īn care īn clasa MotorCar va trebui sa apara un obiect de tip Engine). Īn acest ultim caz va trebui sa definim explicit functiile de interfata: (am presupus ca īn clasa MotorCar, cu compunere, exista un obiect engine din clas Engine)
void MotorCar::setpower (int pow)
{ engine.setpower (pow);}
int MotorCar::getpower () const
{ return (engine.getpower ()); }
// etcetera, la fel si pentru set/getserial(), si set/getfueltype()
Asemenea probleme (redundanta codului) sīnt evitate folosind derivarea.
5.6. Conversia īntre clasele de baza si cele derivate
Atunci cīnd se utilizeaza mostenirea īn definirea unei clase, se poate spune ca un obiect al clasei derivate este īn acelasi timp si un obiect al clasei de baza. Acest lucru are consecinte importante.
Conversii īn asignarea obiectelor
Sa definim doua obiecte, unul al clasei derivate si unul al clasei de baza:
Vehicle
v (900); // vehicol cu greutatea de 900 kg
Auto
a(1200, 130, "Ford");//automobil cu greutatea de 1200 kg, viteza de 30 km/h, //marca Ford
Obiectul a este initializat cu o valoare specifica. Totusi, deoarece Auto este īn acelasi timp si Vehicle, se poate face asignarea obiectului de baza cu obiectul derivat: v = a; Efectul asignarii este ca obiectul v primeste valoarea 1200 pentru cīmpul weight. Un Vehicle nu are cīmpurile speed si name, deci aceste date nu vor fi asignate. Asignarea inversa ( a unui obiect drivat cu unul de baza) nu este acceptata, deoarece pot exista cīmpuri (speed sau name) care nu pot primi o valoare. Regula generala: daca obiectul din stīnga are toate cīmpurile celui din dreapta, se poate face asignarea, altfel nu. Aceasta regula se refera srtict la operatorul de asignare (=) cel implicit. Se poate, de exemplu, redefini operatorul = īn clasa Auto astfel īncīt sa poata fi executata si instructiunea a = v;
Auto const &Auto::operator= (Vehicle const &veh)
{
setweight (veh.getweight ());
.
. //cod referitor la asignarea cīmpurilor neexistente īn Vehicle (speed si name)
}
Conversii īn asignarea pointerilor
Sa definim urmatoarele obiecte si pointeri:
Land l (1200, 130);
Auto a (500, 75, "Daf");
Truck t (2600, 120, "Mercedes", 6000);
Vehicle *vp;
Putem asigna vp cu adresele celor trei obiecte din clasele derivate: vp = &l; vp = &a; vp = &t;. fiecare asemenea asignare fiind legala. De fiecare data se face o conversie implicita a tipului clasei derivate īn tipul clasei de baza, deoarece vp este un pointer la un obiect de tip Vehicle. Īn schimb, folosind vp pot fi apelate numai functiile membru ce manipuleaza cīmpul weight, deoarece este singura functionalitate a clasei Vehicle. Aceasta restrictie are un efect deosebit de important mai ales pentru clasa Truck. Dupa asignarea vp = & t; pointerul vp va pointa catre un obiect Truck, īnsa apelul vp - > getweight() va īntoarce 2600, si nu 8600 (adica suma celor doua greutati, obinuta la apelul t.getweight()). Deci cīnd o functie este apelata prin intermediul unui pointer, tipul pointerului si nu cel al obiectului pointat determina ce functie este accesibila. Exista deigur si o cale de a ocoli conversia implicita, folosind operatorul cast:
Truck truck;
Vehicle *vp;
vp = &truck; // vp pointeaza catre un obiect truck
.
.
Truck *trp;
trp = (Truck *) vp;
printf ("Make: %s\n", trp->getname ());
Codul va fi executat numai daca vp va pointa īntr-adevar catre un obiect Truck si deci va exista o functie getname(). Altfel se va obtine o eroare.
5.7. Stocarea pointerilor clasei de baza
Faptul ca pointerii unei clase de baza pot fi utilizati pentru adresarea claselor derivate poate fi folosit pentru dezvoltarea unor clase generale care sa proceseze obiectele claselor derivate. Un exemplu tipic de asemenea proces este stocarea obiectelor, fie īntr-un vector, fie īntr-o lista, arbore, etc. Clasele definite astfel īncīt sa stocheze alte clase sīnt denumite īn general clase de stocaj (sau clase container). Ca exemplu prezentam clasa VStorage, folosita pentru stocarea pointerilor catre Vehicle. Pointerul actual poate adresa fie catre un obiect Vehicle, dar si un obiect Auto sau Track. Definitia clasei este urmatoarea:
class VSTorage
{
public: // constructori, destructor
VStorage ();
VSTorage (VStorage const &other);
~VStorage ();
VStorage const &operator= (VStorage const &other); // reacoperirea asignarii
// interfata:
void add (Vehicle *vp); // adauga Vehicle* īn zona de stocare
Vehicle *getfirst (void) const; // gaseste primul Vehicle*
Vehicle *getnext (void) const; // gaseste urmatorul Vehicle*
private: // data
Vehicle **storage;
int nstored, current;
};
Facem urmatoarele observatii:
. Clasa contine trei functii de interfata: una pentru a adauga un Vehicle īn lista, alta pentru a regasi primul Vehicle si ultima pentru a obtine urmatorul pointer.
Un exemplu de folosire este urmatorul:
Land l (200, 20); // greutate 200, viteza 20
Auto a (1200, 130, "Ford"); // greutate 1200 , viteza 130, marca Ford
VStorage garage; // zona de stocare
garage.add (&l); // adauga la zona de stocare
garage.add (&a);
Vehicle *anyp;
int total_wt = 0;
for (anyp = garage.getfirst (); anyp; anyp = garage.getnext())
total_wt += anyp->getweight ();
printf ("Total weight: %d\n", total_wt);
Acest exemplu demonstreaza modul īn care tipurile derivate (Auto, Land) sīnt convertite implicit la tipul de baza (Vehicle), astefel īncīt pot fi stocate īn VStorage. Functia getweight(), fiind definita īn clasa de baza, poate fi deci folosita pentru calcularea greutatii totale a vehicolelor stocate.
. Clasa VStorage contine si functiile necesare pentru asignarea unui obiect cu un alt obiect, adica reacoperirea asignarii si constructorul copy.
. Zona de date a clasei este cuprinsa īn sectiunea private: un tablou de pointeri catre obiecte Vehicle si doi īntregi, pentru numarul obiectelor stocate si pentru indexul curent (returnat de getnext()).
Sa retimen deci faptul ca C++ permite procesarea tuturor claselor derivate folosind o clasa de baza. Pare paradoxal ca aceasta clasa data ca exemplu, VStorage, poate stoca toate clasele derivate din Vehicle, chiar si cele viitoare. Faptul este posibil deoarece VStorage utilizeaza un protocol, definit īn Vehicle si obligatoriu deci penru toate clasele derivate.
Dezavantajul clasei VStorage este urmatorul: cīnd adaugam un obiect Truck, fragmentul de cod precedent nu va afisa suma totala a greutatilor (se aduna numai greutatea camionului, nu si a remorcii, adica acea greutate continuta de obiectul Vehicle si returnata de functia anyp->getweight(). Exista, desigur, si un remediu (functii virtuale).
INAPOI INAINTE
--------------------------------------------------------------------------------
Hosted by Integrasoft | Graphics by C!PR!AN | Copyright (C) 2000 Bogdan PADURARU All rights reserved.