Aclaraciones sobre punteros en C

Indice

Introduccion

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.


Declaracion de un puntero

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;

Diferencia entre "*" y "&"

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    */

Inicializacion de un puntero

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;

Generacion de codigo

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.
La forma correcta de hacerlo es declarando la funcion asi:

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          |
        +---------------+ 

Segundo caso

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.
La forma correcta de hacerlo seria:

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          |                      |                 |
        +---------------+                      |                 |

Tercer caso

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

Creditos