402y37
Tipos de objetos IV: Punteros 1
No, no salgas corriendo todavía. Aunque vamos a empezar con un tema que suele asustar a los estudiantes de C++, no es algo tan terrible como se cuenta. Como se suele decir de los leones: no son tan fieros como los pintan.
¡Pánico!, punteros
Vamos a intentar explicar cómo funcionan los punteros de forma que no tengan el aspecto de magia negra ni una galimatías incomprensible.
Pero no bastará con entender lo que se explica en este capítulo. Es relativamente sencillo saber qué son y cómo funcionan los punteros. Para poder manejarlos es necesario también comprender los punteros, y eso significa saber qué pueden hacer y cómo lo hacen. Para comprender los punteros se necesita práctica, algunos necesitamos más que otros, (y yo considero que no me vendría mal seguir practicando). Incluso cuando ya creas que los dominas, seguramente quedarán nuevos matices por conocer.
Pero seguramente estoy exagerando. Si soy capaz de explicar correctamente los conceptos de este capítulo, pronto te encontrarás usando punteros en tus programas casi sin darte cuenta.
Los punteros proporcionan la mayor parte de la potencia al C++, y marcan la principal diferencia con otros lenguajes de programación.
Una buena comprensión y un buen dominio de los punteros pondrá en tus manos una herramienta de gran potencia. Un conocimiento mediocre o incompleto te impedirá desarrollar programas eficaces.
Por eso le dedicaremos mucha atención y mucho espacio a los punteros. Es muy importante comprender bien cómo funcionan y cómo se usan.
Creo que todos sabemos lo que es un puntero, fuera del ámbito de la programación. Usamos punteros para señalar cosas sobre las que queremos llamar la atención, como marcar puntos en un mapa o detalles en una presentación en pantalla. A menudo, usamos el dedo índice para señalar direcciones o lugares sobre los que estamos hablando o explicando algo. Cuando un dedo no es suficiente, podemos usar punteros. Antiguamente esos punteros eran una vara de madera, pero actualmente se usan punteros laser, aunque la idea es la misma. Un puntero también es el símbolo que representa la posición del ratón en una pantalla gráfica. Estos punteros también se usan para señalar objetos: enlaces, opciones de menú, botones, etc. Un puntero sirve, pues, para apuntar a los objetos a los que nos estamos refiriendo.
Pues en C++ un puntero es exactamente lo mismo. Probablemente habrás notado que a lo largo del curso nos hemos referido a variables, constantes, etc como objetos. Esto ha sido intencionado por el siguiente motivo:
C++ está diseñado para la programación orientada a objetos (POO), y en ese paradigma, todas las entidades que podemos manejar son objetos.
Los punteros en C++ sirven para señalar objetos, y también para manipularlos.
Para entender qué es un puntero veremos primero cómo se almacenan los datos en un ordenador.
La memoria de un ordenador está compuesta por unidades básicas llamadas bits. Cada bit sólo puede tomar dos valores, normalmente denominados alto y bajo, ó 1 y 0. Pero trabajar con bits no es práctico, y por eso se agrupan.
Cada grupo de 8 bits forma un byte u octeto. En realidad el microprocesador, y por lo tanto nuestro programa, sólo puede manejar directamente bytes o grupos de dos o cuatro bytes. Para acceder a los bits hay que acceder antes a los bytes.
Cada byte de la memoria de un ordenador tiene una dirección, llamada dirección de memoria.
Los microprocesadores trabajan con una unidad básica de información, a la que se denomina palabra (en inglés word). Dependiendo del tipo de microprocesador una palabra puede estar compuesta por uno, dos, cuatro, ocho o dieciséis bytes. Hablaremos en estos casos de plataformas de 8, 16, 32, 64 ó 128 bits.
Se habla indistintamente de direcciones de memoria, aunque las palabras sean de distinta longitud.
Cada dirección de memoria contiene siempre un byte. Lo que sucederá cuando las palabras sean, por ejemplo, de 32 bits es que accederemos a posiciones de memoria que serán múltiplos de 4.
Por otra parte, la mayor parte de los objetos que usamos en nuestros programas no caben en una dirección de memoria. La solución utilizada para manejar objetos que ocupen más de un byte es usar posiciones de memoria correlativas. De este modo, la dirección de un objeto es la dirección de memoria de la primera posición que contiene ese objeto.
Dicho de otro modo, si para almacenar un objeto se precisan cuatro bytes, y la dirección de memoria de la primera posición es n, el objeto ocupará las posiciones desde n a n+3, y la dirección del objeto será, también, n.
Todo esto sucede en el interior de la máquina, y nos importa relativamente poco. Pero podemos saber qué tipo de plataforma estamos usando averiguando el tamaño del tipo int, y para ello hay que usar el operador sizeof, por ejemplo:
cout << "Plataforma de " << 8*sizeof(int) << " bits"; |
Ahora veamos cómo funcionan los punteros. Un puntero es un tipo especial de objeto que contiene, ni más ni menos que, la dirección de memoria de un objeto. Por supuesto, almacenada a partir de esa dirección de memoria puede haber cualquier clase de objeto: un char, un int, un float, un array, una estructura, una función u otro puntero. Seremos nosotros los responsables de decidir ese contenido, al declarar el puntero.
De hecho, podríamos decir que existen tantos tipos diferentes de punteros como tipos de objetos puedan ser referenciados mediante punteros. Si tenemos esto en cuenta, los punteros que apunten a tipos de objetos distintos, serán tipos diferentes. Por ejemplo, no podemos asignar a un puntero a char el valor de un puntero a int.
Intentemos ver con mayor claridad el funcionamiento de los punteros. Podemos considerar la memoria del ordenador como un gran array, de modo que podemos acceder a cada celda de memoria a través de un índice. Podemos considerar que la primera posición del array es la 0 celda[0].
Si usamos una variable para almacenar el índice, por ejemplo, indice=0, entonces celda[0] == celda[indice]. Finalmente, si prescindimos de la notación de los arrays, podemos ver que el índice se comporta exactamente igual que un puntero.
El puntero indice podría tener por ejemplo, el valor 3, en ese caso, el valor apuntado por indice tendría el valor 'val3'.
Las celdas de memoria tienen una existencia física, es decir son algo real y existirán siempre, independientemente del valor del puntero. Existirán incluso si no existe el puntero.
De forma recíproca, la existencia o no existencia de un puntero no implica la existencia o la inexistencia del objeto. De la misma forma que el hecho de no señalar a un árbol, no implica la inexistencia del árbol. Algo más oscuro es si tenemos un puntero para árboles, que no esté señalando a un árbol. Un puntero de ese tipo no tendría uso si estamos en medio del mar: tener ese puntero no crea árboles de forma automática cuando señalemos con él. Es un puntero, no una varita mágica. :-D
Del mismo modo, el valor de la dirección que contiene un puntero no implica que esa dirección sea válida, en el sentido de que no tiene por qué contener la dirección de un objeto del tipo especificado por el puntero.
Supongamos que tenemos un mapa en la pared, y supongamos también que existen diferentes tipos de punteros láser para señalar diferentes tipos de puntos en el mapa (ya sé que esto suena raro, pero usemos la imaginación). Creamos un puntero para señalar ciudades. Nada más crearlo (o encenderlo), el puntero señalará a cualquier sitio, podría señalar incluso a un punto fuera del mapa. En general, daremos por sentado que una vez creado, el puntero no tiene por qué apuntar a una ciudad, y aunque apunte al mapa, podría estar señalando a un mar o a un río.
Con los punteros en C++ ocurre lo mismo. El valor inicial del puntero, cuando se declara, podría estar fuera del mapa, es decir, contener direcciones de memoria que no existen. Pero, incluso señalando a un punto de la memoria, es muy probable que no señale a un objeto del tipo adecuado. Debemos considerar esto como el caso más probable, y no usar jamás un puntero que no haya sido inicializado correctamente.
Dentro del array de celdas de memoria existirán zonas que contendrán programas y datos, tanto del como del propio sistema operativo o de otros programas, el sistema operativo se encarga de gestionar esa memoria, prohibiendo o protegiendo determinadas zonas.
Pero el propio puntero, como objeto que es, también se almacenará en memoria, y por lo tanto, también tendrá una dirección de memoria. Cuando declaramos un puntero estaremos reservando la memoria necesaria para almacenarlo, aunque, como pasa con el resto del los objetos, el contenido de esa memoria contendrá basura.
En principio, debemos asignar a un puntero, o bien la dirección de un objeto existente, o bien la de uno creado explícitamente durante la ejecución del programa o un valor conocido que indique que no señala a ningún objeto, es decir el valor 0. El sistema operativo, cuanto más avanzado es, mejor suele controlar la memoria. Ese control se traduce en impedir el a determinadas direcciones reservadas por el sistema.
Declaración de punteros
Los punteros se declaran igual que el resto de los objetos, pero precediendo el identificador con un asterisco (*).
Sintaxis:
<tipo> *<identificador>; |
Ejemplos:
int *pEntero;
char *pCaracter;
struct stPunto *pPunto; |
Los punteros sólo pueden apuntar a objetos de un tipo determinado, en el ejemplo, pEntero sólo puede apuntar a un objeto de tipo int.
La forma:
<tipo>* <identificador>; |
con el (*) junto al tipo, en lugar de junto al identificador del objeto, también está permitida. De hecho, también es legal la forma:
<tipo> * <identificador>; |
Veamos algunos matices. Tomemos el primer ejemplo:
int *pEntero; |
equivale a:
int* pEntero; |
Otro detalle importante es que, aunque las tres formas de situar el asterisco en la declaración son equivalentes, algunas de ellas pueden inducirnos a error, sobre todo si se declaran varios objetos en la misma línea:
int* x, y; |
En este caso, x es un puntero a int, pero y no es más que un objeto de tipo int. Colocar el asterisco junto al tipo puede que indique más claramente que estamos declarando un puntero, pero hay que tener en cuenta que sólo afecta al primer objeto declarado, si quisiéramos declarar ambos objetos como punteros a int tendremos que hacerlo de otro modo:
int* x, *y;
// O:
int *x, *y;
// O:
int* x;
int* y; |
Obtener punteros a objetos
Los punteros apuntan a objetos, por lo tanto, lo primero que tenemos que saber hacer con nuestros punteros es asignarles direcciones de memoria válidas de objetos.
Para averiguar la dirección de memoria de cualquier objeto usaremos el operador de dirección (&), que leeremos como "dirección de".
Por supuesto, los tipos tienen que ser "compatibles", no podemos almacenar la dirección de un objeto de tipo char en un puntero de tipo int.
Por ejemplo:
int A;
int *pA;
|
Según este ejemplo, pA es un puntero a int que apunta a la dirección donde se almacena el valor del entero A.
Objeto apuntado por un puntero
La operación contraria es obtener el objeto referenciado por un puntero, con el fin de manipularlo, ya sea modificando su valor u obteniendo el valor actual.
Para manipular el objeto apuntado por un puntero usaremos el operador de indirección, que es un asterisco (*).
En C++ es muy habitual que el mismo símbolo se use para varias cosas diferentes, este es el caso del asterisco, que se usa como operador de multiplicación, para la declaración de punteros y, como vemos ahora, como operador de indirección.
Como operador de indirección sólo está permitido usarlo con punteros, y podemos leerlo como "objeto apuntado por".
Por ejemplo:
int *pEntero;
int x = 10;
int y;
pEntero = &y;
*pEntero = x; // (1) |
En (1) asignamos al objeto apuntado por pEntero en valor del objeto x. Como pEntero apunta al objeto y, esta sentencia equivale (según la secuencia del programa), a asignar a y el valor de x.
Diferencia entre punteros y otros objetos
Debemos tener muy claro, en el ejemplo anterior, que pEntero es un objeto del tipo "puntero a int", pero que *pEntero NO es un objeto de tipo int, sino una expresión.
¿Por qué decimos esto?
Pues porque, como pasa con todos los objetos en C++, cuando se declaran sólo se reserva espacio para almacenarlos, pero no se asigna ningún valor inicial, (recuerda que nuestro puntero para árboles no crea árbol cada vez que señalemos con él). El contenido del objeto permanecerá sin cambios, de modo que el valor inicial del puntero será aleatorio e indeterminado. Debemos suponer que contiene una dirección no válida.
Si pEntero apunta a un objeto de tipo int, *pEntero será el contenido de ese objeto, pero no olvides que *pEntero es un operador aplicado a un objeto de tipo "puntero a int". Es decir, *pEntero es una expresión, no un objeto.
Declarar un puntero no creará un objeto del tipo al que apunta. Por ejemplo: int *pEntero; no crea un objeto de tipo int en memoria. Lo que crea es un objeto que puede contener la dirección de memoria de un entero.
Podemos decir que existe físicamente un objeto pEntero, y también que ese objeto puede (aunque esto no es siempre cierto) contener la dirección de un objeto de tipo int.
Como todos los objetos, los punteros también contienen "basura" cuando son declarados. Es costumbre dar valores iniciales nulos a los punteros que no apuntan a ningún sitio concreto:
int *pEntero = 0; // También podemos asignar el valor NULL
char *pCaracter = 0;
|
NULL es una constante, que está definida como cero en varios ficheros de cabecera, como "cstdio" o "iostream", y normalmente vale 0L. Sin embargo, hay muchos textos que recomiendan usar el valor 0 para asignar a punteros nulos, al menos en C++.
Ir al Principio