💻 Programación desde cero
Qué es programar, cómo elegir tu primer lenguaje, tipos de datos, variables, control de flujo, funciones, alcance y el modelo mental del programa. Teoría y práctica con C y Python.
Programación desde cero
Programar es darle instrucciones a una máquina. Pero entre tú y la máquina hay un abismo: tú piensas en conceptos («guarda la lista de usuarios»), la máquina solo ejecuta operaciones sobre bytes. Tu trabajo como programador es traducir ideas en un lenguaje que la máquina pueda ejecutar, y hacerlo de forma que otro humano (o tú mismo en 6 meses) lo pueda entender.
Esta página te da el vocabulario completo que necesitas para entender cualquier tutorial: qué es un lenguaje, qué es un tipo, qué es una variable, qué es una función. Con C y Python como lenguajes de referencia.
Qué es un lenguaje de programación
Un lenguaje de programación es un sistema de notación para describir computaciones. Tiene dos caras:
- Sintaxis: las reglas de escritura (cómo se escriben las instrucciones).
- Semántica: el significado de cada construcción (qué hace).
# Python: sintaxis limpia, legible casi como inglés
total = 0
for i in range(10):
total += i
print(total)
// C: sintaxis más densa, te obliga a declarar tipos y manejar memoria
#include <stdio.h>
int main(void) {
int total = 0;
for (int i = 0; i < 10; i++) {
total += i;
}
printf("%d\n", total);
return 0;
}
Ambos programas hacen exactamente lo mismo (suman 0..9). La diferencia no es de capacidad, sino de nivel de abstracción y de qué control te da sobre la máquina.
Niveles de los lenguajes
| Nivel | Ejemplos | Qué controlas | Cerca de la máquina |
|---|---|---|---|
| Bajo nivel | Ensamblador, C, C++, Rust | Registros, memoria, punteros | Muy cerca |
| Medio | Go, Java, C#, TypeScript | La mayoría, con runtime que te protege | Media |
| Alto nivel | Python, Ruby, JavaScript, PHP | Estructuras y lógica; la memoria la gestiona el runtime | Lejos |
💡 Regla mental: cuanto más bajo el nivel, más control y más responsabilidad (y más fácil meter la pata). Cuanto más alto, más productividad y más distancia del hardware. Los ingenieros serios saben al menos un lenguaje de cada extremo.
Compilados vs interpretados
- Compilado (C, Go, Rust): un compilador traduce TODO tu código a lenguaje de máquina antes de ejecutarlo. El resultado es un binario (
.exe,.out). Rápido en ejecución, lento en desarrollo. - Interpretado (Python, Ruby): un intérprete lee y ejecuta tu código línea por línea al correrlo. Sin binario, más lento en ejecución, más rápido en desarrollo.
- Compilados a bytecode con VM (Java, C#, Python internamente): el compilador produce bytecode (código intermedio) que luego una máquina virtual (VM) ejecuta y optimiza.
C: código fuente ──compilador──▶ binario (lenguaje máquina) ──▶ CPU
Python: código fuente ──intérprete──▶ ejecuta cada línea
Java: código fuente ──compilador──▶ bytecode ──JVM──▶ máquina
⚠️ Terminología clave: compilador (traduce todo el programa), intérprete (ejecuta línea a línea), bytecode (código intermedio), VM / máquina virtual (ejecuta bytecode), runtime (todo lo que da soporte a tu programa al ejecutarse: gestión de memoria, librerías base).
Cómo elegir tu primer lenguaje
La ruta del plan recomienda aprender C o Python como cimientos. ¿Cuál?
- C te enseña la máquina de verdad: memoria, punteros, cómo funciona todo por dentro. Es el lenguaje de los sistemas operativos, las bases de datos y los kernels.
- Python te enseña a pensar en problemas y estructuras sin pelearte con la máquina. Es el lenguaje de la ciencia de datos, la IA y el backend moderno.
Para fundamentos CS, empieza con C (te obliga a entender memoria y tipos) y usa Python como segundo lenguaje para expresar algoritmos con claridad. El plan de backend de este sitio profundiza en ambos: Python y Go.
Variables y tipos de datos
Una variable es un nombre que apunta a un valor guardado en memoria. Un tipo de dato describe qué clase de valor es: un número, un texto, un verdadero/falso.
Tipos básicos (primitivos)
| Tipo | Qué es | Ejemplo | Tamaño típico (64 bits) |
|---|---|---|---|
| Entero (integer) | Número sin decimales | 42, -7, 0 |
4-8 bytes |
| Flotante (float/double) | Número con decimales | 3.14, 2.5e10 |
4-8 bytes |
| Booleano (boolean) | Verdadero o falso | true, false |
1 byte |
| Carácter (char) | Un símbolo | 'a', 'ñ', '\n' |
1-4 bytes |
| String | Cadena de caracteres | "hola mundo" |
variable |
En C los tipos son explícitos: le dices a la máquina qué tipo es cada variable y cuántos bytes ocupa:
#include <stdio.h>
int main(void) {
int edad = 30; // entero
double precio = 19.99; // decimal de doble precisión
float peso = 70.5f; // decimal simple
char inicial = 'A'; // un carácter (comillas simples)
_Bool activo = 1; // 0 o 1 (C no tiene true/false nativo hasta C23)
printf("edad=%d, precio=%.2f, inicial=%c\n", edad, precio, inicial);
return 0;
}
En Python los tipos son dinámicos: la variable toma el tipo del valor que le asignas y puede cambiar:
edad = 30 # int
precio = 19.99 # float
activo = True # bool
nombre = "Ana" # str
print(f"{edad}, {precio}, {nombre}")
⚠️ Terminología clave: fuertemente tipado (no mezclas tipos sin conversión: Python, C, Java) vs débilmente tipado (los convierte por ti: JavaScript, PHP). Estático (el tipo se conoce al compilar: C, Go) vs dinámico (se conoce al ejecutar: Python, JS).
Integer overflow y precisión
Un int no puede guardar cualquier número: tiene un rango finito.
#include <stdio.h>
#include <limits.h>
int main(void) {
int max = INT_MAX; // 2147483647 en 32 bits
printf("%d\n", max + 1); // ⚠️ overflow: "da la vuelta" a -2147483648
return 0;
}
Los flotantes tienen precisión limitada: 0.1 + 0.2 != 0.3 en punto flotante binario:
print(0.1 + 0.2) # 0.30000000000000004
💡 Por eso el dinero nunca se guarda como
float: se guarda como entero (centavos) o como tipo decimal de precisión exacta. Y por eso los enteros sin signo (unsigned) duplican el rango positivo a costa de no poder ser negativos.
Control de flujo
El código no se ejecuta siempre en línea recta: hay decisiones (condicionales) y repeticiones (bucles).
Condicionales
edad = 18
if edad >= 18:
print("Mayor de edad")
elif edad >= 13:
print("Adolescente")
else:
print("Menor")
int edad = 18;
if (edad >= 18) {
printf("Mayor de edad\n");
} else if (edad >= 13) {
printf("Adolescente\n");
} else {
printf("Menor\n");
}
Bucles
# for: repetir un número conocido de veces / recorrer una colección
for i in range(5):
print(i) # 0 1 2 3 4
# while: repetir mientras una condición sea verdadera
n = 0
while n < 5:
n += 1
for (int i = 0; i < 5; i++) { // inicialización; condición; paso
printf("%d\n", i);
}
int n = 0;
while (n < 5) {
n++;
}
⚠️ Terminología: break (salir del bucle), continue (saltar a la siguiente iteración), iteración (una vuelta del bucle), loop infinito (bucle que nunca termina porque la condición nunca es falsa — normalmente un bug).
Switch
Cuando comparas una variable contra muchos valores fijos, switch es más claro que un encadenado de if:
int opcion = 2;
switch (opcion) {
case 1: printf("Uno\n"); break;
case 2: printf("Dos\n"); break;
default: printf("Otro\n"); break;
}
Funciones
Una función es un bloque de código con nombre que recibe parámetros (entradas), hace algo y devuelve un valor de retorno. Es la unidad básica de organización del código.
def suma(a, b):
"""Suma dos números y devuelve el resultado."""
return a + b
resultado = suma(3, 4) # 7
#include <stdio.h>
int suma(int a, int b) { // tipo de retorno, nombre, parámetros
return a + b;
}
int main(void) {
int resultado = suma(3, 4);
printf("%d\n", resultado); // 7
return 0;
}
Terminología de funciones
| Término | Qué significa |
|---|---|
| Firma | Nombre + parámetros + tipo de retorno |
| Argumento | El valor concreto que pasas al llamar |
| Parámetro | La variable que recibe el valor dentro de la función |
| Retorno | El valor que la función devuelve al que la llamó |
| Efecto secundario | Cambio de estado fuera de la función (modificar un archivo, una variable global, imprimir) |
| Pureza | Una función pura solo depende de sus argumentos y no tiene efectos secundarios |
| Recursión | Una función que se llama a sí misma |
Recursión
Una función recursiva se llama a sí misma con un caso más pequeño. Toda recursión necesita un caso base (cuándo parar) y un paso recursivo (reducir el problema):
def factorial(n):
if n <= 1: # caso base
return 1
return n * factorial(n - 1) # paso recursivo
int factorial(int n) {
if (n <= 1) return 1; // caso base
return n * factorial(n - 1); // paso recursivo
}
⚠️ Sin caso base, la recursión se desborda: cada llamada reserva un stack frame y el stack se llena → stack overflow. Recuerda: la recursión usa el stack de llamadas.
Alcance (scope) y nombres
El alcance (scope) define dónde existe una variable. Reglas fundamentales:
- Variable local: existe solo dentro del bloque/función donde se declara.
- Variable global: declarada fuera de todo, accesible en todo el archivo.
- Shadowing: una variable local que «tapa» a una global con el mismo nombre.
x = 10 # global
def foo():
x = 5 # local (hace shadowing de la global)
print(x) # 5
foo()
print(x) # 10 (la global no cambió)
int x = 10; // global
int main(void) {
int x = 5; // local: tapa a la global
printf("%d\n", x); // 5
return 0;
}
💡 Buenas prácticas: menos variables globales = menos sorpresas. Las funciones deben comunicarse por parámetros y retorno, no por estado global.
Estructuras de datos básicas integradas
Antes de las estructuras de datos «de libro» (página siguiente), todos los lenguajes traen colecciones integradas:
| Estructura | Ordenada | Permite duplicados | Ejemplo (Python) | Ejemplo (C) |
|---|---|---|---|---|
| Array | Sí | Sí | [1, 2, 3] (lista) |
int a[3] |
| Mapa / diccionario | No | Claves únicas | {"a": 1} |
struct + tabla hash |
| Conjunto (set) | No | No | {1, 2, 3} |
— |
| Tupla | Sí | Sí | (1, "a") |
struct |
frutas = ["manzana", "pera", "mango"]
frutas.append("fresa")
print(frutas[0]) # "manzana"
print(len(frutas)) # 4
precios = {"manzana": 2.5, "pera": 3.0}
print(precios["pera"]) # 3.0
precios["mango"] = 5.0 # agregar clave
#include <stdio.h>
#include <string.h>
int main(void) {
char frutas[4][16];
strcpy(frutas[0], "manzana");
strcpy(frutas[1], "pera");
printf("%s\n", frutas[0]);
return 0;
}
⚠️ Índices: las colecciones se indexan desde 0 (el primer elemento es el
[0]). El último de una colección de tamañones[n-1]. Este es un error clásico: el off-by-one.
Modelo mental del programa
Cuando tu código corre, esto es lo que pasa:
- El código fuente se compila o interpreta.
- El proceso se carga en memoria: el código (text), los datos globales, el stack (variables locales y llamadas) y el heap (memoria dinámica).
- La CPU ejecuta instrucciones de una en una, siguiendo el control de flujo: condiciones, bucles, llamadas a funciones.
- Los valores viven en registros o memoria; los nombres que usas son referencias a esas ubicaciones.
Profundizamos en esto en Cómo funciona una computadora y en la wiki de memoria y punteros en C.
Errores y depuración
Todo programa tiene errores. Aprende los tres tipos:
| Tipo | Qué es | Ejemplo |
|---|---|---|
| Error de sintaxis | Escribiste algo que el lenguaje no entiende | if edad = 18 (falta =) |
| Error en tiempo de ejecución | El programa corre pero se rompe | dividir entre cero, índice fuera de rango |
| Error lógico | El programa corre pero hace algo distinto a lo que querías | usar < en vez de <= |
Depurar (debugging) es encontrar y corregir errores. Herramientas básicas:
- Imprimir valores:
print(variable)para ver qué hay en cada paso. - Lector de trazas (stack trace): te dice en qué línea se rompió y cómo se llegó ahí.
- Debugger: ejecuta línea a línea y te deja inspeccionar variables (gdb, pdb, vscode).
def divide(a, b):
return a / b
print(divide(10, 2)) # 5.0
print(divide(10, 0)) # ZeroDivisionError → stack trace
💡 Regla de oro: reproduce el error mínimo, encuentra la línea, entiende el por qué, corrige, y verifica que no rompiste nada (de ahí los tests, página de testing).
Cheatsheet de terminología
| Término | Significado |
|---|---|
| Algoritmo | Secuencia de pasos para resolver un problema |
| Programa | Algoritmo escrito en un lenguaje ejecutable |
| Bug | Error en el programa |
| Compilador | Traduce todo el programa a lenguaje máquina |
| Intérprete | Ejecuta el programa línea a línea |
| Runtime | Infraestructura que da soporte al programa al ejecutarse |
| Variable | Nombre que apunta a un valor en memoria |
| Tipo | Clase de valor: entero, flotante, booleano, cadena… |
| Función | Bloque de código reutilizable con nombre |
| Parámetro / argumento | Entrada declarada / entrada concreta |
| Retorno | Valor que devuelve la función |
| Scope (alcance) | Región donde existe una variable |
| Recursión | Función que se llama a sí misma |
| Iteración | Una vuelta de un bucle |
| Depuración | Proceso de encontrar y corregir bugs |
Práctica propuesta
- Escribe un programa que diga si un número es primo (en C y en Python).
- Escribe una función recursiva
fibonacci(n)y otra iterativa. Compara. - Escribe un programa que cuente las vocales de una cadena.
- Investiga qué pasa en tu lenguaje con
0.1 + 0.2y explica por qué. - Escribe una función que invierta un array sin crear otro (pista: dos índices, uno por cada extremo).
Compila y ejecuta tus programas de C con:
gcc programa.c -o programa && ./programa
Y ejecuta Python con:
python3 programa.py
Para profundizar
- CS50x — Harvard: el mejor curso de introducción del mundo, con C, memoria y estructuras. Subtítulos en español.
- Composing Programs: SICP reescrito en Python, para programar como un científico.
- SICP: el clásico del MIT que «te cambia el modelo mental».
- The C Programming Language (K&R): el libro canónico de C.
- Python.org — Tutorial: el tutorial oficial en español.
- Sigue con 📦 Estructuras de datos y luego ⏱️ Algoritmos.