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
1
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.
¿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.
3
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.
Los errores
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++)
Compilar con -Wall avisa de los dos: «suggest parentheses around assignment» y «this for clause does not guard».
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++)
4
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.
Una solución posible
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.
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
5
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.
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.
Una solución posible
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.
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.
6
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 *.
7
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.
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.
Los errores
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");
Con char nombre[20], el texto copiado tiene que caber con su '\0' final: como mucho 19 letras.
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
8
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.
9
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.
10
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.
Respuesta: Declaraciones que comparten varios archivos .c. Las cabeceras declaran funciones, tipos y constantes; las definiciones van en los .c.
11
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.
12
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.
Una solución posible
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.
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.
13
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.
14
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.
¿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.
15
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.
Los errores
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;
Los dos son comportamiento indefinido: pueden no notarse, colgar el programa o abrir un fallo de seguridad. AddressSanitizer los señala con «heap-buffer-overflow» y «heap-use-after-free».
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.
* 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.
¿Nos dejas medir las visitas?
Con tu permiso usamos Google Analytics, que guarda cookies, para saber cuántas personas visitan la web y qué páginas les resultan útiles. No usamos cookies de publicidad y tus archivos nunca salen de tu navegador. Puedes cambiar de opinión cuando quieras en «Configurar cookies», al pie de la página. Más información.