Você está na página 1de 14

Lenguaje C

Apuntadores (pointers en inglés; también punteros)


Variables y Memoria
La memoria del computador es como una larguísima línea (1 dimensión) en la que se guardan pulsos (dígitos 0 y 1), allí solo se puede
avanzar para adelante y para atrás; lo cual es muy limitado para guardar datos del mundo real: arreglos, caracteres, dibujos, sonidos,
imágenes, video, etc. Para ello se clasifica la memoria en segmentos:

tipo 1 tipo 2 tipo 3

Cada tipo de segmento contiene datos que se interpretan como números, caracteres, imágenes, etc. A veces un tipo de dato tiene varias
interpretaciones, por ejemplo, un dato que contenga 1000001 puede ser interpretado como 65 10 o como 4116 o como la letra A; para
hacer esto, solo escribimos:
int n = 65;
Memoria RAM n
...01011 1000001 01100..
0x7ff...c (dirección de n en base hexadecimal)
En la realidad solo existen pulsos interpretados como 0s y 1s, los objetos en rojo no existen, sólo sirven para informar a los humanos, so-
bre el contenido digital y organizativo de la RAM y su interpretación. En tiempo de ejecución, el sistema operativo, ubica a n en una posi-
ción disponible, por ejemplo, 0x7ff...c, es casi seguro que esta posición cambie si se reprocesa el programa. El programador no asigna
estas posiciones, solamente puede referirse a ellas, preparemos las herramientas para ello. Ya hemos utilizado en modo limitado dos
operadores:
Operador &: suministra la dirección de una variable, por ejemplo:
printf(“%p\n", &n);
Salida: 0x7ff...c

Operador *: suministra el contenido de una dirección, por ejemplo:


printf(“%d\n", *&n);
Salida: 65

En resumen:
Operador Descripción
& Operador de dirección (referencia) : &n = dirección de n = 0x7ff...c
* Operador de indirección (dereferencia): *&n = valor de la dirección de n = 65
* es casi el inverso de &: *&n = *(&n) = n; pero &*n no compila.

Apuntador
Sea una variable:
int n = 65; // En tiempo de ejecución n es aloja en una posición hexadecimal, ejemplo: 0x7ff...c .
RAM n
65
0x7ff...c
Aplicando el operador de referencia, se cumple:
&n = 0x7ff...c
Supongamos una variable pn, de tipo hexadecimal, podemos asignar:
pn = &n; // = 0x7ff...c
Aplicando el operador de desreferencia, se cumple:
*pn // = *0x7ff...c = 65
¿Cómo se define pn?, podría ser:
hexa pn; // puede ser; pero NO hay tipo hexadecimal
Más bien aplicaremos un artificio muy elegante, definiendo a *pn:
int *pn = &n; // *pn es de tipo int, y pn = &n
Se cumplirá:
RAM pn n
0x7ff...c 65
0x7ff...0 0x7ff...c
Un apuntador pn es una variable, como cualquier otra; ocupa 8 bytes de memoria; almacena un valor hexadecimal, el cual es la direc -
ción de otra variable. pn puede ser interpretada como un variable espía de n, con las siguientes capacidades:
Sabe la dirección de n: pn → 0x7ff...c
Sabe el valor de n : *pn → 65
Puede cambiar el valor de n: *pn = 66; // asigna 66 a n.

PÁGINA: 1
Lenguaje C
El formato de impresión es p: printf("%p", pn);
Se puede leer un apuntador : scanf("%p", pn); // compila y ejecuta pero no tiene sentido en tiempo de ejecución.

Definición de un apuntador
int n;
int *pn = &n; // *pn es el valor (de tipo int) apuntado por pn

Se lee así:
El valor apuntado por pn es de tipo int
int *pn = &n;
valor de pn = dirección de n

Se puede definir y asignar por separado:


int n = 65;
int *pn;
pn = &n; // pn toma como valor la dirección de n
O definir todo junto:
int n = 65, *pn = &n;

Atento a los detalles


1) Al asignar valor a pn:
int n, m, *pn;
pn = &n; // pn apunta a n (al primer byte)
pn = &m; // p apunta a m (otra variable)
pn = NULL; // pn no apunta a nada

pn = 0x7ff...C; // No compila porque no podemos manejar directamente a la memoria; esta es tarea del sistema operativo.
*pn = 0x7ff...c; // No compila , además *pn es de tipo entero.
n = 0x6ff...c; // No compila , además n es de tipo entero, no hexadecimal

2) & y * son opuestos para apuntadores; pero no para otro tipo de variables:
pn es apuntador: *&pn = pn = 0x7ff...c; &*pn = pn = 0x7ff...c
n no es apuntador: *&n = n = 65; pero &*n, no compila, por que *n no compila por no ser posición de memoria.

3) Un apuntador tiene tres atributos:


 tipo de dato al que apuntará : int, char, etc. (un solo tipo).
 tipo : apuntador, ocupa 8 bytes, el formato de impresión es %p: printf(“%p", pn);
 valor : dirección de la variable o bloque apuntado (en este caso) = 0x7ff...c
tipo dato, tipo, valor

int n = 65, *pn = &n;

4) Variables y valores del ejemplo:


n = 65 pn = 0x7ff...c
&n = 0x7ff...c &pn = 0x7ff...0
*n error, n no es posición de memoria *pn = 65

5) Mientras pn apunte a n: int n = 65, *pn = &n, m;


Se cumplen las siguientes relaciones:
Variables Relación Ejemplo
&n y pn Equivalentes para scanf(“%d", &n);
leer valor scanf(“%d", pn);
No equivalentes pn = &m; // apunta a otra variable
para asignar valor &n = &m; // error: no se asigna posición de memoria a una variable
n y *pn Equivalentes para n = 65; *pn = 65;
asignar y escribir n += 2; *pn += 2;
valores printf(“%d", n); printf(“%d", *pn);

6) Ya utilizamos apuntadores para pasar valores por referencia en funciones :


miFun(&n); // el valor de n cambiará a 3.
PÁGINA: 2
Lenguaje C
Llama a:
void miFun(int *n) {*n= 3;}
Pero el uso más frecuente es para apuntar a bloques de datos, como veremos luego.

7) Un solo operador * representa a dos operaciones distintas:


* : Indirección
* : Multiplicación
El operador de indirección * tiene mayor prioridad que el operador de multiplicación *, ejemplo:
int n=2, *pn= &n, b;
b = *pn * 2; // es equivalente a: b = *pn*2; b = 2 * *pn; b = 2**pn; b = 2 * * pn; (no importa la cantidad de espacios)
No abuse de la escritura confusa, de ser necesario use paréntesis para aclararla. Más adelante estudiaremos las reglas de prioriza-
ción.

Ejemplo: Una variable puede ser apuntada por varios apuntadores:

p1 p2 n m
0x7ff...a 0x7ff...a 2 8
0x7ff...a

#include<stdio.h>
void main(void){
int n=2 , *p1= &n, *p2= &n, m = 8; // declaración y asignación de valores
if(p1==p2) printf("%d %d\n", *p1, *p2); // Igualdad de apuntadores
}
Salida:
2 2

Observaciones:
El valor de n puede cambiar de 3 modos:
n = 4;
*p1 = 4;
*p2 = 4;

Un puntero apunta a 0 o 1 variable; pero puede cambiar de variable apuntada:


p1 = &m; // y deja de apuntar a n

Apuntador a un arreglo de una dimensión


Un apuntador puede apuntar a un arreglo, ejemplo:
int arr[3] = {10, 20 ,30}, arr1[4], *ptr = arr; // Atento, no es necesario &, ya lo explicaremos luego.

memoria ptr arr


0x7ff...0 10 20 30
Direcciones 0x7fa...b 0x7ff...0 0x7ff...4
Indices: 0 1 2

Aritmética de apuntadores
Dado que un apuntador apunta a una dirección sobre una línea, sólo se puede desplazar hacia adelante o atrás, mediante sumas y
restas:
ptr; // == 0x7ff...0. Apunta a arr[0]
*ptr; // == 10
ptr + 1; // Apunta a la siguiente posición de tipo int: avanza 4 bytes: 0x7ff...4
// si ptr apuntara a un tipo long, sumará 8 bytes, para char sumará 1 byte; etc.
*(ptr+1); // == 20
ptr+n; // Apunta n posiciones más adelante
*(ptr+n); // valor n posiciones más adelante

ptr-n; // Apunta n posiciones previas


*(ptr-n); // valor n posiciones previas

Se permite la resta de dos punteros:


int nn[10], *ptr1, *ptr2, n;
ptr1 = nn; // equivalente a: ptr1 = &nn[0];
// apunta a nn[0].
PÁGINA: 3
Lenguaje C
ptr2 = &nn[2]; // apunta a nn[2].
n = ptr2 - ptr1; // diferencia (en posiciones enteras) = 2.

No se permiten otro tipo de operaciones


ptr = ptr * 2; // Error: No se admiten multiplicaciones de valores de memoria
ptr1 = ptr1 + ptr2; // Error

Asignaciones:
ptr = ptr + n; // equivale a ptr += n;
ptr = ptr - n; // equivale a ptr -= n;
ptr + 2*3+1; // = pptr + 7; apunta 7 posiciones de bytes, es decir 7*4 bytes, más adelante.
Ptr++; // suma 1.

Pre y postoperadores, funcionan como siempre; atento, aplique las precedencias:


Operador Descripción Asociatividad
() Paréntesis (llamar a funciones) izquierda-a-derecha
[] Corchetes (arreglos)
. selección de miembro vía objeto (ver capítulo de estructuras o clases)
-> selección de miembro vía apuntador (ver capítulo de estructuras)
++ -- Post incremento/decremento
++ -- Pre incremento/decremento Derecha-a-izquierda
+- Operadores unarios más/menos
!~ Negación lógica/bitwise complemento
(type) Casting (convierte valor a valor temporal de type)
* Desreferencia
& Dirección (de operador)
sizeof Determine el tamaño en bytes

ptr++; // aumenta 1 a ptr;


printf("%p\n", ptr++); // printf("%p\n", ptr); ptr++;
printf("%p\n", (ptr+1)++); // error de compilación, no se puede postoperar a una expresión (ptr+1)

*ptr++;
printf("%d\n", *ptr++); // printf("%d\n", *ptr); ptr++;
printf("%d\n",*(ptr++)); // printf("%d\n", *ptr); ptr++;
printf("%d\n", *(ptr+1)++); // error de compilación

++ptr; // aumenta 1 a ptr;


printf("%p\n", ++ptr); // ++ptr; printf("%p\n", ptr);
printf("%p\n", ++(ptr+1)); // error de compilación

++*ptr; // aumenta 1 a *ptr;


printf("%d\n", ++*ptr); // aumenta 1 a *ptr; printf("%d\n", *ptr) ;
printf("%d\n",++(*ptr)); // aumenta 1 a *ptr; printf("%d\n", *ptr) ;
printf("%d\n", ++*(ptr+1)); // aumenta 1 a *(ptr+1); printf("%d\n", *(ptr+1));

Apuntador constante
int a=10, b=20;
int * const p = &a; // p apunta solo a la variable a.
*p = 15; // Correcto: asigna 15 a a, el valor apuntado es variable.
p=&b; // ERROR: El valor de p es constante
p a b
0x7ff...a 10 20
0x7ff...a

PÁGINA: 4
Lenguaje C
Arreglo de una dimensión visto como apuntador
Un arreglo de una dimensión es un bloque de n datos del mismo tipo almacenados en modo consecutivo que se comporta como un
puntero constante que apunta a su primer elemento, ejemplo:

int arr[3] = {10, 20 ,30}, *ptr = arr;


memoria ptr arr
0x7ff...0 10 20 30
Direcciones 0x7fa...b 0x7ff...0 0x7ff...4
Indices: 0 1 2

arr == &arr[0] // arr apunta a su primer elemento.


*arr == arr[0] // 10

En modo general un arreglo de una dimensión y un apuntador se comportan casi equivalentemente:


Arreglo Puntero Valor
Notación arreglo Notación de Notación arreglo Notación de
punteros punteros
Arr ptr 0x7ff...0
arr[0] *arr // *(arr+0) ptr[0] *ptr // *(ptr+0) 10
arr[1] *(arr+1) ptr[1] *(ptr+1) 20
arr[2] *(arr+2) ptr[2] *(ptr+2) 30
arr[1] = 80; *(arr+1) = 80; ptr[1] = 80; *(ptr+1) = 80; 80
n = fun(arr, 3); n = fun(arr, 3); n = fun(ptr, 3); n = fun(ptr, 3); Llama a la función fun()
int fun(int arr[ ], int n){ int fun(int *arr, int n) { int fun(int ptr[ ], int n){ int fun(int *ptr, int n) { Define a la función fun()
arr[0] = 2; *arr = 2; ptr[0] = 2; *ptr = 2;
arr[1] = 3; *(arr+1) = 3; ptr[1] = 3; *(ptr+1) = 3;
*(arr+2) = 4; arr[2] = 4; *(ptr+2) = 4; ptr[2] = 4;
return arr[2]; return *(arr+2); return ptr[2]; return *(ptr+2);
} } } }

Mientras ptr apunte a un arreglo de 1 dimensión arr, por ejemplo:


int arr[3] = {10, 20, 30}, *ptr = arr;
Son equivalentes (sinónimos):
arr[i] = = ptr[i] = = *(arr+i) = = *(ptr+i)

Pero hay tres diferencias:


1) ptr es un puntero y puede apuntar a a cualquier variable; arr es como un puntero constante y solo se apunta a su primer elemento:
ptr = arreglo1; ptr = &var // es válido, ya que ptr es una variable
arr = arreglo1; // no compila, ya que arr es constante y no puede cambiar de valor

2) El valor de ptr y &ptr son diferentes; el valor de arr y &arr son iguales;
ptr tiene una dirección y apunta a otra dirección, no puede apuntarse a sí mismo
printf("%p\n", &ptr); // dirección de ptr: 0x7fa...b
printf("%p\n", ptr); // apunta a 0x7ff...0

arr tiene una dirección y se apunta a sí misma;


printf("%p\n", &arr); // dirección de ptr: 0x7ff...0
printf("%p\n", arr); // apunta a 0x7ff...0

3) La función sizeof() que da el tamaño de una variable en bytes, funciona de modo distinto, ejemplo:
#include <stdio.h>
void miFun(int arr[ ], int *ptr);
void main(void){
int arr[5]= {1,2,3,4,5}, *ptr = arr;
printf("%lu\n", sizeof(arr)); // salida 20 = 5*4, 4 es el sizeof de cada variable entera
printf("%lu\n", sizeof(ptr)); // salida 8, el sizeof de una variable de tipo apuntador.
miFun(arr, ptr);
}

PÁGINA: 5
Lenguaje C
void miFun(int arr[ ], int *ptr){
printf("%lu\n", sizeof(arr)); // salida 8, el sizeof de un apuntador, porque arr se comporta como apuntador
printf("%lu\n", sizeof(ptr)); // salida 8, el sizeof de un apuntador.
}

Aplicaciones de los apuntadores


Un apuntador se usa frecuentemente para recorrer un arreglo:
// 06_01a.c Notación de arreglos // 06_01b.c Notación de apuntadores
#include<stdio.h>
void main(void){
int n = 3, arr[3] = {1, 2, 3},
// variables de recorrido // apuntadores de recorrido
i = 0, // para iniciar el arreglo *p = arr, // apunta al inicio del arreglo
imax = i + n, // una posición después del final *pmax = p + n, // apunta una posición más allá del fin
j = imax -1; // finaliza el arrglo *q = pmax -1; // apunta a la posición final: arr[2]
for(; i < n; i++, j--) for(; p < pmax; p++, q--)
printf("Subiendo: %d Bajando: %d\n", arr[i], arr[j]); printf("Subiendo: %d Bajando: %d\n", *p, *q);
} }
Salida:
Subiendo: 1 Bajando: 3
Subiendo: 2 Bajando: 2
Subiendo: 3 Bajando: 1
Atento: El arreglo arr, base de los datos es fijo, tiene una posición de inicio y un número de elementos; es
responsabilidad del programador:
No perder la posición de inicio
No excederse ni a la izquierda ni a la derecha de arr.
Las variables i y j permiten el recorrido de arr Los apuntadores *p y *q recorren físicamente el arreglo

Otro ejemplo similar utilizando funciones


// Arreglo de una dimensión: Crear un arreglo, reportarlo, sumarle 2 a cada elemento y reportarlo
// 06_01c.c : Notación de arreglos // 06_01d.c : Notación de apuntadores
#include<stdio.h>
void reporte(int arr[], int n);
void suma2(int arr[], int n);
void main(void){
int n=10, arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
printf("Datos iniciales: ");
reporte(arr, n);
suma2(arr, n);
printf("Datos + 2 : ");
reporte(arr, n);
}
void reporte(int arr[ ], int n){ // Equivalente: void reporte(int *arr, int n){ arr[ ] es un puntero
// Los desplazamientos son lógicos // Los desplazamientos son físicos
int i; int *p = arr, *pmax = p+n;
// La variable i permite recorrer al arreglo de inicio a fin // el puntero p permite recorrer al arreglo de inicio a fin
for(i=0; i<n; i++) printf("%d ", arr[i]); for(; p<pmax; p++) printf("%d ", *p);
printf("\n");
}
void suma2(int arr[ ], int n){ // Equivalente: void reporte(int *arr, int n){ arr[ ] es un puntero
// Los desplazamientos son lógicos // Los desplazamientos son físicos
int i; // i es variable de recorrido int *p, *pmax = arr+n; // p es variable de recorrido
for(i=0; i<n; i++) arr[i] +=2; for(p=arr; p<pmax; p++) *p +=2;
}
Salida:
Datos iniciales: 0 1 2 3 4 5 6 7 8 9
Datos + 2 : 2 3 4 5 6 7 8 9 10 11

PÁGINA: 6
Lenguaje C

Fortalezas y debilidades de los apuntadores:


Tipo de variable apuntada Fortaleza/debilidad
Variables simples : int, float, etc. Debilidad: ocupa doble espacio de memoria, es lento, engorroso
Arreglo de una dimensión Similar: veremos una ventaja de apuntadores en memoria dinámica
Variables complejas: arreglos de más Fortaleza: permite que el usuario maneje la organización de información de tipo complejo,
de 1 dimensión, estructuras, pila, etc. cosa que no puede hacer un lenguaje.

Ventajas y desventajas de los apuntadores


Ventajas 1) Permiten adaptarse a la memoria lineal del computador y manejarla flexblemente en tiempo de ejecución; para
cada caso específico; el programador desarrolla los algoritmos para: alojar, cambiar de tamaño, liberar, acceder,
pasar a funciones, ordenar, proteger/desproteger, etc. datos, en especial datos complejos y grandes: arreglos,
estructuras, pilas, árboles, etc.
2) Un arreglo de datos, ejemplo una matriz de estudiantes, puede ser vista tal cual es físicamente, y al mismo
tiempo ser vista ordenada por código, apellido, etc. con la ayuda de apuntadores; esto da gran flexibilidad y se
hace muy rápidamente ya que al ordenar solo se mueven apuntadores y no datos.
3) El paso de valores por referencia para funciones utiliza apuntadores.
4) Las funciones que reciben arreglos no son paramétricas a partir de la segunda dimensión, los apuntadores si lo
son.
Desventajas 1) La notación es un poco más compleja y no se recomienda usarla para manipular datos simples.

Ejemplo:
// 06_02.c : Dado un puntero float *f1. Verificar que al sumar n a f1 (f2 = f1 = n); la dirección aumenta a:
n*sizeof(float) bytes*/
#include<stdio.h>
void main(void){
float f, *f1 = &f, *f2;
int n = 2;
printf("\nPrimera dirección apuntada: %p (f1)\n", f1);
f2 = f1 + n;
printf("Segunda dirección apuntada: %p (f2 = f1 + n; n = 2)\n", f2);
printf(" --------------\n");
printf("Aumento de dirección : %lu bytes\n", (long int)f2 - (long int)f1);
printf("n*sizeof(float) = %d * %lu : %lu bytes\n", n, sizeof(float), n*sizeof(float));
printf("f2 - f1 : %lu posiciones\n", f2 -f1);
}
Salida:
Primera dirección apuntada : 0x7fff329c5ee0 (f1)
Segunda dirección apuntada: 0x7fff329c5ee8 (f1 + 2)
–-------------------
Aumento de dirección : 8 bytes
n*sizeof(float) = 2 * 4 : 8 bytes
f2 – f1 : 2 posiciones

Arreglo de dos dimensiones


Podemos visualizar con facilidad una matriz, basta representarla en un papel (dos dimensiones):
2 4 6 8
10 12 14 16
La memoria del computador es de una sola dimensión, no queda otra alternativa que adaptar las dos dimensiones a una:
int arr[2][4] = {2, 4, 6, 8, 10, 12, 14, 16};

Matriz Línea
j→
i 2 4 6 8 2 4 6 8 10 12 14 16
10 12 14 16
Cada fila i de la matriz aporta 4 datos a la línea + j que aporta j datos: la posición [i][j] ocupa la posición i*4+j en la línea.
En general, para un arreglo arr[m][n], el elemento arr[i][j] ocupa la posición líneal: i*n+j en la RAM.

PÁGINA: 7
Lenguaje C
Los arreglos de dos dimensiones presentan limitaciones:
1. El lenguaje C los concibe como un arreglo de arreglos

arr[0]: fila 0 // arr[0][0] → 2 arr[0][1] → 4 arr[0][2] → 6 arr[0][3] → 8


arr[1]: fila 1 // arr[1][0] → 10 arr[1][1] → 12 arr[1][2] → 14 arr[1][3] → 16
2. Esta solución no es unidimensional y presentará inconvenientes más adelante.
Se debe estimar en tiempo de programación el número de filas y columnas por exceso, lo cual es un gran desperdicio de
memoria; por otra parte si en tiempo de ejecución nos excedemos de los límites, no hay advertencia de ello, y nos esperan
errores y/o el aborto del programa.
3. Al pasar arreglos a funciones se pierde la parametrización a partir de la segunda dimensión.
4. Un arreglo de más de una dimensión ya no se comporta como un apuntador, como lo hace el de una sola dimensión; pero un
apuntador si puede apuntar a un arreglo de dos dimensiones.

Estos problemas se resuelven en modo óptimo trabajando con punteros, asignando memoria dinámicamente y controlando el número de
elementos en cada dimensión; lo cual haremos en dos capítulos más adelante; por el momento hagamos una aproximación trabajando
con arreglos y apuntadores:

Apuntador a arreglos de dos dimensiones


int arr[2][3] = {2, 4, 6, 8, 10, 12}, *ptr = arr; // no se requiere &. ptr y arr apuntan a la misma dirección.

Posiciones: 0 1 2 3 4 5
ptr arr[0][0] arr[0][1] arr[0][2] arr[1][0] arr[1][1] arr[1][2]
0x7234… 2 4 6 8 10 12
0x7234...
arr[i][j] es equivalente a *(ptr+ i*3+j) y a ptr[i*3+j]

En general para: int arr[m][n];


arr[i][j] ocupa la posición lineal: i*n+j.
arr[i][j] es equivalente a *(ptr+i*n+j) y p[ptr+i*n+j]

La notación de apuntadores permite parametrizar más allá de la primera dimensión.

Paso de argumentos a función


Paso de puntero a variable
Ejemplo: Calcular la mitad de un número:
int n = 2, *pn= &n;
pn n
0x7ff...2 2
0x7ff...0 0x7ff...2

// 06_04a.c: Paso de argumento


Por valor Por referencia Puntero a variable
#include <stdio.h> #include <stdio.h> #include <stdio.h>
void mitad(int n); // prototipo void mitad(int *n); // prototipo void mitad(int *pn); // prototipo
void main(void){ void main(void){ void main(void){
int n = 2; int n = 2; int n = 2, *pn = &n;
mitad(n); // llamando mitad(&n); // llamando mitad(pn); // llamando
printf("n = %d\n", n); printf("n = %d\n", n); printf("n = %d\n", n);
} } }

void mitad(int n){ // entrada valor 2 void mitad(int *n){ // entrada valor 0x7ff...2
n /=2; *n /=2;
} }
Salida: n = 2 Salida: n = 1

PÁGINA: 8
Lenguaje C
Paso de puntero a arreglo
Ejemplo: Calcular la mitad de los elementos de un arreglo:
int arr[4] = {2, 4, 6, 8}, *parr= arr;
parr arr
0x7ff...2 2 4 6 8
0x7ff...0 0x7ff...2

// 06_04b.c: Calcular la mitad de los elementos de un arreglo


Usando punteros Sin usar punteros
1 dimensión 2 dimensiones
#include <stdio.h> #include <stdio.h> #include <stdio.h>
void mitad(int *parr, int n); void mitad(int *parr, int m, int n); void mitad(int parr[][2], int m, int n);
void main(void){ void main(void){ void main(void){
int arr[4] = {2, 4, 6, 8}, i; int arr[2][2] = {2,4,6,8}, *parr = &arr[0][0], i, j; int arr[2][2] = {2, 4, 6, 8}, i, j;
mitad(arr, 4); mitad(parr, 2, 2); mitad(arr, 2, 2);
for(i=0; i < 4; i++) for(i=0; i < 2; i++) for(i=0; i < 2; i++)
printf("%d\t", arr[i]); for(j=0; j < 2; j++) printf("%d\t", arr[i][j]); for(j=0; j < 2; j++) printf("%d\t", arr[i][j]);
printf("\n"); printf("\n"); printf("\n");
} } }
void mitad(int *parr, int n){ void mitad(int *parr, int m, int n){ void mitad(int arr[][2], int m, int n){
// entran los valores 0x7ff...2, y 4 // entran los valores 0x7ff...2, 2 y 2 // entran los valores 0x7ff...2, 2 y 2
int *p = parr, *pmax = p + n; int *p = parr, *pmax = p + m*n; int i, j;
for(i=0; i < m; i++)
for(; p < pmax; p++) *p /=2; for(; p < pmax; p++) *p /=2; for(j=0; j < n; j++) arr[i][j] /=2;
} } }
Salida: 1 2 3 4 Salida: 1 2 3 4 Salida: 1 2 3 4
Nota: 1 Nota: 2 Nota: 3
Notas:
1: El programa es paramétrico en una dimensión.
2: El programa es paramétrico en las dos dimensiones, note que *parr = &arr[0][0], también se puede hacer *parr = arr; pero compila
con warning (advertencia), debido a que un apuntador de tipo int (lineal) apunta a una matriz (arreglo de arreglos) de tipo int; pero
ejecuta bien. Otra forma es usar solo punteros y memoria dinámica y lo estudiaremos dos capítulos más adelante.
3: El programa es paramétrico solo en la primera dimensión, más no en la segunda.

Ejercicio: Definir un arreglo de dos dimensiones, calcular el mínimo, la suma y la media

Veamos dos temas que usaremos más adelante.

Arreglo de apuntadores
Un puntero es una variable como cualquier otra, por lo tanto podemos tener arreglos de punteros, ejemplo:
int *ptr[4], n = 6;
ptr[0] = ptr[1] = &n;
*ptr[1]; // = 6

ptr n
0xff..c 0xff..c 6
Posiciones 0 1 2 3 0xff..c

Ejemplo de uso específico: Dividir un área en diferentes particiones


// 06_03.c Dividir un área en diferentes particiones
#include<stdio.h>
void main(void){
int i, j, k, a[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12};
// área vista como arreglo de 1 dimensión: 12 elementos
int *p1 = &a[0];
for(i=0; i<12; i++) printf("%d ", p1[i]);
printf(" : 1 dimensión de 12 elementos\n");

// área vista como arreglo de 2 dimensiones: 2 x 6

PÁGINA: 9
Lenguaje C
int *p2[2]; // arreglo de punteros
p2[0] = &a[0], p2[1] = &a[6];
for(i=0; i<2; i++)
for(j=0; j<6; j++) printf("%d ", p2[i][j]);
printf(" : 2 dimensiones 2 x 6\n");

// área vista como arreglo de 3 dimensiones: 2 x 3 x 2


int *p3[2][3];
p3[0][0] = &a[0], p3[0][1] = &a[2], p3[0][2] = &a[4];
p3[1][0] = &a[6], p3[1][1] = &a[8], p3[1][2] = &a[10];
for(i=0; i<2; i++)
for(j=0; j<3; j++)
for(k=0; k<2; k++) printf("%d ", p3[i][j][k]);
printf(" : 3 dimensiones 2 x 3 x 2\n");
}
Salida:
1 2 3 4 5 6 7 8 9 10 11 12 : 1 dimensión de 12 elementos
1 2 3 4 5 6 7 8 9 10 11 12 : 2 dimensiones de 2 x 6
1 2 3 4 5 6 7 8 9 10 11 12 : 3 dimensiones 2 x 3 x 2

Apuntador a apuntador
Un puntero es una variable como cualquier otra, por lo tanto podemos definir un puntero a puntero:

Sintaxis:
tipo **nombreApuntador;
ejemplo:
int a = 3, *p1 = &a, **p2 = &p1;

p2 p1 a
0x7234.. 0x8174.. 3
0x65734.. 0x7234.. 0x8174..

printf("%d\n", a); // resultado: 3


printf("%d\n", *p1); // resultado: 3
printf("%d\n", **p2); // resultado: 3
a = 4; // es equivalente a: *p1 = 4; **p2 = 4;

Atento: el operador * tiene diferentes usos:


Uso
* Para indicar puntero: *p1
** Para indicar puntero a puntero: int **p2
* Operador de multiplicación
Expresión 2 ***p2; // es válida y el compilador la reconoce, pero ud. se puede confundir, no abuse, use paréntesis para separar.

Ejemplo: Definir, asignar valores y representar la memoria física:


int arr[4] = {15, 13, 11, 9}, *p1 = arr, **p2 = &p1;

La implantación en memoria sería:


p2 p1 arr
0x7234.. 0x8174.. 15 13 11 9
0x65734.. 0x7234.. 0x8174..

printf("%d\n", *arr); // resultado: 15


printf("%d\n", *p1); // resultado: 15
printf("%d\n", **p2); // resultado: 15

Ejemplo: Sea el arreglo arr[5] = {17, 16, 18, 10, 19}, podemos ordenarlo utilizando el método del mínimo; acá vamos a copiar este
método y modificarlo para ordenar las filas de una matriz arr[5][2] por la primera columna, lo haremos de dos modos
• Utilizando arreglos: Se mueven las filas de la matriz para ordenarla
• Utilizando apuntadores en 3 pasos:
1) Se crea un arreglo de apuntadores *p[ ] a las filas de la matriz;
PÁGINA: 10
Lenguaje C
2) Se ordena ascendentement a p, no las filas de la matriz;
3) Se accede a los valores de la matriz a travez de los apuntadores: *p[i]

Paso 1 paso 3
Apuntadores Matriz Apuntadores Matriz
parr arr parr arr
parr[0] 17 3 parr[0] 17 3
parr[1] 16 6 parr[1] 16 6
parr[2] 18 6 parr[2] 18 6
parr[3] 10 1 parr[3] 10 1
parr[4] 19 2 parr[4] 19 2

Recorrido de arreglos:
parr arr
parr[0] 17 3
parr[1] 16 6
parr[2] pi 18 6
parr[3] 10 1
parr[4] 19 2
pj

Ordenar ascendentemente una matriz por la primera columna utilizando el método del mínimo
// 06_05a.c: utilizando arreglos // 06_04b.c: utilizando apuntadores
#include<stdio.h> #include<stdio.h>
void ordenar(int arr[][2], int n, int m); void ordenar(int *parr[], int n, int m);
void reportar(int arr[][2], int n, int m); void reportar(int *parr[], int n, int m);
void main(void){ void main(void){
int n = 5, m = 2, arr[5][2] = {17, 3, 16, 6, 18, 6, 10, int n = 5, m = 2, arr[5][2] = {17, 3, 16, 6, 18, 6, 10, 1, 19, 2};
1, 19, 2}; int i, *parr[n];
for(i=0; i<n; i++) parr[i] = arr[i];
printf("Arreglo original:\n"); printf("Arreglo original:\n");
reportar(arr, n, m); reportar(parr, n, m);
ordenar(arr, n, m); ordenar (parr, n, m);
printf("\nArreglo ordenado:\n"); printf("\nArreglo ordenado:\n");
reportar(arr, n, m); reportar(parr, n, m);
} }
void reportar(int arr[ ][2], int n, int m) { // restricción void reportar(int *parr[ ], int n, int m) {
int i, j; int **pi = parr, **pimax = pi+n, *pj, *pjmax;
for(i=0; i<n; i++) { for(; pi<pimax; pi++) {
for(j=0; j<m; j++) printf("%d\t", arr[i][j]); for(pj=*pi, pjmax= pj+m; pj<pjmax; pj++) printf("%d\t", *pj);
printf("\n"); printf("\n");
} }
} }
void ordenar(int arr[][2], int n, int m) { // restricción void ordenar(int *parr[], int n, int m) {
int i, j, imin, amin; int **pi = parr, **pimax = pi+n, **pj, **pjmax, **imin, amin, *temp;
for(i=0; i<n-1; i++){ for(; pi<pimax-1; pi++) {
imin = i; imin = pi;
amin = arr[i][0]; amin = **pi;
for(j=i+1; j<n; j++) for(pj = pi+1; pj<pimax; pj++)
if (arr[imin][0] > arr[j][0]){ if (amin > **pj){
amin = arr[j][0]; amin = **pj;
imin = j; imin = pj;
} }
if(imin>i){ if(imin>pi){
arr[imin][0] = arr[i][0]; temp = *imin;
arr[i][0] = amin; *imin = *pi;
amin = arr[imin][1]; *pi = temp;
PÁGINA: 11
Lenguaje C
arr[imin][1] = arr[i][1];
arr[i][1] = amin;
} }
} }
} }
Salida:
Arreglo original
17 3
16 6
18 6
10 1
19 2

Arreglo ordenado:
10 1
16 6
17 3
18 6
19 2
Note: la restricción en la segunda dimensión No hay restricción en la segunda dimensión

Reglas de precedencia de los modificadores *, ( ) y [ ]


Ver apéndice 1.
Un nombre (identificador), ejemplo nn, se puede modificar de varios modos:
*nn Indica el apuntador nn
**nn Indica el apuntador a apuntador nn
nn() Indica la función nn
nn[ ] indica el arreglo[ ]

Se puede utilizar más de un modificador al mismo tiempo, por ejemplo en la declaración:


int *nn[2][3];

Reglas de precedencia de los modificadores


1) Cercanía del modificador (derecha o izquierda) al identificador
2) ( ) y [ ] tienen mayor prioridad que *
3) Use paréntesis para dar mayor prioridad, como en: (3 + 4) * 2

Ejemplos:
int *p[2];
* y [2] son adyacentes a p, [2] tiene mayor prioridad que * (regla 2)
→ arreglo de 2 punteros a enteros.

int (*p)[2];
(*p) Es un puntero
(*p)[2] Es un puntero a un arreglo de 2 elementos de tipo entero

PÁGINA: 12
Lenguaje C

// 06_06.c: 4 formas, entre otras, para acceder a los datos de una matriz utilizando punteros.
// Aplicación de las precedencias de *, ( ) y [ ]
#include<stdio.h>
void main(void){
int i, j, a[3][2] = {1, 2, 3, 4, 5, 6};
int *p1[6]; int (*p2)[2]; int *p3; int *p3, **p4;
// arreglo de 6 punteros // puntero a arreglo de 2 elementos // puntero a fila // puntero a puntero entero
printf("Arreglo de punteros\n"); printf("\nPuntero a arreglo\n"); printf("\nPuntero a entero\n"); printf("\nPuntero a puntero\n");
p2 = a; // apunta a matriz
for(i=0; i<3; i++) for(i=0; i<3; i++) for(i=0; i<3; i++) { for(i=0; i<3; i++) {
p3 = a[i]; // apunta a fila p3 = a[i];
p4 = &p3; // Apunta a p3
for(j=0; j<2; j++) { for(j=0; j<2; j++) for(j=0; j<2; j++) for(j=0; j<2; j++)
p1[i*2+j] = &a[i][j];
printf("%d\n", *p1[i*2+j]); printf("%d\n", p2[i][j]); printf("%d\n", p3[j]); printf("%d\n", (*p4)[j]);
} } }
} } } }
Salida: 1 2 3 4 5 6

Estas reglas se aplican también para funciones:


int *miFun(); // miFun() retorna un puntero a int
int miFun(int *ptr); // El parámetro de miFun() es de tipo puntero a int

Apuntador a void
La palabra reservada void (se pronuncia void en inglés y significa vacío, nulo, vacante, nada, ...) se utiliza para crear un puntero que
apuntará a un tipo de dato genérico, para usarlo se requiere hacer casting al apuntador, ejemplo para int: pInt = (int *)pVoid;
int in=1;
double db=2.4;
void *pvoid; // un apuntador a void puede apuntar a cualquier tipo de dato:

pvoid = &in; int *pin = (int *)pvoid; printf("%d\n", *pin); // = 1


pvoid = &db; double *pdb = (double *)pvoid; printf("%lf\n", *pdb); // = 2.4

pin pdb pvoid in db


1 2.4

Ejemplo: La función free( ), para liberar memoria dinámica apuntada por un puntero p de cualquier tipo, tiene la sintaxis:
void free(void *ptr);
Se puede usar:
free(p); // p es un apuntador a cualquier tipo de dato, free() libera la memoria y asigna p = NULL.

¡Apuntaste bien!!!!

Origen: https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQFyz_WY0xkicaiWFizB_rnTM9D-9X8wB9QhPv5jS2h4mQGdPMC6g

Si fallas en punteros, cometerás el famoso error: “violación de segmento", que es difícil de corregir, ya que no se sabe donde ocurrió, te
parecerá que la máquina se volvió loca; para identificar donde perdiste la puntería puedes ayudarte escribiento trazas, ejemplo:
printf(“Estoy en AA %d\n", n); // valor de n en la línea AA
printf(“Estoy en BB %d\n", n); // valor de n en la línea BB

Usando variables de tipo puntero -muy importantes, potentes y peligrosas- pasas del manejo estático (en tiempo de programación) de la
RAM, al manejo dinámico (en tiempo de ejecución). Toda la teoría está bien organizada y completa; para la práctica te recomiendo: 1)
Enamórate del tema y cuéntale a tu novi@ para que no se ponga celos@, 2) Haz una sesión personal especial y trata de entender todos
PÁGINA: 13
Lenguaje C
los detalles, y 3) Reúnete con los amigos para tirar flechas, perdón, para resolver problemas de apuntadores. Ya casi eres un profesio -
nal.

Ejercicios:
1) Para un arreglo tridimensional, por ejemplo:
int a[2][3][2] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} ;
¿Que posición lineal ocupará a[i][j][k]?

2) Una matriz p[4][12] representa una producción de los 12 meses en 4 años. Lea la matriz y use punteros para calcular el promedio de
producción en cada año y en los 12 meses:
Años
1 2 3 4
Promedio: xx xx xx xx

Meses
1 2 3 4 … 12
Promedio: xx xx xx xx xx

3) Visite la página: http://www.cimat.mx/~alram/cpa/pointersC.pdf

PÁGINA: 14

Você também pode gostar