Laboratorul 03: Clase. Funcții Constructor

In cadrul Laboratorului 3 vom explica si exemplifica cateva notiuni teoretice ce stau la baza Programarii Orientate pe Obiecte, precum clasele, functiile membre, constructorii si destructorii. De asemenea, vom prezenta o aplicatie C++, construita prin crearea de fisiere distincte, tocmai pentru a ne obisnui cu acest mod de dezvoltare a aplicatiilor.

Pentru a aprofunda aceste concepte, recomandam urmatoarele capitole din Absolute C++

  • Capitolul 6 (Chapter 6. pag 239-274): Structures and Classes
  • Capitolul 7 (Chapter 7. pag 275-320): Constructors and Other Tools

1. Introducere în programarea obiectuală

1.1. Abordarea orientată pe obiecte

Asa cum am observat in curs, evolutia limbajelor de programare a condus la cresterea gradului de abstractizare, ceea ce inseamna ca un program se distanteaza din ce in ce mai mult de limitarile impuse de masina pe care ruleaza acesta.
Abordarea orientata pe obiecte reprezinta cea mai flexibila si puternica abstractizare de limbaj de pana acum, ce ne permite sa descriem o solutie in termenii problemei, mai repede decat in cei ai masinii pe care solutia ruleaza.
In cadrul programarii obiectuale, structurile de date sunt imbinate cu algoritmi necesari prelucrarii datelor, rezultand tipuri de date complexe, cu numele de clase, rezultand o abordare pragmatica si naturala a problemei ce trebuie rezolvata de catre programator.

Avantaje ale utilizarii POO:

  • Protejarea datelor prin incapsulare
  • Modularizare sporita a aplicatiei
  • Usurinta in impartirea sarcinilor in echipa
  • Suport pentru extinderea usoara a aplicatiei (mostenire, polimorfism)

1.2. Clase

Clasa reprezinta un tip de date definit de utilizator, astfel incat sa il ajute in rezolvarea problemei care ii este data.
O clasa contine:

  • un nume propriu
  • un set de atribute (date)
  • un set de comportamente/functionatiltati - functii membre/metode

Putem gandi o clasa ca pe o matrita pe care o utilizam in instantierea / crearea de obiecte similare, deci care au aceleasi atribute.
De exemplu, in lume exista sute de producatori de masini, fiecare cu propriile modele. Cu toate ca modelele difera intre ele, ne putem gandi la atribute comune tuturor masinilor (numele producatorului, modelul, numarul de locuri, culoarea etc), dar si la metode comune (afisare, modificare, getCuloare, getNr_locuri etc).
Astfel, putem pune la un loc aceste atribute pentru a crea clasa Masina. Aceasta clasa va fi instantiata, pentru a crea obiecte cu date proprii:

class Masina
{
  private: //implicit private, daca nu specificam 
   string producator; // pentru tipul string folositi documentatia din laboratorul 10
   string model;
   string culoare;
   int nr_locuri;
 
  public:
   void afisare(void) const;
   void modificare(const string&, const string&, string&, int);
   string& getCuloare(void) const;
   int getNr_locuri(void) const;
};

Structura vs Clasa:

  • asemanare fundamentala: ambele pot contine atat date, cat si functii
  • diferenta fundamentala: membrii claselor sunt implicit private, spre deosebire de structuri, unde acestia sunt implicit public

Cuvintele cheie (modificatori de acces) ce modifica “vizibilitatea” membrilor:

  • private - date vizibile doar in clasa din care fac parte
  • public - date vizibile de oriunde

Obiectele reprezinta instante ale unei clase, in cadrul carora atributele au valori, iar metodele pot fi folosite.

1.3. Functiile membre (metode) ale unei clase

Metodele reprezinta proceduri ce sunt asociate claselor si obiectelor. Aceste pot accesa atributele si pot apela alte functii membre ale unei clase (inclusiv pe cele declarare private).
Un exemplu de implementare a unei metode:

void Masina::modificare(const string& prod, const string& mod, const string& cul, int locuri){
producator = prod; //referiri la atributele clasei
model = mod;
culoare = cul;
nr_locuri = locuri;
}

Pentru a implementa functiile membre sunt necesare referiri la atributele clasei, nu la un obiect anume

Apelarea functiilor se face folosind operatorii de selectie . / . La apelare, functia este informata asupra identitatii obiectului asupra caruia va actiona prin transferul unui parametru implicit - referinta la obiectul care face apelul:

Masina m; //instantierea unui obiect de tipul Masina
m.modificare("Dacia", "Logan", "rosu", 5)
// SAU
Masina *m = new Masina(); //instantierea, de aceasta data sub forma de pointer
m->modificare("Dacia", "Logan", "rosu", 5)

La apelul functiei, este testata declararea acesteia in clasa Masina (acelasi nume si aceeasi semnatura), dar si daca este implementata. Daca da, se apeleaza functia si i se transmite referita catre m (adresa lui m)

Metodele pot fi:

  • declarate inline
  • supradefinite
  • declarate public (in general), dar pot fi si private

1.4. Constructori

Constructorii reprezinta metode speciale, membre ale clasei, ce prezinta urmatoarele proprietati:

  • sunt apelati in momentul crearii obiectelor
  • au acelasi nume ca si clasa
  • NU au un tip de date returnat
  • se pot supradefini

Conform standardului C98, constructorul fara parametri si cel de copiere sunt generati automat de catre compilator, doar in lipsa implementarii efective a unui constructor

Tipurile de constructori sunt:

  • Constructori fara parametri
  • Constructori cu parametri
  • Constructori de copiere

1.5. Destructori

Destructorii reprezinta un alt tip de metode speciale, fara tip sau parametri, cu rolul de a elibera spatiul de memorie ocupat de un obiect. Formatul lor este ~nume_clasa()
Destructorul se apeleaza explicit doar in contextul pointerilor, altfel se apeleaza automat cand o variabila elibereaza spatiul de memorie (la finalul duratei sale de viata)

Compilatorul genereaza automat un destructor in lipsa implementarii de catre utilizator. Totusi, pentru clase ce contin atribute pointer, destructorul trebuie implementat, pentru a asigura o eliberare corecta a memoriei

1.6. Exemplu

In continuare, vom prezenta un exemplu de implementare al constructorilor si destructorului pentru o clasa:

class Dreptunghi
{
 private:
  int lungime;
  int latime;
 
 public:
  Dreptunghi(); //Constructor fara parametri, generat default daca nu este definit
  Dreptunghi(int, int = 0); //Constructor cu parametri. Daca este implementat, cel fara parametri nu mai este generat default
  Dreptunghi(const Dreptunghi&); //Constructor de copiere, generat default daca nu este definit
  ~Dreptunghi(); //Destructor. Este generat default si nu este necesar in acest context, deoarece nu avem tipuri de date pointer (*). Doar pentru exemplificare
};
 
///Implmementare metode///
 
Dreptunghi::Dreptunghi()
{
    lungime = 0;
    latime = 0;
}
 
Dreptunghi::Dreptunghi(int lungime, int latime)
{
    this->lungime = lungime;
    this->latime = latime;
}
 
Dreptunghi::Dreptunghi(const Dreptunghi& d) //As putea sa nu implementez constr de copiere si sa il folosesc pe cel generat automat.
{                                           //deoarece nu am atribute pointer
    this->lungime = d.lungime;
    this->latime = d.latime;
}
 
Dreptunghi::~Dreptunghi()
{
    //Nu trebuie eliberat manual spatiul de memorie. As putea sa nu implementez destructorul si sa il folosesc pe cel generat automat.
 
 
}

Puteti rula aici exemplul de mai sus pentru a observa diferite modalitati de intializare ale obiectelor. Observati utilizarea a 3 fisiere in cadrul acestui exemplu. Despre acest tip de implementare vom vorbi in partea a doua a laboratorului

O initializare de tipul Dreptunghi *g = new Dreptunghi[2]{{10, 20},{20, 30}} se numeste List initialization si reprezinta o modalitate de apelare a constructorilor cu parametri pentru elementele unui vector, spre deosebire de Dreptunghi *g = new Dreptunghi[2], cand este apelat de 2 ori constructorul fara parametri. Puteti afla aici mai multe detalii

2. Structura de fișiere a unei aplicații

Structura de fisiere a unei aplicatii CPP cu clase este:

  • nume.h - fisier header care contine definitia clasei (sau a unui grup de clase inrudite)
  • nume.cpp - fisier sursa care contine implementarea clasei/claselor - concret e vorba de functiile membre care nu au fost definite chiar in definitia clasei
  • altnume.cpp - program principal (de exemplu un program de test)

Impartirea in fisiere header si sursa nu este obligatorie, dar este indicata pentru usurinta in citirea codului.

In continuare, vom prezenta un exemplu de aplicatie ce respecta structura de fisiere mentionata anterior. Fisierele pot fi descarcate printr-un click pe numele lor, iar intreaga aplicatie poate fi rulata si descarcata aici:

Fisierul stack.h contine definitia clasei:

stack.h
class stack 
{
    int *buf;
    int sp; // Acesti membri sunt impliciti private.
    int nrmax; // Ei pot fi accesati doar prin functii (metode)
    char nume[10]; // ale clasei
 
public:
//
// Aceasta este o functie constructor.
// Ea are acelasi nume cu clasa si este fara tip (nici macar void).
// Functia constructor este apelata la crearea obiectelor (statica, auto sau dinamica).
//
    stack (const char *);
    stack (int, const char *); // Alt constructor care va fi definit in exterior
 
    ~stack(); // Destructor: functie care va fi apelata la incetarea
// duratei de viata a obiectului
 
    int is_empty() { return sp == -1; }     // Functii implementate in 
    int is_full() { return sp == nrmax-1; } // definitia clasei
    void push (int);
    int pop();
    char *getnume() { return (char *) nume; }
};

Membrii private ai clasei stack sunt:

  • buf: pointer la un tablou de intregi
  • sp: indicatorul de stiva (initializat cu -1 ⇐⇒ stiva vida)
  • nrmax: dimensiunea bufferului
  • nume: un nume asociat obiectului

Exista doi constructori, unul cu parametru unic (numele clasei) si al doilea cu 2 parametri (dimensiune buffer si numele obiectului). Faptul ca pot exista mai multe functii constructor este o consecinta directa a supradefinirii.

Metodele sunt: is_empty, is_full, push, pop, getnume, cu semnificatiile evidente.

Fisierul stack.cpp contine implementarile metodelor clasei (doar cele care nu au fost implementate in definitie):

stack.cpp
#include <iostream>
#include <cstdlib>
using namespace std;
#include <string.h>
#include "stack.h" //header-ul este inclus in fisierul sursa
 
stack::stack(const char *obnume)
{
  buf = new int [100]; 
  sp = -1; nrmax = 100;
  strncpy(nume, obnume, 9);
  cout<<"Constructor cu dimensiune implicita pentru obiectul "<<nume<<"\n";
}
 
stack::stack(int dim, const char *obnume)
{
  // Constructor cu dimensiune precizata
  buf = new int [nrmax = dim];
  sp = -1;
  strncpy(nume, obnume, 9);
 
  cout << "Constructor cu dimensiune explicita = " << dim << " pentru obiectul " << nume << "\n";
}
 
stack::~stack()
{
  delete [] buf;
  cout << "Destructor pentru obiectul " << nume << "\n";
}
 
void stack::push(int elem)
{
  if (!is_full())
    buf[++sp] = elem;
 
  else {
    cout << "Eroare: stiva plina\n";
    exit(1);
  }
}
 
int stack::pop()
{
  if (!is_empty())
    return buf[sp--];
 
  else {
    cout << "Eroare: stiva vida\n";
    exit(1);
    return -1; // Doar ca sa se respecte tipul functiei
  }
}

Pentru a pune in evidenta secventa de creare/distrugere a obiectelor, in functiile constructor si destructor s-au prevazut mesaje adecvate. De observat utilizarea operatorului de rezolutie ::, care precizeaza ca functiile respective sunt membre ale clasei stack (am putea avea o alta functie is_full care sa fie membra a unei clase coada).

Fisierul main.cpp reprezinta programul principal (de test):

main.cpp
#include <iostream>
#include <cstdlib>
using namespace std;
#include "stack.h" // Fisier header stack
 
stack a("a"); //variabila globala
 
int main(int argc, char *argv[])
{
    int i;
    stack b("b");
    cout << "\n\n" << "Start main\n";
 
    stack c(200, "c");
    stack *pd = new stack(300, "d");
 
    a.push(1);
    a.push(2);
    pd->push(3);
    cout << "Pop din stiva " << pd->getnume() << ": " << pd->pop() << "\n";
 
//
// Etc., etc.
//
    delete pd; // A fost alocat dinamic, deci eliberam memoria
    cout << "Sfarsit main\n";
 
    system("PAUSE");
    return EXIT_SUCCESS;
}

Se defineste un obiect stack la nivel exterior “a” (cu alocare statica). El va fi creat inainte de a incepe executia functiei main. In main sunt instantiate doua obiecte (“b” si “c”) si inca unul prin alocare dinamica (a se vedea forma new stack(…)). Apoi se fac cateva operatii uzuale cu stivele astfel definite. La consola se va urmari si explica secventa de mesaje din functiile constructor, destructor si main. De observat afisarea comoda la consola (cout) cu operatorul « (este necesar #include <iostream.h>).

poo-is-aa/laboratoare/03.txt · Last modified: 2024/08/14 20:10 (external edit)
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0