19 de septiembre de 2016

Apuntadores.

   Los apuntadores son uno de los mitos del lenguaje de programación C (otro es la recursividad aunque ésta no está asociada con ningún lenguaje sino con la programación).

   Esta entrada introduce a los conceptos y manipulación de apuntadores; la principal intención es que los apuntadores dejen de ser un mito y pasen a ser parte del repertorio de herramientas fundamentales del programador, ya que los apuntadores son, en más de un sentido, la piedra angular del lenguaje de programación C.

Definición, estructura y representación.
   La sencillez de los apuntadores se basa en su definición la cual es bastante fácil de recordar y es, en esencia, casi todo lo que se tiene que saber respecto a los apuntadores y de ahí la importancia de su comprensión.

   Un apuntador almacena direcciones de memoria. Básicamente hay dos tipos de apuntadores:
  1. Apuntadores a variables.
  2. Apuntadores a funciones.
   Si el apuntador es a una variable, entonces el apuntador almacena la dirección en memoria de dicha variable. Si el apuntador es a una función, entonces el apuntador almacena la dirección en memoria del inicio del código de dicha función. Por ser en principio más simples, se presentarán primero los apuntadores a variables.

   Las siguientes figuras muestran la representación de un apuntador, y su explicación se verá reforzada cuando se analice el Ejemplo 7.1. Mientras tanto, tome en cuenta que un apuntador sólo puede hacer referencia o apuntar a objetos de su mismo tipo de datos, esto es: un apuntador a int, sólo puede hacer referencia a variables de tipo int, un apuntador a char, sólo puede hacer referencia a variables de tipo char, etc.
(a) Representación de un apuntador (abstracción).
(b) Representación física (en memoria) de un apuntador.
 
    La declaración de una variable de tipo apuntador tiene la siguiente estructura general en el lenguaje de programación C:

tipo_de_dato * nombre_del_apuntador;

en donde:
  • tipo_de_dato es cualquier tipo de dato válido en C.
  • nombre_del_apuntador es un identificador válido en C.
  • * es el operador que denota que la variable nombre_del_apuntador es una variable de tipo apuntador a tipo_de_dato.
   Una variable de tipo apuntador debe ser congruente, es decir, sólo puede hacer referencia (apuntar) a variables de su mismo tipo de datos; así por ejemplo, una variable de tipo apuntador a int puede hacer referencia a una variable de tipo int, mientras que una variable de tipo apuntador a float puede hacer referencia a una variable de tipo float y así sucesivamente para los demás tipos de datos.

Piedra angular de la piedra angular.
   El Ejemplo 7.1 es fundamental para entender a los apuntadores; por lo que resulta indispensable su total y absoluta comprensión para poder continuar y construir, en base a él, los conceptos subsecuentes. Recuerde apoyarse de las figuras anteriores para la mejor comprensión del ejemplo.

   La línea 9 muestra la declaración de dos variables de tipo apuntador a entero: aPtr y bPtr (note que como parte del identificador para cada una de las variables, se ha utilizado Ptr (Pointer) a forma de sufijo, esto no es obligatorio ni por sí mismo vuelve a la variable de tipo apuntador, pero es una buena práctica de programación el hacerlo así, debido a que refuerza, por auto documentación, que la variable es de tipo apuntador, ya que después de la declaración, no hay nada particular en la variable que en sí misma denote que es o no de tipo apuntador). Observe que el operador "*" no se distribuye en las variables como lo hace el tipo de dato, por lo que por cada variable de tipo apuntador que se necesite, se deberá escribir dicho operador para especificar que la variable asociada es de tipo apuntador.

   Las líneas 11, 12 y 13 inicializan a la variable a con 40178, y a aPtr y bPtr con la dirección en memoria de a. El operador "&" obtiene la dirección en memoria del operando (variable) asociado(a), y se conoce como el operador de dirección.

   Una variable de tipo apuntador, al ser una variable cuyo contenido es la dirección en memoria de otra variable, almacena direcciones de memoria obtenidas por el operador de dirección (el único valor que puede asignarse directamente a una variable de tipo apuntador es el 0, denotando así que el apuntador no hace referencia a nada, es decir, que no apunta a ninguna dirección válida de memoria en cuyo caso se dice que es un apuntador nulo), y aunque en principio es posible asignarle a un apuntador un valor específico, esto generará, con toda seguridad una violación de memoria y el programa será terminado por la mayoría de sistemas operativos (ningún sistema operativo debería permitir la intromisión en áreas de memoria que no sean las propias del programa, ya que ésto incurriría en graves errores de seguridad). Las figuras anteriores (a) y (b) ilustran lo que ocurre en las líneas 11-13.

   Las funciones printf de las líneas 15 y 16 contienen el especificador de formato "%p" (pointer), el cual permite visualizar en la salida estándar direcciones de memoria. Note cómo la dirección de memoria que se imprime en estas líneas es la misma, tal y como lo muestra la siguiente figura:

Salida del Ejemplo 7.1.
 
    La línea 19 muestra el uso del operador de desreferencia "*", el cual se utiliza para acceder al valor de la variable a la que se apunta o se hace referencia a través del apuntador.

   Para variables de tipo apuntador, el operador "*" tiene una dualidad:
  1. En la declaración de variables: se utiliza o sirve para denotar que la variable asociada es de tipo apuntador.
  2. En la desreferencia de variables: se utiliza o sirve para acceder al valor al que hace referencia o apunta el apuntador.
   Finalmente, la línea 21 modifica el valor de la variable a a través del apuntador bptr (recuerde que la variable a está siendo referida (apuntada) por las variables aPtr y bPtr (líneas 12 y 13 respectivamente)), y del operador de desreferencia, por lo que el valor a imprimir en la salida estándar para la variable a, será 2018, tal y como lo muestra la figura anterior.



   Antes de continuar, asegúrese de entender en su totalidad el Ejemplo 7.1 y lo que se ha descrito de él, ya que se insiste en que su comprensión es fundamental para entender los conceptos descritos en las entradas siguientes.