En esta página
Conceptos de C que usa el Rush 00
Este documento explica los conceptos, no el ejercicio. El ejercicio está en
README.md. Lee esto primero si llevas poco tiempo programando:
cada concepto aquí tiene un ejemplo mínimo y una línea que dice dónde aparece
en el rush.
Hecho con amor para Laura, por Bogdan.
Todos los conceptos comparten un mismo enemigo: el anidamiento. Cuanto más
adentro está tu código (más llaves, más indentación), más difícil es entenderlo.
Variables con buenos nombres, funciones chicas, return temprano y bucles
simples son las cuatro herramientas para mantenerlo plano.
Variables
Qué es
Una variable es una caja con nombre en la memoria. La caja tiene tres cosas:
| Propiedad | Ejemplo | Detalle |
|---|---|---|
| un nombre | column | cómo la llamas en el código |
| un tipo | int | qué forma tiene lo que guarda y cuánto ocupa |
| un valor | 0 | lo que hay adentro ahora mismo |
int column; /* creo la caja. Adentro hay BASURA. */
column = 0; /* guardo un 0. Solo ahora sé qué hay. */
La primera línea es una declaración; la segunda, una asignación. También se pueden juntar:
int column = 0; /* declaro y asigno en una sola línea */
Las dos formas son válidas. En este proyecto usamos la primera porque la Norm de 42 pide las declaraciones al principio del bloque, separadas del código.
El tipo importa
El tipo decide cuántos bytes ocupa la caja y cómo se interpretan:
| Tipo | Tamaño típico | Guarda | Ejemplo |
|---|---|---|---|
char | 1 byte | un carácter (o un número entre -128 y 127) | 'o' |
int | 4 bytes | un entero | -3, 0, 42 |
long | 8 bytes | un entero más grande | 1000000 |
Si guardas 'o' en un char, la caja contiene el número 111 (su código ASCII).
Si guardas 300 en un char, se desborda: no entra. Ese es el motivo por el
que el ancho y el alto del rush son int y no char.
Ciclo de vida (scope)
Una variable vive entre la llave que la abre y la llave que la cierra. Al salir de ese bloque, la caja desaparece:
void rush(int x, int y)
{
int row; /* row vive hasta el } de rush */
row = 0;
while (row < y)
{
/* aquí row sigue viviendo */
row++;
}
/* aquí row sigue viviendo */
}
/* aquí row ya no existe: usarla es un error de compilación */
Dos consecuencias que sorprenden el primer día:
- Una variable no se puede usar fuera de su bloque.
- Dos funciones pueden tener una variable llamada
rowsin pisarse: son cajas distintas en lugares distintos.
Por eso print_horizontal_edge y print_vertical_edge pueden declarar cada una
su propio column: no comparten nada.
Nunca uses una variable sin inicializar
int column; /* contiene basura: lo que quedó de otro programa */
while (column < 5) /* compara basura con 5: resultado impredecible */
Esto no siempre da error: da comportamiento raro, que es peor. La regla es simple: la primera vez que nombres una variable, dale un valor.
Dónde aparece en el rush
| Variable | Para qué |
|---|---|
x, y | parámetros: ancho y alto |
width | copia del ancho dentro de las funciones auxiliares |
row | qué fila estoy imprimiendo |
column | qué columna de esa fila estoy imprimiendo |
Ojo: width es una copia de x. Si dentro de la función cambiaras width,
x en rush no se entera. Eso se explica en Funciones.
Funciones
Qué es
Una función es un bloque de código con nombre que puedes ejecutar cuando quieras, todas las veces que quieras, sin copiar y pegar.
void ft_putchar(char c) /* firma: qué devuelve, cómo se llama, qué recibe */
{
write(1, &c, 1); /* cuerpo: lo que hace */
}
La firma se lee así:
void ft_putchar (char c)
│ │ │
│ │ └── parámetro: qué recibe
│ └─────────────── nombre
└────────────────────────────── qué devuelve (void = nada)
Parámetro vs argumento
Se confunden todo el tiempo y no son lo mismo:
| Término | Dónde vive | Ejemplo |
|---|---|---|
| parámetro | en la definición de la función | char c |
| argumento | en el lugar donde la llamas | el 'o' de ft_putchar('o') |
El parámetro es la caja vacía; el argumento es lo que metes en la caja al llamar.
void significa dos cosas distintas
Sí, C reusa la palabra. Mira:
void ft_putchar(char c); /* void aquí: NO DEVUELVE nada */
int main(void); /* void aquí: NO RECIBE argumentos */
| Posición | Significado |
|---|---|
| antes del nombre | no devuelve valor |
| entre paréntesis | no recibe parámetros |
Valor de retorno
Una función puede devolver un valor, y ese valor se puede usar:
int add(int a, int b)
{
return (a + b); /* devuelve el resultado y termina la función */
}
int main(void)
{
int total;
total = add(2, 3); /* total vale 5 */
return (0);
}
return hace dos cosas a la vez: entrega el valor y sale de la función.
Nada de lo que está después se ejecuta.
En una función void no hay valor que entregar, así que se escribe return ;
(sí, con espacio antes del punto y coma, por la Norm).
Paso por valor: las funciones reciben COPIAS
Este es el concepto que más cuesta. En C, cuando llamas a una función, se copian los valores:
void romper(int n)
{
n = 999; /* cambia la COPIA, no el original */
}
int main(void)
{
int numero;
numero = 5;
romper(numero);
/* numero sigue valiendo 5 */
}
Si quieres que una función modifique la variable original, tienes que pasarle la
dirección de esa variable (un puntero). Eso es lo que hace write:
void ft_putchar(char c)
{
write(1, &c, 1); /* &c = "la dirección donde está la caja c" */
}
| Forma | Qué significa |
|---|---|
c | el valor guardado en la caja |
&c | la dirección de la caja |
write no pide el carácter, pide dónde leerlo. Como c es la copia local
del parámetro, &c es una dirección válida mientras la función se ejecuta, y
por eso funciona.
Declaración, definición y llamada
Tres cosas que se parecen y no son iguales:
void ft_putchar(char c); /* 1. DECLARACIÓN (prototipo): "existe, y así es" */
void ft_putchar(char c) /* 2. DEFINICIÓN: el cuerpo, lo que hace */
{
write(1, &c, 1);
}
int main(void)
{
ft_putchar('o'); /* 3. LLAMADA: ejecútala ahora */
return (0);
}
¿Por qué existe la declaración? Porque el compilador lee los archivos de
arriba a abajo y de a uno. Cuando compila rush00.c, no ve ft_putchar.c. El
prototipo es la promesa de que esa función existe en otro lado, y le permite
verificar que la llames con los argumentos correctos.
Si la promesa no se cumple (declaras ft_putchar pero nunca compilas su
archivo), el error aparece solo al final, en el enlazador:
undefined reference to 'ft_putchar'.
static: funciones privadas del archivo
static void print_horizontal_edge(int width)
static aquí significa: esta función solo existe dentro de este archivo.
Nadie de afuera puede llamarla. Sirve para dos cosas:
- decir "esto es un ayudante interno, no parte de la interfaz";
- evitar que dos archivos choquen si definen una función con el mismo nombre.
No hace falta para aprobar. Sí hace el código más honesto.
Dónde aparece en el rush
rush ← decide qué fila toca
├── print_horizontal_edge ← dibuja una fila de borde
└── print_vertical_edge ← dibuja una fila del medio
└── ft_putchar ← escribe un carácter
Cada nivel usa al de abajo y no sabe cómo funciona por dentro. rush no
sabe qué carácter es una esquina: solo pide "dibuja un borde".
Por qué subdividir en funciones
El problema que resuelve
Supongamos que no usamos funciones auxiliares. Todo en una sola:
void rush(int x, int y)
{
int row;
int column;
row = 0;
while (row < y)
{
column = 0;
while (column < x)
{
if (row == 0 || row == y - 1)
{
if (column == 0 || column == x - 1)
ft_putchar('o');
else
ft_putchar('-');
}
else
{
if (column == 0 || column == x - 1)
ft_putchar('|');
else
ft_putchar(' ');
}
column++;
}
ft_putchar('\n');
row++;
}
}
Funciona. Pero para entender qué hace tienes que sostener cuatro decisiones a la vez: fila, columna, borde, relleno. Y la indentación ya va por el cuarto nivel.
Con funciones queda así:
while (row < y)
{
if (row == 0 || row == y - 1)
print_horizontal_edge(x);
else
print_vertical_edge(x);
row++;
}
Leído en voz alta: "mientras haya filas: si es la primera o la última, dibuja un borde; si no, dibuja el medio". Se entiende sin mirar las otras funciones.
Los cuatro beneficios reales
| Beneficio | Qué significa en la práctica |
|---|---|
| Nombre en lugar de código | print_horizontal_edge(x) se explica solo; un while con cuatro if no |
| Una tarea por función | si algo está mal en el borde, sabes en qué función mirar |
| Sin duplicación | la lógica de "primera o última columna" está escrita una vez |
| Menos anidamiento | cada función nueva es un nivel de indentación que desaparece |
Cuándo NO subdividir
Esto es tan importante como lo anterior. Subdividir de más también es un error:
void print_o(void) { ft_putchar('o'); } /* no aporta nada */
void print_dash(void){ ft_putchar('-'); } /* no aporta nada */
Esa función no tiene nombre propio ni lógica propia: solo agrega un salto mental. Reglas prácticas:
- Regla de tres: si el mismo bloque aparece tres veces, extráelo a una función. Dos veces todavía se tolera.
- Nombre honesto: si para describirla necesitas un "y", son dos funciones.
- Una pantalla: si no entra en ~25 líneas, probablemente haga demasiado.
- Si el nombre repite el cuerpo, no la crees.
Dónde aparece en el rush
print_horizontal_edge y print_vertical_edge se ganaron su lugar porque cada
una tiene un bucle propio y un nombre que dice exactamente lo que hacen. En
cambio no creamos print_newline(): sería una función de una línea cuyo nombre
ya es la función de librería.
Early return (guard clauses)
Qué es
Una guard clause es un return al principio de la función que resuelve el
caso inválido y sale. El resto de la función queda para el caso normal.
void rush(int x, int y)
{
if (x <= 0 || y <= 0)
return ; /* caso inválido: me voy ya */
/* de aquí para abajo, x e y son válidos. Siempre. */
}
Por qué al principio y no al final
Compará las dos versiones:
Sin guard clause (todo anidado):
void rush(int x, int y)
{
int row;
if (x > 0 && y > 0)
{
row = 0;
while (row < y)
{
/* ... */
row++;
}
}
}
Con guard clause:
void rush(int x, int y)
{
int row;
if (x <= 0 || y <= 0)
return ;
row = 0;
while (row < y)
{
/* ... */
row++;
}
}
La segunda tiene una indentación menos y, sobre todo, elimina la pregunta.
En la primera, cada vez que lees el while tienes que recordar "esto solo pasa
si x e y son positivos". En la segunda, el while no está condicionado por
nada: si llegaste hasta ahí, los datos son válidos.
Las tres razones
| Razón | Detalle |
|---|---|
| Aplana el código | cada guard clause es un nivel de if que no aparece |
| Protege el resto | después del return no hay estados inválidos posibles |
| Es fácil de auditar | todos los casos raros están juntos, arriba, en tres líneas |
El patrón tiene nombre en la industria: "return early, return often". La idea es que el happy path (el camino feliz, el caso normal) quede alineado a la izquierda, sin escalones.
return no es exit
| Instrucción | Qué hace | Necesita |
|---|---|---|
return ; | sale de esta función y vuelve a quien la llamó | nada |
return (0); | sale de main y le devuelve 0 al sistema operativo | nada |
exit(1); | mata todo el programa, desde cualquier función | #include <stdlib.h> |
En el rush usamos return ;. Si usaramos exit(), el programa moriría y el
main no podría seguir probando otros casos. En un examen eso es fatal.
El error clásico
if (x == 0 || y == 0) /* MAL: no cubre los negativos */
return ;
El enunciado dice "cualquier entero, incluidos cero y negativos". == 0 deja
pasar -3. Por eso se escribe <= 0: una comparación que cubre los dos casos
de una vez.
Dónde aparece en el rush
if (x <= 0 || y <= 0)
return ;
Es la primera instrucción de rush. Sin ella, con y = -3 la condición
row == y - 1 se vuelve absurda y la función puede quedarse dando vueltas.
Bucles
Qué es
Un bucle repite un bloque mientras se cumpla una condición. Existe para no escribir lo mismo cinco veces:
ft_putchar('o'); /* quiero "ooooo" */
ft_putchar('o');
ft_putchar('o');
ft_putchar('o');
ft_putchar('o');
column = 0;
while (column < 5)
{
ft_putchar('o');
column++;
}
La anatomía de todo bucle: tres piezas
Cualquier bucle, en cualquier lenguaje, necesita estas tres cosas. Si falta una, el bucle se rompe:
| Pieza | En el rush | Qué pasa si falta |
|---|---|---|
| inicio | column = 0; | empieza con basura |
| condición | column < width | no hay forma de terminar |
| avance | column++; | bucle infinito |
El 90 % de los bucles infinitos de un principiante son la tercera pieza
olvidada. Si tu programa no termina, busca el ++.
Cómo funciona while, paso a paso
column = 0;
while (column < 3)
{
ft_putchar('#');
column++;
}
| Momento | column | column < 3 | Acción |
|---|---|---|---|
| antes de entrar | 0 | verdadero | ejecuta el cuerpo |
| fin de vuelta 1 | 1 | verdadero | ejecuta el cuerpo |
| fin de vuelta 2 | 2 | verdadero | ejecuta el cuerpo |
| fin de vuelta 3 | 3 | falso | sale del bucle |
Salida: ###.
Dos detalles:
- La condición se evalúa antes de cada vuelta. Si es falsa desde el principio, el cuerpo no se ejecuta nunca (cero vueltas es válido).
i++es lo mismo quei = i + 1. Existe tambiéni--,i += 2, etc.
while y for son lo mismo
column = 0;
while (column < width)
{
/* cuerpo */
column++;
}
for (column = 0; column < width; column++)
{
/* cuerpo */
}
El for junta las tres piezas en una línea: inicio, condición, avance. Se usa
cuando el recorrido es "de 0 a n". El while se usa cuando no sabes cuántas
vueltas van a hacer falta. En el rush usamos while porque es el estilo que
predomina en la piscina de 42, pero ambos son correctos.
Existe también do { ... } while (condición);, que ejecuta el cuerpo al menos
una vez. Aquí no lo necesitamos.
Bucles anidados
Un bucle dentro de otro recorre una grilla: el de afuera las filas, el de adentro las columnas.
row = 0;
while (row < 3) /* 3 filas */
{
column = 0;
while (column < 5) /* 5 columnas por fila */
{
ft_putchar('#');
column++;
}
ft_putchar('\n');
row++;
}
Salida:
#####
#####
#####
Las tres piezas del bucle interno se reinician en cada fila. Si te olvidas
de column = 0; dentro del while externo, la primera fila sale bien y las
demás salen vacías. Es un error clásico y silencioso.
El error de "uno de más" (off-by-one)
while (column <= width) /* MAL si los índices van de 0 a width - 1 */
Con width = 5, column toma los valores 0, 1, 2, 3, 4, 5: seis
iteraciones. En el rush eso imprime un carácter extra. En un array, eso es
leer o escribir fuera de los límites, que es peor (ver
Matrices).
Regla mnemotécnica: si empiezas en 0, comparas con <. Si empiezas en 1,
comparas con <=. No mezcles.
break y continue
| Instrucción | Qué hace |
|---|---|
break; | sale del bucle más interno ahora mismo |
continue; | salta al final de la vuelta y evalúa la condición |
Funcionan, pero en la piscina se evitan: hacen que la condición del while deje
de contar la historia completa. Si necesitas un break, probablemente la
condición esté mal escrita o falte una guard clause.
Dónde aparece en el rush
Hay tres bucles, uno por cada nivel:
| Bucle | Recorre | En qué función |
|---|---|---|
row | las filas (0 a y - 1) | rush |
column | las columnas (0 a x - 1) | print_horizontal_edge |
column | las columnas (0 a x - 1) | print_vertical_edge |
Y una decisión de diseño: en lugar de anidar los dos bucles dentro de rush,
dejamos el bucle de columnas dentro de las funciones auxiliares. Así rush
tiene un solo bucle. Subdividir funciones también sirve para esto: bajar el
anidamiento.
Matrices y grillas
Primero: arrays de una dimensión
Un array es una fila de cajas contiguas en memoria, todas del mismo tipo:
char linea[5];
índice: 0 1 2 3 4
┌─────┬─────┬─────┬─────┬─────┐
│ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┘
Se accede con linea[0], linea[4], etc. El índice no es un adorno: es la
distancia desde el principio. linea[3] significa "3 bytes después del
inicio".
Matriz = array de arrays
char tablero[3][5];
Se lee de derecha a izquierda: "5 char por fila, 3 filas". Son 15 bytes
seguidos:
tablero[0][0] tablero[0][1] tablero[0][2] tablero[0][3] tablero[0][4]
tablero[1][0] tablero[1][1] tablero[1][2] tablero[1][3] tablero[1][4]
tablero[2][0] tablero[2][1] tablero[2][2] tablero[2][3] tablero[2][4]
Dos reglas que evitan el 80 % de los bugs:
- Primero la fila, después la columna:
tablero[fila][columna]. - En memoria, las filas van enteras y en orden. Eso se llama
row-major order y es la razón por la que al imprimir hay que terminar cada
fila con
'\n'.
La dirección del elemento [fila][columna] es:
dirección_base + (fila * ancho + columna) * tamaño_del_elemento
Con char el tamaño es 1, así que tablero[1][2] está exactamente 7 bytes
después del inicio (1 * 5 + 2).
Recorrer una matriz
Siempre con dos bucles anidados: el externo las filas, el interno las columnas.
row = 0;
while (row < 3)
{
column = 0;
while (column < 5)
{
/* aquí trabajas con tablero[row][column] */
column++;
}
row++;
}
Si inviertes el orden (columnas afuera, filas adentro) el resultado no es un
error de compilación: es un dibujo transpuesto. Y si intercambias los
índices (tablero[column][row]), puedes leer fuera de los límites. El compilador
no te va a avisar: en C, pasarse de un array es comportamiento indefinido.
Puede andar, puede imprimir basura, puede explotar.
Ejemplo completo: dibujar en una matriz y después imprimir
Este programa llena un tablero de 3×5 con el mismo rectángulo del rush y después lo imprime. Es la versión "con memoria", para que veas la mecánica.
#include <unistd.h>
/* Suponemos que ft_putchar ya existe. */
void ft_putchar(char c);
void dibujar_tablero(void)
{
char tablero[3][5];
int row;
int column;
/* 1. Llenar la matriz celda por celda */
row = 0;
while (row < 3)
{
column = 0;
while (column < 5)
{
if (row == 0 || row == 2 || column == 0 || column == 4)
tablero[row][column] = 'o';
else
tablero[row][column] = ' ';
column++;
}
row++;
}
/* 2. Imprimir la matriz fila por fila */
row = 0;
while (row < 3)
{
column = 0;
while (column < 5)
{
ft_putchar(tablero[row][column]);
column++;
}
ft_putchar('\n');
row++;
}
}
Salida:
ooooo
o o
ooooo
Fíjate que son dos recorridos completos: uno para llenar, otro para imprimir. Es el patrón estándar cuando el dibujo depende de toda la matriz (por ejemplo, para resolver un Sudoku o un laberinto).
Por qué el rush NO usa una matriz
Porque no hace falta. rush calcula el carácter de cada posición en el
momento y lo escribe: no guarda nada. Ventajas:
| Enfoque | Ventaja | Desventaja |
|---|---|---|
| Escribir directo (el rush) | no usa memoria, menos código, imposible pasarse de límites | no puedes "volver atrás" ni cambiar una celda ya impresa |
| Matriz primero | puedes consultar y modificar celdas | necesitas tamaño fijo o memoria dinámica, y recorrerla dos veces |
Además hay un detalle de C: char tablero[x][y] con x e y que llegan por
parámetro no compila en C clásico, porque el tamaño tiene que conocerse al
compilar. Se puede con VLA (C99) o con malloc, pero en la piscina eso trae
más problemas que soluciones. Si el ejercicio no pide guardar, no guardes.
Dónde aparece en el rush
El rectángulo es una matriz: x columnas por y filas. Lo único que cambia
es que en lugar de guardar cada celda en memoria, la mandamos a la pantalla en
el momento. El razonamiento fila = 0..y-1, columna = 0..x-1 es idéntico.
Y para el rush01 (el de las vistas) sí vas a necesitar matrices de verdad.
Esta sección es la base para eso.
Char y ASCII
Un char guarda un byte, y un byte es un número de 0 a 255. Las letras son
una convención: la tabla ASCII dice que el número 111 es 'o' y el 45 es '-'.
| Carácter | Número |
|---|---|
' ' (espacio) | 32 |
'\n' (salto de línea) | 10 |
'-' | 45 |
'o' | 111 |
| `' | '` |
Consecuencias prácticas:
ft_putchar('o')yft_putchar(111)hacen exactamente lo mismo. El compilador traduce'o'a 111.'\n'es un carácter, no dos. La barra invertida es la forma de escribir caracteres que no se ven."o"(comillas dobles) es un string: un array de 2 bytes (oy\0). No sirve como argumento deft_putchar, que espera un solochar.
Comillas simples = un carácter. Comillas dobles = texto. No se mezclan.
Índices y el cero
Los índices empiezan en 0 porque un índice es una distancia, no un número de orden. El primer elemento está a distancia 0 del inicio.
posición: 1° 2° 3° 4° 5°
índice: 0 1 2 3 4
Consecuencias que vas a usar todo el tiempo:
| Quieres | Escribís |
|---|---|
| el primero | [0] |
el último de n | [n - 1] |
| recorrer todos | while (i < n) |
| el del medio (aproximado) | [n / 2] |
En el rush, width - 1 es la última columna y y - 1 la última fila. No es
capricho: es la distancia desde 0.
Y esto explica el caso raro del enunciado: si x = 1, entonces
width - 1 = 0, así que la columna 0 es a la vez la primera y la última.
Por eso rush(1, 5) imprime o arriba, o abajo y | en el medio, sin
ningún if especial.
Compilar: qué es gcc y por qué esos flags
gcc no es "un programa", es un director de orquesta
GCC significa GNU Compiler Collection (colección de compiladores de GNU).
Nació en 1987 como "GNU C Compiler", pero hoy compila C, C++, Objective-C,
Fortran, Ada, Go y más. gcc es el que usas para C.
Lo importante: gcc no traduce nada por sí mismo. Es un driver: recibe
tus archivos, coordina cuatro programas distintos y te devuelve un ejecutable.
main.c ──► preprocesador ──► compilador ──► ensamblador ──► enlazador ──► rush00
(cpp) (cc1) (as) (ld)
.c → .i .i → .s .s → .o .o + libs → binario
| Etapa | Programa | Qué hace | Flag para frenar ahí | Salida |
|---|---|---|---|---|
| 1. Preprocesar | cpp | resuelve #include, #define y borra comentarios | -E | .i (C todavía legible) |
| 2. Compilar | cc1 | traduce C a ensamblador | -S | .s (ensamblador) |
| 3. Ensamblar | as | traduce ensamblador a código máquina | -c | .o (objeto) |
| 4. Enlazar | ld | une los .o y las librerías en un ejecutable | — | binario |
Puedes verlo con tus propios ojos:
gcc -E main.c -o main.i # solo preprocesar
gcc -S main.c -o main.s # hasta ensamblador
gcc -c main.c -o main.o # hasta objeto (sin enlazar)
file main.o # "ELF 64-bit ... relocatable"
Por qué esto te importa: hay DOS familias de errores
| Familia | Cuándo aparece | Ejemplo real |
|---|---|---|
| Error de compilación | etapas 1–3 | falta un ;, tipo equivocado, variable sin declarar |
| Error de enlazado (link) | etapa 4 | la función existe pero nunca la compilaste |
Ejemplo de compilación, con -Wall -Wextra -Werror:
w1.c: In function 'main':
w1.c:5:13: error: suggest parentheses around assignment used as truth value [-Werror=parentheses]
5 | if (x = 0)
| ^
Ejemplo de enlazado (olvidaste pasar ft_putchar.c):
/usr/bin/ld: /tmp/ccXa9fT2.o: in function `print_horizontal_edge':
rush00.c:(.text+0x2b): undefined reference to `ft_putchar'
Fíjate en el prefijo: ld: significa que el compilador ya terminó y el que se
queja es el enlazador. Ahí no hay nada mal escrito en tu código: falta una
pieza en el comando.
Por qué cada .c se compila por separado
Cada archivo es una unidad de compilación independiente. gcc compila
main.c sin mirar rush00.c, genera main.o, y solo al final junta todo.
De ahí sale todo lo que ya viste:
- Necesitas prototipos: el compilador no puede adivinar qué hay en otro archivo.
- Cambiar un solo
.cno obliga a recompilar los demás (por esomakees rápido en proyectos grandes). - Si dos archivos definen la misma función global, el enlazador se queja de multiple definition.
El comando, palabra por palabra
gcc -Wall -Wextra -Werror main.c ft_putchar.c rush00.c -o rush00
| Palabra | Qué es | Qué hace |
|---|---|---|
gcc | programa | el driver |
-Wall | flag | activa el primer grupo de warnings |
-Wextra | flag | activa un segundo grupo, más estricto |
-Werror | flag | convierte cada warning en error |
main.c ft_putchar.c rush00.c | entradas | los tres se compilan y se enlazan juntos |
-o rush00 | flag + valor | nombre del ejecutable de salida |
Sin -o, el ejecutable se llama a.out. Funciona, pero no dice nada.
Los flags, uno por uno
-Wall
Activa un conjunto curado de warnings que casi siempre son bugs reales:
| Warning | Qué detecta |
|---|---|
-Wunused-variable | declaraste algo y nunca lo usaste |
-Wparentheses | if (x = 0) en lugar de == |
-Wreturn-type | una función int sin return en algún camino |
-Wuninitialized | usas una variable sin darle valor |
-Wimplicit-function-declaration | llamas a una función sin prototipo |
El nombre engaña: -Wall no activa "todos" los warnings, solo los que el
equipo de GCC considera indiscutibles. Hay warnings que quedan afuera porque
son ruidosos o discutibles.
-Wextra
Activa warnings adicionales que son útiles pero más estrictos. Los más comunes en la piscina:
| Warning | Qué detecta |
|---|---|
-Wunused-parameter | un parámetro que no usas dentro de la función |
-Wsign-compare | comparas un int con un unsigned |
-Wtype-limits | comparaciones que nunca pueden ser verdaderas (char > 300) |
-Wmissing-field-initializers | inicializas una struct a medias |
Por eso en rush usamos los dos parámetros: si y no se usara, -Wextra
avisaría. Y por eso las funciones auxiliares reciben width aunque no lo
modifiquen: lo leen, y eso cuenta como uso.
-Werror
Convierte cada warning en un error: el compilador se niega a generar el ejecutable. La diferencia práctica:
gcc -Wall archivo.c -o prog # muestra el warning, exit code 0, genera prog
gcc -Wall -Werror archivo.c -o prog # muestra el error, exit code 1, NO genera prog
Y el detalle que confirma el pipeline: la última línea del error dice
cc1: all warnings being treated as errors
cc1 es el compilador propiamente dicho, no el driver. Ahí ves quién se queja.
¿Por qué 42 lo exige? Porque un warning es una sospecha de bug, y en un proyecto de 40 líneas puedes revisarlas todas. En un proyecto de 40.000 no. El hábito se instala desde el día uno. Además, el corrector de 42 compila con flags estrictos: si tu código "andaba" con warnings, puede que no compile ahí.
¿Cuándo desactivarlo? Solo mientras debuggeas, para ver si el programa llega a ejecutarse. Nunca en la entrega.
-o nombre
Define el nombre del archivo de salida. Dos reglas:
- El nombre va inmediatamente después de
-o. - Si no lo pones, se llama
a.out(histórico: assembler output).
gcc main.c -o rush00 # crea ./rush00
gcc main.c -o rushoo # error de tipeo: crea ./rushoo, y ./rush00 no existe
-c
Compila un archivo hasta objeto, sin enlazar. Sirve para dos cosas:
- revisar rápido si un archivo tiene errores de sintaxis;
- compilar por partes en proyectos grandes.
gcc -Wall -Wextra -Werror -c rush00.c -o rush00.o
Si usas -c con los tres archivos, obtienes tres .o y ningún ejecutable. Es
normal: todavía falta la etapa 4.
-g
Agrega información de depuración (nombres de variables, números de línea) al
ejecutable. No cambia el comportamiento, solo hace el binario más grande y
permite que gdb o valgrind te digan en qué línea falló.
gcc -Wall -Wextra -Werror -g main.c ft_putchar.c rush00.c -o rush00
gdb ./rush00
En la piscina no se usa para entregar, pero sí para entender un crash.
-I ruta (i mayúscula)
Agrega una carpeta a la búsqueda de #include. Si tu header está en
includes/ft.h, escribes -I includes y después #include "ft.h". En el rush
no hace falta porque no hay headers propios.
-std=c99, -std=c11, etc.
Elige la versión del estándar de C. Cambia qué puedes escribir:
| Estándar | Permite |
|---|---|
c89 / c90 | declarar variables solo al principio del bloque |
c99 | declarar en medio del bloque, VLAs, // |
c11 | _Static_assert, threads, alineación |
La Norm de 42 es de estilo C89, por eso en este proyecto las variables se
declaran arriba. Si compilas con -std=c89 -pedantic, una declaración en medio
de la función te marca ISO C90 forbids mixed declarations and code; con
-std=c99 eso es válido y el warning desaparece. Sin -pedantic, GCC acepta la
declaración igual, como extensión.
-fsanitize=address,undefined
Instrumenta el binario para detectar en ejecución lo que el compilador no puede ver en compilación: lecturas fuera de un array, uso de memoria liberada, desbordamiento de enteros. Es la herramienta más útil para aprender, porque convierte el "mi programa imprime basura" en un mensaje con línea exacta.
gcc -Wall -Wextra -Werror -g -fsanitize=address,undefined \
main.c ft_putchar.c rush00.c -o rush00_debug
Ojo: no se entrega con sanitizers (el binario es distinto y más lento). Se usa para investigar.
-O0, -O2, -O3
Nivel de optimización. -O0 no optimiza: el binario se parece línea por línea
a tu código, ideal para depurar. -O2 reordena y elimina cosas para que corra
más rápido, y al depurar te marea porque el código ya no coincide con el fuente.
En la piscina: -O0 (el default) siempre.
-lm y compañía (para más adelante)
-l enlaza una librería. Ejemplo clásico: si usas sqrt de <math.h> y
compilas sin -lm, el compilador no se queja (el prototipo existe) pero el
enlazador sí:
gcc main.c -o prog # undefined reference to `sqrt'
gcc main.c -o prog -lm # funciona
Ese error es la prueba viviente de la diferencia entre compilar y
enlazar. En el rush no hace falta: write está en la libc, que se enlaza
sola.
-D NOMBRE=valor
Define una macro desde la línea de comandos, como si hubieras escrito
#define NOMBRE valor al principio del archivo. Sirve para activar código de
prueba sin editar el fuente:
gcc -DDEBUG=1 main.c -o prog
Cómo leer un mensaje de error
rush00.c:23:14: error: 'row' undeclared (first use in this function)
│ │ │
│ │ └── columna
│ └───── línea
└────────────── archivo
Cuatro reglas:
- Lee el primero, no el último. Un error de sintaxis genera errores en cascada: arreglas el primero y desaparecen diez.
- La columna (
23:14) te dice dónde; muchas veces el problema está en la línea anterior (un;olvidado). - El texto entre corchetes (
[-Wunused-variable]) es el nombre del warning. Con ese nombre puedes buscarlo en la documentación de GCC. errordetiene la compilación;warningno (salvo con-Werror).
Los comandos que vas a usar de verdad
| Momento | Comando |
|---|---|
| Compilar y ejecutar | gcc -Wall -Wextra -Werror main.c ft_putchar.c rush00.c -o rush00 && ./rush00 |
| Solo verificar sintaxis de un archivo | gcc -Wall -Wextra -Werror -c rush00.c -o /tmp/rush00.o |
| Investigar un crash | gcc -g -fsanitize=address,undefined main.c ft_putchar.c rush00.c -o debug && ./debug |
| Ver el preprocesado | gcc -E main.c | less |
El && significa "si el anterior terminó bien, ejecuta el siguiente". Si la
compilación falla, el programa no se ejecuta y ves los errores en vez de una
salida vieja. Ese detalle evita muchísima confusión: no estás mirando la salida
de tu código nuevo, estás mirando la del binario anterior.
Dónde aparece en el rush
En el comando de compilación del README.md.
Los tres flags (-Wall -Wextra -Werror) son los que usa la piscina; el resto
(-g, -fsanitize, -c) son herramientas de diagnóstico que vas a necesitar
en cuanto algo no salga como esperas.
Errores de concepto frecuentes
| Error | Por qué está mal | Cómo se escribe |
|---|---|---|
if (x = 0) | asigna, no compara; y la condición da falso | if (x == 0) |
int i; y usarla | contiene basura | int i; i = 0; |
| contar desde 1 | el último índice es n - 1 | empezar en 0 |
while (i <= n) | una vuelta de más | while (i < n) |
olvidar i++ | bucle infinito | i++; en el cuerpo |
char c = "o"; | string en una caja de un carácter | char c = 'o'; |
write(1, c, 1) | falta la dirección | write(1, &c, 1) |
| cambiar el parámetro y esperar que el original cambie | C pasa copias | devolver el valor o usar puntero |
tablero[columna][fila] | índices invertidos | tablero[fila][columna] |
declarar char m[x][y] con parámetros | el tamaño debe ser constante | tamaño fijo, VLA o malloc |
exit(0) dentro de una función | mata el programa entero | return ; |
función de 60 líneas con 4 niveles de if | no se puede explicar | guard clause + funciones chicas |
Cómo practicar
En orden de dificultad. Cada uno se resuelve con lo de arriba y nada más.
| # | Ejercicio | Qué practica |
|---|---|---|
| 1 | Imprimir ##### (una fila, sin función auxiliar) | bucle simple |
| 2 | Imprimir 5 filas de # | bucle anidado |
| 3 | Imprimir un cuadrado hueco de 5×5 con # | condiciones dentro del bucle |
| 4 | Lo mismo, pero con ancho y alto por parámetro | parámetros |
| 5 | Extraer el borde a una función propia | subdivisión |
| 6 | Agregar guard clause para tamaños inválidos | early return |
| 7 | Llenar una matriz 3×3 con X en la diagonal y . fuera, después imprimirla | matrices |
| 8 | Contar cuántas o tiene tu rectángulo, sin imprimirlo | acumulador + matriz |
Del 7 sale un patrón que vas a repetir toda la piscina: if (fila == columna)
marca la diagonal principal de cualquier matriz cuadrada. Guárdalo.
Siguiente paso
Vuelve al README.md y lee la traza de rush(5, 3) con estos
conceptos en la cabeza. Si puedes explicar en voz alta por qué la columna 4
imprime o, ya tienes el ejercicio.