Punteros en C.

Anuncio
Tema 13
Punteros
Índice
1.− Introducción
2.− Operaciones
3.− Aritmética de punteros
4.− Punteros y vectores/matrices
5.− Punteros y estructuras
6.− Asignación dinámica de memoria
7.− Punteros y funciones:
− Paso por referencia
− Devolución de punteros
− Vectores/matrices como parámetros
− Punteros a función
Tema 13
Punteros
Pág 1
Introducción
Los punteros son uno de los tipos de datos más
temidos de C. Sin embargo, también son los más
versátiles y los que mayor potencia proporcionan.
Su uso permite el paso de argumentos por
referencia, la construcción de estructuras dinámicas
de datos y la mejora en la velocidad de ejecución
de algunos algoritmos. Sin embargo, mal usados
pueden producir errores graves, difíciles de
detectar y, en ocasiones, los peores errores
posibles: los no reproducibles.
Un puntero es una variable que contiene una
dirección de memoria. En la mayoría de los
sistemas operativos actuales, se suele tratar de un
número entero de cuatro u ocho bytes (32 o 64
bits).
Recordemos de temas anteriores que todo objeto de
un programa se debe situar en memoria principal
(ya sea en la pila, en el montón, en el segmento de
código o en el de datos). Cada objeto del programa
tiene, por tanto, una posición de memoria (aquella
en la que comienza a almacenarse) que lo
identifica. Los punteros se usan para almacenar
posiciones (direcciones) de memoria de otros
objetos del programa. Un puntero puede
considerarse como una referencia a otro objeto del
Tema 13
Punteros
Pág 2
programa (una variable, una función...).
Los punteros son variables, exactamente igual que
las demás. Se declaran junto con el resto, usando la
siguiente sintaxis:
<tipo> *<nombre>;
Donde <nombre> es el nombre de la variable
puntero y tipo es el tipo de objeto cuya dirección se
va a almacenar. Es importante que se sepa a qué va
a apuntar un puntero, por motivos que veremos
luego.
Ejemplos:
int *punt;
/* puntero a entero */
float *v, *w, *z; /* punts. a real */
struct coche *a, *b;
/* punts. a estructura */
void *pos; /* puntero genérico */
Los punteros a void son un tipo especial de puntero
que no están asociados a un tipo particular.
Una vez declarado, a un puntero se le pueden
asignar valores, se pueden realizar operaciones con
él, se puede usar su valor en expresiones...
Tema 13
Punteros
Pág 3
Operaciones
Sobre una variable puntero se pueden realizar una
serie de operaciones que veremos a continuación:
− Dirección: el operador de dirección no es
realmente uno que se aplique sobre las variables
puntero (normalmente), sino sobre otros tipos de
variable. Éste operador, representado con el
símbolo ampersand (&) obtiene la dirección de
memoria de la variable a la que precede. Así, si la
variable entera w está almacenada en la posición de
memoria 32012, la operación
int *punt;
punt= &w;
asignará el valor 32012 a la variable punt.
− Indirección: el operador de desreferenciación o
de indirección sí se aplica a valores de tipo
puntero. Éste operador se representa por un
asterisco (*) y devuelve un valor del tipo apuntado
por el operando. Este valor es el contenido en la
posición apuntada por el puntero. Así, en el
siguiente código
float *p;
float q= 1.44;
p= &q;
print("%f\n", *p);
Tema 13
Punteros
Pág 4
el valor que se imprime es 1.44, ya que p apunta a
la dirección de q.
En general, si el puntero x apunta a un tipo de
datos T, la expresión *x es de tipo T.
− Incremento, decremento: los valores de tipo
puntero se pueden incrementar y decrementar,
siempre en valores enteros. Se admiten los
operadores ’+’, ’−’, ’++’ y ’−−’.
− Resta de punteros: se puede hallar la diferencia
entre dos punteros, que es la distancia que separa
las direcciones a las que apuntan en memoria.
− Impresión: para mostrar el valor de un puntero se
usa un especificador de formato especial en la
instrucción printf: ’%p’.
Únicamente se permiten estas operaciones entre
punteros.
Aritmética de punteros
Las operaciones de incremento, decremento y resta
de punteros tienen una semántica especial. Cuando
un puntero p se incrementa o decrementa en una
cantidad X, no se modifica la dirección apuntada
en X bytes, sino en X elementos del tipo apuntado
por p. Es decir, p se incrementa o decrementa en
Tema 13
Punteros
Pág 5
sizeof(T) bytes.
Así, por ejemplo (suponiendo sizeof(char)==1 y
sizeof(float)==4):
char *p= 20232;
float *q= 20232;
p++; /* p vale 20233 */
q++; /* q vale 20236 */
p= p+5; /* p vale 20238 */
q= q+2; /* q vale 20246 */
La resta de punteros debe hacerse siempre entre
punteros del mismo tipo (es decir, que apunten al
mismo tipo de datos) y el resultado no es el número
de bytes que los separan, sino el número de
elementos.
Punteros y vectores/matrices
Hay que hacer unas cuantas consideraciones acerca
de cómo entiende C los vectores:
− Para C, un vector es un puntero que apunta a
una zona de memoria reservada en tiempo de
compilación.
− El nombre de un vector es un puntero al
inicio del mismo.
− Cuando C analiza una expresión del tipo
vector[indice]
realmente la convierte a otra de tipo
*(vector+indice)
Tema 13
Punteros
Pág 6
Y, cosa curiosa, la mayoría de los
compiladores de C admiten las expresiones
indice[vector]
Un ejemplo que ilustra estos puntos:
char cadena[80], *p;
strcpy(cadena, "palabra");
p= cadena; /* el nombre es el puntero */
/* imprimen lo mismo */
printf("%c, %s\n", cadena[5], cadena );
printf("%c, %s\n", *(p+5), p );
En cuanto a las matrices, C las almacena en
memoria disponiendo sus elementos por filas de
forma consecutiva. Así, los elementos de una
matriz de 3x4 estarían así:
m00 m01 m02 m03 m10 m11 m12 m13 m20 m21 m22 m23
Por otra parte, pudiera parecer que una matriz de
dos dimensiones es un vector de vectores (o de
punteros, que es igual), y que C la almacena así:
m0
m00 m01 m02 m03
m1
m2
m10 m11 m12 m13
m20 m21 m22 m23
Tema 13
Punteros
Pág 7
Donde las filas podrían estar en zonas no contiguas
de memoria. Sin embargo, no es así. Todos los
elementos están seguidos en la memoria. Pero,
como caso particular, las expresiones m[0], m[1] y
m[2] sobre la matriz de nuestro ejemplo producen
las direcciones donde comienzan las filas primera,
segunda y tercera respectivamente. Es decir:
int m[3][4], *fila, f, c;
for(f=0; f<3; f++) {
fila= m[f];
for(c= 0; c<4; c++)
printf("%d\n", fila[c]);
}
Pero recordemos que esto no es porque m sea un
vector de punteros a vectores, sino porque el
compilador nos facilita la tarea permitiéndonos
hablar de m[f].
Sabiendo que las matrices se almacenan de forma
contigua, una forma bastante común de acceder a
un elemento de una matriz bidimensional de FxC a
partir de la posición de inicio de la matriz es
mediante la fórmula siguiente:
elemento(fila, columna)=
*(puntero+(fila*C)+columna)
Esto nos permitirá, como veremos más tarde, pasar
matrices de cualquier tamaño a una función.
Tema 13
Punteros
Pág 8
Punteros y estructuras
Un puntero puede estar asociados a cualquier tipo
de datos, incluídas estructuras (y, por supuesto,
punteros).
Cuando se tiene un puntero a estructura, se puede
acceder a los campos de la misma de dos formas
diferentes: desreferenciando el puntero y aplicando
el operador ’.’ o directamente sobre el puntero,
aplicando el operador ’−>’:
struct s {
int a;
float b;
};
struct s var, *punt;
punt= &var;
var.a= 143;
printf("%d, %d, %d\n", var.a,
(*punt).a, punt−>a );
Debe notarse que, debido a que la precedencia del
operador ’.’ es superior a la del operador ’*’, hay
que usar paréntesis para desreferenciar antes de
acceder al campo.
Los punteros a estructuras son muy usados cuando
se trata con estructuras dinámicas de datos, tal y
como veremos en temas posteriores.
Tema 13
Punteros
Pág 9
Asignación dinámica de memoria
Uno de los usos más comunes de los punteros es
mantener referencias a zonas de memoria
dinámica. Tal y como se vio en un tema anterior,
existe una zona de la memoria asignada al
programa que se utiliza para almacenamiento de
datos dinámicos. La gestión de los datos dinámicos
se realiza mediante llamadas al sistema que
permiten reservar y liberar zonas de memoria.
Estas llamadas suelen devolver o aceptar como
parámetros valores de tipo puntero.
Para la gestión de memoria dinámica en C se usan
varias funciones, cuyos prototipos están declarados
en el fichero de cabecera ’stdlib.h’.
En primer lugar, la función
void *malloc( int n )
permite reservar una zona de memoria dinámica de
n bytes de longitud. La dirección de inicio de dicha
zona se devuelve como resultado. Si no se
consiguió reservar esa cantidad de bytes, se
devuelve el valor NULL. Como se ve, el tipo de
retorno de malloc es un puntero genérico (void *),
que deberá ser convertido al tipo adecuado según
lo que se vaya a almacenar en la zona reservada.
Tema 13
Punteros
Pág 10
Un ejemplo del uso de malloc para reservar espacio
para un valor float sería éste:
float *p;
p= (float *)malloc( sizeof(float) );
if( p==NULL ) {
perror("Fallo en malloc()");
exit(1);
}
else
*p= 3.141592;
Otro ejemplo, en el que se reserva espacio para un
vector de 10 estructuras, sería:
struct s {
int a;
float b;
};
struct s *p;
p= (struct s *)malloc(10*
sizeof(struct s))
if( p==NULL ) {
perror("Fallo en malloc()");
exit(1);
}
else {
p[3].a= 32;
p[0].b= 1.45;
p[9].a= 4365;
}
Debe observarse que, al ser un vector de
estructuras, no se necesita el operador ’−>’, puesto
que la notación p[x] devuelve una estructura, no un
puntero.
Tema 13
Punteros
Pág 11
Una macro que puede ser de utilidad para agilizar
las llamadas a malloc es la siguiente:
#define NEW(x,n) (x*)malloc(n*sizeof(x))
int *p;
struct s *vec;
p= NEW(int, 1);
vec= NEW(struct s, 120);
Sigue siendo necesario, por supuesto, comprobar si
malloc ha retornado NULL.
La dirección devuelta por malloc está siempre
convenientemente alineada para almacenar
cualquier tipo de dato (en las arquitecturas que
necesitan alineación), evitando así la posibilidad de
un error de bus (ver tema de gestión de memoria en
tiempo de ejecución).
Una función alternativa a malloc es
void *calloc( int tam, int cant )
que reserva un número de bytes igual a tam*cant y
pone a cero el espacio reservado antes de devolver
la dirección de inicio.
Si se necesita cambiar el tamaño de una zona de
memoria reservada por malloc o calloc, ya sea para
ampliarlo o para reducirlo, se puede usar la función
Tema 13
Punteros
Pág 12
void *realloc(void *ant, int tam)
que cambia el tamaño de la zona apuntada por ant a
tam bytes y devuelve la posición de la nueva zona.
El cambio de tamaño puede implicar mover los
datos dentro de la memoria, y la propia función se
ocupa de ello. Si no se puede reservar la nueva
memoria, realloc devuelve NULL pero los datos
originales siguen en su sitio (ant).
Cuando se usan pequeñas cantidades de memoria
dinámica en un programa que va a ejecutarse
durante poco tiempo, no se suelen plantear
problemas. Pero si la cantidad de memoria
dinámica a usar es grande o el programa debe estar
ejecutándose de manera indefinida, puede suceder
que se agote la memoria asignada al programa. Por
lo tanto, y en cualquier caso, deben liberarse las
zonas de memoria dinámica que se dejen de
utilizar. Para ello se usa la función
void free( void *zona )
Al terminar un programa se libera automáticamente
toda la memoria que ocupa, por lo que en
programas cortos no sería necesario usar free. Sin
embargo,
es
aconsejable
hacerlo
para
acostumbrarse. Cuando un programa largo olvida
liberar parte de la memoria dinámica que reserva,
Tema 13
Punteros
Pág 13
se produce lo que se llama una ’fuga de memoria’
(memory leak) que puede acabar por agotar la
memoria asignada y detener el programa. Existen
bibliotecas de funciones que substituyen malloc,
calloc, realloc y free por versiones especiales que,
aunque más lentas, llevan una contabilidad de las
zonas de memoria reservadas y liberadas y
permiten detectar las fugas de memoria.
Punteros y funciones
Paso por referencia:
Uno de los usos de los punteros es el paso de
parámetros por referencia a una función.
Recordemos que los parámetros de una función se
pueden pasar por valor o por referencia. Si se pasan
por valor, las modificaciones que se hagan sobre
ellos no se verán en el programa principal. Si se
pasan por referencia, sí se reflejan en el programa
principal estas modificaciones.
En C, todos los parámetros de las funciones se
pasan por valor, sin excepción. Para simular un
paso de parámetro por referencia en C, lo que se
hace es pasar un puntero al objeto que se pasa. Así,
la función tiene acceso no sólo al valor del
parámetro sino también a su situación en memoria,
lo que permite su modificación.
Tema 13
Punteros
Pág 14
Un ejemplo de paso de un entero por referencia a
una función que lo incremente:
int x= 14;
inc(&x);
...
void inc(int *par) {
(*par)++;
}
El paso por referencia posibilita que una función
pueda generar más de un valor. La función
devolverá (con return) uno de los valores de
retorno y almacenará en parámetros pasados por
referencia el resto de los valores. Un ejemplo:
int JulianoAdma( int jul, int *dia, int
*mes, int *anno )
Esta función convierte una fecha expresada en días
Julianos (una forma de representar fechas) a día,
mes y año. Devuelve cero si todo ha ido bien y 1 si
se produjo la fecha juliana no es válida.
Como se ve en este ejemplo, si una función
devuelve varios valores, se suele utilizar el
principal (el devuelto por return) como código de
error y los parámetros por referencia para el resto
de los valores.
Tema 13
Punteros
Pág 15
Devolución de punteros:
Una función puede retornar un tipo de datos
puntero. La función se declararía así
tipo *función( argumentos )
Este tipo de funciones se suelen usar para reservar
memoria o crear elementos en estructuras
dinámicas de datos.
Debe notarse que cuando una función devuelve una
dirección de un objeto reservado con malloc no hay
por qué declarar nada estático. La memoria
dinámica es global, y está accesible desde cualquier
punto del programa, siempre que se tenga el
puntero adecuado.
Vectores/matrices como parámetros:
Como se ha dicho más arriba, C considera que un
vector (o una matriz, es igual) es un puntero que
señala a una zona de memoria reservada en tiempo
de compilación y capaz de albergar los elementos
del mismo. Y el nombre del vector es equivalente a
la dirección de comienzo de esta zona.
Por lo tanto, pasar un vector o una matriz a una
función es lo mismo que pasarle un puntero al tipo
almacenado en el vector o matriz. En la llamada a
Tema 13
Punteros
Pág 16
la función se colocará como parámetro real el
nombre de la variable, y en la declaración y
definición se usará, en caso de que se trate de un
vector, una de estas notaciones:
void funcion( int *vector )
void funcion( int vector[] )
void funcion( int vector[TAMANO] )
Como se ve, en el caso de los vectores se puede
obviar el tamaño. No es así en el caso de las
matrices, ya que para acceder a un elemento de una
matriz, según la fórmula que vimos anteriormente,
se deben conocer al menos todas las dimensiones
de la matriz salvo la primera. Por lo tanto, si se
quiere construir una función que trabaje con
matrices de tamaño fijo y conocido, se puede
declarar de una de estas formas:
void funcion( float mat[FILAS][COLS] )
void funcion( float mat[][COLS] )
Pero si se quiere crear una función genérica (algo
muy recomendable) que permita trabajar con
cualquier tamaño de matriz, se declararía como
void funcion( float *mat, int f, int c )
(se debe especificar obligatoriamente el número de
columnas, aunque las filas se suelen pasar
también). En el interior de la función, cada
elemento mat[fila][col] se accedería así:
Tema 13
Punteros
Pág 17
*(mat+fila*c+col)
Punteros a función:
En C se puede también mantener en un puntero la
dirección de inicio del código de una función. Este
puntero es un puntero a función. Se declara así:
retorno (*punt) ( argumentos );
Donde <retorno> es el tipo de datos que devuelve
la función y <argumentos> es la lista de parámetros
de la misma. Así, para declarar un puntero a una
función que devuelve un entero y acepta una
cadena, un float y un vector de floats, tendremos:
int (*pt)(char
v[]);
*cad,
float
x,
float
Y para asignar un valor a un puntero a función
necesitamos
a) una función del mismo tipo al que apunta el
puntero y
b) obtener la dirección de comienzo de dicha
función en memoria.
Supongamos que tenemos la función
int f( char *nom, float vel, float z[]);
Tema 13
Punteros
Pág 18
Podremos asignar su dirección al puntero pt usando
el nombre de la función sin paréntesis:
pt= f;
y luego podríamos llamar a la función de una de
tres formas diferentes:
f( "pepe", 123.2, vec );
(*pt)( "pepe", 123.3, vec );
pt( "pepe", 123.3, vec );
Las aplicaciones de los punteros a función son
múltiples. Propondremos dos fragmentos de código
de ejemplo.
En el primero, se crea una función tabula() que
acepta un valor inicial, un valor final, un
incremento y un puntero a función. Muestra una
tabla de valores f(inicial), f(inicial+incremento)...
f(final) para la función cuyo puntero se haya
pasado:
void tabula( double ini, double fin,
double inc,
double (*fun)(double x) );
double prueba(double arg);
...
tabula(1, 100, 1, sin);
tabula(−10, 10, 0.1, prueba);
tabula(0, 10, 0.2, log);
...
void tabula( double ini, double fin,
double inc,
double (*fun)(double x) )
Tema 13
Punteros
Pág 19
{
double x, y;
for( x=ini; x<=fin; x+=inc ) {
y= fun(x);
printf("%f −> %f\n", x, y );
}
}
Se puede, incluso, declarar un vector de punteros a
función
double (*vec[3])(double)=
{ sin, prueba, log };
Y llamar a
tabula(1, 10, 0.2, vec[loquesea]);
El segundo ejemplo utiliza dos funciones de la
biblioteca estándar de C: qsort() y bsearch(). Estas
funciones aplican los algoritmos de ordenación
Quicksort
y
de
búsqueda
dicotómica,
respectivamente, sobre un vector. Los prototipos
son:
void qsort( void *vec, int n, int tam,
int (*comp)(void *a, void *b) );
void *bsearch( void *clave, void *vec,
int n, int tam,
int (*comp_c)(void *a, void *b);
La función qsort tiene como parámetros un puntero
genérico que será la dirección de comienzo del
Tema 13
Punteros
Pág 20
vector a ordenar, el número de elementos del
mismo (n), el tamaño en bytes de cada elemento
(tam) y un puntero a la función de comparación
comp().
La función de comparación recibe dos punteros a
los dos elementos que deben compararse.
Devolverá un número negativo si el primero es
menor que el segundo, un número positivo si el
segundo es mayor que el primero y cero si son
iguales. Un ejemplo de función de comparación
para números reales es éste:
int comparar( void *a, void *b )
{
float x, y;
x= *( (float*)a );
y= *( (float*)b );
if( x<y )
return −1;
else if( x>y )
return +1;
else
return 0;
}
Los argumentos de la función bsearch() son un
puntero a la clave que se quiere buscar, un puntero
al vector en el que se realizará la búsqueda, el
número de elementos del mismo, el tamaño de cada
uno y un puntero a la función de comparación. Esta
función recibe dos parámetros: un puntero a la
Tema 13
Punteros
Pág 21
clave a buscar y otro puntero a uno de los
elementos del vector. Al igual que la función
anterior, debe devolver un entero negativo, positivo
o cero en función de la comparación de la clave a
buscar con la del elemento que se le pasa.
Tema 13
Punteros
Pág 22
Descargar