Bitácora del desarrollo de mi clase de C++, en el que publicaré el material de la clase y recibiré comentarios y sugerencias de mis alumnos.

lunes, 12 de marzo de 2007

8.2. Apuntadores

Apuntadores
Los apuntadores son variables que contienen direcciones de memoria como valores. Por lo regular una variable contiene directamente un valor específico. Un apuntador, por otra parte, contiene la dirección de una variable que contiene un valor específico. Un nombre de variable común se refiere directamente a un valor y un apuntador se refiere indirectamente a un valor. Los apuntadores le permiten a los programas simular llamadas por referencia, crear y manipular estructuras de datos.

Declaración e inicialización de variables de apuntador.
Como cualquier otra variable, los apuntadores deben ser declarados antes de que puedan ser utilizados. De forma general, un apuntador se declara como:

tipo *nombre_apuntador;

La declaración

int *contadorPtr, contador;

declara la variable contadorPtr del tipo int *, es decir, un apuntador a un valor entero y se lee contadorPtr es un apuntador a entero o apunta a un entero. También la variable contador se declara como tipo entero (no como apuntador).

Los apuntadores deben ser inicializados cuando son declarados o en un enunciado de asignación. Un apuntador puede ser inicializado a 0 (cero), NULL o a una dirección. Un apuntador con el valor NULL apunta a nada, NULL es una constante simbólica definida en el archivo de cabecera stdio.h. Inicializar un apuntador a 0 es equivalente a inicializar un apuntador a NULL, pero es preferible NULL. Cuando se asigna 0, primero se convierte en un apuntador al tipo apropiado. El valor 0 es el único valor entero que puede ser directamente asignado a una variable apuntador.


Operadores de apuntador
El & (ampersand), u operador de dirección, es un operador que regresa la dirección de su operando. Por ejemplo, suponiendo las declaraciones

int y=5;
int *yPtr;

el enunciado

yPtr = &y;

asigna la dirección de la variable y a la variable apuntador yPtr. Se dice entonces que la variable yPtr “apunta a y”. Suponiendo que la variable entera y se almacena en la posición de memoria 600000, y la variable de apuntador yPtr se almacena en la posición 500000, el valor de yPtr (en la posición de memoria 500000) es la dirección 600000, donde se encuentra almacenado el valor de y.

El operador * regresa el valor del objeto hacia el cual su operando apunta, el operando es un apuntador. Por ejemplo:

printf(“%d”, *yPtr);

imprime el valor de la variable y, es decir 5. *yPtr, se lee como el contenido de la dirección a la que apunta yPtr.

En el siguiente ejemplo se comprueba el uso de los operadores de apuntadores. %p se utiliza para mostrar el equivalente hexadecimal de la dirección de memoria.

#include (stdio.h)
#include (conio.h)
main( )
{
int y, *yPtr;
y=7;
clrscr();
yPtr=&y;
printf("La dirección de y es %p\n""El valor de yPtr es %p\n\n",&y, yPtr);
printf("El valor de y es %d\nEl valor de *yPtr es %d\n\n",y, *yPtr);
printf("* y & son operadores complementarios\n&*yPtr = %p\n\n*&yPtr = %p\n", &*yPtr, *&yPtr);
return 0;
}

La dirección de y es FFF4
El valor de yPtr es FFF4

El valor de y es 7
El valor de *yPtr es 7

* y & son operadores complementarios
*&yPtr = FFF4
&*yPtr = FFF4


Cómo pasar parámetros a funciones por referencia
Como ya se ha visto existen dos formas de pasar argumentos o parámetros a una función, por valor y por referencia. Muchas funciones requieren la capacidad de modificar una o más variables de la rutina que la llama. Aunque en C todas las llamadas a funciones son por valor, se pueden utilizar apuntadores para simular llamadas por referencia y no sobrecargar con copias de variables. Cuando se llama a una función con argumentos que deban modificarse, se pasan las direcciones de los argumentos, esto se lleva a cabo aplicando el operador de dirección & a la variable cuyo valor deba ser modificado. Al recibir la dirección como parámetro, la función puede utilizar el operador * para modificar el contenido de esa dirección, es decir, el valor de esa posición en la memoria del llamador. A continuación se presentan dos ejemplos para explicar la diferencia.

/* Elevar una variable al cubo utilizando llamada por valor */
#include (stdio.h)
#include (conio.h)
int cuboPorValor(int);
main( )
{
int numero=5;
clrscr();
printf("El valor original de numero es %d\n", numero);
numero = cuboPorValor(numero);
printf("El nuevo valor de numero es %d\n", numero);
return 0;
}

int cuboPorValor(int n)
{
return n * n * n;
}

Llamada por referencia:
/* Elevar una variable al cubo utilizando llamada por referencia */
#include (stdio.h)
#include (conio.h)
void cuboPorReferencia(int *);
main( )
{
int numero=5;
clrscr();
printf("El valor original de numero es %d\n", numero);
cuboPorReferencia(&numero);
printf("El nuevo valor de numero es %d\n", numero);
return 0;
}

void cuboPorReferencia(int *nPtr)
{
*nPtr= *nPtr * *nPtr * *nPtr;
}

En ambos casos la salida es:

El valor original de numero es 5
El nuevo valor de numero es 125

En el primer ejemplo se hace una asignación directa de lo que devuelve cuboPorValor a la variable numero, en el segundo ejemplo, la misma función cuboPorReferencia se encarga de modificar la variable numero, es decir, el contenido de la dirección de numero.

Como utilizar el calificador Const con apuntadores
El calificaor const permite al programador informarle al compilador que el valor de una variable particular no deberá ser modificado. En las primeras versiones de C no existía el calificador const; fue añadido al lenguaje por el comité de ANSI C.

A través de los años, una gran base de código heredado quedó escrita en las primeras versiones de C, que no utilizan a const porque no estaba disponible. Por esta razón, existen grandes oportunidades de mejoría en la ingeniería de software del código existente de C. También, muchos programadores, que en la actualidad utilizan ANSI C, en sus programas no utilizan const, porque empezaron a programar usando las primeras versiones de C. Estos programadores están perdiendo muchas oportunidades de buena ingeniería de Software.

Existen seis posibilidades para el uso (o el no uso) de const con parámetros de función, dos al pasar parámetros en llamadas por valor y cuatro al pasar parámetros con llamadas por referencia. ¿Cómo escoger una de las seis posibilidades?, deje que el principio del mínimo privilegio sea su guía. Siempre otorgue a una función suficiente acceso a los datos en sus parámetros para llevar a cabo su tarea específica, pero no más.

Existen cuatro formas para pasar un apuntador a una función: un apuntador no constante a datos no constantes, un apuntador constante a datos no constantes, un apuntador no constante a datos constantes y un apuntador constante a datos constantes. Cada una de las cuatro combinaciones proporciona un nivel distinto de privilegio de acceso.

El nivel más alto de acceso de datos se consigue mediante un apuntador no constante a datos no constantes. En este caso, los datos pueden ser modificados a través de un apuntador desreferenciado, y el apuntador puede ser modificado para señalar a otros elementos de datos. Una declaración para un apuntador no constante a datos no constantes no incluye const Tal apuntador pudiera ser utilizado para recibir una cadena como argumento a una función que utiliza aritmética de apuntadores para procesar ( y posiblemente para modificar) cada carácter dentro de la cadena. La función convertirEnMayusculas declara como su argumento un apuntador no constante a datos no constantes, llamado s (char *s).

/* convertir letras minusculas a letras mayusculas*/
/* Utilizando un apuntador no constante a datos no constantes */
#include (stdio.h)
#include (conio.h)
void convertirEnMayusculas (char *);
main ( )
{
char string []="caracteres en minusculas";
printf ("La cadena antes de la conversion es: %s\n", string);
convertirEnMayusculas (string);
printf ("La cadena despues de la conversion es: %s\n", string);
getch ();
return 0;
}
void convertirEnMayusculas (char *s)
{
while (*s != '\0')
{
if (*s)= 'a' && *s (= 'z')
*s -=32;
++s;
}
}

Un apuntador no constante a datos constantes es un apuntador que puede ser modificado para apuntar a cualquier elemento de datos del tipo apropiado, pero no pueden ser modificados los datos hacia los cuales apunta. Tal apuntador pudiera ser utilizado para recibir un argumento de arreglo a una función, que procesaría cada elemento del arreglo, sin modificar los datos. Por ejemplo la función imprimeCaracteres declara los parámetros del tipo const char *. La declaración se lee de derecha a izquierda de la forma “s es un apuntador a una constante de carácter”. El cuerpo de la función utiliza una estructura for para extraer cada carácter de la cadena, hasta que encuentre el carácter NULL. Después de haber impreso cada carácter, el apuntador s es incrementado, para que apunte al siguiente carácter dentro de la cadena.

/* Imprimiendo una cadena mediante un caracter a la vez*/
/* Utilizando un apuntador no constante a datos constantes */
#include (stdio.h)
#include (conio.h)
void imprimeCaracteres (const char *);
main ( )
{
char string []="Imprimiendo los caracteres de una cadena";
printf ("La cadena es:\n");
imprimeCaracteres (string);
putchar ('\n');
getch ( );
return 0;
}

void imprimeCaracteres (const char *s)
{
for ( ;*s != ‘\0’ ; s++)
putchar(*s);
}

En el siguiente programa se muestra los mensajes de error producidos por el compilador Borland C++ al intentar compilar una función que recibe un apuntador no constante a datos constante y la función utiliza el apuntador con el fin de modificar los datos. /* Intentando modificar datos a traves de*/ /* Un apuntador no constante a datos constantes */ #include (stdio.h) #include (conio.h) void funcion (const int *); main ( ) { int y; funcion (&y); getch ( ); return 0; } void funcion (const int *x) { *x=100; } Un apuntador constante a datos no constantes, es un apuntador que siempre apunta a la misma posición de memoria y los datos en esa posición pueden ser modificados a través del apuntador. Esta es la forma por omisión de un nombre de arreglo. Un nombre de arreglo es un apuntador constante al inicio de dicho arreglo y los subíndices del mismo. Un apuntador constante a datos no constantes puede ser utilizado para recibir un arreglo como argumento de una función, que tiene acceso a los elementos del arreglo utilizando solo notaciones de subíndices del arreglo. Los apuntadores declarados const deben ser inicializados al ser declarados (si el apuntador es un parámetro de función, será inicializado con un apuntador que se pasa a la función). El programa siguiente intenta modificar un apuntador constante. El apuntador ptr se declara ser del tipo int * const. La declaración se lee de derecha a izquierda como “ptr es un apuntador constante a un entero”. El apuntador se inicializa con la dirección de la variable entera x. El programa intenta asignar la dirección de y a ptr, por lo que se genera el mensaje de error. /* Intentando modificar un apuntador constante*/ /* a un dato no constante */ #include (stdio.h) #include (conio.h) main ( ) { int x, y; int * const ptr=&x; ptr=&y; getch ( ); return 0; } El privilegio del mínimo acceso se concede mediante un apuntador constante a datos constantes. Un apuntador de este tipo siempre apunta a la misma posición de memoria y los datos en esa posición de memoria no pueden modificados. Esta es la forma en que debería pasarse un arreglo a una función que solo ve al arreglo utilizando notación de subíndices del mismo y no modifica dicho arreglo. El siguiente programa declara la variable de apuntador ptr ser del tipo const int * const. Esta declaración se lee de derecha a izquierda como “ptr es un apuntador constante a un entero constante”. Al compilarlo se muestran los mensajes de error cuando se intenta modificar los datos a los cuales apunta ptr, y cuando se intenta modificar la dirección almacenada en la variable del apuntador. /* Intentando modificar un apuntador constante*/ /* a un dato constante */ #include (stdio.h) #include (conio.h) main ( ) { int x=7, y; const int * const ptr=&x; *ptr=5; ptr = &y; getch ( ); return 0; }

Expresiones y aritmética de apuntadores
Los apuntadores son operadores válidos en expresiones aritméticas, en expresiones de asignación y en expresiones de comparación, sin embargo no todos los operadores normalmente utilizados en estas expresiones son válidos, en conjunción con las variables de apuntador. En este tema se describen los operadores que pueden tener apuntadores como operadores y como se utilizan dichos operadores.

Con apuntadores se pueden ejecutar un conjunto limitado de operaciones aritméticas. Un apuntador puede ser incrementado (++) o decrementado (- -), se puede añadir un entero a un apuntador (+ o +=), un entero puede ser restado de un apuntador (- o -=), o un apuntador puede ser sustraído o restado de otro.

Suponga que ha sido declarado el arreglo int v[10] y su primer elemento esta en memoria en la posición 3000. Suponga que el apuntador vptr ha sido inicializado para apuntar a v[0], es decir el valor de vptr es 3000. En la figura siguiente se muestra esta situación para una máquina con enteros de 4 bytes. Note que vptr puede ser inicializado para apuntar al arreglo v con cualquiera de los siguientes enunciados:

vptr = v;
vptr = &v[0];

En aritmética convencional, la adición 3000+2 da como resultado 3002, por lo regular, este no es el caso en la aritmética de apuntadores, cuando se añade o resta un entero de un apuntador, el apuntador no se incrementa o decrementa sólo por el valor de dicho numero, sino por el entero multiplicado por el tamaño del objeto al cual el apuntador se refiere. El número de bytes depende del tipo de datos del objeto. Por ejemplo el enunciado:

vptr += 2;

produciría 3008 (3000+2*4), suponiendo un entero almacenado en 4 bytes de memoria. En el arreglo v, vptr ahora señalaría a v[2]. Si un entero se almacena en 2 bytes de memoria, entonces el cálculo anterior resultaría en una posición de memoria 3004 (3000+2*2).

Si vptr ha sido incrementado a 3016, lo que señala a v[4], el enunciado:

vptr -= 4;

definiría a vptr de vuelta a 3000 lo que es decir al principio del arreglo. Si un apuntador está siendo incrementado o decrementado por uno, pueden ser utilizados los operadores de incremento (++) y de decremento (- -). Cualquiera de los enunciados:

++vptr;
vptr++;

incrementan el apuntador, para apunte a la siguiente posición dentro del arreglo. Cualquiera de los enunciados:

--vptr;
vptr--;

decrementa el apuntador, para que apunte al elemento anterior del arreglo.

Las variables de apuntadores pueden ser restadas una de otra, por ejemplo si vptr contiene la posición 3000, y v2ptr contiene la dirección 3008, el enunciado:

x = v2ptr-vptr;

asignaría a x el número de los elementos del arreglo de vptr hasta v2ptr, en este caso sería 2. La aritmética de apuntadores no tiene efecto, a menos que se ejecute en un arreglo. No podemos suponer que dos variables del mismo tipo estén almacenadas de manera contigua en memoria, a menos de que sean elementos adyacentes de un arreglo.


Relación entre apuntadores y arreglos
Los arreglos y los apuntadores en c están relacionados en forma íntima y pueden ser utilizados casi en forma indistinta. Un nombre de arreglo puede ser considerado como un apuntador constante. Los apuntadores pueden ser utilizados para hacer cualquier operación que involucre subíndices de arreglos.

Suponga que han sido declarados el arreglo entero b[5] y la variable de apuntador entera bptr, dado que el nombre del arreglo (sin subíndice) es un apuntador al primer elemento del arreglo, podemos definir bptr igual a la dirección del primer elemento en el arreglo b, mediante el enunciado:

bptr = b;

Este enunciado es equivalente a tomar la dirección del primer elemnto del arreglo, como se muestra a continuación:

bptr = &b[0];

Alternativamente el elemento del arreglo b[3] puede ser referenciado con la expresión de apuntador:

*(bptr+3)

El 3 en la expresión arriba citada es el desplazamiento del apuntador, cuando el apuntador apunta al principio de un arreglo, el desplazamiento indica qué elemento del arreglo debe ser referenciado, y el valor de desplazamiento es idéntico al subíndice del arreglo. La notación anterior se conoce como notación apuntador/desplazamiento. Son necesarios los paréntesis porque la precedencia de * es más alta que la de +, sin los paréntesis la expresión arriba citada sumaría 3 al valor de la expresión *bptr (es decir, se añadirían 3 a b[0], suponiendo que bptr apunta al principio del arreglo). Al igual que el elemento del arreglo puede ser referenciado con una expresión de apuntador, la dirección &b[3] puede ser escrita con la expresión de apuntador bptr+3.

El arreglo mismo puede ser tratado como un apuntador y utilizado en aritmética de apuntador, por ejemplo la expresión:

*(b+3)

también se refiere al elemento del arreglo b[3], en general, todas las expresiones de arreglos con subíndices pueden ser escritas mediante un apuntador y un desplazamiento, en este caso se utilizó notación apuntador/desplazamiento junto con el nombre del arreglo como apuntador. Note que el enunciado anterior no cambia de forma alguna el nombre del arreglo; b aún apunta al primer elemento del arreglo.

Los apuntadores pueden tener subíndices exactamente de la misma forma que los arreglos, por ejemplo la expresión:

bptr[1]

se refiere al elemento del arreglo b[1], esto se conoce como notación apuntador/subíndice, recuerde que el nombre de un arreglo es, en esencia un apuntador constante, siempre apunta al principio del arreglo, por lo tanto la expresión:

b+=3

resulta inválida, porque intenta modificar el valor del nombre de un arreglo con aritmética de apuntador.

El programa siguiente utiliza los cuatro métodos analizados aquí para referir a los elementos del arreglo, arreglos con subíndices, apuntador/desplazmiento con el nombre del arreglo como un apuntador, subíndice de apuntador y apuntador/desplzamiento con un apuntador para imprimir los cuatro elementos del arreglo entero b.

/* Usando subindices y notación de apuntadores con arreglos */
#include (stdio.h)
#include (conio.h)
main ( )
{
int i, despl, b[ ]={10, 20, 30, 40};
int *bptr=b;
printf(“Arreglo b impreso con notacion de subindices de arreglos\n”);
for (i=0; i(=3; i++)
printf(“b[%d] = %d\n”, i, b[i]); printf(“\nNotacion apuntador/desplazamiento cuando\nel apuntador es el nombre del arreglo\n”); for (despl=0; despl<=3; despl++) printf(“*(b+%d) = %d\n”, despl, *(b+despl)); printf(“\nUsando notacion de subindices de apuntador\n”); for (i=0; i<=3; i++) printf(“bptr[%d] = %d\n”, i, bptr[i]); printf(“\nUsando notacion de apuntador/desplazamiento\n”); for (despl=0; despl<=3; despl++) printf(“*(bptr+%d) = %d\n”, despl, *(bptr+despl)); getch ( ); return 0; } Arreglos de apuntadores
Los arreglos pueden contener apuntadores. Un uso común para una estructura de datis como ésta, es formar un arreglo de cadenas, conocida como un arreglo de cadenas. Cada entrada en el arreglo es una cadena, pero en C una cadena es en esencia un apuntador a su primer carácter. Por lo que en un arreglo de cadenas cada entrada es de hecho un apuntador al primer carácter de cada una de las cadenas. Veamos la declaración del arreglo de cadenas figuras, que pudiera ser útil para representar un mazo de naipes.

char *figuras[4]={“Corazones”, “Diamantes”, “Treboles”, “Espadas”};

La porción figuras[4] de la declaración indica un arreglo de cuatro elementos. La porción char * de la declaración indica que cada elemento del arreglo figuras es del tipo “apuntador a char”. Los cuatro valores a colocarse en el arreglo son “Corazones”, “Diamantes”, “Treboles” y “Espadas”. Cada una de estas está almacenada en memoria como una cadena de caracteres terminada por NULL, de una longitud de un carácter más largo que en número de caracteres entre comillas. Las cuatro cadenas son de 10, 10, 9 y 8 caracteres de longitud, respectivamente. Aunque pareciera como si estas cadenas están colocadas en el arreglo figuras, de hecho en el arreglo sólo están almacenados los apuntadores.

Cada apuntador señala al primer carácter de su cadena correspondiente. Por lo que, aunque el arreglo figuras es de tamaño fijo, permite el acceso a cadenas de caracteres de cualquier longitud. Esta flexibilidad es un ejemplo de las propiedades poderosas de estructuración de datos en C.

Práctica de apuntadores
Planteamiento: Se pretende simular el barajado y repartición de una baraja americana.

El primer asunto que hay que resolver es la representación de la baraja dentro del programa. Una de las maneras de representarla es con un arreglo bidimensional, donde el renglón sea la figura de la baraja (palo) y la columna represente el número o cara de cada carta.

int baraja[4][13];

de esta manera se tiene un arreglo de 52 elementos (4 * 13) para representar cada una de las cartas de la baraja, el número que almacenará cada carta, que es de tipo entero, representará la posición en la que se encuentran las cartas una vez que se ha barajado.

El siguiente punto será resolver las acciones de barajar y de repartir.

El proceso de barajar debe ser aleatorio, es decir, para cada carta que se vaya revolviendo, o cada posición, debe obtenerse un número aleatorio entre 0 y 4 para la figura y otro entre 0 y 12 para el número de la carta que se está acomodando. Todas las cartas deben iniciarse en 0 antes de barajar, y cuando se haya barajado, cada carta (cada elemento del arreglo) tendrá el número de posición en el que quedó ordenada la carta.

La función para revolver la baraja sería de la siguiente forma:

void barajar(int vBaraja[][13])
{
int carta, figura, cara;
for (carta = 1; carta <= 52; carta++)
{
figura = rand() % 4;
cara = rand() % 13;

/*Se verifica que la carta esté en 0, es decir que no haya
salido */
while (vBaraja[figura][cara] != 0)
{
figura = rand() % 4;
cara = rand() % 13;
}
/* al elemento del arreglo se le asigna su orden correspondiente */
vBaraja[figura][cara] = carta;
}
}


Una vez que se ha barajado, se requiere interpretar el proceso de repartición, en este caso, la repartición se hará a “un solo jugador”, en el orden en el que quedaron las cartas después de barajar. La manera como se hará la repartición consiste en un ciclo que irá de 1 a 52 (para cada carta), y dentro un doble ciclo para recorrer el arreglo en ambos sentidos, hasta localizar el elemento correspondiente a la carta que sigue en el orden que quedaron después de barajar. Si el elemento es el correspondiente a la posición, entonces se imprimirá.

void repartir(const int vBaraja[][13], const char *vCara[],
const char *vFigura[])
{
int carta, figura, cara;
for (carta = 1; carta <= 52; carta++)
for (figura = 0; figura<=3; figura ++)
for (cara = 0; cara <=12; cara ++)
if (vBaraja[figura][cara] == carta)
printf("%7s de %-10s%c", vCara[cara], vFigura[figura],
carta % 2 == 0 ? '\n':'\t');
}

La función que reparte no es del todo óptima, puesto que ningún ciclo termina cuando encuentra la carta que sigue, sino que de cualquier manera recorre la baraja en todos sus renglones (figuras) y todas sus columnas (números).

Se utilizan también dos arreglos auxiliares para darle nombre a los palos de la baraja y a los números para poder imprimirlos de manera presentable.

El programa completo se muestra a continuación:

/* Simulacion de barajar y repartir una baraja */
#include (stdio.h)
#include (stdlib.h)
#include (time.h)
#include (conio.h)

void barajar(int [][13]);
void repartir(const int [][13], const char *[], const char *[]);

main( )
{
char *figuras[4]={"Corazones","Diamantes","Treboles","Espadas"};
char *caras[13]={"As","Dos","Tres","Cuatro","Cinco","Seis","Siete",
"Ocho","Nueve","Diez","Jota","Reina","Rey"};
int baraja[4][13] = {0};
clrscr();

srand(time(NULL)); /*generación de semilla para números aleatorios*/
barajar(baraja);
repartir(baraja, caras, figuras);
return(0);
}

void barajar(int vBaraja[][13])
{
int carta, figura, cara;
for (carta = 1; carta <= 52; carta++)
{
figura = rand() % 4;
cara = rand() % 13;
while (vBaraja[figura][cara] != 0)
{
figura = rand() % 4;
cara = rand() % 13;
}
vBaraja[figura][cara] = carta;
}
}


void repartir(const int vBaraja[][13], const char *vCara[],
const char *vFigura[])
{
int carta, figura, cara;
for (carta = 1; carta <= 52; carta++)
for (figura = 0; figura<=3; figura ++)
for (cara = 0; cara <=12; cara ++)
if (vBaraja[figura][cara] == carta)
printf("%7s de %-10s%c", vCara[cara], vFigura[figura],
carta % 2 == 0 ? '\n':'\t');
}

Asignación dinámica de arreglos
Cuando se define cada variable en un programa, se le asigna suficiente especio de almacenamiento a partir de un conjunto de ubicaciones de memoria en la computadora, disponibles al compilador. Una vez que se reservan ubicaciones específicas para una variable en memoria, estas ubicaciones quedan fijas por el tiempo que dure esa variable, ya sea que se emplee o no. Por ejemplo, si una función requiere espacio de almacenamiento para un arreglo de 500 números enteros, el espacio reservado para el arreglo se asigna y se fija a partir del punto de definición del arreglo. Si la aplicación requiere menos de 500 números enteros, el espacio de almacenamiento sin usar no se devolverá al sistema hasta que el arreglo deje de existir. En cambio, si la aplicación requiere más de 500 números enteros, el tamaño del arreglo en número entero debe aumentarse y la función que define el arreglo debe compilarse de nuevo.

Una alternativa a esta asignación fija o estática de las ubicaciones de almacenamiento de memoria es la asignación dinámica de memoria. En un esquema de asignación dinámica, la cantidad de espacio de almacenamiento que debe asignarse se determina y ajusta al ejecutar el programa, en vez de fijarlo al momento de compilar.

La asignación dinámica de memoria es sumamente útil para manejar listas, pues permite ampliarlas cuando se agregan elementos o reducirlas si se eliminan. Por ejemplo, al elaborar una lista de calificaciones es posible que no se sepa el número exacto de calificaciones necesarias, en lugar de crear un arreglo fijo para almacenar las calificaciones, resulta mucho más útil tener un mecanismo mediante el cual el arreglo se pueda incrementar o reducir según sea necesario. Dos operadores en C++, new y delete, ofrecen esta capacidad, y se explican a continuación:

new.- reserva el número de bytes solicitado por la declaración, devuelve la dirección de la primer ubicación reservada, o NULL si no hay suficiente memoria disponible.

delete.- desocupa un bloque de bytes previamente reservado, la dirección de la primer ubicación reservada se transmite como un argumento a la función.

La solicitud explícita de espacio de almacenamiento dinámico para variables o arreglos a escala se hacen como parte de una instrucción declaratoria a una de asignación. Por ejemplo, la instrucción declaratoria *num=new int; reserva espacio suficiente para contener un número entero y coloca la dirección de dicho espacio en el apuntador num. Esta misma asignación dinámica se puede lograr al declarar primero el apuntador que emplea la instrucción declaratoria int *num; y subsecuentemente asignar al apuntador una dirección con la instrucción num=new int. En cualquier caso el espacio de almacenamiento asignado viene del espacio libre en la memoria de la computadora.

Un método similar y más útil es la asignación dinámica de arreglos, por ejemplo, la declaración:

Int *calif=new int[200];

Reserva suficiente espacio de almacenamiento para 200 números enteros y coloca la dirección del primer número entero en el apuntador calif. En este ejemplo se usó la constante 200, pero pudo emplearse una dimensión variable, como se describe en el siguiente ejemplo:

/* Utilizando memoria dinamica con new y delete*/
#include (stdio.h)
#include (conio.h)

main ( )
{
int numcalif, i,*calif;
printf ("Introduce el numero de calificaciones a procesar:");
scanf ("%d",&numcalif);
calif=new int[numcalif];
for (i=0; i(numcalif;i++)
{
printf ("Introduce la calificacion [%d]:",i+1);
scanf ("%d",&calif[i]);
}
printf ("\nSe creo un arreglo para %d numeros enteros\n",numcalif);
printf ("Los valores almacenados son:\n");
for (i=0; i(numcalif;i++)
printf ("%d\n",calif[i]);

getch ();
delete calif;
return 0;
}

0 comentarios:

Publicar un comentario

Suscribirse a Enviar comentarios [Atom]

<< Inicio