[C++] distruttori e memoria stack
Ciao!
prima di esprimere la mia perplessità scrivo un frammento di codice(funzione)
quando il programma esce dalla funzione vengono distrutte le variabili locali A,B
la memoria allocata, con la prima istruzione nella funzione, rimane tale anche dopo l'uscita dalla funzione.
domanda uno: il puntatore sum_Base viene distrutto lasciando intatto l'oggetto a cui punta?
domanda due: se al posto di passare A,B avessi passato &A e &B non appena il programma sarebbe uscito dall'ambito della funzione sarebbero stati distrutti gli oggetti A,B?
a tal proposito ho pensato che vengono distrutti solo gli elementi presenti sulla memoria stack e che quindi
1- il puntatore viene distrutto
2- gli oggetti A,B non vengono distrutti perchè non sono sullo stack.
prima di esprimere la mia perplessità scrivo un frammento di codice(funzione)
class Base{ int x; }; Base& operator+(Base A,Base B){ Base* sum_Base{new Base}; sum_Base.x=A.x+B.x; return *sum_Base; }
quando il programma esce dalla funzione vengono distrutte le variabili locali A,B
la memoria allocata, con la prima istruzione nella funzione, rimane tale anche dopo l'uscita dalla funzione.
domanda uno: il puntatore sum_Base viene distrutto lasciando intatto l'oggetto a cui punta?
domanda due: se al posto di passare A,B avessi passato &A e &B non appena il programma sarebbe uscito dall'ambito della funzione sarebbero stati distrutti gli oggetti A,B?
a tal proposito ho pensato che vengono distrutti solo gli elementi presenti sulla memoria stack e che quindi
1- il puntatore viene distrutto
2- gli oggetti A,B non vengono distrutti perchè non sono sullo stack.
Risposte
La memoria allocata da [tt]new Base[/tt] non viene deallocata automaticamente, ma deve essere deallocata manualmente dal codice che utilizza la tua funzione. Gli oggetti A e B sono nello stack essendo variabili temporanee allocate quando la funzione viene richiamata. È un pessimo esempio sinceramente in quanto non è affatto chiaro che il valore restituito è stato allocato dinamicamente.
Concordo, non è una buona prassi passare un oggetto allocato dinamicamente come riferimento invece che come puntatore. Non è inoltre molto comune allocare dinamicamente dentro l'operatore di somma[nota]Tra l'altro, l'operatore di somma dovrebbe, a mio avviso, sempre ritornare la classe e non un riferimento.[/nota].
Per farti capire il problema, supponiamo che l'operatore moltiplicazione si comporti nello stesso modo e che si faccia qualcosa come:
Questo codice apparentemente innocuo e valido produrrebbe ben 3 memory leaks!
Per farti capire il problema, supponiamo che l'operatore moltiplicazione si comporti nello stesso modo e che si faccia qualcosa come:
auto result = a * b + c * d;
Questo codice apparentemente innocuo e valido produrrebbe ben 3 memory leaks!
Comunque se vuoi capire che succede puoi anche compilare ed eseguire qualcosa come questo:
#include <iostream> struct Base { Base( ) : x( 0 ) { std::cout << "costruttore default\n"; } explicit Base( int x ) : x( x ) { std::cout << "costruttore con valore " << x << std::endl; } Base( const Base& B ) : x( B.x ) { std::cout << "costruttore copia con valore " << x << std::endl; } ~Base( ) { std::cout << "distruttore elemento con valore " << x << std::endl; } int x; }; Base& operator+( Base A, Base B ) { std::cout << "Operazione iniziata\n"; Base* sum_Base{new Base}; sum_Base->x = A.x + B.x; std::cout << "Operazione finita\n"; return *sum_Base; } int main( ) { auto A = Base{3}; auto B = Base{5}; std::cout << "Prime allocazioni\n"; auto* C = &( A + B ); // per renderlo valido std::cout << "Operazione conclusa\n"; std::cout << "Il risultato e' " << C->x << std::endl; delete C; }
Pardon per l'esempio, non è un qualcosa che ho implementato.
Vi riporto quello che ho fatto realmente(mi sto esercitando quindi provo ad inventarmi qualcosa).
@Vict
ho ritornato un riferimento perché onestamente non sapevo come tenermi la memoria allocata visto che opero con zone di memoria.
definizioni che possono essere utili
l'esempio era praticamente il seguente
l'operatore rimuove dalla lista di sinistra gli elementi comuni con la lista di destra
quello che viene ritornato è il riferimento ad una lista
in questo caso quando si uscirà dall'ambito dell'oggetto ritornato la memoria verrà deallocata.
per esempio se compilo questo codice e setto un breakpoint alla riga 12, viene evocato il distruttore e dealloca la memoria.
Quando si esce dall'ambito di operator- vengono deallocate la zona di memoria occupata dal puntatore e la copia dei due oggetti i quali sono allocata sullo stack, diversamente accade per l'oggetto puntato che è allocato dinamicamente e quindi persiste uscendo dalla funzione, fino a quando non si esce dall'ambito di definizione dell'oggetto ritornato. E' corretto?
invece per il seguente frammento
alla riga 11 l'oggetto la memoria non viene deallocata poiché il riferimento è passato ad un oggetto con scope più ampio.
PS: in questo caso le operazioni che vengono fatte per copiare gli oggetti all'interno della funzione sono parecchie e potrebbero essere allocati parecchi byte di memoria: non sarebbe meglio passare le liste per riferimento evitandosi tutte queste operazioni?
Vi riporto quello che ho fatto realmente(mi sto esercitando quindi provo ad inventarmi qualcosa).
@Vict
ho ritornato un riferimento perché onestamente non sapevo come tenermi la memoria allocata visto che opero con zone di memoria.
definizioni che possono essere utili
l'esempio era praticamente il seguente
l'operatore rimuove dalla lista di sinistra gli elementi comuni con la lista di destra
template<class T> List<T>& operator-(List<T> a_List,List<T> b_List){ List<T>* diff_List{new List<T>{a_List}}; for(size_t i{a_List.size()} ; i>=1; --i){ size_t j{1}; while(j<=b_List.size() and a_List[i]!=b_List[j] ) ++j; if(j<=b_List.size()) (*diff_List).remove(i); } return (*diff_List); }
quello che viene ritornato è il riferimento ad una lista
in questo caso quando si uscirà dall'ambito dell'oggetto ritornato la memoria verrà deallocata.
per esempio se compilo questo codice e setto un breakpoint alla riga 12, viene evocato il distruttore e dealloca la memoria.
1 #include "Listdef.cpp" 2 3 int main(void) { 4 5 List<int> A{1,2,3}; 6 List<int> B{3,7}; 7 8 { 9 List<int> C{B-A}; 10 } 11 12 }
Quando si esce dall'ambito di operator- vengono deallocate la zona di memoria occupata dal puntatore e la copia dei due oggetti i quali sono allocata sullo stack, diversamente accade per l'oggetto puntato che è allocato dinamicamente e quindi persiste uscendo dalla funzione, fino a quando non si esce dall'ambito di definizione dell'oggetto ritornato. E' corretto?
invece per il seguente frammento
1 #include "Listdef.cpp" 2 3 int main(void) { 4 5 List<int> A{1,2,3}; 6 List<int> B{3,7}; 7 List<int> C{}; 8 9 { 10 C=B-A; 11 } 12 13 }
alla riga 11 l'oggetto la memoria non viene deallocata poiché il riferimento è passato ad un oggetto con scope più ampio.
PS: in questo caso le operazioni che vengono fatte per copiare gli oggetti all'interno della funzione sono parecchie e potrebbero essere allocati parecchi byte di memoria: non sarebbe meglio passare le liste per riferimento evitandosi tutte queste operazioni?
Nel codice che hai scritto c'è una lista temporanea che non viene deallocata. Ti consiglio di fare uso di unique_ptr o shared_ptr per gestire la memoria se hai dubbi. Fai inoltre uso di std::vector invece di scriverti una tua versione.
"anto_zoolander":
@Vict
ho ritornato un riferimento perché onestamente non sapevo come tenermi la memoria allocata visto che opero con zone di memoria.
[...]
l'esempio era praticamente il seguente
l'operatore rimuove dalla lista di sinistra gli elementi comuni con la lista di destra
template<class T> List<T>& operator-(List<T> a_List,List<T> b_List){ List<T>* diff_List{new List<T>{a_List}}; for(size_t i{a_List.size()} ; i>=1; --i){ size_t j{1}; while(j<=b_List.size() and a_List[i]!=b_List[j] ) ++j; if(j<=b_List.size()) (*diff_List).remove(i); } return (*diff_List); }
quello che viene ritornato è il riferimento ad una lista
in questo caso quando si uscirà dall'ambito dell'oggetto ritornato la memoria verrà deallocata.
per esempio se compilo questo codice e setto un breakpoint alla riga 12, viene evocato il distruttore e dealloca la memoria.
1 #include "Listdef.cpp" 2 3 int main(void) { 4 5 List<int> A{1,2,3}; 6 List<int> B{3,7}; 7 8 { 9 List<int> C{B-A}; 10 } 11 12 }
Quando si esce dall'ambito di operator- vengono deallocate la zona di memoria occupata dal puntatore e la copia dei due oggetti i quali sono allocata sullo stack, diversamente accade per l'oggetto puntato che è allocato dinamicamente e quindi persiste uscendo dalla funzione, fino a quando non si esce dall'ambito di definizione dell'oggetto ritornato. E' corretto?
invece per il seguente frammento
1 #include "Listdef.cpp" 2 3 int main(void) { 4 5 List<int> A{1,2,3}; 6 List<int> B{3,7}; 7 List<int> C{}; 8 9 { 10 C=B-A; 11 } 12 13 }
alla riga 11 l'oggetto la memoria non viene deallocata poiché il riferimento è passato ad un oggetto con scope più ampio.
PS: in questo caso le operazioni che vengono fatte per copiare gli oggetti all'interno della funzione sono parecchie e potrebbero essere allocati parecchi byte di memoria: non sarebbe meglio passare le liste per riferimento evitandosi tutte queste operazioni?
Tutto quello che hai detto è in realtà sbagliato. L'unica differenza tra i due casi è che l'oggetto \(C\) ha una vita diversa per cui la sua memoria verrà deallocata in momenti diversi. Tuttavia la vita degli oggetti temporanei allocati nella funzione non cambia. La lista allocata dinamicamente in particolare non viene deallocata.
Il metodo probabilmente "corretto" sarebbe stato implementando un "move constructor" e scrivendo:
template<class T> List<T> operator-(const List<T>& a_List, const List<T>& b_List){ List<T> diff_List = a_List; // ... return std::move(diff_List); }
Nota che le due liste passate come argomento sono passate per riferimento e non per valore. Le stai inutilmente copiando.
Il codice interno è comunque incredibilmente inefficiente (qualcosa come \(O(n^3)\) probabilmente), ma non l'ho calcolata più di tanto bene. In linea di massima, questo NON è il modo in cui vuoi usare una lista concatenata.
"anto_zoolander":
anzi se mi deste qualche esercizio tosto di python/c++ ve ne sarei grato.
Non sono sicuro che cosa intendi. Vuoi che inventiamo dei problemi da risolvere in C++ o Python? Hai dato un occhiata a siti come Codility? Vuoi qualcosa più sull'organizzazione del codice?
Ciao 
Diciamo che sto smanettando con c++ in autonomia e senza troppe pretese sulla efficienza per adesso.
Probabilmente se volessi provare a farlo meglio eviterei la classe Node e lavorerei con una sola classe.
Ho messo quel blocco in mezzo al nulla per chiarirmi un dubbio: un oggetto allocato dinamicamente all'interno di una funzione, e ritornato da essa il suo riferimento, ha lo scope della variabile che 'prende' questo oggetto.
Nel messaggio precedente, alla fine, avevo proprio chiesto se non fosse meglio passarle per riferimento proprio al fine di evitare sprechi.
Non conosco Codiliity, adesso lo guardo.
Mi interessano esercizi che mi allenino ad, appunto, organizzare meglio il codice e allo stesso tempo che siano "complicati".
Ovviamente non chiederei di crearmi esercizi, però se ne avete a disposizione...

Diciamo che sto smanettando con c++ in autonomia e senza troppe pretese sulla efficienza per adesso.
Probabilmente se volessi provare a farlo meglio eviterei la classe Node e lavorerei con una sola classe.
"apatriarca":
l'oggetto C ha una vita diversa per cui la sua memoria verrà deallocata in momenti diversi. Tuttavia la vita degli oggetti temporanei allocati nella funzione non cambia. La lista allocata dinamicamente in particolare non viene deallocata
Ho messo quel blocco in mezzo al nulla per chiarirmi un dubbio: un oggetto allocato dinamicamente all'interno di una funzione, e ritornato da essa il suo riferimento, ha lo scope della variabile che 'prende' questo oggetto.
"apatriarca":
Nota che le due liste passate come argomento sono passate per riferimento e non per valore. Le stai inutilmente copiando
Nel messaggio precedente, alla fine, avevo proprio chiesto se non fosse meglio passarle per riferimento proprio al fine di evitare sprechi.
"apatriarca":
Non sono sicuro che cosa intendi. Vuoi che inventiamo dei problemi da risolvere in C++ o Python? Hai dato un occhiata a siti come Codility? Vuoi qualcosa più sull'organizzazione del codice?
Non conosco Codiliity, adesso lo guardo.
Mi interessano esercizi che mi allenino ad, appunto, organizzare meglio il codice e allo stesso tempo che siano "complicati".
Ovviamente non chiederei di crearmi esercizi, però se ne avete a disposizione...

"anto_zoolander":
Ho messo quel blocco in mezzo al nulla per chiarirmi un dubbio: un oggetto allocato dinamicamente all'interno di una funzione, e ritornato da essa il suo riferimento, ha lo scope della variabile che 'prende' questo oggetto.
Non ce l'ha. Per vederlo puoi considerare il seguente codice:
#include <iostream> struct A { A() { std::cerr << "Create A\n"; } A(const A& a) { std::cerr << "Copy A\n"; } A(A&& a) { std::cerr << "Move A\n"; } ~A() { std::cerr << "Destroy A\n"; } }; A& operator-(const A& a1, const A& a2) { A* result = new A; return (*result); } int main(void) { A first; A second; A third{second - first}; }
Se provi ad eseguirlo ottieni
Create A Create A Create A Copy A Destroy A Destroy A Destroy A
Vengono cioè creati/allocati 4 oggetti di tipo [tt]A[/tt] ma solo 3 vengono distutti/deallocati.
Questo perché non viene deallocato l'oggetto definito all'interno del costruttore di copia, giusto?
Vediamo se ho capito:
Se
Fosse stato
Si sarebbero creati e deallocati esattamente 3 oggetti?
Vediamo se ho capito:
Se
A third{second-first} ;
Fosse stato
A& third{second - third} ;
Si sarebbero creati e deallocati esattamente 3 oggetti?
Ti consiglio di fare esperimenti, ma il risultato in quel caso sarebbe:
Hai insomma eliminato la creazione e distruzione dell'oggetto [tt]third[/tt] ottenendo 3 chiamate al construttore e due al distruttore (e nessuna al costruttore di copia che veniva usato per [tt]third[/tt]).
Se allochi della memoria in una funzione che deve essere deallocata al di fuori di essa hai le seguenti opzioni:
1. Restituire un puntatore a tale memoria. Il codice che fa uso della funzione si deve ricordare di deallocare tale memoria.
2. Restituire la memoria usando uno smart pointer ([tt]shared_ptr[/tt] o [tt]unique_ptr[/tt]).
In linea di massima, quando si ha a che fare con un container di qualche tipo è difficile che uno si metta a definire una funzione come [tt]operator-[/tt] a meno di copiare il container (eventualmente usando la move semantic). L'alternativa più comune è quella di richiedere il passaggio (per riferimento) di un container o un iteratore da usare come output. A tale proposito considera la funzione set_difference in [tt]algorithm[/tt]. L'output è scritto su un output iterator (un iteratore che permette la scrittura nella posizione corrente e l'incremento).
Create A Create A Create A Destroy A Destroy A
Hai insomma eliminato la creazione e distruzione dell'oggetto [tt]third[/tt] ottenendo 3 chiamate al construttore e due al distruttore (e nessuna al costruttore di copia che veniva usato per [tt]third[/tt]).
Se allochi della memoria in una funzione che deve essere deallocata al di fuori di essa hai le seguenti opzioni:
1. Restituire un puntatore a tale memoria. Il codice che fa uso della funzione si deve ricordare di deallocare tale memoria.
2. Restituire la memoria usando uno smart pointer ([tt]shared_ptr[/tt] o [tt]unique_ptr[/tt]).
In linea di massima, quando si ha a che fare con un container di qualche tipo è difficile che uno si metta a definire una funzione come [tt]operator-[/tt] a meno di copiare il container (eventualmente usando la move semantic). L'alternativa più comune è quella di richiedere il passaggio (per riferimento) di un container o un iteratore da usare come output. A tale proposito considera la funzione set_difference in [tt]algorithm[/tt]. L'output è scritto su un output iterator (un iteratore che permette la scrittura nella posizione corrente e l'incremento).
Grazie per i consigli.
Ci tengo molto ad imparare bene questo linguaggio, mi piace molto.
In effetti ne faccio molti di esperimenti ma poi finisco per impelagarmi in qualche groviglio
ho compilato questo:
Ottengo che la variabile $y$ assume "l'identità" di x
Mentre se tolgo il riferimento da $y$ no.
che penso sarebbe la stessa cosa di tornare un puntatore.
A questo punto mi viene spontanea una domanda: le funzioni che ritornano un riferimento ha senso usarle, in un constesto di inizializzazione/assegnazione, solo per variabili reference?
PS: mi sono iscritto su Codility, grazie per avermelo segnalato
Ci tengo molto ad imparare bene questo linguaggio, mi piace molto.
In effetti ne faccio molti di esperimenti ma poi finisco per impelagarmi in qualche groviglio

ho compilato questo:
#include <iostream> int& f(void) { int* x = new int{}; std::cout<<"indirizzo nella funzione= "<<x<<std::endl; return *x; } int main(void) { int& y{f()}; std::cout<<"indirizzo fuori funzione= "<<&y<<std::endl; }
Ottengo che la variabile $y$ assume "l'identità" di x
Mentre se tolgo il riferimento da $y$ no.
che penso sarebbe la stessa cosa di tornare un puntatore.
A questo punto mi viene spontanea una domanda: le funzioni che ritornano un riferimento ha senso usarle, in un constesto di inizializzazione/assegnazione, solo per variabili reference?
PS: mi sono iscritto su Codility, grazie per avermelo segnalato

È certamente possibile scrivere
dove [tt]y[/tt] è un riferimento ad un blocco di memoria allocato dinamicamente. Tuttavia si da normalmente per scontato che la memoria puntata da un riferimento sia gestita da qualcun altro e che la vita della memoria è più grande della vita del riferimento.
Una funzione che restituisce un reference ha senso in questi casi:
1. Stai restituendo [tt]*this[/tt] da una variabile membro di una funzione. È questo per esempio il caso degli operatori di assegnamento.
2. Restituisci il riferimento ad una variabile globale o una variabile statica in una classe o funzione.
Non è comunque difficile pensare ad esercizi che puoi fare. Per esempio puoi provare a scrivere un programma che
1. legge una espressione e ne calcola il risultato. O che ne calcola la derivata o altro. Una specie di calcolatrice insomma o un linguaggio di programmazione semplificato.
2. copia il comportamento di qualche utility linux (grep per esempio)
3. implementa un qualche gioco di carte o scacchi/dama..
4. implementa una semplice chat
5. implementa un ray tracer
delete &y;
dove [tt]y[/tt] è un riferimento ad un blocco di memoria allocato dinamicamente. Tuttavia si da normalmente per scontato che la memoria puntata da un riferimento sia gestita da qualcun altro e che la vita della memoria è più grande della vita del riferimento.
Una funzione che restituisce un reference ha senso in questi casi:
1. Stai restituendo [tt]*this[/tt] da una variabile membro di una funzione. È questo per esempio il caso degli operatori di assegnamento.
2. Restituisci il riferimento ad una variabile globale o una variabile statica in una classe o funzione.
Non è comunque difficile pensare ad esercizi che puoi fare. Per esempio puoi provare a scrivere un programma che
1. legge una espressione e ne calcola il risultato. O che ne calcola la derivata o altro. Una specie di calcolatrice insomma o un linguaggio di programmazione semplificato.
2. copia il comportamento di qualche utility linux (grep per esempio)
3. implementa un qualche gioco di carte o scacchi/dama..
4. implementa una semplice chat
5. implementa un ray tracer
In generale una funzione che ritorna un riferimento può essere anche utilizzata come l-value.
E' un esempio stupido, ma è la prima cosa che mi è venuta in mente:
Se ti può essere utile butto lì alcune idee basate su programmi che ho realizzato in passato: una classe per la gestione dei numeri razionali o del calcolo matriciale, gioco della scopa, risolutore di sudoku (magari basato anche su strategie logiche e non solo sulla forza bruta), tetris, ...
E' un esempio stupido, ma è la prima cosa che mi è venuta in mente:
#include <iostream> using namespace std; int& massimo(int* v, const unsigned int dim) { unsigned int i = 0; for(unsigned int j = 1; j < dim; ++j) { if(v[j] > v[i]) { i = j; } } return v[i]; } int main() { const unsigned int dim = 7; int v[dim] = {4, 5, 3, 1, -6, 8, -2}; massimo(v, dim) = 0; for(unsigned int i = 0; i < dim; ++i) { cout << v[i] << " "; } }
Se ti può essere utile butto lì alcune idee basate su programmi che ho realizzato in passato: una classe per la gestione dei numeri razionali o del calcolo matriciale, gioco della scopa, risolutore di sudoku (magari basato anche su strategie logiche e non solo sulla forza bruta), tetris, ...