Programare Competitivă

Membri și specificatori de acces

Până acum, toate câmpurile și metodele erau publice (accesibile din orice cod). Dar una dintre ideile-cheie ale OOP e încapsularea: să ascundem detaliile interne și să expunem doar ce trebuie.

Pentru asta folosim specificatori de acces.


Cei 3 specificatori de acces

În C++ există trei specificatori:

  • public - accesibil de oriunde
  • private - accesibil doar din interiorul clasei
  • protected - accesibil din clasă și din clasele derivate (discutăm la moștenire)
class Cont {
public:
    string titular;       // oricine poate citi/modifica

private:
    double sold;          // doar metodele clasei pot accesa

protected:
    int id;               // doar clasa și descendenții
};

De ce private?

Exemplu concret: un cont bancar. Vrei ca soldul să fie modificabil doar prin operații controlate (depunere, retragere), nu să poată fi schimbat direct de oricine.

Varianta naivă (totul public)

class Cont {
public:
    string titular;
    double sold;
};

int main() {
    Cont c;
    c.titular = "Ana";
    c.sold = 100;
    c.sold = -1000000;  // aici cineva poate "sparge" soldul!
    c.sold += 50;       // poate să adauge oricât, fără control
}

Problema: oricine poate modifica direct soldul, chiar cu valori aberante.

Varianta corectă (sold privat)

class Cont {
public:
    string titular;

    void depune(double suma) {
        if (suma > 0) sold += suma;
    }

    void retrage(double suma) {
        if (suma > 0 && suma <= sold) sold -= suma;
    }

    double getSold() {
        return sold;
    }

private:
    double sold = 0;
};

int main() {
    Cont c;
    c.titular = "Ana";
    c.depune(100);
    c.retrage(30);
    // c.sold = -1000;   // EROARE - sold e privat
    cout << c.getSold(); // 70
}

Acum soldul se modifică doar prin operații validate.


Încapsulare: de ce e importantă?

Încapsularea aduce trei beneficii majore:

1. Validare centralizată

Toate modificările trec prin metode. Poți valida într-un singur loc.

void depune(double suma) {
    if (suma <= 0) return;     // validare
    if (suma > 1000000) return; // limită
    sold += suma;
}

2. Libertate să schimbi implementarea

Dacă schimbi cum stochezi soldul intern (de ex. din double în cenți long long), utilizatorii clasei nu simt. Doar metodele tale se modifică.

3. Protecție împotriva greșelilor

Nu poți seta soldul aiurea pentru că nu poți accesa variabila direct.


Sintaxa specificatorilor

Specificatorii se aplică până la următorul specificator sau până la finalul clasei:

class Exemplu {
public:
    int a, b;      // public
    void f1();     // public

private:
    int c, d;      // private
    void f2();     // private

public:
    int e;         // public (din nou)
};

Putem alterna, dar de obicei scriem:

  1. public: - tot ce e expus
  2. private: - detaliile interne

Getters și Setters

Pentru câmpuri private pe care vrem să le expunem controlat, scriem metode speciale:

  • Getter: metoda care returnează valoarea câmpului
  • Setter: metoda care setează valoarea câmpului (cu validare)
class Persoana {
public:
    string getNume() { return nume; }
    void setNume(string numeNou) {
        if (!numeNou.empty()) nume = numeNou;
    }

    int getVarsta() { return varsta; }
    void setVarsta(int varstaNoua) {
        if (varstaNoua >= 0 && varstaNoua <= 150) varsta = varstaNoua;
    }

private:
    string nume;
    int varsta = 0;
};

int main() {
    Persoana p;
    p.setNume("Ana");
    p.setVarsta(25);
    cout << p.getNume() << " " << p.getVarsta();
}

Când ai nevoie de getter/setter?

  • Getter + Setter: acces citire + scriere controlat.
  • Doar getter: valoare read-only din exterior.
  • Doar setter: valoare write-only (rar, mai mult pentru logging).
  • Niciunul: câmpul e complet ascuns (detaliu intern).

Exemplu complet: Cont bancar cu validări

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

class Cont {
public:
    // Constructor simplu (vom aprofunda în lecția 122)
    void seteaza(string titularNou, double soldInitial) {
        titular = titularNou;
        if (soldInitial >= 0) sold = soldInitial;
        else sold = 0;
    }

    bool depune(double suma) {
        if (suma <= 0) return false;
        sold += suma;
        return true;
    }

    bool retrage(double suma) {
        if (suma <= 0 || suma > sold) return false;
        sold -= suma;
        return true;
    }

    void afisare() {
        fout << titular << ": " << sold << " lei\n";
    }

    double getSold() { return sold; }

private:
    string titular;
    double sold = 0;
};

int main() {
    Cont c;
    c.seteaza("Ana", 1000);
    c.afisare();

    c.depune(500);
    c.afisare();

    c.retrage(200);
    c.afisare();

    if (!c.retrage(5000))
        fout << "Retragere esuata: fonduri insuficiente\n";

    if (!c.depune(-100))
        fout << "Depunere esuata: suma invalida\n";

    return 0;
}

date.out:

Ana: 1000 lei
Ana: 1500 lei
Ana: 1300 lei
Retragere esuata: fonduri insuficiente
Depunere esuata: suma invalida

Ce am câștigat?

  • Imposibil să faci sold negativ (retragerea verifică).
  • Imposibil să depui sume negative.
  • Toate modificările trec prin metode care validează.

Accesul la membrii privați din interiorul clasei

Fiecare metodă a clasei are acces complet la toți membrii, inclusiv privați. Restricția e doar din exterior.

class Test {
public:
    void f() {
        a = 5;    // OK - suntem în clasă
        g();      // OK - apel la metoda privată
    }

private:
    int a;
    void g() { /* ... */ }
};

int main() {
    Test t;
    t.f();     // OK
    // t.a = 5;   // EROARE - a e privat
    // t.g();     // EROARE - g e privat
}

friend - excepții la regula privată

Uneori vrei ca o funcție din afară să aibă acces la membrii privați. Folosești friend:

class Cont {
public:
    void afisare() { cout << sold; }

private:
    double sold = 100;

    friend void operatiuneSpeciala(Cont &c);  // declarăm prieten
};

void operatiuneSpeciala(Cont &c) {
    c.sold += 1000;   // OK - suntem "prieteni"
}

Atenție: friend sparge încapsularea. Folosește cu parcimonie. Rar e necesar.


Exemplu clasic: clasa Fractie

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

class Fractie {
public:
    void set(int numarator, int numitor) {
        if (numitor == 0) {
            a = 0;
            b = 1;
            return;
        }
        a = numarator;
        b = numitor;
        simplifica();
    }

    double valoare() {
        return (double)a / b;
    }

    void afisare() {
        fout << a << "/" << b;
    }

private:
    int a, b;  // a/b

    int gcd(int x, int y) {
        return y == 0 ? x : gcd(y, x % y);
    }

    void simplifica() {
        int d = gcd(a < 0 ? -a : a, b < 0 ? -b : b);
        if (d > 0) { a /= d; b /= d; }
        if (b < 0) { a = -a; b = -b; }  // păstrăm numitorul pozitiv
    }
};

int main() {
    Fractie f;
    f.set(6, 8);
    f.afisare();
    fout << " = " << f.valoare() << "\n";

    f.set(-9, 12);
    f.afisare();
    fout << "\n";

    f.set(1, 0);  // validare - numitor 0
    f.afisare();

    return 0;
}

date.out:

3/4 = 0.75
-3/4
0/1

Observă:

  • Câmpurile a, b sunt private - nu poți strica fracția din exterior.
  • simplifica și gcd sunt private - sunt detalii de implementare, utilizatorul nu le vede.
  • Fracția se auto-simplifică la orice setare. Utilizatorul nu se ocupă de asta.

Cum decizi ce e public și ce e privat?

Regula de aur

Public: CE poate face obiectul.

Private: CUM face obiectul acele lucruri.

Exemplu

Pentru o clasă Cerc:

  • Public: arie(), perimetru(), getRaza(), setRaza()
  • Private: câmpul raza (doar prin getter/setter)

Pentru o clasă Lista:

  • Public: adauga(), scoate(), cauta(), dimensiune()
  • Private: vectorul intern, variabila de dimensiune, detalii de realocare

Greșeli frecvente

1. Totul public

class Elev {
public:
    string nume;
    int varsta;
    double medie;
};

Acum ai un struct. Dacă nu ai metode sau validări, folosește struct. Dacă vrei încapsulare, pune câmpurile private.


2. Setter fără validare

void setVarsta(int v) { varsta = v; }  // acceptă orice, inclusiv -50

Scopul setterului e să valideze. Dacă nu validezi, mai bine faci câmpul public.


3. Getter care returnează referință la membru privat

int& getX() { return x; }  // RISC - altcineva poate modifica!

Returnarea prin referință permite modificare din exterior, distrugând încapsularea. Returnează prin valoare dacă vrei read-only.


4. Confuzia class cu struct

class Punct {
    int x, y;   // implicit PRIVATE
};

Punct p;
p.x = 5;  // EROARE - x e privat

La class, implicit e private. Adaugă explicit public: sau folosește struct.


5. Date private inutile

Dacă toată logica e publică și toate câmpurile au getter/setter triviali, clasa devine zgomot. Private are sens când ascunzi sau validezi ceva.


Ce să reții

  • Specificatori de acces: public (oricine), private (doar clasa), protected (clasa + derivați).
  • Încapsulare = ascunderea detaliilor interne + controlarea accesului.
  • Private pentru date și pentru metode de implementare.
  • Public pentru interfața clasei (ce poate face obiectul).
  • Getter / Setter = metode publice care accesează/modifică un câmp privat, adesea cu validare.
  • În class implicit e private, în struct implicit e public.
  • friend e o excepție - o funcție externă cu acces privat. Folosește rar.
  • Întreabă-te mereu: “am nevoie ca cineva din afară să modifice asta direct?” - dacă nu, private.