Problemas I

Punteros no inicializados

La sola declaracion de un puntero, independientemente del tipo (type) 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 estamos 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 podriamos 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 podemos 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).

Punteros y literales de cadena

Un literal de cadena es un conjunto de caracterese 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* menu1[] = {"Archivo", "Abrir", "Nuevo", "Guardar", "Guardar como...", "Salir"};

Esto es mas facil de manejar que un array multidimensional, con "menu[n]" accederemos a cada una de las cadenas, en un estilo muy similar al de lenguajes que conciben las cadenas de caracteres como un tipo (type) 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.

El mensaje "Null pointer assignment"

Un progreso importante en el manejo y comprension de bugs y mensajes de error es nuestra 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 veremos en detalle que significa esto, cuales son los pasos que lleva a cabo un compilador (aqui TurboC++1.01) para emitirlo, como podemos reproducirlo de manera controlada, y por ultimo que cuidados podemos tener para evitarlo.

Antes que nada recordaremos 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 a donde 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 estamos 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);         //buscamos 'ch' dentro de 'cad'.
...................
*p = 'x';                    //Como 'ch' no esta en 'cad' strchr() retorno un puntero nulo,
....................etc      //que ahora es desreferenciado y escrito.

En este ejemplo hemos invocado una funcion, la misma retorno un puntero nulo y luego, y sin redireccionar el puntero, le damos 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 operaramos 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 nos avisa que hemos 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 hemos 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.

"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 el siguiente grafico:

Vamos a comentar 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 estamos 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 (bass pointer) y SP (stack pointer)'. En esas localidades de memoria, estaran los valores de 'f1' hasta que salgamos de la funcion.

III- Salimos de la funcion f1 retornando 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- Llamamos 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'. Nuestro 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 funcion
3-Retornar el valor utilizando un parametro de la funcion llamadora.
Se analizaran 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' le estamos 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 nuestro 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 nos informa nada al respecto. Las funciones de las librerias de C y C++ utilizan en general las dos ultimas alternativas, (2) y (3).

 

PRINCIPAL


1