APUNTADORES


Una variable referenciada o variable dinámica es una variable que, a diferencia de una variable común, no se referencia mediante un nombre sino de manera indirecta a través de un puntero.

Un puntero es una variable que contiene la dirección de memoria de una variable dinámica donde se podrá almacenar un valor.

Cuando se declara un puntero, de igual manera que con cualquier variable, su contenido es indefinido hasta que se le asigne un valor. Mientras esto no ocurra no se puede decir que exista una variable refenciada, en esta situación se dice que el puntero no está apuntado a una dirección válida. Un apuntador puede inicializarse en NULL que corresponde a una dirección 0 o nula. NULL es una constante simbólica definida en el archivo de cabeceras stddef.h el cual a su vez es incluido en el archivo de cabeceras stdio.h. Al inicializar un puntero en NULL se garantiza que el puntero no apunte a una dirección inválida pero con esto tampoco se define una variable referenciada.

Declaración de un Puntero

tipo [near] ¦ [far] *variable;

Ejemplo:

int *a;
float far *k;
int near *b;

En entornos de desarrollo como el Borland C++, un puntero cercano o near pointer es una variable que se define en 2 bytes, esto se debe a que este tipo de puntero sólo puede recibir una dirección de memoria que se encuentre en el segmento de datos, por lo tanto sólo requiere almacenar el desplazamiento (offset) de la dirección, el segmento de la direcció se maneje a través del registro DS. Un puntero lejano o far pointer, emplea 4 bytes para definir una variable, la razón de esto es que este tipo de puntero puede almacenar culaquier dirección de memoria, por lo tanto se debe guardar en ella tanto el segmento como el desplazamiento de la memoria.

En entornos d edesarrollo que empleen Win32, como DEV-C++, Visual C++, etc, no se distingue entre direcciones de memoria cercanas y lejenas. Así las claúsulas near y far son ignoradas, y todos los punteros son tratados de la misma manera.

Operadores de punteros

Básicamernte existen dos operadores para manipular los punteros, estos son el * y el &. El operador * es aplicado a un puntero y nos da el valor de la variable referencia. El operador & se aplica a variables comunes, nos da la dirección de memoria de variable a la que se aplica.

Formas de utilizar el operador * y el operador &.

int a = 1,  b = 2,  c[10] = {10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
int *pt;  ¬ Aquí el puntero se define pero no la variable referenciada, se debe inicializar el puntero.

pt = &a;  ¬ Se inicializa el puntero "pt" con una direcció válida, la direccíón de la variable "a". Se dice a partir de aquí que "pt" apunta a "a" o que la variable referenciada por "pt" coincide con "a". El valor de la variable refernciada por "pt" (*pt) es 1 (el mismo valor que tiene la variable "a").

*pt = 73;  ¬ Se modifica el valor de la variable referenciada, ahora *pt tiene el valor de 73. La variable "a" también cambia a 73.

b = *pt;   ¬ La variable "b" ahora tiene el valor de 73.

pt = NULL;   ¬ El puntero "pt" apunta ahora a NULL, ya no existe a partir de aquí la variable referenciada.
*pt = 25;   ¬ ERROR: No existe la variable referenciada en este punto.

pt = c;   ¬ "c" es un arreglo, por lo tanto el identificador "c" está relacionado con la dirección del primer elemento del arreglo. Esta instrucción hace que "pt" apunte a esa misma dirección de memoria.

*pt = 88;   ¬ Modifica el elemento "c[0]" en 88.
pt[0] = 88;   ¬ Equivale a la enterior sentencia, modifica el elemento "c[0]" en 88.

pt[2] = 51;   ¬ Modifica el elemento "c[2]" en 51. Un puntero puede manejarse como un arreglo.

pt = &c[3];   ¬ El puntero "pt" apunta al cuarto elemento de c.

*pt = 102;   ¬ Modifica el elemento "c[3]" en 102.
pt[0] = 102;   ¬ Equivale a la enterior sentencia, modifica el elemento "c[3]" en 102.

pt[2] = 59;   ¬ Modifica el elemento "c[5]" en 59.

Aritmética de punteros

Un puntero, como se dijo anteriormente, contiene una dirección de memoria. Esta dirección de memoria no es otra cosa que un valor entero, por lo tanto en teoría pordríamos sumar o restar valores enteros a un puntero de modo que el puntero pueda apuntar a la memoria que se encuentre antes o después de la dirección asignada. Pues bien, esto si se puede hacer en un programa. Las siguentes instrucciones muestran operaciones válidas para un puntero:

int c[10] = {10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
int *pt;

pt = c;

pt++;
pt += 5;
pt -= 3;
pt--;

No obstante se puedan realizar estas ordenes, la operación que se realiza no es totalmente transparente. Esto quiere decir que "pt++;" no suma 1 al pumtero pt, ni que "pt += 5;" sume 5 a pt, ni que "pt -= 3;" o "pt--;" reste 3 ó 1 al puntero pt respectivamente.

Cuando se ejecuten esas órdenes el compilador multiplicará el valor que se desea añadir o restar al puntero por el tamaño de la variable referenciada. Esto quiere decir que, para el ejemplo anterior:

pt++;  ® El compilador lo traduce como: pt += 1*sizeof(int);
pt += 5;   ® El compilador lo traduce como: pt += 5*sizeof(int);
pt -= 3;   ® El compilador lo traduce como: pt -= 3*sizeof(int);
pt--;  ® El compilador lo traduce como: pt -= 1*sizeof(int);

En otras palabras, sí se define "int *pt;" para versiones de C/C++ en la que un int se represente en 2 bytes (sizeof(int) = 2):

De igual manera sí se define float *pt; (sizeof(float) = 4):

La razón de este comportamiento es la de poder hacer eficiente las operaciones que se hagan con punteros. Si cuando a un puntero definido como "long *pt;" (sizeof(long)=4), se le quiere aplicar una opreación como "pt + 1", en vez de sumarle el valor 1*sizeof(long) a la dirección que contiene el puntero se le sumase 1 el resultado no tendría coherencia.

El motivo de esto es que el puntero contiene la dirección del primer byte de un conjunto de 4 bytes que conforman un valor de tipo long, cuando se opera la variable referenciada "*pt" el sistema toma, a partir de esa dirección cuatro bytes consecutivos, y es la representación binaria de esos bytes los que conforman un valor de tipo long. Si el puntero se desplazara sólo un byte, la variable "pt" tendía la dirección del segundo byte de la variable referenciada anteriormente, por lo tanto la nueva variable referenciada estaría conformada por el segundo, tercer y cuarto byte de la variable anterior más el byte que le sigue, lo que se pueda formar con esos bytes no será un valor coherente. Es por esto que la operación suma 1*sizeof(long) para que el puntero reciba la direccón de inicio de un nuevo valor que sí sea coherente.

El siguiente ejemplo ilustra lo expresado anteriormente.

Para las siguientes instrucciones:

long a[5] = {2000, 3000, 400, 5000, 600};
long *pt;
pt = a;

Se tendría la siguiente información guardada en la memoria:

Si cuando se realiza la operación "pt+1", se le sumase sólo 1 al puntero se tendría:

Pero si se le sumase 1*sizeof(long) al puntero se tendría:

Esta es la rázón por la que un puntero se puede manejar como si fuera un arreglo. Si recordamos el capítulo anterior, allí se menciona que si luego de definir un arreglo como "int a[10];" se desea manipular un elemento del arreglo como "a[3] = 7;", el sistema emplea la fórmula "DMA3 = DMA + 3 x sizeof(int)" para llegar a ese elemento. Si se tiene un puntero que apunta al arreglo (pt = a;), la operación "pt + 3"; nos da la dirección del cuarto elemento del arreglo, por tanto "*(pt+3)" nos permite llegar una variable refenciada que corresponde al cuarto elemento del arreglo, por consiguiente "*(pt+3)" es quivalente a "p[3]" lo que a su vez es equivalente a "a[3]".

Finalmente si se desea realizar operaciones con las variables referenciadas se debe tener cuidado con los operadores unarios como ++ ó -- debido a que estos operadores se agrupan de derecha a izquierda, a diferencia de la mayoría lo hacen de izquierda a derecha. El siguiente ejemplo ilustra este problema.

int a[5] = {27, 35, 42, 51, 67};
long *pt;
pt = a;

a[2] = *pt / 3;    ¬ Aquí la operación hace que al valor contenido en la variable referenciada por pt (27) se le divida entre 3 y el resultado (9) se le asigne a la variable a[2].

a[2] = *pt++ / 3;    ¬ Aquí se podría pensar que el operador ++ se aplica a la variable referenciada y que luego de evaluar la expresión el contenido de *pt (ó a[0]) se incrementa en 1 (28). Sin embargo esto no sucede, el operador ++ se aplica al puntero pt y no a la variable referenciada *pt, por lo tanto a[0] se mantien en 27, pero pt ahora apunta a a[1].

a[2] = (*pt)++ / 3;  ¬ Aquí si el operador ++ se aplica a la variable referenciada, por lo tanto luego de evaluar la expresión el contenido de *pt se incrementa en 1 (35).

Ejemplo:

El siguiente ejemplo muestra otra manera de asignar un valor válido a un puntero, esto se hace mediante un valor constante, observe que al valor constante se le aplica un "cast" para que ese valor sea tomado como una dirección de memoria.

Grabar este ejemplo

/* Este programa sólo puede ser ejecutado en el entorno de desarrollo
   Borland C++.

   El programa coloca ciertos caracteres directamente en la memoria de
   video (dirección B800:0000).

   En las posiciones de memoria pares se colocan los caracteres y en las
   impares los atributos del caracter (color del caracter y del fondo).
*/

#include <stdio.h>
#include <conio.h>

void main ()
{ unsigned char far *p;
  p=(unsigned char far *) 0xB8000000l;  // memoria de video
  *p='H';      // caracter
  p++;
  *p=200;      // color
  p++;
  *p='O';
  p++;
  *p=250;
  p++;
  *p='L';
  p++;
  *p=280;
  p++;
  *p='A';
  p++;
  *p=295;

  getch ( );
}

Paso de referencias a funciones mediante punteros

Como se indicó en el capítulo de funciones, en el lenguaje C, a diferencia del lenguaje C++, los parámetors que se emplean en las funciones sólo pueden ser por valor, no se implementan parámetros por referencia. Sin embargo a una función se le puede pasar como parámetro la dirección de memoria de una variable, esta direcció puede ser tomada por un puntero quien a su vez podrá modificar la variable referenciada, esta última operación modificaría el contenido de la variable cuya dirección se pasó como parámetro.

En otras palabras, a pesar que en el lenguaje C no existen los parámetros por referencia, estos se pueden simular mediante los operadores & y *

Por ejemplo, si se quisiera intercambiar dos valores mediante una función en lenguaje C:

int a = 3, b = 7;
int c[5] = {10, 15, 20, 25, 30};
intercambiar (a, b);  ® Esto no modificará el contenido de "a" ni de "b" porque se está enviando el valor de las variables.
intercambiar (c[1], c[3]);  ® Esto tampoco los modifica.
La razón es porque se tendría que definir la funció de la siguiente manera:
void intercambiar (int x, int y)
{ int aux;
  aux = x;
  x = y;
  y = aux;
}

La manera correcta de hacerlo sería de la forma siguiente:

int a = 3, b = 7;
int c[5] = {10, 15, 20, 25, 30};
intercambiar (&a, &b);  ® Aquí se envía la dirección de las variables "a" y "b".
intercambiar (&c[1], &c[3]);  ® Lo mismo aquí, se envía la dirección de las variables "c[1]" y "c[3]".

Ambas direcciones son recibidas por sendos punteros, como a continuación se detalla:

void intercambiar (int *x, int *y)
{ int aux;
  aux = *x;
  *x = *y;
  *y = aux;
}

Las variables referenciadas por los punteros x y y coinciden con los parámetros, en este caso las variables a y b o las variables c[1] y c[3]. Por esta razón cualquier cambio que se realice sobre las variables referenciadas *x o *y se reflejará en los argumentos, por lo que se estará simulando un parámetro por referencia.



Volver a contenidos AtrásSiguiente