Capitulo 05
Capitulo 05
Unidad V
Punteros
Cátedra:
Computación II (ELO)
Informática II (BIO)
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO) Introducción al Lenguaje de Programación C
INDICE
5 Punteros................................................................................................................................................. 1
5.1 Introducción.................................................................................................................................... 1
5.2 Conceptos básicos .......................................................................................................................... 1
5.3 Declaración de punteros ................................................................................................................. 2
5.4 Paso de punteros a una función ...................................................................................................... 4
5.5 Punteros y arreglos unidimensionales ............................................................................................ 5
5.6 Operaciones con punteros .............................................................................................................. 7
5.6.1 Punteros de tipo “genérico”. ................................................................................................. 10
5.7 Asignación dinámica de memoria ................................................................................................ 10
5.8 Punteros y arreglos multidimensionales....................................................................................... 11
5.8.1 Arreglo de punteros............................................................................................................... 12
5.8.2 Puntero a un arreglo de punteros........................................................................................... 13
5.9 Punteros a funciones..................................................................................................................... 15
5.10 Algo más sobre declaración de punteros .................................................................................. 17
5.11 Ejercitación con punteros ......................................................................................................... 18
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
Capítulo 5
5 Punteros
5.1 Introducción
Un puntero es una clase de variable cuyo contenido no es un dato directamente usable, sino
que almacena la posición de memoria (la dirección de memoria) de otro dato, tal como una
variable o un elemento de un arreglo. Los punteros son usados frecuentemente en C y tienen
gran cantidad de aplicaciones. El uso cuidadoso de los punteros provee claridad y simplicidad
al programa. Los punteros, por ejemplo, pueden usarse para pasar información entre una
función y sus puntos de llamada, y en particular proporcionan una forma de devolver varios
datos desde una función mediante los propios argumentos de la misma. Además, permiten
asignar dinámicamente la memoria necesaria para una variable o conjunto de variables,
manejar listas encadenadas y acceder a distintas posiciones dentro de una misma estructura
de datos, por ejemplo arreglos, cadenas, estructuras, etc.
Los punteros están muy relacionados con los arreglos y proporcionan una vía alternativa de
acceso a los elementos individuales de los mismos. Es más, proporcionan una forma
conveniente para representar arreglos multidimensionales.
Esta nueva variable (pv) es un puntero a la variable v. Recordar, sin embargo, que pv
representa la dirección de v y no su valor. De esta forma pv se refiere como variable puntero.
La relación entre estas dos variables esta ilustrada en la figura 1.
dirección de v valor de v
pv v
Figura 1- Relación entre v y pv.
El dato representado por v puede ser accedido por la expresión *pv, donde * es un operador
monario, llamado el operador indirección, que opera solo sobre una variable puntero. Por lo
tanto, *pv y v representan el mismo dato (el contenido de las mismas celdas de memoria).
Además, si escribimos,
1
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
pv = &v;
u = *pv;
entonces u y v representan el mismo valor (se asume que u y v están declaradas como
variables del mismo tipo de dato).
Operador Descripción
& Retorna la dirección de memoria de una variable.
* Retorna el valor de la variable cuya dirección se especifica.
Los operadores monarios & y * son miembros del mismo grupo de precedencia que los otros
operadores monarios, por ejemplo -, ++, --, !, sizeof(tipo), que fueron presentados en
el Capítulo 2 . Se debe recordar que este grupo de operadores tiene mayor precedencia que el
de los operadores aritméticos y la asociatividad de los operadores monarios es de derecha a
izquierda. El operador & sólo puede actuar sobre operandos con dirección única, como
variables ordinarias o elementos de un arreglo. Por consiguiente, el operador dirección no
puede actuar sobre expresiones aritméticas, tales como 2 * (u + v).
El operador indirección * sólo puede actuar sobre operandos que sean punteros. Sin embargo,
si pv apunta a v (pv = &v), entonces una expresión como *pv puede ser intercambiable con
la correspondiente variable v. Así una referencia indirecta puede aparecer en lugar de una
variable dentro de una expresión mas complicada.
Considere el siguiente ejemplo,
#include <stdio.h>
int main( ) {
int u1, u2;
int v = 3;
int *pv; /* puntero a enteros */
pv = &v; /* pv apunta a v */
u2 = 2 * (*pv + 5); /*expresión alternativa */
printf("\nu1 = %d u2 = %d", u1, u2);
return 0;
}
En este programa la salida u1 y u2 son iguales ya que v y *pv representan al mismo dato.
Las variables puntero pueden apuntar a variables numéricas o de caracteres, arreglos, funciones
u otras variables puntero, y a otras estructuras de datos como veremos posteriormente. Por tanto,
a una variable puntero se le puede asignar la dirección de una variable ordinaria (por ejemplo pv
= &v). También se le puede asignar la dirección de otra variable puntero (por ejemplo pv = px),
siempre que ambas variables puntero apunten al mismo tipo de dato. Además, a una variable
puntero se le puede asignar un valor nulo (cero), como se explicará posteriormente. Por otra
parte, a variables ordinarias no se les puede asignar direcciones arbitrarias (una expresión como
&x no puede aparecer en la parte izquierda de una oración de asignación).
3
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
Dentro de la declaración de variable, una variable puntero puede ser inicializada asignándole la
dirección de otra variable. Tener en cuenta que la variable cuya dirección se asigna al puntero
debe estar previamente declarada en el programa. Por ejemplo, un programa contiene las
siguientes declaraciones,
float u, v;
float *pv = &v;
Las variables u y v son declaradas como variables de punto flotante y pv es declarada como
una variable puntero que apunta a cantidades de punto flotante y se le asigna la dirección de la
variable v.
4
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
asignarse directamente a un elemento del arreglo o al área de memoria cuya dirección es la del
elemento del arreglo. Por otra parte, a veces es necesario asignar una dirección a un
identificador. En tales situaciones una variable puntero puede aparecer en la parte izquierda de
la asignación, sin embargo no es posible asignar una dirección arbitraria a un nombre de
arreglo o a un elemento del mismo. Por lo tanto, expresiones como x, (x + i) y &x[i] no
pueden aparecer en la parte izquierda de una asignación. Además, la dirección de un arreglo
no puede alterarse arbitrariamente, por lo que expresiones como ++x no están permitidas. En
general, la dirección de un arreglo declarado estáticamente como en el capítulo anterior es una
constante fijada por el compilador (el linker en realidad) y por ende, resulta imposible
cambiar su valor.
Para ejemplificar algunos de los aspectos enunciados consideremos el siguiente esqueleto de
programa,
#include <stdio.h>
int main( ) {
int linea[80];
int *pl;
...
/*asignar valores */
linea[2] = linea[1];
linea[2] = *(linea + 1);
*(linea + 2) = linea[1];
*(linea + 2) = *(linea + 1);
/* asignar direcciones */
pl = linea;
pl = &linea[0];
pl = &linea[1];
pl = linea + 1;
...
}
Cada una de las primeras cuatro oraciones de asignación le asigna el valor del segundo
elemento del arreglo (linea[1]) al tercer elemento del arreglo (linea[2]), por lo tanto las
cuatro son oraciones equivalentes. Cada una de las cuatro últimas estructuras asigna
direcciones. Las dos primeras asignan la dirección del primer elemento del arreglo al puntero
pl. Las dos últimas asignan la dirección del segundo elemento del arreglo al puntero pl.
Hemos establecido que un nombre de arreglo es en realidad un puntero al primer elemento dentro
del arreglo. Por lo tanto, debe ser posible definir un arreglo como una variable puntero en lugar de
definirlo como un arreglo convencional. Sintácticamente, las dos definiciones son equivalentes.
Sin embargo, la definición convencional de un arreglo produce la reserva de un bloque fijo
de memoria al principio de la ejecución de un programa, mientras que esto no ocurre si el
arreglo se representa en términos de una variable puntero. Como resultado del uso de una
variable puntero para representar un arreglo se torna necesario algún tipo de asignación inicial de
memoria, antes que los elementos del arreglo puedan ser utilizados. Generalmente, tales tipos de
reserva de memoria se realizan usando una función de la biblioteca <stdlib.h> llamada malloc
(por Memory ALLOCation), pero el método exacto varia de una aplicación a otra. Si el arreglo ha
sido definido como una variable puntero, no se le pueden asignar valores iniciales numéricos. Por
tanto, será necesaria una definición convencional si se quiere inicializar los valores de los
elementos de un arreglo.
6
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
Para ilustrar este uso de punteros, considérese de nuevo el problema de ordenar una lista de
enteros que fue descrito en el capitulo de arreglos.
#include <stdio.h>
int main( ) {
int i, n, *x;
void reordenar(int n, int *x);
/* Leer el valor para n */
printf ("\nCuantos números serán introducidos? ");
scanf ("%d", &n);
printf("\n");
/* reserva de memoria */
x = (int *)malloc(n * sizeof(int));
/* leer la tabla de números */
for (i = 0; i < n; ++i) {
printf("\ni= %d x = ", i + 1);
scanf ("%d", x + i);
}
/* reordenar todos los elementos del arreglo */
reordenar (n, x);
/* mostrar la tabla reordenada */
printf ("\n\n La Tabla de números reordenada es: ");
for (i = 0; i < n; ++i) {
printf ("i = %d x = %d\n", i + 1, *(x + i));
}
free(x); /*se libera la memoria solicitada */
return 0;
}
void reordenar (int n, int *x) {
int i, elem, temp;
for (elem = 0; elem < n - 1; ++elem) {
/* buscar el menor del resto de los elementos */
for (i = elem + 1; i < n; ++i) {
if (*(x + i) < *(x + elem)) {
/* intercambiar los elementos */
temp = *(x + elem);
*(x + elem) = *(x + i);
*(x + i) = temp;
}
}
}
}
7
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
px+3
Una variable puntero puede restarse de otra siempre que ambas variables apunten a elementos
del mismo arreglo. El valor resultante indica el número de elementos del tamaño del tipo de dato
especificado para el arreglo, que separan a los correspondientes elementos del arreglo.
En el siguiente programa, si se asume que los int tienen un tamaño de dos bytes, y dadas las
dos variables puntero apuntan al primero y al último elemento de un arreglo de enteros.
8
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
#include <stdio.h>
int main( ) {
int *px, *py; /* punteros a enteros */
int a[6] = { 1, 2, 3, 4, 5, 6};
px = &a[0];
py = &a[5];
printf ("px=%X py=%X", px, py);
printf ("\n\npy - px = %X", py - px);
return 0;
}
Ejecutando el programa se produce la siguiente salida,
px= 52 py= 5C
py - px = 5
9
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
5. Una variable puntero puede ser restada de otra con tal que ambas apunten al mismo tipo de
datos.
6. Dos variables puntero pueden compararse siempre y cuando apunten a datos del mismo
tipo.
No se permiten otras operaciones aritméticas con punteros. Así una variable puntero no puede
ser multiplicada por una constante, dos punteros no pueden sumarse, etc.
Dado que pa es un puntero a int, cualquier operación aritmética que se realice sobre él tendrá
necesariamente en cuenta el tamaño del tipo int, y si el valor de a fuera 0x1234 y el tamaño
del tipo int fuera de 2 bytes, el resultado de la operación (pa + 3) resultaría en 0x123A,
mientras que la operación (vpa + 3) resultaría en 0x1237 dado que vpa apunta al tipo
void.
Debe quedar claro que los desplazamientos que se realicen con un (void *) sobre un
conjunto de datos que pertenezcan a algún tipo de dato definido pierde todo rasgo de
coherencia y posibilidad de utilización directa, pero resulta extremadamente útil para recorrer la
memoria byte a byte sin importar el tipo de los datos recorridos.
Esta función lo que hace es solicitarle al sistema operativo un cierto bloque de memoria de
longitud cantidad_bytes. El bloque de memoria debe venir expresado en bytes y no en datos.
El sistema operativo responde a nuestro programa entregando la dirección del primer byte del
bloque de memoria asignada o en su defecto la constante NULL para indicar que lo solicitado
es imposible de ser asignado. Supongamos el siguiente ejemplo aclaratorio:
...
int *pv;
int n=100;
/* Pedido de memoria */
if((pv = (int *)malloc(100 * sizeof(int)))==NULL) {
printf(“No hay memoria disponible\n”);
exit(1);
}
...
free(pv); /* Liberar memoria*/
En este ejemplo se solicita memoria para 100 datos de tipo entero y a través de la estructura if
se verifica si el sistema operativo accedió al pedido o no. En la estructura se puede ver que la
función malloc retorna un puntero (pv) al cual se le realiza un cast de tipo para indicar que el
bloque de memoria solicitado es para datos de tipo entero.
La oración final del ejemplo anterior, utiliza la función free() para devolverle al sistema
operativo la memoria solicitada. Esta acción de devolución debe realizarse cuando los datos en
la memoria ya no sean de interés para nuestro programa.
11
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
Y en este caso, cada puntero del arreglo dará origen a una de las filas del arreglo
bidimensional. Similarmente un arreglo n-dimensional puede definirse como un arreglo de
punteros de dimensión (n-1) escribiendo,
<tipo-dato> *nombre_puntero[expresion1][expresion2]...[expresión(n-1)];
en lugar de,
<tipo-dato> nombre_arreglo[expresion1][expresion2]...[expresión n];
Se debe notar que el nombre del arreglo precedido por asterisco no está encerrado entre
paréntesis en este tipo de declaración. Así la regla de precedencia de derecha a izquierda
asocia primero el par de corchetes con el arreglo, definiéndolo como tal. El asterisco que lo
precede establece que el arreglo contendrá punteros que apuntan al tipo de dato especificado.
Como ejemplo consideremos otra versión del programa que suma matrices.
#include <stdio.h>
#include <stdlib.h>
#define MFIL 30
/* prototipos de función */
void leematriz(int *a[MFIL], int nfil, int ncols);
void sumamatriz(int *a[MFIL], int *b[MFIL], int *c[MFIL], int nfil, int ncols);
void escribematriz(int *c[MFIL], int nfil, int ncols);
/* cada arreglo bidimensional se representa como un arreglo de punteros, donde
cada puntero indica una fila del arreglo bidimensional original */
int main( ) {
int fila, nfilas, ncols;
/* definición de punteros */
int *a[MFIL], *b[MFIL], *c[MFIL];
printf("Cuantas filas?");
scanf("%d",&nfilas);
printf("Cuántas columnas?");
scanf("%d", &ncols);
/*reserva espacio de memoria*/
for(fila =0; fila < nfilas; ++fila) {
a[fila] = (int *)malloc(ncols * sizeof(int));
b[fila] = (int *)malloc(ncols * sizeof(int));
c[fila] = (int *)malloc(ncols * sizeof(int));
}
printf("Primera Matriz:\n");
leematriz(a, nfilas, ncols);
12
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
printf("Segunda Matriz:\n");
leematriz(b, nfilas, ncols);
sumamatriz(a, b, c, nfilas, ncols);
printf("Matriz Suma:\n");
escribematriz(c, nfilas, ncols);
return 0;
}
/* Lee matriz*/
void leematriz(int *a[], int m, int n) {
int fila, col;
for (fila = 0; fila < m; ++fila) {
printf ("Introducir datos para la fila %2d\n", fila + 1);
for (col = 0; col < n; ++col) {
printf (" x[%d][%d]= ",fila + 1, col + 1);
scanf ("%d", (a[fila]+col));
}
}
}
/* Suma Matrices */
void sumamatriz(int *a[], int *b[], int *c[], int m, int n) {
int fila, col;
for (fila = 0; fila < m; ++fila)
for (col = 0; col < n; ++col)
*(c[fila]+col) = *(a[fila]+col) + *(b[fila]+col);
}
/* Escribe el resultado */
void escribematriz(int *c[], int m, int n) {
int fila, col;
for (fila = 0; fila < m; ++fila) {
for (col = 0; col < n; ++col)
printf ("c[%d][%d]= %4d",fila + 1, col + 1,*(c[fila]+col));
printf("\n");
}
}
printf("Cuantas filas?");
scanf("%d",&nfilas);
printf("\nCuántas columnas?");
scanf("%d", &ncols);
/* reserva espacio de memoria para cada arreglo de punteros */
a = (int **)malloc( nfilas * sizeof( int *) );
b = (int **)malloc( nfilas * sizeof( int *) );
c = (int **)malloc( nfilas * sizeof( int *) );
/* reserva espacio de memoria para cada fila igual que antes */
for (fila =0; fila < nfilas; ++fila) {
a[fila] = (int *) malloc (ncols * sizeof(int));
b[fila] = (int *) malloc (ncols * sizeof(int));
c[fila] = (int *) malloc (ncols * sizeof(int));
}
printf("Primera Matriz:\n");
leematriz(a, nfilas, ncols);
printf("Segunda Matriz:\n");
leematriz(b, nfilas, ncols);
sumamatriz(a, b, c, nfilas, ncols);
printf("Matriz Suma:\n");
escribematriz(c, nfilas, ncols);
/* Liberar memoria: Primero las filas */
for(fila =0; fila < nfilas; ++fila) {
free( a[fila] );
free( b[fila] );
free( c[fila] );
}
/* y luego los arreglos de punteros */
free( a );
free( b );
free( c );
14
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
return 0;
}
/* Lee matriz*/
void leematriz(int **a, int m, int n) {
int fila, col;
for (fila = 0; fila < m; ++fila) {
printf ("Introducir datos para la fila %2d\n", fila + 1);
for (col = 0; col < n; ++col) {
printf ("x[%d][%d]=", fila + 1, col + 1);
scanf ("%d", (a[fila]+col));
}
}
}
/* Suma Matrices */
void sumamatriz(int **a, int **b, int **c, int m, int n) {
int fila, col;
for (fila = 0; fila < m; ++fila) {
for (col = 0; col < n; ++col) {
*(c[fila]+col) = *(a[fila]+col) + *(b[fila]+col);
}
}
}
/* Escribe el resultado */
void escribematriz(int **c, int m, int n) {
int fila, col;
for (fila = 0; fila < m; ++fila) {
for (col = 0; col < n; ++col) {
printf ("c[%d][%d]=%4d", fila + 1, col + 1, *(c[fila]+col));
}
printf("\n");
}
}
15
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
}
…
int (*ptr)( int, int ) = func1;
y ahora es posible invocar a la función por medio del puntero!
resultado = ptr( 10, 20 );
Análogamente, es también posible enviar punteros a funciones como parámetros de otras
funciones, tal como muestra la función proceso() en el ejemplo a continuación, donde existen
cuatro funciones: main, proceso, suma, y multiplica, y cada una de ellas retorna un entero.
#include <stdio.h>
#include <stdlib.h>
int main( ) {
int i, j;
i = proceso( suma ); /* se envía suma; retorna un valor para i */
printf( "Suma=%d\n", i);
Notar que main llama a la función proceso dos veces. En la primera pasa suma a proceso,
mientras que en la segunda pasa multiplica. Cada llamada retorna un valor entero. Estos
valores son asignados a las variables enteras i y j, respectivamente. Examinemos la definición
de proceso. Esta función tiene un parámetro formal, pf, declarado como un puntero a función.
La función a la que apunta pf devuelve un valor entero. Dentro de proceso se llama a la función
a la que apunta pf. Se pasan dos cantidades enteras (a y b) como argumentos a la función. La
función devuelve un entero que es asignado a la variable c. El resto de las definiciones de
variables son normales (suma y multiplica). Estas son las funciones pasadas desde main
hasta proceso. Los valores resultantes se obtienen de los argumentos y cada función procesa
sus argumentos de entrada de modo diferente.
16
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
Indica una función que acepta un argumento entero y devuelve un puntero a entero. Por otra
parte, la declaración:
int (*p)(int a);
Indica un puntero a función que acepta un argumento entero y retorna un entero. En esta última
declaración, los primeros paréntesis se usan para anidar y los segundos para indicar una
función. La interpretación de declaraciones mas complejas puede hacerse molesta. Por
ejemplo, considerar la siguiente declaración,
int *(*p)(int (*a)[ ]);
En esta declaración, (*p)(...) indica un puntero a función. Por esto, int (*p)(...) indica
un puntero a función que retorna un entero. Dentro de los dos últimos paréntesis (la
especificación de los argumentos de la función), (*a)[] indica un puntero a un arreglo. Como
resultado, int (*a)[] representa un puntero a un arreglo de enteros. Uniendo las piezas,
(*p)(int (*a)[]) representa un puntero a una función cuyo argumento es un puntero a un
arreglo de enteros. Finalmente la declaración completa,
int *(*p)(int (*a)[]);
Representa un puntero a una función que acepta un puntero a un arreglo de enteros como
argumentos y devuelve un puntero a entero, aunque una función puede devolver punteros a
cualquier tipo de datos.
/* Función que retorna un puntero al primer carácter en s que es igual a c */
char *match( char c, char *s ) {
int cont = 0;
int main( ) {
char s[80], *p, ch;
17
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
int main( ) {
int m, n, p, i, l;
int *a[MAX], *b[MAX], *c[MAX];
clrscr();
printf(”\nlngrese nro filas a, columnas a, y columnas b”);
scanf("%d %d %d”, &m, &n, &p);
for ( i=0; i<m; i++) {
a[i] = (int *) malloc (n * sizeof (int));
c[i] = (int *) malloc (p * sizeof (int));
}
for (i=0; i<n; i+ +) {
b[i] = (int *)malloc(p * sizeof (int));
}
leem(a,m,n);
leem(b,n,p);
prod(a,b,c,m,n,p);
escrib(c,m,p);
return 0;
}
void leem(int *a[[MAX], int m, int n) {
int fil, col;
printf(”Matriz:\n”);
for(fil=0; fil<m; fil++) {
for(col=0; col<n; col++) {
printf(”Elemento [&d][%d] = “, fil+ 1, col+ 1);
scanf(”%d”, a[fil] + col);
/* Lo siguiente tambien funciona
scanf(” %d”, &a [fil][col] ); */
}
printf(”\n”);
}
}
void prod(int *a[MAX], int *b[MAX], int *c[MAX], int m, int n, int p) {
int fil, col, i;
for(fil = 0; fil<m; fil++) {
18
COMPUTACIÓN II (ELO) - INFORMÁTICA II (BIO)
19