Punters en C
Objectius
- Introduir el concepte de punter.
- Entendre la relació entre punter i vectors/matrius en C.
- Operadors bàsics d’accés.
Introducció
En aquesta unitat s’introdueix un nou tipus de dades anomenat punter. Els punters són variables que en comptes de contenir un valor (com els enters, caràcters o reals), contenen una adreça de memòria. A continuació veurem les seves propietats, operacions i relació amb altres conceptes que hem vist anteriorment.
1. Ús de punters
Un punter és una variable el valor de la qual és una adreça de memòria. Atès que un punter conté una adreça de memòria, l’espai que ocupa en memòria serà sempre el mateix, la mida d’una adreça. Aquesta mida dependrà de si el nostre sistema és de 32 bits o 64 bits, però serà independent del tipus que li assignem al punter.
1.1. Declaració
Igual que qualsevol altra variable, un punter es declararà per a poder-lo utilitzar:
[Exemple 13_01]
var
p: pointer to type;
end var
p:=NULL;
int main() {
/* Variable definition */
type *p=NULL;
}
On type és qualsevol tipus de dades (enter, real, etc.). La constant NULL té un valor 0, i s’interpreta com que aquest punter no ha estat inicialitzat, o sigui, que no apunta a cap lloc.
1.2. Operacions
Encara que les adreces són iguals sigui quin sigui el tipus, se li assigna un tipus per a indicar què conté l’adreça que es guarda en aquest punter. Cal tenir molta cura amb els tipus dels punters, encara que els punters de tots els tipus ocupen el mateix, el tipus assignat a un punter té un gran efecte en el resultat que s’obté en operar amb ell.
Atès que un punter conté una adreça de memòria, que és un valor numèric, podem realitzar les mateixes operacions que amb qualsevol valor numèric:
1.2.1. Assignació
Com hem dit anteriorment, un punter conté una adreça de memòria. Si volem que un punter apunti a una determinada variable, o sigui, assignar al punter l’adreça d’una determinada variable, ho farem de la manera següent (fixeu-vos que els tipus han de coincidir entre variable i punter):
[Exemple 13_02]
var
a: integer;
b: real;
pa: pointer to integer;
pb: pointer to real;
end var
a:=3;
b:=4.5;
pa:=address(a);
pb:=address(b);
int main() {
/* Variable definition */
int a=3;
float b=4.5;
int *pa=&a;
float *pb=&b;
}
En aquest exemple, a la variable pa se li assigna l’adreça de la variable a, i a la variable pb l’adreça de la variable b. A continuació, es mostra una representació simplificada de com quedaria en memòria:
Observem que quan assignem a un punter l’adreça d’una variable, estem assignant l’adreça del primer byte, per tant, aquesta operació no es veu afectada per la mida en memòria de la variable (donada pel seu tipus).
1.2.2. Contingut
L’adreça d’una variable és una informació que per si sola no ens aporta gran cosa, ja que cada vegada que executem el nostre programa aquest valor canviarà. El que és realment important és el fet que a partir d’aquesta adreça podem accedir al contingut que es guarda en aquesta posició de memòria. Seguint amb l’exemple anterior, a partir de l’adreça guardada a pb, podem recuperar el contingut b. Dit d’una altra manera, podem accedir al contingut de la variable apuntada per pb.
El codi següent mostra l’operador contingut, que ens permet recuperar el contingut a partir del punter.
[Exemple 13_03]
var
a: integer;
pa: pointer to integer;
end var
a:=5;
pa:=NULL;
pa:=address(a);
pa^:=25;
int main() {
/* Variable definition */
int a=5;
int *pa=NULL;
pa=&a;
*pa=25;
}
Observeu que quan recuperem el contingut, el tipus del punter és molt important, ja que el punter només conté l’adreça inicial i el tipus és el que ens permetrà saber quants bytes cal utilitzar i com interpretar-los. A continuació, es mostra una representació de la memòria en diferents situacions:
- L’estat inicial de les variables.
- El resultat d’assignar l’adreça d’a al punter pa.
- Canviar el contingut del punter pa, assignant-li un nou valor.
Cal tenir present que el contingut d’un punter a enter és un enter i, per tant, es pot assignar a una variable de tipus enter i se li pot aplicar qualsevol operació entera, com per exemple, sumar-li un altre enter o escriure’l per pantalla. El mateix ocorre amb qualsevol altre tipus, és a dir, el contingut d’un punter a real és un real i per tant el podem tractar com a tal.
1.2.3. Operacions lògiques
Cal tenir molt present que quan comparem dos punters, com en el següent codi:
[Exemple 13_04]
var
a: integer;
b: integer;
pa: pointer to integer;
pb: pointer to integer;
end var
a:=3;
b:=5;
pa:=address(a);
pb:=address(b);
if pa < pb then
writeString("pa is smaller than pb");
end if
int main() {
/* Variable definition */
int a=3;
int b=5;
int *pa=&a;
int *pb=&b;
if (pa < pb) {
printf("pa is smaller than pb");
}
return 0;
}
Estem comparant les adreces que guarden, no els valors de les variables a les quals apunten. O sigui, encara que a<b és cert, pa<pb no té un valor conegut, ja que dependrà de les adreces d’a i b. Si el que volem comparar és el contingut de l’adreça a la qual apunta el punter, caldrà utilitzar l’operador de contingut, com es fa en el següent codi:
[Exemple 13_05]
var
a: integer;
b: integer;
pa: pointer to integer;
pb: pointer to integer;
end var
a:=3;
b:=5;
pa:=address(a);
pb:=address(b);
if pa^ < pb^ then
writeString("the value pointed by pa is smaller than the value pointed by pb");
end if
int main() {
/* Variable definition */
int a=3;
int b=5;
int *pa=&a;
int *pb=&b;
if (*pa < *pb) {
printf("the value pointed by pa is smaller than the value pointed by pb");
}
return 0;
}
1.2.4. Operacions aritmètiques
Com hem vist anteriorment, un punter és un tipus de dades que permet guardar una adreça de memòria. Les adreces són valors numèrics i, com a tals, hi podem aplicar operadors aritmètics. Hi ha una diferència important en el comportament d’aquests operadors quan s’apliquen a punters, ja que es treballa amb unitats de dades, no amb valors, és a dir, si per exemple sumem 3 a un punter a enter que conté l’adreça 30, el resultat no serà l’adreça 33, sinó que serà 33 + 3 * mida(enter) = 33 + 3 * 4 = 45. De la mateixa manera, si tenim un punter a caràcter que conté l’adreça 33 i li sumem 3, com que la mida d’un caràcter és 1 byte, el resultat obtingut serà 33 + 3 * mida(caràcter) = 33 + 3 * 1 = 36.
Per tant, el tipus del punter determina el resultat final de l’operació.
1.3. Exemple 13_06
Per a veure com funcionen els punters, fixem-nos en el següent exemple:
var
a: integer;
b: integer;
p: pointer to integer;
end var
a:=3;
b:=5;
p:=NULL;
p:=address(a);
p^:=a+b;
int main() {
/* Variable definition */
int a=3;
int b=5;
int *p=NULL;
p=&a;
*p=a+b;
return 0;
}
El que es fa en aquest codi és:
- Defineix tres variables: dos enters a i b, i un punter a enter p. Assignem els valors inicials 3, 5 i null respectivament a les tres variables.
- Assignem a p l’adreça d’a, que és l’adreça on comença a en la memòria. Direm que p apunta a a.
- Al contingut de p, li assignem la suma entre a i b. Atès que p apunta a a, el contingut de p equival a a, i per tant això és equivalent a assignar el resultat de la suma directament a a.
A continuació, es veu gràficament l’evolució de la memòria per a cadascun dels passos d’aquest codi:
Cal que ens fixem especialment en la diferència entre assignar un valor al punter o assignar-lo al contingut del punter. En el primer cas, s’assigna una adreça, en el segon, s’assigna un valor a la variable apuntada pel punter. Dit d’una altra manera, quan accedim al punter p, accedim (per a llegir o escriure) en l’adreça de la pròpia variable p (en l’exemple l’adreça 40), mentre que quan accedim al contingut de p accedim (per a llegir o escriure) a l’adreça que conté p (en l’exemple l’adreça 32, que equival a la variable a).
Punters a tuples
Igual que tenim punters a enters, els podem tenir a qualsevol altre tipus, inclosos els tipus estructurats o tuples. Sigui quina sigui la tupla, els camps que contingui i la seva mida en memòria, un punter a la tupla continuarà ocupant el mateix que un punter a un caràcter o un enter, el que ocupi una adreça de memòria, i el seu contingut serà l’adreça de la primera posició de memòria on es guarda la tupla.
[Exemple 13_07]
type
tpoint = record
x: real
y: real
end record
end type
var
a: tPoint;
pPoint: pointer to tPoint;
pCoord: pointer to real;
end var
a.x:=3;
a.y:=5;
pPoint:=NULL;
pCoord:=NULL;
pPoint:=address(a);
pPoint^.x=-2;
pCoord:=address(a.y);
pCoord^:=6;
int main() {
/* Type definition */
typedef struct {
float x;
float y;
} tPoint;
/* Variable definition */
tPoint a;
tPoint *pPoint=NULL;
float *pCoord=NULL;
a.x=3;
a.y=5;
pPoint=&a;
pPoint->x=-2;
pCoord=&(a.y);
*pCoord=6;
return 0;
}
El que es fa en aquest codi és:
- Definir tres variables: una variable a de tipus tPoint, una variable pPoint que és un punter a tPoint i una variable pCoord que és un punter a real. Assignem 3 i 5 als camps x i y de tPoint respectivament. Inicialitzem els dos punters amb el valor null.
- Assignem a pPoint l’adreça de la variable a. Per tant, pPoint apunta a a.
- Assignem al camp x del contingut de pPoint el valor -2. En llenguatge C tenim l’operador -> que facilita aquesta acció. pPoint->x equival a (*pPoint).x.
- Assignem a pCoord l’adreça del camp y de la variable a. Tingueu en compte que el tipus del camp i del punter coincideixen.
- Assignem al contingut del punter pCoord (o sigui, en el camp y d’a) el valor 6.
A continuació, es veu gràficament l’evolució de la memòria per a cadascun dels passos d’aquest codi:
2. Punters i vectors/matrius en C
El cas dels vectors (incloses les cadenes de caràcters) i les matrius és diferent de la resta dels tipus. En realitat, quan declarem una variable com a vector o matriu, realment la variable que es declara és de tipus punter al tipus d’aquest vector/matriu, i el seu valor és el de la primera posició d’aquest vector/matriu. Encara que en llenguatge algorísmic podríem expressar l’assignació sense problemes, quan ho implementem en llenguatge C aquest fet provoca dos efectes que cal tenir presents:
Efecte 1: no podem assignar els valors d’un vector/matriu a un altre vector/matriu.
El problema és que en assignar un vector/matriu a un altre, realment el que fem és canviar l’adreça on apunta el punter. Vegem un codi d’exemple:
[Exemple 13_08]
int main() {
/* Variable definition */
int a[3]={1, 2, 3};
int b[3]={0, 0, 0};
b=a;
return 0;
}
En aquest codi, el tipus de les variables a i b realment és int*, o sigui, punters a enters. Per tant, quan assignem b a a, estem assignant-li l’adreça de la primera posició d’a. Si ara mostréssim el contingut dels dos vectors, podríem tenir la sensació que l’assignació s’ha fet correctament, ja que en efecte veiem els mateixos valors, però si modifiquem qualsevol dels valors de b també es modificaran els valors d’a, ja que a efectes pràctics hem fet que els dos punters apuntin al mateix vector.
Tampoc funcionaria intentar assignar el contingut del vector, ja que si fem:
[Exemple 13_09]
int main() {
/* Variable definition */
int a[3]={1, 2, 3};
int b[3]={0, 0, 0};
*b=*a;
return 0;
}
Com que a i b apunten a la primera posició dels vectors, el que faríem amb aquest codi seria copiar el contingut de la primera posició del vector a, és a dir l’enter 1 a la primera posició del vector b, el qual passaria a tenir els valors {1, 0, 0}.
Efecte2: quan passem un vector/matriu com a paràmetre a una acció o funció, aquest paràmetre sempre serà d’entrada/sortida.
En passar un vector/matriu realment estem passant un punter i, per tant, com hem vist a l’apartat anterior, els canvis que es facin en aquest vector perduraran en finalitzar l’acció o funció. El llenguatge C ens permet protegir-nos d’aquest efecte. Si volem evitar que una acció o funció modifiqui els continguts d’un vector/matriu, o de qualsevol punter que li passem com a paràmetre, podem declarar-los com a constants. En el següent exemple es mostra com fer-ho:
[Exemple 13_10]
void f(const int *p) {
*p=*p+2; /* ERROR */
}
int main() {
/* Variable definition */
int a=3;
f(&a);
printf("%d\n", a);
return 0;
}
En haver afegit la paraula reservada const, el compilador no deixarà que modifiquem el contingut del punter.
Resum
En aquesta guía hem vist un nou tipus de dades, el punter. També hem vist com passar d’una variable a la seva adreça i com obtenir el valor de l’adreça continguda en un punter. Finalment hem relacionat els punters amb els paràmetres d’entrada/sortida i els vectors i matrius. També hem vist que, tret que indiquem el contrari explícitament, qualsevol paràmetre d’una acció o funció que sigui un vector/matriu és un paràmetre d’entrada/sortida.