Punteros

El concepto de puntero está unido a la forma en que los tipos de datos son almacenados en la memoria de un ordenador, ya que denotan la dirección (address) o localización de una variable determinada. El nombre de la variable determina el tipo (char, int, float o double) y su dirección determina dónde está almacenada. Conocer la dirección de una variable es importante porque:
El nombre de un array es un puntero al array. Por tanto, los punteros y los arrays están íntimamente ligados en C y en C++.

Terminología básica.

Para entender los punteros y los vectores, es necesario conocer primero cómo se almacenan los números en la memoria del ordenador. El valor de cada variable en un programa de ordenador se guarda en una sección de memoria cuyo tamaño está determinado por el tipo de dato de la variable. La localización de esta sección de memoria es almacenada en la dirección de la variable. Por tanto, es como si cada variable estuviera compuesta de dos partes: su valor y su dirección. Cada celda de memoria se puede considerar compuesta de una parte con el contenido, y otra en la que se almacena la dirección. Esto es an´alogo a una fila de casas: cada casa tiene diferentes contenidos, y para mirarla necesitamos conocer su dirección.
Las direcciones se representan normalmente por un número hexadecimal, pudiendo contener un carácter, un entero o un real (aunque en realidad todos son almacenados como números binarios).
Para obtener la dirección de una variable se utiliza el operador dirección &:

#include <iostream.h>
main()
{
  double d = 2.7183;
  cout << "numero = " << d << "\tdirección = " << &d << '\n';
}

El valor de d es 2.7183. El valor de &d es una dirección (donde 2.7183 está almacenado). La dirección es imprimida en formato hexadecimal.

Direcciones y punteros

Un puntero guarda la dirección de un objeto en memoria, y como tal un puntero es también una variable. Puede parecer algo confuso, es como decir que el contenido de una casa es la dirección de otra vivienda. Las direcciones se guardan como números hexadecimales, por lo que no hay ninguna razón por la que no podamos definir otro tipo de dato, similar a un entero, pero a través del cual se puede modificar el valor almacenado en esa dirección. Es importante entender la relación entre punteros y direcciones>

Un puntero en C o en C++ se declara anteponiendo un * al nombre de la variable, que es el operador inverso a &. El puntero apunta entonces a una variable del tipo especificado, y no debe ser usado con variables de otros tipos. Un experto en C podría forzar la utilización de un puntero con un tipo distinto del que se ha declarado, pero no es recomendable, ya que podría conducir a un uso erróneo.

Veamos algunos ejemplos de declaración de punteros:

int *puntero1;
int *puntero2, *puntero3;
char variable, *punteroCaracter;
float *punteroReal, real;

En la primera línea, declaramos un puntero a un entero. En la segunda, dos punteros a entero. En la tercera, un carácter ( variable) y un puntero a carácter (punteroCaracter). Por último, punteroReal) es un puntero a un real, y real es declarado como un número real.

Veamos un ejemplo de uso del operador * y del operador de dirección (&):

#include <iostream.h> main()   double d, *dp;
   d = 2.7183;
  *dp = &d;
  cout << "numero = " << d << "\tdirección = " << &d << '\n';

Veamos un programa completo:

# include <iostream.h>

main ()
{
  int *ptInt;
  int var1 = 7, var2 = 27;

  ptInt = &var1;
  var2 = *ptInt;

  cout << " var1 = " << var1 << " var2 = " <<var2 << "*ptInt = " <<*ptInt << '\n';

  *ptInt = 5;

  cout << " var1 = " << var1 << " var2 = " <<var2 << "*ptInt = " <<*ptInt << '\n';

ptInt = &*ptInt;
var1 = *&var1;
}

El resultado de la ejecución es:

var1 =7 var2 = 7 *ptInt = 7
var1 = 5 var2 =7 *ptInt = 5

Estudiemos el funcionamiento del programa:

Otro ejemplo:

#include <iostream.h>

main()
{
  int i = 2;
  int *ip = &i;
  int **ipp = &ip;
  int ***ippp = &ipp;

  cout << " i = " << i << endl
     << " *ip = " << *ip << endl
     << " *ipp = " << *ipp << endl
     << " *ippp = " << *ippp << endl

     << " &i = " << &i << endl
     << " ip = " << ip << endl
     << " ipp = " << ipp << endl
     << " ippp = " << ippp << endl;
}
La salida de este programa es:

i = 2
*ip = 2
*ipp = 0x11ffff908
*ippp = 0x11ffff900
&i = 0x11ffff908
ip = 0x11ffff908
ipp = 0x11ffff900
ippp = 0x11ffff8f8

Operaciones con punteros

Operaciones con punteros

Un puntero es un tipo de dato similar a un entero, y hay un conjunto de operaciones definidas para punteros:

Veamos un ejemplo de utilización de punteros:
#include <iostream.h> main()
{
  int vector[3];
  int princPunt = vector;
  int finPunt = &vector;

  vector[2] = 15;
  cout << *(princPunt+2) << *finPunt <<'\n';

  if (princPunt == finPunt)
    cout << " Esto no puede suceder " << '\n';

  cout << "Numero de elementos" <<finPunt-princPunt << '\n';
}


Veamos cómo trabaja este programa:

Siempre que se realiza una operación aritmética sobre un puntero, sumando o restando un entero, el puntero se incrementa o decrementa un número apropiado de sitios tal que el nuevo valor apunta a la variable que está n elementos (no n bytes) antes o después que el dado. De la misma forma, al restar dos punteros se obtiene el número de objetos entre las dos localizaciones. Finalmente, dos punteros son iguales si y sólo si apuntan a la misma variable (el valor de las direcciones es el mismo). No son necesariamente iguales si sus valores indirectos son los mismos, ya que estas variables podrín estar en diferentes localizaciones de memoria.

Punteros constantes y punteros a constantes.

C++ permite definir un puntero a una constante, de forma que el valor al que el puntero apunta no pueda ser cambiado, pero el puntero en si puede cambiar para apuntar a otra variable o constante:

char const *p;

p es un puntero a un carácter, declarado como constante. El carácter al que apunta p no debe ser cambiado. El puntero p en si puede modificarse. Así, *p = 'a' no es válido, pero p++ si lo es.

Además, se puede declarar un puntero constante, que no puede ser cambiado:

En la declaración:

char *cont p;

p es un puntero constante que no puede ser cambiado. El carácter al que apunta sin embargo sí puede modificarse.

Por último :

char const *const p;

indica que ni el puntero p ni el carácter al que apunta pueden ser modificados.

Estas construcciones pueden utilizarse para mejorar la calidad del código. Si un puntero o una variable no serán nunca cambiados deben declararse como constantes. El compilador indicará si inadvertidamente se han modificado estos valores.

Puntero a void.

El puntero a void es también un elemento del ANSI-C. A un puntero a void puede asignarse el valor de cualquier puntero de otro tipo. Por ejemplo, en el siguiente programa, al puntero general se le asigna primero la dirección de un entero, y después la de un número real:

#include <iostream.h>

main ()
{
  int *ptInt;
  float *ptFloat;
  int var1 = 7, var2 = 27;
  float x = 1.2345, y = 32.14;
  void *general;

  ptInt = &var1;
  *ptInt + = var2;
  cout << " var1 tiene ahora el valor" << *ptInt << '\n';
  general = ptInt;

  ptFloat = &x;
  y + =5 * (*ptFloat);
  cout << " y tiene ahora el valor" << y << '\n';
  general = ptFloat;

}

Esto permite al programador definir un puntero que puede ser usado para apuntar a variables de tipos diferentes para transferir información a lo largo del programa. Un buen ejemplo es la función malloc(), que devuelve un puntero a void. Este puntero puede ser asignado a un puntero de cualquier tipo, transfiriendo el puntero devuelto al tipo correcto.
Un puntero a void se almacena en memoria de forma que puede ser utilizado con cualquiera de los tipos simples predefinidos en C++, pero también pueden ser utilizados con los tipos compuestos que el usuario define, puesto que los tipos compuestos están formados por tipos simples.

Reserva dinámica de memoria

Los operadores new y delete se utilizan para reservar y liberar memoria dinámicamente, de forma similar a las funciones malloc() y free() en C. Ahora los operadores new y delete son parte del lenguaje C++ y no parte de una librería. El propósito de new es crear arrays cuyo tamaño pueda ser determinado mientras el programa corre.
Delete funciona igual que free en C. El contenido al que apunta el puntero es borrado, pero no el puntero en sí. Se pueden crear más variables y hacer que el localización sea el mismo puntero.
Veamos un ejemplo de utilización de estos operadores
# include <iostream.h>

main()
{
  int index, *point1, *point2;

  point1 = &index;
  *point1 = 77;
  point2 = new int;
  *point2 = 173;
  cout <<"Los valores son " << index <<" " << *point1 << " "<< *point2 <<'\n';

  point1 = new int;
  point2 = point1;
  *point1 = 999;
  cout <<"Los valores son " << index <<" " << *point1 << " "<<   *point2 <<'\n';

  delete point1;

  float *float_point1, *float_point2 = new float;
  float _point1 = new float;
  *float_point2 = 3.14159;
  *float_point1 = 2.4 * (*float_point2);
  delete float_point2;
  delete float_point1;

  char *c_point;

  c_point = new date;
  delete c_point;   c_point = new char [sizeof(int) + 133];
  delete c_point;
}


Resultado de la ejecución:

Los valores son 77 77 173
Los valores son 77 999 999

Punteros a funciones Punteros a funciones

Los punteros a funciones ya existían en C, aunque no se utilizan regularmente. Veamos un ejemplo:

#include <iostream.h>

void printMensaje (float dato);
void printNumero (float dato);
void (*funcPuntero)(float);

main()
{
  float pi = 3.14159;
  printMensaje(pi);
  funcPuntero = printMensaje;
  funcPuntero (pi);
  funcPuntero = printNumero;
  funcPuntero (pi);
  printNumero(pi);
}

void printMensaje(float dato)
{
  cout <<" Esta es la funcion printMensaje " <<'\n';
}

void printNumero(float dato)
{
  cout <<" Este es el dato: " << dato << '\n';
}


La salida del programa será:

Esta es la funcion printMensaje
Esta es la funcion printMensaje
Este es el dato: 3.14159
Este es el dato: 3.14159

Hemos declarado dos funciones, printMensaje y printNumero, y después funcPuntero, que es un puntero a una función que recibe un parámetro real y no devuelve nada (void). Las dos funciones declaradas anteriormente se ajustan precisamente a este perfil, y por tanto pueden ser llamadas por este puntero.
En la función principal, llamamos a la función printMensaje con el parámetro pi, y en la línea siguiente asignamos al puntero a función funcPuntero el valor de printMensaje y utilizamos el puntero para llamar a la misma función de nuevo. Por tanto, las dos llamadas a la función printMensaje son idénticas gracias a la utilización del puntero funcPuntero.

Dado que hemos asignado el nombre de una función a un puntero a función, y el compilador no da error, el nombre de una función debe ser un puntero a una función. Esto es exactamente lo que sucede. Un nombre de una función es un puntero a esa función, pero es un puntero constante que no puede ser cambiado. Sucede lo mismo con los vectores: el nombre de un vector es un puntero constante al primer elemento del vector.
Ya que el nombre de una función es un puntero a esa función, podemos asignar el nombre de una función a un puntero constante, y usar el puntero para llamar a la función. Pero el valor devuelto, así como el número y tipo de parámetros deben ser idénticos. Muchos compiladores de C y C++ no avisan de las diferencias entre las listas de parámetros cuando se hacen las asignaciones. Esto se debe a que las asignaciones se hacen en tiempo de ejecución, cuando la información de este tipo no está disponible para el sistema.
Si en el ejemplo anterior añadimos una función que tiene como parámetro un entero e intentamos llamarla con el puntero a función funcPuntero, el compilador lanzará el siguiente mensaje de error:

   error: In this statement, the referenced type of the pointer value "print_int" is "function (int) returning void", which is not compatible with "function (float) returning void". Compilation terminated with errors.