Cuprins
Cuprins

Supraincarcare (overloading) & supradefinire (overriding)

Supraincarcare functii

In C++ putem avea mai multe functii cu acelasi nume dar trebuie ca parametrii ori sa fie de tipuri diferite, ori sa difere numarul de parametrii, ori parametrii sa fie in alta ordine (ma refer la tipul lor de ex.
int, int, float
si
float, int, int
nu la denumire, asta nu conteaza). De exemplu:
1
void f(int x) {}            // numar diferit de parametrii fata de functiile de mai jos
2
void f(int x, float y) {}   // difera ordinea tipurilor parametriilor fata de functia de mai jos
3
void f(float x, int y) {}
Totusi ai grija la parametrii cu valori implicite, de exemplu mai jos avem eroare pentru ca apelul de functie este ambiguu, nu se stie care functie sa se aleaga.
1
#include <iostream>
2
using namespace std;
3
4
void f(int x) {}
5
void f(int x, int y = 4) {}
6
7
int main() {
8
    f(5); // eroare aici
9
}
Tehnic vorbind functiile trebuie sa aiba signaturi diferite. Signatura se refera la numele functiei, numarul, ordinea si tipul parametrilor.
Daca avem ceva de genul:
1
#include <iostream>
2
using namespace std;
3
4
void f(int x, int y) {}
5
void f(double x, int y) {}
6
7
int main() {
8
    f(2, 2.3);
9
}
Cum se alege ce functie se apeleaza? Compilatorul se uita pe rand la fiecare argument si creaza o multime de functii candidat pentru fiecare argument, adica tipul argumentului (cand apelezi functia) se poate converti (cumva) in tipul parametrului (in definitia functiei). Pentru fiecare argument se creeaza o multime de functii care se potrivesc cel mai bine (chiar daca exista multe functii canditat unele fac mai multa "treaba" decat altele sa converteasca, de aceea se aleg cele care se potrivesc cel mai bine). Din multimile astea de functii care se potrivesc cel mai bine pentru fiecare argument se face o intersectie si functia rezultata este cea care se apeleaza. Daca intersectia e vida sau contine mai mult de 1 functie avem eroare de compilare.
De exemplu:
1
#include <iostream>
2
using namespace std;
3
4
class MyClass {
5
    // o clasa goala pentru exemplu
6
};
7
8
void f(MyClass x, long long y) {}   // functia 1
9
void f(long long x, MyClass y) {}   // functia 2
10
void f(MyClass x, MyClass y) {}     // functia 3
11
12
int main() {
13
    int x = 4;
14
    MyClass y;
15
16
    f(x, y);
17
    f(x, x);
18
}
Sa ne uitam la linia 16:
f(x, y);
. Mai intai la primul argument de tip
MyClass
, multimea functiilor candidat = {functia 1, functia 2},multimea functiilor care ce potrivesc cel mai bine = {functia 1, functia 2}. Acum sa ne uitam la al doilea argument de tip
int
, multimea functiilor candidat = multimea functiilor care se potrivesc cel mai bine = {functia 1} (pentru ca se poate converti
int
in
long long
).
{functia 1, functia 2} ∩ {functia 1} = {functia 1}. Asta inseamna ca in final pentru
f(x, y);
se apeleaza functia 1.
Daca ne uitam la linia 17:
f(x, x);
, pentru primul argument multimea functiilor care se potrivesc cel mai bine = {functia 2}, pentru al doilea argument multimea functiilor care se potrivesc cel mai bine = {functia 1}, iar {functia 2} ∩ {functia 1} = Ø si deci eroare.
In general daca avem o functie care e supraincarcata si avem doua variante F1 si F2, cum alege compilatorul functia care se potriveste cel mai bine? Functia F1 este mai "buna" decat F2 daca in primul rand conversiile implicite pe care le face F1 (de la tipul argumentelor la tipul parametrilor) nu sunt mai rele decat cele facute de F2 (pt. fiecare argument) si in plus:
  1. E cel putin un argument in F1 a carui conversie implicita este mai buna decat conversia implicita pentru acel argument in F2
  2. Sau daca nu e asa, dar doar in cazul conversiilor definite de tine (gen operatorul de cast intr-o clasa), daca conversia implicita de la tipul returnat de F1 la tipul destinatie este mai buna decat conversia implicita de la tipul returnat de F2 la tipul destinatie.
    1
    class MyClass {
    2
    public:
    3
        operator float() { return 5.5; }
    4
        operator int() { return 1; }
    5
    };
    6
    7
    int main() {
    8
        MyClass a;
    9
        double x = a;   // se apeleza prima functie vezi mai jos de ce
    10
        cout << x;      // se afiseaza 5.5
    11
    }
  3. Sau daca nu e asa, F1 este non-template iar F2 este template sau ambele sunt template dar F1 este mai specializata (dar despre template vorbesc separat).
Acum ce inseamna ca o conversie e mai buna decat alta? Exista 3 tipuri (in ordinea importantei):
  1. Exact match
    Nu trebuie sa se faca nicio "conversie" sau daca se face se considera exact match.
    1
    void f(int& x);
    2
    void f(double x);
    3
    int x = 42;
    4
    f(x); // argument type is int; exact match with int&
    5
    6
    /////////////////////////////////////////
    7
    8
    void g(int* p);
    9
    void g(void* p);
    10
    11
    int a[100];
    12
    g(a); // calls f(int*); exact match with array-to-pointer conversion
  2. Promotion
    Aici intra integral promotion si floating-point promotion. Promotia e un tip special de conversie pentru tipurile de date built-in (
    int
    ,
    float
    etc.) si este garantat ca nu schimba valoarea. (adica tipul de date la care se converteste poate reprezenta exact orice valoare a tipului de date de la care se converteste)
    Integral promotion
    • signed char
      sau
      signed short
      se pot converti la
      int
    • unsigned char
      sau
      unsigned short
      se pot converti la
      int
      daca
      int
      poate retine toate valorile sau la
      unsigned int
      daca nu poate. (e posibil ca short/char si int sa aiba acelasi nr. de biti pe un calculator si de aceea nu poti din unsigned short/char in int)
    • char
      se poate converti la
      int
      sau
      unsigned int
      (depinde daca prin
      char
      te referi la
      signed char
      sau
      unsigned char
      )
    • bool
      se poate converti la
      int
      ,
      false
      devine
      0
      iar
      true
      devine
      1
    Floating-point promotion
    • Se refera doar la conversia de la
      float
      la
      double
      .
  3. Conversion
    Aici intra integral conversion, floating-point conversion, floating-integral conversion, bool conversion
    Integral conversion
    • aici intra conversii dintre 2 tipuri de date ce reprezinta nr. intregi dar nu e promotion, de exemplu din
      int
      in
      long
      sau din
      unsigned short
      in
      short
    Floating-point conversion
    • aici intra conversii dintre 2 tipuri de date ce reprezinta nr. cu virgula dar nu e promotion, de exemplu din
      double
      in
      float
      sau din
      float
      in
      long double
      1
      void f(double);
      2
      void f(long double);
      3
      f(0.0f);
      Se apeleaza prima functie pentru ca promotia (de la
      float
      la
      double
      ) este mai buna decat conversia (de la
      float
      la
      long double
      )
    Floating-integral conversion
    • aici intra conversii dintre un tip de date ce reprezinta nr. cu virgula si un tip de date care reprezinta nr. intregi de exemplu din
      double
      in
      int
      sau din
      int
      in
      float
      1
      void f(int);
      2
      void f(long double);
      3
      f(0.0f);
      Eroare de compilare, apelul este ambiguu, avem 2 conversii (de la
      float
      la
      int
      si de la
      float
      la
      long double
      )
    Bool conversion
    • de exemplu din
      int
      in
      bool
      sau din
      float
      in
      bool
      . (de ex.
      bool x = 5; // true
      )

Supraincarcare operatori

  1. Majoritatea operatorilor din C++ pot fi supraincarcati (ori ca functii membre intr-o clasa ori ca functii prieten, insa unii pot fi DOAR functii membre).
  2. Nu poti fi supraincarcati:
    .  .*  ::  ?: sizeof
    (da,
    sizeof
    e un operator, nu functie).
  3. Nu poti modifica precedenta operatorilor sau numarul de argumente pe care le primesc.
  4. Toti operatorii care pot fi supraincarcati NU pot avea parametrii default, exceptie face
    ()
    (operatorul 'apel de functie').
  5. Toti operatorii supraincarcati intr-o clasa de baza sunt mosteniti de clasa derivata,cu exceptia
    =
    (operatorul de atribuire).
  6. Operatorii
    =
    ()
    []
    ->
    trebuie declarati ca functii membre ale unei clase (nu merge sa fie functii prieten).
Mai multe informatii despre supraincarcarea operatorilor sunt aici.

Supradefinire functii

Supradefinirea de functii, folosind
virtual
, are legatura cu polimorfismul la runtime. Daca o metoda din clasa de baza este virtuala si este redefinita intr-o clasa derivata, o putem apela folosindu-ne de un pointer/referinta la clasa de baza (care arata spre un obiect de tipul clasei derivate). In mod normal, daca metoda nu ar fi virtuala, nu am putea face asta deoarece compilatorul s-ar uita doar la tipul pointerului/referintei (nu il intereseaza daca am facut upcast)
Cand supradefinesti o functie (care e virtuala in clasa de baza) in clasa derivata trebuie sa fie exact la fel, cu tot cu tipul returnat (o diferenta fata de supraincarcarea functiilor). Totusi pentru tipul returnat exista o exceptie: cand intorci pointeri/referinte. Metoda supradefinita din clasa derivata poate intoarce ceva mai "specializat" (daca metoda din clasa de baza intorcea B*, cea din clasa derivata poate intoarce D*, unde D mosteneste B). La ce ne trebuie asta? De obicei asta se foloseste la un design pattern care se numeste "prototype" sau "clone".
Metodele statice nu pot fi virtuale (pentru ca nu corespund unui obiect ci intregii clase).
1
#include <iostream>
2
using namespace std;
3
4
class Base {
5
public:
6
    // f nu este virtuala, g si h sunt virtuale
7
    void f() { cout << "Base f()" << endl; }
8
    virtual void g() { cout << "Base g()" << endl; }
9
    virtual void h() { cout << "Base h()" << endl; }
10
};
11
12
class Derived : public Base {
13
public:
14
    void f() { cout << "Derived f()" << endl; }
15
    void g() { cout << "Derived g()" << endl; }
16
    // g este virtuala si aici chiar daca nu apare virtual !
17
};
18
19
int main()
20
{
21
    Base* p1 = new Base;
22
    Base* p2 = new Derived; // upcast
23
24
    p1->f(); // Base f()
25
    // ne uitam la tipul lui p1 care este Base*, metoda f din clasa Base nu este virtuala, deci ea se executa
26
27
    p2->f(); // Base f()
28
    // ne uitam la tipul lui p2 care este Base*, metoda f din clasa Base nu este virtuala, deci ea se executa
29
30
    p1->g(); // Base g()
31
    // ne uitam la tipul lui p1 care este Base*, metoda g din clasa Base este virtuala
32
    // ne uitam acum la tipul obiectului la care arata p1, adica ceva de tip Base
33
    // deci metoda g din clasa Base se executa
34
35
    p2->g(); // Derived g()
36
    // ne uitam la tipul lui p2 care este Base*, metoda g din clasa Base este virtuala
37
    // ne uitam acum la tipul obiectului la care arata p2, adica ceva de tip Derived
38
    // in clasa Derived este supradefinita metoda g, deci ea se apeleaza.
39
    
40
    p1->h(); // Base h()
41
    // p1 este de tip Base*, metoda h din Base este virtuala
42
    // ne uitam la tipul obiectului la care arata p1, adica Base, deci se executa metoda din Base.
43
44
    p2->h(); // 
45
    // p2 este de tip Base*, metoda h din Base este virtuala
46
    // ne uitam la tipul obiectului la care arata p2, adica Derived
47
    // in Derived nu este supradefinita metoda h, deci se executa metoda din Base (care e mostenita practic in clasa Derived).
48
    return 0;
49
}
Daca nu lucrezi cu pointeri sau referinte nu adauga nimic in plus faptul ca o functie e virtuala.
Ce faci daca ai metode virtuale dar totusi vrei sa apelezi numai metoda din clasa de baza de exemplu? Folosesti operatorul de scop si numele clasei. Te poti duce doar in sus in ierarhia de clase, nu in jos.
1
class SuperBase {
2
public:
3
    virtual void g() { cout << "SuperBase g()"; }
4
};
5
6
class Base : public SuperBase{
7
public:
8
    virtual void g() { cout << "Base g()" << endl; }
9
    // g este deja virtual, nu este necesar sa scrii iar virtual
10
};
11
12
class Derived : public Base {
13
public:
14
    void g() { cout << "Derived g()" << endl; }
15
    // g este virtuala si aici chiar daca nu apare virtual !
16
};
17
18
int main(){
19
    Base *p = new Derived;  // upcast
20
    
21
    p->g();  // Derived g() (polimorfism la runtime)
22
    p->Base::g();  // Base g()
23
    
24
    p->SuperBase::g();  // Superbase g()
25
    // p este de tip Base*, Base mosteneste SuperBase deci putem merge in "sus" in ierarhie
26
    
27
    p->Derived::g(); // EROARE
28
    // nu putem merge in "jos" in ierarhie
29
}
Metoda virtuala din clasa de baza nu trebuie sa fie accesibila sau vizibila neaparat ca sa fie supradefinita. Poate fi
private
de exemplu. Pe langa asta se ignora faptul ca o metoda e virtuala in constructori.
1
class B {
2
private:
3
    virtual void f() { cout << "B"; }
4
    // f e private
5
public:
6
    B() { f(); } 
7
    // in constructor se apeleaza metoda f de mai sus, nu se tine cont de virtual !
8
    // de ce? pentru ca am apela metoda f din clasa derivata inainte sa terminam de construit ce avem in clasa de baza
9
10
    void do_f() { f(); }
11
    // aici se apeleaza ori metoda f din clasa de baza ori cea din clasa derivata
12
    // in functie de obiectul curent
13
};
14
15
class D : public B {
16
    void f() { cout << "D"; }
17
};
18
19
20
int main()
21
{
22
    D ob;
23
    B& ref = ob;
24
25
    ref.do_f(); // D
26
}
Ai grija ca atunci cand ai mostenire multipla, daca clasele care mostenesc virtual supradefinesc o functie, trebuie supradefinita si in clasa "nepot" pentru ca altfel ar exista 2 supradefiniri in clasa cea mai derivata si n-ar fi ok.
1
class Base {
2
public:
3
    virtual int f() const { return 5; } 
4
    // metoda e const deci trebuie sa fie const si cand supradefinesti !!
5
};
6
7
class Derived_1 : public virtual Base {
8
public:
9
    int f() const { return 10; } 
10
};
11
12
class Derived_2 : public virtual Base {
13
public:
14
    int f() const { return 20; }
15
};
16
17
class MM : public Derived_1, public Derived_2{
18
public:
19
    int f() const { return 50; }
20
    // daca lipseste metoda f aici, ai eroare
21
    // ai avea aici si metoda f din Derived_1 si metoda f din Derived_2
22
    // care supradefinesc metoda f din Base deci nu se stie care e varianta finala
23
};
24
25
int main()
26
{
27
    Base *p = new MM;
28
    cout << p->f() // 50;
29
30
    return 0;
31
}
Daca functia din clasa derivata arata altfel (acelasi nume, lista de parametrii diferita), nu o mai supradefineste pe cea din clasa de baza, o ascunde.
1
class SuperBase {
2
public:
3
    virtual void f() { cout << "SuperBase" << endl; }
4
};
5
6
class Base : public SuperBase {
7
public:
8
    void f(int) { cout << "Base" << endl; }
9
    // asta nu supradefineste, o ascunde pe cea din SuperBase
10
};
11
12
class Derived : public Base {
13
public:
14
    void f() { cout << "Derived" << endl; }
15
    // supradefineste metoda din SuperBase
16
    // metoda de aici o ascunde si pe cea din Base
17
};
18
19
int main()
20
{
21
    SuperBase* p1 = new SuperBase;
22
    SuperBase* p2 = new Base;
23
    SuperBase* p3 = new Derived;
24
    
25
    p1->f(); // SuperBase
26
    p2->f(); // SuperBase (pt ca in Base nu se supradefineste)
27
    p3->f(); // Derived (pt ca in Derived se supradefineste)
28
    
29
    Derived ob;
30
    ob.f(); // Derived
31
    // ob.f(2); // eroare, f(int) din Base nu e vizibila
32
    ob.Base::f(2); // acum e ok, se apeleaza explicit
33
    
34
    
35
    Base* p4 = new Base;
36
    Base* p5 = new Derived;
37
    
38
    // p4->f(); // eroare, in Base avem doar f(int)
39
    // p5->f(); // eroare, in Base avem doar f(int)
40
    
41
    p4->SuperBase::f(); // SuperBase
42
    p5->SuperBase::f(); // SuperBase (cand apelezi asa explicit nu se tine cont de virtual)
43
44
    return 0;
45
}
Putem avea destructori virtuali. Asta se face cand vrei sa stergi un obiect de tipul clasei derivate printr-un pointer de tipul clasei de baza. Destructorii sunt automat supradefiniti (pentru ca fiecare clasa). Daca clasa derivata nu aloca memorie pe heap, nu este necesar sa mai fie virtual.
1
class B {
2
public:
3
    virtual ~B() { cout << "~B"; }
4
};
5
6
class D : public B {
7
    int* v;
8
public:
9
    D() { v = new int[100]; }
10
    ~D() { delete[] v; cout << "~D"; }
11
};
12
    
13
14
int main()
15
{
16
    B* p = new D;
17
    delete p;  // ~D~B
18
    // daca destructorul din B nu ar fi virtual, nu s-ar executa destructorul din D
19
    // la delete p, pentru ca p este de tip B* (se comporta ca o metoda normala)
20
    return 0;
21
}
Mai multe detalii despre functii virtuale aici (de la Microsoft) si aici (de la cppreference)

Ce face endl?

Fisiere si std::flush

Sa scrii intr-un fisier dureaza, pentru ca acel fisier nu este stocat in RAM. Dureaza pana se creeaza o conexiune la harddisk sau ssd ca sa accesezi ce e pe el, asa ca in loc sa scrii cate un byte (caracter) pe rand intr-un fisier, mai bine pui mai multi intr-un loc temporar (buffer) si cand se umple, pui totul in fisier deodata. Asta se intampla si cand scrii ceva intr-un fisier (sau cand citesti) folosind
fstream
.
1
#include <iostream>
2
#include <fstream>
3
4
using namespace std;
5
6
int main()
7
{
8
    ofstream g("out.txt");
9
    
10
    g << "Hello world!";
11
12
    return 0;
13
}
Aici
"Hello world!"
se pune intr-un buffer, un fel de array cu cateva mii de caractere, si cand se umple, continutul lui e pus in fisier (tot deodata). Asta se intampla si cand se apeleaza destructorul pentru obiectul g (pentru ca g este un obiect de tip
ofstream
). Acest lucru se face pentru a se goli bufferul in caz ca s-a scris ceva si nu s-a umplut bufferul. In cazul asta bufferul nu e plin, dar la sfarsitul functiei main, se distruge obiectul g si se scrie in fisier ce mai avea in buffer. Acum daca am avea o eroare dupa
g << "Hello world!";
, programul s-ar termina brusc si in fisier nu am mai avea nimic, pentru ca nici bufferul nu s-a umplut, nici destructorul nu a fost apelat.
1
#include <iostream>
2
#include <fstream>
3
4
using namespace std;
5
6
int main()
7
{
8
    ofstream g("out.txt");
9
    
10
    g << "Hello world!";
11
    int x = 0;
12
    cout << 1 / x; // eroare aici
13
    return 0;
14
}
Acum o sa avem o eroare la linia 12, ca impartim la 0. Eroarea asta nu e prinsa si programul se opreste. Fisierul ramane gol (chiar daca avea ceva in el la inceput, cand creezi un obiect de tip
ofstream
, daca nu mai dai alte argumente constructorului, se sterge continutul mai intai). Ok dar daca avem un fisier special unde scriem niste date din program sa tinem evidenta de ceva. Daca avem eroare si nu este prinsa, fisierul nu o sa fie complet, poate ne-ar fi ajutat continutul fisierului sa gasim eroarea.
Pentru asta exista
std::flush
, care este un manipulator pentru operatii de output (definit in <ostream>), ceva gen
std::setw()
. Daca scrii
g << "Hello world!" << flush;
fortezi bufferul sa fie scris in fisier, indiferent daca e plin sau nu. Deci este scris
"Hello world!"
in fisier si dupa avem eroarea, deci la final avem in fisier string-ul. Acum, sa fortezi scrierea bufferului in fisier daca nu e plin nu este deloc eficient, tocmai de asta exista bufferul, ca sa scrii cat mai mult deodata in fisier deci trebuie folosit doar cand ai nevoie.
Ce treaba are asta cu
endl
? Poate sti ca
g << endl
trece la urmatoarea linie, e ca si cum ai scrie
g << "\n";
dar pe langa asta face si un flush!, deci e echivalent cu
g << '\n' << flush;
(apropo,
cout << "\n";
e la fel cu
cout << '\n';
doar ca
"\n"
e un sir de caractere si
'\n'
e un caracter, deci mai bine folosesti un caracter)
Daca scrii multe linii in fisier si folosesti la fiecare
endl
, o sa ruleze mult mai incet decat daca nu ai folosi
endl
si ai pune
'\n'
pentru linie noua.
1
#include <iostream>
2
#include <fstream>
3
using namespace std;
4
5
void printWithEndl() {
6
    ofstream g("out.out");
7
    for (int i = 0; i < 100000; ++i) {
8
        g << "Hello world!" << endl;
9
    }
10
}
11
12
void printNoEndl() {
13
    ofstream g("out.out");
14
    for (int i = 0; i < 100000; ++i) {
15
        g << "Hello world!" << '\n';
16
    }
17
}
18
19
int main()
20
{
21
    printWithEndl();   // 300 ms
22
23
    printNoEndl();     // 18 ms
24
    return 0;
25
}
Functiile astea de mai sus printeaza 100000 de linii intr-un fisier. Functia care foloseste
endl
a durat 300ms si cea care nu foloseste a durat 18ms (am facut un test rapid pe calculatorul meu). Diferenta este mai mare de 10 ori (daca marim numarul de linii la 1 milion, diferenta e si mai drastica, 3000ms vs 130ms )
Deci, daca vrei ca programul tau sa ruleze mai rapid, nu mai folosi
endl
.