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,makecompila todos los ejemplos en la carpetabuild/ymake testlos 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 conscanf”, “De la fórmula al programa” y “Funciones demath.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:
- Se calcula el valor de la derecha, con los valores actuales de las variables.
- Ese resultado se guarda en la variable de la izquierda. Lo que había antes se pierde.
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 |


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,gccno dice nada sobre este programa, y las revisiones de-fsanitizetampoco lo detectan al ejecutarlo. Por eso compilamos siempre con-Wall -Wextray 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.
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 unchar. 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 / 5da -3, no -4. - El resto se calcula para que todo cuadre. Siempre se cumple
(a / b) * b + a % b == a. Por eso-17 % 5da -2, ya que(-3) * 5 + (-2)es -17. El resto toma el signo del primer operando, y por eso17 % -5da 2. %solo funciona con enteros. Con undouble, no compila:gccdaerror: invalid operands to binary %(el programa esbroken/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:
- En una operación entre
intydouble, elintse convierte adoubleantes de operar. Por eso7.0 / 2es7.0 / 2.0y da 3.5. - En una asignación, el valor se convierte al tipo de la variable. De
intadoubleno se pierde nada en nuestros ejemplos: 2 se vuelve 2.0. Dedoubleaintse pierden los decimales: se descartan, no se redondea. Si la parte entera tampoco cabe en elint, 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
-Wallni-Wextraavisan dedouble half = 1 / 2;, ni deint 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
heightsigue 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, no2.68. Undoubleno 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.
printfconfía en lo que dice el formato, así que un%dcon undoubleno convierte nada: es comportamiento indefinido, y suele escribir un número sin relación con el valor.-Walllo 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.&gradedice dónde guardarlo. El&da la dirección de memoria (memory address) de la variable: el lugar donde está. Si escribimosgradesin&,scanfrecibirí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: enscanf, 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.
-Wallavisa 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.


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
scanfno 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, comoint 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:
- Datos y resultado. ¿Qué se lee, de qué tipo, y qué se escribe?
- Un ejemplo a mano. Elijan números y calculen el resultado. Es su prueba.
- Pseudocódigo.
- C. Con cuidado con los tipos.
- 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
inthace exacta la cuenta. Condouble, S/ 7.80 sería una aproximación, y%ni siquiera compilaría. Para los ejemplos cortos de esta semana, como el IGV, undoublebasta. 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 ^ 3compila sin problema y hace otra cosa, una operación sobre bits. Para potencias se usapow, y para un cuadrado basta conx * x.
En C. Con
-std=c17no está disponible la constanteM_PI, que aparece en muchos ejemplos de internet, ygccda 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 + 1es 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.
-Walllo avisa. - Los tipos son
int,doubleychar, ysizeofdice cuánto ocupan: 4, 8 y 1 byte en el entorno del curso.charguarda 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 / 2vale 3,7 % 2vale 1 y7.0 / 2vale 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 dedoubleaintdescarta 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.%dy%lfsaltan espacios;%cno, salvo con un espacio antes:" %c".scanfdevuelve cuántos datos guardó: lo esperado si todo salió bien, menos si algo falló, yEOF(un valor negativo, -1 en nuestro entorno) si se acabó la entrada. Actuar según eso requiere elifde la semana 4.- Para
math.h:#include <math.h>y-lmal 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.
-
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; -
Precedencia. Calculen cada expresión mostrando el orden de las operaciones, y digan si el resultado es
intodouble:5 + 6 / 4;(5 + 6) / 4;5.0 + 6 / 4;18 % 7 * 2;-9 / 2;-9 % 2. -
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; -
Formatos. Escriban un
printfque, a partir deint rate = 18;ydouble total = 59.0;, escriba exactamenteTotal: 59.00 soles (18%). -
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; } -
Lo que devuelve
scanf. Parascanf("%d %lf", &n, &x), connyxiniciados en 0, digan qué devuelve y qué valores quedan si se escribe:3 2.5;3 hola;hola 3;7.5 2; yCtrl+D. Escriban un programa para comprobarlo. -
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.
-
Soles y céntimos. Escriban un programa que lea un monto en céntimos, como
1234, y escriba12 soles con 34 céntimos. -
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é? -
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 conpow. Con 1000 soles, tasa 5 y 3 años, debe dar 1157.63 (es un ejemplo para probar, no una tasa real). -
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). -
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 concat. Luego escriban ennotas.txtsolo dos notas. ¿Qué sale? ¿Por qué el programa no avisa? -
Buses (reto). A un paseo van
nestudiantes, y en cada bus caben 40. Escriban un programa que leany escriba cuántos buses hacen falta, conceil. ¿Por quéceil(n / 40)da una respuesta equivocada connigual a 41? Después, resuélvanlo solo con enteros, sinmath.h. Pista: sumen algo anantes de dividir.