Jerson Moreno

Full Stack Developer & Emprendedor

Construyo soluciones digitales escalables y experiencias web de alto impacto.

Guía Introductoria a C++: De Cero a Héroe

Introducción: ¿Por qué C++ sigue siendo relevante hoy?

C++ es uno de los lenguajes de programación más potentes y versátiles jamás creados. A pesar de tener décadas de existencia, su relevancia en el mundo del desarrollo de software no ha disminuido. Desde sistemas operativos y videojuegos de alto rendimiento hasta aplicaciones financieras de baja latencia y sistemas embebidos, C++ es la columna vertebral de innumerables tecnologías que usamos a diario.

Para los desarrolladores principiantes, aprender C++ ofrece una comprensión profunda de cómo funciona realmente el software a bajo nivel, incluyendo la gestión de memoria y la optimización del rendimiento. Para los programadores intermedios, dominar C++ abre las puertas a áreas especializadas y muy demandadas. Esta guía te llevará a través de los fundamentos y las características más importantes que necesitas para empezar tu viaje con C++.

Un Vistazo a su Historia y Evolución

Para apreciar C++, es útil conocer su origen.

H3 Orígenes: De C a C++

C++ fue diseñado en 1979 por Bjarne Stroustrup en los Laboratorios Bell. Originalmente llamado “C con Clases”, su objetivo era añadir características de programación orientada a objetos (POO) al ya popular lenguaje C, sin sacrificar la velocidad y la flexibilidad que C ofrecía. Esta compatibilidad con C es una de sus señas de identidad: casi cualquier programa en C es también un programa válido en C++.

Evolución y Estándares Modernos

Con el tiempo, C++ ha evolucionado enormemente. El comité de estándares de C++ ha introducido actualizaciones significativas para modernizar el lenguaje. Las más notables son C++11, C++14, C++17 y C++20, que han añadido características como punteros inteligentes, expresiones lambda, una mejor gestión de la concurrencia y mucho más. Hoy en día, hablar de “C++ Moderno” se refiere al uso de estas nuevas características para escribir código más seguro, limpio y eficiente.

Configurando tu Entorno de Desarrollo

Para empezar a programar en C++, necesitas dos cosas principales: un compilador y un editor de código o IDE.

  • Compilador: Es el programa que traduce tu código C++ a código máquina que el ordenador puede ejecutar. El más común es GCC (GNU Compiler Collection), que es gratuito y viene preinstalado en la mayoría de los sistemas Linux. Para Windows, puedes usar MinGW (una versión de GCC para Windows) o el compilador de Visual Studio. En macOS, Clang (parte de Xcode) es la opción estándar.
  • Editor de Código/IDE: Puedes usar un editor simple como Visual Studio Code (con la extensión de C++), Sublime Text o Atom. O bien, un Entorno de Desarrollo Integrado (IDE) completo como Visual Studio (en Windows), CLion (multiplataforma) o Code::Blocks (gratuito y multiplataforma).

Para verificar tu instalación, puedes compilar y ejecutar el clásico “Hola, Mundo”.

#include <iostream>

int main() {
    std::cout << "¡Hola, Mundo!" << std::endl;
    return 0;
}

Guarda este código como hola.cpp y compílalo desde la terminal con g++ hola.cpp -o hola. Luego, ejecútalo con ./hola. Si ves el mensaje en pantalla, ¡estás listo para continuar!

Conceptos Fundamentales de C++

Todo lenguaje tiene una sintaxis y unas reglas básicas. Aquí repasamos las más importantes en C++.

Variables y Tipos de Datos

Las variables son contenedores para almacenar datos. En C++, debes declarar el tipo de dato que una variable contendrá.

  • int: para números enteros (ej: int edad = 25;).
  • double / float: para números con decimales (ej: double precio = 19.99;).
  • char: para un solo carácter (ej: char inicial = 'A';).
  • bool: para valores de verdadero o falso (ej: bool esValido = true;).
  • std::string: para cadenas de texto (requiere #include <string>).

Estructuras de Control

Permiten dirigir el flujo de tu programa.

  • Condicionales (if-else): Ejecutan código basado en una condición.
int edad = 18;
if (edad >= 18) {
    std::cout << "Eres mayor de edad." << std::endl;
} else {
    std::cout << "Eres menor de edad." << std::endl;
}
  • Bucles (for, while): Repiten un bloque de código.
// Bucle for para contar hasta 5
for (int i = 1; i <= 5; ++i) {
    std::cout << "Número: " << i << std::endl;
}

// Bucle while
int contador = 0;
while (contador < 3) {
    std::cout << "Iteración de while." << std::endl;
    contador++;
}

Funciones

Las funciones son bloques de código reutilizables que realizan una tarea específica. Ayudan a organizar el código y hacerlo más modular.

#include <iostream>

// Declaración de la función
int sumar(int a, int b) {
    return a + b;
}

int main() {
    int resultado = sumar(5, 3); // Llamada a la función
    std::cout << "La suma es: " << resultado << std::endl; // Imprime 8
    return 0;
}

El Poder de la Programación Orientada a Objetos (POO)

La POO es el paradigma que hizo famoso a C++. Permite modelar entidades del mundo real como “objetos” que tienen propiedades (atributos) y comportamientos (métodos).

Clases y Objetos

Una clase es una plantilla para crear objetos. Un objeto es una instancia de una clase.

#include <iostream>
#include <string>

class Coche {
public:
    // Atributos
    std::string marca;
    int anio;

    // Método
    void mostrarInfo() {
        std::cout << "Marca: " << marca << ", Año: " << anio << std::endl;
    }
};

int main() {
    // Crear un objeto (instancia) de la clase Coche
    Coche miCoche;
    miCoche.marca = "Toyota";
    miCoche.anio = 2021;

    miCoche.mostrarInfo(); // Llama al método del objeto
    return 0;
}

La Biblioteca de Plantillas Estándar (STL)

La STL (Standard Template Library) es una de las características más potentes de C++. Es una colección de clases y funciones que proporcionan estructuras de datos y algoritmos de uso común.

  • Contenedores: Estructuras de datos para almacenar colecciones de objetos. Los más comunes son std::vector (un array dinámico), std::list (una lista doblemente enlazada), y std::map (un diccionario clave-valor).
  • Algoritmos: Funciones para operar sobre los contenedores, como std::sort (para ordenar), std::find (para buscar) y std::for_each (para iterar).
  • Iteradores: Objetos que actúan como punteros para recorrer los elementos de un contenedor.

Usar la STL te ahorra tiempo y te permite escribir código más robusto y eficiente.

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numeros = {5, 2, 8, 1, 9};

    // Ordenar el vector
    std::sort(numeros.begin(), numeros.end());

    // Imprimir el vector ordenado
    std::cout << "Números ordenados: ";
    for (int num : numeros) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

Conclusión: Tus Próximos Pasos en C++

Hemos cubierto los pilares de C++: su historia, configuración, sintaxis básica, el paradigma de POO y la indispensable STL. Aunque esto es solo la punta del iceberg, ahora tienes una base sólida para seguir construyendo.

¿Qué sigue?

  1. Practica, practica y practica: Resuelve problemas en plataformas como LeetCode o HackerRank.
  2. Profundiza en la gestión de memoria: Aprende sobre punteros, new/delete y, más importante, los punteros inteligentes (std::unique_ptr, std::shared_ptr) de C++ moderno.
  3. Construye un proyecto pequeño: Intenta crear una aplicación de consola simple, como una lista de tareas o una pequeña agenda.

C++ es un lenguaje desafiante pero inmensamente gratificante. Dominarlo no solo te convertirá en un mejor programador, sino que también te dará acceso a algunas de las áreas más emocionantes de la informática. ¡Buena suerte en tu viaje!

Continue

Lo que me hubiera gustado saber sobre punteros en C++ cuando era junior

Lo que me hubiera gustado saber sobre punteros en C++ cuando era junior

Resumen: En este post te cuento, en lenguaje humano, qué son realmente los punteros en C++, por qué asustan tanto al principio y qué cosas me hubiera encantado entender desde el día uno para evitar segfaults y dolores de cabeza.

Nivel: Junior

Introducción

Si estás empezando con C++, es muy probable que la palabra puntero te produzca una mezcla de respeto y miedo.
Y no te culpo: casi todos hemos roto programas (y paciencia) por culpa de un puntero mal usado.

Cuando yo era junior, pensaba que los punteros eran algo “mágico” que había que memorizar,
en vez de entender de verdad qué estaba pasando en memoria.
En este post quiero contarte lo que me hubiera gustado saber desde el principio:

  • Qué es realmente un puntero (sin humo).
  • La diferencia entre una variable normal, una referencia y un puntero.
  • Por qué new, delete y el temido segmentation fault no son brujería.
  • Errores típicos que todos cometemos al empezar (y cómo evitarlos).

Qué es de verdad un puntero

Un puntero no es más que una variable que guarda una dirección de memoria.
Nada más. No es un monstruo, no es una entidad oscura del compilador.
Es como un papelito donde apuntas: “el valor que buscas está en la dirección X”.

Ejemplo básico:


int x = 42;      // variable normal
int* p = &x;     // p es un puntero a int, guarda la dirección de x
  • x guarda el valor 42.
  • p guarda “dónde está x en memoria”.
  • &x significa “la dirección de x”.

Para acceder al valor apuntado por p, usas el operador * (dereferencia):


std::cout << *p << std::endl; // imprime 42

Lo que me hubiera gustado que me dijeran es:
el puntero es un número (dirección), la estrella (*) es cómo vas desde esa dirección al valor real.

Variable, referencia y puntero: misma historia, distintas interfaces

Cuando eres junior, de pronto te tiran esto encima:


int x = 10;
int& ref = x;
int* ptr = &x;

Y tu cabeza dice: “¿Por qué tres formas de hacer lo mismo?”. La clave:

  • Variable normal → guarda un valor.
  • Referencia (&) → es un alias de la variable, no puedes cambiar a qué “apunta”.
  • Puntero (*) → guarda una dirección que SÍ puedes cambiar en tiempo de ejecución.

Ejemplo muy simplificado:


int x = 10;
int y = 20;

int& r = x;   // referencia a x
int* p = &x;   // puntero a x

r = 15;        // modifica x, ahora x = 15
*p = 30;       // también modifica x, ahora x = 30

p = &y;        // ahora p apunta a y
*p = 99;       // modifica y, ahora y = 99

// r no puede "reasignarse" para referenciar y, se queda con x

Lo que me hubiera ayudado mucho: pensar que las referencias son un “puntero constante implícito”,
más seguro y cómodo, mientras que los punteros son más flexibles pero también más peligrosos.

Memoria dinámica: por qué existen new y delete

Otra cosa que me hubiera encantado entender pronto:
por qué necesitamos memoria dinámica. ¿No basta con hacer int x = 5; y ya?

Las variables “normales” suelen vivir en la pila (stack):

  • Se crean cuando entras a una función.
  • Se destruyen automáticamente cuando sales de la función.

Pero a veces quieres que algo viva más allá de esa función:


int* crearNumero() {
    int* p = new int(42);
    return p; // devolvemos un puntero a memoria dinámica
}

int main() {
    int* numero = crearNumero();
    std::cout << *numero << std::endl; // 42
    delete numero; // importante: liberar la memoria
}

Aquí la memoria se reserva en el heap con new, y no se libera sola.
Esa es la razón de ser de delete (y de las fugas de memoria cuando se nos olvida).

El clásico terror: segmentation fault

Uno de los mejores “profesores” que vas a tener en C++ es el temido segmentation fault.
Cada vez que te aparece, es básicamente el sistema operativo diciéndote:
“estás tocando memoria que no es tuya”.

Las causas típicas que me hubiera gustado tener claras desde el día uno:

  • Dereferenciar punteros nulos: int* p = nullptr; *p = 10; // BOOM
  • Acceder fuera de rangos en arrays.
  • Usar punteros colgantes (que apuntan a memoria ya liberada).

Ejemplo de puntero colgante:


int* peligrosito() {
    int x = 10;
    return &x;   // MALA IDEA: x se destruye al salir de la función
}

int main() {
    int* p = peligrosito();
    std::cout << *p << std::endl; // comportamiento indefinido
}

Aquí el puntero p parece válido, pero en realidad apunta a algo que ya no existe.
Este tipo de bug es de los más difíciles de depurar.

Lo que me hubiera gustado saber sobre buenas prácticas con punteros

1. Inicializa SIEMPRE tus punteros

No dejes punteros “a lo que haya por ahí en memoria”.


int* p = nullptr;  // mejor que dejarlo sin inicializar

Y antes de usarlo:


if (p != nullptr) {
    // usa p con tranquilidad razonable
}

2. Cada new debería tener su delete

Como junior, yo hacía new por todos lados y me olvidaba de delete.
Resultado: fugas de memoria como si no hubiera un mañana.


int* p = new int(5);
// ...
delete p;   // si no lo haces, memoria perdida
p = nullptr; // buena práctica: evitar punteros colgantes

Más adelante, lo ideal es usar smart pointers (std::unique_ptr, std::shared_ptr)
para no tener que preocuparte tanto de esto, pero entender la base de new/delete ayuda muchísimo.

3. No uses punteros si una referencia o una variable normal es suficiente

Algo que me habría ahorrado muchos bugs:
no todo tiene que ser un puntero.
A veces una referencia o pasar un objeto por valor es más simple y más seguro.


// Mucho más legible y seguro:
void incrementar(int& x) {
    x++;
}

En lugar de:


void incrementar(int* x) {
    if (x != nullptr) {
        (*x)++;
    }
}

Los punteros tienen sentido cuando realmente necesitas esa flexibilidad:
memoria dinámica, estructuras de datos, interoperabilidad con C, etc.

4. Dibujar la memoria en papel ayuda muchísimo

Una de las cosas que más me ayudó (y que ojalá hubiera hecho antes) fue dibujar lo que está pasando:
cuadritos para variables, flechas para punteros, tachar cuando algo se destruye, etc.

Por ejemplo, para este código:


int x = 10;
int* p = &x;
int* q = p;
*q = 20;

Visualmente:


x: 20
p ----┐
      v
q ----┘

Entender que p y q apuntan al MISMO sitio de memoria hace que el comportamiento del programa deje de ser “mágico”.

Punteros y arrays: otro clásico que confunde

Otra cosa que me confundió muchísimo al principio es que los arrays y los punteros están muy relacionados,
y la sintaxis no ayuda.


int arr[3] = {1, 2, 3};
int* p = arr;       // arr "decáe" a puntero a su primer elemento

Aquí, p apunta a arr[0].
Entonces:

  • *p es arr[0].
  • *(p + 1) es arr[1].
  • *(p + 2) es arr[2].

Y esta es la parte divertida (y confusa):


p[0] == *p;
p[1] == *(p + 1);

Es decir, p[i] es solo azúcar sintáctico para *(p + i).
Cuando entendí esto, todo el tema de arrays + punteros empezó a tener más sentido.

Resumen: lo que realmente me hubiera ahorrado tiempo

  • Un puntero solo guarda una dirección. La magia está en la dereferencia (*).
  • Referencia vs puntero:
    la referencia es un alias seguro; el puntero es flexible pero peligroso.
  • Memoria dinámica: si usas new,
    alguien tiene que hacer delete. Si no, fuga de memoria.
  • Segmentation fault casi siempre es:
    estás tocando memoria que no deberías (puntero nulo, colgante o fuera de rango).
  • No todo necesita ser un puntero: usa referencias y valores cuando puedas.
  • Dibuja la memoria: papel y lápiz son tus amigos cuando estás aprendiendo.

Conclusión

Los punteros son una de las partes más temidas de C++, pero también una de las más poderosas.
Entenderlos bien te abre la puerta a estructuras de datos, optimización y a entender qué pasa “debajo del capó”
cuando programas en otros lenguajes de más alto nivel.

Si estás ahora mismo peleándote con punteros, es normal.
Nos pasa a todos. La clave es practicar con ejemplos pequeños, dibujar lo que pasa en memoria
y no tener miedo a romper cosas en tus pruebas.

En próximos posts podemos ver:

  • Introducción a std::unique_ptr y std::shared_ptr.
  • Cómo implementar estructuras de datos (listas, árboles) usando punteros.
  • Errores típicos con punteros inteligentes y cómo evitarlos.

Recursos recomendados

  • Documentación de cppreference sobre punteros y referencias.
  • Tutoriales visuales de memoria en C++ en YouTube — los diagramas ayudan mucho.
Continue

Listas enlazadas en C++: entendiendo la estructura sin morir en el intento

Listas enlazadas en C++: entendiendo la estructura sin morir en el intento

Resumen: En este post te explico qué es una lista enlazada en C++, por qué existe si ya hay arrays, y cómo implementar una lista enlazada simple paso a paso con ejemplos de código.

Nivel: Junior

Introducción

Si estás empezando con C++ o estructuras de datos, probablemente ya te preguntaste:
“Si tengo arrays y std::vector, ¿para qué quiero una lista enlazada?”.
Buena pregunta.

Las listas enlazadas son una estructura de datos clásica que aparece en entrevistas, exámenes y, sobre todo,
te obliga a entender mejor cómo funcionan los punteros y la memoria dinámica en C++.
En este post vamos a ver:

  • Qué es una lista enlazada y cómo se diferencia de un array.
  • Cuándo tiene sentido usarla (y cuándo es mejor no complicarse).
  • Una implementación básica en C++ con las operaciones típicas.
  • Errores comunes que comete todo el mundo la primera vez (yo incluido).

¿Qué es una lista enlazada?

Una lista enlazada (singly linked list) es una colección de nodos donde:

  • Cada nodo guarda un valor.
  • Cada nodo sabe quién es el siguiente nodo (puntero next).
  • La lista solo conoce el primer nodo, llamado head.

Visualmente, algo así:


[valor1 | *] → [valor2 | *] → [valor3 | *] → nullptr

A diferencia de un array:

  • Los elementos no están uno al lado del otro en memoria.
  • Para acceder al elemento N, tienes que recorrer desde el inicio.
  • Agregar o eliminar elementos al inicio puede ser muy barato (O(1)).

¿Cuándo usar listas enlazadas y cuándo no?

Te conviene usar una lista enlazada cuando:

  • Necesitas hacer muchas inserciones y eliminaciones al inicio de la colección.
  • No te importa tanto el acceso “dame el índice 10” rápido.
  • Estás aprendiendo estructuras de datos y quieres entender bien punteros y memoria dinámica.

No es la mejor idea cuando:

  • Vas a acceder constantemente por índice (tipo v[100000]).
  • Tu caso de uso es básicamente una lista que solo crece y rara vez eliminas en medio.
  • Ya puedes usar std::vector o std::list de la STL sin reinventar la rueda.

En la práctica, en C++ del mundo real, normalmente usarás std::vector y compañía.
Pero entender listas enlazadas te hace mucho mejor programador.

Implementación básica de una lista enlazada en C++

Vamos con una implementación simple de una lista enlazada de enteros.

Definiendo el nodo


#include <iostream>

struct Node {
    int data;      // valor que guarda el nodo
    Node* next;    // puntero al siguiente nodo

    // Constructor cómodo
    Node(int value) : data(value), next(nullptr) {}
};

Manejando la lista

Podemos manejar la lista con un puntero head que apunte al primer nodo:


struct LinkedList {
    Node* head;

    LinkedList() : head(nullptr) {}

    // Insertar al inicio
    void push_front(int value) {
        Node* newNode = new Node(value);
        newNode->next = head;
        head = newNode;
    }

    // Mostrar todos los elementos
    void print() const {
        Node* current = head;
        while (current != nullptr) {
            std::cout << current->data << " -> ";
            current = current->next;
        }
        std::cout << "nullptr" << std::endl;
    }

    // Liberar memoria (destructor)
    ~LinkedList() {
        Node* current = head;
        while (current != nullptr) {
            Node* nextNode = current->next;
            delete current;
            current = nextNode;
        }
    }
};

Usando la lista en main


int main() {
    LinkedList list;

    list.push_front(10);
    list.push_front(20);
    list.push_front(30);

    list.print(); // 30 -> 20 -> 10 -> nullptr

    return 0;
}

Aquí estamos insertando siempre al inicio, así que el orden queda invertido:
el último que insertas es el primero de la lista.

Operaciones típicas en una lista enlazada

Insertar al final

Insertar al final requiere recorrer la lista hasta llegar al último nodo.


void push_back(int value) {
    Node* newNode = new Node(value);

    if (head == nullptr) {
        head = newNode;
        return;
    }

    Node* current = head;
    while (current->next != nullptr) {
        current = current->next;
    }
    current->next = newNode;
}

Eliminar el primer nodo con un cierto valor


bool remove(int value) {
    if (head == nullptr) return false;

    // Si el que hay que borrar es el primero
    if (head->data == value) {
        Node* temp = head;
        head = head->next;
        delete temp;
        return true;
    }

    Node* current = head;
    while (current->next != nullptr && current->next->data != value) {
        current = current->next;
    }

    if (current->next == nullptr) {
        // No se encontró el valor
        return false;
    }

    Node* nodeToDelete = current->next;
    current->next = current->next->next;
    delete nodeToDelete;
    return true;
}

Buscar un valor


bool contains(int value) const {
    Node* current = head;
    while (current != nullptr) {
        if (current->data == value) {
            return true;
        }
        current = current->next;
    }
    return false;
}

Ejemplo completo


#include <iostream>

struct Node {
    int data;
    Node* next;

    Node(int value) : data(value), next(nullptr) {}
};

struct LinkedList {
    Node* head;

    LinkedList() : head(nullptr) {}

    void push_front(int value) {
        Node* newNode = new Node(value);
        newNode->next = head;
        head = newNode;
    }

    void push_back(int value) {
        Node* newNode = new Node(value);

        if (head == nullptr) {
            head = newNode;
            return;
        }

        Node* current = head;
        while (current->next != nullptr) {
            current = current->next;
        }
        current->next = newNode;
    }

    bool remove(int value) {
        if (head == nullptr) return false;

        if (head->data == value) {
            Node* temp = head;
            head = head->next;
            delete temp;
            return true;
        }

        Node* current = head;
        while (current->next != nullptr && current->next->data != value) {
            current = current->next;
        }

        if (current->next == nullptr) {
            return false;
        }

        Node* nodeToDelete = current->next;
        current->next = current->next->next;
        delete nodeToDelete;
        return true;
    }

    bool contains(int value) const {
        Node* current = head;
        while (current != nullptr) {
            if (current->data == value) {
                return true;
            }
            current = current->next;
        }
        return false;
    }

    void print() const {
        Node* current = head;
        while (current != nullptr) {
            std::cout << current->data << " -> ";
            current = current->next;
        }
        std::cout << "nullptr" << std::endl;
    }

    ~LinkedList() {
        Node* current = head;
        while (current != nullptr) {
            Node* nextNode = current->next;
            delete current;
            current = nextNode;
        }
    }
};

int main() {
    LinkedList list;

    list.push_front(10);
    list.push_front(20);
    list.push_back(5);

    list.print(); // 20 -> 10 -> 5 -> nullptr

    list.remove(10);
    list.print(); // 20 -> 5 -> nullptr

    std::cout << "Contiene 5? " << (list.contains(5) ? "sí" : "no") << std::endl;
    std::cout << "Contiene 42? " << (list.contains(42) ? "sí" : "no") << std::endl;

    return 0;
}

Errores típicos con listas enlazadas (y cómo evitarlos)

  • Olvidar inicializar punteros: deja siempre next como nullptr en el constructor.
  • Acceder a current->next sin comprobar current != nullptr:
    esto es receta segura para un segmentation fault.
  • Fugas de memoria: cada new necesita un delete.
    Por eso el destructor que recorre y libera la lista es tan importante.
  • Perder la referencia a la lista: si reasignas head sin guardar el nodo anterior,
    te quedas sin acceso al resto de nodos.

Conclusión

Las listas enlazadas son una de esas estructuras que, al principio, parecen más complicadas de lo que “valen”.
Pero una vez las entiendes, te abren la puerta a muchas otras estructuras más avanzadas
(listas dobles, colas, pilas, árboles, grafos…).

En C++ moderno, en la mayoría de casos reales usarás contenedores de la STL
como std::vector o std::list.
Aún así, implementar tu propia lista enlazada te obliga a entender punteros, memoria dinámica y destructores,
lo cual es oro puro si quieres crecer como desarrollador C++.

Si te gustaría, en otro post podemos ver:

  • Listas doblemente enlazadas.
  • Implementar una pila o una cola usando una lista enlazada.
  • La misma estructura pero usando templates para soportar cualquier tipo, no solo int.

Recursos recomendados

  • Búsqueda en Google de “singly linked list C++” con imágenes: ayuda un montón a visualizar.
  • Documentación de std::list en C++ para ver cómo lo hace la STL.