MySQL Java JavaScript PHP Python HTML-CSS C-sharp C++ Go

Funcții virtuale și suprascrierea lor

Atunci când o funcție este apelată, programul trebuie să determine care implementare a funcției trebuie utilizată, adică să lege apelul funcției de funcția corespunzătoare. În C++ există două tipuri de legare: legare statică și legare dinamică.

Când apelurile funcțiilor sunt stabilite înainte de rularea programului, în timpul compilării, acest lucru se numește legare statică (static binding) sau legare timpurie (early binding). În acest caz, apelul funcției printr-un pointer este determinat doar de tipul pointerului, nu de obiectul către care pointează. De exemplu:

#include <iostream>

class Person
{
public:
    Person(std::string name): name{name} { }
    void print() const
    {
        std::cout << "Name: " << name << std::endl;
    }
private:
    std::string name;
};

class Employee: public Person
{
public:
    Employee(std::string name, std::string company): Person{name}, company{company} { }
    void print() const
    {
        Person::print();
        std::cout << "Works in " << company << std::endl;
    }
private:
    std::string company;
};

int main()
{
    Person tom {"Tom"};
    Person* person {&tom};
    person->print();     // Name: Tom

    Employee bob {"Bob", "Microsoft"};
    person = &bob;
    person->print();    // Name: Bob
}

În acest caz, clasa Employee moștenește clasa Person, dar ambele definesc o funcție print() care afișează informații despre obiect. În funcția main, creăm două obiecte și le atribuim, pe rând, unui pointer de tip Person. Apoi apelăm funcția print() prin acel pointer. Chiar dacă pointerul referă un obiect Employee, tot se va apela implementarea funcției din Person:

Employee bob {"Bob", "Microsoft"};
person = &bob;
person->print();    // Name: Bob

Așadar, alegerea implementării funcției se face în funcție de tipul pointerului, nu de tipul real al obiectului. Ieșirea în consolă va fi:

Name: Tom  
Name: Bob

Legare dinamică și funcții virtuale

Celălalt tip de legare este legarea dinamică (dynamic binding), cunoscută și ca legare târzie (late binding), care permite determinarea la rulare a funcției ce trebuie apelată. În C++, acest lucru se realizează cu ajutorul funcțiilor virtuale. Pentru a declara o funcție virtuală, aceasta trebuie precedată de cuvântul cheie virtual în clasa de bază. Această funcție poate fi apoi suprascrisă în clasa derivată.

Să facem funcția print() din clasa Person virtuală:

#include <iostream>

class Person
{
public:
    Person(std::string name): name{name} { }
    virtual void print() const
    {
        std::cout << "Name: " << name << std::endl;
    }
private:
    std::string name;
};

class Employee: public Person
{
public:
    Employee(std::string name, std::string company): Person{name}, company{company} { }
    void print() const
    {
        Person::print();
        std::cout << "Works in " << company << std::endl;
    }
private:
    std::string company;
};

int main()
{
    Person tom {"Tom"};
    Person* person {&tom};
    person->print();     // Name: Tom

    Employee bob {"Bob", "Microsoft"};
    person = &bob;
    person->print();     // Name: Bob
                         // Works in Microsoft
}

Astfel, clasa de bază Person definește o funcție virtuală print(), iar clasa derivată Employee o suprascrie. În exemplul anterior, unde print() nu era virtuală, Employee doar o ascundea, nu o suprascria. Acum, apelând print() printr-un pointer Person* către un obiect Employee, se va apela implementarea din Employee.

Ieșirea va fi:

Name: Tom  
Name: Bob  
Works in Microsoft

Aceasta este diferența dintre suprascrierea funcțiilor virtuale și ascunderea funcțiilor.

O clasă care definește sau moștenește o funcție virtuală se numește clasă polimorfă (polymorphic class). Așadar, Person și Employee sunt clase polimorfe.

Este important de menționat că apelul unei funcții virtuale prin numele unui obiect este întotdeauna rezolvat static:

Employee bob {"Bob", "Microsoft"};
Person p = bob;
p.print();  // Name: Bob – legare statică

Legarea dinamică este posibilă doar prin pointer sau referință:

Employee bob {"Bob", "Microsoft"};
Person &p {bob};
p.print();  // legare dinamică

Person *ptr {&bob};
ptr->print();  // legare dinamică

Pentru ca o funcție să participe la legare dinamică, în clasa derivată ea trebuie să aibă exact aceeași semnătură: aceiași parametri, același tip de retur și aceleași calificatori (precum const). Dacă semnătura diferă (de exemplu lipsește const), funcția nu este suprascrisă, ci ascunsă, și se va aplica legarea statică.

De asemenea, funcțiile statice nu pot fi virtuale.

Cuvântul cheie override

Pentru a indica în mod explicit că vrem să suprascriem o funcție (și nu să o ascundem), putem folosi cuvântul cheie override:

#include <iostream>

class Person
{
public:
    Person(std::string name): name{name} { }
    virtual void print() const
    {
        std::cout << "Name: " << name << std::endl;
    }
private:
    std::string name;
};

class Employee: public Person
{
public:
    Employee(std::string name, std::string company): Person{name}, company{company} { }
    void print() const override
    {
        Person::print();
        std::cout << "Works in " << company << std::endl;
    }
private:
    std::string company;
};

int main()
{
    Person tom {"Tom"};
    Person* person {&tom};
    person->print();     // Name: Tom

    Employee bob {"Bob", "Microsoft"};
    person = &bob;
    person->print();     // Name: Bob
                         // Works in Microsoft
}

Aici, linia:

void print() const override

indică în mod explicit că vrem să suprascriem funcția print(). Poate părea inutil, deoarece și fără override suprascrierea funcționează, dar:

  • override îl informează pe compilator despre intenția noastră
  • dacă semnătura nu corespunde funcției virtuale din clasa de bază, compilatorul va genera o eroare
  • fără override, eroarea poate trece neobservată, iar funcția va fi tratată ca o funcție nouă (ascunsă), nu suprascrisă

De aceea, este recomandat să folosim override atunci când suprascriem funcții virtuale. O funcție virtuală poate fi suprascrisă de-a lungul întregii ierarhii, nu doar în clasele derivate directe.

Mecanismul de execuție al funcțiilor virtuale

Funcțiile virtuale au un cost: obiectele claselor cu funcții virtuale ocupă puțin mai multă memorie și sunt ușor mai lente la apel. Acest lucru se întâmplă deoarece:

  • la instanțierea unui obiect al unei clase polimorfe, se creează un pointer special în obiect
  • acest pointer indică spre o tabelă virtuală (vtable) – o structură de date care conține adresele funcțiilor virtuale pentru acea clasă
  • la apelul unei funcții virtuale, programul folosește pointerul către vtable, caută adresa funcției dorite în vtable, și o apelează

Această căutare face ca apelul unei funcții virtuale să fie puțin mai lent decât apelul unei funcții obișnuite, dar oferă flexibilitatea polimorfismului.

Interzicerea suprascrierii

Cu ajutorul specificatorului final, putem interzice ca funcțiile din clasele derivate să fie redefinite, dacă acestea au același nume, tip de retur și listă de parametri ca o funcție virtuală din clasa de bază. De exemplu:

class Person
{
public:
    virtual void print() const final
    {
         
    }
};

class Employee : public Person
{
public:
    void print() const override     // Eroare!!!
    {
         
    }
};

De asemenea, putem suprascrie o funcție din clasa de bază, dar interzice ca aceasta să fie suprascrisă mai departe în clasele derivate:

class Person
{
public:
    virtual void print() const // suprascrierea este permisă
    {
         
    }
};

class Employee : public Person
{
public:
    void print() const override final   // în clasele derivate din Employee suprascrierea este interzisă
    {
         
    }
};