Enrere Mòdul 5
Fonaments de Programació. Llenguatge C/C++---
Pràctica  Exercicis
Pràctica d'ampliació

 
Resum Teòric

Introducció als punters

Quan es declara una variable, es reserva espai a la memòria per contenir el valor d’aquesta variable. El nom de la variable queda associat a l'adreça de memòria on comença aquest espai reservat. La quantitat d’espai reservat depèn del tipus de variable, per exemple, en la següent declaració:

char carac,carac2;
int comptador;
float x;
int i;

es tindrà el següent esquema a la memòria de l’ordinador (les posicions concretes de memòria són orientatives):

Un punter és una variable que conté l'adreça d’una altra variable. Es diu que la variable punter o el punter apunta a aquesta segona variable..

Els punters proporcionen una gran potència als llenguatges C i C++ i marquen la diferència entre aquests i altres llenguatges de programació. Els punters ens permeten aproximar-nos al tipus de treball que fa l’ordinador. Els programes que utilitzen punters són normalment més eficients, encara que els punters són un element perillós en el sentit que un punter sense valor inicial o incontrolat pot provocar un mal funcionament del sistema i provocar errors de difícil localització.

La importància dels punters està principalment en aquests tres punts:

  1. Proporcionen els mitjans pels quals les funcions poden modificar els seus arguments de crida.

  2. Permeten l’assignació dinàmica de memòria. Això vol dir que amb l’ajuda dels punters es pot reservar la memòria en temps d’execució en lloc de en temps de compilació, el que significa que l’espai reservat per les dades pot ser determinat per l’usuari en lloc de pel programador.

  3. Poden substituir als vectors o variables indexades per tal d’incrementar l'eficàcia del programa.

El fet de treballar en sistemes de 32 bits fa que els punters puguin directament apuntar a "qualsevol" lloc de la memòria. No és necessari l’ús de segments i desplaçaments com necessitaven els sistemes de 16 bits, no obstant això, els sistemes operatius Windows (NT, 95,98 i 2000) no permeten que els punters apuntin fora de la memòria reservada per a l'execució del programa.

Declaració d'un punter

Les variables punters s'han de declarar com qualsevol altra variable en C/C++. El format general de la declaració d'un punter és:

tipus_basic *nom_de_la_variable

on tipus_basic és qualsevol dels tipus bàsic de dades i defineix el tipus de dades que trobarem en el lloc on apunta el punter. L'asterisc es pot llegir de moment com "punter a tipus_basic".

El nom de la variable punter és un identificador de variable normal i, com a tal, és correcte qualsevol identificador.

Per exemple:

char carac, *ptrcarac;

Aquesta sentència declara dues variables, una variable tipus char anomenada carac, i una altra de tipus char * ( punter a char ) anomenada ptrcarac.

Si fem les següents assignacions:

carac = 'A';
ptrcarac=&carac;

tindrem el següent esquema a la memòria:

Els números de la columna de l'esquerra representen, en format hexagesimal, l'adreça de cada posició de memòria (aquestes adreces són orientatives). Dintre dels quadres es representa el contingut de la memòria: La variable carac està assignada a la posició 006515A1 i el seu contingut és 'A', o bé el número 65. Aquesta variable ocupa un únic octec. La variable ptrcarac està assignada a la posició 006515A2 i el seu contingut és la posició de la variable carac, és a dir, la posició 006515A1. En Visual C, les variables punters ocupen 4 octets, independentment del tipus de variable a la qual apunten.

 

Els operadors de manipulació de punters: & i *

L'operador & (operador d'adreça) és un operador unari que retorna l'adreça de memòria del seu operant. El seu operant pot ser qualsevol tipus de variable, incloent-hi les variables punters. A l'exemple anterior, l'assignació:

ptrcarac=&carac;

fa que a la variable ptrcarac s'emmagatzemi l'adreça de la variable carac. En aquest moment, la variable ptrcarac apuntarà a la variable carac.

L'operador * (operador d'indirecció) és un altre operador unari que retorna el valor de la variable on està apuntant l'operant (que serà un punter). Per exemple, la sentència:

val=*ptrcarac;

assignarà a la variable val (declarada prèviament com a char) el valor 65.

L'operador & actua sobre qualsevol variable. L'operador * actua sobre variables punters.

No s'ha de confondre l'operador unari * amb l'operador binari * que representa el producte de dos nombres. En expressions complicades en les quals pugui haver confusions, es pot posar parèntesis per evitar aquestes confusions.

 

Assignacions a punters

Com qualsevol variable, es pot fer servir un punter a la part dreta d'una sentència per assignar el valor del punter a un altre punter, per exemple:

int a,*px,*py;

a=100;
px=&a;
py=px

printf("adreça de py:%p\napunta al valor: %d", py,*py);

El punter py apuntarà a la variable a, per tant, *py serà igual a 100. El codi de format per mostrar adreces de memòria en hexagesimal amb la funció printf() és %p.

 

Aritmètica de punters

En C es pot fer servir els operadors ++,--, + i - sobre punters. Una expressió com:

p++;

sobre un punter p fa que apunti a la següent posició de memòria, entenent com a següent posició la que s'obté de sumar el nombre d'octets que ocupa el tipus base del punter. Per exemple, si la variable punter p, declarada com un punter a enter (int * p), conté l'adreça 0065A510, la sentència p++  fa que aquest punter contingui ara l'adreça 0065A514, ja que, una variable enter ocupa 4 octets.

Els llenguatges C/C++ no es limiten només als increments i decrements, també es pot sumar i restar enters als punters. Per exemple, si a la variable p definida al paràgraf anterior, sumem 5 amb la sentència:

p=p+5;

el valor actual serà 0065A514+5*(mida d'un enter)=0065A528 (Si no heu entès aquesta suma, recordeu que les adreces s'expressen normalment en hexagesimal, de fet, s'ha sumat 20 posicions de memòria).

 

Inicialització de punters

Si una variable local no s'inicialitza, el seu valor és indeterminat. Si aquest fet pot ser perillós en el cas d’una variable normal, és especialment perillós en el cas dels punters. Sempre cal inicialitzar els punters abans de fer-los servir. Per treballar amb punters buits (que temporalment no apuntin enlloc), es poden inicialitzar a un valor especial anomenat NULL (punter nul). NULL és una constant simbòlica que està definida en alguns arxius de capçalera estàndards com stdio.h o ioscrean.h i de fet és un valor 0, el valor fals en les condicions.

Si un punter sempre apunta a una mateixa variable és una bona pràctica inicialitzar-la en la declaració, per exemple:

int main() {
    int a = 6;
    int *pa = &a;

 

Pas d’arguments per valor i per referència

En C/C++, quan cridem a una funció amb un argument (una variable), es passa una còpia del contingut d’aquesta variable. Es diu que l’argument s’ha passat per valor. La funció no pot modificar el contingut de la variable original. La principal restricció del mètode de crida per valor és que la funció només pot tornar un únic valor.

Una altra possibilitat és passar arguments per referència, és a dir, passar l’adreça de la variable en lloc del seu valor. Això fa que la funció no té la necessitat de crear una còpia d’aquesta variable i, a més, les modificacions que faci la funció afectaran al valor de la variable una vegada acabada la funció. D’aquesta forma una funció pot modificar més d’un valor. En C/C++ es pot crear una crida per referència utilitzant un punter com argument. A la primera pràctica podreu entendre la diferència entre aquests dos tipus de pas d'arguments.

 

Punters del tipus void *

C/C++ incorporen la possibilitat de declarar un punter com void *, això permet que el punter apunti a qualsevol tipus de dades. Això pot ser útil en molts casos. Podem pensar, per exemple, en la funció estàndard d'entrada C: scanf(), que admet com arguments punters a qualsevol tipus de dades. Quan es vol manipular una variable punter void *, primer s'ha de fer una conversió explícita a qualsevol tipus de dada vàlida C/C++. A la pràctica 3 es tracta un exemple d'aquesta característica.

 

Vectors o variables indexades

Els vectors (també coneguts com variables indexades, arrays, arreglos, formacions, matrius, etc.) són un conjunt de variables del mateix tipus i amb el mateix nom. Per referir-nos a un element concret d’un vector es fa servir un o més índexs tancats entre claudàtors. El nombre de claudàtors representarà la dimensió del vector. En el cas que la dimensió sigui superior a 1 se sol anomenar matriu.

Per declarar un vector o matriu d'una o diverses dimensions, s'ha d'escriure el tipus i el nom seguit d’uns claudàtors amb un nombre d'elements per a cada dimensió. Per exemple:

int x[10];     //defineix un vector de 10 variables int
float a[2][3]; //defineix una matriu de 6 variables float.

Per referir-nos a una de les 10 variables int del vector x es fa servir un índex que pot ser qualsevol expressió que torni un enter entre 0 i n-1, essent n el nombre que s’ha utilitzat en la definició.

A les matrius de dues dimensions, els elements es van emmagatzemant per variació dels índexs de més a la dreta cap als índexs de més a l’esquerra, per exemple:

int m[3][4];

s’emmagatzemarà a la memòria en el següent ordre:

m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2], m[1][3], m[2][0], m[2][1], m[2][2], m[2][3]

Una cosa molt important a tenir en compte és que C/C++ no fa comprovació de límits, això vol dir que si es declara un vector de dimensió n, pot passar que s’utilitzi un índex amb un valor més gran que n. Aquesta circumstància no és controlada pel compilador i pot provocar la caiguda del sistema.

Inicialització de vectors

Com tota variable, un vector pot prendre valors inicials després de la seva declaració. El format general és:

tipus identificador_variable [grandaria] = { llista_de_valors };

La llista de valors és una llista separada per comes " , ", de constants que són del mateix tipus que el tipus base del vector.

Exemple:

int num[5] = {0,1,2,3,4};

En aquest exemple la primera constant de valor 0 l'emmagatzemarà a la primera posició del vector, la segona constant de valor 1 a la segona posició del vector, i així successivament fins completar les cinc constants. El compilador les situarà en posicions contigües de memòria.

No és necessari posar valor inicial a tot el vector, en aquest cas el compilador posarà zeros a tots aquells elements que no li hem assignat cap valor. Per exemple:

int num[5] = {0,1,2};

En aquest exemple el compilador assignarà zeros als dos últims elements del vector.

També és possible al posar els valors inicials al vector, no declarar la seva grandària. El compilador la determina calculant el nombre de valors enumerats. Així, l’assignació:

int num[] = {0,1,2,3,4};

fa que la dimensió de la variable num sigui 5.

Per posar valors inicials als vectors multidimensionals ho farem d'una forma semblant a la feta pels vectors unidimensionals. Són exemples de posar valors inicials les següents declaracions d'assignació:

int tab[2][5] = {{0,1,2,3,4},{5,6,7,8,9}};

equivalent a:

int tab[2][5] = {0,1,2,3,4,5,6,7,8,9};

i també equivalent a:

int tab[2][5] ={
                                {0,1,2,3,4},
                                {5,6,7,8,9}
                               };

En aquest exemple declarem una matriu d'enters de dues files per cinc columnes.

Així, com en els vectors unidimensionals, el posar valor inicial ho podíem fer en forma parcial, ara també és possible, a condició de començar pel principi de cada índex del vector. No obstant és necessari anar en compte perquè en les declaracions incompletes les confusions són fàcils com mostra el següent exemple:

int taula1[2][4] = {1,2,3,4,5,6,7,8};

/* valors inicials complets, correspon a:

1 2 3 4
5 6 7 8

*/

int taula2[2][4] = {1,2,3};

/* valors inicials incomplets, correspon a:

1 2 3 0
0 0 0 0

*/

int taula3[2][4] = {{1},{2,3}};

/* valors inicials incomplets, correspon a:

1 0 0 0
2 3 0 0

*/

/* valors inicials a un vector sense dimensions. La dimensió indeterminada serà sempre la primera */

int taula4[][2] = {{1,2},{3,4},{5,6}};

/* valors inicials que correspon a:

1 2
3 4
5 6

*/

 

Relació entre vectors i punters

Existeix una estreta relació entre vectors i punters. De fet, el nom d'un vector és un punter a l'adreça de memòria que conté el primer element del vector, és a dir, si definim el vector:
int vect[5]

L'identificador vect és equivalent a &vect[0].

En general, vect+i serà un punter que apuntarà a vect[i]. De fet, als punters se'ls poden posar índexs i, si s'ha definit un punter com:
int * p;

és equivalent p+i que p[i].

En el cas dels vectors multidimensionals o matrius, la relació entre aquests i els punters és una mica més sofisticat. En el cas d'una matriu bidimensional, el nom de la matriu és un punter al primer element d'un vector de punters, per exemple, si definim:
int mat[2][3];

mat és un punter al primer element del vector de punters mat[]. Per tant, mat és equivalent a &mat[0] i mat[0] és equivalent a &mat[0][0], de la mateixa forma que mat[1] és equivalent a &mat[1][0].

Pas de vectors com arguments d'una funció

Per considerar el pas de vectors com arguments d'una funció, és necessari entendre la relació entre aquests i els punters.

Si passem a una funció un element d'un vector, estem passant el valor d'aquest element. En aquest cas, l'argument de la funció s'ha de declarar del tipus de dada que es passa. Per exemple:

 
int funcio(int);

int main(){
    int vect[10] , a;
    ......
    a = funcio(vect[1]);
    ......
}

int funcio(int a){
     .......
   

Podem passar directament tot el vector. En aquest cas la crida es farà amb el nom del vector que, com ja se sap, és un punter. Es pot fer de dues formes:

 
void funcio(int v[]);

int main(){
    int vect[10];
    ......
    funcio(vect);
    ......
}

int funcio(int v[]){
     .......

void funcio(int*);

int main(){
    int vect[10];
    ......
    funcio(vect);
    ......
}

int funcio(int *v){
     .......

 

En el cas de matrius multidimensionals, és necessari donar les dimensions de la matriu que se passa com argument excepte la primera. Per exemple:
int funcio(int v[][10]);

void main(){
    int vect[10][10];
    ......
    funcio(vect);
    ......
}

int funcio(int v[][10]){
     .......

int funcio(int (*v)[10]);

void main(){
    int vect[10][10];
    ......
    funcio(vect);
    ......
}

int funcio(int (*v)[10]){
    .......

 

A la segona versió, la de la dreta, el parèntesi de int (*v)[10] és necessari degut a la major prioritat de l'operador [] sobre l'operador *

Punters a funcions

Les funcions, a l'igual que les variables, tenen la seva pròpia adreça de memòria. Una característica molt interessant, al mateix temps que confusa, és la de punter a funció. Aquest punter correspondrà a l'adreça inicial del codi de la funció.

Els punters a funcions permeten referenciar de forma indirecta una funció, i també permeten que una funció pugui ser passada com argument a una altra funció.

Com passa amb els vectors, el nom d'una funció sense els seus parèntesi s'interpreta com un punter a la funció.

Per declarar un punter a una funció es fa normalment de dues formes: La primera forma és declarar directament una variable punter a funció de la següent forma:

  • tipus (*nom_punter_funció)(tipus, tipus,...);

És necessari posar parèntesi al voltant del nom de la funció (*nom_punter_funció) degut a que l’operador * té menor prioritat que el parèntesi que l’encercla.

La segona forma és declarar un punter tipus void, que posteriorment serà assignat a una funció. Aquesta assignació es pot fer posant el nom d'una funció, sense parèntesi, al costat dret d'una sentència d'assignació:

  • void punter;

  • punter=funció;