C++: Riferimenti e Puntatori

crono87
Il semplice programmino che segue inserisce o estrae interi dalla pila secondo il sistema LIFO.
La creazione del programma è molto semplice, ma non ho mai capito una cosa: perchè e quando usare i riferimenti o i puntatori.
In ogni funzione ho messo il riferimento ad un tipo pila semplicemente perchè senza di esso non funziona.
Non riesco a trovare documenti che soddisfino le mie lacune.
Vi ringrazio in anticipo

PS: ho postato un codice semplicemente per agevolare la risposta, ma la mia domanda si riferisce ad un caso generale qualsiasi

#include
#include

using namespace std;
typedef int T;
const int DIM = 5;
struct pila
{
int top;
T stack[DIM];
};
//inizializzazione della pila
void inip(pila& pp)
{
pp.top = -1;
}

bool empty(const pila& pp) // pila vuota?
{
if (pp.top == -1) return true;
return false;
}
bool full(const pila& pp) // pila piena?
{
if (pp.top == DIM - 1) return true;
return false;
}
bool push(pila& pp, T s) // inserisce un elemento in pila
{
if (full(pp)) return false;
pp.stack[++(pp.top)] = s;
return true;
}
bool pop(pila& pp, T& s) // estrae un elemento dalla pila
{
if (empty(pp)) return false;
s = pp.stack[(pp.top)--];
return true;
}
void stampa(const pila& pp) // stampa gli elementi
{
cout << "Elementi contenuti nella pila: " << endl;
for (int i = pp.top; i >= 0; i--)
cout << '[' << i << "] " << pp.stack << endl;
}

int main()
{
pila st;
inip(st);
T num;
if (empty(st)) cout << "Pila vuota" << endl;
for (int i = 0; i < DIM; i++)
if (push(st,DIM - i))
cout << "Inserito " << DIM - i <<
". Valore di top: " << st.top << endl;
else cerr << "Inserimento di " << i << " fallito" << endl;
if (full(st)) cout << "Pila piena" << endl;
stampa(st);
for (int i = 0; i < DIM - 2; i++)
if (pop(st, num))
cout << "Estratto " << num << ". Valore di top: "
<< st.top << endl;
else cerr << "Estrazione fallita" < for (int i = 0; i < DIM; i++)
if (push(st, i))
cout << "Inserito " << i << ". Valore di top: "
<< st.top << endl;
else cerr << "Inserimento di " << i << " fallito" << endl;
stampa(st);
for (int i = 0; i < 2; i++)
if (pop(st, num))
cout << "Estratto " << num << ". Valore di top:
" << st.top << endl;
else cerr << "Estrazione fallita" < stampa(st);
system("PAUSE");
return 0;
}

Risposte
Kawashita
Ciao, ciò che scrivo di seguito utilizza il C, spero che possa comunque esserti utile per capire meglio i puntatore.

puntatore: variabile che assume come valori degli indirizzi in memoria.

-consente di mantenere riferimenti ad altre variabili (è un punto di accesso ad altre variabili)
-consente di definire strutture dati dinamiche
-consente di realizzare il passaggio di parametri per referenza
-aritmetica dei puntatori (puntatori e array).

Dichiarazione di una variabile di tipo puntatore a tipo_T:

tipo_T *punt
;

Per capire bene l'utilizzo dei puntatori bisogna anche conoscere il passaggio di parametri per referenza:
-tu immagina da una funzione di chiamarne un'altra:(versione semplificata)
-viene allocato lo spazio necessario per la nuova funzione.
-viene svolta la funzione.
-viene deallocato lo spazio e restituito se c'è da restituire.
Quando il chiamante passa dei parametri es:

funz(){
int x=0;
chiamaFunzione(x);
prinf("x=%d",x);}

void funz2(int x){
x=x+1;}

in questo caso nella funz2 viene creata una variabile sosia della prima (x), viene incrementata di uno, e poi viene deallocata, ciò che verrà stampato da funz sarà x=0;:shock:
se invece il programma è
funz(){
int x=0;
chiamaFunzione(&x);
prinf("x=%d",x);}

void funz2(int x){
x=x+1;}

dal chiamante verrà passato la variabile vera e propria e non una copia, così alla fine verrà stampato x=1;(questo è un caso stupido in verità si possono passare infiniti parametri)

Tramite i puntatori diventa possibile realizzare strutture dati dinamiche, infatti posso
immaginare di costruire delle struct tali per cui uno dei campi è un puntatore a una
struttura dello stesso tipo.
esempio una lista o pila :
Ogni nodo è costituito da un dato e uno o + puntatori che mantengono la
struttura.
Infatti essi vengono legati fra loro tramite l'indirizzo che gli identifica univocamente, di creare anche puntatori secondari per scorrere la pila sulla base degli indirizzi. (FINE PRIMA PARTE)

dimmi cos'hai capito o se non hai capito nulla (probabile ):D che provo a riscrivertelo.

crono87
si, effettivamente ho capito poco :-D :-D :-D

diciamo che la mia domanda è questa

int a[10];

for (i=0, i<10; i++)
{
a=i;
}

questo è un semplice programma (parte di programma) che ad ogni a[x] associa il valore (x-1)

ora io so che quando dichiaro a[10] mi vengono allocate in memoria 10 celle di memoria consecutive, quidi potrei anche scrivere il programma come

int a[10];
int *p=&a; //il puntatore p viene inizializzato ad a[0]

for (i=0; i<10; i++)
{
*p=i;
p++;
}

Ecco. Le due parti di programma, almeno quanto a risultato, sono le stesse.

Quello che io mi chiedo: perchè ci si complica la vita con i riferimenti e i puntatori (come in questo caso)?
Ci sono delle situazioni che richiedono d'obbligo l'utilizzo di questi? (La risposta è scontata: chiaramente si, e questo lo dimostra in programma che ho postato prima), ma allora quali sono questi casi, e in che modo particolare devo sfruttare * e &?

elgiovo
Provo a risponderti: nel caso del tuo programma non riesci a capire perchè complicarsi la vita con dei puntatori in quanto effettivamente, in questa occasione, non serve!
L'utilità di puntatori e referenze sta tutta nel risparmio di memoria RAM quando viene eseguito un programma C++. Come giustamente Kawashita ha detto prima, con i puntatori si può fare il passaggio di parametri per referenza, e non per valore. Nel tuo ultimo programma, però, non vi sono passaggi di parametri, perchè non ci sono metodi, ma soltanto delle istruzioni sequenziali. Passando il parametro per referenza (quindi dichiarando come parametro formale del metodo un puntatore ad una zona di memoria) si evita che il metodo crei nello stack (che è la quantità di memoria che il programma si riserva all'avvio) zone di memoria che sono "doppioni" dei parametri formali del metodo stesso. In parole povere, se passi per valore, il metodo si "masterizza" i parametri, e questo implica un uso di memoria doppio; se passi per referenza, il metodo va direttamente a leggere i parametri, senza copiarseli, e questo implica un risparmio di memoria. Ecco perchè nelle applicazioni piuttosto complesse si preferisce un uso "dinamico" della memoria ad un uso "statico" di questa, mentre in quelle più semplici e immediate la differenza è impercettibile e, per semplicità, non si fa uso di puntatori.

crono87
ecco, ora ho capito già qlc... quindi la differenza sta solo nella velocità di esecuzione...
ma, per esempio, nel mio programma precedente, perchè ho dovuto usare i riferimenti?
sono stato obbligato, senza di essi non funziona

Splair
Provo a spiegarti bene tutti i punti che non ti sono chiari (anche se già lo hanno fatto elgiovo e Kawashita)....:

L'uso dinamico della memoria è sicuramente migliore rispetto ad un uso statico della stessa poichè si utilizza solo lo spazio che serve...
ad esempio:
se io dichiaro un array di 100 interi anche in modo dinamico (che dinamico in fondo non è) nel seguente modo:

int n=100;
int *vett=new int[n];

utilizzo solo la memoria che mi serve....Il fatto è che se (in fase di run-time) immetto 100 valori e ne cancello 90, lo spazio occupato in memoria sarà sempre quello di 100 interi.
L'uso dei puntatori è molto utile a questo scopo...
Se io creo, ad esempio una lista di 100 interi e poi ne elimino 90, lo spazio occupato in memoria sarà quello occupato da 10 interi e non da 100!!!! e questo non è poco...logicamente per applicazione complesse.
Ecco perchè è preferibile utilizzare i puntatori.
Per quanto riguarda il passaggio per valori e per riferimento (o per referenza) si distinguono perchè:

il passaggio per valori, non opera sul dato direttamente, ma ne effettua una copia e poi lo modifica.
Il passaggio per riferimento, opera direttamente sul dato passato alla funzione.

Utilizziamo una metafora forse un po sciocca ma molto utile...
Supponiamo di avere una lattina con un oggetto all’interno. La lattina identifica la variabile e l’oggetto il valore della variabile. Se effettuiamo un passaggio per valore, alla funzione o procedura, passeremo solo una copia dell’oggetto all’interno della lattina (quindi una copia della variabile). La lattina e l’oggetto non vengono direttamente modificati. Se effettuiamo un passaggio per riferimento, alla funzione o procedura, passeremo l’intera lattina con l’oggetto e saranno quindi, proprio la lattina e l’oggetto ad essere direttamente modificati. Per default, se non è specificato altro, il passaggio avviene per valore. Quanto detto però non vale per gli array poichè il passaggio per quest'ultimo tipo di dato avviene sempre per riferimento.

Il passaggio di riferimento ad esempio è molto utile quando si vuole creare una lista, inserire in testa alla lista, e tante altre cose...
Spero di aver soddisfatto le tue richiesti..
ciao

Splair
bool empty(const pila& pp)


se immetti const all'inizio il passaggio avviene per riferimento ma non viene modificato il valore, quindi potresti anche farne a meno in tutte le funzioni....

sinceramente non riesco a capire perchè questo programma così semplice lo esegui in modo così complesso...
Perchè utilizzi tutti bool????

Rispondi
Per rispondere a questa discussione devi prima effettuare il login.