0% encontró este documento útil (0 votos)
112 vistas8 páginas

Uso de Variables Dinámicas en C++

1) Los punteros son variables que contienen las direcciones de memoria de otras variables y permiten acceder y modificar los valores almacenados en esas direcciones. 2) Se pueden obtener las direcciones de variables usando el operador & y acceder a los valores usando el operador *. 3) Los arrays se almacenan de forma contigua en memoria y el nombre del array es equivalente a un puntero a su primer elemento.
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
112 vistas8 páginas

Uso de Variables Dinámicas en C++

1) Los punteros son variables que contienen las direcciones de memoria de otras variables y permiten acceder y modificar los valores almacenados en esas direcciones. 2) Se pueden obtener las direcciones de variables usando el operador & y acceder a los valores usando el operador *. 3) Los arrays se almacenan de forma contigua en memoria y el nombre del array es equivalente a un puntero a su primer elemento.
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

VARIABLES DINÁMICAS

En la mayoría de los lenguajes de alto nivel actuales existe la posibilidad de trabajar con
variables dinámicas, que son aquellas que se crean en tiempo de ejecución. Para soportar
el empleo de estas variables aparecen los conceptos de puntero y referencia que están
íntimamente relacionados con el concepto de dirección física de una variable.

Punteros y direcciones
Ya hemos mencionado que el C++ es un lenguaje que pretende acercarse mucho al nivel
de máquina por razones de eficiencia, tanto temporal como espacial. Por está razón el
C++ nos permite controlar casi todos los aspectos de la ocupación y gestión de memoria
de nuestros programas (sabemos lo que ocupan las variables, podemos trabajar con
direcciones, etc.).
Uno de los conceptos fundamentales en este sentido es el de puntero. Un puntero es una
variable que apunta a la dirección de memoria donde se encuentra otra variable. La clave
aquí está en la idea de que un puntero es una dirección de memoria.
Pero ¿cómo conocemos la dirección de una variable declarada en nuestro programa? La
solución a esto está en el uso del operador de referencia (&), ya mencionado al hablar de
los operadores de indirección. Para obtener la dirección de una variable solo hay aplicarle
el operador de referencia (escribiendo el símbolo & seguido de la variable), por ejemplo:
int i = 2; int *pi = &i; // ahora pi contiene la
dirección de la variable i.

El operador de indirección sólo se puede ser aplicado a variables y funciones, es decir, a


LValues. Por tanto, sería un error aplicarlo a una expresión (ya que no tiene dirección).
Por otro lado, para acceder a la variable apuntada por un puntero se emplea el operador
de indirección (*) poniendo el * y después el nombre del puntero:
int j = *pi; // j tomaría el valor 2, que es el contenido de la variable
i anterior

Para declarar variables puntero ponemos el tipo de variables a las que va a apuntar y el
nombre del puntero precedido de un asterisco. Hay que tener cuidado al definir varios
punteros en una misma línea, ya que el asterisco se asocia al nombre de la variable y no
al tipo. Veamos esto con un ejemplo:
char *c, d, *e; // c y e son punteros a carácter, pero d es una variable
carácter
Como los punteros son variables podemos emplearlos en asignaciones y operaciones,
pero hay que diferenciar claramente entre los punteros como dato (direcciones) y el
valor al que apuntan. Veremos esto con un ejemplo, si tenemos los punteros:
int *i,

*j; La
operación:
i= j;

hace que i y j apunten a la misma dirección de memoria, pero la operación:


*i = *j;
hace que lo apuntado por i pase a valer lo mismo que lo apuntado por j, es decir, los
punteros no han cambiado, pero lo que contiene la dirección a la que apunta i vale lo
mismo que lo que contiene la dirección a la que apunta j. Es decir, si i y j apuntaran a las
variables enteras a y b respectivamente:
int
a, b;
int
*i =
&a;
int
*j =
&b;

lo anterior sería equivalente a:


a= b;

Por último indicaremos que hay que tener mucho cuidado para no utilizar punteros sin
inicializar, ya que no sabemos cuál puede ser el contenido de la dirección indefinida que
contiene una variable puntero sin inicializar.

El puntero NULL
Siempre que trabajamos con punteros solemos necesitar un valor que nos indique que el
puntero es nulo (es decir, que no apuntamos a nada). Esto se consigue dándole al puntero
el valor 0 o NULL. NULL no es una palabra reservada, sino que se define como una
macro en las
cabeceras estándar <stddef.h> y <stdlib.h>, y por tanto será necesario incluirlas para
usarlo. Si no queremos usar las cabeceras podemos definirlo nosotros de alguna de las
siguientes formas:
#define NULL (0)
#define NULL (0L)
#define NULL ((void *) 0)

la primera y segunda formas son válidas porque 0 y 0L tienen conversión implícita a


puntero, y la tercera es válida porque convertimos explícitamente el 0 a puntero void.
Una forma adecuada de definir NULL es escribiendo:
#ifndef NULL
#define NULL ((void *) 0)
#endif

que define NULL sólo si no está definido (podría darse el caso de que nosotros no
incluyéramos las cabeceras que definen NULL, pero si se hiciese desde alguna otra
cabecera que si hemos incluido).
Cualquier indirección al puntero NULL se transforma en un error de ejecución.

Punteros void
Ya hemos mencionado que el C++ define un tipo especial denominado void (vacío), que
utilizábamos para indicar que una función no retorna nada o no toma parámetros. Además
el tipo void se emplea como base para declarar punteros a variables de tipo desconocido.
Debemos recordar que no se pueden declarar variables de tipo void, por lo que estos
punteros tendrán una serie de restricciones de uso.
Un puntero void se declara de la forma normal:
void *ptr;

y se usa sólo en asignaciones de punteros. Para trabajar con los datos apuntados por un
puntero tendremos que realizar conversiones de tipo explícitas (casts):
char a; char *p
= (char *) ptr;
a = *p;

¿Cuál es la utilidad de estos punteros si sólo se pueden usar en asignaciones? Realmente


se emplean para operaciones en las que no nos importa el contenido de la memoria
apuntada, sino sólo la dirección, como por ejemplo en las funciones estándar de C para
manejo de memoria dinámica (que también son válidas en C++, aunque este lenguaje ha
introducido un operador que se encarga de lo mismo y que es más cómodo de utilizar).
La definición de algunas de estas funciones es:
void *malloc (size_t N); // reserva N bytes de memoria
void free (void *); // libera la memoria reservada
con malloc

/* size_t es un tipo que se usa para almacenar tamaños de memoria definido


en las cabeceras estándar mediante un typedef, generalmente lo consideraremos
un entero sin más */ y para usarlas hacemos:
void *p; p= malloc (1000); // reservamos 1000 bytes y asignamos
a p la dirección del
// primer byte free(p);
// liberamos la memoria asignada con malloc.

Aunque podemos ahorrarnos el puntero void haciendo una conversión explícita del
retorno de la función:
long *c;
c=(long *)malloc(1000); // aunque se transforma en puntero a long sigue
reservando
// 1000 bytes, luego si cada long ocupa 4 bytes
sólo nos
// cabrán 250 variables de tipo long

Por último señalar que en todas las ocasiones en las que hemos hecho conversiones con
punteros hemos usado él método de C y no el de C++:
c = long *(malloc (1000));

ya que esto último da error de compilación. La solución a este problema es definir tipos
puntero usando typedef:
typedef long
*ptr_long; c =
ptr_long(malloc(100
0));

Aritmética con punteros


Las variables de tipo puntero contienen direcciones, por lo que todas ellas ocuparan la
misma memoria: tantos bytes como sean necesarios para almacenar una dirección en el
computador sobre el que trabajemos. De todas formas, no podemos declarar variables
puntero sin especificar a qué tipo apuntan (excepto en el caso de los punteros void, ya
mencionados), ya que no es sólo la dirección lo que nos interesa, sino que también
debemos saber que es lo apuntado para los chequeos de tipos cuando dereferenciamos (al
tomar el valor apuntado para usarlo en una expresión) y para saber que cantidades
debemos sumar o restar a un puntero a la hora de incrementar o decrementar su valor.
Es decir, el incremento o decremento de un puntero en una unidad se traduce en el
incremento o decremento de la dirección que contiene en tantas unidades como bytes
ocupan las variables del tipo al que apunta.

Además de en sumas y restas los punteros se pueden usar en comparaciones (siempre y


cuando los punteros a comparar apunten al mismo tipo). No podemos multiplicar ni
dividir punteros.

Punteros y parámetros de funciones


Lo único que hay que indicar aquí es que los punteros se tratan como las demás variables
al ser empleadas como parámetro en una función, pero tienen la ventaja de que podemos
poner como parámetro actual una variable del tipo al que apunta el puntero a la que
aplicamos el operador de referencia. Esta frase tan complicada se comprende mejor con
un ejemplo, si tenemos una función declarada como:
void f (int *);

es decir, una función que no retorna nada y recibe como parámetro un puntero a entero,
podemos llamarla con:
int
i;
f(&i)
;

Lo que f recibirá será el puntero a la variable i.

Punteros y arrays
La relación entre punteros y arrays en C++ es tan grande que muchas veces se emplean
indistintamente punteros y vectores. La relación entre unos y otros se basa en la forma de
tratar los vectores. En realidad, lo que hacemos cuando definimos un vector como:
int v[100];

es reservar espacio para 100 enteros. Para poder acceder a cada uno de los elementos
ponemos el nombre del vector y el índice del elemento al que queremos acceder:
v[8] = 100;

Pero ¿cómo gestiona el compilador los vectores?. En realidad, el compilador reserva un


espacio contiguo en memoria de tamaño igual al número de elementos del vector por el
número de bytes que ocupan los elementos del array y guarda en una variable la dirección
del primer elemento del vector. Para acceder al elemento i lo que hace el compilador es
sumarle a la primera dirección el número de índice multiplicado por el tamaño de los
elementos del vector. Esta es la razón de que los vectores comiencen en 0, ya que la
primera dirección más cero es la dirección del primer elemento.
Hemos comentado todo esto porque en realidad esa variable que contiene la dirección del
primer elemento de un vector es en realidad un puntero a los elementos del vector y se
puede utilizar como tal. La variable puntero se usa escribiendo el nombre de la variable
array sin el operador de indexado (los corchetes):
v [0] = 1100;

es igual que:
*v = 1100;

Para acceder al elemento 8 hacemos:


*(v + 8)

Es decir, la única diferencia entre declarar un vector y un puntero es que la primera opción
hace que el compilador reserve memoria para almacenar elementos del tipo en tiempo de
compilación, mientras que al declarar un puntero no reservamos memoria para los datos
que va a apuntar y sólo lo podremos hacer en tiempo de ejecución (con los operadores
new y delete). Excepto por la reserva de memoria y porque no podemos modificar el valor
de la variable de tipo vector (no podemos hacer que apunte a una distinta a la que se ha
reservado para ella), los vectores y punteros son idénticos.
Todo el tiempo hemos hablado sobre vectores, pero refiriéndonos a vectores de una
dimensión, los vectores de más de una dimensión se acceden sumando el valor del índice
más a la derecha con el segundo índice de la derecha por el número de elementos de la
derecha, etc. Veamos como acceder mediante punteros a elementos de un vector de dos
dimensiones:
int mat[4][8];

*(mat + 0*8 + 0) // accedemos a mat[0][0]


*(mat + 1*8 + 3) // accedemos a mat[1][3]
*(mat + 3*8 + 7) // accedemos a mat[3][7] (último elemento de la matriz)

Por último mencionar que podemos mezclar vectores con punteros (el operador de vector
tiene más precedencia que el de puntero, para declarar punteros a vectores hacen falta
paréntesis).
Ejemplos:
int (*p)[20]; // puntero a un vector de 20 enteros
int p[][20]; // igual que antes, pero p no se puede
modificar int *p[20]; // vector de 20 punteros a
entero

Operadores new y delete


Hemos mencionado que en C se usaban las funciones malloc() y free() para el manejo
de memoria dinámica, pero dijimos que en C++ se suelen emplear los operadores new y
delete.

El operador new se encarga de reservar memoria y delete de liberarla. Estos operadores


se emplean con todos los tipos del C++, sobre todo con los tipos definidos por nosotros
(las clases). La ventaja sobre las funciones de C de estos operadores está en que utilizan
los tipos como operandos, por lo que reservan el número de bytes necesarios para cada
tipo y cuando reservamos más de una posición no lo hacemos en base a un número de
bytes, sino en función del número de elementos del tipo que deseemos.
El resultado de un new es un puntero al tipo indicado como operando y el operando de un
delete debe ser un puntero obtenido con new.

Veamos con ejemplos como se usan estos operadores:


int * i = new int; // reservamos espacio para un entero, i apunta

a él delete i; // liberamos el espacio reservado para i

int * v = new int[10]; // reservamos espacio contiguo para 10 enteros, v


apunta
// al primero delete []v;

// Liberamos el espacio reservado para v

/*
En las versiones del ANSI C++ 2.0 y anteriores el delete se debía poner
como:

delete [10]v; // Libera espacio para 10 elementos del


tipo de v */

Hay que tener cuidado con el delete, si ponemos:


delete v;
sólo liberamos la memoria ocupada por el primer elemento del vector, no la de los 10
elementos.
Con el operador new también podemos inicializar la variable a la vez que reservamos la
memoria:
int *i = new int (5); // reserva espacio para un entero y le asigna el
valor

En caso de que se produzca un error al asignar memoria con new, se genera una llamada
a la función apuntada por
void (* _new_handler)(); // puntero a función que no retorna nada y
no tiene // parámetros

Este puntero se define en la cabecera <new.h> y podemos modificarlo para que apunte a
una función nuestra que trate el error. Por ejemplo:
#include <new.h>

void
f() {
...
cout << "Error asignando memoria"
<< endl; ... }

void main
() { ...
_new_handler
= f; ...
}

Punteros y estructuras
Podemos realizar todas las combinaciones que queramos con punteros y estructuras:
podemos definir punteros a estructuras, campos de estructuras pueden ser punteros,
campos de estructuras pueden ser punteros a la misma estructura, etc.
La particularidad de los punteros a estructuras está en que el C++ define un operador que
a la vez que indirecciona el puntero a la estructura accede a un campo de la misma. Este
operador es el signo menos seguido de un mayor que (->). Veamos un ejemplo:
struct
dos_enteros {
int i1; int
i2; };
dos_enteros
*p;

(*p).i1 = 10; // asignamos 10 al campo i1 de la estructura apuntada


por p p->i2 = 20; // asignamos 20 al campo i2 de la estructura
apuntada por p

A la hora de usar el operador -> lo único que hay que tener en cuenta es la precedencia
de operadores. Ejemplo:
++p->i1; // preincremento del campo i1, es como poner ++ (p-
>i1) (++p)->i1; // preincremento de p, luego acceso a i1 del nuevo
p.

Por último diremos que la posibilidad de definir campos de una estructura como punteros
a elementos de esa misma estructura es la que nos permite definir los tipos recursivos
como los nodos de colas, listas, árboles, etc.

Punteros a punteros
Además de definir punteros a tipos de datos elementales o compuestos también
podemos definir punteros a punteros. La forma de hacerlo es poner el tipo y luego tantos
asteriscos como niveles de indirección:
int *p1; // puntero a entero int
**p2; // puntero a puntero a
entero char *c[]; // vector de
punteros a carácter

Para usar las variables puntero a puntero hacemos lo mismo que en la declaración, es
decir, poner tantos asteriscos como niveles queramos acceder:
int ***p3; // puntero a puntero a puntero a entero

p3 = &p2; // trabajamos a nivel de puntero a puntero a puntero a


entero // no hay indirecciones, a p3 se le asigna un
valor de su mismo tipo
*p3 = &p1; // el contenido de p2 (puntero a puntero a entero) toma la
dirección de
// p1 (puntero a entero). Hay una indirección, accedemos a lo
apuntado
// por p3 p1 = **p3; // p1 pasa a valer lo apuntado
por lo apuntado por p3 (es decir, lo // apuntado por
p2). En nuestro caso, no cambia su valor, ya que p2
// apuntaba a p1 desde la operación anterior
***p3 = 5 // El entero apuntado por p1 toma el valor 5 (ya que p3
apunta a p2 que // apunta a p1)

Como se ve, el uso de punteros a punteros puede llegar a hacerse muy complicado, sobre
todo teniendo en cuenta que en el ejemplo sólo hemos hecho asignaciones y no
incrementos o decrementos (para eso hay que mirar la precedencia de los operadores).

También podría gustarte