Siempre que se ejecuta un programa, se carga primero (típicamente desde disco) a la memoria del ordenador. 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 llamada 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 empezamos 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á, se da por sentado que el tipo de un elemento y la forma en que se define determina 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 una 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 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 estos:
f(): 4198736 dog: 4323632 cat: 4323636 bird: 4323640 fish: 4323644 i: 6684160 j: 6684156 k: 6684152
Se puede apreciar como las variables que se han definido dentro
de main()
están en un área distinta que las
variables definidas fuera de main()
;
entenderá el porque cuando se profundice más en el
lenguaje. También, f()
parece estar en su
propia área; el código normalmente se separa del resto 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 en el
computador en que ha sido ejecutado el programa, un entero ocupa
cuatro bytes.
¿Qué se puede hacer con las direcciones de memoria, además de este interesante experimento de mostrar cuanta memoria ocupan? 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 un si fuese un tipo
simple. Sin embargo, con un int
u otro tipo de
datos básico, se puede decir:
int a, b, c;
así que con un puntero, diría:
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á mas 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 de programación de 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 más fácil 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 de lo 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.
Estas 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, podemos 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á más adelante.
Para conseguir otras muchas técnicas de programación ingeniosas, sobre las que aprenderá en el resto del libro.