Apuntes · Capítulo 2Fundamentos

Variables y programas secuenciales

Variables y programas secuenciales

La semana pasada escribieron algoritmos en pseudocódigo, los siguieron a mano y compilaron su primer programa en C. Ese programa siempre escribía lo mismo. También siguieron a mano el algoritmo del vuelto para S/ 7.80: para otro monto, había que volver a hacer todo el trabajo.

Esta semana los programas empiezan a trabajar con datos. Van a guardar valores en variables, a calcular con ellos y a leer datos del teclado. Con eso, el mismo programa sirve para cualquier monto. Al final del capítulo resolvemos el vuelto para cualquier monto en doce líneas de cálculo.

Todos los programas de hoy son secuenciales: ejecutan sus instrucciones una tras otra, de arriba hacia abajo, cada una exactamente una vez. Todavía no hay decisiones (semana 4) ni repeticiones (semana 5). Aun así, con fórmulas ya se puede hacer mucho.

Lectura. Este capítulo es el texto de la semana. Su código está en code/week-02. Dentro del entorno del curso, en esa carpeta, make compila todos los ejemplos en la carpeta build/ y make test los prueba.

Ruta de lectura.

  • Antes de clase (lectura obligatoria, 15 a 20 minutos): la introducción, “Cómo seguir este capítulo” y “Variables”. Es lo mínimo para seguir la clase.
  • En clase: “Tipos de datos”, “Operadores y expresiones”, “Conversión entre tipos”, “Escribir con printf”, “Leer con scanf”, “De la fórmula al programa” y “Funciones de math.h”.
  • Después de clase: para el laboratorio, “El laboratorio, paso a paso”; para repasar, “Resumen” y “Ejercicios”. Opcional: “La entrada y la salida pueden ser archivos”.

Cómo seguir este capítulo

Cada ejemplo es un programa de code/week-02, aunque a veces mostramos solo sus líneas importantes. Pueden compilarlo como la semana pasada, con gcc -Wall -Wextra -std=c17, o con make, un programa que lee el archivo Makefile y ejecuta por ustedes los gcc que hacen falta. Con make, los ejecutables quedan en build/, y por eso aquí los verán como build/balance y no como ./balance. También se compilan con -g -fsanitize=address,undefined: con -fsanitize, el programa revisa algunos errores mientras corre, y con -g, los mensajes indican el archivo y la línea.

Toda salida que aparece aquí salió de una ejecución real en el entorno del curso. El texto exacto de los mensajes de gcc puede cambiar un poco con la versión del compilador.

Variables

Un nombre para un lugar en memoria

Mientras un programa corre, sus datos están en la memoria de la computadora. La memoria es una fila enorme de bytes, y cada dato ocupa algunos de ellos. Una variable (variable) es un nombre para un lugar en memoria donde se guarda un valor.

Imaginen una caja con una etiqueta. La etiqueta es el nombre. Lo que hay dentro es el valor. Se puede sacar una copia de lo que hay dentro todas las veces que haga falta, y se puede reemplazar por otra cosa.

En pseudocódigo ya usaron esta idea con ←. En C, una variable se declara indicando su tipo y su nombre, y normalmente se le da un valor inicial:

int balance = 10;

Esta línea dice tres cosas: balance es el nombre; int es el tipo, que significa número entero; y 10 es su primer valor. Veamos un programa completo. Es el saldo de una tarjeta de transporte, en soles enteros:

// Una variable cambia de valor: la asignación no es una ecuación.
#include <stdio.h>

int main(void) {
    int balance = 10;           // saldo de la tarjeta, en soles
    printf("Saldo inicial: %d\n", balance);

    balance = balance - 3;      // un pasaje
    printf("Después del pasaje: %d\n", balance);

    balance = balance + 5;      // una recarga
    printf("Después de la recarga: %d\n", balance);

    return 0;
}
$ build/balance
Saldo inicial: 10
Después del pasaje: 7
Después de la recarga: 12

Dentro de printf, el %d marca un hueco: ahí se escribe el valor del int que viene después de la coma. Lo vemos con detalle más adelante.

Sigamos el programa a mano, como aprendieron la semana pasada. Cada fila de la tabla muestra el valor de la variable después de ejecutar la instrucción:

Instrucción balance
int balance = 10; 10
balance = balance - 3; 7
balance = balance + 5; 12

Asignar no es igualar

La línea balance = balance - 3; sería absurda en matemáticas: ningún número es igual a sí mismo menos 3. En C, = no afirma que dos cosas sean iguales. Es una orden que se ejecuta en dos pasos:

  1. Se calcula el valor de la derecha, con los valores actuales de las variables.
  2. Ese resultado se guarda en la variable de la izquierda. Lo que había antes se pierde.
Arriba, la línea balance = balance - 3; con la palabra balance de la izquierda subrayada en azul, como el lugar donde se guarda, y balance - 3 subrayado en rojo, como lo que se calcula. Abajo, una caja llamada balance y a su derecha otra caja con el cálculo 10 - 3 da 7. Una flecha roja va de la caja al cálculo y dice que toma el valor actual, 10. Una flecha azul vuelve del cálculo a la caja y dice que guarda el 7 y que el 10 se pierde. Dentro de la caja, el 10 aparece tachado junto al 7Arriba, la línea balance = balance - 3; con la palabra balance de la izquierda subrayada en azul, como el lugar donde se guarda, y balance - 3 subrayado en rojo, como lo que se calcula. Abajo, una caja llamada balance y a su derecha otra caja con el cálculo 10 - 3 da 7. Una flecha roja va de la caja al cálculo y dice que toma el valor actual, 10. Una flecha azul vuelve del cálculo a la caja y dice que guarda el 7 y que el 10 se pierde. Dentro de la caja, el 10 aparece tachado junto al 7
Primero se calcula la derecha con el valor que la variable tiene en ese momento. Solo después se reemplaza ese valor.

Por eso la forma x = x + 1 es tan común: toma el valor de x, le suma 1 y guarda el resultado otra vez en x. En pseudocódigo se ve mejor que es una orden: x ← x + 1.

A la izquierda de = siempre va una variable. balance - 3 = balance; no tiene sentido y no compila: no hay ningún lugar llamado balance - 3 donde guardar algo.

En C. = asigna. Para preguntar si dos valores son iguales se usa ==, y lo veremos con las decisiones de la semana 4. En pseudocódigo, en cambio, ← asigna y = compara.

Una asignación no deja una fórmula

Una asignación guarda un valor, no la manera de calcularlo. Si han usado una hoja de cálculo, ahí una celda con una fórmula se actualiza sola. En C no pasa eso:

int price = 2;
int total = price * 3;      // total recibe 6, no la fórmula

price = 10;                 // total no se entera de este cambio

Al escribir las dos variables al final (programa completo en not_a_formula.c):

$ build/not_a_formula
price = 10
total = 6

Cuando se ejecutó la segunda línea, price valía 2 y total recibió 6. Lo que pase después con price no le afecta. Si quieren que total cambie, hay que volver a ejecutar una asignación.

Idea clave. Un programa secuencial es una película, no una foto. Cada instrucción usa los valores que las variables tienen en ese momento. Si cambian el orden de dos líneas, pueden cambiar el resultado.

El orden importa: intercambiar dos valores

Queremos intercambiar los valores de dos variables. Lo primero que se le ocurre a casi todo el mundo es esto:

left = right;
right = left;

Sigámoslo a mano, con left en 4 y right en 9:

Instrucción left right
(al inicio) 4 9
left = right; 9 9
right = left; 9 9

La primera asignación destruyó el 4. Cuando la segunda lo necesita, ya no existe. Hace falta guardarlo antes en otra variable, igual que para intercambiar el contenido de dos vasos se necesita un tercer vaso. Primero en pseudocódigo:

algoritmo intercambiar()
    left ← 4
    right ← 9
    saved ← left
    left ← right
    right ← saved
    escribir left, right
fin algoritmo

Y después en C (programa completo en swap_values.c):

int saved = left;           // guardamos left antes de perderlo
left = right;
right = saved;
$ build/swap_values
left = 9, right = 4
Instrucción left right saved
(al inicio) 4 9
int saved = left; 4 9 4
left = right; 9 9 4
right = saved; 9 4 4
Animación con dos partes. A la izquierda, sin tercera variable, las cajas left y right empiezan en 4 y 9. Primero, left = right copia el 9 en left y el 4 ya no está en ninguna parte. Después, right = left copia otro 9 y las dos quedan en 9. A la derecha, con una tercera variable, las cajas left, right y saved. Primero, int saved = left guarda una copia del 4 en saved. Después, left = right copia el 9 en left. Al final, right = saved copia el 4 en right, y quedan left en 9, right en 4 y saved en 4Animación con dos partes. A la izquierda, sin tercera variable, las cajas left y right empiezan en 4 y 9. Primero, left = right copia el 9 en left y el 4 ya no está en ninguna parte. Después, right = left copia otro 9 y las dos quedan en 9. A la derecha, con una tercera variable, las cajas left, right y saved. Primero, int saved = left guarda una copia del 4 en saved. Después, left = right copia el 9 en left. Al final, right = saved copia el 4 en right, y quedan left en 9, right en 4 y saved en 4
Cada asignación copia un valor y borra el que había en el destino. Por eso hace falta guardar el 4 antes de que se pierda.

Nombres de variables

Un nombre en C puede tener letras, dígitos y _, y no puede empezar con un dígito. Mayúsculas y minúsculas son distintas: total y Total son dos variables. Tampoco se pueden usar las palabras reservadas del lenguaje, como int o return.

En este curso los nombres van en inglés y en snake_case: palabras en minúsculas, separadas con _, como final_grade. Eviten las tildes y la ñ en los nombres. Elijan nombres que digan qué guarda la variable: final_grade se explica solo y x2 obliga a leer todo el programa.

Una variable se declara una sola vez, y antes de usarla. Después de declararla se escribe sin el tipo: balance = 7;. Si escriben int balance = 7; por segunda vez, gcc da un error.

Usar una variable sin darle valor

Declarar una variable reserva el lugar, pero en C no lo limpia. Si no le dan un valor inicial, contiene lo que haya quedado en esa parte de la memoria.

int total;

printf("total = %d\n", total);

Con -Wall, gcc lo detecta. (El programa es broken/uninitialized.c. La carpeta broken/ guarda los programas con errores a propósito.)

$ gcc -Wall -Wextra -std=c17 broken/uninitialized.c -o build/uninitialized
broken/uninitialized.c: In function ‘main’:
broken/uninitialized.c:7:5: warning: ‘total’ is used uninitialized [-Wuninitialized]
    7 |     printf("total = %d\n", total);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
broken/uninitialized.c:5:9: note: ‘total’ was declared here
    5 |     int total;
      |         ^~~~~

Es un aviso (warning), no un error: gcc igual crea el ejecutable. Lean la primera línea del aviso: dice el archivo, la línea (7), la columna (5) y el problema. “Is used uninitialized” significa “se usa sin inicializar”.

Error común. Usar una variable sin darle valor. No crean que vale 0. Leer esa variable es comportamiento indefinido (undefined behavior): el lenguaje C no dice qué debe pasar, así que el programa puede escribir cualquier cosa, y puede hacer algo distinto en otra computadora. Lo peor es que a veces parece funcionar. Sin -Wall, gcc no dice nada sobre este programa, y las revisiones de -fsanitize tampoco lo detectan al ejecutarlo. Por eso compilamos siempre con -Wall -Wextra y arreglamos todos los avisos.

La regla práctica: den un valor a cada variable en la misma línea en que la declaran.

Tipos de datos

int, double y char

Todo lo que hay en la memoria son bytes. El tipo de dato (data type) le dice al compilador cuántos bytes usa una variable y cómo interpretarlos. Usaremos tres tipos:

Tipo Guarda Valores de ejemplo Para qué
int números enteros 20, 0, -3 cantidades, notas, años
double números con decimales 3.5, -0.25, 2.0 medidas, promedios
char un carácter 'A', '7', '?' una letra, un símbolo

Fíjense en cómo se escribe cada valor. 7 es un int. 7.0 es un double, aunque su parte decimal sea cero, y esa diferencia va a importar mucho al dividir. '7' es un char, entre comillas simples. "7" es otra cosa, una cadena (string), como las que ya escribieron con printf.

También existe el tipo float, un double con menos precisión. En este curso usamos double.

Cuántos bytes ocupan

El operador sizeof dice cuántos bytes ocupa un tipo o una variable. Su resultado se escribe con %zu en lugar de %d, porque sizeof no da un int sino un tipo propio para tamaños:

printf("int: %zu bytes\n", sizeof(int));

El programa sizes.c hace esto para cada tipo y para una variable age, de tipo int:

$ build/sizes
int: 4 bytes
double: 8 bytes
char: 1 bytes
la variable age: 4 bytes
age vale 19

(La computadora no sabe gramática: 1 bytes lo escribimos nosotros en el formato.)

Estos tamaños son los del entorno del curso, y son los habituales hoy en computadoras personales. char es el único fijado por el lenguaje: ocupa 1 byte siempre. Los otros podrían variar en otra máquina, y por eso, si alguna vez necesitan el tamaño, se lo preguntan a sizeof en lugar de suponerlo.

Tres filas de celdas dibujadas a escala, donde cada celda es un byte. char letter = 'A' ocupa 1 celda, con el valor 65, y dice 1 byte. int age = 19 ocupa 4 celdas, con el valor 19, y dice 4 bytes. double height = 1.685 ocupa 8 celdas, con el valor 1.685, y dice 8 bytesTres filas de celdas dibujadas a escala, donde cada celda es un byte. char letter = 'A' ocupa 1 celda, con el valor 65, y dice 1 byte. int age = 19 ocupa 4 celdas, con el valor 19, y dice 4 bytes. double height = 1.685 ocupa 8 celdas, con el valor 1.685, y dice 8 bytes
Los tamaños de los tres tipos en el entorno del curso. El valor es de la variable completa: no se reparte una cifra por celda.

Un tamaño fijo significa un límite. En 4 bytes no cabe cualquier entero, y en 8 no cabe cualquier número con decimales.

  • int. Tiene un valor máximo y un mínimo. Un resultado que no cabe es un overflow, y en C es un error del programa, no algo que se arregle solo. Los números de este capítulo son lejanos a ese límite. La semana 3 explica de dónde sale y qué pasa al cruzarlo.
  • double. Guarda una aproximación con unas 15 cifras significativas. No guarda exactamente valores como 0.1. La semana 3 explica por qué.

char es un número pequeño

Un char ocupa un byte y guarda un número: el código de un carácter. Para escribirlo como carácter se usa %c, y para ver su código, %d:

char letter = 'A';
char digit = '7';

printf("%c tiene el código %d\n", letter, letter);
printf("La letra siguiente es %c\n", letter + 1);
printf("%c tiene el código %d\n", digit, digit);
printf("El valor numérico de %c es %d\n", digit, digit - '0');

Al ejecutar char_codes.c:

$ build/char_codes
A tiene el código 65
La letra siguiente es B
7 tiene el código 55
El valor numérico de 7 es 7

Vean tres cosas. La letra A se guarda como 65. Como char es un número, se puede calcular con él: letter + 1 da 66, y 66 es el código de B. Y el carácter '7' no es el número 7: su código es 55. Restándole '0' se obtiene el valor del dígito. La tabla de códigos se llama ASCII, y la estudiamos la próxima semana.

Error común. Guardar una ñ o una vocal con tilde en un char. Esos caracteres ocupan más de un byte en los archivos del curso, así que no caben.

char letter = 'ñ';

Esto es lo que dice gcc sobre ese programa, broken/accent_char.c. Mostramos solo el primer aviso. Hay un segundo aviso, cuyo texto cambia según el procesador de la computadora.

$ gcc -Wall -Wextra -std=c17 broken/accent_char.c -o build/accent_char
broken/accent_char.c: In function ‘main’:
broken/accent_char.c:5:19: warning: multi-character character constant [-Wmultichar]
    5 |     char letter = 'ñ';
      |                   ^~~

En una cadena sí pueden ir: printf("Sección: %c\n", section) escribe bien la tilde de Sección. La semana 3 explica cómo se guarda la ñ y por qué ocupa 2 bytes.

Constantes y elección del tipo

Algunos valores no deben cambiar mientras el programa corre, como una tasa de impuesto. Si anteponen const a la declaración, tienen una constante (constant): si el programa intenta asignarle otro valor, gcc da un error. Sus nombres van en mayúsculas. Así usa una constante el programa igv_price.c, que calcula un precio con IGV:

const double IGV_RATE = 0.18;
double price = 0.0;

printf("Precio sin IGV: ");
scanf("%lf", &price);

double igv = price * IGV_RATE;
double total = price + igv;
$ build/igv_price
Precio sin IGV: 50
IGV: 9.00
Total: 59.00

price * IGV_RATE dice más que price * 0.18, y si la tasa cambia, se cambia en un solo lugar. (scanf lee un dato del teclado. Lo vemos en detalle más adelante.)

Para elegir el tipo, piensen en qué valores puede tomar el dato: una cantidad de estudiantes es un int, porque no hay medio estudiante; un promedio o una estatura, un double; la sección del curso, un char. Para montos en soles usamos double por ahora. La semana 3 explica por qué en un sistema real es mejor guardar céntimos en un int.

Operadores y expresiones

Operadores aritméticos

Una expresión es una combinación de valores, variables y operadores que se calcula y da un resultado. Un operador (operator) es el símbolo de una operación:

Operador Significa Ejemplo Resultado
+ suma 17 + 5 22
- resta 17 - 5 12
* multiplicación 17 * 5 85
/ división 17 / 5 3 (entre enteros)
% resto de la división 17 % 5 2

Las dos últimas filas merecen su propia sección. Primero veamos en qué orden se calcula.

Precedencia

Igual que en matemáticas, la multiplicación va antes que la suma. La precedencia dice qué operador se aplica primero:

Nivel Operadores Si hay varios del mismo nivel
1 (primero) ( ) de adentro hacia afuera
2 - de un solo operando (como en -x), cast (double)
3 * / % de izquierda a derecha
4 + - de izquierda a derecha
5 (último) =

La columna de la derecha es tan importante como el orden: % tiene el mismo nivel que * y /, y 20 / 2 * 5 se calcula de izquierda a derecha.

El programa arithmetic.c escribe cada expresión y su resultado:

$ build/arithmetic
10 - 2 * 3 = 4
(10 - 2) * 3 = 24
20 / 2 * 5 = 50
10 - 4 - 3 = 3
7 + 8 % 3 * 2 = 11

Sigamos el último paso a paso. Cada fila resuelve una operación:

Expresión Se calcula ahora
7 + 8 % 3 * 2 8 % 3 da 2 (% y * tienen igual nivel; va primero el de la izquierda)
7 + 2 * 2 2 * 2 da 4
7 + 4 7 + 4 da 11
11

Y 20 / 2 * 5 da 50, no 2: primero 20 / 2 da 10, y luego 10 * 5 da 50. Cuando tengan dudas, usen paréntesis. No cuestan nada y hacen que se lea mejor.

(Dentro del texto de printf, el % se escribe %%. Lo explicamos más adelante.)

División entera

Esta es la sorpresa más grande del capítulo: en C, 7 / 2 vale 3.

La regla es esta: si los dos operandos son enteros, / hace una división entera y el resultado es entero. Los decimales se descartan; no se redondea. Si al menos uno es double, la división es la de siempre: 7.0 / 2 vale 3.5.

La intuición es repartir 7 caramelos entre 2 personas sin partir ninguno: a cada una le tocan 3. Y sobra 1. Ese sobrante lo da el operador %, llamado resto o módulo: 7 % 2 vale 1.

El programa division.c escribe cada expresión y su resultado:

$ build/division
17 / 5 = 3
17 % 5 = 2
17.0 / 5 = 3.400000
17 / 5.0 = 3.400000
-17 / 5 = -3
-17 % 5 = -2
17 % -5 = 2

Tres detalles con precisión:

  • Con negativos, la división entera descarta los decimales, es decir, se acerca a cero. -17 / 5 da -3, no -4.
  • El resto se calcula para que todo cuadre. Siempre se cumple (a / b) * b + a % b == a. Por eso -17 % 5 da -2, ya que (-3) * 5 + (-2) es -17. El resto toma el signo del primer operando, y por eso 17 % -5 da 2.
  • % solo funciona con enteros. Con un double, no compila: gcc da error: invalid operands to binary % (el programa es broken/mod_double.c).

Parece que % sirve para poco, pero es de los operadores más útiles:

Expresión Da Sirve para
n % 2 0 si n es par, 1 si es impar y positivo saber si un número es par
n % 10 el último dígito de un n positivo separar dígitos
n / 10 n sin su último dígito separar dígitos
total % 60 los segundos que sobran al formar minutos convertir tiempos

Dividir entre cero

Dividir un entero entre cero no tiene resultado posible, y en C es otro caso de comportamiento indefinido. Para que el error aparezca al ejecutar, leemos el divisor:

scanf("%d %d", &a, &b);

int quotient = a / b;

Al ejecutar division_by_zero.c con 5 y 0:

$ build/division_by_zero
Dos enteros: 5 0
division_by_zero.c:12:22: runtime error: division by zero

Este mensaje viene de -fsanitize=undefined, y dice dónde está el problema: archivo division_by_zero.c, línea 12, columna 22. Es un error de ejecución, como los que vieron la semana pasada: el programa compiló bien y falló al correr. Lo que pasa después del mensaje depende de la computadora: en unos procesadores el programa se cae y en otros sigue con un resultado sin sentido. Sin -fsanitize no aparece ningún mensaje que lo explique.

Con double es distinto. En el entorno del curso, como en casi toda computadora, el resultado es un valor especial que se escribe inf, de infinito, y el programa sigue (double_by_zero.c escribe Cociente: inf).

Evitar estas divisiones requiere preguntar si el divisor es cero, y eso llega con el if de la semana 4.

Conversión entre tipos

Conversiones automáticas

C convierte tipos por su cuenta en dos situaciones:

  1. En una operación entre int y double, el int se convierte a double antes de operar. Por eso 7.0 / 2 es 7.0 / 2.0 y da 3.5.
  2. En una asignación, el valor se convierte al tipo de la variable. De int a double no se pierde nada en nuestros ejemplos: 2 se vuelve 2.0. De double a int se pierden los decimales: se descartan, no se redondea. Si la parte entera tampoco cabe en el int, es comportamiento indefinido.

La trampa está en mezclar las dos. Miren esta línea:

double half = 1 / 2;

Hay un double a la izquierda, y uno espera 0.5. Pero la asignación es lo último que se ejecuta. Primero se calcula 1 / 2, y ahí los dos son enteros: da 0. Recién entonces ese 0 se convierte a double y se guarda 0.0. La variable es double, pero la división fue entera. Lo que manda es el tipo de los operandos, no el de la variable que recibe.

Para corregirlo, basta que un operando sea double: 1.0 / 2.

El cast

Cuando los operandos son variables, no se les puede agregar .0. Para eso está el cast: se escribe el tipo entre paréntesis delante de un valor, y se obtiene ese valor convertido a ese tipo. La variable no cambia.

int sum = 7;
int count = 2;

double late = sum / count;              // divide enteros y luego convierte
double cast_first = (double) sum / count;
double cast_late = (double) (sum / count);
double from_int = count;                // de int a double: 2.0

int truncated = 3.99;                   // de double a int: se pierden los decimales
int cast_positive = (int) 3.99;
int cast_negative = (int) -3.99;

El programa conversions.c escribe cada valor:

$ build/conversions
sum / count = 3.000000
(double) sum / count = 3.500000
(double) (sum / count) = 3.000000
from_int = 2.000000
truncated = 3
(int) 3.99 = 3
(int) -3.99 = -3

El cast tiene más precedencia que la división, así que solo afecta a lo que tiene inmediatamente después. Comparen, con sum en 7 y count en 2:

Expresión Qué se calcula Resultado
sum / count división entera, 3 3.0 al guardarlo en un double
(double) sum / count 7.0 / 2, y count se convierte solo 3.5
(double) (sum / count) primero la división entera, 3; luego 3.0 3.0

La tercera fila es el error típico: el cast llega tarde, cuando los decimales ya se perdieron. Las últimas líneas de la salida muestran el otro sentido: tanto 3.99 como -3.99 pierden sus decimales, sin redondeo y siempre hacia cero.

Error común. Ni -Wall ni -Wextra avisan de double half = 1 / 2;, ni de int truncated = 3.99;. Son instrucciones válidas, que hacen algo que seguramente no querían. El compilador no adivina la intención. Por eso se prueba con un ejemplo hecho a mano.

Escribir con printf

Ya usaron printf para escribir texto. Su primer argumento es una cadena de formato. Dentro de ella, cada % marca un hueco, y los valores que vienen después de la coma los van llenando en orden:

Formato Escribe Ejemplo con el valor Sale
%d un int 15 15
%f un double, con 6 decimales 1.685 1.685000
%.2f un double, con 2 decimales 1.685 1.69
%c un char 'B' B
%zu el resultado de sizeof sizeof(int) 4
%% el símbolo % %

Dentro de la cadena, \n es un salto de línea y \t un tabulador.

El programa printf_formats.c usa todos estos formatos, con grade en 15, height en 1.685 y section en 'B':

$ build/printf_formats
Nota: 15
Estatura: 1.685000
Estatura: 1.69
Sección: B
Nota 15, sección B
Avance: 80%
Columna 1	Columna 2
2.675 con dos decimales: 2.67

Dos precisiones sobre %.2f:

  • Solo cambia lo que se escribe. La variable height sigue valiendo lo mismo. Para los cálculos siguientes, se usa el valor completo.
  • Redondea el valor que de verdad está guardado. La última línea escribe 2.67, no 2.68. Un double no puede guardar exactamente 2.675: guarda el valor más cercano que puede, que resulta estar un poquito por debajo. La semana 3 explica por qué.

Error común. Que el formato no coincida con el valor. printf confía en lo que dice el formato, así que un %d con un double no convierte nada: es comportamiento indefinido, y suele escribir un número sin relación con el valor. -Wall lo detecta, e incluso sugiere el formato correcto (broken/printf_wrong_format.c).

Leer con scanf

Un dato del teclado

scanf hace lo contrario que printf: lee de la entrada y guarda lo leído en variables.

// Primer programa que lee un dato del teclado.
#include <stdio.h>

int main(void) {
    int grade = 0;

    printf("Nota: ");
    scanf("%d", &grade);    // aún no revisamos lo que devuelve: ver read_check.c
    printf("Leí la nota %d\n", grade);
    printf("Con dos puntos más sería %d\n", grade + 2);
    return 0;
}
$ build/read_grade
Nota: 15
Leí la nota 15
Con dos puntos más sería 17

El 15 lo escribimos en el teclado y terminamos con Enter. El programa se detiene en scanf hasta que llega el dato. Por eso, antes de leer, el programa escribe un mensaje que dice qué espera: sin él, la persona ve una pantalla quieta y no sabe qué hacer.

Vean las piezas de scanf("%d", &grade):

  • "%d" dice qué esperar y cómo interpretarlo: un entero.
  • &grade dice dónde guardarlo. El & da la dirección de memoria (memory address) de la variable: el lugar donde está. Si escribimos grade sin &, scanf recibiría el valor de la variable, y no sabría dónde dejar lo leído. Las direcciones las estudiamos en la semana 9. Por ahora, la regla es: en scanf, el & va delante de cada variable.

Formatos de scanf

Para leer Formato en scanf Formato en printf
int %d %d
double %lf %f
char %c %c

Fíjense en double: %lf para leer y %f para escribir. Hay una razón. printf recibe un valor, y los float se convierten solos a double al pasárselos, así que %f sirve para ambos. scanf recibe un lugar, y necesita saber cuántos bytes va a escribir ahí. %f es para un float; %lf, para un double.

Error común. Estos dos errores producen programas que compilan y fallan. -Wall avisa de ambos. Cuando vean un aviso sobre un formato, deténganse a corregirlo.

Primero, olvidar el &:

$ gcc -Wall -Wextra -std=c17 broken/missing_ampersand.c -o build/missing_ampersand
broken/missing_ampersand.c: In function ‘main’:
broken/missing_ampersand.c:7:13: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]
    7 |     scanf("%d", grade);
      |            ~^   ~~~~~
      |             |   |
      |             |   int
      |             int *

El aviso dice que %d espera un int * (la dirección de un int) y que recibió un int. Si ignoran el aviso y lo ejecutan, en este caso el programa se cae con un error llamado segmentation fault: scanf intenta escribir en un lugar de la memoria que el programa no puede usar. Otras veces es peor, porque no se cae y escribe en un lugar equivocado.

Segundo, leer un double con %f:

$ gcc -Wall -Wextra -std=c17 broken/scanf_float_format.c -o build/scanf_float_format
broken/scanf_float_format.c: In function ‘main’:
broken/scanf_float_format.c:7:13: warning: format ‘%f’ expects argument of type ‘float *’, but argument 2 has type ‘double *’ [-Wformat=]
    7 |     scanf("%f", &price);
      |            ~^   ~~~~~~
      |             |   |
      |             |   double *
      |             float *
      |            %lf

Espacios y saltos de línea

%d y %lf saltan los espacios y saltos de línea que encuentren antes del número. Por eso se pueden leer varios datos a la vez, separados como quiera quien los escribe:

scanf("%d %d", &first, &second);

Se pueden escribir en una línea, 4 9, o en dos. Da igual.

%c es la excepción: lee el siguiente carácter, sea cual sea, y el salto de línea también es un carácter. Esto provoca un error muy común:

printf("Cantidad: ");
scanf("%d", &quantity);
printf("Talla: ");
scanf("%c", &size);

printf("Leí %d y un carácter de código %d\n", quantity, size);

Al ejecutar char_trap.c, escribiendo 3 y Enter:

$ build/char_trap
Cantidad: 3
Talla: Leí 3 y un carácter de código 10

El programa no esperó la talla. Veamos por qué. Cuando escribimos 3 y Enter, llegaron a la entrada dos caracteres: 3 y el salto de línea. %d tomó el 3 y dejó ahí el salto de línea. Entonces %c encontró ese salto de línea, cuyo código es 10, y lo tomó como la talla.

La solución es un espacio antes de %c. En un formato de scanf, un espacio significa “salta todos los espacios y saltos de línea que haya”. En char_fixed.c, la lectura es scanf(" %c", &size);, y el programa sí espera la talla.

Animación con una caja llamada entrada y dos variables debajo, quantity en 0 y size en el carácter de interrogación. Primero, se escribe 3 y Enter, y a la entrada llegan dos caracteres, el dígito 3 y el salto de línea de código 10. Luego, scanf con %d toma el 3, que pasa a quantity, y deja ahí el salto de línea. Luego, scanf con %c toma el salto de línea, size queda con 10, la entrada queda vacía y el programa no espera la talla. Al final, en char_fixed.c, scanf con un espacio antes de %c salta el salto de línea, size sigue sin cambiar y el programa espera la tallaAnimación con una caja llamada entrada y dos variables debajo, quantity en 0 y size en el carácter de interrogación. Primero, se escribe 3 y Enter, y a la entrada llegan dos caracteres, el dígito 3 y el salto de línea de código 10. Luego, scanf con %d toma el 3, que pasa a quantity, y deja ahí el salto de línea. Luego, scanf con %c toma el salto de línea, size queda con 10, la entrada queda vacía y el programa no espera la talla. Al final, en char_fixed.c, scanf con un espacio antes de %c salta el salto de línea, size sigue sin cambiar y el programa espera la talla
El Enter también es un carácter, y se queda en la entrada hasta que alguien lo lee. El espacio en el formato lo salta.

Revisar lo que devuelve scanf

Hasta ahora hemos supuesto que la persona escribe lo que se le pide. No siempre es así. ¿Qué pasa si escribe hola cuando se espera un número?

scanf devuelve un número: cuántos datos logró guardar. Ese resultado se puede guardar en una variable, igual que cualquier otro valor. Este programa lo escribe:

// scanf devuelve cuántos datos pudo guardar. Aquí solo lo mostramos:
// actuar según ese número necesita el if de la semana 4.
#include <stdio.h>

int main(void) {
    int first = 0;
    int second = 0;

    printf("Dos enteros: ");
    int count = scanf("%d %d", &first, &second);

    printf("scanf devolvió %d\n", count);
    printf("first = %d, second = %d\n", first, second);
    return 0;
}

Con dos enteros, como 4 9, scanf devuelve 2. Con hola en el segundo dato, guarda el primero, se detiene y devuelve 1:

$ build/read_check
Dos enteros: 4 hola
scanf devolvió 1
first = 4, second = 0

Con hola desde el inicio, no guarda nada y devuelve 0. Si escribimos 3.7 5, %d toma el 3 y se detiene en el punto, porque un entero no lleva punto. El segundo %d encuentra .7, y falla: devuelve 1.

Y si la entrada se termina antes de leer algo, scanf devuelve un valor negativo, que en nuestro entorno es -1. Este valor tiene nombre: EOF, de end of file. En la terminal, se termina la entrada con Ctrl+D. (Por eso no se ve ningún dato después del mensaje.)

$ build/read_check
Dos enteros: scanf devolvió -1
first = 0, second = 0

Resumiendo, para scanf("%d %d", &first, &second):

Se escribe Devuelve Qué pasó
4 9 2 Guardó los dos datos
4 hola 1 Guardó solo el primero
3.7 5 1 Guardó 3; no pudo leer .7 como entero
hola 0 No guardó nada
(Ctrl+D) -1 Se terminó la entrada

Lo que scanf no puede leer no se pierde: se queda en la entrada y es lo primero que ve la siguiente lectura.

El programa leftover.c intenta leer un entero y luego, con " %c", un carácter:

$ build/leftover
Un entero: hola
scanf devolvió 0
number = 0
Lo siguiente en la entrada es h

Idea clave. Una variable que scanf no pudo llenar conserva el valor que tenía. Si no tenía ninguno, usarla es el error de la variable sin valor. Por eso, en los programas de esta semana, toda variable que se va a leer empieza con un valor conocido, como int grade = 0;.

Ese valor inicial evita el comportamiento indefinido, pero no hace correcto el resultado. Miren qué pasa si alguien escribe mal el precio en igv_price.c:

$ build/igv_price
Precio sin IGV: hola
IGV: 0.00
Total: 0.00

El programa no dice que algo salió mal: escribe una cuenta falsa. Esto es importante.

Un programa serio revisa lo que devuelve cada scanf y actúa si no es lo esperado. Actuar requiere una decisión, y las decisiones (if) llegan en la semana 4. A partir de entonces, todo programa del curso revisará sus lecturas. Hasta entonces, nuestros programas confían en que los datos llegan bien, y un comentario en cada uno recuerda que esto queda pendiente. No es que la entrada no pueda fallar: es que aún no tenemos cómo reaccionar.

De la fórmula al programa

Ya tenemos todas las piezas. Para pasar de una fórmula a un programa, sigan siempre estos pasos:

  1. Datos y resultado. ¿Qué se lee, de qué tipo, y qué se escribe?
  2. Un ejemplo a mano. Elijan números y calculen el resultado. Es su prueba.
  3. Pseudocódigo.
  4. C. Con cuidado con los tipos.
  5. Comprobar. Ejecuten con el ejemplo del paso 2, y luego con otros.

En pseudocódigo escribimos las divisiones como en matemáticas: / da el resultado exacto, con decimales. Para la división entera y el resto usamos dos palabras, div y mod: 7 / 2 es 3.5, 7 div 2 es 3 y 7 mod 2 es 1. Así el pseudocódigo dice lo que queremos, y decidir los tipos para lograrlo es trabajo del paso 4.

Ejemplo 1: un promedio ponderado

Un curso calcula el promedio final con esta fórmula, en la que el examen final pesa el doble:

Promedio final = (EP + 2 EF + PP) / 4

Paso 1. Se leen tres enteros: examen parcial ep, examen final ef y promedio de prácticas pp. (En la realidad, pp puede tener decimales. Lo simplificamos para este ejemplo.) Se escribe el promedio con dos decimales.

Paso 2. A mano, con 13, 15 y 14: 13 + 2 · 15 + 14 = 57, y 57 / 4 = 14.25.

Paso 3.

algoritmo promedio_final()
    leer ep
    leer ef
    leer pp
    promedio ← (ep + 2 * ef + pp) / 4
    escribir promedio
fin algoritmo

Paso 4. Una primera versión (final_grade_bug.c) traduce directamente la línea del cálculo:

double final_grade = (ep + 2 * ef + pp) / 4;

Paso 5. Compila sin avisos. Comprobamos con el ejemplo:

$ build/final_grade_bug
Examen parcial: 13
Examen final: 15
Promedio de prácticas: 14
Promedio final: 14.00

Esperábamos 14.25 y salió 14.00. El paso 2 acaba de encontrar un bug. La causa ya la conocen: los operandos de / son enteros, así que la división es entera, aunque el resultado se guarde en un double. El arreglo es dividir entre 4.0:

// Promedio final ponderado: (EP + 2 EF + PP) / 4.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <stdio.h>

int main(void) {
    int ep = 0;
    int ef = 0;
    int pp = 0;

    printf("Examen parcial: ");
    scanf("%d", &ep);
    printf("Examen final: ");
    scanf("%d", &ef);
    printf("Promedio de prácticas: ");
    scanf("%d", &pp);

    double final_grade = (ep + 2 * ef + pp) / 4.0;

    printf("Promedio final: %.2f\n", final_grade);
    return 0;
}
$ build/final_grade
Examen parcial: 13
Examen final: 15
Promedio de prácticas: 14
Promedio final: 14.25

Ejemplo 2: el vuelto, ahora para cualquier monto

La semana pasada buscaron a mano el vuelto de S/ 7.80 con la menor cantidad de monedas: una de S/ 5, una de S/ 2, una de S/ 0.50, una de S/ 0.20 y una de S/ 0.10. El método era tomar la moneda más grande que quepa, tantas veces como se pueda, y seguir con lo que falta.

Ese “tantas veces como se pueda” parece necesitar un ciclo, pero div y mod lo resuelven de una vez. resto div 500 dice cuántas monedas de S/ 5 caben, y resto mod 500 dice cuánto falta después de usarlas.

Paso 1. Se lee el vuelto. Para evitar decimales, trabajamos en céntimos, con un int: S/ 7.80 son 780 céntimos. Se escribe cuántas monedas de cada valor se usan.

Paso 2. A mano, con 780, el resultado es el de arriba.

Paso 3.

algoritmo vuelto(resto)
    monedas_500 ← resto div 500
    resto ← resto mod 500
    monedas_200 ← resto div 200
    resto ← resto mod 200
    monedas_100 ← resto div 100
    resto ← resto mod 100
    monedas_50 ← resto div 50
    resto ← resto mod 50
    monedas_20 ← resto div 20
    resto ← resto mod 20
    monedas_10 ← resto div 10
    resto ← resto mod 10
    escribir monedas_500, monedas_200, monedas_100
    escribir monedas_50, monedas_20, monedas_10, resto
fin algoritmo

Paso 4. En C, / entre dos int ya es div, y % es mod:

// Vuelto con la menor cantidad de monedas, usando / y % en lugar de un ciclo.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <stdio.h>

int main(void) {
    int remaining = 0;

    printf("Vuelto en céntimos: ");
    scanf("%d", &remaining);

    int coins_500 = remaining / 500;
    remaining = remaining % 500;
    int coins_200 = remaining / 200;
    remaining = remaining % 200;
    int coins_100 = remaining / 100;
    remaining = remaining % 100;
    int coins_50 = remaining / 50;
    remaining = remaining % 50;
    int coins_20 = remaining / 20;
    remaining = remaining % 20;
    int coins_10 = remaining / 10;
    remaining = remaining % 10;

    printf("5 soles: %d\n", coins_500);
    printf("2 soles: %d\n", coins_200);
    printf("1 sol: %d\n", coins_100);
    printf("50 céntimos: %d\n", coins_50);
    printf("20 céntimos: %d\n", coins_20);
    printf("10 céntimos: %d\n", coins_10);
    printf("Sin cubrir: %d céntimos\n", remaining);
    return 0;
}

Paso 5.

$ build/change
Vuelto en céntimos: 780
5 soles: 1
2 soles: 1
1 sol: 0
50 céntimos: 1
20 céntimos: 1
10 céntimos: 1
Sin cubrir: 0 céntimos

Sigamos a mano cómo avanza la variable remaining, que es la que lleva lo que falta:

Moneda remaining antes remaining / moneda remaining después
500 780 1 280
200 280 1 80
100 80 0 80
50 80 1 30
20 30 1 10
10 10 1 0

Son 5 monedas, igual que lo que hallaron a mano. Y ahora el programa sirve para otro monto. Con 785 sobran 5 céntimos, que estas monedas no pueden cubrir, y la última línea lo dice: Sin cubrir: 5 céntimos.

Idea clave. Guardar dinero como céntimos en un int hace exacta la cuenta. Con double, S/ 7.80 sería una aproximación, y % ni siquiera compilaría. Para los ejemplos cortos de esta semana, como el IGV, un double basta. La semana 3 explica por qué en un sistema real no.

Funciones de math.h

C trae funciones matemáticas ya escritas en una biblioteca. Para usarlas hay que hacer dos cosas: incluir math.h al inicio del programa, y agregar -lm al final del comando de compilación. Esta -lm significa “enlaza con la biblioteca matemática”. (make ya lo hace en los ejemplos del curso.)

Función Calcula Con x en 2.5 escribe
sqrt(x) raíz cuadrada 1.581139
pow(x, 3) x elevado a 3 15.625000
fabs(x) valor absoluto 2.500000
floor(x) redondea hacia abajo 2.000000
ceil(x) redondea hacia arriba 3.000000
round(x) redondea al entero más cercano 3.000000

Los resultados son los del programa math_demo.c. Todas reciben double y devuelven double, incluso floor, ceil y round, y por eso se escriben con %f. Si les pasan un int, como el 3 de pow(x, 3), C lo convierte a double.

En diagonal.c, la raíz cuadrada da la diagonal de un rectángulo, por el teorema de Pitágoras:

double diagonal = sqrt(width * width + height * height);
$ build/diagonal
Ancho y alto en metros: 3 4
Diagonal: 5.00 metros

Si olvidan -lm, el error es este:

$ gcc -Wall -Wextra -std=c17 diagonal.c -o build/diagonal_no_lm
/usr/bin/ld: /tmp/ccFvYsrb.o: in function `main':
diagonal.c:(.text+0x58): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status

El nombre /tmp/ccFvYsrb.o cambia en cada compilación, y +0x58 depende de la máquina. Lo importante es undefined reference to sqrt. Ese mensaje no viene de la traducción de su código, que salió bien. Viene del último paso, el enlazador (ld), que junta su programa con las bibliotecas y no encontró sqrt. Se arregla agregando -lm:

gcc -Wall -Wextra -std=c17 diagonal.c -o diagonal -lm

Error común. En C, ^ no es la potencia, aunque así se escriba a veces en matemáticas. En C, 2 ^ 3 compila sin problema y hace otra cosa, una operación sobre bits. Para potencias se usa pow, y para un cuadrado basta con x * x.

En C. Con -std=c17 no está disponible la constante M_PI, que aparece en muchos ejemplos de internet, y gcc da un error de nombre no declarado. Si necesitan π, declárenla: const double PI = 3.14159265358979;.

La entrada y la salida pueden ser archivos

Un programa que lee con scanf y escribe con printf no sabe de dónde vienen los datos. Lee de la entrada estándar y escribe en la salida estándar, que normalmente son el teclado y la pantalla. La terminal puede conectarlas a archivos, sin cambiar el programa:

Se escribe Significa
./programa < entrada.txt La entrada viene de entrada.txt, en lugar del teclado
./programa > salida.txt La salida va a salida.txt, en lugar de la pantalla
./programa < entrada.txt > salida.txt Las dos cosas

A esto se le llama redirección. Esto es muy útil al probar: se escriben los datos una vez y no hay que volver a teclearlos. La carpeta del código trae grades.txt, cuyo contenido es 13 15 14. Podemos pasárselo al programa del promedio:

$ build/final_grade < grades.txt
Examen parcial: Examen final: Promedio de prácticas: Promedio final: 14.25

Noten dos cosas. Los mensajes aparecen todos seguidos, porque los números ya no se escriben en la terminal, y por eso no hay Enter. Y aunque el programa lee con tres scanf separados, los tres números pueden estar en una sola línea: %d salta los espacios.

Ahora también con la salida en un archivo, que luego miramos con cat:

$ build/final_grade < grades.txt > build/result.txt
$ cat build/result.txt
Examen parcial: Examen final: Promedio de prácticas: Promedio final: 14.25

El primer comando no escribe nada en pantalla: todo se fue al archivo. Y los mensajes como Examen parcial: también se fueron. printf no distingue un mensaje para una persona de un resultado. Por eso los programas pensados para trabajar con archivos no escriben esos mensajes, y así serán los del laboratorio.

Error común. > borra el contenido anterior del archivo sin preguntar. Si escriben ./programa < datos.txt > datos.txt, pierden sus datos: la terminal vacía el archivo antes de que el programa empiece.

El laboratorio, paso a paso

En la guía de la semana están las cinco actividades. En code/week-02/lab/ están los archivos con los que empiezan, con el formato exacto de salida que se pide en cada uno. Al terminar, make test-lab comprueba su trabajo; falla hasta que completen todo.

Los programas del laboratorio no escriben mensajes pidiendo el dato, solo el resultado. Así funcionan con la redirección y con las pruebas.

Actividad 1: de Celsius a Fahrenheit. La fórmula es F = C * 9 / 5 + 32. Antes de escribir C, háganlo a mano con 37: 37 · 9 = 333, 333 / 5 = 66.6 y 66.6 + 32 = 98.6. La temperatura puede tener decimales, así que es double, se lee con %lf y se escribe con %.2f. Cuidado: si escriben 9 / 5 entre paréntesis, ¿cuánto vale? Esa es la trampa que las pruebas buscan.

Actividad 2: predecir y comprobar. Completen predictions.txt antes de ejecutar. El objetivo no es acertar, es descubrir qué creían. Las expresiones 7.0 / 2 y (double) 7 / 2 usan %.1f porque son double, y 7 % 2 lleva %% en el formato.

Actividad 3: de segundos a horas, minutos y segundos. Es la misma idea del vuelto, con “monedas” de 3600 y de 60. Primero a mano, con 3725. 3725 div 3600 da 1 hora y 3725 mod 3600 deja 125; 125 div 60 da 2 minutos y 125 mod 60 deja 5 segundos. Escriban el pseudocódigo y luego el C. El resultado es 1 h 2 min 5 s.

Actividad 4: archivos. Creen lab/entrada.txt con un número de segundos. Desde code/week-02, ejecuten su programa de la actividad 3 así, y miren el resultado con cat:

make lab
build/lab/time_split < lab/entrada.txt > lab/salida.txt
cat lab/salida.txt

Actividad 5: tamaños. Escriban una línea con sizeof y %zu para cada uno de los tres tipos, y comparen con la salida de sizes.c.

Resumen

  • Una variable es un nombre para un lugar en memoria. Se declara con un tipo: int age = 19;.
  • = asigna: calcula la derecha y la guarda en la izquierda. x = x + 1 es válido y frecuente. Una asignación guarda un valor, no una fórmula. El orden de las líneas importa.
  • Usar una variable sin valor es comportamiento indefinido. -Wall lo avisa.
  • Los tipos son int, double y char, y sizeof dice cuánto ocupan: 4, 8 y 1 byte en el entorno del curso. char guarda un número pequeño, y una ñ no cabe en él.
  • Precedencia: *, / y % antes que + y -. A igual nivel, de izquierda a derecha.
  • División entera: entre dos enteros, / descarta los decimales. 7 / 2 vale 3, 7 % 2 vale 1 y 7.0 / 2 vale 3.5. Lo que manda es el tipo de los operandos, no el de la variable.
  • Un cast convierte un valor: (double) sum / count. Pasar de double a int descarta los decimales.
  • Dividir un entero entre cero es comportamiento indefinido.
  • printf: %d, %f, %.2f, %c, %zu, %%. El formato debe coincidir con el valor.
  • scanf: %d, %lf, %c, con & delante de la variable. %d y %lf saltan espacios; %c no, salvo con un espacio antes: " %c".
  • scanf devuelve cuántos datos guardó: lo esperado si todo salió bien, menos si algo falló, y EOF (un valor negativo, -1 en nuestro entorno) si se acabó la entrada. Actuar según eso requiere el if de la semana 4.
  • Para math.h: #include <math.h> y -lm al compilar.
  • < y > conectan la entrada y la salida del programa con archivos.
  • Una fórmula se vuelve programa así: datos, ejemplo a mano, pseudocódigo, C, y comprobar.

Ejercicios

Resuélvanlos en papel primero y comprueben después compilando. Todos se pueden hacer sin if ni ciclos.

  1. Seguir a mano. Hagan la tabla de variables. ¿Cuánto vale cada una al final?

    int a = 3;
    int b = a + 1;
    a = b * 2;
    b = a - b;
    a = a % b;
  2. Precedencia. Calculen cada expresión mostrando el orden de las operaciones, y digan si el resultado es int o double: 5 + 6 / 4; (5 + 6) / 4; 5.0 + 6 / 4; 18 % 7 * 2; -9 / 2; -9 % 2.

  3. Conversiones. ¿Qué guarda cada variable?

    double p = 3 / 2;
    int q = 7.9;
    double r = (double) 3 / 2;
    double s = (double) (3 / 2);
    int t = (int) 2.5 * 2;
  4. Formatos. Escriban un printf que, a partir de int rate = 18; y double total = 59.0;, escriba exactamente Total: 59.00 soles (18%).

  5. Encontrar los errores. Este programa tiene tres errores. Encuéntrenlos sin compilar, y después comprueben qué avisos da gcc:

    #include <stdio.h>
    
    int main(void) {
        int n;
        double half = n / 2;
        scanf("%d", n);
        printf("%d\n", half);
        return 0;
    }
  6. Lo que devuelve scanf. Para scanf("%d %lf", &n, &x), con n y x iniciados en 0, digan qué devuelve y qué valores quedan si se escribe: 3 2.5; 3 hola; hola 3; 7.5 2; y Ctrl+D. Escriban un programa para comprobarlo.

  7. Una fórmula. Escriban en pseudocódigo y en C un programa que lea el ancho y el alto de un rectángulo, con decimales, y escriba su perímetro y su área con dos decimales. Hagan antes un ejemplo a mano.

  8. Soles y céntimos. Escriban un programa que lea un monto en céntimos, como 1234, y escriba 12 soles con 34 céntimos.

  9. Dígitos. Escriban un programa que lea un entero positivo de tres dígitos y escriba la suma de sus dígitos. Con 472, el resultado es 13. ¿Qué escribe con un número de cuatro dígitos? ¿Por qué?

  10. Interés compuesto. Un capital crece cada año multiplicándose por 1 + tasa / 100. Escriban en pseudocódigo y en C un programa que lea el capital, la tasa y los años, y escriba el monto final con pow. Con 1000 soles, tasa 5 y 3 años, debe dar 1157.63 (es un ejemplo para probar, no una tasa real).

  11. Distancia. Escriban en pseudocódigo y en C un programa que lea las coordenadas de dos puntos del plano y escriba la distancia entre ellos, usando sqrt. Compruébenlo con los puntos (0, 0) y (3, 4).

  12. Redirección. Escriban su programa del promedio de tres notas en dos versiones: con mensajes y sin mensajes. Córranlas con < notas.txt > resultado.txt, y comparen los dos archivos con cat. Luego escriban en notas.txt solo dos notas. ¿Qué sale? ¿Por qué el programa no avisa?

  13. Buses (reto). A un paseo van n estudiantes, y en cada bus caben 40. Escriban un programa que lea n y escriba cuántos buses hacen falta, con ceil. ¿Por qué ceil(n / 40) da una respuesta equivocada con n igual a 41? Después, resuélvanlo solo con enteros, sin math.h. Pista: sumen algo a n antes de dividir.