← Programación en C para Sistemas Operativos Bloque 1 · Del código fuente al programa · Tema 2 de 42

Tema 2

El entorno de trabajo

Para programar en C no hace falta un entorno de desarrollo enorme: alcanzan un compilador, la biblioteca de C con sus encabezados, un editor de texto, la terminal y el manual. Pero cada una de esas piezas tiene que estar en su lugar, y hay que saber qué hace cada una. En este tema instalamos y comprobamos las herramientas del curso, vemos dónde guarda el sistema los archivos que usa el compilador, configuramos un editor, compilamos un programa que informa sobre el propio entorno, aprendemos a leer lo que dice el compilador cuando algo sale mal, y a encontrar en el manual la respuesta exacta sobre cualquier función.

1. Las piezas del entorno

En el tema 1 compilamos hola.c con una sola orden, gcc. Detrás de esa orden trabajan varias piezas distintas, y cuando algo falla conviene saber cuál de ellas es la responsable:

PiezaPara qué sirveEn este curso
EditorEscribir el código fuente, que es texto plano.El que prefieras: nano, vim o VS Code.
CompiladorTraducir el fuente a instrucciones de máquina y producir el ejecutable.gcc, de GNU.
EncabezadosDeclarar las funciones de la biblioteca, para que el compilador sepa cómo se llaman y qué devuelven./usr/include, del paquete libc6-dev.
Biblioteca de CContener el código de printf, malloc y las demás funciones, y hacer de puente con el núcleo.glibc, el libc.so.6 que mostraba ldd.
TerminalCompilar, ejecutar, ver la salida y el código de salida.Bash, como en los cursos anteriores.
ManualSaber exactamente qué hace cada función, qué devuelve y qué encabezado necesita.man, secciones 2 y 3.
Herramientas de diagnósticoEjecutar paso a paso, encontrar errores de memoria, ver las llamadas al sistema.gdb, valgrind, strace.

Muchos programadores trabajan con un entorno de desarrollo integrado (IDE), un único programa que junta editor, compilador y depurador detrás de un botón de «ejecutar». En este curso preferimos ver las piezas por separado, desde la terminal: es lo que encontrarás en cualquier servidor, lo que usan los Makefile y los scripts de compilación, y la única forma de entender qué pasa cuando el botón deja de funcionar.

2. Dónde trabajar

Los ejemplos del curso usan Linux, porque los temas que vienen después, procesos, memoria, archivos y bibliotecas, dependen de cómo funciona ese sistema. Hay varias formas de tener un Linux a mano:

OpciónCómo esA tener en cuenta
Linux instaladoLinux en la propia computadora, solo o junto a otro sistema.La opción más directa: todo funciona como en el curso.
Máquina virtualUn Linux completo dentro de VirtualBox, VMware, UTM o similar.Aislado y fácil de rehacer si algo se rompe. Necesita memoria y disco.
WSL 2Un Linux real que corre dentro de Windows: wsl --install -d Ubuntu.Muy cómodo. Guarda los archivos en el Linux (~), no en /mnt/c.
ContenedorUna imagen como debian o ubuntu en Docker o Podman.Útil para compilar; algunas herramientas, como strace o gdb, piden permisos extra.
macOSTiene un compilador de C y una terminal Unix.No es Linux: su gcc es en realidad clang, no hay strace ni /proc. Mejor una máquina virtual.
Si usas WSL

Trabaja dentro del sistema de archivos de Linux, en tu directorio personal, y no en las carpetas de Windows que aparecen bajo /mnt/c. Los archivos de Windows se ven desde Linux a través de una capa de traducción que es mucho más lenta, y que no conserva bien los permisos de Unix. Desde el Explorador de Windows puedes llegar igualmente a tus archivos de Linux escribiendo \\wsl$ en la barra de direcciones.

3. Instalar las herramientas

Cada distribución agrupa las herramientas de desarrollo en un paquete o grupo de paquetes, y les agrega por separado el depurador, el manual de programación y las herramientas de diagnóstico:

terminal · Debian, Ubuntu y derivadas
$ sudo apt update
$ sudo apt install build-essential gdb valgrind strace manpages-dev manpages-posix-dev
terminal · Fedora
$ sudo dnf group install development-tools
$ sudo dnf install gdb valgrind strace man-pages
terminal · Arch Linux
$ sudo pacman -S base-devel gdb valgrind strace man-db man-pages

build-essential no contiene programas propios: es un metapaquete, un paquete vacío cuyo único efecto es depender de otros. apt-cache depends muestra cuáles arrastra:

terminal · lo que trae build-essential
$ apt-cache depends build-essential
build-essential
 |Depends: libc6-dev
  Depends: <libc-dev>
    libc6-dev
  Depends: gcc
  Depends: g++
  Depends: make
    make-guile
  Depends: dpkg-dev
…

Las dos piezas que importan para C son gcc, el compilador, y libc6-dev, los archivos de desarrollo de la biblioteca de C: los encabezados como stdio.h y los archivos que el enlazador necesita para unir el programa con glibc. Esa separación es habitual en Linux: la biblioteca en sí, libc6, está instalada en cualquier sistema porque la usan todos los programas; los paquetes terminados en -dev solo hacen falta para compilar, y por eso no vienen por omisión. Lo mismo ocurre con manpages-dev: sin él, man 3 printf no encuentra la página.

4. Comprobar la instalación

Lo primero es confirmar que el compilador existe, dónde está y qué versión es:

terminal · el compilador
$ gcc --version
gcc (Ubuntu 15.2.0-16ubuntu1) 15.2.0
Copyright (C) 2025 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ which gcc cc
/usr/bin/gcc
/usr/bin/cc
$ ls -l /usr/bin/gcc /usr/bin/cc
lrwxrwxrwx 1 root root 20 feb 23  2026 /usr/bin/cc -> /etc/alternatives/cc
lrwxrwxrwx 1 root root  6 feb 23  2026 /usr/bin/gcc -> gcc-15
$ cc --version | head -n 1
cc (Ubuntu 15.2.0-16ubuntu1) 15.2.0
$ gcc -dumpmachine
x86_64-linux-gnu

Hay tres datos útiles en esa salida. gcc es un enlace simbólico a gcc-15: el sistema puede tener varias versiones instaladas a la vez, y el nombre sin número apunta a la principal. cc, el nombre tradicional del compilador de C en Unix, apunta en Linux a ese mismo gcc; muchas herramientas, como make, usan cc por omisión. Y -dumpmachine dice para qué produce código: procesadores x86_64, sistema linux, biblioteca gnu. Es la misma tripleta que aparece en directorios como /usr/lib/x86_64-linux-gnu.

En lugar de comprobar las herramientas una por una, conviene un script que lo haga todo, como los del curso de Bash. Este busca cada programa con command -v, comprueba que el preprocesador encuentre stdio.h y que el manual tenga la sección 3, y termina con código 1 si falta algo:

verificar.sh
#!/usr/bin/env bash
# verificar.sh — comprueba que estén las herramientas del curso de C
faltan=0

for prog in gcc make gdb valgrind strace man; do
    if command -v "$prog" > /dev/null; then
        printf '  ok     %-9s %s\n' "$prog" "$(command -v "$prog")"
    else
        printf '  FALTA  %s\n' "$prog"
        faltan=$((faltan + 1))
    fi
done

# los encabezados de la biblioteca de C (paquete libc6-dev)
if echo '#include <stdio.h>' | gcc -E - > /dev/null 2>&1; then
    printf '  ok     %-9s %s\n' "stdio.h" "encabezados de la biblioteca de C"
else
    printf '  FALTA  stdio.h (libc6-dev)\n'
    faltan=$((faltan + 1))
fi

# las páginas de manual de programación (paquete manpages-dev)
if man -w 3 printf > /dev/null 2>&1; then
    printf '  ok     %-9s %s\n' "man 3" "páginas de la biblioteca de C"
else
    printf '  FALTA  man 3 printf (manpages-dev)\n'
    faltan=$((faltan + 1))
fi

if (( faltan > 0 )); then
    echo "Herramientas que faltan: $faltan" >&2
    exit 1
fi
echo "Entorno listo."
terminal · un sistema al que le falta valgrind
$ chmod +x verificar.sh
$ ./verificar.sh
  ok     gcc       /usr/bin/gcc
  ok     make      /usr/bin/make
  ok     gdb       /usr/bin/gdb
  FALTA  valgrind
  ok     strace    /usr/bin/strace
  ok     man       /usr/bin/man
  ok     stdio.h   encabezados de la biblioteca de C
  ok     man 3     páginas de la biblioteca de C
Herramientas que faltan: 1
$ echo $?
1

La comprobación de stdio.h usa una opción que veremos en el tema 4: gcc -E ejecuta solo el preprocesador, la etapa que resuelve los #include, sobre un programa de una línea leído de la entrada estándar (-). Si el encabezado no está, el preprocesador falla, y el if lo detecta por el código de salida. Es una prueba más fiel que buscar el archivo, porque comprueba exactamente lo que hará el compilador.

5. Dónde vive cada cosa

Cuando un programa escribe #include <stdio.h>, el compilador busca ese archivo en una lista de directorios predefinida. La opción -v la muestra:

terminal · dónde busca gcc los encabezados
$ echo | gcc -E -v -x c - 2>&1 | sed -n '/search starts here/,/End of search/p'
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-linux-gnu/15/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
$ ls -l /usr/include/stdio.h
-rw-r--r-- 1 root root 34258 sep  3 14:09 /usr/include/stdio.h
$ dpkg -S /usr/include/stdio.h
libc6-dev:amd64: /usr/include/stdio.h

Los cuatro directorios se recorren en orden. El primero tiene los encabezados que trae el propio compilador, como stddef.h o stdarg.h, que dependen de cómo gcc genera el código. /usr/local/include es para lo que instales a mano. Los dos últimos son los del sistema: /usr/include, con stdio.h y casi todos los demás, y un subdirectorio con las partes que dependen del procesador. dpkg -S confirma de qué paquete viene stdio.h: de libc6-dev. Los encabezados son texto, y puedes abrirlos: less /usr/include/stdio.h muestra la declaración de printf, entre muchas otras.

La biblioteca, en cambio, es código ya compilado, y el compilador sabe dónde encontrarla:

terminal · dónde está la biblioteca de C
$ gcc -print-file-name=libc.so.6
/usr/lib/gcc/x86_64-linux-gnu/15/../../../x86_64-linux-gnu/libc.so.6
$ ldd --version | head -n 1
ldd (Ubuntu GLIBC 2.43-2ubuntu2.4) 2.43

Esa ruta, una vez resueltos los .., es /usr/lib/x86_64-linux-gnu/libc.so.6, y la versión de glibc es la 2.43. Retén la diferencia, que volverá muchas veces en el curso: el encabezado le dice al compilador cómo es una función, su nombre, sus parámetros y lo que devuelve; la biblioteca contiene la función de verdad, y se une al programa al enlazar y al ejecutarlo. Los bloques 3 y 8 tratan cada una de esas dos mitades.

6. El editor

Un programa en C es un archivo de texto plano, y cualquier editor de texto sirve para escribirlo. Un procesador de textos como Word o LibreOffice Writer, en cambio, no sirve: guarda formato, y cambia las comillas rectas " por comillas tipográficas que el compilador no entiende. Lo que sí conviene que tenga el editor es resaltado de sintaxis, números de línea, porque los mensajes del compilador citan líneas, y sangría automática.

EditorCómo esPara empezar
nanoEn la terminal, sencillo, con los atajos escritos al pie de la pantalla.nano -l -i -E -T 4 hola.c: números de línea, sangría automática, cuatro espacios en lugar de tabuladores.
vimEn la terminal, muy potente, disponible en cualquier servidor. Requiere aprender sus modos.vimtutor, un tutorial de media hora incluido con vim.
VS CodeGráfico, con terminal integrada y extensiones para C.La extensión C/C++ de Microsoft o clangd. Con WSL, abrirlo con code . desde la terminal de Linux.

Las extensiones de C de VS Code agregan algo que va más allá del resaltado: analizan el código mientras se escribe, subrayan los errores, completan nombres de funciones y muestran su declaración al pasar el cursor. Son muy útiles, pero no reemplazan al compilador: el que decide si el programa es correcto es gcc, con las opciones que le pases, y sus mensajes son los que hay que aprender a leer.

Si usas vim, estas líneas en ~/.vimrc dan una configuración razonable para C:

~/.vimrc
syntax on                   " resaltado de sintaxis
set number                  " números de línea
set autoindent cindent      " sangría automática, con las reglas de C
set expandtab               " espacios en lugar de tabuladores
set tabstop=4 shiftwidth=4  " sangría de cuatro espacios

Y cualquiera que sea el editor, un archivo .editorconfig en el directorio del curso fija las mismas reglas para todos los editores que lo entienden, VS Code y vim entre ellos:

~/c/.editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{c,h}]
indent_style = space
indent_size = 4

[Makefile]
indent_style = tab

Cada regla tiene su motivo. end_of_line = lf evita los finales de línea de Windows, \r\n, que a gcc no le molestan pero rompen los scripts de Bash, como ya viste en el curso anterior. insert_final_newline asegura que el archivo termine con un salto de línea, como espera el estándar de C. Y la excepción para Makefile no es una preferencia: make exige que las órdenes de cada regla empiecen con un tabulador, como veremos en el tema 35.

7. Organizar el trabajo

A lo largo del curso escribirás decenas de programas pequeños. Un directorio por tema evita que se mezclen, y que el hola de un tema pise al de otro:

terminal · la estructura del curso
$ mkdir -p ~/c/tema02 && cd ~/c/tema02
$ tree ~/c
/home/ana/c
├── .editorconfig
├── tema01
│   ├── fuera.c
│   ├── hola.c
│   └── memoria.c
└── tema02
    ├── entorno.c
    └── verificar.sh

Guarda en él solo los fuentes. Los ejecutables se regeneran en un segundo con gcc, de modo que no hace falta conservarlos, ni copiarlos, ni subirlos a un repositorio. Si usas git, como se recomienda, un .gitignore que ignore los ejecutables y los archivos *.o mantiene el repositorio limpio.

8. El ciclo editar, compilar, ejecutar

Con un script de Bash el ciclo tenía dos pasos: editar y ejecutar. Con C tiene tres, y el del medio es el que más información da:

El ciclo de trabajo: en el editor se escribe hola.c; gcc lo compila, usando los encabezados y la biblioteca de C del sistema, y produce el ejecutable hola; si hay errores o advertencias, se vuelve al editor; al ejecutar ./hola se obtiene la salida y el código de salida, que también llevan de vuelta al editor para corregir stdio.h · libc.so.6 lo que aporta el sistema editor escribir hola.c texto gcc compilar hola ejecutable ELF errores y advertencias de gcc ./hola: la salida y el código de salida ($?)
Cada vuelta del ciclo termina en el editor. La primera respuesta llega del compilador, antes de ejecutar nada; la segunda, de la ejecución.

Para recorrerlo, nuestro primer programa del tema informa sobre el propio entorno: qué compilador lo compiló, con qué versión del estándar, contra qué biblioteca de C, y cuánto mide un puntero en esta máquina.

entorno.c
#include <stdio.h>
#include <gnu/libc-version.h>

int main(void)
{
    printf("Compilador:   gcc %d.%d.%d\n", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
    printf("Estándar C:   %ld\n", __STDC_VERSION__);
    printf("Biblioteca C: glibc %s\n", gnu_get_libc_version());
    printf("Puntero:      %zu bytes\n", sizeof(void *));
    return 0;
}
terminal · el mismo fuente, compilado de dos formas
$ gcc -std=c17 -Wall -Wextra -o entorno entorno.c && ./entorno
Compilador:   gcc 15.2.0
Estándar C:   201710
Biblioteca C: glibc 2.43
Puntero:      8 bytes
$ gcc -Wall -Wextra -o entorno entorno.c && ./entorno
Compilador:   gcc 15.2.0
Estándar C:   202311
Biblioteca C: glibc 2.43
Puntero:      8 bytes

El programa mezcla dos tipos de información, y la diferencia es instructiva:

La forma de compilar también tiene su detalle: gcc … && ./entorno ejecuta el programa solo si la compilación terminó con código 0. No es un capricho. Si la compilación falla, gcc no toca el ejecutable anterior, y un ./entorno sin condición ejecutaría la versión vieja, sin tus cambios, como si nada hubiera pasado. Es una de las confusiones más comunes al empezar.

9. Las opciones de gcc

gcc tiene cientos de opciones, pero en el día a día bastan unas pocas. Estas son las que usaremos a lo largo del curso:

OpciónQué haceTema
-o nombrePone nombre al ejecutable. Sin ella, se llama a.out, y cada compilación pisa la anterior.2
-std=c17Elige la versión del estándar. -std=gnu17 agrega las extensiones de GNU.1, 2
-Wall -WextraEncienden la gran mayoría de las advertencias útiles.2, 4
-pedanticAdvierte de todo lo que no sea C estándar estricto.4
-WerrorConvierte cada advertencia en un error: no se produce el ejecutable.2
-gAgrega información de depuración: nombres de variables y números de línea, para gdb.39
-O0, -O2Nivel de optimización: ninguno, el de omisión, o el habitual en producción.4
-fsanitize=address,undefinedAgrega comprobaciones de memoria y de comportamiento indefinido al ejecutar.1, 29
-E, -S, -cDetienen la compilación después del preprocesador, del compilador o del ensamblador.4
-l, -L, -IAgregan una biblioteca y directorios donde buscar bibliotecas o encabezados.32, 33

Para los ejemplos del curso, la línea de referencia es esta, que puedes copiar en cada tema:

terminal · la orden de compilación del curso
$ gcc -std=c17 -Wall -Wextra -g -o programa programa.c

La opción -g agrega unos mil bytes al ejecutable de hola, de 15952 a 17048, con la información que necesita un depurador para relacionar las instrucciones de máquina con las líneas del fuente. No hace al programa más lento, porque esa información no se carga al ejecutarlo, y es mejor tenerla desde el principio que descubrir que falta cuando hay que depurar. file la delata: dice with debug_info, not stripped.

10. Leer los mensajes de error

Al principio, la mayor parte del tiempo de programación en C se pasa leyendo lo que dice el compilador. Sus mensajes son largos, pero siguen siempre la misma forma, y una vez que se aprende a leerla dicen exactamente dónde está el problema. Este programa olvidó el punto y coma al final de printf:

puntoycoma.c
#include <stdio.h>

int main(void)
{
    printf("Hola, sistema operativo\n")      /* falta el ; */
    return 0;
}
terminal
$ gcc -std=c17 -Wall -Wextra -o puntoycoma puntoycoma.c
puntoycoma.c: In function ‘main’:
puntoycoma.c:5:40: error: expected ‘;’ before ‘return’
    5 |     printf("Hola, sistema operativo\n")
      |                                        ^
      |                                        ;
    6 |     return 0;
      |     ~~~~~~
$ echo $?
1

Cada mensaje tiene cuatro partes, en este orden:

  1. El contexto: In function ‘main’ dice en qué función está el problema. En un programa con muchas funciones, es la primera pista. contexto
  2. La ubicación: puntoycoma.c:5:40 es archivo, línea y columna. Los editores entienden ese formato: en VS Code, un clic en él abre el archivo en el lugar exacto. archivo:línea:columna
  3. La gravedad: error impide producir el ejecutable; warning no lo impide, pero señala algo sospechoso; note agrega información sobre el mensaje anterior. error · warning · note
  4. El fragmento: la línea del fuente, con un ^ bajo el punto exacto y, cuando el compilador puede adivinar la corrección, una línea más con lo que falta: aquí, el ;. fragmento

Un detalle importante: el compilador informa donde se dio cuenta del error, que no siempre es donde está. El punto y coma falta en la línea 5, pero el compilador solo puede estar seguro cuando encuentra el return de la línea 6 donde esperaba un ;, y por eso muestra las dos. Cuando un error no tiene sentido en la línea indicada, hay que mirar la anterior. Y cuando hay muchos errores, conviene corregir solo el primero y volver a compilar: a menudo los demás son consecuencias de ese.

El segundo error más común es usar una función sin incluir su encabezado:

terminal · sininclude.c, sin el #include <stdio.h>
$ gcc -std=c17 -Wall -Wextra -o sininclude sininclude.c
sininclude.c: In function ‘main’:
sininclude.c:3:5: error: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
    3 |     printf("Hola, sistema operativo\n");
      |     ^~~~~~
sininclude.c:1:1: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
  +++ |+#include <stdio.h>
    1 | int main(void)

El compilador no sabe qué es printf, porque nadie se lo declaró, y la nota sugiere la solución con la línea exacta que falta, marcada con +. Entre corchetes aparece el nombre de la opción que controla ese diagnóstico, -Wimplicit-function-declaration, que sirve para buscarlo en el manual de gcc. Este caso fue una advertencia durante décadas, y los programas viejos compilaban igual; desde gcc 14 es un error, porque llamar a una función sin declararla produce errores sutiles.

El tercer error es distinto en su naturaleza. Aquí el nombre de la función está mal escrito:

terminal · errata.c, con prinft en lugar de printf
$ gcc -std=c17 -Wall -Wextra -o errata errata.c
errata.c: In function ‘main’:
errata.c:5:5: error: implicit declaration of function ‘prinft’; did you mean ‘printf’? [-Wimplicit-function-declaration]
    5 |     prinft("Hola, sistema operativo\n");
      |     ^~~~~~
      |     printf
$ gcc -std=gnu89 -o errata errata.c
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccuLfRSt.o: in function `main':
errata.c:(.text+0x18): undefined reference to `prinft'
collect2: error: ld returned 1 exit status

Con las reglas actuales, el compilador lo detecta y hasta sugiere el nombre correcto. Pero con -std=gnu89, que imita las reglas de 1989 y permite llamar a funciones no declaradas, el compilador lo deja pasar, y el error aparece en otro lugar: el mensaje ya no tiene número de línea y viene de ld, el enlazador, que no encontró ninguna función llamada prinft para unir al programa. Son dos programas distintos que trabajan uno después del otro, y sus errores se reconocen por la forma:

Error de compilaciónError de enlace
Lo emiteEl compilador, cc1.El enlazador, ld, a través de collect2.
Formaarchivo.c:línea:columna: error: …undefined reference to `nombre'
SignificaEl fuente no es C válido, o usa algo no declarado.Algo se declaró y se usó, pero su código no está en ningún archivo ni biblioteca.
Causas típicasSintaxis, tipos, falta un #include.Nombre mal escrito, falta un archivo .c o una biblioteca -l.

El tema 4 sigue esas etapas una por una, y el tema 31 está dedicado a los símbolos y a los errores de enlace.

11. Las advertencias

Los errores impiden compilar, y por eso no pasan inadvertidos. Las advertencias, en cambio, dejan compilar, y es fácil ignorarlas. Este programa tiene tres problemas, ninguno de los cuales es un error para el lenguaje:

aviso.c
#include <stdio.h>

int main(void)
{
    int total;
    int edad = 30;

    printf("Edad: %d\n", "treinta");
    printf("Total: %d\n", total);
    return 0;
}
terminal · sin opciones, con advertencias y con -Werror
$ gcc -std=c17 -o aviso aviso.c && ./aviso
aviso.c: In function ‘main’:
aviso.c:8:20: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘char *’ [-Wformat=]
    8 |     printf("Edad: %d\n", "treinta");
      |                   ~^     ~~~~~~~~~
      |                    |     |
      |                    int   char *
      |                   %s
Edad: 1658150916
Total: 0
$ gcc -std=c17 -Wall -Wextra -o aviso aviso.c
aviso.c: In function ‘main’:
aviso.c:8:20: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘char *’ [-Wformat=]
…
aviso.c:6:9: warning: unused variable ‘edad’ [-Wunused-variable]
    6 |     int edad = 30;
      |         ^~~~
aviso.c:9:5: warning: ‘total’ is used uninitialized [-Wuninitialized]
    9 |     printf("Total: %d\n", total);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
aviso.c:5:9: note: ‘total’ was declared here
    5 |     int total;
      |         ^~~~~
$ gcc -std=c17 -Wall -Wextra -Werror -o aviso aviso.c
aviso.c:8:20: error: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘char *’ [-Werror=format=]
…
cc1: all warnings being treated as errors

Sin opciones, gcc avisa solo del problema más grave, y el programa compila y se ejecuta: muestra como «edad» un número sin sentido, que en realidad es parte de la dirección de memoria de la cadena "treinta", y como «total» un 0 que es pura casualidad, porque total nunca recibió un valor. Con -Wall -Wextra aparecen los tres problemas: el formato que no coincide con el argumento, la variable sin usar, y la variable usada sin inicializar, que es comportamiento indefinido. Con -Werror, además, la compilación falla y no hay ejecutable.

La regla del curso

Compila siempre con -Wall -Wextra, y no des por terminado un programa mientras quede una advertencia. Casi todas señalan un error real, y las que no, señalan código confuso que conviene aclarar. Un programa que compila «limpio» hace que cualquier advertencia nueva salte a la vista; uno que ya tiene veinte la esconde entre las demás. -Werror es la forma de no tener la tentación de ignorarlas.

12. El manual

El manual de Linux, que ya usaste en los cursos anteriores para los comandos, tiene también una página para cada llamada al sistema y para cada función de la biblioteca de C. Está dividido en secciones numeradas, y para este curso importan sobre todo tres:

SecciónContieneEjemplos
1Comandos de usuario.ls, gcc, printf (el comando)
2Llamadas al sistema, funciones que provee el núcleo.write, open, fork
3Funciones de biblioteca, de glibc y de otras bibliotecas.printf, strlen, malloc
5Formatos de archivo.passwd, proc
7Visiones generales, convenciones y conceptos.signal, man-pages, ascii
8Comandos de administración.mount, ldconfig

El número de sección importa porque un mismo nombre puede estar en varias. whatis, o su equivalente man -f, muestra todas las páginas que hay para un nombre:

terminal · un nombre, varias páginas
$ whatis printf
printf (1)           - Print output based off of the format string and proceeding arg...
printf (3)           - formatted output conversion
$ whatis write
write (2)            - write to a file descriptor

Hay un printf en la sección 1, el comando de la shell que usaste en el curso de Bash, y otro en la 3, la función de C. man printf, sin número, muestra la primera sección donde lo encuentra, que es la 1: la página equivocada. Por eso, para funciones de C, se escribe siempre el número: man 3 printf, man 2 write. Es también la convención para nombrarlas en un texto, printf(3) o write(2), que encontrarás en la documentación y en el resto del curso.

Las páginas de las secciones 2 y 3 tienen siempre la misma estructura, y conviene saber dónde mirar. Esta es la de strlen, completa:

terminal · man 3 strlen
strlen(3)                    Library Functions Manual                    strlen(3)

NAME
       strlen - calculate the length of a string

LIBRARY
       Standard C library (libc, -lc)

SYNOPSIS
       #include <string.h>

       size_t strlen(const char *s);

DESCRIPTION
       The  strlen()  function calculates the length of the string pointed to by s,
       excluding the terminating null byte ('\0').

RETURN VALUE
       The strlen() function returns the number of bytes in the string pointed to by s.

ATTRIBUTES
       │ strlen()                                          │ Thread safety │ MT-Safe │

STANDARDS
       C11, POSIX.1-2008.

HISTORY
       POSIX.1-2001, C89, SVr4, 4.3BSD.

NOTES
       In cases where the input buffer may not contain a terminating null byte,
       strnlen(3) should be used instead.

SEE ALSO
       string(3), strnlen(3), wcslen(3), wcsnlen(3)
ApartadoQué responde
LIBRARYEn qué biblioteca está la función, y qué opción de enlace necesita. La de C, -lc, se agrega sola.
SYNOPSISQué #include hace falta, y la declaración: el tipo de cada parámetro y del resultado. Es lo que hay que copiar al usarla.
DESCRIPTIONQué hace exactamente, incluidos los casos límite.
RETURN VALUEQué devuelve si funciona, y qué si falla. El apartado que más se consulta.
ERRORSEn las funciones que pueden fallar, los valores posibles de errno y qué significa cada uno.
ATTRIBUTESSi es segura para usar desde varios hilos a la vez.
STANDARDSQué estándar la define: si dice C, existe en cualquier sistema; si dice solo POSIX, en los Unix; si dice GNU o Linux, solo aquí.
NOTES, BUGSTrampas conocidas y advertencias. Vale la pena leerlas.
SEE ALSOFunciones relacionadas, a menudo una alternativa más segura.

Las funciones que piden algo al sistema pueden fallar, y para ellas el apartado RETURN VALUE es imprescindible. Este es el de write(2):

terminal · man 2 write, apartado RETURN VALUE
RETURN VALUE
       On success, the number of bytes written is returned.  On error, -1 is
       returned, and errno is set to indicate the error.

       Note that a successful write() may transfer fewer than count bytes.  Such
       partial writes can occur for various reasons; …

Dos frases que dicen mucho: si falla, write devuelve -1 y deja en errno el motivo, la convención de casi todas las llamadas al sistema, que estudiaremos en el tema 37; y aunque funcione, puede escribir menos bytes de los pedidos, algo que un programa correcto tiene que contemplar. Ninguna de las dos cosas se adivina: hay que leerlas en el manual.

Cuando no sabes el nombre de la función, apropos, o man -k, busca una palabra en las descripciones de todas las páginas. Con -s se limita a ciertas secciones:

terminal · buscar en el manual
$ apropos -s 2,3 'directory' | head -n 6
alphasort... (3)     - scan a directory for matching entries
bindtextdomain... (3) - set directory containing message catalogs
chdir... (2)         - change working directory
chroot... (2)        - change root directory
closedir... (3)      - close a directory
dirfd... (3)         - get directory stream file descriptor
$ man -k . -s 2 | wc -l
861
$ man -k . -s 3 | wc -l
1969

Los puntos suspensivos tras un nombre indican que esa página documenta varias funciones juntas: alphasort, por ejemplo, se explica en la misma página que scandir. Y los números son elocuentes: casi novecientas páginas en la sección 2 y casi dos mil en la 3: nadie las conoce todas, y no hace falta. Lo que hace falta es la costumbre de abrir la página de cada función que se usa por primera vez. Mientras lees una, / busca una palabra, como en less, y q sale.

13. Compilar con make, sin Makefile

Escribir la orden completa de gcc cada vez es tedioso. La solución definitiva es un Makefile, del tema 35, pero make ya sabe compilar un programa de un solo archivo sin que se lo expliquen: tiene reglas implícitas, y una de ellas dice que hola se construye a partir de hola.c con el compilador de C.

terminal · make, sin ningún Makefile en el directorio
$ make hola
cc     hola.c   -o hola
$ make hola
make: 'hola' is up to date.
$ export CFLAGS="-std=c17 -Wall -Wextra -g"
$ touch hola.c
$ make hola
cc -std=c17 -Wall -Wextra -g    hola.c   -o hola

Hay tres ideas en esas líneas. make muestra cada orden antes de ejecutarla, y la orden usa cc, que es gcc. La segunda vez no hace nada, porque hola es más nuevo que hola.c: make compara las fechas de modificación, y solo recompila lo que cambió; touch actualiza la fecha de hola.c y obliga a recompilar. Y la regla implícita agrega a la orden el contenido de la variable CFLAGS, que es donde se ponen las opciones del compilador. Si agregas el export CFLAGS=… a tu ~/.bashrc, cada make programa compilará con las opciones del curso:

terminal · las opciones del curso, para siempre
$ echo 'export CFLAGS="-std=c17 -Wall -Wextra -g"' >> ~/.bashrc
$ source ~/.bashrc
$ make entorno && ./entorno
cc -std=c17 -Wall -Wextra -g    entorno.c   -o entorno
Compilador:   gcc 15.2.0
…

14. Problemas frecuentes

Casi todos los tropiezos de los primeros días son de una de estas formas. Leer el mensaje con atención suele bastar para saber de cuál se trata:

Mensaje o síntomaCausaSolución
gcc: command not foundEl compilador no está instalado.Instalar build-essential o el equivalente de tu distribución.
fatal error: stdio.h: No such file or directoryEstá el compilador, pero no los encabezados de la biblioteca.Instalar libc6-dev.
No manual entry for printf in section 3Faltan las páginas de programación.Instalar manpages-dev.
hola: command not foundEl directorio actual no está en el PATH, por seguridad.Ejecutar con la ruta: ./hola.
./hola.c: Permission deniedSe intentó ejecutar el fuente, que es texto, en lugar del ejecutable.Compilar y ejecutar ./hola.
El programa no refleja los cambiosLa compilación falló, y se ejecutó el ejecutable anterior; o se editó un archivo y se compiló otro.Compilar con gcc … && ./programa, y leer los mensajes.
undefined reference to `nombre'Error de enlace: el nombre está mal escrito, o falta un archivo o una biblioteca.Revisar el nombre; temas 4 y 31.
Se generó a.outSe compiló sin -o.Agregar -o nombre, o usar make nombre.
make: 'hola' is up to date.El fuente no cambió desde la última compilación.Nada: es correcto. Para forzarlo, touch hola.c.

El caso de hola: command not found es un viejo conocido del curso de Bash: la shell solo busca los programas en los directorios del PATH, y el directorio actual no está entre ellos, para que nadie pueda dejar en una carpeta un programa llamado ls que se ejecute en lugar del verdadero. El ./ le dice explícitamente dónde está.

15. Resumen

16. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué diferencia hay entre el paquete libc6 y el paquete libc6-dev?

libc6 contiene la biblioteca de C ya compilada, que necesitan todos los programas para ejecutarse, y por eso está en cualquier sistema. libc6-dev contiene lo necesario para compilar programas que la usan: los encabezados como stdio.h y los archivos que usa el enlazador. Solo hace falta para programar.

2. ¿Qué diferencia hay entre un encabezado como stdio.h y la biblioteca libc.so.6?

El encabezado es texto, y declara las funciones: su nombre, sus parámetros y el tipo que devuelven, para que el compilador pueda comprobar las llamadas. La biblioteca contiene el código de las funciones ya compilado, que se une al programa al enlazarlo y al ejecutarlo.

3. ¿Por qué se recomienda compilar con gcc … && ./programa en lugar de dos órdenes separadas?

Porque si la compilación falla, gcc no produce un ejecutable nuevo, pero deja el anterior en su lugar. Con &&, el programa solo se ejecuta si la compilación terminó con código 0; sin él, se ejecutaría la versión vieja, sin los cambios, y parecería que no surtieron efecto.

4. En entorno.c, ¿por qué __STDC_VERSION__ cambia según la opción -std, pero la versión de glibc no?

Porque __STDC_VERSION__ es una macro predefinida por el compilador, que la reemplaza por un valor según el estándar con el que está compilando. La versión de glibc la devuelve una función de la biblioteca mientras el programa corre, y depende de la biblioteca instalada en el sistema, no de cómo se compiló.

5. ¿Qué significan las tres partes de puntoycoma.c:5:40, y por qué el error no siempre está en la línea indicada?

Son el archivo, la línea y la columna donde el compilador detectó el problema. Pero el compilador informa donde se dio cuenta del error, que puede ser después de donde está: si falta un ; al final de una línea, solo lo sabe al leer el comienzo de la siguiente. Si la línea indicada parece correcta, hay que mirar la anterior.

6. ¿Cómo se distingue un error de compilación de un error de enlace?

El de compilación lo emite el compilador, cita un archivo, una línea y una columna del fuente, e indica que el código no es válido o usa algo no declarado. El de enlace lo emite ld, tiene la forma undefined reference to `nombre', termina con ld returned 1 exit status, y significa que una función usada no está en ningún archivo ni biblioteca de los que se unieron al programa.

7. aviso.c compila y se ejecuta sin errores. ¿Qué problemas tiene, y qué opción los muestra?

Pasa una cadena a printf donde el formato %d espera un entero, declara una variable que nunca usa, y usa total sin haberle dado un valor, lo que es comportamiento indefinido. Sin opciones, gcc solo advierte del primero; con -Wall -Wextra advierte de los tres, y con -Werror se niega a producir el ejecutable.

8. ¿Por qué man printf no muestra la página de la función de C, y cómo se pide la correcta?

Porque hay dos páginas con ese nombre: la del comando printf de la shell, en la sección 1, y la de la función de C, en la sección 3. Sin número, man muestra la primera que encuentra, la de la sección 1. La correcta se pide con man 3 printf; whatis printf muestra las dos.

9. ¿En qué apartado de una página del manual se encuentra qué #include necesita una función, y en cuál cómo informa un error?

El #include está en SYNOPSIS, junto con la declaración de la función. Cómo informa un error está en RETURN VALUE, y para las funciones que usan errno, el apartado ERRORS lista sus valores posibles y qué significa cada uno.

10. ¿Cómo puede make hola compilar un programa si en el directorio no hay ningún Makefile?

Porque make tiene reglas implícitas, y una de ellas dice cómo construir un ejecutable hola a partir de un fuente hola.c: con cc, agregando las opciones de la variable CFLAGS. Además compara las fechas de los dos archivos, y solo compila si el fuente es más nuevo que el ejecutable.

Para practicar

  1. Instala las herramientas, guarda verificar.sh, y ejecútalo hasta que diga Entorno listo. Luego agrégale una comprobación para git y otra que verifique que gcc sea al menos la versión 11, usando gcc -dumpversion.
  2. Compila entorno.c con -std=c99, -std=c11, -std=c17 y -std=c23, y anota el valor de __STDC_VERSION__ en cada caso. Prueba también -std=c89: ¿qué dice el compilador, y por qué?
  3. Abre /usr/include/stdio.h con less y busca la declaración de printf con /printf. Compárala con el SYNOPSIS de man 3 printf.
  4. Introduce a propósito en hola.c cada uno de estos errores, de a uno, y lee el mensaje completo: borrar la llave final, escribir Main en lugar de main, quitar las comillas finales de la cadena, y escribir retrun 0;. ¿Cuáles son errores de compilación y cuáles de enlace?
  5. Con whatis, averigua en qué secciones hay páginas para open, exit, kill, stat y time. ¿Cuáles son llamadas al sistema, cuáles funciones de biblioteca y cuáles comandos?
  6. Lee man 3 malloc y responde con el manual: ¿qué encabezado necesita?, ¿qué devuelve si no puede reservar la memoria?, ¿qué valor puede quedar en errno? Usa apropos para encontrar la función que libera esa memoria.
  7. Agrega CFLAGS a tu ~/.bashrc, compila aviso.c con make aviso y corrige el programa hasta que compile sin ninguna advertencia, también con -Werror.