Sunday, January 08, 2006

Apuntadores C++

APUNTADORESC++

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.
Todo esto sucede en el interior de la máquina, y nos importa más bien poco. 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" << endl;
Ahora veremos cómo funcionan los punteros. Un puntero es un tipo especial de variable que contiene, ni más ni menos que, una dirección de memoria. Por supuesto, a partir de esa dirección de memoria puede haber cualquier tipo 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.
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:
*;
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:
* ;
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 *;
Estos punteros pueden apuntar a objetos de cualquier tipo.
Por supuesto, también se puede emplear el "casting" con punteros, sintaxis:
( *)
Por ejemplo: #include 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: Hentero: 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 struct stEstructura { int a, b; } estructura, *e; int main() { estructura.a = 10; estructura.b = 32; e = &estructura; cout << "variable" <<>a <<>b << endl; cout << "puntero" << endl; cout << estructura.a << endl; cout << estructura.b << endl; return 0; }
EJEMPLOS:
Veamos algunos ejemplos de cómo trabajan los punteros.
Primero un ejemplo que ilustra la diferencia entre un array y un puntero: #include int main() { char cadena1[] = "Cadena 1"; char *cadena2 = "Cadena 2"; cout << cadena1 << endl; cout << cadena2 << endl; //cadena1++; // Ilegal, cadena1 es constante cadena2++; // Legal, cadena2 es un puntero cout << cadena1 << endl; cout << cadena2 << endl; cout << cadena1[1] << endl; cout << cadena2[0] << endl; cout << cadena1 + 2 << endl; cout << cadena2 + 1 << endl; cout << *(cadena1 + 2) << endl; cout << *(cadena2 + 1) << endl; }
Aparentemente, y en la mayoría de los casos, cadena1 y cadena2 son equivalentes, sin embargo hay operaciones que están prohibidas con los arrays, ya que son punteros constantes.
Otro ejemplo:#include int main() { char Mes[][11] = { "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"}; char *Mes2[] = { "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"}; cout << "Tamaño de Mes: " << sizeof(Mes) << endl; cout << "Tamaño de Mes2: " << sizeof(Mes2) << endl; cout << "Tamaño de cadenas de Mes2: " << &Mes2[11][10]-Mes2[0] << endl; cout << "Tamaño de Mes2 + cadenas : " << sizeof(Mes2)+&Mes2[11][10]-Mes2[0] << endl; return 0; }
En este ejemplo declaramos un array "Mes" de dos dimensiones que almacena 12 cadenas de 11 caracteres, 11 es el tamaño necesario para almacenar el mes más largo (en caracteres): "Septiembre".
Después declaramos "Mes2" que es un array de punteros a char, para almacenar la misma información. La ventaja de este segundo método es que no necesitamos contar la longitud de las cadenas para calcular el espacio que necesitamos, cada puntero de Mes2 es una cadena de la longitud adecuada para almacenar cada mes.
Parece que el segundo sistema es más económico en cuanto al uso de memoria, pero hay que tener en cuenta que además de las cadenas también se almacenan los doce punteros.
El espacio necesario para almacenar los punteros lo dará la segunda línea de la salida. Y el espacio necesario para las cadenas lo dará la tercera línea.
Si las diferencias de longitud entre las cadenas fueran mayores, el segundo sistema sería más eficiente en cuanto al uso de la memoria.
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 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 << b = " << *b << endl; cout << " c = " << *c << endl; cout << " d =" (">x << ", " <<>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.

OPERADORES . Y ->
Operador de selección (.). Permite acceder a variables o campos dentro de una estructura.
Sintaxis:
.
Operador de selección de variables o campos para estructuras referenciadas con punteros. (->)
Sintaxis:
->
OPERADORES DE MANEJO DE MEMORIA "new" Y "delete"
Veremos su uso en el capítulo de punteros II y en mayor profundidad en el capítulo de clases y en operadores sobrecargados.
Operador new:
El operador new sirve para reservar memoria dinámica.
Sintaxis:
[::] new [] [()]
[::] new [] () [()]
[::] new [] []
[::] new [] ()[]
El operador opcional :: está relacionado con la sobrecarga de operadores, de momento no lo usaremos. Lo mismo se aplica a emplazamiento.
La inicialización, si aparece, se usará para asignar valores iniciales a la memoria reservada con new, pero no puede ser usada con arrays.
Las formas tercera y cuarta se usan para reservar memoria para arrays dinámicas. La de la memoria reservada con new será válida hasta que se libere con delete o hasta el fin del programa. Aunque es aconsejable liberar siempre la memoria reservada con new usando delete. Se considera una práctica sospechosa no hacerlo.
Si la reserva de memoria no tuvo éxito, new devuelve un puntero nulo, NULL.
Operador delete:
El operador delete se usa para liberar la memoria dinámica reservada con new.
Sintaxis: [::]
delete []
[::] delete[] []
La expresión será normalmente un puntero, el operador delete[] se usa para liberar memoria de arrays dinámicas.
Es importante liberar siempre usando delete la memoria reservada con new. Existe el peligro de pérdida de memoria si se ignora esta regla (memory leaks).
Cuando se usa el operador delete con un puntero nulo, no se realiza ninguna acción. Esto permite usar el operador delete con punteros sin necesidad de preguntar si es nulo antes.
Nota: los operadores new y delete son propios de C++. En C se usan las funciones malloc y free para reservar y liberar memoria dinámica y liberar un puntero nulo con free suele tener consecuencias desastrosas.
Veamos algunos ejemplos: int main() { char *c; int *i = NULL; float **f; int n; c = new char[123]; // Cadena de 122 caracteres f = new float *[10]; // Array de 10 punteros a float for(n = 0; n < 10; n++) f[n] = new float[10]; // Cada elemento del array es un array de 10 float // f es un array de 10*10 f[0][0] = 10.32; f[9][9] = 21.39; c[0] = 'a'; c[1] = 0; // liberar memoria dinámica for(n = 0; n < 10; n++) delete[] f[n]; delete[] f; delete[] c; delete i; return 0; }

PUNTEROS COMO PARÁMETROS DE FUNCIONES:
Cuando pasamos como parámetro por valor de una función un puntero pasa lo mismo que con las variables. Dentro de la función trabajamos con una copia del puntero. Sin embargo, el objeto apuntado por el puntero será el mismo, los cambios que hagamos en los objetos apuntados por el puntero se conservarán al abandonar la función, sin embargo los cambios que hagamos al propio puntero no.
Ejemplo: #include void funcion(int *q); int main() { int a; int *p; a = 100; p = &a; // Llamamos a funcion con un puntero funcion(p); cout << "Variable a: " << a << endl; cout << "Variable *p: " << *p << endl; // Llamada a funcion con la dirección de "a" (constante) funcion(&a); cout << "Variable a: " << a << endl; cout << "Variable *p: " << *p << endl; return 0; } void funcion(int *q) { // Cambiamos el valor de la variable apuntada por // el puntero *q += 50; q++; }
Con este tipo de parámetro para función pasamos el puntero por valor. ¿Y cómo haríamos para pasar un puntero por referencia?:
void funcion(int* &q); El operador de referencia siempre se pone junto al nombre de la v

No comments: