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:
| Pieza | Para qué sirve | En este curso |
|---|---|---|
| Editor | Escribir el código fuente, que es texto plano. | El que prefieras: nano, vim o VS Code. |
| Compilador | Traducir el fuente a instrucciones de máquina y producir el ejecutable. | gcc, de GNU. |
| Encabezados | Declarar 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 C | Contener 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. |
| Terminal | Compilar, ejecutar, ver la salida y el código de salida. | Bash, como en los cursos anteriores. |
| Manual | Saber exactamente qué hace cada función, qué devuelve y qué encabezado necesita. | man, secciones 2 y 3. |
| Herramientas de diagnóstico | Ejecutar 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ón | Cómo es | A tener en cuenta |
|---|---|---|
| Linux instalado | Linux en la propia computadora, solo o junto a otro sistema. | La opción más directa: todo funciona como en el curso. |
| Máquina virtual | Un Linux completo dentro de VirtualBox, VMware, UTM o similar. | Aislado y fácil de rehacer si algo se rompe. Necesita memoria y disco. |
| WSL 2 | Un Linux real que corre dentro de Windows: wsl --install -d Ubuntu. | Muy cómodo. Guarda los archivos en el Linux (~), no en /mnt/c. |
| Contenedor | Una imagen como debian o ubuntu en Docker o Podman. | Útil para compilar; algunas herramientas, como strace o gdb, piden permisos extra. |
| macOS | Tiene 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. |
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:
$ sudo apt update
$ sudo apt install build-essential gdb valgrind strace manpages-dev manpages-posix-dev
$ sudo dnf group install development-tools
$ sudo dnf install gdb valgrind strace man-pages
$ 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:
$ 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:
$ 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:
#!/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."
$ 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:
$ 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:
$ 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.
| Editor | Cómo es | Para empezar |
|---|---|---|
| nano | En 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. |
| vim | En la terminal, muy potente, disponible en cualquier servidor. Requiere aprender sus modos. | vimtutor, un tutorial de media hora incluido con vim. |
| VS Code | Grá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:
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:
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:
$ 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:
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.
#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;
}
$ 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:
- Lo que sabe el compilador.
__GNUC__y__STDC_VERSION__no son variables, sino macros predefinidas: nombres que el propio compilador reemplaza por un valor antes de compilar.__STDC_VERSION__dice qué versión del estándar se está usando, como año y mes:201710es C17, y202311es C23. Por eso cambia según la opción-std: sin ella,gcc15 usa C23, como vimos en el tema 1. - Lo que sabe la biblioteca.
gnu_get_libc_version()es una función de glibc, declarada engnu/libc-version.h, que devuelve su versión mientras el programa corre. No es parte del estándar de C, sino una extensión de GNU: el programa no compilaría con otra biblioteca de C. - Lo que depende de la máquina.
sizeof(void *)vale 8 porque el sistema es de 64 bits: una dirección ocupa 8 bytes. En un sistema de 32 bits valdría 4. El tema 5 trata los tamaños con detalle.
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ón | Qué hace | Tema |
|---|---|---|
-o nombre | Pone nombre al ejecutable. Sin ella, se llama a.out, y cada compilación pisa la anterior. | 2 |
-std=c17 | Elige la versión del estándar. -std=gnu17 agrega las extensiones de GNU. | 1, 2 |
-Wall -Wextra | Encienden la gran mayoría de las advertencias útiles. | 2, 4 |
-pedantic | Advierte de todo lo que no sea C estándar estricto. | 4 |
-Werror | Convierte cada advertencia en un error: no se produce el ejecutable. | 2 |
-g | Agrega información de depuración: nombres de variables y números de línea, para gdb. | 39 |
-O0, -O2 | Nivel de optimización: ninguno, el de omisión, o el habitual en producción. | 4 |
-fsanitize=address,undefined | Agrega comprobaciones de memoria y de comportamiento indefinido al ejecutar. | 1, 29 |
-E, -S, -c | Detienen la compilación después del preprocesador, del compilador o del ensamblador. | 4 |
-l, -L, -I | Agregan 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:
$ 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:
#include <stdio.h>
int main(void)
{
printf("Hola, sistema operativo\n") /* falta el ; */
return 0;
}
$ 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:
- El contexto:
In function ‘main’dice en qué función está el problema. En un programa con muchas funciones, es la primera pista. contexto - La ubicación:
puntoycoma.c:5:40es 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 - La gravedad:
errorimpide producir el ejecutable;warningno lo impide, pero señala algo sospechoso;noteagrega información sobre el mensaje anterior. error · warning · note - 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:
$ 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:
$ 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ón | Error de enlace | |
|---|---|---|
| Lo emite | El compilador, cc1. | El enlazador, ld, a través de collect2. |
| Forma | archivo.c:línea:columna: error: … | undefined reference to `nombre' |
| Significa | El 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ípicas | Sintaxis, 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:
#include <stdio.h>
int main(void)
{
int total;
int edad = 30;
printf("Edad: %d\n", "treinta");
printf("Total: %d\n", total);
return 0;
}
$ 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.
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ón | Contiene | Ejemplos |
|---|---|---|
1 | Comandos de usuario. | ls, gcc, printf (el comando) |
2 | Llamadas al sistema, funciones que provee el núcleo. | write, open, fork |
3 | Funciones de biblioteca, de glibc y de otras bibliotecas. | printf, strlen, malloc |
5 | Formatos de archivo. | passwd, proc |
7 | Visiones generales, convenciones y conceptos. | signal, man-pages, ascii |
8 | Comandos 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:
$ 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:
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)
| Apartado | Qué responde |
|---|---|
LIBRARY | En qué biblioteca está la función, y qué opción de enlace necesita. La de C, -lc, se agrega sola. |
SYNOPSIS | Qué #include hace falta, y la declaración: el tipo de cada parámetro y del resultado. Es lo que hay que copiar al usarla. |
DESCRIPTION | Qué hace exactamente, incluidos los casos límite. |
RETURN VALUE | Qué devuelve si funciona, y qué si falla. El apartado que más se consulta. |
ERRORS | En las funciones que pueden fallar, los valores posibles de errno y qué significa cada uno. |
ATTRIBUTES | Si es segura para usar desde varios hilos a la vez. |
STANDARDS | Qué 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, BUGS | Trampas conocidas y advertencias. Vale la pena leerlas. |
SEE ALSO | Funciones 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):
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:
$ 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.
$ 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:
$ 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íntoma | Causa | Solución |
|---|---|---|
gcc: command not found | El compilador no está instalado. | Instalar build-essential o el equivalente de tu distribución. |
fatal error: stdio.h: No such file or directory | Está el compilador, pero no los encabezados de la biblioteca. | Instalar libc6-dev. |
No manual entry for printf in section 3 | Faltan las páginas de programación. | Instalar manpages-dev. |
hola: command not found | El directorio actual no está en el PATH, por seguridad. | Ejecutar con la ruta: ./hola. |
./hola.c: Permission denied | Se intentó ejecutar el fuente, que es texto, en lugar del ejecutable. | Compilar y ejecutar ./hola. |
| El programa no refleja los cambios | La 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.out | Se 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
- El entorno de C son piezas separadas: editor, compilador, encabezados, biblioteca de C, terminal, manual y herramientas de diagnóstico.
- El curso usa Linux: instalado, en una máquina virtual, en WSL 2 o en un contenedor. En WSL, los archivos van en
~, no en/mnt/c. - En Debian y Ubuntu,
build-essentialtraegcc,makeylibc6-dev;gdb,valgrind,straceymanpages-devse instalan aparte. - Los paquetes
-devcontienen lo necesario para compilar contra una biblioteca, como sus encabezados; la biblioteca en sí ya está en el sistema. - Los encabezados del sistema están en
/usr/includey declaran las funciones; la biblioteca,libc.so.6, contiene su código. - El editor debe guardar texto plano; conviene configurarlo con números de línea, sangría de espacios y finales de línea de Unix, por ejemplo con un
.editorconfig. - La orden de compilación del curso es
gcc -std=c17 -Wall -Wextra -g -o programa programa.c, y se ejecuta con&& ./programapara no correr un ejecutable viejo. - Los mensajes de
gcctienen la formaarchivo:línea:columna: gravedad: mensaje. Hay que corregir el primero, y mirar la línea anterior si la indicada parece correcta. - Los errores de compilación citan una línea del fuente; los de enlace, como
undefined reference, vienen deld. - Las advertencias no impiden compilar, pero casi siempre señalan un error real;
-Werrorlas convierte en errores. - Las funciones de C se consultan con el número de sección:
man 2 write,man 3 printf.SYNOPSISdice qué incluir yRETURN VALUE, qué devuelve y cómo informa los errores. make programacompilaprograma.csin necesidad de unMakefile, con las opciones de la variableCFLAGS.
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
- Instala las herramientas, guarda
verificar.sh, y ejecútalo hasta que digaEntorno listo.Luego agrégale una comprobación paragity otra que verifique quegccsea al menos la versión 11, usandogcc -dumpversion. - Compila
entorno.ccon-std=c99,-std=c11,-std=c17y-std=c23, y anota el valor de__STDC_VERSION__en cada caso. Prueba también-std=c89: ¿qué dice el compilador, y por qué? - Abre
/usr/include/stdio.hconlessy busca la declaración deprintfcon/printf. Compárala con elSYNOPSISdeman 3 printf. - Introduce a propósito en
hola.ccada uno de estos errores, de a uno, y lee el mensaje completo: borrar la llave final, escribirMainen lugar demain, quitar las comillas finales de la cadena, y escribirretrun 0;. ¿Cuáles son errores de compilación y cuáles de enlace? - Con
whatis, averigua en qué secciones hay páginas paraopen,exit,kill,statytime. ¿Cuáles son llamadas al sistema, cuáles funciones de biblioteca y cuáles comandos? - Lee
man 3 mallocy responde con el manual: ¿qué encabezado necesita?, ¿qué devuelve si no puede reservar la memoria?, ¿qué valor puede quedar enerrno? Usaapropospara encontrar la función que libera esa memoria. - Agrega
CFLAGSa tu~/.bashrc, compilaaviso.cconmake avisoy corrige el programa hasta que compile sin ninguna advertencia, también con-Werror.