Manipulación de punteros

Obtención de dirección de memoria. Operador &

    Para almacenar una referencia a un objeto en un puntero se puede aplicar al objeto el operador prefijo &, que lo que hace es devolver la dirección que en memoria ocupa el objeto sobre el que se aplica. Un ejemplo de su uso para inicializar un puntero es:


int x =10;
int * px = &x;

    Este operador no es aplicable a expresiones constantes, pues éstas no se almacenan en ninguna dirección de memoria específica sino que se incrustan en las instrucciones. Por ello, no es válido hacer directamente:


int px = &10;  // Error 10 no es una variable con dirección propia

    Tampoco es válido aplicar & a campos readonly, pues si estos pudiesen ser apuntados por punteros se correría el riesgo de poderlos modificar ya que a través de un puntero se accede a memoria directamente, sin tenerse en cuenta si en la posición accedida hay algún objeto, por lo que mucho menos se considerará si éste es de sólo lectura.

    Lo que es sí válido es almacenar en un puntero es la dirección de memoria apuntada por otro puntero. En ese caso ambos punteros apuntarían al mismo objeto y las modificaciones a éste realizadas a través de un puntero también afectarían al objeto visto por el otro, de forma similar a como ocurre con las variables normales de tipos referencia. Es más, los operadores relacionales típicos (==, !=, <, >, <= y >=) se han redefinido para que cuando se apliquen entre dos punteros de cualesquiera dos tipos lo que se compare sean las direcciones de memoria que estos almacenan. Por ejemplo:

 
 int x = 10;
 int px = &x;
 int px2 = px; // px y px2 apuntan al objeto almacenado en x
 Console.WriteLine( px == px2);  // Imprime por pantalla True

    En realidad las variables sobre las que se aplique & no tienen porqué estar inicializadas. Por ejemplo, es válido hacer:

 
 private void f()
 {
  int x;
  unsafe
   { int px = &x;}
 }

    Esto se debe a que uno de los principales usos de los punteros en C# es poderlos pasar como parámetros de funciones no gestionadas que esperen recibir punteros. Como muchas de esas funciones han sido programadas para inicializar los contenidos de los punteros que se les pasan, pasarles punteros inicializados implicaría perder tiempo innecesariamente en inicializarlos.

Acceso a contenido de puntero. Operador *

    Un puntero no almacena directamente un objeto sino que suele almacenar la dirección de memoria de un objeto (o sea, apunta a un objeto) Para obtener a partir de un puntero el objeto al que apunta hay que aplicarle al mismo el operador prefijo *, que devuelve el objeto apuntado. Por ejemplo, el siguiente código imprime en pantalla un 10:


 int x = 10;
 int * px= &x;
 Console.WriteLine(*px);

    Es posible en un puntero almacenar null para indicar que no apunta a ninguna dirección válida. Sin embargo, si luego se intenta acceder al contenido del mismo a través del operador * se producirá generalmente una excepción de tipo NullReferenceException (aunque realmente esto depende de la implementación del lenguaje) Por ejemplo:


int * px = null;
Console.WriteLine(*px); // Produce una NullReferenceException

    No tiene sentido aplicar * a un puntero de tipo void * ya que estos punteros no almacenan información sobre el tipo de objetos a los que apuntan y por tanto no es posible recuperarlos a través de los mismos ya que no se sabe cuanto espacio en memoria a partir de la dirección almacenada en el puntero ocupa el objeto apuntado y, por tanto, no se sabe cuanta memoria hay que leer para obtenerlo.

Acceso a miembro de contenido de puntero. Operador ->

    Si un puntero apunta a un objeto estructura que tiene un método F() sería posible llamarlo a través del puntero con:


(*objeto).F();

    Sin embargo, como llamar a objetos apuntados por punteros es algo bastante habitual, para facilitar la sintaxis con la que hacer esto se ha incluido en C# el operador ->, con el que la instrucción anterior se escribiría así:


objeto->f();

    Es decir, del mismo modo que el operador . permite acceder a los miembros de un objeto referenciado por una variable normal, -> permite acceder a los miembros de un objeto referenciado por un puntero. En general, un acceso de la forma O -> M es equivalente a hacer (*O).M. Por tanto, al igual que es incorrecto aplicar * sobre punteros de tipo void *, también lo es aplicar ->

Conversiones de punteros

    De todo lo visto hasta ahora parece que no tiene mucho sentido el uso de punteros de tipo void * Pues bien, una utilidad de este tipo de punteros es que pueden usarse como almacén de punteros de cualquier otro tipo que luego podrán ser recuperados a su tipo original usando el operador de conversión explícita. Es decir, igual que los objetos de tipo object pueden almacenar implícitamente objetos de cualquier tipo, los punteros void * pueden almacenar punteros de cualquier tipo y son útiles para la escritura de métodos que puedan aceptar parámetros de cualquier tipo de puntero.

    A diferencia de lo que ocurre entre variables normales, las conversiones entre punteros siempre se permiten, al realizarlas nunca se comprueba si son válidas. Por ejemplo:


 char c = 'A';
 char* pc = &c;
 void* pv = pc;               
 int* pi = (int*)pv;
 int i = *pi;// Almacena en 16 bits del char de pi + otros 16
// indeterminados
 Console.WriteLine(i);
 *pi = 123456;// Machaca los 32 bits apuntados por pi

    En este código pi es un puntero a un objeto de tipo int (32 bits), pero en realidad el objeto al que apunta es de tipo char (16 bits), que es más pequeño. El valor que se almacene en i es en principio indefinido, pues depende de lo que hubiese en los 16 bits extras resultantes de tratar pv como puntero a int cuando en realidad apuntaba a un char.

    Del mismo modo, conversiones entre punteros pueden terminar produciendo que un puntero apunte a un objeto de mayor tamaño que los objetos del tipo del puntero. En estos casos, el puntero apuntaría a los bits menos significativos del objeto apuntado.

    También es posible realizar conversiones entre punteros y tipos básicos enteros. La conversión de un puntero en un tipo entero devuelve la dirección de memoria apuntada por el mismo. Por ejemplo, el siguiente código muestra por pantalla la dirección de memoria apuntada por px:


 int x = 10;
 int *px = &x;
 Console.WriteLine((int) px);

    Por su parte, convertir cualquier valor entero en un puntero tiene el efecto de devolver un puntero que apunte a la dirección de memoria indicada  por ese número. Por ejemplo, el siguiente código hace que px apunte a la dirección 1029 y luego imprime por pantalla la dirección de memoria apuntada por px (que será 1029):


int *px = (int *) 10;  
Console.WriteLine((int) px);

     Nótese que aunque en un principio es posible hacer que un puntero almacene cualquier dirección de memoria, si dicha dirección no pertenece al mismo proceso que el código en que se use el puntero se producirá un error al leer el contenido de dicha dirección. El tipo de error ha producir no se indica en principio en la especificación del lenguaje, pero la implementación de Microsoft lanza una referencia NullReferenceException. Por ejemplo, el siguiente código produce una excepción de dicho tipo al ejecurtase:

 
 using System;
 
 class AccesoInválido
 {
  public unsafe static void Main()
  {
   int * px = (int *) 100;
   Console.Write(*px); // Se lanza NullReferenceException
  }
 }

Aritmética de punteros

    Los punteros se suelen usar para recorrer tablas de elementos sin necesidad de tener que comprobarse que el índice al que se accede en cada momento se encuentra dentro de los límites de la tabla. Por ello, los operadores aritméticos definidos para los punteros están orientados a facilitar este tipo de recorridos.

    Hay que tener en cuenta que todos los operadores aritméticos aplicables a punteros dependen del tamaño del tipo de dato apuntado, por lo que no son aplicables a punteros void * ya que estos no almacenan información sobre dicho tipo. Esos operadores son:

  • ++ y --: El operador ++ no suma uno a la dirección almacenada en un puntero, sino que le suma el tamaño del tipo de dato al que apunta. Así, si el puntero apuntaba a un elemento de una tabla pasará a apuntar al siguiente (los elementos de las tablas se almacenan en memoria consecutivamente) Del mismo modo, -- resta a la dirección almacenada en el puntero el tamaño de su tipo de dato. Por ejemplo, una tabla de 100 elementos a cuyo primer elemento inicialmente apuntase pt podría recorrerse así:


for (int i=0; i<100; i++)
Console.WriteLine("Elemento{0}={1}", i, (*p)++);

El problema que puede plantear en ciertos casos el uso de ++ y -- es que hacen que al final del recorrido el puntero deje de apuntar al primer elemento de la tabla. Ello podría solucionarse almacenando su dirección en otro puntero antes de iniciar el recorrido y restaurándola a partir de él tras finalizarlo.

  • + y -: Permiten solucionar el problema de ++ y -- antes comentado de una forma  más cómoda basada en sumar o restar un cierto entero a los punteros. + devuelve la dirección resultante de sumar a la dirección almacenada en el puntero sobre el que se aplica el tamaño del tipo de dicho puntero tantas veces como indique el entero sumado. - tiene el mismo significado pero r estando dicha cantidad en vez de sumarla. Por ejemplo, usando + el bucle anterior podría rescribirse así:

 
for (int i=0; i<100; i++)
Console.WriteLine("Elemento{0}={1}", i, *(p+i));

El operador - también puede aplicarse entre dos punteros de un mismo tipo, caso       en que devuelve un long que indica cuántos elementos del tipo del puntero pueden almacenarse entre las direcciones de los punteros indicados.

  • []: Dado que es frecuente usar + para acceder a elementos de tablas, también se ha redefinido el operador [] para que cuando se aplique a una tabla haga lo mismo y devuelva el objeto contenido en la dirección resultante. O sea *(p+i) es equivalente a p[i], con lo que el código anterior equivale a:


for (int i=0; i<100; i++)
Console.WriteLine("Elemento{0}={1}", i, p[i]);

No hay que confundir el acceso a los elementos de una tabla aplicando [] sobre una variable de tipo tabla normal con el acceso a través de un puntero que apunte a su primer elemento. En el segundo caso no se comprueba si el índice indicado se encuentra dentro del rango de la tabla, con lo que el acceso es más rápido pero también más proclive a errores difíciles de detectar.

    Finalmente, respecto a la aritmética de punteros, hay que tener en cuenta que por eficiencia, en las operaciones con punteros nunca se comprueba si se producen desbordamientos, y en caso de producirse se truncan los resultados sin avisarse de ello mediante excepciones. Por eso hay que tener especial cuidado al operar con punteros no sea que un desbordamiento no detectado cause errores de causas difíciles de encontrar.

Manipulación de punteros
José Antonio González Seco

José Antonio es experto en tecnologias Microsoft. Imparte cursos y conferencias en congresos sobre C# y .NET en Universidades de toda España (Sevilla, Barcelona, San Sebastián, Valencia, Oviedo, etc.) en representación de grandes empresas como Microsoft.
Fecha de alta:05/12/2006
Última actualizacion:05/12/2006
Visitas totales:27553
Valorar el contenido:
Últimas consultas realizadas en los foros
Últimas preguntas sin contestar en los foros de devjoker.com