Usuario:Humberto0601ad jc/Felo
| ||
Sumario
Punteros.
Los punteros proporcionan la mayor parte de la potencia al C y 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. 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. Y aquí llegamos al quid, cada byte tiene una dirección, llamada normalmente dirección de memoria. La unidad de información básica es la palabra, dependiendo del tipo de microprocesador una palabra puede estar compuesta por dos, cuatro, ocho o dieciséis bytes. Hablaremos en estos casos de plataformas de 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 de 32 bits es que accederemos a posiciones de memoria que serán múltiplos de 4.
Declaración de punteros.
Los punteros se declaran igual que el resto de las variables, pero precediendo el identificador con el operador de indirección, (*), que leeremos como "puntero a".
Sintaxis: <tipo> *<identificador>; Ejemplos: int *entero; char *carácter; struct stPunto *punto;
Los punteros siempre apuntan a un objeto de un tipo determinado, en el ejemplo, "entero" siempre apuntará a un objeto de tipo "int".
La forma: <tipo>* <indentificador>; con el (*) junto al tipo, en lugar de junto al identificador de variable, también está permitida.
Veamos algunos matices. Tomemos el primer ejemplo:
int *entero; equivale a: int* entero; Debes tener muy claro que "entero" es una variable del tipo "puntero a int", y que "*entero" NO es una variable de tipo "int".
Si "entero" apunta a una variable de tipo "int", "*entero" será el contenido de esa variable, pero no olvides que "*entero" es un operador aplicado a una variable de tipo "puntero a int", es decir "*entero" es una expresión, no una variable.
Para averiguar la dirección de memoria de cualquier variable usaremos el operador de dirección (&), que leeremos como "dirección de".
Declarar un puntero no creará un objeto. Por ejemplo: "int *entero;" no crea un objeto de tipo "int" en memoria, sólo crea una variable que puede contener una dirección de memoria. Se puede decir que existe físicamente la variable "entero", y también que esta variable puede contener la dirección de un objeto de tipo "int". Lo veremos mejor con otro ejemplo:
int A, B; int *entero; ... B = 213; /* B vale 213 */ entero = &A; /* entero apunta a la dirección de la variable A */ *entero = 103; /* equivale a la línea A = 103; */ B = *entero; /* equivale a B = A; */ ...
En este ejemplo vemos que "entero" puede apuntar a cualquier variable de tipo "int", y que podemos hacer referencia al contenido de dichas variables usando el operador de indirección (*).
Como todas las variables, los punteros también contienen "basura" cuando son declaradas. Es costumbre dar valores iniciales nulos a los punteros que no apuntan a ningún sitio concreto: entero = NULL; caracter = NULL;
NULL es una constante, que está definida como cero en varios ficheros de cabecera, como "stdio.h" o "iostream.h", y normalmente vale 0L.
Correspondencia entre arrays y punteros:
Existe una equivalencia casi total entre arrays y punteros. Cuando declaramos un array estamos haciendo varias cosas a la vez:
- Declaramos un puntero del mismo tipo que los elementos del array, y que apunta al primer elemento del array.
- Reservamos memoria para todos los elementos del array. Los elementos de un array se almacenan internamente en el ordenador en posiciones consecutivas de la memoria.
La principal diferencia entre un array y un puntero es que el nombre de un array es un puntero constante, no podemos hacer que apunte a otra dirección de memoria. Además, el compilador asocia una zona de memoria para los elementos del array, cosa que no hace para los elementos apuntados por un puntero auténtico.
Ejemplo: int vector[10]; int *puntero; puntero = vector; /* Equivale a puntero = &vector[0]; esto se lee como "dirección del elemento cero de vector" */ *puntero++; /* Equivale a vector[0]++; */ puntero++; /* entero == &vector[1] */
¿Qué hace cada una de estas instrucciones?: La primera incrementa el contenido de la memoria apuntada por "entero", que es vector[0].
La segunda incrementa el puntero, esto significa que apuntará a la posición de memoria del siguiente "int", pero no a la siguiente posición de memoria. El puntero no se incrementará en una unidad, como tal vez sería lógico esperar, sino en la longitud de un "int".
Análogamente la operación: puntero = puntero + 7;
No incrementará la dirección de memoria almacenada en "puntero" en siete posiciones, sino en 7*sizeof(int).
Otro ejemplo:
struct stComplejo {
float real, imaginario;
} Complejo[10];
stComplejo *p;
p = Complejo; /* Equivale a p = &Complejo[0]; */
p++; /* entero == &Complejo[1] */
En este caso, al incrementar p avanzaremos las posiciones de memoria necesarias para apuntar al siguiente complejo del array "Complejo". Es decir avanzaremos sizeof(stComplejo) bytes.
Operaciones con punteros:
Aunque no son muchas las operaciones que se pueden hacer con los punteros, cada una tiene sus peculiaridades.
Asignación.
Ya hemos visto cómo asignar a un puntero la dirección de una variable. También podemos asignar un puntero a otro, esto hará que los dos apunten a la misma posición:
int *q, *p; int a; q = &a; /* q apunta al contenido de a */ p = q; /* p apunta al mismo sitio, es decir, al contenido de a */
Operaciones aritméticas.
También hemos visto como afectan a los punteros las operaciones de suma con enteros. Las restas con enteros operan de modo análogo.
Pero, ¿qué significan las operaciones de suma y resta entre punteros?, por ejemplo:
int vector[10]; int *p, *q; p = vector; /* Equivale a p = &vector[0]; */ q = &vector[4]; /* apuntamos al 5º elemento */ cout << q-p << endl;
El resultado será 4, que es la "distancia" entre ambos punteros. Normalmente este tipo de operaciones sólo tendrá sentido entre punteros que apunten a elementos del mismo array. La suma de punteros no está permitida.
Comparación entre punteros.
Comparar punteros puede tener sentido en la misma situación en la que lo tiene restar punteros, es decir, averiguar posiciones relativas entre punteros que apunten a elementos del mismo array.
Existe otra comparación que se realiza muy frecuente con los punteros. Para averiguar si estamos usando un puntero es corriente hacer la comparación:
if(NULL != p) o simplemente if(p) Y también: if(NULL == p) O simplemente if(!p)
Punteros genéricos.
Es posible declarar punteros sin tipo concreto: void *<identificador>; Estos punteros pueden apuntar a objetos de cualquier tipo. Por supuesto, también se puede emplear el "casting" con punteros, sintaxis: (<tipo> *)<variable puntero>
Por ejemplo:
#include <iostream.h>
int main() {
char cadena[10] = "Hola";
char *c;
int *n;
void *v;
c = cadena; // c apunta a cadena
n = (int *)cadena; // n también apunta a cadena
v = (void *)cadena; // v también
cout << "carácter: " << *c << endl;
cout << "entero: " << *n << endl;
cout << "float: " << *(float *)v << endl;
return 0;
}
El resultado será: carácter: H entero: 1634496328 float: 2.72591e+20
Vemos que tanto "cadena" como los punteros "n", "c" y "v" apuntan a la misma dirección, pero cada puntero tratará la información que encuentre allí de modo diferente, para "c" es un carácter y para "n" un entero. Para "v" no tiene tipo definido, pero podemos hacer "casting" con el tipo que queramos, en este ejemplo con float.
Nota: el tipo de línea del tercer "cout" es lo que suele asustar a los no iniciados en C y C++, y se parece mucho a lo que se conoce como código ofuscado. Parece como si en C casi cualquier expresión pudiese compilar.
Punteros a estructuras:
Los punteros también pueden apuntar a estructuras. En este caso, para referirse a cada elemento de la estructura se usa el operador (->), en lugar del (.).
Ejemplo:
#include <iostream.h>
struct stEstructura {
int a, b;
} estructura, *e;
int main() {
estructura.a = 10;
estructura.b = 32;
e = &estructura;
cout << "variable" << endl;
cout << e->a << endl;
cout << e->b << endl;
cout << "puntero" << endl;
cout << estructura.a << endl;
cout << estructura.b << endl;
return 0;
}
Variables dinámicas:
Donde mayor potencia desarrollan los punteros es cuando se unen al concepto de memoria dinámica.
Cuando se ejecuta un programa, el sistema operativo reserva una zona de memoria para el código o instrucciones del programa y otra para las variables que se usan durante la ejecución. A menudo estas zonas son la misma zona, es lo que se llama memoria local. También hay otras zonas de memoria, como la pila, que se usa, entre otras cosas, para intercambiar datos entre funciones. El resto, la memoria que no se usa por ningún programa es lo que se conoce como "heap" o montón. Cuando nuestro programa use memoria dinámica, normalmente usará memoria del montón, y no se llama así porque sea de peor calidad, sino porque suele haber realmente un montón de memoria de este tipo.
C++ dispone de dos operadores para acceder a la memoria dinámica, son "new" y "delete". En C estas acciones se realizan mediante funciones de la librería estándar "mem.h".
Hay una regla de oro cuando se usa memoria dinámica, toda la memoria que se reserve durante el programa hay que liberarla antes de salir del programa. No seguir esta regla es una actitud muy irresponsable, y en la mayor parte de los casos tiene consecuencias desastrosas. No os fieis de lo que diga el compilador, de que estas variables se liberan solas al terminar el programa, no siempre es verdad. Veremos con mayor profundidad los operadores "new" y "delete" en el siguiente capítulo, por ahora veremos un ejemplo:
#include <iostream.h>
int main() {
int *a;
char *b;
float *c;
struct stPunto {
float x,y;
} *d;
a = new int;
b = new char;
c = new float;
d = new stPunto;
*a = 10;
*b = 'a';
*c = 10.32;
d->x = 12; d->y = 15;
cout << "a = " << *a << endl;
cout << "b = " << *b << endl;
cout << "c = " << *c << endl;
cout << "d = (" << d->x << ", " << d->y << ")" << endl;
delete a;
delete b;
delete c;
delete d;
return 0;
}
Y mucho cuidado: si pierdes un puntero a una variable reservada dinámicamente, no podrás liberarla.
Ejemplo:
int main() {
int *a;
a = new int; // variable dinámica
*a = 10;
a = new int; // nueva variable dinámica, se pierde la anterior
*a = 20;
delete a; // sólo liberamos la última reservada
return 0;
}
En este ejemplo vemos cómo es imposible liberar la primera reserva de memoria dinámica. Si no la necesitábamos habría que liberarla antes de reservarla otra vez, y si la necesitamos, hay que guardar su dirección, por ejemplo con otro puntero.