Punteros en C/C++ - ramos on

Anuncio
Punteros en C/C++
por Jorge Walter Hefty
[email protected]
http://es.geocities.com/jorge_hefty/index.html
Los problemas y 'bugs' ocasionados por el uso de punteros suelen ser dificiles de localizar y depurar. Aqui se
reune informacion sobre algunos problemas que se pueden considerar tipicos, mas algunas notas sobre
conceptos generales relacionados con el manejo de memoria. Se utiliza poco codigo, el minimo suficiente para
generar el efecto buscado. Algunos temas pueden ser dependientes del compilador, en este caso se utiliza
distintas versiones de compiladores Borland. Se ha puesto mayor atencion en C++, pero buena parte del
material se aplica tambien a C.
Cualquier genero de dudas, comentarios, correcciones o sugerencias son bienvenidos en el email del autor.
Tabla de contenido
1
Nociones elementales ................................................................................................................................. 3
1.1
¿Que es una variable? ......................................................................................................................... 3
1.2
¿Que es un array? ................................................................................................................................ 5
1.3
¿Que es un puntero? ........................................................................................................................... 5
1.4
Tipos definidos por el programador .................................................................................................... 7
1.4.1
Clases ........................................................................................................................................... 7
2 Punteros I .................................................................................................................................................... 9
2.1
Declaracion e inicializacion.................................................................................................................. 9
2.2
Desreferenciacion ("indirection")...................................................................................................... 12
2.3
Asignacion de punteros ..................................................................................................................... 14
3 Punteros II ................................................................................................................................................. 16
3.1
Punteros a 'void' ................................................................................................................................ 16
3.2
Punteros y 'const' .............................................................................................................................. 17
3.3
Puntero nulo ("Null pointer") ............................................................................................................ 18
3.4
Puntero a puntero ............................................................................................................................. 18
4 Punteros III ................................................................................................................................................ 20
4.1
Puntero a funcion .............................................................................................................................. 20
4.2
Punteros a objetos............................................................................................................................. 20
4.3
El puntero implicito "this" ................................................................................................................. 21
5 Utilidad de los punteros ............................................................................................................................ 23
5.1
Aritmetica de punteros...................................................................................................................... 23
5.2
Itinerar en un array............................................................................................................................ 24
5.3
Mapear localidades de memoria ....................................................................................................... 25
5.4
Paso de parametros en funciones ..................................................................................................... 25
5.5
Reserva de Memoria Dinamica ......................................................................................................... 26
6 Problemas con punteros I.......................................................................................................................... 28
6.1
Punteros no inicializados ................................................................................................................... 28
6.2
Punteros y literales de cadena .......................................................................................................... 29
6.3
El mensaje "Null pointer assignment" ............................................................................................... 30
6.4
"Dangling pointers" ........................................................................................................................... 31
7 Problemas con punteros II......................................................................................................................... 34
7.1
Memoria dinamica............................................................................................................................. 34
7.2
Datos miembros punteros y copia de objetos .................................................................................. 36
A. Anexo 1: Cadenas de caracteres o strings estilo 'C' .................................................................................. 38
A.1
¿Que es una cadena de caracteres en C/C++? .................................................................................. 38
A.2
Array y cadena de caracteres: algunas diferencias ........................................................................... 39
A.3
Ejemplos de funciones standard ....................................................................................................... 41
A.4
Sobreescritura de variables ............................................................................................................... 41
B. ANEXO 2: Memoria .................................................................................................................................... 43
B.1
Modelos de memoria ........................................................................................................................ 43
B.2
Rol de la STACK (pila) ......................................................................................................................... 44
B.3
Gestion de memoria en C++ .............................................................................................................. 45
B.4
'R-value' y 'L-value' ............................................................................................................................ 45
1 Nociones elementales
1.1 ¿Que es una variable?
Un computador opera manipulando direcciones de memoria y los valores almacenados en dichas direcciones. Un lenguaje
de programacion es una herramienta que permite al programador codificar operaciones binarias en un lenguaje mas
cercano al natural. Un programa que realiza la traduccion de instrucciones desde un lenguaje de programacion dado al
lenguaje de maquina es un compilador.
Una variable es un recurso, entre otros, para manipular un dato binario de modo mas legible. Una variable es un
identificador, al igual que el nombre de una funcion, este NOMBRE representa para la maquina una localidad de memoria
donde el programa puede almacenar y manipular un dato.
Una declaracion de variable como:
int var;
produce una asociacion entre el nombre 'var' y un espacio de almacenamiento en memoria. Por lo tanto hay dos elementos
relacionados con el nombre 'var': un valor que se puede almacenar alli y una direccion de memoria para la variable, algunos
autores se refieren a estos dos aspectos como el "rvalue" (rigth-value) y "lvalue" (left-value), de la variable (ver ítem B.4).
Ademas del identificador "var", se tiene la palabra "int" que indica el TIPO (type) de la variable. El tipo indica:
1. CUANTAS CELDAS DE MEMORIA (bytes) se asocian a ese nombre de variable.
2. DE QUE MODO SERAN INTERPRETADOS los datos que se encuentren en tal localidad de memoria,
1- Un byte es la menor unidad de informacion que pueden direccionar la mayoria de las computadoras. En la
mayoria de las arquitecturas el tipo char ocupa un solo byte, por lo tanto es la unidad minima. Un bool admite solo
dos valores diferentes, pero es almacenado como un byte. El tipo integer ocupa generalmente 2 bytes, un long 4,
double 8, y asi con el resto de los tipos.
2- El otro punto es la relacion entre LO QUE HAY en una celda de memoria y COMO ES INTERPRETADO. Lo que hay
en una celda cuya extension es un byte es simplemente un conjunto de ocho estados posibles (8 bits) que a nivel
hardware admiten dos estados diferenciales, estados que pueden ser interpretados como 'verdadero/falso', 0/1, o
cualquier otro par de valores. Una celda de memoria del sector de datos, podria contener algo como lo siguiente:
¿Que es esto? Depende en gran parte del TIPO (type) que se haya asociado a esa celda (y suponiendo que exista tal
asociacion). Ese valor interpretado como un hexadecimal es 0x61, en decimal es 97, y si fue asociada al tipo char
representara la letra 'a', cuyo Ascii es igual a 97. En ninguna localidad de memoria hay algo como la letra 'a', lo que se
encuentra son valores binarios que en caso de estar asociados a char y en caso de que se muestre en pantalla como char
hara que se vean encendidos ciertos pixeles de pantalla, en los cuales se reconoce una representacion de la letra 'a'.
La representacion binaria de datos ocupa demasiado espacio, por ese motivo es preferible utilizar el sistema hexadecimal,
ademas de ser muy facil de traducir a binario es mas economico que este o el decimal.
Observar los bytes de un sector de memoria de un programa facilita la comprension sobre el modo en que cada tipo (type)
se asocia a direcciones de memoria.
Suponga un programa que declara, define e inicializa las siguientes variables:
int main()
{
int a = 5;
long b = 8;
char cad[ ]= "abcd";
char ch = '6';
char hh = 6;
etc....
La representacion de estos datos en memoria, en el segmento de datos, tal como lo muestra un debugger, tendra el
siguiente aspecto (se omiten caracteres problematicos para navegadores y se deja constancia que diferentes compiladores
pueden ordenar los datos de otro modo):
ffd0 ........................
ffe0 ............6abcd..
fff0 .......................
20 20 20 20 00 8F 12 00 00 00 F6 FF BC 04 00 FF
F6 F6 00 00 F6 FF C7 04 06 36 61 62 63 64 00 00
08 00 00 00 05 00 00 00 0B 01 00 00 00 00 00 00
Los datos que se han declarado primero en el codigo (int a) figuran al final, los bytes 05 00 son la representacion de la
variable entera 'a' de valor = 5, los cuatro bytes 08 00 00 00 lo son del long 'b', luego sigue el array "aaaa", el char '6' que
corresponde con el hexadecimal 0x36, y por ultimo un char seteado con el valor entero 6.
Ademas se puede realizar las siguientes observaciones:
1. Que el segmento de datos almacena los datos comenzando desde el final (0xffff). La primera variable declarada y
definida es el entero 'a', que no esta verdaderamente en el final del segmento, es asi porque esos valores (como
0B 01) guardan valores de STACK para restablecer algunos registros cuando el programa salga de main() y termine.
Sobreescribir ese valor podria producir un crash.
2. Que la variable entera de valor 5 guarda este valor ubicando los bytes al reves. Lo logico seria que la
representacion fuera 00 05, pero los bytes estan invertidos, esto es una norma general de la mayoria de los
procesadores y responde a una pauta de mayor eficiencia en la lectura de variables numericas.
3. El array 'cad' se declara de modo implicto con 5 bytes, las cuatro letras mas el caracter terminador '\0', se ocupa un
byte mas porque un numero par de bytes es mas eficiente. Observese que un array no invierte la posicion de sus
elementos.
4. Un char ocupa exactamente un byte. El primer char esta definido con el caracter '6' que corresponde al ascii 0x36,
la segunda variable char (hh) es seteada a partir de un valor entero (6) lo que genera una conversion implicita de
tipos.
Se podria profundizar mas el tema de que funcion tiene este sector de memoria, su relacion con la pila (STACK) y los
modelos de memoria, pero eso se vera en otros apartados. Por ahora es importante tener en cuenta la relacion entre el
tipo (type) usado para declarar una variable y el modo en que se almacena en memoria. En la siguiente tabla se encuentran
mas ejemplos:
DECLARACION
Inicializacion
int a;
a = 5;
char ch;
ch = 'e';
char
cad[]="hola";
-
long a;
Representacion Numero
en memoria
de bytes
05 00
2
65
1
68 6F 6C 61 00
5
a=4
04 00 00 00
4
long a;
a=0x1234
34 12 00 00
4
long a;
a = 65535
FF FF 00 00
4
Cuando en el flujo de un programa se asigna un valor a una variable lo que sucede es que la localidad (o localidades) de
memoria asociadas a las variables son seteadas con tal valor. La asociacion entre localidades de memoria y variable no
siempre existe desde el comienzo al final de un programa. Las variables declaradas como 'locales' a una funcion solo tienen
asociada una localidad de memoria mientras el flujo del programa se encuentra en tal funcion, al salir de la misma tales
localidades seran usadas por otros datos. En cambio las variables 'globales' o declaradas como 'static' conservan su
localidad de memoria durante toda la ejecucion del programa.
1.2 ¿Que es un array?
Un array (arreglo) es una coleccion ordenada de elementos del mismo tipo (type), estos tipos pueden ser los que
proporciona el lenguaje, como char, integer, float, long integer, etc., o bien puede tratarse de un tipo definido por el
programador, como una estructura de dato o una clase.
Estos elementos se encuentran ordenados en celdas consecutivas de memoria. Ver los siguientes ejemplos:
Declaracion e inicializacion
Representacion en memoria
Bytes
int a []= {3, 345, 54, 4};
03 00 63 01 72 01 03 27
2x4=8
int a[4]={2};
02 00 00 00 00 00 00 00
2x4=8
char a [] = {"Mensaje 1"};
4d 65 6e 73 61 6a 65 20 31 00
9+1= 10
char a [8] = {hola};
68 6F 6C 61 00 00 00 00
7+1 = 8
long a [] = {9, 16, 0x23b2a};
09 00 00 00 12 00 00 00 2a 3b 02 00
3 x 4 = 12
El tipo (type) del array determina cuantos bytes ocupa cada uno de sus elementos, y tambien de que modo se almacena el
dato.
Es importante mencionar que este modo de inicializacion es solo posible cuando se realiza en la misma linea que en la
declaracion, no es posible inicializar al mismo tiempo varios elementos de un array en una linea diferente a la de la
declaracion. Tambien hay que mencionar el hecho de que al dar mas elementos inicializadores que los que figuran entre
corchetes se genera un error de compilacion, al dar menos elementos el compilador setea el resto de los elementos con el
valor '0'.
Una cadena en C/C++ es representada internamente como un array de tipo char y utiliza un caracter 'terminador' para
indicar el fin de la cadena, ese caracter es el correspodiente al Ascii = 0 (para mas detalles ver cadenas estilo C/C++ en
A. Anexo 1 o directamente en http://es.geocities.com/jorge_hefty/String1.htm).
La notacion "Nombre_array[int n]" permite seleccionar cada uno de los elementos de ese array, esa expresion tiene el
mismo tipo (type) que un elemento individual, y esto puede ser importante para distinguir mas claramente las asignaciones
y conversiones posibles.
1.3 ¿Que es un puntero?
Un puntero es un tipo especial de variable, que almacena el valor de una direccion de memoria. Esta direccion puede ser la
de una variable individual, pero mas frecuentemente sera la de un elemento de un array, una estructura de datos u objeto
de una clase. Los punteros, al igual que una variable comun, pertenecen a un tipo (type), se dice que un puntero 'apunta a'
ese tipo al que pertenece. Ejemplos:
int* pint;
char* pchar;
fecha* pfecha;
//Declara un puntero a entero
//Puntero a char
//Puntero a objeto de clase 'fecha'
Independientemente del tamaño (sizeof) del objeto apuntado, el valor almacenado por el puntero sera el de una unica
direccion de memoria. En sentido estricto un puntero no puede almacenar la direccion de memoria de 'un array'
(completo), sino la de un elemento de un array, y por este motivo no existen diferencias sintacticas entre punteros a
elementos individuales y punteros a arrays. La declaracion de un puntero a char y otro a array de char es igual.
Al definir variables o arrays hemos visto que el tipo (type) modifica la cantidad de bytes que se usaran para almacenar tales
elementos, asi un elemento de tipo 'char' utiliza 1 byte, y un entero 2 o 4. No ocurre lo mismo con los punteros, el tipo no
influye en la cantidad de bytes asociados al puntero, pues todas las direcciones de memoria se pueden expresar con solo 2
bytes (o 4 si es una direccion de otro segmento).
Veamos los efectos de un codigo como el siguiente, en la zona de almancenamiento de datos:
char cad[] = "hola";
char * p;
p = cad;
//Puntero 'p' apunta a 'cad'
El puntero esta en la direccion 0xffee pero el valor que hay en esa localidad de memoria es otra direccion, los bytes "F0 FF"
indican que el puntero apunta a FFF0, donde comienza la cadena de caracteres 'cad' con el contenido 'hola' mas el cero de
fin de cadena.
En las lineas de codigo no se indica a que caracter del array apunta el puntero, pero esa notacion es equivalente a:
p = &cad[0];
que indica de modo mas explicito que se trata de la direccion del primer elemento de ese array de caracteres. El juego con
las direcciones puede ilustrarse tambien del siguiente modo:
ffee
ffef
fff0
fff1
fff2
fff3
fff4
F0
FF
61
61
61
61
00
<----- El puntero ocupa dos bytes para representar la direccion FFF0, direccion a la que 'apunta'.
<----<------ cad[0]. .Primer char del array de caracteres, direccion apuntada por el puntero
<------ cad[1]
<------ cad[2]
<------ cad[3]
<------ cad[4] Fin del array, caracter ascii = 0 de fin de cadena
Puesto que un puntero tiene como valor una direccion de memoria, es logico que al llamar a funciones de impresion con un
puntero como argumento, la salida en pantalla sea la de una direccion de memoria. Para este tipo de pruebas es
interesante usar la libreria iostream.h de C++, pues no obliga a especificar el formato (como hace printf ). Para un puntero
'p' la salida en pantalla sera algo similar a lo siguiente:
cout<<p;
printf("%p",p)
//sale: 0x8f82fff0;
//sale: FFF0
En este caso se trata de un puntero que almacena en 2 bytes una direccion de memoria, la cual es FFF0. ¿Porque razon la
impresion con 'cout' da 4 bytes? Porque agrega 2 bytes (8f y 82) para indicar el 'segmento' donde se encuentra esa
direccion. Se trata en todo caso de una misma localidad de memoria, con distinto formato de presentacion en pantalla.
La salida en pantalla de un puntero a char es diferente, pues es tratado como apuntando a una cadena de caracteres, en tal
caso no sale en pantalla una direccion de memoria, sino un conjunto de caracteres hasta encontrar el '\0'.
Un puntero puede almacenar la direccion de ("apuntar a") muy diferentes entidades: una variable, un objeto, una funcion,
un miembro de clase, otro puntero, o un array de cada uno de estos tipos de elementos, tambien puede contener un valor
que indique que no apunta actualmente a ningun objeto (puntero nulo).
1.4 Tipos definidos por el programador
Tipos como 'bool', 'int' o 'char', son "tipos predefinidos", pertenecientes al lenguaje. En C++ al igual que otros lenguajes, es
posible definir tipos nuevos. Las enumeraciones, uniones, estructuras y clases, son tipos nuevos que implementa el
programador.
La declaracion de un tipo no produce ningun efecto en memoria, no hay ningun identificador donde almacenar un dato, por
esa razon no tendria sentido, dentro de la definicion de una estructura o clase, intentar dar un valor a sus datos, seria lo
mismo que intentar dar un valor a un tipo predefinido, por ejemplo:
long = 8;
Para asignar un valor se necesita un objeto, pues un objeto implica una region de memoria donde almacenar un valor.
El almacenamiento en memoria de una union, enumeracion o estructura (C), no presenta importantes cambios respecto a
los tipos predefinidos, sus elementos se ordenaran de modo consecutivo de acuerdo a su 'sizeof'. Respecto a C, C++ aporta
un nuevo tipo predefinido, las clases, entidad que no solo es un agregado de datos sino tambien de funciones, y que por
ello presenta novedades de importancia respecto a los tipos anteriores.
1.4.1
Clases
Una clase es basicamente un agregado de datos y funciones para manipular esos datos. Las clases, y la programacion
'orientada a objetos' en general, ha representado un gran avance para produccion de software a gran escala, los recursos
de herencia, encapsulamiento, ocultacion de datos, clases virtuales, etc., estan pensados con esa finalidad. Aqui solo nos
detendremos en la nocion minima de 'clase' y el modo en que es almacenado un objeto en memoria.
Suponga una clase muy simple:
class gente
{
char nombre[10];
int edad;
public:
gente (char*cad, int a)
{
strcpy(nombre,cad);
edad = a;
}
};
Se trata de una clase cuyos miembros son dos datos y una sola funcion. Una vez declarada la clase se puede definir objetos
como pertenecientes a ese tipo. Una clase no ocupa espacio, pero si un objeto perteneciente a esa clase. El espacio
ocupado en memoria por tal objeto puede ser conocido a traves de 'sizeof'.
gente pp1;
cout<<sizeof(pp1);
//saca en pantalla '12'
El valor podria ser ligeramente diferente segun el compilador, por efecto de optimizacion. Lo importante es observar que el
monto de memoria del objeto (retornado por sizeof), esta determinado por la suma del espacio ocupado por los datos,
'sizeof' no tiene en cuenta a la funcion.
Cada objeto de tipo 'gente' ocupara 12 bytes, pues posee una copia individual de los datos de clase, en cambio hay una sola
copia del miembro funcion (aqui el constructor) utilizado por todos los objetos.
Al declarar dos objetos de tipo 'gente':
gente pp1("gerardo", 33);
gente pp2("miguel", 34);
Al observar ahora que efectos producen estas entidades 'pp1' y 'pp2', en memoria. Los datos que se utilizan se obtienen en
TurboC++ (cualquier version) posando el cursor sobre el objeto de interes (aqui 'pp1' y 'pp2') y pulsando 'Alt+f4', tambien al
consultar los registros de la CPU (con "Windows/Registers"). En un programa, que define la clase 'gente' y dos objetos (pp1
y pp2) inicializados como muestran las lineas de codigo previas, se puede observar lo siguiente:
El valor especifico de cada dato (como el valor de segmento) puede variar con cada ejecucion, lo que cuenta es la relacion
entre tales valores. Interpretando estos datos.
1- En la ventana de cada objeto (pp1 y pp2) figura en primer lugar la direccion de memoria donde almacena sus
valores, ambas direcciones tienen el mismo valor de segmento (0x8F86), que coincide por otra parte con el valor
de DS (segmento de datos) y de SS (segmento de stack) de la CPU. Sus direcciones difieren ligeramente en offset,
la resta de los mismos (0xFFEA - 0xFFDE) es igual a 12, que es el espacio que ocupa (en bytes) cada objeto en
memoria.
2- Esos 12 bytes por objeto corresponden a 10 para la cadena de caracteres ('cad') y 2 para almacenar el entero
('edad'). Estos datos estan almacenados alli donde indica el offset, no en otro sitio, por lo tanto un puntero al
objeto 'pp1' apuntara (en este caso) a la misma direccion de memoria que un puntero a su elemento 'cad', y otro
tanto para 'pp2'. Los datos miembros se exponen con sus nombres a la izquierda y el valor que contienen a la
derecha. La cadena de caracteres es terminada en '\0' (seguido de caracteres aleatorios), y el entero es mostrado
en formato decimal y hexadecimal
3- Debajo, y separado por una linea, se encuentra un espacio donde se enumeran las funciones miembro de la clase.
Alli se encuentra el prototipo de la funcion miembro y al lado la direccion de memoria donde se inicia su codigo.
Ese es el valor que almacenaria un puntero a esa funcion. Observese que tal direccion es la misma para ambos
objetos, por la razon antes mencionada de que hay solo una copia de funciones miembro por objeto. El segmento
donde se encuentra tal funcion se corresponde con el valor que muestra la ventana CPU para CS (segmento de
codigo).
Se puede sintetizar lo visto respecto a clases del siguiente modo:
• Una clase no es un 'dato' (es un tipo), no tiene una localidad de memoria asociada y por lo tanto no puede almacenar
ningun valor.
• Un objeto de tal clase si define una region de memoria, un espacio de almacenamiento de datos. Esta es la diferencia
entre 'clase' y 'objeto'.
• Cada objeto de una misma clase posee una copia propia de cada uno de los datos miembros de la clase, pero comparte
una misma copia de las funciones miembros.
Por otra parte, un array de objetos (instancias de clase) es almacenado como una sucesion consecutiva, mientras que un
puntero a objeto sera (como todo puntero) un par de bytes que apunte a una direccion de memoria donde se almacena el
objeto.
2 Punteros I
2.1 Declaracion e inicializacion
Un puntero, como cualquier variable u objeto, ademas de ser declarado (para comenzar a existir) necesita ser inicializado
(darle un valor de modo controlado), lo cual se realiza mediante el operador de asignacion ('='). Desde que el puntero es
declarado almacena un valor, el problema es que se trata de un valor aleatorio, intentar operar con un puntero sin haberlo
inicializado es una frecuente causa de problemas.
2.1.1 Asignacion erronea: "Cannot assign..."
En primer lugar veamos un caso simple de asignacion erronea para comprender el mensaje enviado por el compilador.
void main(){
int a;
int* b;
b = a;
}
//Error
El programa no compila, y se recibe el mensaje de error:
"Cannnot assign 'int' to 'int near*' en function main();
Se ha tratado de inicializar el puntero 'b' asignandole un valor equivocado, de otro tipo. El analisis de los mensajes de error
siempre es instructivo, al profundizar en este:
1- En primer lugar: ¿Que es un "int near*"? Los punteros pueden ser clasificados como 'near' (cercano) o 'far' (lejano)
de acuerdo a si la direccion apuntada se encuentra en el mismo segmento que el puntero. Las principales
diferencias se exponen en el siguiente cuadro.
Tipo de puntero Caracteristicas
Cantidad de bytes que utiliza el puntero
Near
La direccion apuntada se encuentra
dos - (offset)
en el mismo segmento que el puntero
Far
Se encuentran en diferente segmento cuatro - (segmento::offset)
Si un programa no requiere de una gran cantidad de datos significa que pueden entrar en un solo segmento, y los
punteros seran 'near' por defecto, en caso contrario el default sera 'far'. Esto es determinado directamente por el
modelo de memoria utilizado por el programa.
2- En segundo lugar, el mensaje indica una discordancia de tipos. Uno es 'int', y el otro es 'int near*', al obviar la
caracteristica de 'near' se ve que la expresion "int*" coincide con la declaracion del puntero. Lo mas instructivo de
esto es comprender que el asterisco pertenece al tipo (type), no al nombre ('b'). Algunos autores discuten sobre
cual de las dos siguientes declaraciones es la mas adecuada para declarar un puntero:
int *b;
int* b;
Ambas son perfectamente validas, la unica diferencia es que el primer caso se sugiere que '*' forma parte de 'b', y
en el segundo que '*' forma parte del tipo.
Lo recomendable es adoptar la segunda forma, la primera se presta a confundir el operador '*' con el operador de
'indirection', y es muy importante comprender que aqui no hay nada de 'indirection', es solo una declaracion de un
identificador (b), ligado a un tipo (int*). Es el mensaje del compilador el que indica esta ultima interpretacion.
Para que el programa compile sin problemas es necesario utilizar el operador '&' antes del nombre de la variable, el efecto
del operador es devolver la direccion en memoria de la variable, la cual se asigna naturalmente a un puntero.
void main(){
int a;
int* b;
b = &a;
}
//El puntero 'b' apunta a 'a'.
Una variable individual de tipo 'T' y un array de elementos de tipo 'T' pertenecen a tipos diferentes, no es posible la
asignacion entre un entero y un array de enteros. Al intentar sin embargo tal asignacion:
void main(){
int a;
int b[4];
a = b;
}
//Asignacion erronea, no compila
Lo mas interesante del ejemplo es que el mensaje de error es similar (pero inverso) al primer ejemplo fallido:
"Cannnot assign 'int near*' to 'int' en function main();
Lo cual puede resultar sorprendente, pues en el ejemplo no se ha declarado ningun puntero, solo un entero y un array de
enteros. Lo que esta sucediendo es que el compilador se esta refieriendo al array de enteros como un "int near*", como un
puntero.
Un array y un puntero no son exactamente lo mismo, hay algunas diferencias, pero la relacion que existe entre ambos es
muy estrecha y la sintaxis aplicable a ambas entidades es en gran parte identica. Esta relacion explica que el siguiente
ejemplo compile bien sin ninguna complicacion:
void main(){
int a [4];
int* b;
b = a;
}
//o bien --> b = &a[0];
Podria haberse esperado algun problema, puesto que no se ha obtenido la direccion del array con el operador '&', pero no
ocurre asi, el solo nombre del array es tomado como sinonimo de la direccion de su primer elemento (o puntero a su
primer elemento).
Bien, esta ha sido una introduccion para comprender el mensaje de error tipico en una inicializacion fallida. El intento de
asignar una variable individual a un array produce un mensaje de error distinto ("Lvalue requerido"), ver B Anexo2.
Veamos ahora ejemplos de inicializaciones de punteros correctas.
2.1.2 Opciones de inicializacion
Un puntero puede ser inicializado con la direccion de memoria de un objeto, tal objeto debe pertenecer a un tipo acorde al
tipo al que apunta el puntero. Puede tratarse de la direccion de un elemento de un array o de una variable individual, el
operador '&' antepuesto a un objeto nos devuelve su direccion de memoria. Tambien puede utilizarse un "literal", ya sea
numerico, de caracter, o de otro tipo, y puede inicializarse como puntero nulo, en este caso esta permitido usar el 0, el
unico entero permitido, por su equivalencia con el valor NULL.
Suponiendo un tipo cualquiera "T", son inicializaciones validas las del siguiente cuadro. Sobre este cuadro caben las
siguientes aclaraciones:
1. Inicializar un puntero apuntando al primer elemento de un array admite dos notaciones equivalentes, en la segunda se
sobreentiende que el elemento apuntado es el primer elemento del array.
2. La equivalencia entre el valor 0 (cero) y NULL es de uso general, sin embargo existen compiladores que dan a NULL un
valor diferente a cero.
3. Un 'literal' debe ser el apropiado para el tipo de puntero inicializado. Si es un puntero a char, una cadena de caracteres
cualquiera (ej: "hola") sera un literal adecuado, si se trata de tipo numerico, para un int "4" sera apropiado.
Puntero inicializado a Declaracion e inicialización Declaracion e inicialización
partir de:
en una misma linea
desdobladas
Un objeto individual
T x;
T* ptr = &x;
T* ptr;
ptr = &x;
T* ptr = &x[0];
T* ptr;
ptr = &x[0];
T* ptr = x;
T* ptr;
ptr = x;
Otro puntero del mismo
tipo
T* ptr = x;
T* x;
T* ptr;
ptr = x;
T* ptr = 0;
T* ptr;
ptr = 0;
T* ptr = NULL;
T* ptr;
ptr = NULL;
T* ptr = [literal]
T* ptr;
ptr = [literal];
Un array de objetos
T x [10];
Valor 0 = puntero nulo
Null=0
Un literal
Al tomar como ejemplo el tipo "char", siguiendo al cuadro anterior, se tiene las siguientes opciones de inicializacion:
Puntero inicializado a partir Declaracion e inicializacion en Declaracion e inicializacion
de:
una misma linea
desdobladas
Un elemento
char ch;
char* p = &ch;
char* p;
p = &ch;
Un array
char* p = cad;
char* p;
p = cad;
char cad[10];
char* p = &cad[0];
char* p;
p = &cad[0];
Valor 0 = puntero nulo
char* p = 0;
Null=0
(0 es el unico valor entero
que puede inicializar un char* p = NULL;
puntero)
char* p;
p = 0;
Otro puntero (ya inicializado)
char* p = ptr;
char *ptr;
char* p;
p = ptr;
Un literal de cadena
"casa";
char* p;
p = "casa";
char* p = "casa";
char* p;
p = NULL;
Se ha insistido lo suficiente en que un puntero almacena como valor una direccion de memoria, por eso la presencia de un
'literal', en esta ultima tabla el literal de cadena "casa", puede sorprender. Es importante tener claro que todos los literales
se almacenan desde el comienzo del programa en un lugar del segmento de datos, no es posible obtener su direccion (por
medios normales) pero existe, es al comienzo de dicho segmento, el mismo sitio que se reserva a valores constantes y
variables globales.
Un literal es tratado como constante, esto es lo que permite que una funcion pueda retornar una constante sin temor a que
dicho almacenamiento se pierda al salir de la funcion, un literal no es una variable 'local'. No hay obstaculos para inicializar
un puntero con una variable constante, por lo tanto lo mismo se aplica a literales de cualquier tipo.
Las tablas anteriores no abarcan todos los casos posibles de inicializacion de punteros, aun no se han mencionado los casos
donde el puntero apunta a una funcion o un objeto miembro de una clase, ni la opcion de inicializar a traves de memoria
dinamica.
2.1.3 Inicializacion a traves de memoria dinamica
Esta modalidad se diferencia de todas las enumeradas hasta ahora y puede considerarse como la principal. Todas las formas
vistas hasta aqui asignan al puntero la direccion de memoria de otra entidad (elemento, array, puntero, literal) ademas del
caso especial del valor NULL. Ya se ha mencionado que la declaracion de un puntero no implica la reserva de memoria,
salvo 2 bytes para almacenar una direccion, por esa razon podria decirse que el puntero, cuando es inicializado por otro
elemento, 'vive' de la memoria que le aporta el objeto al que apunta.
La reserva de memoria dinamica requiere el uso obligado de un puntero, el cual apuntara al comienzo de la zona reservada.
Lo diferente aqui es que se trata del unico caso donde el puntero no necesita de otro elemento que le aporte memoria
necesaria, no necesita apuntar a algun otro objeto. Cuando se reserva dinamicamente 40 bytes para un puntero a char, se
opera con el puntero 'como si' apuntara a un segundo objeto (un array de caracteres), pero tal array no existe.
A pesar de no existir propiamente un segundo objeto, sigue siendo esencial, el tipo (type) segun el cual se declara el
puntero, pues esto determina el modo en que el puntero permitira acceder a tal zona de memoria. No hay un ‘objeto
apuntado’, pero el puntero se conduce igual que si lo hubiera, por esa razon hablaremos en general del ‘objeto apuntado’
por el puntero, sin aclarar el caso especial que se esta considerando.
En C++ la reserva y liberacion de memoria dinamica se realiza a traves de los operadores new y delete, y su sintaxis, para un
puntero de nombre ‘ptr’ es la siguiente:
Elemento
de tipo ‘T’
individual
Reserva
Liberacion
T* ptr = new T;
delete ptr;
Array de 'n' elementos
T* ptr = new T[n]; delete [] ptr;
de tipo 'T'
Atraves del operador new se solicita una cierta cantidad de memoria dinamica, es posible que no exista suficiente memoria
disponible, en tal caso el operador devolvera un puntero NULL (o apuntando a 0), y es por esta razon que luego de una
solicitud es recomendable inspeccionar si el puntero devuelto es nulo. Esta seria la respuesta 'clasica' a una reserva fallida
de memoria dinamica, sin embargo existen diferentes compiladores que, ajustandose al standard C++ no devuelven un
puntero nulo sino que lanzan una excepcion (bad_alloc).
Algunos viejos compiladores no reconocen la opcion de borrar el puntero con corchetes vacios y exigen que se especifique
el numero de bytes a borrar (los mismos que los reservados), TC++ a partir de su version 3.0 admite esa notacion. Escribir
"delete ptr;", sin los corchetes, solo libera el primer elemento del array, y es por lo tanto un error importante.
2.2 Desreferenciacion ("indirection")
2.2.1 Concepto
Un puntero almacena una direccion de memoria de alguna entidad, esto en si mismo no seria demasiado util si no fuera
posible, a traves del puntero, acceder a lo que esta almacenado en esa direccion. Segun el creador de C++: "La operacion
fundamental de un puntero es desreferenciar, es decir, referir al objeto al que apunta el puntero." (Stroustrup, 1997). A
continuacion se desarrolla esta definicion.
La funcion de 'desreferenciar' un puntero es llevada a cabo por el operador '*', que ademas cumple otras funciones en C++.
Como su papel es complementario a una de las funciones del operador '&' se comenzara estudiando la relacion y diferencia
de estos dos operadores
Ambos operadores tienen mas de un sentido dependiendo del contexto en que aparecen, por lo tanto son casos de
sobrecarga de operadores Sus distintos usos aparecen en la siguiente tabla. El primer uso de cada operador se distingue
claramente de los otros dos, derivan de C y no tienen relacion con el tema punteros. Los que figuran en segundo lugar
pertenecen a la sintaxis basica de declaracion de punteros y referencias. Nos concentraremos en el tercer significado de
estos operadores.
OPERADOR
Usos
*
&
Multiplicacion
int a = 3, b=2,c;
c = a * b;
Operacion Bitwise AND
char a=0x37;
a &=0x0F;
Declaracion type puntero
int n;
int* p = n;
Declaracion del type referencia
int a;
int &b = a;
Dereferencing (indirection) Referencing
cout<<*p;
cout<<&a;
El papel opuesto y complementario
dadas las siguientes declaraciones:
del
tercer
uso
de
ambos
operadores
se
podria
sintetizar
asi:
int v = 4;
int* p = &v;
El puntero 'p' es equivalente a la direccion de memoria a la que
apunta.
La variable 'v' es equivalente al valor que almacena
cout<<p
saca en pantalla una direccion de memoria
(por ej: 0x8f70fff0)
cout<<v
saca en pantalla '4'
Mientras que la expresion '*p' es sinonimo del elemento Mientras que la expresion '&v' es un sinonimo de la
individual que se encuentra en la localidad apuntada por el direccion de memoria donde se encuentra esa variable
puntero
cout<<&v saca en pantalla una direccion de memoria
cout<<*p saca en pantalla '4'
(ej: 0x8f70fff0)
Como puede observarse, el efecto de ambos operadores es inverso, en un caso dada una localidad de memoria se accede al
elemento almacenado en ella (el caso de '*'), en el otro ('&') dada una variable se accede a la direccion de memoria donde
almacena su valor.
El termino usado para este efecto del operador '*' es el de 'indirection' o 'dereferencing' traducido generalmente como
'indireccion' o 'desreferenciacion'. Su sentido mas llano seria: operador que permite referirnos al elemento individual
apuntado por el puntero, en lugar de la direccion en que ese elemento se encuentra almacenado.
A veces se utiliza, para ejemplificar la 'indirection', un puntero que apunta a char, estos ejemplos pueden oscurecer el
sentido del termino 'indirection', en especial porque con tales punteros la linea "cout<<p" no hubiera sacado en pantalla
una direccion de memoria, sino una cadena de caracteres.
2.2.2 El caso especifico de un puntero a char
Dadas las siguientes declaraciones e inicializaciones:
char cad[] = "hola";
char* ptr = cad; //Aqui el '*' es un indicador de tipo, no de 'indirection'
El puntero 'ptr' apunta a 'cad', al char inicial de 'cad'. Veamos que saldria en pantalla con 'p' y con '*p', para el caso usamos
la libreria "iostream.h" de C++, pues las funciones de C impondrian su formato a la salida.
cout<<ptr;
cout<<*ptr;
//sale en pantalla: "hola"
//sale en pantalla: 'h'
Lo que puede desorientar aqui es que 'ptr' no imprima en pantalla una direccion de memoria, que es lo esperable
tratandose de un puntero. Se trata de una caracteristica propia de las funciones que tratan con punteros a char, y no de un
rasgo diferencial de los punteros a char, estos tienen las mismas caracteristicas generales de cualquier puntero.
Utilizando una funcion C de "stdio.h", las lineas anteriores son equivalentes a
printf("%s", ptr);
printf("%c", *ptr);
Al analizar el funcionamiento de printf, esta funcion recibe como argumento un puntero a char, algo cuyo tipo es char*, es
decir una direccion de memoria. En C o C++ no hay otro modo de pasar un array a una funcion que a traves de una
direccion de memoria. El especificador de formato, "%s", le indica a la funcion que interprete esa direccion como siendo el
comienzo de una cadena de caracteres (la 's' es de 'string'). Lo que la funcion hace es interpretar los bytes, uno a uno, como
indicando caracteres ascii, y los sacara ordenadamente en pantalla hasta encontrar un '\0', sin importar donde se encuentre
ese '\0' o si excede o no la capacidad del array original.
En C++, el flujo de salida 'cout' y el operador de insercion '<<', no requieren de un formato especifico para sacar algo en
pantalla, esto significa que imponen un formato predeterminado segun el dato enviado como parametro. En el caso de que
este parametro sea un puntero a char imponen el formato "cadena de caracteres", exactamente igual que printf con
formato "%s". Esto no es obvio, dado que se trata de un puntero podrian sacarlo en pantalla como una direccion de
memoria, pero no ocurre asi.
Es por esta razon que la idea de 'indirection' se oscurece en relacion a 'punteros a char', pues las funciones standard de
impresion en pantalla de C y C++ no tratan a tal puntero como una direccion de memoria mas (aunque lo sea).
Siendo 'p' un puntero a tipo char, para las funciones standard de impresion: 'p' es la cadena apuntada, '*p' el caracter
individual apuntado.
2.3 Asignacion de punteros
Un puntero puede ser asignado a otro puntero del mismo tipo a traves del operador '='. El significado de tal asignacion es
similar al de una asignacion entre variables, el valor almacenado en el elemento de la derecha se copia en el elemento de la
izquierda. Solo que en el caso de punteros este valor es una direccion de memoria, y esto puede producir un efecto distinto
al esperado.
void f (char* cad1, char* cad2) {
cad1 = cad2;
*cad1 = '3';
//Efecto: modificacion de cadena "dos".
//.................................
}
char uno = "1111";
char dos = "2222";
f(uno, dos);
//Llamado a funcion f();
La funcion 'f()' recibe dos punteros a char desde otra funcion, sigue luego una asignacion de 'cad2' en 'cad1', y una
modificacion de un char a traves de desreferenciacion del puntero.
Si la intencion era copiar el contenido de la cadena original "dos" en la cadena "uno", para modificar "uno" sin alterar "dos",
estamos ante un error. El caracter '3' se copiara en la cadena original "dos", por la razon de que luego de la asignacion de
punteros (cad1=cad2) ambos apuntan a "dos".
Hay casos donde puede ser util que dos punteros apunten a una misma direccion, y entonces sera correcto asignar
punteros mediante el operador '=', pero si lo que se busca es copiar el contenido de un array, entonces se debe hacer de
otro modo, copiando uno a uno los elementos de dicho array.
Dados dos punteros ("pt1" y "pt2") a array, que apuntan a direcciones diferentes (son dos arrays diferentes), los efectos de
una asignacion de punteros y copia de array son los siguientes:
Operacion
Efecto
pt1 = pt2;
Asignacion de punteros. Lo que se copia realmente son los 2 (o 4) bytes de direccion de
memoria.
El puntero 'pt1' deja de apuntar al array original, ahora apunta a la misma direccion que 'pt2'.
while (*pt2!=0)
*pt1=pt2;
Copia de array. La copia se realiza elemento por elemento. Se copian tantos elementos como
caracteres tenga el array 'pt2'. En el caso de una cadena de caracteres, podemos confiar en el
'\0' para saber cuantos elementos copiar.
Es muy importante diferenciar ambas operaciones. Un array no puede ser copiado mediante el operador de asignacion '=',
hay que copiar elemento por elemento, un puntero puede ser copiado con tal operador, pero el efecto provocado puede
ser distinto al efecto deseado.
La confusion entre copia de punteros y copia de array puede provocar otro tipo de problemas en relacion a memoria
dinamica o constructores de copia, problemas que se analizan mas adelante.
3 Punteros II
3.1 Punteros a 'void'
Un puntero puede apuntar a un objeto de cualquier tipo, predefinido por el lenguaje o definido por el usuario. 'Void' es el
nombre de un tipo, pero su uso esta sujeto a mayores restricciones respecto a otros tipos.
El termino 'void' puede ser usado como tipo de una funcion o de un puntero, pero no para declarar un objeto. Las
declaraciones posibles de tipo void, en C y C++, se resumen en el siguiente cuadro.
Declaraciones
C
Objeto de tipo void
Ej:
void x;
Retorno
de
función
Ej:
void func(){
......................
Puntero a void
Ej:
void* p;
C++
No permitido. Mensaje de error : "Objeto de sizeof desconocido."
El compilador no esta en condiciones de determinar el monto de memoria
que requiere el objeto.
un
Significa que la funcion no retorna ningun valor
('tipo pseudodevuelto').
Un puntero a void es
Puntero a objeto de tipo desconocido.
tratado como un puntero a
Requiere conversion explicita a otro tipo
char.
antes de ser utilizado.
Conversion implicita.
En C se accede a bytes no vinculados a ningun tipo mediante punteros a char, esto es natural si se considera que un 'char'
ocupa 1 byte de almacenamiento, y de ahi que exista conversion implicita entre ambos tipos.
La funcion C standard "memset()", retorna un puntero a void. El siguiente codigo es aceptable en C:
void f(char*cad, char ch, int n){
char* s;
s = memset(cad,ch,n);
//conversion implicita de void* a char*
}
//valido en C, pero no en C++.
C++ no lo permite debido a su mas estricta comprobacion de tipos. Lanza el mensaje de error, "no se puede convertir void*
a char*". Hay otras funciones similares a memset que derivan de C y retornan void, algunas declaradas en "mem.h" y
"string.h". Todas pueden ser utilizadas igualmente en C++, pues la cadena afectada por la funcion es enviada como
parametro. Si se quisiera utilizar el puntero de retorno seria necesario un puntero a "void" o una conversion explicita, por
ejemplo:
s = (char*)memset(cad,ch,n);
//valido en C++
El significado en C++ de un puntero a 'void' es el de un puntero que apunta a una zona de memoria no inicializada,
memoria 'en bruto', o en la cual se encuentra almacenado un objeto de tipo desconocido, en general se trata de codigo que
trabaja a nivel hardware o relacionado con administracion de memoria.
Las operaciones permitidas y no permitidas, en C++, para un puntero a void se resumen en el siguiente cuadro:
1- Asignar a void* un puntero de cualqueir tipo
Operaciones 2- Asignar un void a otro void
permitidas 3- Convertir explicitamente un void a otro tipo
void*=T*
void* = void*
T* = (T*)void*
4-Comparaciones de igualdad o desigualdad entre void* (void*!=void*)
1-Usar un void
(void*)++;
Operaciones 2-Convertir implicitamente un void a otro tipo (no void) T* = void*
prohibidas 3-Desreferenciar un void.
*v;
4-Asignar a void punteros a funciones o a miembros
3.2 Punteros y 'const'
Un puntero implica la intervencion de dos elementos: el puntero y el objeto apuntado (salvo que sea nulo). El termino
reservado "const" puede tener dos significados diferentes segun el sitio que ocupe en la declaracion, haciendo constante al
puntero o al objeto apuntado.
3.2.1 Puntero constante
El operador '*const', en lugar de '*' solo, declara al puntero como constante, esto significa que la direccion a la que apunta
el puntero no puede cambiar en todo el programa. La variable apuntada si puede cambiar. Ejemplo:
int a = 5;
int *const ptr = &a;
*ptr = 4;
ptr = NULL;
//Puntero constante a int
//Bien, se modifica la variable
//Error, intento de modificar el puntero constante
Al no poder modificar la direccion a la que apuntan, estos punteros se aproximan al sentido que tiene una referencia.
3.2.2 Puntero a constante
Aqui el termino "const" afecta al tipo al que apunta el puntero.
int a = 5;
const int* ptr = &a;
ptr = NULL;
//Bien, el puntero puede cambiar, ser reasignado
*ptr = 6;
//Error. No se puede cambiar el objeto apuntado.
Es importante observar que en el ejemplo la variable 'a' no fue declarada originalmente como "const", pero el puntero la
toma como "constante". En este sentido, aunque la variable no puede ser modificada a traves de ese puntero, si podria
serlo a traves de otro identificador, el propio nombre de la variable u otro puntero que no apunte a const.
Declarar un puntero a const suele ser util al declarar argumentos de funciones, sirve para especificar que el argumento
puntero no puede ser modificado dentro de la funcion. La funcion C standard "strcpy", en su declaracion:
char* strcpy (char*p, const char* q);
impide que el segundo argumento sea modificado por la funcion. La funcion copia el contenido de 'q' en 'p', por esa razon el
primer argumento, que no es 'const', sera modificado. Esto no significa que al llamar a la funcion el segundo parametro
necesite ser una constante, es 'tomado' como constante por el puntero de la funcion.
Como se ha visto, se puede asignar una variable no constante a un puntero a constante, esto por la razon de que no puede
producir ningun perjuicio, un puntero a const es un puntero con restricciones. Lo inverso, asignar una variable constante a
un puntero que no apunte a const, no esta permitido, pues se perderia el sentido de haber restringido la operatividad de la
variable y existiria el peligro de modificar sus datos. El siguiente cuadro resume la sintaxis de punteros y 'const':
Entidad
Ejemplo
Comentario
Puntero
constante
int *const ptr = &a;
Puntero constante, no puede modificar la direccion a
la que apunta.
const int* ptr = &a;
Puntero que apunta a const. No puede modificarse el
objeto apuntado a traves de ese puntero.
Puntero a const
int const* ptr = &a;
Puntero
a const
const
const int *const ptr = &a;
Notacion alternativa para puntero a const.
No puede modificarse la direccion apuntada ni el
objeto a traves de ese puntero.
3.3 Puntero nulo ("Null pointer")
Algunos autores definen a este puntero como "aquel que no apunta a ningun sitio" y otros como "un puntero que no
apunta a ningun objeto" (Stourtrup, 1997). La segunda definicion es mas clara, mientras que la primera puede introducir
alguna confusion. De hecho no esta claro que podria significar 'no apuntar a ningun sitio'. El concepto de 'puntero nulo'
existe por la necesidad practica de hablar de un puntero que no esta ligado a ningun objeto, muchisimas funciones de las
librerias de C/C++ devuelven punteros y entre los posibles valores de retorno cuentan con el de puntero nulo (ej: strchr () ),
tambien se presenta cuando se solicita memoria dinamica, el operador 'new' retorna un puntero nulo si no hay memoria
suficiente (no todos los compiladores).
La localidad de memoria donde esta el puntero contiene siempre algun valor (¡no existen celdas vacias! ¡cero es un valor!).
Un puntero apunta a una direccion, la indicada por el valor que almacena, por lo tanto es logico concluir que un puntero
siempre apunta a algun sitio.
Lo que distingue a un puntero nulo no es que 'no apunte a ningun sitio' sino que apunta a alguna localidad de memoria
que, por convencion del compilador utilizado, no puede estar asociada a ningun objeto o variable. Ese valor, esa direccion
'prohibida' para almacenar alli algun objeto, varia para diferentes compiladores, para Borland (y la mayoria) es la direccion
0 (cero) del segmento de datos. Cuando el puntero apunta a la localidad 0 el compilador considera que su valor es 'null', o
lo que es lo mismo, para este compilador 'null' es equivalente a cero. Esa direccion existe y es el comienzo del segmento de
datos-stack, puede ser visualizado, y si no hay errores su valor deberia ser 0.
Existe otro concepto que no debe confundirse con el de puntero nulo, el de "wild pointer". Un puntero nulo apunta a un
sitio bien determinado, en cambio un 'wild pointer' puede estar apuntando a cualquier sitio, una direccion indeterminada
dentro del segmento.
3.4 Puntero a puntero
Un puntero almacena la direccion de un objeto, puesto que ese objeto puede ser otro puntero, es posible declarar un
puntero que apunta a puntero. La notacion de puntero a puntero requiere de un doble asterisco, '**', la sola notacion suele
generar un efecto de confusion considerable, y es la razon de que Mats Henricson y Erik Nyquist, en Rules and
Recommendations on C++, sugieran en lo posible reemplazar punteros a punteros por alguna otra alternativa (una clase con
miembro puntero) en su Rec 48.
Sin embargo, el concepto en si mismo no es complejo. La relacion entre una variable comun, un puntero y un puntero a
puntero se muestra en las siguientes lineas:
int a = 4;
int* pt1 = &a;
int**pt2 = &pt1;
Por un lado se tiene el valor que almacena la variable 'a', el puntero 'pt1' almacena la direccion de esa variable, y el puntero
'pt2' almacena la direccion del puntero 'pt1'. Son tres identificadores, cada uno tiene un doble aspecto: la localidad de
memoria donde se asienta, y el valor que almacena en esa localidad de memoria.
Declaracion
inicializacion
e Direccion de memoria Valor que almacena en
(hipotetica)
tal direccion de memoria
int a = 4;
0xfff6
4
int* pt1 = &a;
0xfff4
0xfff6
int**pt2 = &pt1;
0xfff2
0xfff4
Es interesante comprobar las diferentes salidas en pantalla de 'pt2' en los siguientes casos:
cout<<pt2;
//Imprime la direccion del propio puntero 'pt2', aqui: "0xfff2"
cout<<*pt2;
//Imprime la direccion almacenada en 'pt2', "0xfff4"
cout<<**pt2;
//Imprime el valor almacenado en '*pt1 = a', "4".
El comportamiento de la salida en pantalla es coherente, pues se cumplen las siguientes igualdades:
*pt2
*(*pt2)
*pt1
**pt2
==
==
==
==
pt1;
*(pt1);
a;
a;
//Desreferenciacion de 'pt2'
//Aplicamos '*' a ambos lados
//De esto y la linea previa se deduce...
//...esta igualdad
Leanse las anteriores lineas como 'igualdades' (comparaciones que dan 'Verdadero') y no como asignaciones.
La estrecha relacion existente entre los conceptos de puntero y array, es la razon de que el asterisco doble (**) pueda ser
interpretado indistintamente como puntero a puntero, o bien como un array de punteros.
4 Punteros III
4.1 Puntero a funcion
Es posible tomar la direccion de una funcion y asignarla a un puntero. Una funcion tiene una direccion, esta se encuentra
dentro
del
segmento
de
codigo
y
marca
el
comienzo
del
codigo
para
esa
funcion.
Un puntero a funcion se declara especificando el tipo devuelto y los argumentos aceptados por la funcion a la que apunta,
estos dos elementos del puntero deben coincidir con los de la funcion.
La sintaxis para estos punteros es la siguiente:
FUNCION
Type devuelto
Nombre
(argumento/s)
Ejemplo:
Int
f1
(int, char*);
PUNTERO A FUNCION
Type devuelto
(*Nombre)
(argumento/s)
Ejemplo:
Int
(*pf)
(int, char*);
Inicializacion de puntero
pf = &f1; (Las dos formas están pf = f1; bien)
a funcion
La sintaxis de una expresion como:
int (*pf) (int, char*);
puede resultar poco obvia, a veces es comodo definir un tipo (con typedef) para simplificar las declaraciones. Tambien
puede declararse un array de punteros a funcion (todas deben coincidir en tipo devuelto y parametros), un ejemplo de
ambos recursos se ve a continuacion:
typedef void (*pmenu) ();
pmenu Archivo [] = {&Abrir, &Guardar, &Cerrar, &Salir};
En primer lugar definimos un tipo, que es un puntero a funciones. Ese tipo nos permite inicializar otros punteros a
funciones, en este caso un array de punteros a funciones.
Invocar un puntero a funcion no requiere de desreferenciacion y es muy similar a un llamado comun de funcion. Su sintaxis
es:
Nombre_de_funcion (argumento/s);
//Puntero a funcion
Nombre_de_funcion [indice] (argumento/s); //Para un array de punteros a función
Un puntero a funcion solo puede ser inicializado utilizando la direccion de una funcion, debe existir concordarncia entre
funcion y puntero respecto a tipo devuelto y argumentos. Se trata de un puntero especial, no requiere almacenamiento
extra de memoria y no esta hecho para itinerar ni para aritmetica de punteros, solo para almacenar la direccion de una
funcion, por medio de la cual esta es invocada.
4.2 Punteros a objetos
Pueden declararse punteros que apuntan a objetos instancias-de-clase de cierto tipo, la sintaxis a utilizar para la
declaracion es la comun, solo es diferente el modo en que es invocado un dato o funcion miembro. Con tal fin se utiliza el
operador "->".
Al igual que punteros que apuntan a tipos definidos en el lenguaje, un puntero a objeto, luego de declarado, puede ser
inicializado con la direccion de memoria de un objeto conveniente (del mismo tipo), sea de un objeto individual o un array,
tambien se lo puede inicializar como puntero nulo o a traves de otro puntero ya inicializado. Sin embargo estas opciones
hacen al puntero una entidad dependiente de la memoria que se haya reservado para los objetos que asignan su direccion
al puntero. Para que el puntero tenga respaldo independiente en memoria la via indicada es la reserva de memoria
dinamica para el mismo.
Si existiera una clase llamada Fecha, una declaracion de puntero a objeto y la reserva de memoria dinamica tendria la
forma:
Fecha* hoy;
hoy = new Fecha;
O bien:
Fecha* hoy = new Fecha;
La cantidad de memoria a reservar sera calculada de modo automatico a traves del sizeof de la clase. Ahora bien, el sizeof
de una clase no es un valor dinamico, es un valor fijo que queda establecido en tiempo de compilacion y no se modifica, de
modo que si miembros de la clase necesitaran memoria dinamica esto deberia implementarse de algun modo en otro sitio,
especificamente a traves de un constructor.
4.3 El puntero implicito "this"
Cuando una funcion miembro (metodo) es invocada para un objeto, la funcion siempre recibe un parametro extra, no
declarado, que es un puntero al objeto que invoco la funcion, ese puntero recibe el nombre generico de "this", y puede ser
usado de modo implicito o explicito. Veamos esto detenidamente.
Una clase puede contener miembros 'privados', 'publicos' o 'protegidos', para este tema solo se consideran los dos
primeros. Se trata de especificadores de acceso. Uno de los primeros pasos al manipular clases es la comprobacion de que
un dato declarado como 'private' no puede ser accedido de modo normal en el cuerpo de cualquier funcion. Para poder
acceder a los datos privados se debe utilizar funciones miembros de la clase donde estan declarados.
Suponga la siguiente clase:
class num{
int x;
public:
par (){x=0;}
void set () {x=3;}
void set2 (int a) {x=a;}
void set3 (int x) {this->x=x;}
..............
};
Declara varias funciones miembro, se han definido dentro de la clase solo para simplificar la exposicion. La primera se
distingue por ser un constructor, pero todas tienen en comun el acceder al dato privado 'x' para darle un valor. No es
inmediatamente obvio como es posible para una funcion miembro acceder al dato privado 'x', recordemos el modo en que
una funcion cualquiera accede generalmente a datos.
Una funcion comun (no miembro) puede acceder a datos:
• Globales: declarados fuera de toda funcion.
• Locales: declarados dentro de esa funcion.
• Parametros: enviados por otra funcion y que son utilizados directamente (por referencia) o a traves de una copia local
(por valor).
En apariencia el dato 'x' no pertenece a ninguna de estas categorias. Cuando se declara un objeto perteneciente a una clase
lo que se tiene es una entidad compuesta de una copia individual de los datos de la clase y de las direcciones de las
funciones miembros. Es decir, luego de:
num uno;
num dos;
Existen dos objetos y cada uno tiene su propia 'x'. Esa es la 'x' que aparece en las funciones definidas en esta clase, sera una
variable distinta para cada objeto que invoque las funciones (metodos). La cuestion es: ¿como llega ese valor a la funcion,
para que esta puede operar con el mismo?
La respuesta es: llega de modo implicito a traves de un puntero a ese objeto, el puntero "this". Tomando como ejemplo la
clase definida antes, es como si sus funciones miembro hubieran sido declaradas y definidas de este modo:
void set2 (num* this, int a) {
this->x = a;
}
Solo que el puntero 'this' esta implicito, no es necesario mencionarlo en la lista de parametros y la mayoria de las veces no
es necesario mencionarlo en el cuerpo de la funcion tampoco. Se puede acceder a la variable 'x' no porque sea 'global' ni
'local', sino porque llega como parametro, solo que es un parametro especial, implicito.
Dos casos frecuentes, donde es necesario explicitar el puntero "this":
1- Existe ambiguedad respecto a los nombres de variables.
2- Una funcion miembro retorna una referencia al objeto que la invoco.
1- El primer caso se produce si un parametro tiene el mismo nombre que un dato privado, en tal caso la ambiguedad se
resuelve utilizando el nombre del parametro, y para poder acceder al dato privado sera necesario explicitar el puntero
'this'. Es lo que sucede en el siguiente caso:
void set3 (int x) {this->x=x;}
Si el parametro tuviera otro nombre ya no seria necesario (aunque tampoco seria un error) explicitar el 'this'. Puede
parecer una complicacion innecesaria dar al parametro el mismo nombre que un dato privado, pero se trata de un
recurso a veces util para detectar rapidamente, en una funcion de seteo, la relacion entre parametros y datos privados.
2- El segundo caso se produce al retornar una referencia al objeto que invoca la funcion, se trata de un recurso frecuente
en la sobrecarga de operadores, pues permite concatenar operaciones, al modo de los operadores '<<' y '>>' en
iostream.h. El siguiente ejemplo, simplificado, muestra una posible implementacion:
class complejo {
double x, y;
public:
complejo& operator+= (complejo a){
x+= a.x;
y+= a.y;
return *this;
}
La funcion retorna una referncia a un "complejo" a traves del puntero 'this'. Los datos 'x' e 'y' del objeto que invoca la
funcion no necesitan un 'this' explicito, esto ocurriria en caso de que el parametro fuera, por ej, un entero 'x' o 'y'. La
linea final, que explicita el retorno de un puntero al mismo objeto que la invoco, es necesaria para detalles relacionados
con la concatenacion de operadores, la modificacion de los datos privados ya se ha hecho en las dos lineas anteriores.
5 Utilidad de los punteros
5.1 Aritmetica de punteros
Son posibles ciertas operaciones con punteros y los operadores de suma y resta (-,+,--,++). Siendo 'T' el tipo a que apunta el
puntero, el siguiente cuadro sintetiza las distintas posiblidades y el tipo de resultado generado:
Operacion
Resultado
Comentarios
Puntero a T mas entero
Puntero a T
Si el puntero apunta mas alla del limite superior
del array el resultado es no definido
Puntero a T menos entero
Puntero a T
Si el puntero apunta mas alla del limite inferior
del array el resultado es no definido
Puntero mas puntero
-------------
No permitido
Entero
El resultado indica el numero de elementos T
entre los dos punteros
Puntero +- entero
Puntero +- puntero
Puntero a T menos puntero a T
Los punteros son direcciones de memoria pero la aritmetica de punteros no es una simple suma o resta de direcciones.
Estas operaciones estan adaptadas especialmente para tratar con arrays, de ahi que incrementar en 1 el valor de un
puntero no apunte a la proxima direccion de memoria, sino al proximo elemento de un array. El unico caso donde 'proxima
direccion' es igual a 'proximo elemento' es el caso de un array de caracteres, los restantes tipos (por lo menos los propios
del lenguaje) ocupan mas de un byte por elemento.
5.1.1 Puntero +- entero
Es el tipo de operacion mas frecuente con punteros, especialmente porque el incremento del puntero en 1 permite recorrer
un array elemento por elemento. Hay dos modos de realizar esto, el primero consiste en modificar el valor del puntero, y el
segundo en direccionar el elemento igual a [puntero+ entero], procedimiento que tiene la ventaja relativa de no modificar
el valor inicial del puntero, que seguira apuntando al mismo elemento del array. Por ejemplo:
long k [4] = {35,34524,543594,354};
long* pk = k;
int a;
//Primer modo ------------------------------------for {a=0;a<4;a++) {
printf("%ld",*pk);
pk++;
}
//Segundo modo-------------------------------------for (a=0;a<4;a++){
printf("%ld, *(pk+a));
}
En ambos casos se obtiene el mismo resultado, pero al salir del bucle el estado del puntero sera diferente segun la
modalidad adoptada. En el primer caso el puntero estara apuntando fuera del array (pk+4), en el segundo el puntero
seguira apuntando al comienzo del array, pues las direcciones sucesivas se tomaban del valor temporal de (pk+a), sin
afectar al valor del puntero. En el bucle de la segunda modalidad es importante la presencia del parentesis, la notacion
*pk+a tendria un efecto por completo diferente, el operador '*' tiene mayor precedencia que '+', por lo tanto se tomaria
siempre el elemento *pk (el primer elemento del array) y luego se le sumaria 'a'.
Debe quedar claro que el puntero se puede incrementar con cualquier valor entero, hay rutinas que necesitan tomar un
elemento por medio, en tal caso dentro de un bucle se toma los valores sucesivos (p+2). O si se necesita obviar los primeros
'n' caracteres de una cadena, y copiar el resto en un buffer, se puede escribir: strcpy(buffer, cad+n); La suma de enteros a
punteros tiene muchas aplicaciones, en especial si se combinan con las librerias standard de C y C++.
En la tabla previa se menciona que de darse el caso de desbordar el limite del array el resultado de la suma (o resta) es
'indeterminado'. Esto puede depender en parte de cada compilador, pero como norma general lo que es indeterminado no
es el valor-resultado del puntero (la direccion que almacena), sino el valor almacenado en tal direccion. Es decir, dado un
puntero-resultado 'pr', por mas que ese valor desborde el array al que apuntaba, 'pr' sera previsible mientras que '*pr' no lo
sera, se dice que su valor es 'indefinido'.
5.1.2 Puntero - puntero
Esta operacion da como resultado no un puntero sino un valor entero. Es necesario que ambos punteros sean del mismo
tipo, en caso contrario se produce un error en tiempo de compilacion. La resta de punteros tiene la propiedad de que su
valor es independiente de los tipos implicados, es decir: dado un puntero 'p1' y otro 'p2' que apuntan respectivamente a los
elementos 'n' y 'm' de un array, el valor entero de 'p1-p2' sera igual a 'n-m', independientemente del tipo a que apunten los
punteros.
El valor entero del resultado debe ser interpretado como 'numero de elementos (del mismo tipo que los punteros) entre
ambos punteros', y no debe ser tomado como una simple resta de valores de memoria. Por ejemplo:
int t [] = {45,345,5,354,345};
int* pt1 = &t[1];
//en vez de &t[2] se puede t+2;
int* pt2 = &t[4];
int res = pt2-pt1;
El valor de 'res' sera 3, mientras que en termino de direcciones de memoria la distancia entre ambos punteros es de 6, pues
cada entero ocupa 2 bytes. No es necesario involucrarse con demasiados detalles de lo que sucede en memoria, los
punteros y su aritmetica estan adaptados para tratar con direcciones de modo implicito.
La resta de punteros no es tan frecuente como las operaciones de incremento pero puede prestar usos valiosos, sobre todo
en relacion a cadenas de caracteres y junto a librerias standard de funciones. Suponga que se necesita una rutina para
extraer una subcadena de una cadena dada, y que la subcadena estuviera determinada por dos caracteres delimitadores,
por ej 'ch1' y 'ch2', el esquema de una funcion de extraccion seria el siguiente:
1- Se setean dos punteros (p1 y p2) apuntando a los delimitadores 'ch1' y 'ch2'. Funcion que realiza strchr().
2- En un bucle se extraen (p1-p2) caracteres a partir de 'ch1'.
En general se recomienda que la aritmetica de punteros se realice en el nivel mas simple posible. La principal fuente de
error proviene de desbordar los limites (inferior o superior) del array al que apunta el puntero. Estas dificultades no se
presentan si se esta tratando con mapeo de memoria, flujos de bytes, analisis sintactico a nivel compilador o rutinas
similares de bajo nivel, pero cuando los datos presenten mayor nivel de estructuracion seran necesarias mayores
precauciones.
5.2 Itinerar en un array
Suponga una cadena 'cad' y un puntero que apunta a esa cadena.
char cad [] = "hola";
char * ptr = cad;
El puntero 'ptr' apunta al primer elemento de 'cad'. Al incrementar el puntero:
ptr++;
este apuntara a cad[1], es decir el segundo byte de cad, y al sacar en pantalla 'p' y '*p' se vera lo siguiente:
printf("%s", p);
// sale "ola"
printf("%c", *p);
// sale 'o'
Como se ha visto, printf() saca en pantalla todo lo que haya desde el char al que apunta el puntero recibido como
parametro hasta el primer '\0' que encuentre. La explicacion de lo sucedido es la siguiente: 'p' contiene (como cualquier
variable) un valor, este valor es una localidad de memoria, por ej 0xfff2, al incrementar el puntero con p++ lo que se hace es
incrementar el valor que contiene, por eso pasara de 0xfff2 a 0xfff3. Si el puntero hubiera sido tipo entero, el incremento
'p++' habria sumado en 2 la localidad apuntada (0xfff4), pues un entero ocupa 2 bytes de memoria.
El mecanismo es simple y muy eficaz para itinerar a traves de un array, pero no solo eso, tambien permite itinerar por
cualquier zona de memoria y es el metodo mas comodo para hacerlo.
5.3 Mapear localidades de memoria
Casi siempre se mencionan a los punteros en relacion a arrays, pero un puntero puede operar de modo totalmente
independiente de cualquier array, precisamente para itinerar libremente por regiones de memoria, segun Stroustrup
(1995), "la implementacion de punteros tiene por finalidad mapear directamente los mecanismos de direccionamiento de
la maquina en que se ejecuta un programa ".
Veamos un ejemplo. Suponga que el programa opera en modelo small (como la mayoria de los ejemplos dados) y que se
quiere observar el estado del segmento de datos-stack durante la ejecucion del programa. Una funcion como la siguiente
podria cumplir esa funcion:
int funcion()
{
char *tt=0;
unsigned char ch;
int a,fil,col;
//Apunta al comienzo del segmento de datos
//Unsigned, para no lidiar con valores negativos
for (a=0;a<256;a++) {
ch = *(tt+a);
//Para ver el final del segmento seria: ch=*(tt+0xff00+a);
col = a%16;
fil = a/16;
gotoxy(col*3+24,fil+4);
printf("%02X",ch);
//Representacion hexadecimal
if (ch<32) ch=46;
//Si ch <32 se reemplaza con puntos
gotoxy(col+2,fil+4);
printf("%c",ch);
//Representacion ascii
}
return 0;}
La funcion saca en pantalla los primeros 256 bytes del segmento de datos-stack. Los detalles del bucle de impresion son
para que la salida sea similar a la de un editor hexadecimal, en una columan los caracteres ascii, excluyendo a aquellos cuyo
valor es menor a 32 (0x20), en realidad muchos de estos caracteres se pueden imprimir bien, mientras que es mejor evitar
algunos como 7, 8, 10, 13, pero se han evitado todo los menores a 32 para simplificar el codigo. En otra columna se
exhibiran los valores hexadecimales de esos caracteres.
El final del segmento de datos es muy interesante pues almacena los valores de las variables locales, por esta causa sufre
importantes cambios con cada llamado a funcion. Para observar tal sector, con el codigo anterior, basta con reemplazar la
primera linea debajo del bucle por la indicada en el comentario. El esquema de la funcion, aplicada a lectura de archivos,
podria ser de utilidad en una salida a pantalla de un editor hexadecimal.
5.4 Paso de parametros en funciones
Por default las variables declaradas dentro de una funcion no estan disponibles para otras funciones. Cuando se necesita
que una funcion acceda a datos de otra el principal recurso es el paso de argumentos. El paso de argumentos se hace
principalmente a traves de la pila (stack), un bloque de memoria especializado en el almacenamiento de datos temporales.
El espacio total disponible para uso de la pila varia segun el modelo de memoria utilizado por el programa (aparte de las
limitaciones de hardware), si el programa utiliza el modelo 'small' de memoria el espacio total para uso de pila y datos sera
de 64 KB.
Cuando se guardan en la pila mas valores de los que caben se produce un 'stack overflow', un desborde de pila. Las
funciones recursivas trabajan haciendo una copia de si mismas y guardandola en la pila, motivo por el cual no es raro
encontrar desbordes de pila provocados por recursiones mal calculadas. Hay muchos motivos para utilizar la pila del modo
mas economico posible, y los punteros cumplen una gran utilidad en este caso.
Un parametro puede ser pasado a una funcion de dos modos diferentes: por valor o por referencia. Pasarlo por valor
implica que la funcion receptora hace una copia del argumento y trabaja con ese doble del original, cualquier modificacion
realizada en la variable-copia no producira ningun cambio en el parametro enviado. En cambio al pasar un valor por
referencia se esta pasando la direccion de memoria del mismo argumento, en este caso no hay otra 'copia' del mismo, el
dato es uno y el mismo para las dos funciones, la que envia el dato y la que lo recibe.
Una variable comun puede ser pasada por valor o por referencia. En el siguiente ejemplo la funcion 'f ' recibe dos
parametros pasados por la funcion 'principal', el primero es pasado por valor y el segundo por referencia. En el primer caso
'f ' incrementara el valor de la variable 'a' que es una copia local del argumento 'x', ese incremento afectara a 'a' pero no a
'x'. En el segundo caso la variable 'y' es pasada por referencia, por lo tanto el incremento operado sobre 'b' es al mismo
tiempo un incremento de 'y'. Tanto la variable 'y' de la primera funcion como 'b' estan asociadas a la misma localidad de
memoria (hay dos nombres para una misma localidad de memoria).
void f (int a, int& b)
{
a++;
b++;
}
//--------------------------void principal()
{
int x = 1;
int y = 1;
f(x,y);
...................
Un array en cambio siempre es pasado por referencia, la funcion que recibe el parametro recibe un puntero al elemento
inicial del array. Por ejemplo:
int xstrlen (char* str)
{
int a=0;
while (*str++!=0) { //alternativa-->
a++; }
return a; }
//----------------------------void principal ()
{
char cad[] = "hola";
printf ("%d", xstrlen(cad));
.......................................
while (str[a]!=0) {a++;}
En el ejemplo, la funcion xstrlen() da el largo de un array de caracteres buscando la posicion del '\0' de fin de cadena, y la
funcion 'principal' sacara ese valor entero en pantalla. Observese la sintaxis alternativa para el bucle, en un caso se anota
'str' como puntero y en el otro como 'array', ambas notaciones son intercambiables.
Al pasar un array por referencia, la funcion receptora solo recibe la direccion inicial del array, es decir 2 bytes, por lo tanto
pasar un array de 30 KB consume menos recursos de stack que pasar una variable de tipo long (pasada por valor), que
requiere 4 bytes. El principal inconveniente de pasar un parametro por referencia radica en la posibilidad que tiene la
funcion receptora de alterar todos los datos del parametro, por esta causa es frecuente que, en la declaracion de la funcion,
tal parametro se declare como "const" para evitar la corrupcion accidental de ese dato.
5.5 Reserva de Memoria Dinamica
En primer lugar recordemos que es la 'memoria dinamica'. Hay tres formas de usar la memoria en C++ para almacenar
valores:
1- Memoria estatica.
Es el caso de las variables globables y las declaradas como 'static'. Tales objetos tienen asignada la misma direccion de
memoria desde el comienzo al final del programa.
2- Memoria automatica.
Usada por los argumentos y las variables locales. Cada entrada en una funcion crea tales objetos, y son destruidos al salir de
la funcion. Estas operaciones se realizan en la pila (stack).
3- Memoria dinamica.
Tambien llamado 'almacenamiento libre' (free store). En estos casos el programador solicita memoria para almacenar un
objeto y es responsable de liberar tal memoria para que pueda ser reutilizada por otros objetos.
La operacion de reservar y liberar espacio para variables globables, estaticas o locales son realizadas de modo implicito por
el programa, la unica modalidad que requiere mayor atencion por parte del programador es la de reservar memoria en
forma dinamica.
El papel de los punteros en relacion a la memoria dinamica es muy importante, por la razon de que al pedir, al sistema
operativo, una cantidad determinada de memoria dinamica para un objeto, el sistema retorna un puntero que apunta a esa
zona de memoria libre, la respuesta dependera de si hay o no el espacio solicitado.
a) Si hay suficiente memoria se retorna un puntero que apunta al comienzo de esa zona de memoria.
b) Si no hay suficiente, retorna un puntero nulo.
En C++ los operadores usados para requerir y liberar memoria dinamica son new y delete. La sintaxis es la siguiente:
Reserva de memoria dinamica
Variable individual
Array de elementos individuales
int* a = new int
int* a = new int [n];
Liberacion de memoria reservada delete a;
delete [] a;
Nota: las primeras versiones de TurboC++ no admiten la linea "delete [ ] a" con corchetes vacios, para liberar memoria
dinamica de un array requiere que se explicite cuantos elementos hay que borrar, explicitando este valor entre corchetes.
En la version TurboC++ 3.0 esta caracteristica de la sintaxis standard ya se encuentra implementada.
Las ventajas de utilizar memoria dinamica se valoran mejor en comparacion con las caracteristicas de la reserva estatica de
memoria.
Reserva estatica de memoria
Reserva dinámica de memoria
Creacion Los objetos locales son creados al entrar en
La memoria se
de
la funcion que los declara. Los globales son
el operador new.
objetos creados al iniciarse el programa.
reserva
Duracion Los objetos locales se destruyen al salir
Los objetos necesitan
de
los de la funcion en que han sido creados. Los
con el operador delete.
objetos globales, al salir del programa.
Al reservar memoria estatica para un array
el valor del indice debe ser un valor constante.
Indice de
Ej:
arrays
int n [20];
int n [variable no const];
//no permitido
ser
explicitamente
destruidos
mediante
explicitamente,
El indice de un array puede ser un valor variable, de modo
que la cantidad de memoria reservada por una linea de código
puede variar en tiempo de ejecucion (runtime).
Ej:
int* n = new int [variable no const]
//correcto
La estrecha relacion que existe entre arrays y punteros explica que la solicitud de memoria dinamica para un array culmine
en la devolucion de un puntero, una vez que ha sido reservada la memoria suficiente se opera sobre el puntero
directamente, de modo muy similar a como se opera con un array.
Los mecanismos de bajo nivel que implementan el uso de memoria dinamica son bastante complejos y no nos detendremos
en ello. Desde el punto de vista del programador, la principal fuente de errores se deriva de una mala coordinacion entre
operadores new y delete, sea que se olvido liberar la memoria que ya no se utiliza, o que se intente borrar, o utilizar, un
objeto ya borrado.
6 Problemas con punteros I
6.1 Punteros no inicializados
La sola declaracion de un puntero, independientemente del tipo a que apunte, no reserva en memoria mas espacio que el
necesario para almacenar un valor que representa una direccion de memoria: es decir 2 o 4 bytes. La siguiente linea de
codigo tiene ese efecto:
char * ptr;
Declarar un puntero de ese modo no es ningun error, el error es olvidar lo siguiente:
1- Que se trata de un puntero 'no inicializado', que puede estar apuntando a cualquier localidad de memoria, tal vez alguna
en la cual sea erroneo escribir algun dato (por ej: el comienzo del segmento de datos).
2- Que no se esta reservando ningun espacio extra para asociar un array, una estructura o un objeto a ese puntero.
Es suficiente continuar la linea anterior con una de las siguientes:
*ptr = 'a';
strcpy (ptr, "hola");
para cometer un error. Es probable que al final del programa aparezca el mensaje 'Null pointer assignment', indicando que
se ha sobreescrito una zona 'prohibida' del segmento de datos. Los detalles de tal mensaje de error se tratan aparte, por
ahora lo importante es insistir en que un puntero no inicializado es peligroso, pues apunta a una localidad de memoria
indeterminada, y que es un error setear una localidad de memoria (desreferenciando el puntero) con un valor sin tener
claro de que localidad de memoria se trata, o a que variable se encuentra ligada.
Si el puntero hubiera sido inicializado por ej, apuntando a un array, entonces el primer problema, adonde apunta, estaria
solucionado. Pero aun se podria olvidar el segundo, el espacio de memoria reservado debe ser suficiente. Por ejemplo:
int main()
{
int x = 4;
//Variable cuyo valor sera destruido en este ejemplo
char cad [] = "hola"; //Reserva estatica de 5 bytes (4 mas 1 del '\0') para 'cad'
char* ptr = cad;
//ptr apunta a cad[0]
strcpy(ptr, "casa"); //bien, 'casa' tiene 4 bytes de texto, no excede a 'hola'
strcpy(ptr, "Buen dia"); //Mal!, esta cadena excede la capacidad de 'cad'.
........................etc
La ultima cadena se copiara de todos modos en la direccion apuntada por 'ptr', desbordarndo la capacidad del array 'cad'
para almacenar ese dato, como consecuencia el valor de la variable 'x' sera destruido.
Se trata de un error que no es dificil de cometer, la regla que se puede seguir es tratar al puntero como un 'alias' del array al
que apunta, los problemas de desbordar la capacidad del array, escribiendo por ejemplo cad[7]='a', son exactamente los
mismos que los de hacer lo mismo con el puntero, solo que por alguna razon es mas facil cometer el error con el puntero
olvidando la capacidad del array al que apunta.
En el siguiente ejemplo se visualizara lo que sucede con las localidades de memoria implicadas cuando se produce un error
de sobreescritura como el antes mencionado.
int main () {
int x = 5, y = 4, z = 3;
char cad[] = "abcde";
char* ptr = cad;
strcpy(ptr,"Hasta luego");
Como puede observarse se han perdido los valores originales de las tres variables enteras.
Las consecuencias concretas de sobreescribir variables dependen enteramente de lo que haga el resto del programa, en
todo caso se trata de un error. Por lo tanto es importante evitar la presencia de punteros no inicializados y que por ello
apuntan a una zona de memoria indeterminada, en ingles generalmente se los denomina 'wild pointers' (punteros salvajes).
6.2 Punteros y literales de cadena
Un literal de cadena es un conjunto de caracteres encerrados entre comillas, por ejemplo la cadena "Hasta luego" del
ejemplo anterior. Los literales de cualquier tipo son tratados como valores constantes y son almacenados, a diferencia de
las variables locales, cerca del comienzo del segmento de datos. Con la linea siguiente:
char* ptr = "hola";
se crean dos entidades, no una. Por una parte se reservan 2 bytes para el puntero 'ptr' (para almacenar una direccion), pero
tambien es creada otra entidad, un literal de cadena, que es constante, su contenido aqui es 'hola' y no es modificable en el
transcurso del programa. Los bytes reservados por esta linea de codigo son:
- 2 para que el puntero almacene una direccion (aqui la direccion donde se encuentra el literal 'hola')
- 5 para el literal.
Es importante comprender que un 'literal de cadena' es una entidad diferente a un array de caracteres o a un puntero a
char*, es un valor constante y su contenido se almacena en un sector especial del segmento de datos, en la parte inicial del
mismo.
En los casos en que un puntero es inicializado con un 'literal de cadena' el error es tomar el puntero e intentar copiar algo
desreferenciandolo. Algunos compiladores daran un mensaje de error en tiempo de compilacion, otros mas antiguos
pueden permitir tal copia. Lo recomendable, cualquiera sea el compilador, y permita o no modificar el valor apuntado, es
no intentar modificar el contenido apuntado por 'ptr' en ningun caso. Si se necesita modificar el contenido lo mejor es
copiar el literal en un array, reservando la memoria suficiente, y operar sobre el array.
Ligar un puntero a un literal de cadena no es un error, si lo es el intentar modificar un valor que debe ser tratado como
constante. Cuidando estos detalles, declarar un puntero a literales puede ser muy comodo para manejar cadenas
constantes, como las que que conforman menus, en estos casos un array de punteros es un buen recurso.
char* menu[]={"Archivo","Abrir", "Nuevo", "Guardar", "Guardar como...", "Salir"};
Esto es mas facil de manejar que un array multidimensional, con "menu[n]" se accede a cada una de las cadenas, en un
estilo muy similar al de lenguajes que conciben las cadenas de caracteres como un tipo propio y no un array. Si las cadenas
necesitaran ser modificadas habra que implementarlo de otro modo, por ejemplo asignando memoria dinamica para su
almacenamiento.
6.3 El mensaje "Null pointer assignment"
Un progreso importante en el manejo y comprension de bugs y mensajes de error es la capacidad de reproducirlos de modo
previsible. Por alguna razon el mensaje "Null pointer assignment" es uno de los que mas cuesta reproducir, posiblemente
porque existe cierta confusion en torno a la nocion de 'puntero nulo'.
La traduccion literal del mensaje de error seria: "asignacion de puntero nulo", o en otros terminos: "se ha asignado un valor
a un puntero que es nulo". A continuacion se ve en detalle que significa esto, cuales son los pasos que lleva a cabo un
compilador (aqui TurboC++1.01) para emitirlo, como se puede reproducirlo de manera controlada, y por ultimo que
cuidados se deben tener para evitarlo.
Antes que todo recordemos que es un puntero nulo: es un puntero que apunta a un sitio donde no debe (pero si puede)
estar almacenado ningun dato, por asi decir, se trata de una zona 'prohibida', se puede leer pero no setear valores alli. De
este modo, cuando una funcion que retorna punteros retorna un puntero "Null", se usara esto como significando 'nada'.
Por ejemplo: la funcion strchr() busca un caracter dentro de una cadena, si lo encuentra retorna un puntero a ese caracter,
si no lo encuentra retorna un puntero nulo. Ahora bien, un puntero apunta siempre a algun sitio, y el 'puntero nulo' no es
una excepcion. Con un compilador Borland y modelo de memoria small o medium un puntero nulo apunta a la direccion 0
del segmento de datos, es decir DS::0000. Escribir alli un valor que no sea 0 garantiza la presencia del mensaje de error,
pero no es la unica localidad de memoria que lo produce.
Las condiciones para producir el mensaje de "Null pointer assignment" son las siguientes:
1- El modelo de memoria utilizado por el programa es SMALL o MEDIUM.
2- Se sobreescribe algun valor del comienzo del segmento de datos-stack. Es decir el intervalo compuesto por 4 bytes con
valor 0 (cero) mas el copyright de Borland.
Si se modifica algun valor a partir de la localidad DS::0x3d, donde comienza el mensaje "Null pointer..." ya no se produce el
mensaje de error, de hecho solo las localidades resaltadas con color pueden producirlo. Con mas exactitud habria que decir
que no es el hecho de 'escribir' alli lo que genera el mensaje de error, sino el hecho de que, al terminar el programa, alguno
de esos bytes contenga un valor diferente al que se observa en la imagen. Por lo tanto si uno sobreescribe el mismo valor
que tiene, o bien lo altera pero antes de salir del programa lo vuelve a reestablecer, el mensaje de "Null pointer
assignment" no se produce. Esto ultimo solo a titulo informativo, no es recomendable de ningun modo intentar operar
sobre esas localidades de memoria.
Los siguientes ejemplos ilustran algunos modos de generar el mensaje de error.
Ejemplo N:1 - Desreferenciacion de un 'wild pointer'
int main ()
{
char* p;
*p = 'a';
return 0;
}
No esta determinado adonde apuntara 'p', pero en los ejemplos observados apunta siempre a 0x0000 o 0x000c, ambos
bytes 'prohibidos', al darle el valor 'a' modifica el comienzo del segmento y aparece el mensaje de error que se esta
estudiando.
Ejemplo N:2 - No requiere comentarios, sucede lo mismo que en el ejemplo anterior.
#include <string.h>
int main()
{
char *p;
strcpy (p, "wxsjkwe");
return 0;
}
Ejemplo N: 3 - Olvidar que una funcion ha retornado un puntero nulo
#include <string.h>
int main ()
{
char* p;
char ch = 'a';
char cad[] = "jorge";
p = strchr(cad, ch); //busca 'ch' dentro de 'cad'.
...................
*p = 'x';
//Como 'ch' no esta en 'cad' strchr() retorna un puntero nulo,
..............etc //que ahora es desreferenciado y escrito.
En este ejemplo se invoca una funcion, que retorna un puntero nulo y luego, y sin redireccionar el puntero, se le da un
valor, como resultado se escribe 'x' en el primer byte del segmento provocando el mensaje de error. Algo similar ocurriria si
al solicitar memoria dinamica 'new' retornara un puntero nulo, y se operara con el mismo sin antes comprobar el exito de la
solicitud.
Existen muchisimos modos de producir el mensaje de error, pero todos se basan en lo mismo. Una observacion mas: el
mensaje de error avisa que se ha escrito en un puntero 'nulo', en este contexto eso significa un puntero que apunta a
0x0000, si apuntara a 0x0001 ya no seria un puntero nulo. Sin embargo se ha visto que no es esa la unica localidad de
memoria que produce el mensaje de error, se trata mas bien de un intervalo de 45 bytes, desde 0x0000 hasta 0x002c, por
lo tanto no es solo la 'asignacion de un puntero nulo' la que provoca el mensaje, aunque asi lo da a entender "Null pointer
assignment", por lo menos asi sucede en los compiladores Borland.
6.4 "Dangling pointers"
Este tipo de problemas suscita muchas preguntas en las diversos foros (o Faq's) sobre C y C++, y se presenta con frecuencia
en funciones que retornan un puntero. La causa del problema es esta: la funcion retorna un puntero que apunta a una
variable o array declarados como locales, al salir de la funcion todas las variables locales son 'deallocated', se pierde la
conexion entre direccion de memoria y variable, la zona de memoria que utilizaban es liberada, por lo tanto el puntero (al
salir de la funcion) apunta a una 'zona liberada', no ligada con ningun array o variable. La siguiente funcion 'f1' reproduce el
problema:
char * f1()
{
char buffer[128];
//Reserva de memoria estatica para variable local
cout << "Entre su nombre: ";
cin.getline( buffer, 128 );
return buffer;
//Retorna como puntero de variable local
}
int main(){
char* ptr;
..................
//Resto del codigo aqui
ptr = f1();
//El puntero ptr recibe la direccion de 'buffer'
f2();
//Llamado a una funcion 'f2' cualquiera
El puntero 'ptr' recibira la direccion 'correcta', la misma en que estaba almacenada la cadena 'buffer', el problema es que
'buffer', al ser declarada como local, pierde su localizacion de memoria.
El rol que juega la stack (pila) en el llamado a funciones se ilustra en la siguiente figura:
Comentemos paso a paso la relacion entre stack, funciones y el codigo anterior:
I. Al comenzar el programa se hace lugar en la pila para albergar todas las variables locales de 'main', este lugar se
encuentra al final del segmento de pila y solo sera liberado al terminar el programa. Hasta ese momento los datos
locales de las restantes funciones 'no existen', en el sentido de que no tienen localidades de memoria donde
almacenar un valor. Distinto es el caso con las variables declaradas como 'static', pero estas se encuentran en la parte
baja de la pila y no producen el problema que se esta viendo.
II. La funcion main ( ) llama a la funcion 'f1'. Como los valores de las variables de main no se pierden hasta el final del
programa, en la pila se hace lugar, debajo de estos valores, para almacenar las variables locales de 'f1', en terminos de
ensamblador diriamos 'la pila crece (hacia abajo) seteando un nuevo valor de BP (base pointer) y SP (stack pointer)'.
En esas localidades de memoria, estaran los valores de 'f1' hasta salir de la funcion.
III. Saliendo de la funcion f1 se retorna un puntero a una variable local. Lo que el puntro retorna es, obviamente, una
direccion de memoria, esa direccion se mantiene, no es borrada ni se pierde. El problema no es el puntero, el
problema es que se pierde la variable local. ¿Que sucede con las variables locales al salir de la funcion? Mientras no se
llame a otra funcion sus valores pueden perdurar, pero no es algo que un compilador garantice.
IV. Al llamar a otra funcion. En ese momento las localidades de memoria asociadas a las variables de la anterior funcion
'f1' seran sobreescritas por las variables locales de la nueva funcion llamada 'f2'. El puntero a la variable local de f1
seguira apuntando a la misma localidad de memoria, pero su contenido sera indeterminado, y sera muy peligroso
usarlo para cualquier proposito (a menos que sea reasignado).
La regla practica seria esta: no retornar nunca un puntero que apunte a una variable declarada como local. Pero entonces,
¿que camino seguir para retornar un array o puntero de modo seguro desde una funcion?
En la literatura existente sobre el tema se analizan y recomiendan tres posibles soluciones, todas apuntan a preservar el
valor de la variable, evitando que sea 'local':
1- Declarar a la variable 'static'.
2- Reservar memoria dinamica para la variable dentro de la función.
3- Retornar el valor utilizando un parametro de la funcion llamadora.
Se analizan cada una de las soluciones, anticipando que las tres son eficaces en evitar el problema de punteros 'dangling',
solo se trata de evaluar sus efectos.
1- Al declarar una variable como 'static' se esta reservando un sitio especial dentro del segmento que no sera
alterado por el flujo general del programa, ese sitio es en la parte baja de la stack, lejos de la parte alta donde se
produce todo el movimiento de variables locales de las distintas funciones. Para esto basta con anteponer 'static' a
la declaracion de la variable:
static char buffer[128];
El unico inconveniente es que esa zona de memoria no sera liberada en todo el transcurso del programa, la
cantidad de bytes reservados determinara si este recurso es demasiado costoso o no.
2- Reservar memoria dinamica dentro de la funcion llamada. En el ejemplo seria:
char* buffer = new char [128];
Aqui sera responsabilidad del programador liberar la memoria reservada, en caso contrario se produciran 'fugas de
memoria' (memory leaks), es decir, memoria fuera de uso que no puede ser reutilizada para almacenar nuevas
variables. Se trata de un tema tecnicamente complejo, al punto de que muchos compiladores no son enteramente
eficaces en la liberacion de memoria reservada dinamicamente (tardan en hacerlo), existe software 'recolector de
basura' (garbage collection) cuya funcion es liberar zonas de memoria a las que ya no puede acceder ninguna
variable en tal punto de un programa.
3- Devolver el valor a traves de un parametro. La variable de retorno no se declara dentro de la funcion que retorna,
sino en la funcion que llama. Por ejemplo:
void f1 (char* buff) {
..................
}
int main () {
...............
char buffer[128];
f1(buffer);
.................etc,
No es necesario retornar explicitamente la variable pues el parametro ha sido pasado 'por referencia', 'buff' de 'f1'
apunta a la misma localidad que 'buffer' de 'main', y pueden ser tratados como un mismo puntero. El unico defecto
del metodo radica que puede disminuir ligeramente la legibilidad del codigo, la funcion retorna un puntero pero de
modo disimulado, el tipo (type) de la funcion no informa nada al respecto. Las funciones de las librerias de C y C++
utilizan en general las dos ultimas alternativas, (2) y (3).
7 Problemas con punteros II
7.1 Memoria dinamica
Las operaciones de reservar memoria dinamica y liberarla, con new y delete, estan enteramente en manos del
programador, esto proporciona gran flexiblidad de recursos pero tambien oporturnidad para diversos tipos de errores.
Cada vez que aparece el operador new en relacion a un objeto deberia haber una aplicacion del operador delete a ese
mismo objeto. Los problemas suelen generarse por dos modalidades de error:
1- No liberar la memoria dinamica reservada para un objeto.
2- Intentar borrar, o desreferenciar, un objeto ya borrado.
7.1.1 Memoria no liberada
La memoria reservada dinamicamente necesita ser liberada de modo explicito con delete. Si existe un objeto que ya no se
usa, y que fue almacenado dinamicamente, nos encontramos frente a una 'fuga de memoria' (memory leak), el caso mas
critico se presenta cuando ya no es posible acceder al objeto, pues no sera posible borrarlo. Durante la ejecucion del
programa el numero de 'fugas de memoria' puede multiplicarse hasta agotar los recursos disponibles.
Un caso muy simple donde no es posible acceder al objeto para liberar memoria es el siguiente:
int f (int a)
{
char* p = new char [a];
return 0;
}
La funcion no hace nada interesante, es simplemente el esquema de un error posible. La memoria reservada para el
puntero 'p' no ha sido liberada al salir de la funcion, la direccion apuntada por 'p' se pierde, y no sera posible liberar esa
memoria en ningun sitio. Es el modo mas simple de producir una fuga de memoria. Es necesario agregar la linea:
delete [] p;
antes de salir de la funcion para que ese monto de memoria sea liberado.
Mientras que es tecnicamente posible reservar memoria en una funcion y liberar esa memoria en otra funcion, se considera
una practica riesgosa, por la posiblidad de olvidar quien tiene la responsabilidad de liberar memoria. Una posible solucion
es reservar memoria para el objeto en la funcion llamadora, pasar el objeto como parametro (por referencia) y retornarlo,
asi la responsabilidad de reservar y liberar memoria respecto al objeto estara en manos de una misma funcion. El esquema
seria el siguiente:
int f (char* c)
{
...............
return 0;
}
int main()
{
int b = 34;
char* t = new char[b];
f(t);
...............
delete [] t;
...............
Otro modo de provocar problemas es reservar memoria dinamica por segunda vez para un puntero, antes que haya sido
liberada la primera reserva. Por ejemplo:
void f (int a, int b)
{
char* p = new char[a];
......................
p = new char[b]; .....................
Aqui la memoria reservada por el primer uso de new ya no podra ser liberada, pues se ha perdido su direccion. Toda
segunda asignacion del puntero 'p' sin antes liberar la memoria dinamica asociada a el, producira fugas de memoria. Otra
variacion del mismo problema es el siguiente:
void f (int a)
{
char* p = new char[a];
char* q = new char[a];
......................
p = q;
.....................
delete [] p;
delete [] q;
}
Este esquema de error es importante pues, como se vera mas adelante, se presenta en forma velada en problemas con
constructores de objetos. El error es reasignar el puntero 'p' antes de liberar la memoria por el reservada. Esto tiene dos
consecuencias negativas:
1- La memoria reservada originalmente por 'p' no podra ser liberada.
2- La memoria reservada por 'q' sera liberada dos veces (muy problematico).
Un modo de fallar en liberar acertadamente la memoria reservada con new, en relacion a un array, consiste en aplicar el
operador delete, olvidando los corchetes entre el operador y el nombre del puntero.
int f ()
{
char* p = new char[100];
........................
delete p;
delete [] p;
//error, solo libera un elemento de 'p'
//bien, libera el array apuntado por 'p'
7.1.2 Operar con un objeto ya borrado
La segunda familia de problemas se produce por intentar desreferenciar o usar un puntero al cual ya se ha aplicado el
operador delete. El principal recurso para evitar este problema es (una vez aplicado el operador delete) setear este puntero
a NULL, esto protege contra posteriores usos equivocados de delete, pues por convencion la aplicacion de delete a un
puntero nulo no tiene ningun efecto.
Tambien es un error desreferenciar un puntero al que se ha aplicado delete sin antes asignarle una nueva direccion. La
razon es que el puntero esta apuntando a alguna zona que almacena valores indeterminados, sobreescribir alli puede
destruir datos pertenecientes a otras variables o a otras reservas dinamicas. La solucion es reasignar una direccion al
puntero antes de desreferenciarlo, norma general que tambien soluciona el problema de los punteros nulos. Nunca se debe
desreferenciar un puntero sin antes asignarle un valor. Los pasos correctos se ejemplifican en el siguiente codigo:
int f () {
char cad[] = "hola";
char* p = new char[40];
.......................
delete [] p;
p = NULL;
.......................
p = cad;
*p = 'a';
//Primera reserva de memoria dinamica
//Liberacion de mem dinamica
//Precaucion por posible sobreborrado de 'p'
//Nueva asignacion
//Desreferenciacion de 'p'
7.2 Datos miembros punteros y copia de objetos
Los datos miembro de un objeto pueden ser inicializados mediante un constructor, una inicializacion de copia, o asignacion
de copia.
Suponiendo la existencia de una clase llamada "Clasex" veamos las siguientes lineas:
Clasex a;
Clasex b;
Clase c;
Clasex d = b;
c = a;
//Inicializacion de copia (constructor copia)
//Asignacion de copia
La sola declaracion de los objetos 'b' y 'a', sin parametros, invoca un constructor por defecto. En la tercera linea no se
invoca al constructor, se realiza una copia del objeto 'b' en el objeto 'a'. A menos que se especifique algo distinto, esta copia
(llamada 'asignacion de copia'), produce una replica miembro a miembro de los datos privados de 'b' en los datos privados
de 'a'.
A primera vista esto es muy natural y no problematico, pero si entre los datos privados figuran punteros entonces pueden
plantearse importantes problemas.
class Clasex {
int x;
char ch;
char* cad;
public:
Clasex (int n = 40) { cad = new char [n]; }
~Clasex () {delete [] cad;}
..............
}
void f() {
Clasex a;
Clasex b = a;
Clase c;
c = b;
}
//Constructor default
//Destructor
//Invoca constructor
//Inicializacion de copia - Problemas con el puntero!
//Invoca constructor
//Asignacion de copia - Problemas con el puntero!
Aqui hay tres objetos "Clasex" en juego. Se trata de objetos locales, por lo tanto el destructor sera invocado de modo
automatico tres veces al salir de la funcion.
El primer problema se plantea cuando se toma conciencia de que, en la funcion "f ( )", el constructor es llamado solo dos
veces (el destructor: tres veces), tanto la inicializacion de copia como la asignacion de copia no utilizan el constructor, sino
que copian datos miembro a miembro.
El esquema de los tres objetos seria el siguiente:
'a'
'b'
'c'
Datos privados.
a.x
b.x
c.x
Una copia individual por cada a.ch b.ch c.ch
objeto
a.cad b.cad c.cad
Funciones publicas:
Clasex::Clasex (int);
una copia para todos los objetos Clasex::~Clasex();
La copia de los datos 'x' y 'ch' no presenta ningun problema, cada objeto tiene su 'x' y su 'ch' en distintas localidades de
memoria, copiar estas variables es copiar el valor almacenado. Cada objeto tiene tambien una localidad de memoria para
'su' puntero 'cad', el problema es adonde apuntan esos punteros.
La copia de punteros (ej, a.cad = b.cad) es copia de las direcciones a la que apuntan. Pero esas direcciones, en el ejemplo,
son localidades de memoria reservadas mediante memoria dinamica, por lo tanto, luego de:
a = b;
Sucede que los punteros "cad" de ambos objetos apuntan a la misma zona de memoria reservada con new, y es aqui donde
se presenta el problema. Hay tres objetos y por lo tanto tres punteros, cada puntero deberia tener sus propios bytes
reservados (el default para el ejemplo es 40). Una llamada comun al constructor reserva esos bytes, y son diferentes para
cada objeto, pero una asignacion de copia hace que un puntero deje de apuntar a 'su propia zona' y apunte a los mismos
bytes que el otro puntero.
Como consecuencia:
Una zona de memoria queda fuera de alcance, no pudiendo ser liberada y se crea una fuga de memoria
Dos punteros apuntan a la misma zona, cuando se invoque el destructor, este liberara dos veces una misma zona de
memoria, lo que es muy problematico.
La solucion. Cuando entre los datos privados hay punteros es necesario explicitar un constructor de copia diferente al
default, para evitar la copia entre punteros, y es necesario tambien proveer de un asignador de copia diferente al default,
esto significa que para disponer de la notacion "a = b" sera necesario sobrecargar el operador '=' y darle un sentido
diferente, que evite la copia de punteros.
La naturaleza del problema puede aclararse con un codigo que no utiliza clases pero que presenta el mismo error.
void f ()
char* a =
char* b =
a = b;
delete []
delete []
}
{
new char[40];
new char[40];
//Error! no se debe reasignar sin antes liberar memoria
b;
a;
Cada puntero tiene 'sus' 40 bytes de memoria dinamica, al reasignar "a=b" el puntero 'a' deja de apuntar a la zona de
memoria reservada con 'new', esa direccion se pierde y no podra ser liberada (fuga de memoria). Por otra parte, las dos
invocaciones de 'delete' cometen el error de liberar una misma zona de memoria.
A. Anexo 1: Cadenas de caracteres o strings estilo 'C'
A.1 ¿Que es una cadena de caracteres en C/C++?
En cualquier lenguaje de programacion las cadenas de caracteres tienen una importancia especial, no solo porque es el tipo
mediante el cual se almacenan los mensajes a pantalla o entradas del teclado, sino porque un caracter (char) es del tamaño
de un byte, y un byte es la menor unidad de informacion 'natural' para la maquina. Una cadena de caracteres es una
coleccion ordenada de bytes. Un archivo, la informacion de pantalla en el sector de memoria de video, las entradas de
bytes por los puertos y muchas otras entidades se pueden conceptualizar comodamente como esto: una coleccion
ordenada de bytes. Es cierto que en muchos casos se adoptan tipos definidos o clases, para una mejor administracion de
datos, pero aun en estos casos tales estructuras de datos complejas suelen utilizar arrays de tipo char en su nivel mas
elemental.
Hay lenguajes que tienen un tipo (type) preestablecido para tratar con cadenas de caracteres, es asi en las distintas
versiones de basic, donde el tipo 'String' es un tipo mas (como 'integer'), y asi como existen arrays de enteros hay arrays de
strings. En cambio en C/C++ podria decirse que el tipo 'String' no existe como tal (en C++ hay implementaciones de la clase
'String' pero no pertenecen propiamente al lenguaje sino a librerias anexas, dependiendo de cada compilador), el recurso
usado por la mayoria de las funciones de estos lenguajes es representar una cadena de caracteres como un array de
elementos tipo char, un array de caracteres.
El solo hecho de que una cadena (string) sea un array plantea dudas a quien viene de otros lenguajes, por ej: ¿como
representar entonces un array de strings? (un array bidimensional no es comodo de manejar), ¿cual es el largo permitido de
una cadena de caracteres? ¿que sucede si ese largo se modifica? ¿como conocer en tiempo de ejecucion el limite de ese
array? etc.
Una cadena de caracteres, representada en memoria, es una simple sucesion de bytes, cada caracter se corresponde con
un byte, si se quiere sacar en pantalla una cadena de caracteres, el problema es: ¿como sabe el programa donde finaliza esa
cadena?, ¿cual es su ultimo byte? A esta pregunta diferentes lenguajes plantean diferentes respuestas, segun un modelo de
'string', los dos modelos clasicos son el de Pascal y el de C:
1- Pascal: el primer byte es reservado para almacenar el largo de cadena, es decir que la cadena de caracteres
propiamente comienza en la segunda posicion. Al haber 1 byte de espacio para almacenar el largo, el maximo permitido
para una cadena sera de 255 bytes. Para cadenas de mayor extension se debe utilizar otro recurso.
2- En C se reserva una funcion especial al caracter cuyo valor ascii es 0 (cero), ese caracter indicara con su presencia que la
cadena finaliza alli, ese char forma parte de la cadena pero por convencion no se lo tiene en cuenta al determinar el
largo de la cadena. De una cadena como "hola" se dice que tiene 4 caracteres, aunque en memoria luego de 'a' se
encuentre el '\0' que forma parte de ella.
En memoria esa celda que indica el fin de cadena tendra el valor 0, insistiendo que no se trata de un elemento extra (como
el EOF de archivos) sino del char que en el conjunto de caracteres ascii corresponde al cero.
Ambos modelos de cadena presentan ventajas e inconvenientes. En el modelo 'Pascal' es muy rapida la operacion de
encontrar la longitud de cadena (se consulta el primer byte) mientras que en C/C++ hay que recorrer toda la cadena en
busqueda del char '\0'. Por otra parte una cadena tipo C no tiene ninguna limitacion en longitud (salvo las indicadas por el
modelo de memoria utilizado o razones de hardware) mientras que en Pascal tendra un limite dictado por el byte que
almacena el largo.
El rol que juega el caracter '\0' es absolutamente esencial para comprender y manejar fluidamente cadenas de caracteres
en C y C++, y la mayor parte de los problemas y bugs tienen relacion con accidentes y descuidos en relacion a ese caracter.
Seria un error creer que en C/C++ el largo de la cadena esta registrado en algun sitio y que luego, en un segundo momento,
el programa situa un '\0' en esa posicion, no!, el largo de cadena no existe como dato en ningun sitio. Existe una funcion
standard que retorna un entero con el largo de una cadena enviada como parametro, es strlen(char*), lo que hace esta
funcion es simplemente contar caracteres hasta que encuentra el '\0', para una masa muy grande de bytes se podria pensar
en una funcion menos costosa, pero strlen funciona de ese modo.
A.2 Array y cadena de caracteres: algunas diferencias
En muchos textos sobre C y C++ se encuentra la siguiente afirmacion:
"En C/C++ una cadena de caracteres (string) es un array de caracteres"
es claro en que sentido esta afirmacion es valida, una cadena de caracteres es un array y no una variable de tipo individual,
como un entero o un float, sin embargo hay ligeras diferencias entre los dos conceptos, el de array y el de cadena de
caracteres.
Mientras que un array de caracteres: es un conjunto ordenado de 'n' bytes de cualquier valor, una cadena de caracteres es
solo un subconjunto de ese array, desde el primer char hasta el primer '\0' encontrado en el array. En realidad no es
totalmente exacto decir 'subconjunto', pues si por error el array de caracteres no tiene un '\0', las funciones standard lo
buscaran fuera del array, y asi la cadena de caracteres llegara a tener mayor extension que el propio array, desbordando su
capacidad.
Luego de la declaracion-inicializacion siguiente:
char cad[20] = "hola";
el largo de la cadena de caracteres es 4, valor que se puede obtener con el llamado "strlen(cad)", sin embargo el largo del
array es 20, pues siguen siendo 20 los bytes en memoria asociados al nombre 'cad', este valor se obtiene con:
sizeof(cad);
Este valor de limite de array no impide que se pueda sobreescribir mas alla del mismo utilizando un indice que exceda el
sizeof() del mismo (generalmente un grave error), para evitarlo se podria consultar ese valor numerico. Lamentablemente
es un recurso que no esta disponible cuando se pasan arrays como argumentos a otra funcion, pues todo array es recibido
por la funcion llamada a traves de un puntero a su elemento inicial.
El hecho de que sizeof(cad) y strlen(cad) ofrezcan dos valores diferentes justifica el que se hable de diferencias entre los dos
conceptos. A este respecto se pueden formular dos preguntas interesantes:
- ¿Es posible que una cadena de caracteres tenga dos bytes con valor 0?
- ¿Es posible que un array de caracteres tenga dos bytes con valor 0?
Si uno se atiene a las definicion estricta de cadena de caracteres la respuesta a la primera pregunta es 'NO', en una cadena
de caracteres hay un solo byte (char) con valor 0, y coincide con su ultimo elemento.
En cambio, la segunda pregunta debe responderse afirmativamente, un array de caracteres no cambia de tamaño durante
la ejecucion de un programa, si se ha declarado de 20 char seguira siendo de 20 hasta el final, hay 20 bytes en memoria que
le pertenecen solo a ese array. Y nada impide que dos o mas de esos bytes tengan el valor 0.
La cuestion no es solo teorica, suponga que se quiere elaborar un programa para analizar datos de archivos binarios, por
ejemplo de archivos EXE, en tal caso nos encontraremos con bytes que valen 0 y estan en cualquier posicion, aqui no tiene
sentido el pensar en esas colecciones de bytes como 'cadenas de caracteres', no son 'palabras' ni 'texto', sin embargo se
quiere hacer un programa que lea y almacene los bytes en un array, y construir funciones que analicen sintacticamente ese
flujo de bytes.
Algunas funciones standard de lectura de filas detienen cada lectura ante un '\n' o ante un '\0', pero hay otras que permiten
leer conjuntos de bytes y los almacenan cualquiera sea su valor, y puesto que son bytes, y estos se correponden con el tipo
char, no se tiene otra opcion que almacenarlos en arrays de caracteres. Ahora bien, la mayoria de las funciones de
tratamiento de cadenas, como las de string.h, no seran utiles, pues interpretan el char 0 como corte de una cadena de
caracteres, por lo tanto se debe construir funciones alternativas.
Teniendo en cuenta las diferencias enumeradas respecto a array y cadena de caracteres, se puede adoptar la siguiente
definicion: en C y C++, una cadena de caracteres es un array de caracteres terminado en '\0'.
Aseguramiento del fin de cadena
Cuando se declara un array o una variable cualquiera sin darle un valor inmediatamente, sin 'inicializarla', esa variable o
array pueden contener cualquier valor, se dice que su valor es indeterminado, las variables globales son una excepcion pues
son inicializadas con un valor default por el compilador, pero la mayor parte de los datos seran locales a una funcion (sea
main() o cualquier otra) y por lo tanto no seran inicializados automaticamente.
Suponga ahora las siguientes lineas de codigo:
int main ()
{
int largo;
char cad[5];
largo = strlen(cad);
........................etc.
La pregunta es "¿cual es el valor de la variable 'largo'?", y el error es creer que ese valor deba ser necesariamente 5, de
hecho el valor de 'largo' podria ser 0, 1, 9932, 234, o casualmente 5. ¿Porque razon?, con la declaracion de 'cad' se ha
declarado un array de caracteres, reservando 5 bytes de memoria estatica para ese array, pero no se ha inicializado ningun
dato, lo mas probable es que esa region de memoria conserve datos aleatorios de algun programa anterior, el valor que
dara "strlen(cad)" se basara en haber comenzado a contar caracteres en memoria hasta encontrar el '\0', que podria estar
en cualquier sitio, si se encontro en el primer byte inspeccionado el valor de 'largo' sera 0.
Si se saca en pantalla el contenido de esa cadena, sea con printf ("%s", cad) de C, o con cout<<cad de C++, su contenido sera
totalmente arbitrario y muy probablemente se vean caracteres 'extraños', esto ocurrira porque las funciones de impresion
en pantalla tambien confian en el char 0 para determinar el fin de cadena.
Los dos graficos siguientes muestran un contenido hipotetico de los bytes de memoria asociados a la variable 'cad', son los
primeros cinco bytes representados. En el primero se muestra un estado posible luego del codigo anterior, los primeros
cinco bytes estan reservados para el array de caracteres 'cad', pero en memoria esos bytes contienen un valor aleatorio e
independiente, en este caso si se llamara a la funcion strlen(cad) retornaria el entero 15 (cuenta 15 antes de encontrar el
00), y si se sacara en pantalla con "printf" o "cout" se verian los caracteres de la izquierda (subrayados).
En cambio, si al declarar el array se inicializa:
char cad[5]="hola";
o bien en un primer paso se declara y luego se copia "hola" en la cadena con strcpy():
char cad[5];
strcpy(cad, "hola");
en ambos casos estara asegurada la presencia del '\0' que indica el fin de cadena.
Ahora strlen() indicaria que el largo de cadena es 4, y las funciones de impresion en pantalla funcionaran normalmente,
todo gracias a la presencia del char '\0'.
La idea es muy simple, sin embargo es necesario cometer muchos errores y desarrollar mucha practica con cadenas 'tipo C'
antes de sentirse comodo con ellas. Las librerias "string.h" y "mem.h" contienen muchas rutinas para tratar con cadenas de
caracteres, es necesario conocer en detalle el modo en que cada funcion trata la cuestion del '\0' final para no encontrarse
con sorpresas.
A.3 Ejemplos de funciones standard
Como ejemplo se observa el modo de operar de "strset" y "memset" y las precauciones necesarias en relacion al valor '\0'.
A.3.1 Memset (char *cad, int ch, int n);
Esta funcion setea los primeros 'n' char de una cadena al valor pasado como segundo argumento (ch). Si se quiere setear los
primeros 5 bytes de una cadena con espacios se podria llamar a la funcion del siguiente modo:
memset (cad, ' ', 5);
La operacion es muy simple y aparentemente inofensiva, sin embargo ¡nada relacionado con cadenas en C/C++ es simple!
Esta funcion no hace nada en relacion al '\0', ni lo usa ni lo setea, podria ocurrir que antes de la anterior linea de codigo el
array 'cad' tuviera la cadena "uno", con el cero en su cuarto byte, en tal caso el llamado a memset estaria sobreescribiendo
ese cero y el limite pasaria a ser indeterminado. Si el proposito es que la cadena contenga solo esos cinco bytes seteados es
muy importante que, luego de haber invocado a memset, se asegure el fin de cadena, por ejemplo con:
cad[5]='\0';
//Tambien es posible "cad[5]=0", pues el 0 (entero)
//se convierte implicitamente en 0 (char)
Podria ser el caso de que se tuviera muy claro que el fin de cadena esta mas alla de esos 5 bytes y se quisiera conservar,
pero si solo interesa una cadena con el caracter ascii 'ch' repetido 'n' veces, una buena practica sea asegurar el fin de
cadena con "cad[n]=0", para evitar problemas posteriores.
A.3.2 Strset (char *cad, int ch);
La explicacion de ayuda en linea de Borland dice que strset "setea todos los caracteres de una cadena a 'ch' ". Esto significa
que si se quiere que 'toda' nuestra cadena 's' pase a estar compuesta por asteriscos (ascii=42) se puede escribir:
strset (cad, '*');
// es equivalente "strset (cad, 42)"
La diferencia con memset() es bastante clara, aquella era para setear 'n' caracteres y esta para 'toda' la cadena, y ahi esta el
principal peligro, en la palabra 'toda'. Pues esta funcion confia exclusivamente en el '\0' para determinar el fin de la cadena
que seteara. Suponga un programa que comenzara con las siguientes lineas:
int main ()
{
char cad[20];
strset (cad, '*');
.....................etc.
Estas lineas de codigo son muy peligrosas, todo depende de si la funcion encuentra o no un '\0' antes de llenar de asteriscos
todo lo que encuentre, hasta llegar al fin del segmento, donde sobreescribiria los valores de stack para salir bien de main() y
posiblemente habria que resetear la maquina.
La explicacion de la funcion dada por la ayuda de TurboC++ es demasiado escueta y no menciona este tipo de problemas, la
enunciacion es correcta: "setea toda la cadena" con el caracter indicado, pero es facil olvidar que significa esto en los
lenguajes C/C++, significa "todo lo que encuentre hasta dar con un '\0' "
Podria darse una lista de posibles errores para los usos de cada una de las funciones que involucran a cadenas. Todos estan
relacionados con el problema del limite del array de caracteres. Se mencionan a continuacion algunos de los muchos
errores posibles.
A.4 Sobreescritura de variables
Ya se vio un ejemplo con strset() de como es posible exceder los bytes que corresponden a un array de caracteres y
sobreescribir bytes que no le corresponden a esa variable. Veamos un ejemplo simple que involucra a la funcion strcat.
La funcion strcat(cad1, cad2) concatena dos arrays de caracteres 'pegando' el segundo argumento luego del primero.
int main () {
char cad1[] = "hola";
char cad2[] = " mundo";
strcat (cad1,cad2);
....................etc
Lamentablemente estas lineas seran problematicas, el modo en que se declararon los arrays hace que para 'cad1' haya
cinco bytes en memoria ("hola" mas el '\0') y 6 bytes para 'cad2'. El resultado esperable seria el de que 'cad1' tuviera ahora
la cadena "hola mundo", pero esto implica 10 bytes mas el '\0', lo cual desborda la capacidad de cad1. La funcion
strcat() hara lo que se le pide de todos modos, sobreescribiendo de ese modo bytes que pertenecen a otras variables.
Muchas veces el problema no aparece inmediatamente sino cuando se intenta operar con las variables 'pisadas', en todo
caso hay que tener en claro que es necesario reservar los suficientes bytes de memoria al usar una variable, ya se trate de
una reserva estatica o dinamica.
Distintos sintomas de sobreescritura de variables
Los efectos de sobreescribir una variable son muy variados, dependiendo en parte del tipo de dato sobreescrito y de la
operacion involucrada. Algunos ejemplos sacados de la practica:
1- El corte imprevisto, durante el transcurso de un programa, como un flujo de archivo puede ser provocado por la
sobreescritura del dato 'fstream' o el puntero "FILE*".
2- La aparicion de caracteres ascii 'extraños' casi siempre se debe a la supresion de un '\0' de fin de cadena. Si se escucha
un pitido (beep) esto solo significa que entre los caracteres 'extraños' estaba el ascii =7.
3- Sobreescribir el comienzo del segmento de datos provoca el mensaje "Null pointer assignment", este tema se tratara
con mas detalle en el apartado dedicado a problemas tipicos con punteros.
Son solo algunos ejemplos, los sintomas de sobreescritura de variables son tan variados como las multiples posibilidades
que produciria dar un valor random, de modo no controlado, a uno o mas datos de un programa.
B. ANEXO 2: Memoria
B.1 Modelos de memoria
La memoria se puede representar como una coleccion de celdas contiguas con la capacidad de almacenar valores. Cada
celda de memoria es individualizada por una 'direccion', que consta de un valor de segmento y otro de offset
(desplazamiento dentro del segmento). Los detalles de como opera la CPU en relacion a la memoria dependen del tipo de
procesador, si este esta funcionando en modo 'real' o 'protegido', sistema operativo y muchos otros factores.
Cada segmento tiene una capacidad de 64 KB. Una importante directiva en todos los programas es la que determina el
MODELO DE MEMORIA que utilizara el programa al ejecutarse. El default suele ser el modelo 'small', pero existen varios
modelos mas, sus principales diferencias estan en el modo en que utilizan los segmentos para almacenar codigo, datos o
ubicar la pila (stack).
Al compilar y ejecutar un programa, en el IDE de TurboC++, se puede examinar los registros de la CPU para datos, codigo y
stack, estas son las siglas de tales registros:
CS (code seg) Segmento de codigo
DS (date seg) Segmento de datos
SS (stack seg) Segmento de pila
El modelo de memoria utilizado por un programa determinara cuanto espacio (en termino de segmentos) se usara para
codigo, datos y stack. El siguiente cuadro sintetiza las distintas opciones:
Modelo de memoria Segmentos Comentarios
Tiny
cs = ds = ss
Codigo, datos y stack utilizan un unico segmento, por lo tanto el ejecutable no
podra ser mayor a 64 KB. Es muy similar a un ejecutable con extension .COM
Small
cs
ds = ss
Un segmento para codigo y uno para datos y stack. Es el modelo default
utilizado, a menos que se especifique uno diferente
Medium
cs
ds = ss
Codigo usa multiples segmentos, datos y pila comparten uno. Es el modelo
de eleccion si hay gran cantidad de codigo y pocos datos
Compact
cs
ds = ss
Un segmento para codigo y multiples segmentos para datos y stack.
Modelo apropiado cuando hay poco codigo pero gran cantidad de datos.
Los datos son referenciados por punteros 'far'
Large
cs
ds = ss
Multiples segmentos para codigo y multiples seg para codigo y stack. Se usan
punteros 'far' para codigo y para datos
Huge
cs
ds = ss
Similar a 'large'
Flat
cs
ds = ss
Usa punteros 'near' como el modelo 'small', pero hecho a medida para
sistemas operativos de 32 bits
Estas categorias no son especificas de un lenguaje de programacion, la mayoria de los compiladores de los diferentes
lenguajes permiten optar por estos diferentes modelos de memoria. Las primeras versiones TurboC++ admiten solo los
primeros seis modelos de la tabla, a partir de TurboC++3.01 esta disponible el modelo 'Flat' tambien.
Cuando se escribe codigo para librerias, un importante y complejo punto en la implementacion es tener en cuenta que las
funciones deben tener la flexibilidad necesaria para adaptarse a diferentes modelos de memoria al pasar por el linker. Es
instructivo observar las declaraciones de las librerias standard y el modo en que resuelven este tema. El principal detalle es
el uso default de punteros 'near', para los modelos de memoria mas restringidos, y punteros 'far' para los mas extensos.
B.2 Rol de la STACK (pila)
La distincion entre codigo y datos es bastante natural, la sintaxis de C (o Modula-2) obliga a declarar, en una funcion,
primero todos los datos antes de ingresar cualquier operacion (codigo). Pero la nocion de STACK tiene una correspondencia
menos obvia con lo que se observa en un lenguaje de alto nivel, se trata de algo manejado de modo automatico por el
compilador. A lo sumo aparecera en relacion a mensajes de error como 'Stack overflow' o 'Desborde de pila' (tambien
'volcado de pila', en Windows). En programacion de bajo nivel (ensamblador) se puede operar directamente sobre la
misma.
Ahora bien, ¿que es la pila?. Es una zona de memoria (no diferente del resto de memoria) requerida por todo programa (la
misma CPU lo requiere) para un uso especial. Su funcion, sinteticamente, es la de servir para el intercambio dinamico de
datos durante la ejecucion de un programa, principalmente para la reserva y liberacion de variables locales y paso de
argumentos entre funciones.
El espacio utilizado para uso de la pila variara segun el modelo de memoria que utilice nuestro programa. Cuando un
programa utliliza el modelo de memoria SMALL usa un mismo segmento para codigo y stack, 64 KB entre ambos.
Suponiendo que un programa opera con tal modelo de memoria, en la mayoria de los compiladores de Borland C++, el
segmento de datos/stack presentara el siguiente aspecto, luego de ingresar en la funcion main() de un programa
cualquiera:
El inicio del segmento(0x0000) contiene una cadena de
Copyright de Borland que no debe ser sobreescrita
(pues daria el mensaje "Null pointer assignment"),
luego se ubican las variables globales y constantes. Los
literales, sean 'de cadena' o 'numericos' son tratados
como constantes y almacenados en la parte baja. Al
final de la pila (desde 0xFFFF) se guardan datos
fundamentales para una buena salida del programa, y
debajo se extiende una zona usada para almacenar
variables locales y datos pasados como parametros, por
lo tanto es la parte mas dinamica del segmento (en el
grafico la parte en blanco).
El espacio total del segmento es de 64 KB, esto significa que el monto de datos que se puede pasar a una funcion sera un
poco menor pues hay espacio ocupado por otros elementos. Esta limitacion se puede sortear utilizando otro modelo de
memoria, pero por ahora nos centraremos en el ejemplo con modelo small.
Cuando se guardan en la pila mas valores de los que caben se produce un 'stack overflow', un desborde de pila. Las
funciones recursivas trabajan haciendo una copia de si mismas y guardandola en la pila, por esa causa es frecuente
provocar desbordes de pila de ese modo. Hay muchos motivos para utilizar la pila del modo mas economico posible, y los
punteros cumplen una gran utilidad en este caso, por ej al pasar arrays, estructuras u objetos entre funciones a traves de
una direccion (solo 2 bytes).
Otros detalles en relacion a punteros. Todo puntero que este dentro de este segmento y apunte a otra direccion del mismo
segmento sera un puntero 'near', para apuntar a un segmento diferente se debe (en modelo small) explicitar un puntero
'far'. Una cuestion interesante es la de si la memoria dinamica se almacena en este segmento o en algun otro. Los detalles
en la implementacion de memoria dinamica son en general bastante oscuros y dependen mucho del compilador utilizado,
pero si el espacio reservado con 'new' se asocia a un puntero 'near' es claro que la memoria reservada estara dentro de
este mismo segmento. Para estudiar este aspecto es recomendable ejecutar el programa en modo debugger y consultar los
datos del puntero, el valor de segmento donde se encuentra y el valor de segmento adonde apunta.
B.3 Gestion de memoria en C++
Todas las variables, arrays, punteros y objetos en general tienen una duracion determinada en el transcurso del programa.
Tales objetos son 'creados' y 'destruidos', o en otros terminos: se asocian sus nombres (identificadores) a una zona de
memoria en la cual no puede asentarse otro objeto, y tales zonas de memoria son liberadas para el uso de otros objetos.
La existencia de tales objetos esta determinada segun tres formas basicas de usar la memoria en C++:
1- Memoria estatica
Los objetos son creados al comenzar el programa y destruidos solo al finalizar el mismo. Mantienen la misma localizacion en
memoria durante todo el transcurso del programa. Estos objetos son almacenados (en compiladores Borland) al principio
del segmento de datos. Los objetos administrados de este modo son: variables globales, variables estaticas de funciones,
miembros static de clases, y literales de cualquier tipo.
2- Memoria automatica
Los objetos son creados al entrar en el bloque en que estan declarados, y se destruyen al salir del bloque. Se trata de un
proceso dinamico pero manejado de modo automatico por el compilador (no confundir con memoria dinamica). Tales
objetos se almacenan en la parte alta de la pila al entrar en la funcion o bloque.
Este procedimiento se aplica a: variables locales y argumentos de funcion.
3- Memoria dinamica
En este caso tanto la creacion como destruccion de los objetos esta en manos del programador, a traves de los operadores
'new' y 'delete'. El sitio donde se almacenan tales objetos se suele denominar en ingles 'heap' o 'free store', traducido como
'monticulo' o 'memoria libre'. Pero el sitio preciso donde se encuentre tal 'monticulo' depende del compilador y el tipo de
puntero utilizado en la reserva de memoria dinamica.
Cualquier tipo de objeto puede ser creado y destruido a traves de este procedimiento.
En C y C++ la administracion explicita de memoria por parte del programador juega un rol muy importante, no es asi en
otros lenguajes (Basic, Smalltalk, Perl) donde la gestion principal es automatica. La administracion 'manual' permite un
mayor grado de flexibilidad pero tambien multiplica la posibilidad de errores. Un modo de gestionar memoria dinamica en
C++, aprovechando las ventajas de la memoria automatica, es la implementacion de destructores que sean llamados de
modo automatico al salir de un bloque, y que se encarguen de la liberacion de memoria dinamica.
B.4 'R-value' y 'L-value'
La forma mas simple de almacenar un valor en una direccion de memoria es a traves de asignaciones. A veces, ante una
asignacion fallida, aparece el mensaje "Lvalue required", veamos que significa.
Las expresiones "R-value" y "L-value" ("rigth-value" y "left-value"), originalmente significaban algo que puede estar a la
derecha o a la izquierda de una asignacion. Por ejemplo:
int x;
4 = x;
//Error en tiempo de compilacion: "Lvalue required"
El compilador informa que se requiere un 'l-value' a la izquierda de la asignacion. El '4' es un literal numerico, y los literales
son tratados como valores constantes, no pueden estar en esa posicion.
Ahora bien, ¿como definir "l-value"? T. Jensen lo define como: "valor de la direccion de una variable", y Stroustrup como:
"algo que esta en la memoria, que ocupa una region continua de memoria". Esto ultimo se ilustra con las siguientes lineas:
int a, b=4, c=5;
a = b + c;
el valor de 'a' sera igual a 9, este valor 9 es producto de "a+b", no ocupa una zona de memoria, no hay un 'nombre' de
variable (de una variable) asociado a el, es un valor temporal sin identificador propio. Por lo tanto, al no estar en memoria
bajo un identificador, no es un "l-value", por esa razon:
b + c = a;
//Error. "Lvalue required"
da ese mensaje de error.
A pesar de la definicion de 'lvalue', como aquello "que puede estar a la izquierda de la asignacion", hay cosas donde esto no
se cumple. Una variable declarada como constante es un 'lvalue', es un objeto en memoria, pero no puede estar a la
izquierda en una asignacion.
Descargar