dimarts, 6 de juny del 2017

Aprendre a programar aux - punters i referències

Aquest és un d'aquells articles que, realment, a PRO1 gairebé no tenen utilitat. L'únic que s'utilitza són les referències, i molt poc. No obstant, mai està de més saber aquestes coses, així que les poso totes en un article a part, i així qui pugui se les llegeix. Jo recomanaria llegir, si més no, la part de referències

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;
};

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

Cap comentari:

Publica un comentari a l'entrada