Está en la página 1de 67

Unidad 3 -

Punteros en C
Juan José Ramírez Lama Omar A. Cabezas Calderón
info2@juaramir.com info2@isjom.com
Www.juaramir.com
Definicion

 Los punteros son un nuevo tipo de variable. Al igual


que todas las variables que conocemos de C, tienen
un tipo de dato predeterminado. Por ejemplo:
 int a;
(está indicando que a es del tipo int, y que sólo podrá
guardar valores de tipo int.)
 Los punteros, a diferencia de las variables, almacenan
direcciones de memoria, no datos. Al igual que las
variables, tienen un tipo: un puntero hará referencia a
una dirección de memoria que está guardando un tipo
de dato, y el puntero debe ser de ese tipo de dato.
2
Para que sirven?

 Los Punteros te permiten manejar las


direcciones de memoria.
 Te otorgan mayor control sobre esta.
 Además, tener acceso a la dirección de
memoria donde se almacena una variable me
permite modificarla en cualquier parte del
programa.
 El acceso a una variable por medio de un
puntero será posible sólo si entregas la
dirección de la variable.
3
Para que sirven?

 Los punteros también tienen otra gracia: nos


permiten crear arreglos de largo
desconocido, es decir, si queremos guardar
las notas de un alumno en un arreglo,
podríamos preguntarle al usuario cuantas notas
desea guardar y luego crear el arreglo del
tamaño necesario. También se pueden crear
matrices, pero advierto desde ya que eso no es
fácil, pero tampoco es imposible de entender.

4
Operadores de Dirección (&)
e Indirección (*)
 Operador Dirección (&): permite hallar la
dirección de la variable a la que se aplica.
 Operador Indirección (*): Permite acceder al
valor depositado en la zona de memoria a la
que apunta un puntero.

Un puntero es una verdadera variable, y por


tanto puede cambiar de valor, es decir,
puede cambiar la variable a la que apunta.
5
Manejo básico de punteros
Declarando punteros
Las variables de tipo puntero se declaran poniendo un *
delante del nombre.
Por ejemplo:
int *p;

Está declarando a p como un puntero. Sabemos que p


guardará una dirección de memoria,y que en esa
dirección de memoria sólo se puede almacenar un
entero. Si tratáramos de almacenar otra cosa que no
fuera un entero (o si tratamos de asignarle a p una
dirección donde no se guarda un entero) el compilador
gentilmente nos recordará el error... 6
Asignando punteros

 Ya sabemos que una variable puntero almacena


direcciones de memoria. Ya sabemos cómo crear una
variable de tipo puntero. ¿Cómo le asigno a un
puntero la dirección de una variable?
 Para eso existe el operador & (ampersand) de C.
Cuando se pone un & delante de una variable, se está
obteniendo la dirección de dicha variable. Entonces,
una forma de asignarle una dirección a un puntero
podría ser:
int a, *p; /*p es un puntero.*/
p = &a; /* le asigno a p la dirección de a. */
7
Obteniendo el valor por medio del
puntero

Ya que los punteros hacen referencia a una variable,


podrías querer obtener el valor de esa variable usando el
puntero. Para eso, se debe usar el operador * delante
del nombre de la variable. Si, esto suele causar
confusión porque el * también se utilizó para declarar la
variable; el tip aquí es que cuando intentas acceder al
dato que está en la dirección de memoria apuntado por p
no poner el tipo de dato del puntero, así que se hace
más fácil diferenciar entre la declaración del puntero y su
uso.

8
Ejemplo 1

9
Ejemplo 2
int i, j, *p; // p es un puntero a int
p = &i; // p contiene la dirección de i
*p = 10; // i toma el valor 10
p = &j; // p contiene ahora la dirección de j
*p = -2; // j toma el valor -2

Las siguientes sentencias son ilegales:


p = &34; // las constantes no tienen dirección
p = &(i+1); // las expresiones no tienen dirección
&i = p; // las direcciones no se pueden cambiar
p = 17654; // habría que escribir p = (int *)17654;
10
Ejemplo 3

11
Casting
No se permiten asignaciones directas (sin
casting) entre punteros que apuntan a distintos
tipos de variables. Sin embargo, existe un tipo
indefinido de punteros (void *, o punteros a void),
que puede asignarse y al que puede asignarse
cualquier tipo de puntero.
Ejemplo:
int *p;
double *q;
void *r;
p = (int *)q; // legal
p = r = q; // legal
p = q; // ilegal 12
Ejemplo

13
Como Funciona la Memoria
 Hay cosas de la memoria del
computador que no
sabemos:
 Cuando tenemos una
instruccion como int a;
ocurren dentro del
computador 2 cosas:
 El sistema nos asigna
(marca) una celda de
memora para utilizar.
 Dejamos en esa celda
nuestra variable.
Nota: la m a la derecha significa que la
direccion 3 nos ha sido asignada, y que
podemos utilizarla (R/W)
14
Como Funciona la Memoria

 Cuando declaras un
puntero, todo se ve
igual. No olvides, eso
sí, que un puntero
guarda direcciones
de memoria y no
datos. Si tuviramos
una instruccion como
int *p; la memoria se
veria asi...
15
Como Funciona la Memoria
 En teoria, cuando creamos un nuevo puntero este no
apunta a ninguna zona de memoria; en la figura, eso se
indica mediante NULO, aunque en C se utiliza la palabra
NULL.
NULL Por lo tanto, las siguientes instrucciones llevaran
siempre a un error:

 ¿Y porque habria de fallar? Pues porque la direccion


apuntada por p no es una direccion que esté marcada para
el uso de tu programa. Por lo tanto, cuando comienzan a
utilizar punteros se comienza a hacer mano a lo que antes
se hacia en forma automatica: Reservar Memoria.
Memoria
16
Ejemplo Direcciones de
Memoria

Retorno:

17
Gestionando la memoria: malloc()
 Ya sabemos que debemos reservar la memoria para
poder utilizar un puntero desde cero. Veamos
entonces cómo se hace eso.
Reservando: malloc()
 Reservar memoria se hace con una simple función:
malloc().
malloc() Esta función recibe como parámetro el
tamaño (en bytes) del segmento de memoria que
quieres pedir.
 Para lograr ser compatibles, se recomienda el uso del
operador sizeof para determinar el tamaño exacto de
un tipo de dato.
 Requiere la libreria malloc.h 18
Gestionando la memoria: malloc()

 Si sizeof no te suena, un pequeño ejemplo:


printf("Un entero ocupa %d bytes de memoria\n",
sizeof(int));
 Con la instrucción anterior lograrás saber cuantos
bytes de memoria ocupa un entero; habitualmente,
son 4 bytes, pero podría ser una cifra distinta.
 El problema de la función malloc() es lo que retorna:
si se mira el manual, esta función... ¡retorna un void*
(puntero a void)! Es ilógico tener un puntero a nada,
pero esa es la forma C de decir un puntero a
cualquier tipo de dato.
19
Gestionando la memoria: malloc()
 La ventaja de un void* es que se puede transformar a
otro tipo de dato puntero.
 El proceso de conversión se llama cast. Esta
conversión (llamada tambíen conversión explícita) se
puede hacer con otros tipos de datos y se hace así:

20
Ejemplo

21
Liberando: free()

 La función malloc() tiene su hermana opuesta:


la función free().
free() El objetivo de free() es el
contrario a malloc(): free() libera una celda de
memoria marcada por la acción de malloc(). Lo
mejor de todo es que free() retorna void, así
que es fácil de usar.
 ¿Y para qué querría liberar la memoria? Estoy
ocupando la variable todo el tiempo, así que
todo el tiempo necesito la memoria ocupada
por la variable.
22
Liberando: free()

 En los ejemplos vistos hasta ahora, la frase


anterior es correcta. Pero no siempre es así:
cuando se conocen las estructuras de datos
dinámicas ahí se utiliza plenamente.
 Pero eso es ya materia de otro tutorial, así que
conformémonos con un ejemplo donde se
utilize a free():

23
Ejemplo

24
Gestion Avanzada de Memoria
 Si recordamos, le pasabamos a
malloc() el tamaño justo y exacto
de un entero. Pero ¿que pasa si
pedimos mas memoria?
 Esto es lo que pasa, se reservan
varios bloques uno al lado del otro
del mismo tipo de dato (te suena
conocido) es un arreglo.
 En la figura se ve que el arreglo
esta entre las direcciones 6 y 9 de
memoria; la posicion 0 del arreglo
corresponde a la direccion 6 de la
memoria.
 Fijate ademas que p apunta a la
primera posicion (posicion 0)
0 del
arreglo. 25
Ejemplo: ”pidiendo mas memoria”

26
La Hermana de malloc(): calloc()
 La función calloc() es una hermana de malloc()
pensada para crear arreglos. A diferencia de malloc(),
calloc() recibe dos parámetros: el número de
elementos que tendrá el arreglo y el tamaño de cada
uno de sus elementos.
 En realidad, no hay mucha diferencia con malloc(). De
hecho, en el código anterior el cambio sería mínimo:
la línea que dice:
arreglo = (int*)malloc(sizeof(int) * largo);
 se reemplaza por
arreglo = (int*)calloc(largo, sizeof(int));
 para obtener el mismo resultado. 27
Aritmética de punteros

 Ya vimos lo que ocurría con la memoria cuando


se reservaba un bloque de memoria y no una
única celda: el puntero se tranformaba en un
arreglo, apuntando a la posición cero. A partir
de eso, nos topamos con que se puede hacer
aritmética de punteros, es decir, incrementar y
decrementar el valor del puntero según sea
necesario.

28
Aritmética de punteros

Los punteros son unas variables un poco


especiales, ya que guardan información –no
sólo de la dirección a la que apuntan–, sino
también del tipo de variable almacenado en
esa dirección. Esto implica que no van a estar
permitidas las operaciones que no tienen
sentido con direcciones de variables, como
multiplicar o dividir, pero sí otras como sumar o
restar. Además estas operaciones se realizan
de un modo correcto, pero que no es el
ordinario.
29
Aritmética de punteros

Así, la sentencia:
p = p+1;
Hace que p apunte a la dirección siguiente de la
que apuntaba, teniendo en cuenta el tipo de
dato. Por ejemplo, si el valor apuntado por p es
short int y ocupa 2 bytes, el sumar 1 a p implica
añadir 2 bytes a la dirección que contiene,
mientras que si p apunta a un double, sumarle
1 implica añadirle 8 bytes.

30
Ejemplo 1

Supóngase que en el momento de comenzar la


ejecución, las direcciones de memoria de las
distintas variables son las mostradas en la Tabla.

31
Ejemplo 1

La Tabla muestra los valores de las variables en la ejecución del programa


paso a paso. Se muestran en negrita y cursiva los cambios entre paso y
paso. Es importante analizar y entender los cambios de valor.
32
Ejemplo 2

33
Ejemplo 2

Rara vez se trabajan los arreglos de esta forma, pero a


veces es necesario para optimizar.

34
Vectores, matrices y
cadenas de caracteres
 Los arrays presentan una especial relación con los
punteros.
 Puesto que los elementos de un vector y una matriz
están almacenados consecutivamente en la memoria,
la aritmética de punteros descrita previamente
presenta muchas ventajas. Por ejemplo, supóngase el
código siguiente:

35
Relacion entre Vectores
y Punteros
 Existe una relación muy estrecha entre los vectores y
los punteros. De hecho, el nombre de un vector es un
puntero (un puntero constante, en el sentido de que
no puede apuntar a otra variable distinta de aquélla a
la que apunta) a la dirección de memoria que contiene
el primer elemento del vector.Supónganse las
siguientes declaraciones y sentencias:
El identificador vect,
vect es decir
el nombre del vector, es un
puntero al primer elemento
del vector vect[ ].
] Esto es lo
mismo que decir que el valor
de vect es &vect[0].
&vect[0]

36
Relacion entre Vectores
y Punteros
 Existen más puntos de coincidencia entre los vectores y los punteros:
 Puesto que el nombre de un vector es un puntero, obedecerá las leyes de
la aritmética de punteros. Por tanto, si vect apunta a vect[0],
vect[0] (vect+1)
vect+1
apuntará a vect[1],
vect[1] y (vect+i)
vect+i apuntará a vect[i].
vect[i]
 Recíprocamente (y esto resulta quizás más sorprendente), a los punteros
se les pueden poner subíndices, igual que a los vectores. Así pues, si p
apunta a vect[0] se puede escribir:
 p[3]=p[2]*2.0; // equivalente a vect[3]=vect[2]*2.0;
 Si se supone que p=vect,
p=vect la relación entre punteros y vectores puede
resumirse como se indica en las líneas siguientes:
 *p //equivale a vect[0], a *vect y a p[0]
 *(p+1) //equivale a vect[1], a *(vect+1) y a p[1]
 *(p+2) // equivale a vect[2], a *(vect+2) y a p[2]
37
Ejemplo
Como  ejemplo  de  la  relación  entre  vectores  y 
punteros,  se  van  a  ver  varias  formas  posibles  para 
sumar los N elementos de un vector  a[ ]. Supóngase 
a[ ]
la siguiente declaración y las siguientes sentencias:

38
Relacion entre Matrices
y Punteros
 En el caso de las matrices la relación con los punteros
es un poco más complicada.
 Supóngase una declaración como la siguiente:
 int mat[5][3], **p, *q;
 El nombre de la matriz (mat) es un puntero al primer
elemento de un vector de punteros mat[ ] (por tanto,
existe un vector de punteros que tiene también el
mismo nombre que la matriz), cuyos elementos
contienen las direcciones del primer elemento de cada
fila de la matriz. El nombre mat es pues un puntero a
puntero. El vector de punteros mat[ ] se crea
automáticamente al crearse la matriz. 39
Relacion entre Matrices
y Punteros
 Así pues, mat es igual a &mat[0]; y mat[0] es
&mat[0][0]. Análogamente, mat[1] es &mat[1][0],
mat[2] es &mat[2][0], etc. La dirección base sobre la
que se direccionan todos los elementos de la matriz
no es mat, sino &mat[0][0].
 Recuérdese también que, por la relación entre
vectores y punteros, (mat+i) apunta a mat[i].
 Recuérdese que la fórmula de direccionamiento de
una matriz de N filas y M columnas establece que la
dirección del elemento (i, j) viene dada por:
 dirección (i, j) = dirección (0, 0) + i*M + j

40
Relacion entre Matrices
y Punteros
Teniendo esto en cuenta y haciendo **p = mat;
se tendrán las siguientes formas de acceder a
los elementos de la matriz:

41
Relacion entre Matrices
y Punteros
 Por otra parte, si la matriz tiene M columnas y
si se hace q = &mat[0][0] (dirección base de la
matriz. Recuérdese que esto es diferente del
caso anterior p = mat), el elemento mat[i][j]
puede ser accedido de varias formas. Basta
recordar que dicho elemento tiene por delante i
filas completas, y j elementos de su fila:

42
Relacion entre Matrices
y Punteros
Todas estas relaciones tienen una gran importancia,
pues implican una correcta comprensión de los
punteros y de las matrices. De todas formas, hay que
indicar que las matrices no son del todo idénticas a
los vectores de punteros: Si se define una matriz
explícitamente por medio de vectores de punteros,
las filas pueden tener diferente número de
elementos, y no queda garantizado que estén
contiguas en la memoria (aunque se puede hacer
que sí lo sean). No sería pues posible en este caso
utilizar la fórmula de direccionamiento y el acceder
por columnas a los elementos de la matriz.
43
Relacion entre Matrices
y Punteros
La  figura  resume  gráficamente  la  relación  entre 
matrices y vectores de punteros.

44
Ejemplo

45
Ejemplo

46
Ejemplo

47
Inicializacion de Vectores
y Matrices
 La inicialización de un array se puede hacer de
varias maneras:
 Declarando el array como tal e inicializándolo luego
mediante lectura o asignación por medio de un
bucle for:

48
Inicializacion de Vectores
y Matrices
Inicializándolo en la misma declaración, en la forma:

Donde es necesario poner un punto decimal tras


cada cifra, para que ésta sea reconocida como un
valor de tipo float o double.

49
Pasando arreglos a funciones

 Con lo que sabemos, la única forma de pasar un arreglo


como parámetro a una función es diciendo
 void funcion(int arreglo[max], int largo)
 Pero ya hemos visto que los arreglos y los punteros son
hermanos, así que la función anterior se pudo haber
declarado como
 void funcion(int *arreglo, int largo)
 Lo que sí es común a ambas es que no pueden saber el
largo del arreglo, así que necesitarían el parámetro largo
si quisieran recorrer el arreglo.

50
Pasando Matrices a Funciones

 Tambien hasta ahora solo pasabamos matrices


de esta forma:
 void funcion(int mat[max][max], int fila, int
columna)
 Aplicando la misma forma de pasar un arreglo
como parametro lo aplicamos a la matriz:
 void funcion(int **mat, int fila, int columna)
 Y asi se seguiria para arreglos de mas
dimensiones.
51
Retornando arreglos en las funciones

 Hasta ahora, no sabíamos la forma de poder devolver


(con return) un arreglo. Eso porque la declaración de
funciones
 int[] funcion(int cuantos);
no es válida. Para ello, volvemos a recurrir a los
punteros:
 int* funcion(int cuantos);
y con eso estoy diciendo que función retornará un
arreglo de enteros.
 Se aplicaria el mismo sistema para las matrices
52
Punteros y variables globales

 Entregar la dirección de memoria de una variable es


un acto de confianza: teniendo la dirección, se tiene
pleno acceso para modificar la variable. Por esa
razón, una variable se pasará por referencia (pasar la
dirección de memoria donde la variable está
almacenada) sólo cuando sea necesario.
 Otra ventaja de los punteros sobre la variable global
es que la variable global podría ser modificada por
cualquier función en cualquier momento, mientras que
con punteros la variable sólo se modificará cuando yo
pase su dirección.

53
Punteros y Struct

Ya habíamos mencionado que un puntero puede


hacer referencia a cualquier tipo de dato. Y
cuando digo a cualquiera, de verdad que me
refiero a cualquiera. Y para que veas...

54
Ejemplo

55
Punteros y Struct
Operador ->

Pues si: se pueden hacer punteros a estructuras sin


problemas.
De todas maneras, hay algo un poquito feo en ese
código: la forma de acceder a cada campo.
Por ejemplo, la idea de tener que decir:
(*f).dia = 23; // el ( ) es necesario por la mayor prioridad del operador (.) respecto a (*).
Parece un poco engorroso, y a los diseñadores de C
también les pareció lo mismo:
Crearon un nuevo operador: el operador flecha (->). ->
Con él, la línea anterior se transforma en
f->dia = 23;
y así queda un código más fácil de escribir y comprender. 56
Paso de Struct a Funciones
 Para la ejemplificación de estos casos
utilizaremos la siguiente estructura:
struct datos_carnet {
long int numero;
char letra;
char nombre[50];
char apellidos[100];
};
struct datos_carnet dni;
Paso de estructuras completas
como parámetros
 Para pasar, la variable dni del ejemplo anterior
por referencia (dirección de memoria)
añadiendo el símbolo “&”, de esta otra manera:
 escribir_dni(&dni);
 A su vez, la función escribir_dni() debe
especificar en su declaración si el argumento
se pasa por variable. El paso por variable
tiene esta forma (usando el símbolo “ *”):
 void escribir_dni(struct datos_carnet* dni)
Paso de estructuras completas
como parámetros
 Dentro de la función, el acceso a los miembros
de la estructura es diferente si ésta ha sido
pasada por valor o por variable. Así, por
ejemplo, el acceso al miembro nombre de la
estructura dni, si ésta ha sido pasada por
variable, se sustituye el punto por la flecha “->”:
 printf("%s", dni->nombre);
Paso de miembros de estructuras
como parámetros
 Si lo que queremos es pasar el miembro dni.numero por
variable, no por valor, lo haremos igual que con cualquier
dato de tipo entero, es decir, agregando el símbolo & a la
llamada:
 escribir_dni(&dni.numero);
 Y en la declaración de la función el parámetro debe llevar
el símbolo “*”:
 void escribir_dni(long int *numero)
 En este caso, cada vez que vaya a usarse el parámetro
número dentro del código de la función, al estar pasado
por variable debe ir precedido del símbolo “*”; por ejemplo:
 *numero = 5;
Punteros y Struct

Como las estructuras pueden anidarse, así


tenemos una posibilidad de tener punteros
anidados (por darles algún nombre). En el fondo,
me refiero a una estructura dinámica que tenga
como campo a otra estructura dinámica. No es
difícil: son simplemente dos llamadas a malloc()...

61
Ejemplo

62
Ejemplo

63
Ejemplo

Un detalle importante. 
¿Por qué elimine r­> inicio antes 
r­> inicio
que a r?
La  respuesta  es  sencilla:  si 
eliminaba a r primero, me perdía 
el puntero a   r­>inicio, y sin ese 
r­>inicio
puntero  no  podría  liberar  la 
memoria  pedida  para  dicha 
variable.

64
Librerias

 Escenciales:
 #include<stdio.h>
 #include<stdlib.h>
 Opcionales:
 #include<malloc.h>

65
66
Elaborado por Juan Jose Ramirez Lama

Basado en:
Punteros en C

 Alejandro Sandoval Véjar


Aprendiendo el Lenguaje ANCI C como si

estuviera en primero.

Http://www.juaramir.com

67