Apuntadores

Anuncio
1
Capítulo 1. Apuntadores
1.1 Conceptos básicos.
Para entender qué es un puntero veremos primero cómo se almacenan los datos en un computador. La
memoria de un computador 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 tiene una dirección, llamada
normalmente dirección de memoria.
1.1.1 Definición de Apuntadores
Un apuntador es una variable, que almacena como contenido una dirección de memoria, de otra
variable a la que apunta, dicha dirección representa el lugar donde se almacena un dato. Los
apuntadores tienen un tipo de dato específico y solo pueden apuntar a espacios de memoria con datos
del mismo tipo. 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.
Con los apuntadores es posible manipular estructuras de datos o asignar memoria dinámica.
Los apuntadores son una de las herramientas más poderosas con que cuenta el Lenguaje C++.
Desafortunadamente, muchos programadores han creado el mito de que el estudio de los apuntadores
es muy complicado, lo cual ha desarrollado una fobia entre quienes se inician en el estudio de las
estructuras dinámicas en lenguaje C/C++.
1.1.2 Declaración de apuntadores
Los apuntadores son variables automáticas cuyos valores representan direcciones de memoria
correspondientes a otras variables.
La sintaxis para la declaración de un apuntador es la siguiente:
tipo * identificador ;
Ejemplo: int *apunt; // Declaración del apuntador apunt
// Se dice que: "apunt va a apuntar a variables de tipo int"
donde:
apunt es el nombre del apuntador y (*) es el operador de indirección
En el ejemplo anterior, puede decirse que:
*apunt se refiere al objeto apuntado por apunt.
apunt es un apuntador a objetos de tipo int
Observe que el operador de indirección utiliza el mismo símbolo que el operador de multiplicación. En
este caso el asterisco le indica al sistema que se define una variable apuntador. Ejemplos:
int *x;
a es un apuntador de tipo entero.
char *y;
c es un apuntador de tipo carácter.
double *p, *q;
p y q son apuntadores de tipo real doble precisión.
1.1.3 Operadores para trabajar apuntadores
Los operadores utilizados para trabajar variables apuntadores son el ( * ) asterisco llamado operador de
indirección, y el ( & ) ampersand, llamado operador de dirección.
2
•
* toma su operando como una dirección de memoria y retorna la información almacenada en ese
lugar.
•
& devuelve la dirección de memoria de su operando.
Veamos un ejemplo llamado Programa1.cpp en el siguiente listado.
1.1.4 Aplicación práctica del uso de apuntadores
Progra1.cpp
#include <iostream.h>
#include <conio.h>
void main(void)
{
int x, y;
Define x, y variables enteras.
int *p, *q;
Define p y q variables tipo apuntador.
p = &x;
asigna a p la dirección de x. (p apunta a x).
q = &y;
asigna a q la dirección de y. (q apunta a y).
*p = 10;
almacena en x el valor 10.
*q = *p * 2;
almacena en y el valor de x multiplicado por 2 (20).
y = y + *p;
a la variable y le suma el valor en x (20+10).
cout<<*p;
imprime el contenido de x (10).
cout<<,*q;
imprime el contenido de y (30).
getch();
}
1.2 Variables automáticas y apuntadores
Las variables automáticas se crean en tiempo de compilación y se destruyen al terminar la ejecución del
módulo donde fueron declaradas. Aunque no es estrictamente necesario, se pueden manejar las
variables automáticas por medio de apuntadores, veamos un ejemplo.
1.2.1 Implementación de variables automáticas
Progra2.cpp
#include <iostream.h>
#include <conio.h>
void main()
{
int automatica ; // Se declara la variable automatica.
int *apunt ; // Se declara el apuntador apunt, que apuntará a objetos de tipo int.
automatica = 100 ; // Se asigna el valor 100 a la variable automática.
apunt = &automatica ; // Se asigna a apunt la dirección de automatica ó apunt apunta a
// automatica.
clrscr();
cout << "VALOR=" << automatica << " \n"; // 100
*apunt="200" ; // Se asigna el valor 200 al objeto apuntado- // por apunt.
cout << "VALOR=" << automatica << " \n"; // 200
getch();
}
3
1.2.2 Análisis de Progra2.cpp
Las instrucciones del listado anterior se traducen en la siguiente secuencia, donde los apuntadores se
representan con una flecha (para simular que "apuntan hacia" ó "señalan" un objeto) y los objetos
apuntados se representan por un cuadro (para simular un recipiente).
INSTRUCCION
REPRESENTACION GRAFICA
int automatica ;
automatica
int *apunt ;
----> ?
?
100
automatica = 100 ;
automatica
apunt = &automatica ;
automatica, *apunt
100
apunt ---->
*apunt = 200 ;
automatica, *apunt
Apunt ---->
200
Un apuntador es una variable que solo puede contener un valor a la vez, por lo que solo puede apuntar
a un objeto al mismo tiempo. Por otro lado, una variable cualquiera puede ser apuntada (referenciada)
por varios apuntadores, ya que su dirección de memoria puede ser almacenada en distintas variables a
la vez. Al declarar un apuntador, se está especificando el tipo de variable al que va a apuntar. Por
ejemplo, no podrá declararse un apuntador a objetos de tipo int y después intentar utilizarlo para
apuntar a objetos de tipo float. Cuando se desee manejar un apuntador a cualquier tipo de objeto, se
puede declarar de tipo void, como en: void *multiusos;
En el siguiente listado se muestra un ejemplo de aplicación de un apuntador de tipo void.
1.2.3 Aplicación de apuntador tipo void
Progra3.cpp
#include <iostream.h>
#include <conio.h>
#define NL
cout << "\n";
void main()
{
int varent="0" ;
float varflot="0.0" ;
void *apmulti="&varent”; // apmulti APUNTA A varent
*(int *)apmulti="2" ; // ASIGNA 2 AL OBJETO NL; // APUNTADO POR apmulti
cout << varent ;
4
apmulti="&varflot" ; // apmulti APUNTA A varflot *(float *)apmulti="1.1" ;
// ASIGNA 1.1 AL OBJETO APUNTADO // POR apvoid NL;
cout << varflot ;
NL;
getch();
}
1.2.4 Análisis de Progra3.cpp
Analicemos la siguiente instrucción:
*(int *)apmulti = 2 ;
en donde:
apmulti es un apuntador de tipo void.
(int *)apmulti está forzando a que apmulti apunte a objetos de tipo int.
*(int *)apmulti se refiere a un objeto de tipo entero apuntado por apmulti.
1.3 Apuntadores y cadenas
Una cadena es un arreglo de caracteres cuyo último elemento es el caracter nulo. Utilizando la
nomenclatura de arreglos, podemos declarar:
char nombre[ ] = "COMERCIO" ;
Esto mismo puede hacerse por medio de apuntadores, como se muestra en siguiente ejemplo.
1.3.1 Implementación de apuntadores y cadenas
Progra4.cpp
#include <iostream.h>
#include <conio.h>
#include <stdio.h>
void main()
{
char *nombre = "COMERCIO" ;
clrscr();
gotoxy(30,12);
cout<< "!! HOLA, " ; puts(nombre); gotoxy(43,12); cout << " !!";
getch();
}
1.3.2 Arreglos de apuntadores
Los apuntadores pueden manejarse en un arreglo, de tal forma que:
char nombres[ ][5] = { "HUGO", "PACO", "LUIS" } ;
es la declaración de un arreglo de cadenas, con asignación de valores iniciales. Su equivalente en
notación de apuntadores es:
char *nombres[ ] = { "HUGO", "PACO", "LUIS" } ;
en el que se declara un arreglo de apuntadores. El programa completo para el manejo de este ejemplo.
5
1.3.3 Implementación de arreglos de apuntadores
Progra5.cpp
#include <iostream.h>
#include <conio.h>
#include <string.h>
void main()
{
char *nombres[ ] = { "HUGO", "PACO", "LUIS" } ;
char invitado[11];
int bandera;
clrscr();
gotoxy(30,10);
cout << "CUAL ES SU NOMBRE ? " ; gotoxy(50,10); cin>> invitado ;
gotoxy(30,12);
for( int x = 0 ; x <3 ; x++ ) if(strcmp(invitado, nombres[x])="=" 0)
bandera="0;" if(bandera="=" 0) cout << "!! PASE, ESTIMADO "
<< invitado << " !!"; else cout << "!! FUERA DE AQUI, " << invitado << "
!!"; getch();
}
1.4 Paso de arreglos como parámetros
Un arreglo puede pasarse como parámetro a una función. Si tuviera que pasarse por valor un arreglo
muy grande, sería un desperdicio de memoria. En el Lenguaje C++ el paso de arreglos se hace por
referencia, ya que el nombre del arreglo corresponde a un apuntador al primer elemento del arreglo.
Al pasar un parámetro correspondiente a un arreglo, se pasa la dirección del primer elemento, por lo
que la función invocada puede modificar cualquier elemento del arreglo. El siguiente programa maneja
una función llamada nputs(), la cual recibe como parámetro un arreglo de caracteres.
1.4.1 Implementación de paso de arreglos como parámetros
Progra6.cpp
#include <iostream.h>
#include <conio.h>
#include <stdio.h>
#include <string.h>
void nputs(char *);
void main()
{
char cadena[81];
clrscr();
gotoxy(10,10);
cout << "ESCRIBA UNA CADENA: "; gets(cadena); gotoxy(10,12); nputs(cadena); getch(); } void
nputs(char cad[ ]) { int x="0;" while(cad[x]) { cout << cad[x] ; x++; } }
6
1.4.2 Implementación de la función nputs() por medio de apuntadores
En el siguiente listado se muestra el manejo de la función nputs(), por medio de apuntadores.
Progra7.cpp
#include <iostream.h>
#include <conio.h>
#include <stdio.h>
#include <string.h>
void nputs(char *);
void main()
{
char cadena[81];
clrscr();
gotoxy(10,10);
cout << "ESCRIBA UNA CADENA: ";
gets(cadena);
gotoxy(10,12);
nputs(cadena);
getch();
}
void nputs(char *cad)
{
while(*cad) cout << *cad++ ;
}
1.4.3 Paso de funciones como parámetros
Toda función tiene asociada una dirección de inicio de código, la cual puede pasarse un apuntador
como parámetro en la invocación a otra función. Veamos un ejemplo de paso de punteros como
parámetros a funciones.
1.4.4 Implementación del paso de funciones como parámetros
Progra8.cpp
#include <iostream.h>
#include <string.h>
#include <conio.h>
int cmpcad(char*, char*);
void compara(char*, char*, int(*)(char*, char*));
void main()
{
char cadx[80], cady[80];
clrscr();
gotoxy(10,5);
cout << "ESCRIBA UNA CADENA : " ; cin>> cadx;
gotoxy(10,7);
cout << "ESCRIBA OTRA CADENA : " ; cin>> cady;
gotoxy(10,9);
compara(cadx, cady, cmpcad);
gotoxy(1,24);
7
}
void compara(char *cad1, char *cad2,
int (*cmpcad)(char*, char*))
{
if(!(*cmpcad)(cad1,cad2))
cout << "LAS CADENAS SON IGUALES";
else cout << "LAS CADENAS SON DISTINTAS";
}
int cmpcad(char *x, char *y)
{
return(strcmp(x,y));
}
Expliquemos la expresión que puede ser un tanto desconocida del listado anterior, la expresión:
int(*cmpcad)(char*, char*) establece que cmpcad es un apuntador a una función, la cual devuelve un
valor de tipo entero.
1.5 Apuntadores a apuntadores
Como se vio al principio de la unidad, un apuntador también es una variable. Su dirección puede ser
almacenada por otra variable apuntador, por lo que puede hablarse de un apuntador a un apuntador.
Esto puede extrapolarse para dos o más variables, como se observa en el ejemplo siguiente de
apuntadores a apuntadores.
1.5.1 Implementación de apuntadores a apuntadores
Progra9.cpp
#include <iostream.h>
#include <conio.h>
void main()
{
int x, *a, **b, ***c ; // 1
clrscr();
a = &x ;
// 2
*a = 100 ;
// 3
b = &a ;
// 4
**b += *a ;
// 5
c = &b ;
// 6
***c += **b + *a ;
// 7
cout << " *a=" << *a << " \n" ;
cout << " **b=" << **b << " \n" ;
cout << "***c=" << ***c << " \n" ;
getch();
}
1.5.2 Análisis de Progra9.cpp
Explicando las líneas marcadas en el listado anterior se tiene.
int x, *a, **b, ***c; // 1
8
Se declaran:
x como una variable de tipo entero. a como un apuntador a objetos de tipo entero. b como un apuntador
a un apuntador, el cual a su vez apuntará a objetos de tipo entero. Se dice que b es "el apuntador del
apuntador". c como un apuntador a un apuntador que apunta a otro apuntador, el cual a su vez apunta
a objetos de tipo entero. Se dice que c es "el apuntador del apuntador del apuntador".
La pila luciría así:
a = &x ; // 2
Se asigna, al apuntador a, la dirección de x. La pila luciría así:
*a = 100 ; // 3
Al objeto apuntado por a se le asigna el valor 100. La pila luciría así:
b = &a ; // 4
Al apuntador b se le asigna la dirección del apuntador a. La pila luciría así:
**b += *a ; // 5
Al objeto apuntado por el apuntador apuntado por b se le suma el valor del objeto apuntado por a. La
pila luciría así:
9
c = &b ; // 6
Al apuntador c se le asigna la dirección del apuntador b. La pila luciría así:
***c += **b + *a ; // 7
Se asigna al objeto apuntado por el apuntador apuntado por el apuntador c, el valor del objeto apuntado
por el apuntador apuntado por el apuntador b más el valor del objeto apuntado por el apuntador a. La
pila luciría así:
1.5.3 Apuntadores y arreglos
El tema de los arreglos está íntimamente ligado al de apuntadores; tanto que es posible intercambiarlos
en la solución de un problema.
El nombre
bre de un arreglo corresponde al de un apuntador que almacena un valor constante. Este valor
constante es la dirección de memoria del primer elemento del arreglo.
Por ejemplo :
int calif[ ]={100,90,95,80,90}; // Declaración e inicialización de un arreglo de 5
// enteros
enteros.
Se puede representar con la figura siguiente.
10
En realidad, el lenguaje manejará al arreglo a través de un apuntador llamado calif, el cual tiene
almacenado el valor 65494, que a su vez corresponde a la dirección de inicio del elemento calif[0].
La representación del apuntador calif, en la zona de variables globales de la memoria RAM, es la
siguiente :
Calif
65494
En el siguiente listado se presenta el manejo del arreglo calif[ ], a través de la notación de arreglos, y en
el listado subsiguiente llamado Progra11.cpp el manejo con la notación de apuntadores.
Manejo de calif[ ] , notación de arreglos
Progra10.cpp
#include <iostream.h>
void main()
{
int calif[ ] = { 100,90,95,80,90};
for(int i=0 ; i <5 ; i++) //Notación de arreglos.
cout << "\n" << calif[i] ;
}
1.5.4 Notación de apuntadores
De igual manera que la notación arreglos se presenta el manejo de calif[ ] en notación de apuntadores.
Progra11.cpp
#include <iostream.h>
void main()
{
int calif[ ] = { 100,90,95,80,90};
for(int i=0 ; i <5 ; i++) // Notación de apuntadores
cout << "\n" << *(calif+i) ;
}
11
1.5.5 Análisis de Progra11.cpp
Como puede observarse, la única diferencia entre los listados llamados Progra10 y 11 es que el
primero utiliza calif[i] y el segundo *(calif+i).
Debido a que la ejecución de los programas de ambos listados producen resultados iguales, se deduce
que:
calif[i] == *(calif+i)
Para entender esto que a simple vista no es obvio, revisaremos algunos conceptos:
1. El nombre del arreglo corresponde al de un apuntador que apunta al primer elemento del arreglo, por
lo que:
calif apunta a calif[0]
Visto gráficamente:
2. Para hacer referencia a un elemento específico del arreglo, se toma como base la dirección del
primer elemento y, con el subíndice del elemento específico, se calcula su dirección. Por ejemplo, para
referirse al segundo elemento del
el arreglo puede procederse así
así:
calif[1] // Notación de arreglos
ó
*(calif+1) // Notación de apuntadores, donde la expresión calif+1sirve para calcular la dirección del
elemento que está una posición más allá del elemento apuntado por calif.
Para referirse a calif[2] ( tercer elemento ), puede escribirse:
*(calif+2)
Lo que significa: "El objeto que se encuentra dos posiciones después del objeto apuntado por calif". En
este caso, una posición es el espacio requerido por cada uno de los elementos, de tal manera que, si
calif apunta a la dirección 65494, entonces calif+2 es la expresión que calcula la dirección 65498. La
figura muestra los elementos del arreglo calif[ ] con sus nombres en notación de arreglos y en notación
de apuntadores.
De lo anterior, se infiere la regla:
calif[i] == *(calif+i)
12
por lo que:
&calif[i] == calif+i
Esto es que, la dirección del i-ésimo elemento de un arreglo se calcula sumándole el subíndice a la
dirección del primer elemento.
Como otro ejemplo, supongamos la siguiente declaración correspondiente a un arreglo de 10 elementos
de tipo float.
float* sueldo[10];
Si la dirección del primer elemento es 65494, entonces:
&sueldo[5] es igual a :
sueldo+5 = 65494 + ( 5 * 4 ) = 65514
Como regla, podemos establecer que el subíndice se refiere a la posición del elemento en el arreglo. Es
por esto que al calcular la dirección por medio del subíndice, éste debe multiplicarse por el número de
bytes que representan el tamaño de cada elemento (dado por el tipo utilizado en la declaración del
arreglo).
1.5.6 Operaciones con apuntadores
•
Se pueden realizar asignaciones entre punteros.
int a = 15;
int *p, *q;
q = &a;
p = q;
/* se asigna la dirección que contiene q a p */
cout<<p;
/* imprime la dirección almacenad en p. */
•
Se pueden operar solamente el +, el -, el ++ y el --.
Int p;
p = p + 1;
p avanza un entero.
p = p – 2;
p retrocede dos enteros.
p++;
p apunta al siguiente entero.
p--;
p apunta al entero anterior.
•
Los punteros se pueden comparar.
int a;
int *b, *c;
if (b + n > c)
b = b +2;
En el siguiente código se realiza un programa que emplea los operadores (& y *).
Progra12.cpp
#include <iostream.h>
#include <conio.h>
int main()
{
clrscr();
int a;
//a es un puntero
13
int * ap; //ap es un apuntador a un puntero
a = 7;
ap= & a; //ap toma la dirección de a
cout <<"la dirección de a es " <<&a;
cout<<" \n el Valor de ap es " << ap;
cout <<"\n el valor de a es" <<a;
cout<<"\n el valor de *ap es " << *ap;
cout<<"\n\n\n Mostrando los valores de * y &" ;
cout <<"\n &* ap = " <<&*ap;
cout <<"\n *& ap = " << *≈
getch();
return 0;
}
Capitulo 2. Gestión dinámica de memoria
2.1 Conceptos Básicos de Memoria
Para iniciar este capítulo es importante que le demos un vistazo a la memoria de la computadora. Si
usted ya sabe cómo funciona la memoria de la computadora, se puede saltar esta sección. Sin
embargo, si no está seguro, le sugiero que la lea, le ayudará a comprender mejor ciertos aspectos de la
programación.
La computadora usa memoria de acceso aleatorio (RAM) para guardar información mientras está en
funcionamiento. La RAM se encuentra en circuitos integrados o chips en el interior de la computadora.
La RAM es volátil, lo que significa que es borrada y reemplazada con nueva información tan pronto
como se necesita. La volatilidad también significa que la RAM “recuerda” solamente mientras la
computadora está encendida, y pierde su información cuando se apaga la computadora.
Cada computadora tiene una determinada cantidad de RAM instalada. La cantidad de RAM en un
sistema se especifica por lo general en Megabytes (Mb) por ejemplo 256 Mb, 512 Mb, en ese orden de
ideas se dice un byte es la unidad de medida fundamental de la memoria de una computadora, de los
cuales se obtiene los Kilobytes, Megabytes, Gigabytes, siendo estos los más usados. Un kilobytes de
memoria equivale a 1,024 bytes.
2.1.1 Tipo de datos y bytes requeridos en memoria
Para darse una idea de que tantos bytes se necesitan para guardar determinados tipos de datos lo
invito a que revise la siguiente tabla de espacios requeridos para guardar datos.
Datos
La letra X
El número 100
El número 125.123
La frase Aprenda usted mismo
Una página escrita completamente
Bytes requeridos
1
2
4
21
3000 (Aproximadamente)
La RAM en la computadora está organizada en forma secuencial, un byte tras otro. Cada byte de
memoria tiene una dirección única mediante la cual es identificado, una dirección que también lo
14
distingue de todos los otros bytes de la memoria. Las direcciones son asignadas a la memoria en orden,
comenzando en 0 y aumentando hasta llegar al límite del sistema.
Para ampliar un poco más la conceptualización a cerca de los tipos de datos se define todo el posible
rango de valores que una variable puede tomar al momento de ser ejecutada en el programa al igual
que en toda la vida útil del propio programa.
2.2 Tipos de datos comunes
Los tipos de datos más comunes utilizados frecuentemente en C++ son:
TIPO DATO
ESPACIO MEMORIA
RANGO
unsigned char
8 bits
0 a 255
Char
8 bits
-128 a 127
short int
16 bits
-32,768 a 32,767
unsigned int
32 bits
0 a 4,294,967,295
Int
32 bits
-2,147,483,648
2,147,483,647
unsigned long
32 bits
0 a 4,294,967,295
Enum
16 bits
-2,147,483,648
2,147,483,647
a
Long
32 bits
-2,147,483,648
2,147,483,647
a
Float
32 bits
3.4 x 10-38 a 3.4 x
10+38(6 dec)
Doublé
64 bits
1.7
x
10-308
1.7*10+308(15 dec)
long doublé
80 bits
3.4 x 10-4932 a 1.1 x
10+4932
Void
sin valor
a
a
2.2.1 Usaos de la Memoria RAM
Alguna vez nos hemos hecho la siguiente pregunta ¿Para qué se usa la memoria RAM de la
computadora? Tiene varios usos, pero solamente uno, el almacenamiento de datos, le interesa al
programador. Los datos significan la información con la cual trabaja un programa. Ya sea que el
programa esté trabajando con una lista de direcciones, monitoreando la bolsa de valores, manejando un
presupuesto o cualquier otra cosa, la información (nombres, precios de acciones, gastos) es guardada
en la RAM de la computadora mientras el programa esté ejecutando.
15
Hasta el momento, la mayoría de los programas los hemos realizado definiendo variables, sin
preocuparnos de que se realiza internamente en el computador, muchas veces en forma indiscriminada,
es decir sin una verdadera depuración, pero existen ocasiones en que no sabemos cuanta memora
necesitaremos para ejecución de determinado programa, por ejemplo si deseamos realizar un
procesador de textos, no sabemos cuál va hacer la longitud del texto.
Por eso a veces es necesario poder reservar memoria según se va necesitando. Además de esta forma
nuestros programas aprovecharán mejor la memoria del computador en el que se ejecuten, usando
sólo los recursos necesarios.
Realmente la utilidad de asignación dinámica de memoria será aplicada
capítulos relacionados con las estructuras lineales.
en gran medida en los
De acuerdo a lo anterior podemos definir dos tipos de variables: estáticas y dinámicas.
2.3 Tipos de variables
Dependiendo el uso que se le dé a las variables por parte del programador, en una rutina o tarea
específica se pueden identificar dos tipos de variables ellas son variables dinámicas y variables
estáticas.
2.3.1 Variables estáticas
Las variables estáticas como recordamos en los inicios de los fundamentos de programación, son
aquellas que el programador les asigna memoria antes de la ejecución del programa o de una función,
las variables estáticas se llaman mediante el nombre de la misma, que ha sido declarado por el
programador.
2.3.2 Aplicación de las variables estáticas
Veamos el código de un programa que hace uso de las variables estáticas
Progra13.cpp
#include <stdio.h>
#include <iostream.h>
#include <stdlib.h>
#include <conio.h>
int priNumero; /* Nuestras variables */
int segNumero;
int suma;
void main()
{
clrscr();
priNumero = 136;
segNumero = 369;
suma = priNumero + segNumero;
16
cout<<"La suma es " <<suma;
getch();
}
2.3.3 Análisis de Progra13.cpp
En el código del programa se hace uso de tres variables de tipo entero que son variables estáticas ellas
son: priNumero que se le asigna el valor 136, mientras que a la variable segNumero se le asigna el
valor 369, de igual manera a la variable suma calcula el resultado de sumar el valor de las dos
variables.
2.4 Variables dinámicas
Las variables dinámicas deben su nombre al hecho de que pueden ser creadas y destruidas durante el
tiempo de ejecución de un módulo.
Para el manejo de variables dinámicas se hace indispensable la utilización de apuntadores, así como
de funciones especiales para la asignación y liberación de la memoria correspondiente a dichas
variables.
2.4.1 Aplicación de las variables dinámicas
Veamos el código de un programa que hace uso de las variables dinámicas
Progra14.cpp
#include <iostream.h>
#include <conio.h>
void main()
{
int x, *a, **b, ***c ; // 1
clrscr();
a = &x ;
// 2
*a = 100 ;
// 3
b = &a ;
// 4
**b += *a ;
// 5
c = &b ;
// 6
***c += **b + *a ;
// 7
cout << " *a=" << *a << " \n" ;
cout << " **b=" << **b << " \n" ;
cout << "***c=" << ***c << " \n" ;
getch();
}
2.4.2 Análisis de Progra14.cpp
En el listado del progra14.cpp se declaran tres variables apuntador de tipo entero ellas son *a, **b y **c
el apuntador a almacena la dirección de la variable x, mientras que **b precedida de dos asteriscos
indica que es una variable que apunta a un apuntador y ***c es un apuntador a apuntador a apuntador.
17
2.5 Asignar y liberar espacios en memoria
En el lenguaje C existen entre otras las funciones Malloc() y Free() para la asignación y liberación de
memoria dinámicamente respectivamente.
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.
Profundizando un poco en la asignación dinámica, encontramos el operador sizeof, el cual determina el
tamaño en bytes que se requiere en la asignación dinámica de memoria, ya sea por medio de los
operadores New y Delete, o por las funciones Malloc y Free, de un arreglo o de cualquier otro tipo de
datos,
Ejemplo utilizar el operador sizeof en una función con el propósito de determinar el tamaño en bytes de
un parámetro.
2.5.1 Aplicación del operador sizeof.
En el siguiente listado se evidencia la aplicación y uso del operador sizeof en cada variable el cual
devuelve el número de bytes dependiendo del tipo de variable.
2.5.2 Implementación del uso de sizeof
Progra15.cpp
#include <iostream.h>
#include <conio.h>
void main()
{
char c;
short s;
int i;
long l;
float f;
double d;
long double ld;
int arreglo[20], * pt = arreglo;
clrscr();
gotoxy(20,2);
cout<<"valores utilizando sizeof para cada una de la varibles \n\n";
cout<<" variable c = " <<sizeof c;
cout<<"\t tipo char = " <<sizeof (char);
18
cout<<"\n variable s = " <<sizeof s;
cout<<"\t tipo short = " <<sizeof (short);
cout<<"\n variable i = " <<sizeof i;
cout<<"\t tipo int = " <<sizeof (int);
cout<<"\n variable l = " <<sizeof l;
cout<<"\t tipo int = " <<sizeof (long);
cout<<"\n variable f = " <<sizeof i;
cout<<"\t tipo float = " <<sizeof (float);
cout<<"\n variable d = " <<sizeof d;
cout<<"\t tipo double = " <<sizeof (double);
cout<<"\n variable ld = " <<sizeof ld;
cout<<"\t tipo int = " <<sizeof (long double);
cout<<"\n variable i = " <<sizeof i;
cout<<"\t tipo int = " <<sizeof (int);
getch();
return 0;
}
Capítulo 3. Operadores y funciones en la gestión de memoria
3.1 Operadores New y Delete
El lenguaje C++ cuenta con dos operadores preconstruidos, ellos son: New y Delete, por esta razón no
se requiere incluir ninguna librería o archivo de cabecera para utilizarlos.
El operador New: Realiza una labor parecida a la de la función malloc(), asignando un
memoria según sea requerido.
bloque de
El operador Delete: Libera un bloque de memoria asignada por New en tiempo de ejecución, de
manera semejante a como lo hace la función free().
La sintaxis para el uso del operador new es:
Apuntador = new tipo_de_dato;
Este operador hace una petición al sistema operativo para que se le asigne un espacio de memoria, con
tamaño de acuerdo al tipo de datos (recordemos la función sizeof), si este espacio está disponible, la
operación regresa la dirección real que se otorga, en caso de no haber espacio regresa el valor de
NULL (0),
La sintaxis para el uso del operador delete es:
delete apuntador;
La ejecución de este operador provoca que se libere espacio, dejando como valor indefinido, es decir el
sistema operativo lo considera como memoria disponible.
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 fiéis de lo que
diga el compilador, de que estas variables se liberan solas al terminar el programa, no siempre es
verdad.
19
3.1.1 Aplicación de los operadores New y Delete
Veamos un ejemplo de utilización de los operadores de c++ utilizados para asignar y liberar memoria
dinámicamente.
“En el listado se declaran las variables index de tipo entero y los apuntadores point1 y point2 ambos de
tipo entero.
Progra16.cpp
# include <iostream.h>
main()
{
int index, *point1, *point2;
point1 = &index;
*point1 = 77;
point2 = new int;
*point2 = 173;
cout <<"Los valores son " << index <<" " << *point1 << " "<< *point2 <<'\n';
point1 = new int;
point2 = point1;
*point1 = 999;
cout <<"Los valores son " << index <<" " << *point1 << " "<< *point2 <<'\n';
delete point1;
float *float_point1, *float_point2 = new float;
float_point1 = new float;
*float_point2 = 3.14159;
*float_point1 = 2.4 * (*float_point2);
delete float_point2;
delete float_point1;
char *c_point;
c_point = new char;
delete c_point;
c_point = new char [sizeof(int) + 133];
delete c_point;
}
3.1.2 Análisis de Progra16.cpp
El resultado de la ejecución del listado llamado Progra16.cpp es:
Los valores son 77 77 173
Los valores son 77 999 999
• En las primeras líneas del programa, se hace uso de los punteros tal y como se hacen también
en C.
•
point2 ilustra el uso del operador new. Este operador requiere un modificador que debe ser un
tipo. La parte new int significa que se crea un nuevo entero en la memoria, y devuelve la
localización del entero creado. Esta localización es asignada a point2. La siguiente línea asigna
173 al entero al que apunta point2. Es importante distinguir entre point2, la localización del
20
entero, y *point2, el entero. El puntero point2 apunta ahora a una variable entera que se ha
reservado dinámicamente, y que puede utilizarse de igual forma que se hacía en C. Como
ejemplo, se imprime el valor al que apunta.
•
A continuación, se reserva memoria para una nueva variable, y point2 se refiere a la misma
variable reservada dinámicamente a la que apunta point1. En este caso, la referencia a la
variable a la que point2 apuntaba previamente se ha perdido, y nunca podrá ser utilizada o su
memoria liberada. Sólo cuando se vuelva al sistema operativo se liberará la memoria que
ocupaba. Por tanto, no debe utilizarse.
•
Ya que el puntero point1 en sí no ha cambiado, apunta realmente al dato original. Este dato
podría referenciarse otra vez utilizando point1, pero no es una buena práctica de programación,
ya que no hay garantía de lo que el sistema pueda hacer con el puntero o el dato. La localización
del dato queda libre para ser reservada en una llamada subsiguiente, y será pronto reutilizada en
cualquier programa.
•
Ya que el operador delete está definido para no hacer nada si se le pasa un valor NULL, se
puede liberar la memoria ocupada por un dato al que apunta un puntero NULL, ya que realmente
no se está haciendo nada. El operador delete sólo puede utilizarse para liberar memoria
reservada con el operador new. Si se usa delete con cualquier otro tipo de dato, la operación no
está definida, y por tanto nada sucede.
•
En el programa también declaramos algunas variables reales, y se realizan operaciones
similares a las anteriores. De nuevo esto ilustra que en C++ las variables no tienen que ser
declaradas al comienzo de cada bloque. Una declaración es una sentencia ejecutable y puede
entonces aparecer en cualquier lugar en la lista de sentencias ejecutables.
•
Finalmente, ya que el operador new requiere un tipo para determinar el tamaño de un bloque
dinámicamente reservado, se muestra cómo reservar un bloque de tamaño arbitrario. Esto es
posible utilizando la construcción de las últimas líneas del programa, donde un bloque de 37
caracteres de tamaño (37 bytes) es reservado. Un bloque de 133 bytes mayor que el tamaño de
un entero se reserva posteriormente. Por tanto, el operador new se puede utilizar con la misma
flexibilidad de la función malloc() de C.
•
Cuando los datos reservados dinámicamente son borrados con delete, todavía quedan en
memoria. Si repetimos la instrucción cout inmediatamente después de utilizar delete, veremos
que todavía se conservan los valores. Si la repetimos de nuevo antes de dejar el programa,
cuando el espacio que ocupaban debe haber sido sobre escrito, veremos que ya no es así.
Incluso aunque el compilador nos dé los números correctos, no es una buena práctica pensar
que esos datos están ahí todavía, porque en un programa dinámico largo la memoria se usará
continuadamente.
•
Las funciones estándar utilizadas en C para manejo dinámico de memoria, malloc(), calloc() y
free(), también se pueden utilizar en C++ de la misma forma que en C. Los operadores new y
delete no deben mezclarse con estas funciones, ya que los resultados pueden ser
impredecibles. Si se está partiendo de código C, lo mejor es continuar utilizando las funciones en
las nuevas líneas de programa. Si no es así, se deben utilizar los nuevos operadores, ya que se
21
han construido como parte del lenguaje en sí, más que añadirse, y por tanto son más
eficientes”1.
•
Cuando se utiliza new para reservar memoria para un vector, el tamaño del vector se sitúa entre
corchetes, siguiendo al tipo:
int *intvector;
intvector = new int [20];
y se libera:
delete [ ] intvector;
3.2 Más sobre la Implementación de New y Delete
El siguiente muestra un ejemplo de aplicación de los operadores new y delete.
Progra17.cpp
#include <iostream.h>
void main()
{
int *apent;
// Declara un apuntador a entero
apent = new int ; // Reserva un bloque de memoria dinámica
// de 2 bytes para manejarlo por medio de apent.
*apent = 10 ; // Asigna el valor 10 al objeto apuntado por apent.
cout << *apent ; // Despliega el contenido del objeto apuntado por apent.
delete apent ; // Libera el espacio de memoria manejado por apent.
}
En el listado llamado Progra 17.cpp, se supone que la reservación será exitosa porque existe espacio
suficiente en el montículo.
Pero ¿quién asegura que el espacio requerido por new está disponible?. Para controlar esta situación y
evitar un mensaje de error por parte del sistema en tiempo de ejecución, en el listado siguiente se
propone una nueva versión del programa.
3.2.1 Implementación de New para verificación de memoria
Progra18.cpp
#include <iostream.h>
#include <stdlib.h> // Para exit().
void main()
{
int *apent; // Declara un apuntador a entero
if((apent = new int)==NULL)//Intenta reservar un bloque de memoria dinámica
{
// de 2 bytes por medio de apent.
cout << "NO hay espacio suficiente\n";
exit(1); // Finaliza la ejecución del programa.
1
Fuente http://www.pablin.com.ar/computer/cursos/c1/allocate.html
22
}
*apent="10" ; // Asigna el valor 10 al objeto apuntado por apent.
cout << *apent ; // Despliega el contenido del objeto apuntado por apent.
delete apent ; // Libera el espacio de memoria manejado por apent.
}
3.2.2 Análisis de Progra18.cpp
Para crear un arreglo de 25 elementos de tipo double, en el montículo, puede escribirse:
Double *dap ;
dap = new double[25];
ó su forma equivalente:
double *dap = new double[25];
En este ejemplo, se está declarando a dap como un apuntador a variables dinámicas de tipo doble; al
tiempo que se le asigna el valor retornado por new. El valor retornado por new es la dirección del inicio
de un bloque de memoria del tamaño requerido para almacenar 25 elementos de tipo double.
En caso de que el montículo no disponga del espacio requerido, new retorna el valor NULL ( nulo ).
3.3 Funciones Malloc() y Free()
3.3.1 La función Malloc()
Es una de las funciones de asignación de memoria del lenguaje de programación C (acrónimo de
memory allocation). Cuando se usa malloc () se pasa la cantidad de bytes de memoria que se necesita.
Malloc () encuentra y reserva un bloque de memoria del tamaño pedido y regresa la dirección del primer
byte del bloque. No hay de qué preocuparse sobre que parte de memoria se usa, ya que esto es
manejado automáticamente.
La función malloc () regresa una dirección, y su tipo de retorno es un apuntador a tipo void. ¿Por qué
void?. Un apuntador a tipo void es compatible con todos los tipos de datos. Como la memoria asignada
por malloc () puede ser usada para guardar cualquiera de los tipos de datos de C, es adecuado el tipo
de retorno void.
3.3.2 La función Free()
Al igual que malloc(), free() es una función del lenguaje de programación C, utilizado para liberar la
memoria asignada por malloc (). Al usar la función free () se debe tener en cuenta la regla de oro
explicada en el apartado del operador delete “toda la memoria que se reserve durante el programa hay
que liberarla antes de salir del programa”
Al usar las funciones malloc () y free () se debe incluir el archivo de encabezado STDLIB.H
3.3.3 Ejemplos de aplicación
Ejemplo 1
// asigna memoria para un arreglo de 50 enteros
Int *números;
Números = (int * ) malloc (50 * sizeof (int));
23
Ejemplo 2
// asigna memoria para un arreglo de 10 valores float
float *números;
Números = (float * ) malloc (10 * sizeof (float));
3.4 Aplicación a la asignación de memoria con Malloc() y Free()
Progra19.cpp
#include <conio.h>
#include <stdlib.h>
#include <stdio.h>
void main()
{
clrscr();
int *p==NULL;
int nbytes=100;
p=(int *)malloc(nbytes);
if(p=NULL)
{
cout<<"Insuficiente espacio en memoria\n");
return -1;
}
cout<<"se han asignado %d bytes de memoria\n", nbytes);
free(p);
getch();
}
El resultado del programa muestra un mensaje en pantalla confirmando que se realizó la asignación
dinámica de memoria con éxito.
Ahora veamos otro ejemplo de aplicación de la asignación dinámica de memoria utilizando las
funciones malloc () y free (), con un ingrediente adicional el uso de una estructura llamada persona que
nos permita almacenar el registro de una persona, dos tipos de datos diferentes, un dato de tipo
carácter que almacena el nombre de la persona y otro dato de tipo entero para almacenar la edad.
3.4.1 Implementación del uso de Malloc() y Free()
Progra20.cpp
// Listado de librerías o archives de cabecera
#include<iostream.h>
#include<stdlib.h>
#include<conio.h>
// Definición de la función principal
void main()
{
clrscr();
24
int n, i;
// Definición de la estructura persona
struct persona
{
char nombre[20];
int edad;
};
// Definición del puntero p de tipo persona utilizado para reservar memoria
persona *p;
cout<<"PROGRAMA QUE GUARDA EL REGISTRO DE PERSONAS"<<"\n";
cout<<"\nNUMERO DE PERSONAS A INGRESAR : ";
cin>>n;
// Reserva de memoria dinámica a través de malloc ()
p =(persona *)malloc(sizeof(persona));
// El ciclo for usado para la entrada de los datos de la persona
for(i=1;i<=n;i++)
{
cout<<"\nDIGITE EL NOMBRE " << i <<" : ";
cin>>p[i].nombre;
cout<<"DIGITE LA EDAD : ";
cin>>p[i].edad;
cout<<"\n";
}
clrscr();
cout<<"LISTADO DE PERSONAS REGISTRADAS "<<"\n";
// El ciclo for usado para la impresión o visualización de los datos registrados
for(i=1;i<=n;i++)
{
cout<<" NOMBRE : "<<p[i].nombre<<"\n";
cout<<" EDAD : "<<p[i].edad<<"\n\n";
}
getch();
// La función free () libera la memoria asignada al apuntador p
free (p);
}
3.5 Otras funciones para asignar memoria dinámica
Aparte de la función Malloc() tratada en la sección anterior, utilizada para la gestión dinámica de
memoria, existen dos funciones adicionales para reservar memoria, ellas son: calloc() y realloc().
3.5.1 La Funcione Calloc()
A continuación se presentan el prototipo para la definición de la función Calloc().
void *calloc(size_t nmemb, size_t size);
25
“Cuando se usa la función malloc() la memoria no es inicializada (a cero) o borrada. Si se quiere
inicializar la memoria entonces se puede usar la función calloc. La función calloc es
computacionalmente un poco más cara pero, ocasionalmente, más conveniente que malloc. Se debe
observar también la diferencia de sintaxis entre calloc y malloc, ya que calloc toma el número de
elementos deseados (nmemb) y el tamaño del elemento (size), como dos argumentos individuales.
Por lo tanto para asignar a 100 elementos enteros que estén inicializados a cero se puede hacer:
int *ip;
ip = (int *) calloc(100, sizeof(int) );
3.5.2 La Función Realloc()
Esta función intenta cambiar el tamaño de un bloque de memoria previamente asignado. El nuevo
tamaño puede ser más grande o más pequeño. Si el bloque se hace más grande, entonces el contenido
anterior permanece sin cambios y la memoria es agregada al final del bloque. Si el tamaño se hace más
pequeño entonces el contenido sobrante permanece sin cambios.
El prototipo de definición para la función realloc() es como se presenta a continuación.
void *realloc(void *ptr, size_t size);
Si el tamaño del bloque original no puede ser redimensionado, entonces realloc intentará asignar un
nuevo bloque de memoria y copiará el contenido anterior. Por lo tanto, la función devolverá un nuevo
apuntador (o de valor diferente al anterior), este nuevo valor será el que deberá usarse. Si no puede ser
reasignada nueva memoria la función realloc devuelve NULL.” 2
Si para el ejemplo anterior, se quiere reasignar la memoria a 60 enteros en vez de 100 apuntados por
ip, se hará;
ip = (int *) realloc ( ip, 60*sizeof(int) );
Referencias Bibliográficas Unidad 1
KENNETH C, louden (2004). Lenguajes de programación. Mexico D.F : Thomson.
AGUILAR, Luis (2003). Fundamentos de programación, algoritmos, estructura de datos y
Objetos(tercera edición). España: McGRAW-HILL.
AGUILAR, Luis (2000). Programación en C++, Algoritmos, estructura de datos y Objetos . España:
McGRAW-HILL.
DEYTEL Y DEYTEL (1999). Como programar C++(segunda Edición). Mexico D.F. : Prentice Hall.
McGRAW-HILL.
FARREL, Joyce (2000). Introducción a la programación lógica y diseño. Mexico D.F : Thomson.
http://www.programacionfacil.com/estructura_de_datos/manejo_de_memoria
http://www.conclase.net/c/fuentes.php?tema=3
http://www.ilustrados.com/publicaciones/EpZVVEZpyEdFpAKxjH.php
2
http://www.fismat.umich.mx/mn1/manual/node10.html consultado en Junio 18 de 2009
Descargar