Semana 2¶
Esta semana continuaremos trabajando en la Unidad 1 del curso. Será una semana muy movida porque abordaremos uno de los conceptos más poderosos de C: los punteros. Adicionalmente, aprenderás sobre arreglos, memoria dinámica, estructuras de datos y archivos.
Trayecto de acciones, tiempos y formas de trabajo¶
Fase 6 (RETO)¶
- Fecha: julio 13 y julio 17 de 2020 - 4 p.m.
- Descripción: ejercicios para el reto.
- Recursos: realiza estos Ejercicios.
- Duración de la actividad:
- Dos sesiones de 1 hora 40 minutos cada una para solución de dudas en tiempo real.
- 5 horas de trabajo autónomo
- Forma de trabajo: individual.
Ejercicios¶
Primero te voy a proponer que hagas dos guía para que trabajes los conceptos básicos y luego una serie de ejercicios que te permitirán practicar varias de las cosas que has hecho hasta ahora.
Guía 1: punteros, arreglos y memoria dinámica¶
Realiza esta guía sobre punteros, arreglos y memoria dinámica.
Nota
¡Alerta de Spoiler!
En este enlace se encuentra la solución a la guía de punteros, arreglos y memoria dinámica.
Ejercicio 1: entrada/salida¶
En la guía introductoria del lenguaje C se discutió la
función scanf para realizar operaciones de entrada en
C. Al realizar el ejercicios final, la calculadora,
¿Notaste algún comportamiento extraño del
programa al leer caracteres? Específicamente scanf("%c",&var).
Ten presente que al introducir texto en la terminal, además de los caracteres visibles, se introduce un ENTER. Así, por ejemplo, al introducir el número 325 y luego presionar ENTER, se están ingresando 4 bytes: 0x33 0x32 0x35 0x0A. los tres primeros bytes corresponden a los códigos ASCII de cada dígito del número 325 y el 0x0A corresponde al código ASCII del ENTER o nueva línea (NEW LINE).
Considere el siguiente código:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <stdio.h>
int main()
{
int num;
char key;
printf("Prueba a scanf. Ingrese el numero 325 y presione ENTER:\n");
scanf("%d",&num);
printf("Ingrese cualquier tecla para terminar y presione ENTER:\n");
scanf("%c",&key);
return 0;
}
|
Ejecuta el código anterior. ¿Cuál es el resultado? ¿Por qué?
El primer scanf (scanf("%d",&num);) buscará en el flujo de entrada una
secuencia de bytes que comience con un carácter numérico y parará de leer
una vez detecte un carácter no numérico, el cual, dejará intacto en el flujo
de entrada. En este caso, scanf("%d",&num); sacará del flujo
los bytes 0x33 0x32 0x35, correspondientes a '3' '2' '5',
y dejará en el flujo el byte 0x0A (correspondiente al ENTER). Luego
convertirá la cadena de 3 bytes en ASCII al número que representan, es decir,
al 325 que en base 16 sería 0x0145 (comprueba esto con la calculadora del
sistema operativo)
El segundo scanf scanf("%c",&key); leerá un carácter del flujo de entrada.
En este caso dicho carácter está disponible y corresponde al ENTER dejado
por el scanf anterior.
¿Cómo solucionar este problema? Una posible solución será (aunque hay otras más):
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <stdio.h>
int main()
{
int num;
char key;
printf("Prueba a scanf. Ingrese el numero 325 y presione ENTER:\n");
scanf("%d",&num);
scanf("%c",&key); // Saco del flujo el ENTER
printf("Ingrese cualquier tecla para terminar y presione ENTER:\n");
scanf("%c",&key);
return 0;
}
|
Ejercicio 2: entrada/salida¶
Para complementar el ejercicio anterior, se propone analizar otros ejemplos (Tomados de este enlace).
1 2 3 4 5 6 7 8 9 | #include <stdio.h>
int main(void)
{
int a = 10;
printf("enter a number: ");
scanf("%d", &a);
printf("You entered %d.\n", a);
}
|
Ingresa un número y ENTER. ¿Qué ocurre? Ahora ingresa una palabra y ENTER. ¿Qué ocurre? ¿Por qué?
Ejercicio 3: scanf return¶
scanf devuelve la cantidad de conversiones realizadas. Analiza este ejemplo (ingresa CRTL+C si algo sale mal):
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <stdio.h>
int main(void)
{
int a;
printf("enter a number: ");
while (scanf("%d", &a) != 1)
{
// input was not a number, ask again:
printf("enter a number: ");
}
printf("You entered %d.\n", a);
}
|
¿Por qué funciona así el programa? Recuerda el ejercicio 1.
Ejercicio 4: cadenas¶
Compila el código que se muestra a continuación así:
gcc -Wall -fno-stack-protector tmp.c -o tmp
Ejecuta el programa con estos vectores de prueba cuando se pregunte por el nombre:
- juan
- juan-fernan
- juan-fernando-franco
1 2 3 4 5 6 7 8 9 | #include <stdio.h>
int main(void)
{
char name[12];
printf("What's your name? ");
scanf("%s", name);
printf("Hello %s!\n", name);
}
|
Explique cómo funciona el programa en cada caso.
Ejercicio 5¶
Repite el ejercicio anterior pero esta vez compilando
sin -fno-stack-protector.
Ejercicio 6¶
Finalmente repita el ejercicio anterior, pero esta vez
usando el siguiente código y compilando sin -fno-stack-protector
1 2 3 4 5 6 7 8 9 | #include <stdio.h>
int main(void)
{
char name[40];
printf("What's your name? ");
scanf("%39s", name);
printf("Hello %s!\n", name);
}
|
Explica por qué en scanf especificamos un 39 sabiendo que name puede almacenar 40 caracteres. Recuerda, de la primera guía, que todas las cadenas en C deben terminar con un 0.
Ejercicio 7¶
Usando el código anterior ingresa: juan fernado franco. ¿Cuál es el resultado?
Ejercicio 8¶
Escribe el siguiente código:
1 2 3 4 5 6 7 8 9 | #include <stdio.h>
int main(void)
{
char name[40];
printf("What's your name? ");
scanf("%39[^\n]", name);
printf("Hello %s!\n", name);
}
|
Nota la línea:scanf("%39[^\n]", name);. En este caso le estamos diciendo a
scanf que lea hasta 39 caracteres y hasta que encuentre un ENTER (\n). También
es posible indicarle a scanf que lea mientras que los caracteres estén en una
lista, por ejemplo: scanf("%39[a-z]", name);.
Ejercicio 9¶
¿Entonces qué usamos para leer la entrada?
Ahora que conocemos mejor los punteros y los arreglos podemos explorar la
función fgets: char *fgets(char *str, int n, FILE *stream). A esta
función le debemos pasar la dirección del buffer donde queremos colocar
los caracteres, la cantidad de caracteres y el flujo. fgets termina de leer
el flujo cuando encuentre un ENTER. Dicho ENTER se saca del flujo
Analiza el funcionamiento de fgets:
1 2 3 4 5 6 7 8 9 10 11 | #include <stdio.h>
int main(void)
{
char name[40];
printf("What's your name? ");
if (fgets(name, 40, stdin))
{
printf("Hello %s!\n", name);
}
}
|
NOTA que en name quedará también el ENTER. Entonces para eliminarlo simplemente hacemos:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <stdio.h>
#include <string.h>
int main(void)
{
char name[40];
printf("What's your name? ");
if (fgets(name, 40, stdin))
{
name[strcspn(name, "\n")] = 0;
printf("Hello %s!\n", name);
}
}
|
strcspn buscará en la cadena name el primer match con
\n y devolverá la posición en name en la cual fue encontrado
el match.
Ejercicio 10¶
(Este ejercicio es tomado de aquí)
Relación arreglos y punteros
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include<stdio.h>
int main()
{
int *p;
int (*ptr)[5];
int arr[5];
p = arr;
ptr = &arr;
printf("p = %p, ptr = %p\n", p, ptr);
p++;
ptr++;
printf("p = %p, ptr = %p\n", p, ptr);
return 0;
}
|
Ejecuta el programa anterior. El resultados es:
1 2 | p = 0x7fff4f32fd50, ptr = 0x7fff4f32fd50
p = 0x7fff4f32fd54, ptr = 0x7fff4f32fd64
|
En la expresión int * p; p es una variable de tipo
int *. En este tipo de variables se almacenan las
direcciones de variables de tipo int. Por tanto, *p
(sin colocar int antes del *) es de tipo int porque
p es de tipo int *.
En la expresión int (*ptr)[5]; ptr es una variable de tipo
int (*)[5]. En este tipo de variables se almacenan direcciones
de variables de tipo int [5], es decir, variables de tipo
arreglo de cinco posiciones. Por tanto, *ptr es de tipo
int [5] porque ptr es de tipo int (*)[5].
En la expresión p = arr; arr es el nombre del arreglo y un puntero
al primer elemento del arreglo.
En este caso arr es de tipo int * porque el primer elemento
del arreglo es de tipo int. Por tanto, *arr
será tipo int.
En la expresión ptr = &arr; &arr es la dirección del arreglo.
&arr es tipo int (*)[5].
La expresión printf("p = %p, ptr = %p\n", p, ptr); imprime el
contenido de p y ptr. Según el resultado
(p = 0x7fff4f32fd50, ptr = 0x7fff4f32fd50`), la dirección del
arreglo y del primer elemento del arreglo es la misma; sin embargo,
como p es tipo int *, la expresión p++ hará que p apunte
(almacene la dirección) al siguiente entero. En cambio, en la
expresión ptr++; ptr apuntará al siguiente arreglo de 5
enteros (5 enteros ocupan 20 bytes en memoria considerando
que cada entero ocupa 4 bytes), ya que ptr es de tipo
int (*)[5].
Ejercicio 11: análisis de una expresión más compleja¶
El siguiente ejercicio es más complejo que el anterior, sin embargo, se analiza de igual manera. Considera el siguiente código:
1 2 3 4 5 6 7 8 9 10 | #include <stdio.h>
int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
int main(void) {
int (*p)[3][4] = &arr;
printf("%d\n", ( (*p)[2] )[3] );
printf("%d\n", *( *(*p + 2) + 3 ) );
return 0;
}
|
arr es un arreglo de arreglos, es decir, es una arreglo de 3 arreglos
de 4 enteros cada uno.
arr es el nombre del arreglo de arreglos y un puntero al primer elemento
del arreglo. Por tanto, arr es de tipo int (*)[4] ya que el primer elemento
de arr es un arreglo de tipo int [4].
p es un puntero que almacena la dirección de un arreglo de arreglos.
Por tanto, p es de tipo int (*)[3][4].
Si p es de tipo int (*)[3][4] entonces *p será de tipo int [3][4] o
int (*)[4] (un puntero al primer elemento del arreglo de arreglos).
El operador [] en la expresión (*p)[2] es equivalente a *( *p + 2).
Como el tipo de (*p + 2) es int (*)[4] el tipo de *( *p + 2)
será int [4]. la expresión (*p)[2] accede al tercer elemento de arr, es
decir, a {9,10,11,12} que es de tipo int [4].
Por último, como (*p)[2] es tipo int [4], entonces ( (*p)[2] )[3] ) es
tipo int y corresponderá al cuarto elemento del tercer arreglo de arr.
Nota que ( (*p)[2] )[3] ) es equivalente a *( (*p)[2] + 3) que a su
vez es equivalente a *( * ( *p + 2)+ 3)
El programa imprimirá el número 12.
La expresión printf("%d\n", *( * ( *p + 2)+ 3)); al ser equivalente a
printf("%d\n", ( (*p)[2] )[3] ); también mostrará un 12.