Esta pagina no es un lujo, ni un vicio, sino una necesidad.
Tras años de aprender C a golpe de practicas (ISO, XC, STD, SPD,
AAD, CASO, SAC, EISO ...) nos hemos decidido a realizar esta pagina que
esperamos os sea de gran ayuda en la realizacion de vuestras
practicas en C.
Falta por aclarar que esta pagina no esta dedicada al publico en
general, sino a cualquiera que empiece a programar en C y ya tenga
nociones de programacion.
Nacho & Pep.
Un puntero, en C, se declara como sigue:
TIPO * nombre_puntero ;
Donde TIPO es cualquier tipo definido.
Asi, un puntero a caracter se declararia de la siguiente forma:
char *pchar;
En C, al contrario que en otros lenguajes de programacion, se puede obtener directamente la direccion de memoria de cualquier variable. Esto es posible hacerlo con el operador unario "&"; asi:
char a; /* Variable 'a' de tipo char */
printf("la direccion de memoria de 'a' es: %p \n", &a);
y para obtener lo apuntado por un puntero se utiliza el operador unario
"*" de esta forma:
char a; /* Variable 'a' de tipo char */
char *pchar; /* Puntero a char 'pchar' */
pchar = &a; /* 'pchar' <- @ de 'a' */
printf("la direccion de memoria de 'a' es: %p \n", &a);
printf("y su contenido es : %c \n", *pchar);
Uno de los casos mas comunes donde se ve la relacion entre estos dos
operadores es la declaracion y utilizacion de funciones:
void Funcion ( int *int_pointer )
/* Paso de una variable de tipo entero por REFERENCIA */
/* equivalente en Modula 2: PROCEDURE Funcion ( VAR a:INTEGER ) */
.
.
.
int a;
a=6;
Funcion ( &a ); /* ya que la declaracion de la funcion pide la
direccion de una variable de tipo entero */
Hay varias maneras de inicializar un puntero. Una ya ha sido vista en los ejemplos del punto anterior ( pchar = &a; ); y al igual que el resto, consiste basicamente en lo mismo; asignar una direccion de memoria al puntero. Para hacer un paralelismo Modula-2 - C, en C existe el equivalente al procedimiento NEW; la funcion malloc:
#include <stdio.h> #include <malloc.h> void *malloc( size_t size );donde 'size' es el numero de bytes que queremos reservar de tipo 'void', es decir, de cualquier tipo.
char *pchar;
int *pint;
pchar = malloc (6); /* pchar apunta al primer byte de los que se han
reservado */
pint = malloc (sizeof(int)*2);
/* pint apunta al primero de los dos enteros
que se han reservado */
Otra forma es inicializarlo con el valor de otro puntero.
.
.
.
int *pint2;
pint2 = pint;
Para comprender mejor algunos de los errores que se describen en
la siguiente seccion, es necesario tener algunas nociones sobre
generacion de codigo y tratamiento de la memoria que realizan los
compiladores.
Empecemos por los distintos espacios de memoria existentes: la pila
y el heap.
La pila es el espacio de memoria donde se reservan todas
las variables locales y globales; esto significa que cada vez que
se llama a una funcion, sus variables se crean en tiempo de ejecucion
en la pila, y se destruyen en cuanto el flujo de ejecucion retorna
al punto en que se llamo a la funcion.
El heap (o monton) es el espacio de memoria destinado a las peticiones
explicitas de memoria (malloc en el caso del C) y solo se pierde cuando
se libera la memoria pedida (free).
El desconocimiento de estos espacios de memoria lleva a la generacion
de errores totalmente 'magicos' ("...yo he reservado el espacio y ya no
esta..." y similares). Algunos de estos errores pasamos a describirlos
mas adelante.
Errores mas comunes
AVISO: Si careces de conocimientos sobre la generacion de codigo,
lee la seccion anterior.
Primer caso
En el siguiente ejemplo se ilustra la inicializacion de un puntero a traves de una funcion.
void inicializa( char *buffer );
main()
{
char *buff;
.
.
.
buff = NULL;
inicializa( buff );
/* en este punto sigue valiendo NULL */
.
.
.
}
void inicializa( char *buffer )
{
buffer = malloc (1); /* reservamos memoria */
*buffer = 'a'; /* y la inicializamos */
}
.
.
.
Por que es incorrecta la inicializacion?
Analicemos la pila al realizar la llamada:
| | SP --->+---------------+ | copia de buff | <------- Parametro pasado a inicializa() +---------------+ | @ de retorno | +---------------+ <------- Hasta aqui llega la pila antes de | buff = NULL | llamar a inicializa() +---------------+ | resto de vars | | locales al | | main | +---------------+Tras la llamada tenemos lo siguiente en la pila
| | +---------------+ | @ de 'a' | +---------------+ | @ de retorno | SP --->+---------------+ | buff = NULL | +---------------+ | resto de vars | | locales al | | main | +---------------+y 'buff' sigue valiendo NULL, ya que en la funcion 'inicializa' lo unico que se ha modificado es la copia de 'buff' que se ha pasado como parametro en la pila.
void inicializa ( char **buffer);
main()
{
char *buff;
.
.
.
buff = NULL;
/* y pasando buff por referencia */
inicializa ( &buff );
/* ahora *buff = 'a' */
.
.
.
}
void inicializa ( char **buffer)
{
*buffer = malloc (1);
*buffer = 'a';
}
.
.
.
ya que la pila ahora queda de la siguiente forma tras la llamada:
| | +---------------+ | @ de buff | +---------------+ | @ de retorno | SP --->+---------------+ | *buff = 'a' | +---------------+ | resto de vars | | locales al | | main | +---------------+
Ahora volvemos a hacer lo mismo de otra manera que parece correcta
char *inicializa();
void otra_funcion();
main()
{
char *buff;
.
.
.
buff = NULL;
buff = inicializa();
/* hasta aqui todo parece correcto */
otra_funcion();
/* aqui ya no se puede asegurar buff = "hola"*/
.
.
.
}
char *inicializa()
{
char buffer[5];
sprintf(buffer,"hola");
return(buffer);
}
.
.
.
Volvamos a analizar la pila:
| | SP --->+---------------+ | 5 bytes para | <------- Espacio reservado para la variable | buffer | local buffer +---------------+ | @ de retorno | +---------------+ | buff = NULL | +---------------+ | resto de vars | | locales al | | main | +---------------+Cuando la funcion retorna tenemos la siguiente situacion:
| | +---------------+ | "hola\0" | @ base de 'buffer' +---------------+ | @ de retorno | SP --->+---------------+ | buff= @buffer | +---------------+ | resto de vars | | locales al | | main | +---------------+En cuanto se llama a otra funcion, el espacio destinado a 'buffer' es destinado a parametros de la llamada o a las variables locales de la funcion invocada, con lo que "hola\0" sera machacado por otros valores. Solo funcionaria si el resto de funciones invocadas no tuvieran ni parametros ni variables locales.
char *inicializa();
main()
{
char *buff;
.
.
.
buff=inicializa();
.
.
.
}
char *inicializa()
{
char *buffer;
buffer = malloc (5);
sprintf(buffer,"hola");
return (buffer);
}
.
.
.
ya que tendriamos la siguiente disposicion en memoria:
PILA HEAP | | | | +---------------+ | | | @ de "hola\0" | --------------+ | | +---------------+ | | | | @ de retorno | | | | SP --->+---------------+ | | | | buff | --------------+ | | +---------------+ | +-----------------+ | resto de vars | +----->| "hola\0" | | locales al | +-----------------+ | main | | | +---------------+ | |
El caso mas trivial de todos: no reservar espacio creyendo que
la declaracion del puntero ya lo hace por si misma.
La cuestion es que este error algunas veces pasa por alto, sobre
todo en maquinas gobernadas por un SO sin proteccion de memoria,
caso de los PC's con MS-DOS. En una maquina Unix tambien puede
ocurrir si el valor del puntero cae dentro de nuestro espacio
de memoria, con lo que el problema llega a ser muy grave y
practicamente indetectable sin la utilizacion del debugger.
Ejemplo:
main()
{
char *pchar;
int *pint;
*pchar='a';
printf("Direccion de 'a': %p",pchar);
pint=malloc(sizeof(int));
*pint=0;
/* Ahora lo apuntado por pchar puede haber cambiado de valor */
}
Seguramente una maquina Unix daria el error 'Segmentation fault'
al ejecutar la primera linea de programa, pero nunca se sabe.
En cambio, una maquina MS-DOS se lo tragaria tal cual, y quizas
provocaria la salida en pantalla del QEMM (si esta cargado).
Bibliografia