Punters:
Fins ara hem vist variables que emmagatzemen valors. Tenim enters, caràcters, nombres reals... Fins i tot tenim strings, tot i que no és un tipus de variable, per emmagatzemar cadenes de caràcters.Un punter no emmagatzema cap valor. El que emmagatzema és l'adreça d'una altra variable. És a dir, apunta a una altra variable. Per declarar-los, només cal posar el tipus al que apuntarem seguit d'un *. Seria una cosa així
int* p; //p és un punter
Aleshores, si volem donar-li valor, el que volem és donar-li una adreça. Per accedir a l'adreça d'una variable, utilitzem l'operador &, davant de la variable. És a dir, si féssim
int* p=&n;
p apuntarà a la variable n. Sempre que utilitzem l'operador & amb una variable de tipus T, ens retornarà un T*. Això ens permet fer
int **p2=&p;
I p2 apunta a p.
Com mirem la variable a la que apunta un punter?
Si volem mirar la variable a la que apunta un punter, es fa amb l'operador de desreferència. Aquest és l'asterisc. Així, si fem
...
int* p;
...
int num=*p;
A num posarem el número que conté la variable apuntada per p.
Si tinguéssim una struct com la següent
struct S {
int a, b;
};
int a, b;
};
I un punter a una struct d'aquestes, podríem fer
int a=p->a;
int b=p->b;
I això equivaldria a fer
int a=(*p).a;
int b=(*p).b;
En C els punters estaven fins i tot a la sopa. Com que no s'havien inventat les referències, es passaven punters a les variables. De fet, això es segueix conservant parcialment, si utilitzes la funció scanf de la llibreria cstdlib, has de passar-li un punter a la variable. És a dir, si volguéssim llegir un enter, podríem fer
int n;
scanf("d",&n);
Això equivaldria a fer
int n;
cin >>n;
No obstant, ara, a C++, els punters perden moltes de les utilitats que tenien abans. Segueixen sent molt útils, però per altres coses
Una cosa en la que potser algú s'ha fixat és en que, mentre que cada tipus de variable ocupa un espai diferent en memòria, un punter emmagatzema adreces. Això fa que un int* tingui la mateixa mida que un char*, o un long*. Aleshores, podríem pensar en utilitzar un punter sense saber, a priori, a què apunta. Total, la mida que ocupen tots els punters és la mateixa...
Void*
Espera, espera, no m'he equivocat? Allà posa void*, és a dir, punter a void. Però no podem crear una variable void, no? No té cap sentit, void vol dir buit. O potser sí que té sentit?
Al cap i a la fi, una funció void és aquella que no retorna res. Per tant, un void* pot ser un punter que no apunta a res en concret. Això ens permet tenir funcions que rebin i/o retornin punters sense indicar a què apunten. Un exemple seria la funció scanf, que rep un punter i un string (un string de C; és a dir, un char[] o char*) que li indica a què apunta aquell punter. La funció fa la conversió, llegeix i emmagatzema allà.
Un altre exemple seria la funció malloc. Aquesta funció serveix per reservar memòria en el heap, que és una regió de la memòria molt útil (es reserva dinàmicament, no pertany a cap funció en concret...). Si volguéssim reservar espai per 100 enters sense utilitzar cap classe (és a dir, no podem utilitzar vectors), podríem utilitzar-la. Faríem
int* arr=malloc(100*sizeof(int));
I arr apuntaria al primer de 100 enters que tenim reservats. De fet, és bastant similar al que fa internament la classe vector. Com podem veure, la funció malloc sempre rep el mateix, que és un enter que li indica quant espai volem reservar (recordem que la funció sizeof retorna la mida en bytes del que li passem). Per tant, no sap en cap moment què desarem allà, i ens retorna un void* apuntant a l'inici. Nosaltres, com que l'emmagatzemem a un int*, ja estem fent una conversió implícita
Com a parèntesi important, si fem un malloc, sempre hem de recordar que hem d'alliberar aquest espai. Es fa amb la funció free, que rep el punter que apunta a l'espai a alliberar. És a dir, si volem alliberar l'espai reservat per arr, faríem
free(arr);
Referències
En C++ apareixen les referències. El concepte és molt senzill. Enlloc d'apuntar a la variable, per què no fer que siguin exactament el mateix. És a dir, quan tu declares una variable n, el que fas és reservar una regió de memòria per n. Quan fas un punter, reserves espai per ell, i fas que contingui l'adreça de la variable on apuntes. La idea de la referència és que les dues variables comparteixin la regió de memòria, de manera que quan es modifica un, es modifica l'altre.
Com es declara?
Fàcil. Per declarar una referència a T, fas T&. Per exemple, si volguéssim declarar una referència a un enter, faríem
int& enter=n;
Això faria que enter i n fossin la mateixa variable, quan canvies el valor d'una canvia la de l'altra.
Per a què serveix això?
Per moltes coses. Imaginem que tenim una matriu mat, i volem modificar-la de manera que el primer element sigui igual al més petit de la matriu, i l'últim el més gran. Podríem fer això:
void arreglar(Matriu& mat) {
int n=mat.size();
int m=mat[0].size();
for (int i=0;i<n;++i) {
for (int j=0;j<m;++j) {
if (primer>mat[i][j]) mat[0][0]=mat[i][j];
else if (ultim<mat[i][j]) mat[n-1][m-1]=mat[i][j];
}
}
}
No obstant, podríem fer això:
void arreglar(Matriu& mat) {
int n=mat.size();
int m=mat[0].size();
int& primer=mat[0][0];
int& ultim=mat[n-1][m-1];
for (int i=0;i<n;++i) {
for (int j=0;j<m;++j) {
if (primer>mat[i][j]) primer=mat[i][j];
else if (ultim<mat[i][j]) ultim=mat[i][j];
}
}
}
Queda una mica més clar, no? I quan arribes més endavant, i tens un vector de llistes on, a dins de cada una, tens un map i a dins un altre vector, i necessites tenir accés a un element en concret d'aquests, doncs és útil. Sobretot perquè l'accés a un map no té temps constant
Funcions que modifiquen la variable
Aquesta és una altra utilitat de les referències. Quan passes una variable a una funció, es passa per còpia. No obstant, si passes una referència, podràs modificar la variable. Això té diverses utilitats:
1: Que la funció modifiqui una variable. Un exemple seria una funció per llegir una variable, l'hauríem de modificar. Un altre seria el següent:
void swap (int& a, int& b) {
int aux=a;
a=b;
b=aux;
}
Això intercanvia els valors de la variable a i la variable b
2: Retornar més d'una variable. Per exemple, imaginem que volem fer una funció que calculi l'element n de la successió de fibonacci. Però hem pensat que podem retornar també l'element n-1, així amb una sola crida podrem fer-ho. Podem retornar una parella, o podem passar-li una variable per referència, i que allà ens deixi l'element n-1. És una solució cutre, però què hi farem
3: Estalviar-nos cost. Si volem buscar el màxim d'un vector de 1000 elements, no el passarem per còpia, ja que és molt llarg, i no ens cal. Podem passar-lo per referència constant. De la mateixa manera, imaginem una funció per llegir un vector. Podem fer
vector<int> v=llegir_vector();
Però això el que farà és llegir el vector en un vector auxiliar, i copiar-lo a v. Però clar, si el vector té 1.000.000 d'elements, doncs estarem copiant 1.000.000 d'elements. El passem per referència, i ja ens estalviem tot això. Fem una funció
void llegir_vector(vector<int>& v) {
int n;
cin >>n;
v=vector<int>(n);
for (int i=0;i<n;++i) cin >>v[i];
}
La cridem fent
vector<int> v;
llegir_vector(v);
I ja tenim el vector, sense fer còpies inútils
void llegir_vector(vector<int>& v) {
int n;
cin >>n;
v=vector<int>(n);
for (int i=0;i<n;++i) cin >>v[i];
}
La cridem fent
vector<int> v;
llegir_vector(v);
I ja tenim el vector, sense fer còpies inútils
Cap comentari:
Publica un comentari a l'entrada