Programación · n.º 11 entre los lenguajes más usados

Aprende C desde cero

El lenguaje de los sistemas operativos: cerca del hardware y muy eficiente.

  • 15 lecciones
  • 19 ejercicios interactivos
  • 19 conceptos clave
  • 21,8 % lo usa*

Qué es C y para qué sirve

C es un lenguaje pequeño y muy cercano a la máquina. El núcleo de Linux, gran parte de Windows, las bases de datos o el firmware de muchos aparatos están escritos en C. Aprenderlo te enseña cómo funciona de verdad la memoria de un ordenador, aunque también te deja cometer errores que otros lenguajes impiden.

  • Sistemas operativos y drivers
  • Microcontroladores y dispositivos
  • Bibliotecas que usan otros lenguajes

Para empezar: Instala GCC o Clang (en Windows, con WSL o MSYS2) y compila cada ejemplo con gcc main.c -o main.

Nivel 1 Básico

Lección 1 · Básico

Tu primer programa

Todo programa en C empieza en main. #include <stdio.h> incluye las declaraciones de entrada y salida, entre ellas printf, que escribe texto con formato.

El compilador convierte el archivo .c en un ejecutable; \n es el salto de línea.

Ejemplo práctico
/* main.c */
#include <stdio.h>

int main(void) {
    printf("¡Hola, mundo!\n");
    printf("%d\n", 2 + 3);   /* 5 */
    return 0;
}

/* gcc -Wall main.c -o main && ./main */

Ejercicio

Completa la función que escribe texto con formato.

("Hola\n");
Ver la solución

Respuesta: printf. printf escribe en la salida estándar sustituyendo los marcadores de formato.

Lección 2 · Básico

Variables, tipos y formatos

Los tipos básicos son int (enteros), double (decimales) y char (un carácter). Cada variable se declara con su tipo y conviene darle un valor inicial.

En printf, cada tipo tiene su marcador: %d para int, %f para double (con %.2f para dos decimales), %c para un carácter y %s para una cadena.

Ejemplo práctico
#include <stdio.h>

int main(void) {
    int unidades = 3;
    double precio = 19.99;
    char talla = 'M';

    printf("%d unidades de talla %c: %.2f euros\n", unidades, talla, unidades * precio);
    /* 3 unidades de talla M: 59.97 euros */
    return 0;
}

Ejercicio

¿Qué marcador de printf muestra un double con dos decimales?

Ver la solución

Respuesta: %.2f. %f es para decimales y .2 limita la salida a dos cifras tras la coma.

Lección 3 · Básico

Condiciones y bucles

if/else, switch, for, while y do…while controlan el flujo. En C no existe un tipo booleano clásico: 0 es falso y cualquier otro valor es verdadero (desde C99 puedes usar bool con <stdbool.h>).

Ojo con confundir = (asignar) con == (comparar) dentro de un if: el compilador con -Wall te avisará.

Ejemplo práctico
#include <stdio.h>

int main(void) {
    for (int i = 1; i <= 10; i++) {
        if (i % 3 == 0) {
            printf("%d es múltiplo de 3\n", i);
        }
    }
    return 0;
}

Ejercicio

En C, ¿qué valor se considera «falso» en una condición?

Ver la solución

Respuesta: 0. Solo el 0 es falso; cualquier otro número cuenta como verdadero.

Encuentra los errores

Este programa debería mostrar «Vuelta» 5 veces (y nada más, porque n no es cero). Tiene 2 errores de un solo carácter.

Ver la solución
  • Línea 5: n = 0 asigna 0 a n (y la condición es falsa), así que el bucle ya no daría ninguna vuelta. Para comparar se usa ==. Corrección: if (n == 0) {
  • Línea 8: El ; al final del for es un bucle vacío: da sus vueltas sin hacer nada y el bloque de llaves se ejecuta una sola vez después. Corrección: for (int i = 0; i < n; i++)

Lección 4 · Básico

Arrays y cadenas de texto

Un array es un bloque de elementos del mismo tipo con tamaño fijo. C no comprueba los límites: leer o escribir fuera del array es un error grave que puede colgar el programa o abrir un fallo de seguridad.

Una cadena es un array de char que termina con el carácter nulo '\0'. Las funciones de <string.h>, como strlen, cuentan hasta ese carácter.

Ejemplo práctico
#include <stdio.h>
#include <string.h>

int main(void) {
    int notas[3] = {7, 8, 9};
    int suma = 0;
    for (int i = 0; i < 3; i++) suma += notas[i];
    printf("Media: %.1f\n", suma / 3.0);   /* 8.0 */

    char nombre[] = "Ana";                  /* 'A' 'n' 'a' '\0' */
    printf("%s tiene %zu letras\n", nombre, strlen(nombre));
    return 0;
}

Ejercicio

¿Cuántos char ocupa la cadena "Ana" en memoria?

Ver la solución

Respuesta: 4. Las tres letras más el carácter nulo '\0' que marca el final.

Escribe el código

Escribe int contarVocales(const char *texto): devuelve cuántas vocales minúsculas sin tilde (a, e, i, o, u) tiene el texto. Recórrelo hasta el carácter '\0' que marca el final.

Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.

Ver la solución
int contarVocales(const char *texto) {
    int cuenta = 0;
    for (int i = 0; texto[i] != '\0'; i++) {
        char c = texto[i];
        if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
            cuenta++;
        }
    }
    return cuenta;
}

En C un texto es un array de char que termina en '\0': el bucle avanza hasta encontrarlo. También se podría usar strchr("aeiou", c) para comprobar la vocal.

Nivel 2 Intermedio

Lección 5 · Intermedio

Funciones

Una función se declara con su tipo de retorno, su nombre y sus parámetros. Si la usas antes de definirla, escribe antes su prototipo (la primera línea terminada en punto y coma).

En C los argumentos siempre se pasan por valor: la función recibe una copia.

Ejemplo práctico
#include <stdio.h>

double con_iva(double precio, double iva);   /* prototipo */

int main(void) {
    printf("%.2f\n", con_iva(100, 21));      /* 121.00 */
    return 0;
}

double con_iva(double precio, double iva) {
    return precio * (1 + iva / 100);
}

Ejercicio

¿Para qué sirve el prototipo de una función?

Ver la solución

Respuesta: Para que el compilador la conozca antes de su definición. El prototipo anuncia el nombre, los parámetros y el tipo de retorno antes de que aparezca el cuerpo.

Escribe el código

Escribe long potencia(int base, int exponente): devuelve base elevado a exponente (que nunca es negativo) multiplicando en un bucle, sin usar pow. Recuerda que cualquier número elevado a 0 es 1.

Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.

Ver la solución
long potencia(int base, int exponente) {
    long resultado = 1;
    for (int i = 0; i < exponente; i++) {
        resultado *= base;
    }
    return resultado;
}

Empezando en 1 y multiplicando por la base exponente veces se obtiene la potencia; si el exponente es 0, el bucle no se ejecuta y queda 1.

Conceptos clave

Lección 6 · Intermedio

Punteros

Un puntero guarda una dirección de memoria. &x da la dirección de x y *p accede al valor al que apunta p.

Pasando punteros, una función puede modificar variables de quien la llama, algo imposible con la copia normal.

Ejemplo práctico
#include <stdio.h>

void intercambiar(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void) {
    int x = 1, y = 2;
    intercambiar(&x, &y);
    printf("x=%d y=%d\n", x, y);   /* x=2 y=1 */
    return 0;
}

Ejercicio

Completa para pasar la dirección de x.

intercambiar(x, &y);
Ver la solución

Respuesta: &. &x es la dirección de memoria de x, que es lo que espera un parámetro int *.

Conceptos clave

Lección 7 · Intermedio

Estructuras (struct)

Un struct agrupa varios datos relacionados bajo un nombre, como los campos de una ficha. Accedes a cada campo con un punto, o con -> si tienes un puntero a la estructura.

Con typedef le das un nombre corto al tipo.

Ejemplo práctico
#include <stdio.h>

typedef struct {
    char nombre[32];
    double precio;
    int stock;
} Producto;

void reponer(Producto *p, int unidades) {
    p->stock += unidades;
}

int main(void) {
    Producto teclado = {"Teclado", 25.90, 2};
    reponer(&teclado, 5);
    printf("%s: %d unidades\n", teclado.nombre, teclado.stock);   /* 7 */
    return 0;
}

Ejercicio

Si p es un puntero a un Producto, ¿cómo lees su stock?

Ver la solución

Respuesta: p->stock. p->stock es la forma corta de (*p).stock.

Encuentra los errores

Este programa debería mostrar «Ana: 7.5», pero no compila. Tiene 2 errores.

Ver la solución
  • Línea 7: La declaración de una struct termina con punto y coma después de la llave. Corrección: };
  • Línea 11: En C no se puede asignar un texto a un array con =; hay que copiarlo carácter a carácter, por ejemplo con strcpy (o mejor snprintf, que no se pasa del tamaño). Corrección: strcpy(a.nombre, "Ana");

Nivel 3 Avanzado

Lección 8 · Avanzado

Memoria dinámica: malloc y free

Cuando no sabes el tamaño hasta que el programa se ejecuta, pides memoria con malloc (de <stdlib.h>). Devuelve NULL si no hay memoria, así que compruébalo siempre.

Lo que reservas con malloc hay que liberarlo con free exactamente una vez. Olvidarlo deja la memoria ocupada (fuga), y usarla después de liberarla es un error grave.

Ejemplo práctico
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t n = 5;
    int *cuadrados = malloc(n * sizeof *cuadrados);
    if (cuadrados == NULL) return 1;

    for (size_t i = 0; i < n; i++) cuadrados[i] = (int)(i * i);
    printf("%d\n", cuadrados[4]);   /* 16 */

    free(cuadrados);
    cuadrados = NULL;
    return 0;
}

Ejercicio

Completa la función que libera la memoria reservada.

(cuadrados);
Ver la solución

Respuesta: free. free devuelve al sistema la memoria obtenida con malloc.

Lección 9 · Avanzado

Leer y escribir archivos

fopen abre un archivo y devuelve un FILE * (o NULL si falla). Con fprintf escribes y con fgets lees línea a línea; fclose lo cierra.

El modo "w" crea o vacía el archivo, "a" añade al final y "r" lo abre para leer.

Ejemplo práctico
#include <stdio.h>

int main(void) {
    FILE *f = fopen("notas.txt", "w");
    if (f == NULL) { perror("fopen"); return 1; }
    fprintf(f, "Comprar pan\nLlamar al médico\n");
    fclose(f);

    char linea[128];
    f = fopen("notas.txt", "r");
    if (f == NULL) { perror("fopen"); return 1; }
    while (fgets(linea, sizeof linea, f) != NULL) printf("- %s", linea);
    fclose(f);
    return 0;
}

Ejercicio

¿Qué modo de fopen añade texto al final sin borrar el contenido?

Ver la solución

Respuesta: "a". "a" (append) abre para añadir al final; "w" vacía el archivo.

Lección 10 · Avanzado

El preprocesador y varios archivos

Antes de compilar, el preprocesador procesa las líneas que empiezan por #: #include pega el contenido de otro archivo y #define crea constantes o macros.

Los programas grandes se reparten en archivos .c con sus cabeceras .h, que solo contienen declaraciones. Las guardas #ifndef evitan incluir dos veces la misma cabecera.

Ejemplo práctico
/* matematicas.h */
#ifndef MATEMATICAS_H
#define MATEMATICAS_H
#define PI 3.14159265358979
double area_circulo(double radio);
#endif

/* matematicas.c */
#include "matematicas.h"
double area_circulo(double radio) { return PI * radio * radio; }

/* main.c */
#include <stdio.h>
#include "matematicas.h"
int main(void) { printf("%.2f\n", area_circulo(1)); return 0; }   /* 3.14 */

/* gcc main.c matematicas.c -o main */

Ejercicio

¿Qué contiene normalmente un archivo .h?

Ver la solución

Respuesta: Declaraciones que comparten varios archivos .c. Las cabeceras declaran funciones, tipos y constantes; las definiciones van en los .c.

Lección 11 · Avanzado

Enumeraciones y switch

Un enum da nombre a un conjunto de valores enteros relacionados: enum Semaforo { ROJO, AMBAR, VERDE };. Por defecto valen 0, 1, 2…, aunque puedes fijar otros (ALTO = 10). El código se entiende mejor que con números sueltos.

Encajan muy bien con switch. Compilando con -Wall, el compilador avisa si un switch sobre un enum no trata alguno de sus valores.

Ejemplo práctico
#include <stdio.h>

enum Semaforo { ROJO, AMBAR, VERDE };

const char *accion(enum Semaforo s) {
    switch (s) {
        case ROJO:  return "Para";
        case AMBAR: return "Precaución";
        case VERDE: return "Adelante";
    }
    return "Desconocido";
}

int main(void) {
    for (int s = ROJO; s <= VERDE; s++) {
        printf("%d: %s\n", s, accion(s));
    }
    return 0;
}
// 0: Para
// 1: Precaución
// 2: Adelante

Ejercicio

Con enum Talla { PEQUENA, MEDIANA, GRANDE };, ¿cuánto vale GRANDE?

Ver la solución

Respuesta: 2. Si no se indica otra cosa, el primer valor es 0 y cada uno suma 1: PEQUENA = 0, MEDIANA = 1, GRANDE = 2.

Lección 12 · Avanzado

Recursividad

Una función recursiva se llama a sí misma con un problema más pequeño, hasta llegar a un caso tan sencillo que se resuelve directamente (el caso base). Sin caso base, se llamaría sin fin hasta agotar la memoria de la pila.

Hay problemas que se expresan de forma muy natural así, como recorrer carpetas o estructuras en árbol. Para cálculos sencillos, un bucle suele ser más eficiente, pero la idea es la misma.

Ejemplo práctico
#include <stdio.h>

long factorial(int n) {
    if (n <= 1) {          // caso base
        return 1;
    }
    return n * factorial(n - 1);   // problema más pequeño
}

int contarDigitos(int n) {
    if (n < 10) return 1;
    return 1 + contarDigitos(n / 10);
}

int main(void) {
    printf("%ld\n", factorial(5));         // 120
    printf("%d\n", contarDigitos(20260));  // 5
    return 0;
}

Escribe el código

Escribe la función recursiva int sumaDigitos(int n): devuelve la suma de las cifras de n (que no es negativo), por ejemplo, 2026 → 10. Sin bucles: el caso base es un número de una sola cifra.

Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.

Ver la solución
int sumaDigitos(int n) {
    if (n < 10) {
        return n;
    }
    return n % 10 + sumaDigitos(n / 10);
}

n % 10 es la última cifra y n / 10 quita esa cifra. Cada llamada suma una cifra y pasa el resto del número, hasta que solo queda una.

Lección 13 · Avanzado

Leer datos con seguridad: fgets y strtol

scanf("%s", texto) no sabe cuánto cabe en el array y, si escribes de más, desborda la memoria. fgets(texto, sizeof texto, stdin) lee una línea completa sin pasarse del tamaño indicado (y guarda también el salto de línea, que conviene quitar).

Para convertir el texto en número, strtol dice además dónde dejó de leer: si no avanzó o quedaron caracteres, el dato no era un número válido. atoi devolvería 0 sin avisar.

Ejemplo práctico
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    char linea[32];
    printf("¿Cuántos años tienes? ");
    if (fgets(linea, sizeof linea, stdin) == NULL) return 1;
    linea[strcspn(linea, "\n")] = '\0';      // quitar el salto de línea

    char *fin;
    long edad = strtol(linea, &fin, 10);
    if (fin == linea || *fin != '\0' || edad < 0 || edad > 130) {
        printf("«%s» no es una edad válida\n", linea);
        return 1;
    }
    printf("El año que viene tendrás %ld\n", edad + 1);
    return 0;
}

Ejercicio

¿Por qué se prefiere fgets a scanf("%s", texto) para leer texto?

Ver la solución

Respuesta: Porque nunca escribe más caracteres de los que caben en el array. fgets recibe el tamaño del array y se detiene antes de desbordarlo; un desbordamiento puede corromper otros datos o abrir un agujero de seguridad.

Lección 14 · Avanzado

Operadores de bits

Cada bit de un número puede funcionar como un interruptor. Con constantes que son potencias de 2 (1 << 0, 1 << 1…) guardas varias opciones en un solo entero: es como se representan los permisos de un archivo o los ajustes de un dispositivo.

| (o) activa bits, & (y) comprueba si están activos, & con ~ (negación) los apaga y ^ (o exclusivo) los invierte. << y >> desplazan los bits a izquierda o derecha.

Ejemplo práctico
#include <stdio.h>

#define LECTURA   (1 << 0)   // 1
#define ESCRITURA (1 << 1)   // 2
#define EJECUCION (1 << 2)   // 4

int main(void) {
    int permisos = LECTURA;

    permisos |= ESCRITURA;                  // activar: 1 | 2 = 3
    printf("%d\n", permisos);               // 3

    if (permisos & ESCRITURA) printf("Se puede escribir\n");

    permisos &= ~LECTURA;                   // desactivar la lectura
    printf("%d\n", permisos);               // 2

    permisos ^= EJECUCION;                  // invertir: ahora sí
    printf("%d\n", permisos);               // 6
    return 0;
}

Ejercicio

¿Qué instrucción activa el permiso de ejecución sin tocar los demás?

Ver la solución

Respuesta: permisos |= EJECUCION;. |= pone a 1 los bits de EJECUCION y deja los demás como estaban. Con = se borrarían los otros permisos y con &= se apagarían todos menos ese.

Lección 15 · Avanzado

Depurar errores de memoria

Los errores de memoria (escribir fuera de un array, usar memoria ya liberada, liberarla dos veces) a menudo no fallan en el momento: corrompen datos y el programa se cae más tarde, en otro sitio. Por eso hay herramientas que los detectan al instante.

Compila mientras desarrollas con -Wall -Wextra -g -fsanitize=address: AddressSanitizer detiene el programa en la línea exacta del fallo y explica qué ha pasado. assert(condición) comprueba algo que siempre debería cumplirse y para el programa si no es así.

Ejemplo práctico
// Compilar: gcc -Wall -Wextra -g -fsanitize=address memoria.c -o memoria
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *duplicar(const char *texto) {
    assert(texto != NULL);                      // no debería llegar nunca NULL
    char *copia = malloc(strlen(texto) + 1);    // +1 para el '\0' final
    if (copia == NULL) return NULL;
    strcpy(copia, texto);
    return copia;
}

int main(void) {
    char *saludo = duplicar("hola");
    if (saludo == NULL) return 1;
    saludo[0] = 'H';
    printf("%s\n", saludo);   // Hola
    free(saludo);
    saludo = NULL;            // así no se puede usar por error después
    return 0;
}

Encuentra los errores

Este programa compila y quizá incluso «funcione», pero tiene 2 errores de memoria que AddressSanitizer detectaría.

Ver la solución
  • Línea 5: strlen no cuenta el '\0' final, así que strcpy escribe un byte más de los reservados (desbordamiento). Corrección: char *copia = malloc(strlen("hola") + 1);
  • Línea 8: Después de free esa memoria ya no es tuya: escribir en ella es un «uso tras liberar». Si había que cambiarla, se hace antes de free; después, lo prudente es poner el puntero a NULL. Corrección: copia = NULL;

¿Has terminado? Sigue practicando

Repasa los conceptos en el diccionario, practica con estas herramientas o prueba otro lenguaje: las ideas que has aprendido (variables, condiciones, funciones, objetos…) te servirán en cualquiera.

Fuentes consultadas

* Porcentaje de personas que dijeron haber usado C en la Stack Overflow Developer Survey 2026: Technology (pregunta sobre lenguajes de programación, de scripts y de marcado; todas las respuestas). El progreso de las lecciones se guarda solo en este navegador.