Siempre que se ejecuta un programa, se carga primero (típicamente desde disco) a la memoria del computador. De este modo, todos los elementos del programa se ubican en algún lugar de la memoria. La memoria se representa normalmente como series secuenciales de posiciones de memoria; normalmente se hace referencia a estas localizaciones como bytes de ocho bits, pero realmente el tamaño de cada espacio depende de la arquitectura de cada máquina particular y se llama normalmente tamaño de palabra de la máquina. Cada espacio se puede distinguir unívocamente de todos los demás espacios por su dirección. Para este tema en particular, se establecerá que todas las máquinas usan bytes que tienen direcciones secuenciales, comenzando en cero y subiendo hasta la cantidad de memoria que posea la máquina.
Como el programa reside en memoria mientras se está ejecutando, cada elemento de dicho programa tiene una dirección. Suponga que se empieza con un programa simple:
//: C03:YourPets1.cpp #include <iostream> using namespace std; int dog, cat, bird, fish; void f(int pet) { cout << "pet id number: " << pet << endl; } int main() { int i, j, k; } ///:~
Listado 3.13. C03/YourPets1.cpp
Cada uno de los elementos de este programa tiene una localización en memoria mientras el programa se está ejecutando. Incluso las funciones ocupan espacio. Como verá, resulta que lo que es un elemento y la forma en que se define determinan normalmente el área de memoria en la que se ubica dicho elemento.
Hay un operador en C y C++ que permite averiguar la dirección de un elemento. Se trata del operador &. Sólo hay que anteponer el operador & delante del nombre identificador y obtendrá la dirección de ese identificador. Se puede modificar YourPets1.cpp para mostrar las direcciones de todos sus elementos, del siguiente modo:
//: C03:YourPets2.cpp #include <iostream> using namespace std; int dog, cat, bird, fish; void f(int pet) { cout << "pet id number: " << pet << endl; } int main() { int i, j, k; cout << "f(): " << (long)&f << endl; cout << "dog: " << (long)&dog << endl; cout << "cat: " << (long)&cat << endl; cout << "bird: " << (long)&bird << endl; cout << "fish: " << (long)&fish << endl; cout << "i: " << (long)&i << endl; cout << "j: " << (long)&j << endl; cout << "k: " << (long)&k << endl; } ///:~
Listado 3.14. C03/YourPets2.cpp
El (long) es un molde. Indica «No tratar como su tipo normal, sino como un long». El molde no es esencial, pero si no existiese, las direcciones aparecerían en hexadecimal, de modo que el moldeado a long hace las cosas un poco más legibles.
Los resultados de este programa variarán dependiendo del computador, del sistema operativo, y de muchos otros tipos de factores, pero siempre darán un resultado interesante. Para una única ejecución en mi computador, los resultados son como éstos:
f(): 4198736 dog: 4323632 cat: 4323636 bird: 4323640 fish: 4323644 i: 6684160 j: 6684156 k: 6684152
Se puede apreciar cómo las variables que se han definido dentro de main() están en un área distinta que las variables definidas fuera de main(); entenderá el porqué cuando se profundice más en el lenguaje. También, f() parece estar en su propia área; el código normalmente se separa de los datos en memoria.
Otra cosa a tener en cuenta es que las variables definidas una a continuación de la otra parecen estar ubicadas de manera contigua en memoria. Están separadas por el número de bytes requeridos por su tipo de dato. En este programa el único tipo de dato utilizado es el int, y la variable cat está separada de dog por cuatro bytes, bird está separada por cuatro bytes de cat, etc. De modo que parece que en el computador en que ha sido ejecutado el programa, un int ocupa cuatro bytes.
¿Qué se puede hacer con las direcciones de memoria, además de este interesante experimento que muestra cómo se distribuye la memoria? Lo más importante que se puede hacer es guardar esas direcciones dentro de otras variables para su uso posterior. C y C++ tienen un tipo de variable especial para guardar una dirección. Esas variables se llaman punteros.
El operador que define un puntero es el mismo que se utiliza para la multiplicación: *. El compilador sabe que no es una multiplicación por el contexto en el que se usa, tal como podrá comprobar.
Cuando se define un puntero, se debe especificar el tipo de variable al que apunta. Se comienza dando el nombre de dicho tipo, después en lugar de escribir un identificador para la variable, usted dice «Espera, esto es un puntero» insertando un asterisco entre el tipo y el identificador. De modo que un puntero a int tiene este aspecto:
int* ip; // ip apunta a una variable int
La asociación del * con el tipo parece práctica y legible, pero puede ser un poco confusa. La tendencia podría ser decir «puntero-entero» como si fuese un único tipo. Sin embargo, con un int u otro tipo de datos básico, se puede decir:
int a, b, c;
mientras que con un puntero le gustaría decir:
int* ipa, ipb, ipc;
La sintaxis de C (y por herencia, la de C++) no permite expresiones tan cómodas. En las definiciones anteriores, sólo ipa es un puntero, pero ipb e ipc son ints normales (se puede decir que «* está más unido al identificador»). Como consecuencia, los mejores resultados se pueden obtener utilizando sólo una definición por línea; y aún se conserva una sintaxis cómoda y sin la confusión:
int* ipa; int* ipb; int* ipc;
Ya que una pauta general de programación en C++ es que siempre se debe inicializar una variable al definirla, realmente este modo funciona mejor. Por ejemplo, las variables anteriores no se inicializan con ningún valor en particular; contienen basura. Es mucho mejor decir algo como:
int a = 47; int* ipa = &a;
Ahora tanto a como ipa están inicializadas, y ipa contiene la dirección de a.
Una vez que se inicializa un puntero, lo más básico que se puede hacer con él es utilizarlo para modificar el valor al que apunta. Para acceder a la variable a través del puntero, se dereferencia el puntero utilizando el mismo operador que se usó para definirlo, como sigue:
*ipa = 100;
Ahora a contiene el valor 100 en vez de 47.
Éstas son las normas básicas de los punteros: se puede guardar una dirección, y se puede utilizar dicha dirección para modificar la variable original. Pero la pregunta aún permanece: ¿por qué se querría cambiar una variable utilizando otra variable como intermediario?
Para esta visión introductoria a los punteros, se puede dividir la respuesta en dos grandes categorías:
Para cambiar «objetos externos» desde dentro de una función. Esto es quizás el uso más básico de los punteros, y se examinará aquí.
Para conseguir otras muchas técnicas de programación ingeniosas, sobre las que aprenderá en el resto del libro.