El lenguaje C es uno de los lenguajes de programación más influyentes y ampliamente utilizados. Fue desarrollado por Dennis Ritchie en los Laboratorios Bell y ha servido como base para C++, Objective-C, Java, Go, Rust y muchos otros lenguajes. Este itinerario trabaja con los estándares C17 (ISO/IEC 9899:2018) y C23 (ISO/IEC 9899:2024), que son los soportados por gcc 14, clang 18 y MSVC actuales. A continuación se resumen las características vigentes del lenguaje y el recorrido del temario.
Arquitectura de memoria de un proceso en C
Todo programa en C divide su memoria en cuatro regiones con tiempos de vida y permisos distintos:
flowchart TB
subgraph Proceso["Espacio de direcciones del proceso"]
T["text (código máquina)<br/>sólo lectura, tamaño fijo"]
D["data (globales y static inicializados)<br/>lectura/escritura, tamaño fijo"]
B["bss (globales y static sin inicializar)<br/>puesta a cero al arrancar"]
H["heap (malloc, calloc, realloc)<br/>crece hacia arriba, vida controlada por free"]
S["stack (variables locales, parámetros, return address)<br/>crece hacia abajo, LIFO"]
end
T --> D --> B --> H
S --> H
El stack es automático (alta rotación por llamadas), el heap es manual y exige free, data/bss viven durante toda la ejecución y text contiene las instrucciones del ejecutable. Dominar esta separación es la base para entender punteros, static, malloc y fugas de memoria.
1. Características y fundamentos del lenguaje C
-
Lenguaje compilado:
El código fuente escrito en C se compila a código máquina específico para la arquitectura de la computadora, lo que permite programas eficientes y rápidos. -
Lenguaje estructurado:
Se basa en la descomposición del programa en funciones y módulos, facilitando el diseño, la lectura y el mantenimiento del código. -
Tipado estático y fuerte:
Cada variable en C debe declararse con un tipo específico (por ejemplo,int,float,char), lo que ayuda a detectar errores durante la compilación. -
Acceso de bajo nivel:
Permite manipular directamente la memoria (por ejemplo, mediante punteros), lo que es fundamental en programación de sistemas y desarrollo de controladores, sistemas operativos y software embebido. -
Portabilidad:
Aunque el código debe compilarse para cada plataforma, C fue diseñado con portabilidad en mente, lo que significa que un programa en C puede ser adaptado a diferentes sistemas con pocos cambios.
2. Ciclo de compilación de un programa en C
Del fichero .c al ejecutable pasamos por cuatro fases. gcc y clang orquestan las cuatro pero se pueden invocar por separado para depurar problemas:
flowchart LR
src["hola.c (fuente)"] --> pre["Preprocesador<br/>cpp<br/>#include, #define, macros"]
pre --> comp["Compilador<br/>cc1<br/>análisis y .s (ensamblador)"]
comp --> asm["Ensamblador<br/>as<br/>.s → .o (código objeto)"]
asm --> link["Enlazador<br/>ld/collect2<br/>resuelve símbolos y libc"]
link --> bin["hola (ejecutable ELF/PE)"]
Útil para diagnosticar fallos: gcc -E se detiene tras preprocesador, -S tras compilador, -c tras ensamblador y sin flag hasta el enlazado final. Los errores de "undefined reference" ocurren en la fase de enlazador, los de #include en la de preprocesador.
3. Estructura básica de un programa en C
Todo programa en C tiene una estructura mínima que incluye la función main(), que es el punto de entrada. Además, se utilizan directivas del preprocesador para incluir librerías o definir macros. Un ejemplo básico:
#include <stdio.h> // Inclusión de la librería estándar de entrada/salida
// Función principal: punto de entrada del programa
int main() {
// Instrucción que imprime un mensaje en la consola
printf("Hola, mundo!\n");
return 0; // Indica que el programa terminó correctamente
}
Elementos a notar:
#include <stdio.h>: Directiva del preprocesador que incluye la librería estándar de entrada y salida.int main() { ... }: Definición de la función principal.printf(...): Función para mostrar información en la consola.return 0;: Devuelve un valor al sistema operativo indicando el estado de salida del programa.
4. Sintaxis principal del lenguaje C
Declaración de variables y tipos de datos
C tiene varios tipos de datos básicos:
- Enteros:
int,short,long,unsigned int, etc. - Punto flotante:
float,double. - Caracteres:
char.
Ejemplo de declaración y asignación:
#include <stdio.h>
int main() {
int edad = 25; // Declaración de un entero
float altura = 1.75; // Declaración de un número de punto flotante
char inicial = 'J'; // Declaración de un carácter
printf("Edad: %d, Altura: %.2f, Inicial: %c\n", edad, altura, inicial);
return 0;
}
Formato de impresión:
%dpara enteros.%fo%.2fpara números flotantes (con 2 decimales en este caso).%cpara caracteres.
Operadores
C cuenta con diversos operadores:
- Aritméticos:
+,-,*,/,%(módulo) - Asignación:
=,+=,-=, etc. - Comparación:
==,!=,<,>,<=,>= - Lógicos:
&&(AND),||(OR),!(NOT) - Incremento/Decremento:
++,--
Ejemplo:
#include <stdio.h>
int main() {
int a = 10, b = 3;
int suma = a + b;
int modulo = a % b;
printf("Suma: %d, Módulo: %d\n", suma, modulo);
return 0;
}
Estructuras de control
Sentencias condicionales
If-Else:
#include <stdio.h>
int main() {
int numero = 7;
if (numero % 2 == 0) {
printf("El número es par.\n");
} else {
printf("El número es impar.\n");
}
return 0;
}
Switch:
#include <stdio.h>
int main() {
int opcion = 2;
switch(opcion) {
case 1:
printf("Opción 1 seleccionada.\n");
break;
case 2:
printf("Opción 2 seleccionada.\n");
break;
default:
printf("Opción no reconocida.\n");
break;
}
return 0;
}
2. Bucles o iteraciones
While:
#include <stdio.h>
int main() {
int contador = 0;
while (contador < 5) {
printf("Contador: %d\n", contador);
contador++;
}
return 0;
}
Do-While:
#include <stdio.h>
int main() {
int contador = 0;
do {
printf("Contador: %d\n", contador);
contador++;
} while (contador < 5);
return 0;
}
For:
#include <stdio.h>
int main() {
int i;
for (i = 0; i < 5; i++) {
printf("Valor de i: %d\n", i);
}
return 0;
}
Funciones
Las funciones permiten modularizar el código. Se pueden declarar y definir funciones para realizar tareas específicas.
Ejemplo con una función que suma dos números:
#include <stdio.h>
// Declaración de la función (prototipo)
int sumar(int a, int b);
int main() {
int resultado = sumar(3, 4);
printf("La suma es: %d\n", resultado);
return 0;
}
// Definición de la función
int sumar(int a, int b) {
return a + b;
}
Aspectos a considerar:
- Los prototipos de función son útiles para indicar al compilador la existencia de funciones que se definirán posteriormente.
- Las funciones pueden retornar valores o ser del tipo
voidsi no retornan nada.
Punteros
Los punteros son variables que almacenan direcciones de memoria. Permiten un control muy preciso sobre la memoria, lo que es crucial en aplicaciones de bajo nivel.
Ejemplo básico de punteros:
#include <stdio.h>
int main() {
int var = 20;
int *puntero = &var; // 'puntero' almacena la dirección de 'var'
printf("Valor de var: %d\n", var);
printf("Dirección de var: %p\n", (void*)&var);
printf("Valor almacenado en puntero: %p\n", (void*)puntero);
printf("Valor al que apunta puntero: %d\n", *puntero); // Desreferenciación
return 0;
}
Conceptos importantes:
&: Operador de dirección.*: Operador de desreferenciación, que accede al valor almacenado en la dirección indicada.
Arrays y cadenas de caracteres
Un arreglo es una colección de elementos del mismo tipo. En C, las cadenas de caracteres se manejan como arreglos de char terminados en un carácter nulo \0.
Ejemplo de un arreglo y una cadena:
#include <stdio.h>
int main() {
int numeros[5] = {1, 2, 3, 4, 5}; // Arreglo de enteros
char saludo[] = "Hola"; // Arreglo de caracteres (cadena)
// Recorrido del arreglo de enteros
for (int i = 0; i < 5; i++) {
printf("numero[%d] = %d\n", i, numeros[i]);
}
// Imprimir la cadena
printf("Saludo: %s\n", saludo);
return 0;
}
Estructuras y uniones
Las estructuras permiten agrupar diferentes tipos de datos bajo un mismo nombre, facilitando la organización y manipulación de datos complejos.
Ejemplo de una estructura:
#include <stdio.h>
// Definición de una estructura para representar una persona
struct Persona {
char nombre[50];
int edad;
float altura;
};
int main() {
// Declaración e inicialización de una variable de tipo 'struct Persona'
struct Persona persona1 = {"Juan", 30, 1.80};
printf("Nombre: %s\n", persona1.nombre);
printf("Edad: %d\n", persona1.edad);
printf("Altura: %.2f\n", persona1.altura);
return 0;
}
Las uniones son similares a las estructuras, pero todos sus miembros comparten el mismo espacio de memoria, lo que permite ahorrar memoria cuando solo se usa uno de los miembros a la vez.
Manejo de memoria dinámica
La biblioteca <stdlib.h> provee funciones para gestionar memoria dinámicamente, como malloc, calloc, realloc y free.
Ejemplo de asignación dinámica:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arreglo;
int n = 5;
// Asignar memoria para 5 enteros
arreglo = (int *)malloc(n * sizeof(int));
if (arreglo == NULL) {
printf("Error en la asignación de memoria.\n");
return 1;
}
// Inicializar y mostrar el arreglo
for (int i = 0; i < n; i++) {
arreglo[i] = i * 2;
printf("arreglo[%d] = %d\n", i, arreglo[i]);
}
// Liberar la memoria asignada
free(arreglo);
return 0;
}
Ciclo de vida de un bloque reservado con malloc
stateDiagram-v2
[*] --> Sin_reserva
Sin_reserva --> Reservado: malloc o calloc devuelve puntero no nulo
Sin_reserva --> Error: malloc devuelve NULL (sin memoria)
Reservado --> Reservado: escritura, lectura, realloc (mismo o nuevo bloque)
Reservado --> Liberado: free(puntero)
Liberado --> [*]: el bloque deja de existir
Liberado --> Use_after_free: usar puntero tras free (UB)
Reservado --> Fuga: olvido de free al perder la última referencia
Acceder al bloque tras free es comportamiento indefinido (use-after-free). Perder la última referencia sin liberar produce una fuga detectable con valgrind o -fsanitize=address.
5. Modernización: qué debes usar en C17 y C23
El estándar C17 (la versión "estable" actual) y C23 (publicado en 2024) han reemplazado al legado K&R / C89. Cuando aprendas C hoy, conviene usar directamente:
bool,true,falsenativos (C23): sin necesidad de#include <stdbool.h>.nullptr(C23): constante de puntero nulo con tiponullptr_t, reemplaza la macroNULL.typeofytypeof_unqual(C23): inferencia de tipo para macros genéricas y templates ligeros.constexpr(C23): constantes evaluadas en tiempo de compilación fuera del preprocesador.- Atributos estándar (C23):
[[nodiscard]],[[deprecated]],[[fallthrough]],[[maybe_unused]], con sintaxis unificada. _BitInt(N)(C23): enteros de anchura exacta para criptografía y DSP.#embed(C23): incrustar recursos binarios en el programa sinxxdni scripts auxiliares.- Enum con tipo subyacente (C23):
enum Estado : uint8_t { A, B };. - Initializers designados (C99, pero infrautilizados):
struct Punto p = { .x = 1, .y = 2 };. restricty reglas de strict aliasing: permiten al compilador generar código más eficiente.
Durante el curso mencionamos C89/C99 sólo cuando aporta contexto histórico; los ejemplos y retos usan C17 por defecto y C23 cuando la característica añade claridad.
6. Herramientas del ecosistema actual
| Herramienta | Uso |
|---|---|
gcc 14+ o clang 18+ |
Compiladores con soporte completo de C17 y parcial/total de C23 (-std=c17, -std=c23) |
CMake 3.28+ o Meson |
Sistemas de construcción modernos con soporte multiplataforma (Linux, macOS, Windows) |
-Wall -Wextra -Wpedantic -Werror |
Flags de aviso recomendados en toda compilación educativa y profesional |
-fsanitize=address,undefined |
AddressSanitizer y UBSan: detectan use-after-free, buffer overflow y UB en tiempo de ejecución |
valgrind --leak-check=full |
Detección de fugas y memoria no inicializada en Linux (complementario a ASan) |
cppcheck |
Analizador estático que encuentra bugs sin ejecutar el programa |
clang-tidy |
Analizador estático con reglas modernas (incluye clang-format para estilo) |
gdb o lldb |
Depuradores interactivos, esenciales para entender stack frames y UB |
make o ninja |
Automatización de compilación (ninja es el backend recomendado para CMake moderno) |
Estas herramientas aparecen a lo largo del temario, especialmente en los módulos de Proyectos y herramientas (Makefile, GDB, assert) y de Gestión de memoria (sanitizers).