General:
● Definir qué son los punteros y describir su rol en la
resolución de problemas.
Específicos:
● Presentar la Definición de punteros.
● Mostrar el cómo se da la Manipulación de punteros.
● Explicar el Aritmética de punteros.
● Enseñar los Operadores relacionados con un código
inseguro.
● Proponer un ejemplo sobre el uso de punteros en C#.
Desarrollo:
Pero el uso de punteros hace el código más proclive a fallos en tanto que se salta
muchas de las medidas incluidas en el acceso normal a objetos, por lo que es
necesario incluir ciertas medidas de seguridad que eviten la introducción
accidental de esta inseguridad, como los punteros son “peligrosos” (es frecuente
que den lugar a errores muy difíciles de encontrar), el compilador nos obliga a
que le digamos que sabemos que esa zona de programa no es segura, usando la
palabra “unsafe”:
Manipulación de los Punteros:
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án 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.
2. Acceso a contenido de puntero. Operador *
(*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 ->
4. 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.
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:
Para el resto de tipos a los que se les puede aplicar, sizeof no tiene porqué
devolver un resultado constante sino que los compiladores pueden alinear en
memoria las estructuras incluyendo bits de relleno cuyo número y valores sean
en principio indeterminado. Sin embargo, el valor devuelto por sizeof siempre
devolverá el tamaño en memoria exacto del tipo de dato sobre el que se aplique,
incluyendo bits de relleno si los tuviese.
Nótese que es fácil implementar los operadores de aritmética de punteros
usando sizeof. Para ello, ++ se definiría como añadir a la dirección almacenada
en el puntero el resultado de aplicar sizeof a su tipo de dato, y -- consistiría en
restarle dicho valor. Por su parte, el operador + usado de la forma P + N (P es
un puntero de tipo T y N un entero) lo que devuelve es el resultado de añadir al
puntero sizeof(T)*N, y P – N devuelve el resultado de restarle sizeof(T)*N. Por
último, si se usa - para restar dos punteros P1 y P2 de tipo T, ello es equivalente
a calcular (((long)P1) - ((long)P2)))/sizeof(T)
➢ Operador stackalloc. Creación de tablas en pila.
Cuando se trabaja con punteros puede resultar interesante reservar una zona de
memoria en la pila donde posteriormente se puedan ir almacenando objetos.
Precisamente para eso está el operador stackalloc. stackalloc reserva en pila el
espacio necesario para almacenar contiguamente el número de objetos de tipo
<tipo> indicado en <número> (reserva sizeof(<tipo>)*<número> bytes) y
devuelve un puntero a la dirección de inicio de ese espacio. Si no quedase
memoria libre suficiente para reservarlo se produciría una excepción
System.StackOverflowException. (stackalloc sólo puede usarse para
inicializar punteros declarados como variables locales y sólo en el momento de
su declaración).
Aunque pueda parecer que stackalloc se usa como sustituto de new para crear
tablas en pila en lugar de en memoria dinámica, no hay que confundirse:
stackalloc sólo reserva un espacio contiguo en pila para objetos de un cierto
tipo, pero ello no significa que se cree una tabla en pila. Las tablas son objetos
que heredan de System.Array y cuentan con los miembros heredados de esta
clase y de object, pero regiones de memoria en pila reservadas por stackalloc
no. Sin embargo, gracias a que como ya se ha comentado en este tema el
operador [] está redefinido para trabajar con punteros, podemos usarlo para
acceder a los diferentes objetos almacenados en las regiones reservadas con
stackalloc como si fuesen tablas, Otra ventaja de la simulación de tablas con
stackalloc es que se reserva la memoria mucho más rápido que el tiempo que se
tardaría en crear una tabla. Esto se debe a que reservar la memoria necesaria en
pila es tan sencillo como incrementar el puntero de pila en la cantidad
correspondiente al tamaño a reservar, y no hay que perder tiempo en solicitar
memoria dinámica. Además, stackalloc no pierde tiempo en inicializar con
algún valor el contenido de la memoria, por lo que la "tabla" se crea antes pero a
costa de que luego sea más inseguro usarla ya que hay que tener cuidado con no
leer trozos de ella antes de asignarles valores válidos.
Ejemplo de Punteros:
using System;
public class BasicosPunteros
{
public static void Main()
{
Console.WriteLine(Environment.NewLine);
// Declaración de un arreglo de enteros como objetos
// (managed code):
int[] arreglo = new int[5] {100, 200, 300, 400, 500};
// El uso de punteros requiere marcar la siguiente región con la palabra clave
unsafe:
{
// Evita al colector de basura relocalizar una variable desplazable:
fixed(int* puntero1 = &arreglo[0]
{
// Creación de un nuevo puntero para incrementar la ubicación de memoria:
int* puntero2 = puntero1;
Console.WriteLine("-----------");
Console.WriteLine(Environment.NewLine);
}
}
Conclusión:
Los punteros son una de las más potentes características de C, pero a la vez uno
de sus mayores peligros. Los punteros nos permiten acceder directamente a
cualquier parte de la memoria. Esto da a los programas C una gran potencia. Sin
embargo son una fuente ilimitada de errores. Un error usando un puntero puede
bloquear el sistema, y a veces puede ser difícil detectarlo. Otros lenguajes no
nos dejan usar punteros para evitar estos problemas, pero a la vez nos quitan
parte del control que tenemos en C.
Los punteros son uno de los tipos de datos más temidos de C. Sin embargo,
también son los más versátiles y los que mayor potencia proporcionan. Su uso
permite el paso de argumentos por referencia, la construcción de estructuras
dinámicas de datos y la mejora en la velocidad de ejecución de algunos
algoritmos. Sin embargo, mal usados pueden producir errores graves, difíciles
de detectar y, en ocasiones, los peores errores posibles: los no reproducibles.
Es por eso que conocer su estructura, formas de manejo y ejecución así como su
declaración, es de vital importancia para la implementación de un programa y
así poder desarrollar una solución eficaz para un problema determinado que se
nos presente y necesita una respuesta por medio de este concepto de Punteros
que traerá consigo los Códigos Inseguros.
Bibliografía:
[4]J. Ortíz Ordonez, "Uso de punteros en C#. Ejemplo básico.", Gist, 2015.
[Online]. Available: https://gist.github.com/Fhernd/1fbe10aae1efe4fc0923.
[Accessed: 22- Feb- 2019].