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:

PropiedadEjemploDetalle
un nombrecolumncómo la llamas en el código
un tipointqué forma tiene lo que guarda y cuánto ocupa
un valor0lo 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:

TipoTamaño típicoGuardaEjemplo
char1 byteun carácter (o un número entre -128 y 127)'o'
int4 bytesun entero-3, 0, 42
long8 bytesun entero más grande1000000

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:

  1. Una variable no se puede usar fuera de su bloque.
  2. Dos funciones pueden tener una variable llamada row sin 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

VariablePara qué
x, yparámetros: ancho y alto
widthcopia del ancho dentro de las funciones auxiliares
rowqué fila estoy imprimiendo
columnqué 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érminoDónde viveEjemplo
parámetroen la definición de la funciónchar c
argumentoen el lugar donde la llamasel '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ónSignificado
antes del nombreno devuelve valor
entre paréntesisno 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" */
}
FormaQué significa
cel valor guardado en la caja
&cla 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

BeneficioQué significa en la práctica
Nombre en lugar de códigoprint_horizontal_edge(x) se explica solo; un while con cuatro if no
Una tarea por funciónsi algo está mal en el borde, sabes en qué función mirar
Sin duplicaciónla lógica de "primera o última columna" está escrita una vez
Menos anidamientocada 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:

  1. Regla de tres: si el mismo bloque aparece tres veces, extráelo a una función. Dos veces todavía se tolera.
  2. Nombre honesto: si para describirla necesitas un "y", son dos funciones.
  3. Una pantalla: si no entra en ~25 líneas, probablemente haga demasiado.
  4. 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ónDetalle
Aplana el códigocada guard clause es un nivel de if que no aparece
Protege el restodespués del return no hay estados inválidos posibles
Es fácil de auditartodos 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ónQué haceNecesita
return ;sale de esta función y vuelve a quien la llamónada
return (0);sale de main y le devuelve 0 al sistema operativonada
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:

PiezaEn el rushQué pasa si falta
iniciocolumn = 0;empieza con basura
condicióncolumn < widthno hay forma de terminar
avancecolumn++;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++;
}
Momentocolumncolumn < 3Acción
antes de entrar0verdaderoejecuta el cuerpo
fin de vuelta 11verdaderoejecuta el cuerpo
fin de vuelta 22verdaderoejecuta el cuerpo
fin de vuelta 33falsosale del bucle

Salida: ###.

Dos detalles:

  1. 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).
  2. i++ es lo mismo que i = i + 1. Existe también i--, 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ónQué 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:

BucleRecorreEn qué función
rowlas filas (0 a y - 1)rush
columnlas columnas (0 a x - 1)print_horizontal_edge
columnlas 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:

  1. Primero la fila, después la columna: tablero[fila][columna].
  2. 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:

EnfoqueVentajaDesventaja
Escribir directo (el rush)no usa memoria, menos código, imposible pasarse de límitesno puedes "volver atrás" ni cambiar una celda ya impresa
Matriz primeropuedes consultar y modificar celdasnecesitas 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ácterNúmero
' ' (espacio)32
'\n' (salto de línea)10
'-'45
'o'111
`''`

Consecuencias prácticas:

  • ft_putchar('o') y ft_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 (o y \0). No sirve como argumento de ft_putchar, que espera un solo char.

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:

QuieresEscribís
el primero[0]
el último de n[n - 1]
recorrer todoswhile (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
EtapaProgramaQué haceFlag para frenar ahíSalida
1. Preprocesarcppresuelve #include, #define y borra comentarios-E.i (C todavía legible)
2. Compilarcc1traduce C a ensamblador-S.s (ensamblador)
3. Ensamblarastraduce ensamblador a código máquina-c.o (objeto)
4. Enlazarldune 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

FamiliaCuándo apareceEjemplo real
Error de compilaciónetapas 1–3falta un ;, tipo equivocado, variable sin declarar
Error de enlazado (link)etapa 4la 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 .c no obliga a recompilar los demás (por eso make es 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
PalabraQué esQué hace
gccprogramael driver
-Wallflagactiva el primer grupo de warnings
-Wextraflagactiva un segundo grupo, más estricto
-Werrorflagconvierte cada warning en error
main.c ft_putchar.c rush00.centradaslos tres se compilan y se enlazan juntos
-o rush00flag + valornombre 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:

WarningQué detecta
-Wunused-variabledeclaraste algo y nunca lo usaste
-Wparenthesesif (x = 0) en lugar de ==
-Wreturn-typeuna función int sin return en algún camino
-Wuninitializedusas una variable sin darle valor
-Wimplicit-function-declarationllamas 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:

WarningQué detecta
-Wunused-parameterun parámetro que no usas dentro de la función
-Wsign-comparecomparas un int con un unsigned
-Wtype-limitscomparaciones que nunca pueden ser verdaderas (char > 300)
-Wmissing-field-initializersinicializas 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:

  1. El nombre va inmediatamente después de -o.
  2. 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ándarPermite
c89 / c90declarar variables solo al principio del bloque
c99declarar 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:

  1. Lee el primero, no el último. Un error de sintaxis genera errores en cascada: arreglas el primero y desaparecen diez.
  2. La columna (23:14) te dice dónde; muchas veces el problema está en la línea anterior (un ; olvidado).
  3. El texto entre corchetes ([-Wunused-variable]) es el nombre del warning. Con ese nombre puedes buscarlo en la documentación de GCC.
  4. error detiene la compilación; warning no (salvo con -Werror).

Los comandos que vas a usar de verdad

MomentoComando
Compilar y ejecutargcc -Wall -Wextra -Werror main.c ft_putchar.c rush00.c -o rush00 && ./rush00
Solo verificar sintaxis de un archivogcc -Wall -Wextra -Werror -c rush00.c -o /tmp/rush00.o
Investigar un crashgcc -g -fsanitize=address,undefined main.c ft_putchar.c rush00.c -o debug && ./debug
Ver el preprocesadogcc -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

ErrorPor qué está malCómo se escribe
if (x = 0)asigna, no compara; y la condición da falsoif (x == 0)
int i; y usarlacontiene basuraint i; i = 0;
contar desde 1el último índice es n - 1empezar en 0
while (i <= n)una vuelta de máswhile (i < n)
olvidar i++bucle infinitoi++; en el cuerpo
char c = "o";string en una caja de un carácterchar c = 'o';
write(1, c, 1)falta la direcciónwrite(1, &c, 1)
cambiar el parámetro y esperar que el original cambieC pasa copiasdevolver el valor o usar puntero
tablero[columna][fila]índices invertidostablero[fila][columna]
declarar char m[x][y] con parámetrosel tamaño debe ser constantetamaño fijo, VLA o malloc
exit(0) dentro de una funciónmata el programa enteroreturn ;
función de 60 líneas con 4 niveles de ifno se puede explicarguard clause + funciones chicas

Cómo practicar

En orden de dificultad. Cada uno se resuelve con lo de arriba y nada más.

#EjercicioQué practica
1Imprimir ##### (una fila, sin función auxiliar)bucle simple
2Imprimir 5 filas de #bucle anidado
3Imprimir un cuadrado hueco de 5×5 con #condiciones dentro del bucle
4Lo mismo, pero con ancho y alto por parámetroparámetros
5Extraer el borde a una función propiasubdivisión
6Agregar guard clause para tamaños inválidosearly return
7Llenar una matriz 3×3 con X en la diagonal y . fuera, después imprimirlamatrices
8Contar cuántas o tiene tu rectángulo, sin imprimirloacumulador + 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.