ProgrammazioneSviluppatore Backend (C++)

Cosa sono le funzioni virtuali e come funziona il meccanismo di linking ritardato in C++?

Supera i colloqui con l'assistente IA Hintsage

Risposta.

Storia della domanda:

In C++ è stato introdotto il supporto per la programmazione orientata agli oggetti, fondamentale per i linguaggi moderni. Le funzioni virtuali sono state utilizzate per implementare il polimorfismo. Questo consentiva di chiamare l'implementazione corretta del metodo al momento dell'esecuzione, e non solo durante la compilazione, il che è critico per un'architettura basata sull'ereditarietà.

Problema:

Un errore comune è la confusione tra la chiamata statica e dinamica dei metodi, i distruttori virtuali dimenticati, un uso errato dell'ereditarietà (ad esempio, oggetto slicing, chiamata alla versione base invece di override). Spesso si confonde quando il polimorfismo funziona realmente.

Soluzione:

Una funzione virtuale viene dichiarata usando la parola chiave virtual nella classe base e può essere sovrascritta (override) nella derivata. Se si chiama una funzione tramite un puntatore o un riferimento alla classe base, verrà eseguita la versione della classe derivata.

Esempio di codice:

struct Base { virtual void foo() { std::cout << "Base::foo "; } }; struct Derived : Base { void foo() override { std::cout << "Derived::foo "; } }; void call(Base& b) { b.foo(); } int main() { Derived d; call(d); // Mostrerà Derived::foo }

Caratteristiche chiave:

  • Linking ritardato (dynamic dispatch): la scelta della versione del metodo avviene durante l'esecuzione
  • Funziona tramite puntatori e riferimenti alla classe base
  • La corretta sovrascrittura delle funzioni richiede la parola chiave override (possibile a partire da C++11)

Domande insidiose.

Il polimorfismo funziona quando si passa un oggetto per valore?

No. Il passaggio per valore porta a "slicing" — viene copiata solo la parte corrispondente al tipo del parametro (di solito la classe base), il polimorfismo si disabilita.

Esempio di codice:

void call(Base b) { b.foo(); } // sempre chiamata a Base::foo

È necessario dichiarare il distruttore come virtuale nella classe base?

Sì, se si prevede di eliminare oggetti derivati tramite un puntatore alla classe base. Altrimenti, ci sarà una perdita di memoria o risorse non chiuse.

Esempio di codice:

struct Base { virtual ~Base() {} };

Cosa succede se non si utilizza la parola chiave override nella classe derivata?

Se nella classe derivata non viene specificato override, ma si modifica erroneamente la firma della funzione (ad esempio, si omette const o si sbaglia nei parametri), la funzione non sovrascrive quella virtuale, ne viene creata una nuova e il polimorfismo non funziona come previsto.

Errori comuni e anti-pattern

  • Mancata dichiarazione del distruttore virtuale
  • Override implementato in modo errato (assenza di override, modifica della firma)
  • Utilizzo di parametri per valore invece di riferimenti/puntatori, portando a slicing

Esempio dalla vita reale

Caso negativo

Un programmatore non ha dichiarato il distruttore della classe base come virtuale; la cancellazione di un array di oggetti tramite un puntatore base ha portato a una perdita di memoria.

Vantaggi:

  • Funzionamento corretto fino al momento dell'eliminazione degli oggetti

Svantaggi:

  • Perdite, le risorse non sono state rilasciate

Caso positivo

È stato dichiarato un distruttore virtuale; sono stati utilizzati solo riferimenti/puntatori al tipo base. Il polimorfismo ha funzionato correttamente.

Vantaggi:

  • Sicurezza nel rilascio della memoria
  • Codice pulito e scalabile

Svantaggi:

  • Leggero aumento della memoria e del tempo di esecuzione a causa della tabella virtuale