Programare Competitivă

Constructori și destructori

Până acum, creării obiectelor urma un pattern ciudat:

Punct p;
p.x = 3;
p.y = 4;

Creez obiectul, apoi îl inițializez manual setând fiecare câmp. E greoi, ușor de uitat, și câteva câmpuri pot rămâne necinstiate (cu valori aleatoare).

Constructorul rezolvă asta: o metodă specială care se apelează automat când creezi un obiect, inițializând datele în mod controlat.


Constructorul: definiție

Constructorul e o metodă specială care:

  • Are același nume ca clasa
  • Nu are tip de return (nici măcar void)
  • Se apelează automat la crearea obiectului
class Punct {
public:
    double x, y;

    // Constructor
    Punct() {
        x = 0;
        y = 0;
    }
};

int main() {
    Punct p;   // constructor apelat automat - x = 0, y = 0
}

Tipuri de constructori

1. Constructor implicit (default)

Fără parametri. Se apelează când scrii Punct p;.

class Punct {
public:
    double x, y;

    Punct() {
        x = 0;
        y = 0;
    }
};

2. Constructor cu parametri

Primește valori inițiale pentru câmpuri.

class Punct {
public:
    double x, y;

    Punct(double xInit, double yInit) {
        x = xInit;
        y = yInit;
    }
};

int main() {
    Punct p(3, 4);   // x = 3, y = 4
}

3. Constructor de copiere (copy constructor)

Se apelează când creezi un obiect dintr-un altul.

class Punct {
public:
    double x, y;

    Punct(double xInit, double yInit) {
        x = xInit;
        y = yInit;
    }
    
    Punct(const Punct &altul) {
        x = altul.x;
        y = altul.y;
    }
};

int main() {
    Punct p1(3, 4);
    Punct p2 = p1;      // copy constructor
    Punct p3(p1);       // echivalent
}

4. Constructorul implicit generat de compilator

Dacă nu scrii niciun constructor, compilatorul îți generează unul gol automat. Asta explică de ce Punct p; funcționează chiar fără să scrii constructor.

Dar atenție: dacă scrii orice constructor, compilatorul nu mai generează cel implicit.

class Punct {
public:
    double x, y;

    Punct(double xInit, double yInit) { x = xInit; y = yInit; }
};

int main() {
    Punct p;          // EROARE - nu mai există constructor fără parametri
    Punct p(3, 4);    // OK
}

Constructori multipli (overloading)

Poți avea mai mulți constructori, atâta timp cât au semnături diferite (parametri diferiți):

class Punct {
public:
    double x, y;

    Punct() {           // implicit
        x = 0; y = 0;
    }

    Punct(double a) {   // cu un parametru
        x = a; y = a;
    }

    Punct(double a, double b) {   // cu doi parametri
        x = a; y = b;
    }
};

int main() {
    Punct p1;           // apelează Punct()
    Punct p2(5);        // apelează Punct(double)
    Punct p3(3, 4);     // apelează Punct(double, double)
}

Compilatorul alege automat constructorul potrivit după parametrii oferiți.


Lista de inițializare (initializer list)

În loc să atribui în corpul constructorului, poți folosi o listă de inițializare:

class Punct {
public:
    double x, y;

    Punct(double xInit, double yInit) : x(xInit), y(yInit) {}
};

De ce e mai bună lista de inițializare?

  1. Mai eficient - inițializarea directă, nu “atribuire peste valoare default”.
  2. Obligatoriu pentru:
    • Câmpuri const
    • Câmpuri referință
    • Apelul constructorului clasei părinte (la moștenire)
class Cerc {
public:
    Cerc(double rInit) : raza(rInit), PI(3.14159) {}

private:
    double raza;
    const double PI;   // const - OBLIGATORIU lista de inițializare
};

Atribuire vs lista de inițializare

// Atribuire în corp (mai puțin eficient)
Punct(double a, double b) {
    x = a;   // x e deja creat cu valoare default, apoi atribuim
    y = b;
}

// Lista de inițializare (mai eficient)
Punct(double a, double b) : x(a), y(b) {}

Inițializarea implicită a câmpurilor (C++11+)

Din C++11, poți seta valori default direct lângă declarație:

class Punct {
public:
    double x = 0;
    double y = 0;

    // Constructorul implicit le folosește automat
};

int main() {
    Punct p;         // x = 0, y = 0
}

Foarte util - nu mai scrii constructor default dacă toate câmpurile au default.


Valori default pentru parametri

Poți combina un singur constructor cu valori default:

class Punct {
public:
    double x, y;

    Punct(double xInit = 0, double yInit = 0) : x(xInit), y(yInit) {}
};

int main() {
    Punct p1;           // x = 0, y = 0
    Punct p2(5);        // x = 5, y = 0
    Punct p3(3, 4);     // x = 3, y = 4
}

Un singur constructor acoperă 3 cazuri. Mai compact.


Destructorul: definiție

Destructorul e o metodă specială care:

  • Are numele ~NumeClasa (cu tilda în față)
  • Nu primește parametri
  • Nu are tip de return
  • Se apelează automat când obiectul e distrus (iese din scope, delete, etc.)
class Test {
public:
    Test() {
        cout << "Creat\n";
    }

    ~Test() {
        cout << "Distrus\n";
    }
};

int main() {
    Test t;    // afișează "Creat"
    // la sfârșitul main, se apelează destructorul → afișează "Distrus"
}

Ieșire:

Creat
Distrus

La ce folosește destructorul?

Principala folosire: eliberarea resurselor alocate dinamic.

class Vector {
public:
    Vector(int capacitate) {
        date = new int[capacitate];
        dim = capacitate;
    }

    ~Vector() {
        delete[] date;   // eliberăm memoria
    }

private:
    int *date;
    int dim;
};

Fără destructor, ai memory leak - memoria alocată cu new nu e niciodată eliberată.

Alte resurse eliberate de destructor

  • Fișiere deschise (close())
  • Conexiuni de rețea
  • Mutex-uri / locks
  • Memorie alocată dinamic

Ordinea apelurilor

La creare

  1. Câmpurile se inițializează în ordinea declarării (nu ordinea din lista de inițializare!).
  2. Apoi corpul constructorului se execută.

La distrugere

  1. Corpul destructorului se execută.
  2. Câmpurile se distrug în ordine inversă declarării.
class Test {
public:
    Test() { cout << "Test creat\n"; }
    ~Test() { cout << "Test distrus\n"; }
};

int main() {
    Test a, b, c;
    // Ieșire: Test creat (x3)
    // La final: Test distrus (x3) - în ordinea c, b, a
}

Ciclul de viață al unui obiect

   ┌─────────────────────┐
   │   CREARE            │
   │  (constructor)      │
   └──────────┬──────────┘
              │
              ▼
   ┌─────────────────────┐
   │   UTILIZARE         │
   │  (apeluri metode)   │
   └──────────┬──────────┘
              │
              ▼
   ┌─────────────────────┐
   │   DISTRUGERE        │
   │  (destructor)       │
   └─────────────────────┘

Exemplu complet: clasa Dreptunghi

#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");

class Dreptunghi {
public:
    // Constructor cu valori default
    Dreptunghi(double l = 0, double w = 0) : lungime(l), latime(w) {
        fout << "Creat dreptunghi " << lungime << "x" << latime << "\n";
    }

    // Destructor
    ~Dreptunghi() {
        fout << "Distrus dreptunghi " << lungime << "x" << latime << "\n";
    }

    double arie() { return lungime * latime; }

private:
    double lungime, latime;
};

int main() {
    Dreptunghi d1(5, 3);     // apel constructor cu 2 args
    Dreptunghi d2(4);        // apel cu 1 arg (latime default 0)
    Dreptunghi d3;           // apel fara args (ambele default 0)

    fout << "Aria lui d1: " << d1.arie() << "\n";

    return 0;
}

date.out:

Creat dreptunghi 5x3
Creat dreptunghi 4x0
Creat dreptunghi 0x0
Aria lui d1: 15
Distrus dreptunghi 0x0
Distrus dreptunghi 4x0
Distrus dreptunghi 5x3

Observă ordinea inversă la distrugere.


Exemplu cu resurse: VectorDinamic

#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");

class VectorDinamic {
public:
    VectorDinamic(int cap) : capacitate(cap), dim(0) {
        date = new int[capacitate];
        fout << "Alocat vector de " << capacitate << "\n";
    }

    ~VectorDinamic() {
        delete[] date;
        fout << "Eliberat vector\n";
    }

    void adauga(int x) {
        if (dim < capacitate) date[dim++] = x;
    }

    void afisare() {
        for (int i = 0; i < dim; i++) fout << date[i] << " ";
        fout << "\n";
    }

private:
    int *date;
    int capacitate;
    int dim;
};

int main() {
    VectorDinamic v(5);
    v.adauga(10); v.adauga(20); v.adauga(30);
    v.afisare();
    return 0;
}

date.out:

Alocat vector de 5
10 20 30
Eliberat vector

Dacă uit destructorul, memoria nu se eliberează și ai memory leak. Cu destructor, totul e curățat automat la final.


Constructori explicit

Uneori constructorii cu un parametru pot crea conversii implicite nedorite:

class Temperatura {
public:
    Temperatura(double c) { celsius = c; }
    double celsius;
};

void proceseaza(Temperatura t) { }

int main() {
    proceseaza(25.0);   // OK - 25.0 convertit automat la Temperatura
}

Pentru a preveni asta, folosim explicit:

class Temperatura {
public:
    explicit Temperatura(double c) { celsius = c; }
    double celsius;
};

int main() {
    proceseaza(25.0);              // EROARE acum
    proceseaza(Temperatura(25.0)); // OK explicit
}

Bun pentru a preveni conversii accidentale.


Greșeli frecvente

1. Constructor cu void sau tip de return

class Punct {
    void Punct() { }    // GRESIT - constructorul nu are return
    Punct() { }         // CORECT
};

2. Uitarea listei de inițializare pentru const

class Cerc {
public:
    Cerc() {
        PI = 3.14;      // EROARE - nu poți atribui la const
    }
    const double PI;
};

// CORECT:
class Cerc {
public:
    Cerc() : PI(3.14) { }
    const double PI;
};

3. Uitarea [] la delete

~Vector() {
    delete date;     // GRESIT - pentru array folosești delete[]
    delete[] date;   // CORECT
}

4. Memory leak - lipsa destructorului

class Nume {
    char *nume;
public:
    Nume(const char *n) {
        nume = new char[100];
        // ... copiez n în nume
    }
    // LIPSA destructorului → memory leak!
};

5. Shallow copy

class Vector {
public:
    Vector(int n) { date = new int[n]; }
    ~Vector() { delete[] date; }
    int *date;
};

int main() {
    Vector v1(10);
    Vector v2 = v1;   // PROBLEMA - ambele au același pointer
    // La ieșire, ambele destruc același pointer → CRASH
}

Soluție: scrii constructor de copiere care copiază și datele, nu doar pointerul. Avansat - vezi “rule of three”.


6. Apelarea explicită a constructorului

Punct p;
p.Punct();   // GRESIT - nu apelezi constructorul manual

Constructorul se apelează doar automat la creare.


Rule of Three (Cinci)

Dacă clasa ta are alocare dinamică (new / delete), probabil trebuie să definești:

  1. Destructorul - eliberează memoria
  2. Constructorul de copiere - face deep copy
  3. Operatorul de atribuire - face deep copy la atribuire

În C++11+, se adaugă și:

  1. Move constructor
  2. Move assignment

Pentru clase simple fără alocare dinamică, compilatorul generează toate astea corect. Grija apare când manipulezi memoria manual.


Ce să reții

  • Constructor = metodă specială apelată la crearea obiectului. Nume = clasa, fără return.
  • Destructor = metodă specială apelată la distrugere. Nume = ~Clasa, fără params.
  • Constructori pot avea parametri, pot fi multipli (overloading), pot avea valori default.
  • Lista de inițializare : x(a), y(b) { } e mai eficientă decât atribuirea în corp.
  • Necesară pentru câmpuri const și referință.
  • Destructorul e crucial pentru eliberarea resurselor (new, fișiere, etc.).
  • Câmpurile se inițializează în ordinea declarării (nu lista), se distrug în ordine inversă.
  • explicit previne conversii implicite.
  • Rule of Three: dacă ai destructor, probabil vrei și constructor de copiere + operator de atribuire.