wandres.dev
APRENDIZ · Setup y toolchain

GCC y Clang comparados

Dos compiladores, dos filosofías: arquitectura interna, calidad de diagnósticos, extensiones propias y estado real del soporte de C23 en 2026. Y por qué un profesional compila siempre con los dos.

⏱ 14 min

GCC y Clang implementan el mismo estándar, pero no son intercambiables. Nacieron de proyectos con objetivos distintos, tienen representaciones internas incompatibles y llegan al mismo binario por caminos que divergen en lo que ven, en lo que avisan y en lo que perdonan. Elegir uno no es cuestión de gusto: es una decisión de ingeniería. Usar los dos a la vez, casi siempre, es la decisión correcta.

🎯 Al terminar esta lección sabrás
  • Distinguir la arquitectura interna de GCC frente a la de Clang.
  • Comparar la calidad, el formato y el alcance de sus diagnósticos.
  • Identificar las extensiones exclusivas de cada compilador.
  • Evaluar el estado real del soporte de C23 y sus divergencias.

Dos linajes, dos arquitecturas

GCC (1987) es un compilador monolítico por diseño. Su cadena interna baja el código fuente a GENERIC, luego a GIMPLE en forma SSA —donde vive casi toda la optimización de alto nivel— y por fin a RTL, muy cercano a la máquina. Esa arquitectura cerrada fue una decisión deliberada del proyecto GNU: dificultar que alguien reutilizara el front-end para construir herramientas propietarias sobre él.

Clang (2007) hizo lo contrario. Es un front-end concebido desde el primer día como biblioteca: construye un AST de alta fidelidad que conserva la forma exacta del código fuente —comentarios, macros, posiciones, paréntesis redundantes— y solo después lo baja a LLVM IR, una representación tipada, textual y reoptimizable.

flowchart LR
S[Codigo fuente C23] --> G[Front-end GCC]
S --> C[Front-end Clang]
G --> GI[GENERIC y GIMPLE SSA]
GI --> R[RTL]
R --> B1[Binario]
C --> AST[AST de alta fidelidad]
AST --> IR[LLVM IR]
IR --> B2[Binario]
AST --> T[clangd clang-tidy clang-format]
IR --> SAN[Sanitizers y LTO]
style T fill:#a6e3a1,color:#11111b
style SAN fill:#a6e3a1,color:#11111b

La consecuencia práctica es enorme y no está en el binario: está en el ecosistema. clangd, clang-tidy, clang-format y los sanitizers existen porque el AST de Clang es reutilizable. GCC ha respondido con -fanalyzer y con complementos, pero el tooling estructural sigue viviendo en LLVM.

Diagnósticos: el terreno donde compiten

Clang popularizó el diagnóstico moderno: cursor de intercalación, rangos subrayados y sugerencias de corrección aplicables (fix-its). GCC alcanzó ese nivel y en algunas áreas lo superó. Hoy la comparación es más fina.

Capacidad GCC Clang
Fix-its aplicables sí, desde GCC 7 sí, con -Xclang -fixit
Análisis interprocedural de rutas -fanalyzer, muy potente clang --analyze y scan-build
Anotaciones de concurrencia no -Wthread-safety
Salida estructurada para CI -fdiagnostics-format=json -fdiagnostics-format=json
Límite de errores -fmax-errors=N -ferror-limit=N

-fanalyzer de GCC es el arma pesada menos conocida del ecosistema: hace análisis simbólico sensible a rutas y detecta doble free, fugas, uso tras liberar y desreferencias nulas sin ejecutar el programa, imprimiendo el camino completo de llamadas que lleva al fallo. -Wthread-safety de Clang no tiene equivalente en GCC: verifica en compilación que accedes a un dato con el mutex correcto cogido, si lo anotas.

Vale la pena ver la divergencia sobre un caso mínimo. Este fragmento tiene una fuga en una sola rama:

char *leer(int n) {
    char *buf = malloc(n);
    if (n > 1024)
        return nullptr;   /* buf se fuga solo por aqui */
    return buf;
}

Con -Wall -Wextra ninguno de los dos dice nada: la fuga no es un error de tipos, es una propiedad del flujo. Solo el análisis de rutas la ve, y cada compilador la presenta a su manera: GCC con -fanalyzer imprime la traza numerada de eventos que conducen a la fuga; Clang con scan-build genera un informe HTML navegable con la ruta resaltada sobre el código. Misma clase de bug, dos ergonomías distintas.

# el mismo fichero ante dos jueces distintos
gcc   -std=c23 -Wall -Wextra -fanalyzer prog.c -o prog_gcc
clang -std=c23 -Wall -Wextra -Wthread-safety prog.c -o prog_clang

# salida estructurada para consumir en CI
gcc -std=c23 -Wall -Wextra -fdiagnostics-format=json -c prog.c 2> avisos.json
💡
Los avisos no son un conjunto, son dos

-Wall -Wextra no activa el mismo conjunto en ambos compiladores, y ninguno de los dos activa todos sus avisos. Clang tiene -Weverything, pensado para explorar el catálogo, no para producción. GCC no tiene equivalente: hay que activar los avisos extra uno a uno.

Extensiones y soporte de C23

Ambos aceptan un núcleo común de extensiones históricas: expresiones-sentencia, __attribute__((cleanup)), __int128, typeof antes de que fuera estándar. Pero cada uno guarda cosas propias:

  • Solo GCC: funciones anidadas, que se implementan con trampolines en la pila y chocan con las políticas de memoria no ejecutable.
  • Solo Clang: los blocks (el operador ^), los calificadores de nulabilidad _Nullable y _Nonnull, y __attribute__((overloadable)).

Compilar con -Wpedantic en los dos es la forma barata de descubrir que dependes de una extensión sin saberlo.

En cuanto a C23, el terreno se ha estabilizado. GCC 14 y Clang 18 aceptan -std=c23; por debajo de esas versiones el nombre era provisional, -std=c2x. GCC 15 llegó incluso a cambiar su dialecto por defecto a gnu23, mientras Clang mantuvo gnu17 durante más tiempo: no asumas nunca el dialecto por defecto, decláralo siempre.

Característica de C23 GCC Clang
nullptr, auto, typeof 13 17
constexpr en objetos 13 19
_BitInt(N) 14 16
Cabecera stdckdint.h 14 18
Directiva #embed 15 19

La forma honesta de saber dónde estás no es consultar una tabla, sino preguntárselo al compilador. C23 estandarizó __has_include y las macros de comprobación de características, así que la detección se escribe una vez y funciona en ambos:

#if __STDC_VERSION__ < 202311L
#  error "Este proyecto exige C23: compila con -std=c23"
#endif

#if defined(__has_embed)
#  define TENGO_EMBED 1
#endif

Comprobar __STDC_VERSION__ en una cabecera común es la disciplina que convierte un fallo confuso de sintaxis en un mensaje de error legible el primer día que alguien compila tu proyecto con un compilador viejo.

La estrategia: compilar con los dos

⚙️

Doble juez en local

Compila con ambos y con -Werror. Cada aviso que uno detecta y el otro no es un bug potencial que habrías enviado a producción.

🧪

Matriz en CI

Cruza compilador por nivel de optimización. Un fallo que solo aparece en clang -O2 casi siempre es comportamiento indefinido, no un bug del compilador.

🧰

Herramientas de Clang, binario de GCC

Nada te obliga a elegir: usa clangd y clang-tidy para editar, y GCC para el binario que envías.

# la matriz mínima, cuatro combinaciones
for cc in gcc clang; do
  for opt in -O0 -O2; do
    "$cc" -std=c23 -Wall -Wextra -Werror "$opt" -g prog.c -o "prog_${cc}${opt}" || echo "FALLO $cc $opt"
  done
done
Dos compiladores son dos modelos del mismo estándar

El estándar de C no define un programa: define una familia de programas válidos y deja enormes zonas de libertad —comportamiento indefinido, no especificado, definido por la implementación—. Un compilador no implementa el estándar: encarna una interpretación concreta de él, con sus propias suposiciones sobre lo que tu código promete no hacer. Cuando compilas con GCC y con Clang estás haciendo algo epistemológicamente más profundo que duplicar avisos: estás contrastando tu código contra dos modelos independientes de la misma especificación. Donde ambos coinciden, tu programa se apoya en el estándar; donde divergen, se apoya en un accidente de implementación, y ese accidente es exactamente el lugar donde el próximo cambio de versión de compilador te romperá el binario en producción. Los bugs más caros de C no son errores de sintaxis: son suposiciones invisibles que un solo compilador valida por casualidad. Un segundo compilador es el revisor más barato que jamás contratarás.

⚔️ Somete tu código a los dos jueces
  1. Coge un fichero .c propio y compílalo con GCC y con Clang usando -std=c23 -Wall -Wextra -Wpedantic. Anota cada aviso que solo aparece en uno.
  2. Ejecuta gcc -fanalyzer sobre un programa con una fuga de memoria deliberada y lee la ruta completa que imprime.
  3. Comprueba el dialecto por defecto de tus compiladores compilando sin -std un fichero que use nullptr.
  4. Escribe algo que use una extensión exclusiva de GCC y observa qué dice Clang con -Wpedantic.
  5. Genera la salida JSON con -fdiagnostics-format=json y examina su estructura: es la base de cualquier integración con CI.