Curso de C++ desde cero, en español

C++ es el lenguaje de los motores de videojuegos como Unreal, de los navegadores Chrome y Firefox, de Photoshop, de las bases de datos MySQL y MongoDB, de buena parte de Windows y de los programas que mueven la bolsa o los coches autónomos. Lleva más de cuarenta años en uso y está tercero en el índice TIOBE de octubre de 2026, solo detrás de Python y C. Es rápido como C, pero añade clases, plantillas y una biblioteca estándar enorme que se ocupa de la memoria por ti si la usas bien. Este curso empieza desde cero, usa C++23, el estándar vigente, con GCC 15, y enseña el C++ moderno: el que evita los punteros sueltos y los errores de memoria que hicieron famoso al lenguaje.

La ruta: ocho lecciones

Están pensadas para leerse en orden. Las dos primeras están en esta misma página y las demás tienen la suya. Cada lección tiene un recuadro «El error que te ahorra», con un fallo que el compilador o un sanitizador detectan si los usas bien, un recuadro «Ojo con la seguridad» con el fallo típico ligado a ese tema, al menos un esquema y retos al final con la solución escondida. Al terminar hay un test de 24 preguntas: adivinar qué imprime cada fragmento.

Lección 1. Primeros pasos

Qué es C++

C++ lo creó Bjarne Stroustrup, un informático danés, en los Laboratorios Bell de AT&T, los mismos donde había nacido C. Empezó en 1979 con una idea concreta: añadir a C las clases del lenguaje Simula, para organizar programas grandes, sin perder ni un poco de la velocidad de C. Lo llamó primero «C with Classes», y a finales de 1983 le puso el nombre definitivo: ++ es el operador de C que suma uno, así que C++ es «el siguiente C». La primera versión comercial salió en octubre de 1985, junto con la primera edición de su libro The C++ Programming Language.

Hoy lo mantiene un comité internacional, el grupo de trabajo WG21 de ISO, que publica una versión nueva cada tres años. La primera fue C++98; C++11 lo cambió tanto que se habla de «C++ moderno» desde entonces, y le siguieron C++14, C++17, C++20 y C++23, la vigente (oficialmente ISO/IEC 14882:2024), que es la que usa este curso. El comité terminó el trabajo técnico de C++26 en marzo de 2026, y está pendiente de la aprobación formal de ISO. Lo contamos en Versiones y parches.

C++ es un lenguaje compilado: el compilador traduce tu código a instrucciones del procesador, sin máquina virtual ni recolector de basura. Casi todo lo que sabes de C sigue valiendo, pero escribir C++ como si fuera C es el origen de muchos de sus fallos de seguridad. Este curso enseña desde el principio la forma moderna: std::string en vez de arrays de char, std::vector en vez de new[], punteros inteligentes en vez de delete. Si te interesa el C de verdad, tenemos un curso de C aparte.

Instalar un compilador

  • Linux: sudo apt install build-essential en Debian y Ubuntu (instala g++, make y las cabeceras), o sudo dnf install gcc-c++ make en Fedora. Para std::print hace falta GCC 14 o posterior; en Ubuntu 24.04, sudo apt install g++-14.
  • macOS: xcode-select --install. Instala Clang con su propia biblioteca estándar; casi todo el curso funciona igual con clang++ -std=c++23.
  • Windows: Visual Studio Community con la carga «Desarrollo para el escritorio con C++» trae el compilador de Microsoft, MSVC, que también soporta C++23. Otras opciones son WSL, que te da un Linux dentro de Windows, o MSYS2 con g++ nativo.
  • Con Docker, sin instalar nada: docker run --rm -it -v "$PWD":/src -w /src gcc:15 bash. Es la imagen oficial que hemos usado para comprobar el curso.

Para ver qué versión tienes:

g++ --version
salidag++ (GCC) 15.3.0
Copyright (C) 2025 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Para escribir código vale cualquier editor: Visual Studio Code con la extensión C/C++ o clangd, CLion, Visual Studio o el que ya uses.

El primer programa

Crea un archivo hola.cpp:

#include <print>

int main()
{
    std::println("Hola, mundo");
}

Compílalo y ejecútalo desde la terminal, en la misma carpeta:

g++ -std=c++23 -Wall -Wextra hola.cpp -o hola
./hola
salidaHola, mundo

Línea a línea: #include <print> trae std::println, que escribe una línea de texto y salta a la siguiente. int main() es la función por la que empieza todo programa. El prefijo std:: dice que println es de la biblioteca estándar, que vive en el espacio de nombres std. Cada instrucción termina en punto y coma, los bloques van entre llaves y, a diferencia del resto de funciones, main puede terminar sin return: devuelve 0.

Las opciones importan. -std=c++23 elige el estándar: g++ 15 usa C++17 si no se lo dices, y sin ella <print> no existe. -Wall -Wextra activan casi todos los avisos del compilador, que en C++ son la diferencia entre enterarte de un error al compilar o cuando el programa ya falla. Úsalas siempre. -o hola le da nombre al ejecutable, que si no se llamaría a.out.

La forma clásica: std::cout

std::println es de C++23. Casi todo el código que encontrarás escrito antes usa std::cout, de <iostream>, con el operador << para ir encadenando lo que se escribe:

#include <iostream>

int main()
{
    int edad = 31;
    std::cout << "Hola, mundo\n";
    std::cout << "Tengo " << edad << " años" << std::endl;
}
salidaHola, mundo
Tengo 31 años

Las dos formas conviven y las verás mezcladas. En este curso usamos std::println, que es más corta, más rápida y más fácil de leer, pero conviene reconocer std::cout. std::endl escribe un salto de línea y además vacía el búfer de salida; si solo quieres el salto, "\n" es más rápido.

Qué hace g++ por dentro

hola.cpp pasa por el preprocesador, que resuelve include y define y produce hola.ii; el compilador lo traduce a ensamblador, hola.s; el ensamblador produce código máquina, hola.o; y el enlazador lo une con la biblioteca estándar de C++ para formar el ejecutable hola. g++ hace los cuatro pasos de una vezDe hola.cpp a un programa que se ejecutahola.cpppreprocesador#include, #definehola.iicompiladorC++ a ensambladorhola.sensambladora código máquinahola.oenlazador+ biblioteca de C++holag++ hace los cuatro pasos de una vez: g++ -std=c++23 hola.cpp -o holaEl resultado ya no necesita a g++: es código máquina para tu procesador y tu sistema.

Los cuatro pasos de la compilación. Con g++ -save-temps se conservan los archivos intermedios. Esquema: decodigo.com.

Puedes ver cada paso. El preprocesador solo sustituye texto: copia dentro el contenido de las cabeceras y quita los comentarios. Con g++ -E se detiene ahí, y el resultado son decenas de miles de líneas antes de tu main, porque <print> arrastra buena parte de la biblioteca estándar:

g++ -std=c++23 -E hola.cpp | wc -l
g++ -std=c++23 -E hola.cpp | tail -n 6
salida65785

# 3 "hola.cpp"
int main()
{
    std::println("Hola, mundo");
}

Por eso C++ compila bastante más lento que C. Con -save-temps, g++ guarda los archivos intermedios del esquema:

g++ -std=c++23 -save-temps hola.cpp -o hola
ls
salidahola
hola.cpp
hola.ii
hola.o
hola.s

El valor de retorno de main

El número que devuelve main llega al sistema operativo como código de salida: 0 significa «todo bien» y cualquier otro valor, un error. Los scripts y herramientas como make lo usan para saber si deben seguir. En la terminal se ve con echo $? justo después de ejecutar:

#include <cstdio>
#include <cstdlib>
#include <print>

int main()
{
    std::println(stderr, "No encuentro el archivo de configuración");
    return EXIT_FAILURE;
}
./salida
echo $?
salidaNo encuentro el archivo de configuración
1

std::println(stderr, ...) escribe en la salida de errores, que se puede separar de la normal, y EXIT_FAILURE (de <cstdlib>) vale 1.

El error que te ahorra: olvidar el #include

int main()
{
    std::println("Hola");
}
g++: errorsin_include.cpp: In function 'int main()':
sin_include.cpp:3:10: error: 'println' is not a member of 'std'
    3 |     std::println("Hola");
      |          ^~~~~~~
sin_include.cpp:1:1: note: 'std::println' is defined in header '<print>'; this is probably fixable by adding '#include <print>'
  +++ |+#include <print>
    1 | int main()

C++ no deja usar nada que no esté declarado. El mensaje es largo, pero lo importante está en la última línea: g++ sabe en qué cabecera vive std::println y te dice que la incluyas.

Escribir con std::println

std::println recibe una cadena de formato con huecos {}, y después los valores que van en cada hueco, en orden. A diferencia del printf de C, el hueco no dice el tipo: el compilador ya lo sabe. Dentro de las llaves, después de dos puntos, se pueden pedir detalles:

#include <print>
#include <string>

int main()
{
    int edad = 31;
    double altura = 1.68;
    char inicial = 'M';
    std::string ciudad = "Monterrey";
    bool socio = true;

    std::println("{}. vive en {}", inicial, ciudad);
    std::println("Tiene {} años y mide {:.2f} m", edad, altura);
    std::println("[{:5}] [{:<5}] [{:05}] [{:^7}]", edad, edad, edad, edad);
    std::println("Hexadecimal: {:x}, binario: {:b}", 255, 5);
    std::println("¿Socio? {}", socio);
    std::println("{1} antes que {0}", "segundo", "primero");
    std::println("Las llaves se escriben así: {{}}");
}
salidaM. vive en Monterrey
Tiene 31 años y mide 1.68 m
[   31] [31   ] [00031] [  31   ]
Hexadecimal: ff, binario: 101
¿Socio? true
primero antes que segundo
Las llaves se escriben así: {}
  • HuecoHaceEjemplo
  • {}el valor, con el formato natural de su tipo31, true
  • {:.2f}decimales fijos1.68
  • {:5}, {:<5}, {:^7}ancho, alineado a la derecha, izquierda o centro[ 31]
  • {:05}ceros a la izquierda00031
  • {:x}, {:b}hexadecimal, binarioff, 101
  • {1}, {0}el argumento por su posiciónreordenar
  • {{, }}una llave literal{{}} escribe {}

Es el mismo lenguaje de formato que std::format (C++20), que en vez de escribir devuelve un std::string. Los números se escriben con punto decimal, sin separadores de miles, sea cual sea el idioma del sistema.

El error que te ahorra: un formato que no cuadra con los valores

#include <print>

int main()
{
    int a = 1;
    std::println("{} y {}", a);
}
g++: errorformato_mal.cpp: In function 'int main()':
formato_mal.cpp:6:18: error: call to consteval function 'std::basic_format_string<char, int&>("{} y {}")' is not a constant expression
    6 |     std::println("{} y {}", a);
      |                  ^~~~~~~~~
In file included from /usr/local/include/c++/15.3.0/print:43,
                 from formato_mal.cpp:1:

El mensaje completo ocupa decenas de líneas (es el precio de las plantillas de C++), pero lo importante es que no compila: falta un valor para el segundo hueco. std::format y std::println comprueban la cadena de formato al compilar. Con el printf de C, el mismo error compila y en ejecución lee basura de la memoria.

Leer del teclado

Para leer, lo más seguro es leer la línea entera con std::getline en un std::string, que crece lo que haga falta, y después convertirla al tipo que necesitas. Aquí, con std::from_chars, que dice si el texto era de verdad un número y no lanza excepciones:

#include <charconv>
#include <iostream>
#include <print>
#include <string>

int main()
{
    std::print("¿En qué año naciste? ");
    std::string linea;
    if (!std::getline(std::cin, linea)) {
        return 1;
    }

    int anio = 0;
    auto [resto, error] = std::from_chars(linea.data(), linea.data() + linea.size(), anio);
    if (error != std::errc{}) {
        std::println("Eso no es un número.");
        return 1;
    }

    std::println("En 2026 cumples {} años.", 2026 - anio);
}
salida¿En qué año naciste? En 2026 cumples 36 años.

No te preocupes todavía por el auto [resto, error]: es una forma de recoger dos resultados a la vez que veremos en la lección 3. Por ahora basta con la receta: std::getline para leer la línea y std::from_chars para convertirla, comprobando siempre el error. Si en vez de un número escribes otra cosa, el programa lo detecta:

salida (escribiendo «hace mucho»)¿En qué año naciste? Eso no es un número.

También verás mucho std::cin >> anio;, que lee un número directamente. Funciona, pero si lo que llega no es un número, deja std::cin en estado de error y todas las lecturas siguientes fallan en silencio:

#include <iostream>
#include <print>

int main()
{
    int a = -1, b = -1;
    std::cin >> a;
    std::cin >> b;
    std::println("a = {}, b = {}, ¿cin bien? {}", a, b, static_cast<bool>(std::cin));
}
salida (escribiendo «hola 7»)a = 0, b = -1, ¿cin bien? false

Al fallar, std::cin pone a a 0 y ya no lee el 7: b sigue valiendo -1. Si usas >>, comprueba siempre el estado con if (std::cin >> a).

Ojo con la seguridad: los datos de fuera nunca son de fiar

Todo lo que llega de fuera del programa (el teclado, un archivo, la red, los argumentos) puede venir mal formado o preparado a propósito para hacer daño. En C++ moderno, leer en un std::string ya evita el desbordamiento de búfer clásico de C, porque la cadena crece sola. El riesgo que queda es usar el dato sin comprobarlo: un número fuera de rango que luego sirve de índice, un tamaño gigante que agota la memoria. Convierte con funciones que informen del error (std::from_chars), comprueba los límites y decide qué hacer si algo no cuadra antes de usarlo.

Más en Los fallos de seguridad típicos de C++.

Lección 2. Tipos, variables y cadenas

Variables y tipos básicos

En C++ cada variable tiene un tipo fijo que se decide al crearla, y el tipo decide cuánta memoria ocupa y qué valores caben. Los básicos son los enteros (char, short, int, long, long long), los decimales (float, double) y bool, con true y false:

#include <print>

int main()
{
    int unidades = 3;
    double precio = 249.90;
    char talla = 'M';
    bool en_oferta = true;

    double total = unidades * precio;
    std::println("{} camisetas talla {}: {:.2f}", unidades, talla, total);
    std::println("¿En oferta? {}", en_oferta);

    unidades = unidades + 1;
    unidades += 2;
    ++unidades;
    std::println("Ahora son {}", unidades);
}
salida3 camisetas talla M: 749.70
¿En oferta? true
Ahora son 7

+=, -=, *= o ++ son atajos para modificar una variable. A diferencia de C, std::println escribe un bool como true o false.

Inicializar con llaves

C++ tiene varias formas de dar valor inicial a una variable, y la más recomendable es la de llaves, que se llama inicialización uniforme. Funciona con todo, desde un int hasta una lista de valores, y tiene una ventaja: no deja perder información sin avisar.

#include <print>

int main()
{
    int a = 7;
    int b(7);
    int c{7};
    int d{};

    double precio = 19.99;
    int truncado = precio;

    std::println("{} {} {} {} {}", a, b, c, d, truncado);
}
salida7 7 7 0 19

int d{} sin nada dentro vale 0. Fíjate en truncado: el = convierte 19.99 en 19 sin decir nada (con -Wall -Wextra tampoco; haría falta -Wconversion). Con llaves, esa pérdida, que se llama estrechamiento (narrowing), está prohibida:

El error que te ahorra: una conversión que pierde datos

int main()
{
    double precio = 19.99;
    int truncado{precio};
    return truncado;
}
g++: avisoestrechamiento.cpp: In function 'int main()':
estrechamiento.cpp:4:18: warning: narrowing conversion of 'precio' from 'double' to 'int' [-Wnarrowing]
    4 |     int truncado{precio};
      |                  ^~~~~~

El estándar dice que este programa está mal formado, pero g++, por compatibilidad con código antiguo, solo avisa cuando el valor es una variable. Si el valor se conoce al compilar, sí es un error:

int main()
{
    int truncado{19.99};
    return truncado;
}
g++: errorestrechamiento2.cpp: In function 'int main()':
estrechamiento2.cpp:3:18: error: narrowing conversion of '1.9989999999999998e+1' from 'double' to 'int' [-Wnarrowing]
    3 |     int truncado{19.99};
      |                  ^~~~~

Para que la primera versión también sea un error, compila con -Werror=narrowing o con -pedantic-errors. Y si de verdad quieres el entero, dilo con una conversión explícita: int truncado = static_cast<int>(precio);. Así quien lea el código sabe que la pérdida es a propósito.

auto: que el compilador deduzca el tipo

#include <print>
#include <string>

int main()
{
    auto n = 42;
    auto pi = 3.14159;
    auto letra = 'z';
    auto nombre = std::string("Ana");
    auto texto = "Ana";

    std::println("{} {} {} {} {}", n, pi, letra, nombre, texto);
    std::println("{} {} {}", sizeof(n), sizeof(pi), sizeof(texto));
}
salida42 3.14159 z Ana Ana
4 8 8

Con auto, el tipo sale del valor inicial: 42 es int, 3.14159 es double. Ahorra escribir tipos largos, y lo usarás mucho con los contenedores de la lección 4. Ojo con la última: "Ana" entre comillas no es un std::string, sino un puntero a caracteres heredado de C (por eso ocupa 8 bytes). Si quieres una cadena de C++, créala como en la línea de nombre.

Cuánto ocupa cada tipo: sizeof

El estándar no fija el tamaño exacto de los tipos, solo un mínimo: un int tiene al menos 16 bits, aunque en los ordenadores actuales tenga 32. El operador sizeof dice cuántos bytes ocupa algo en tu sistema:

#include <print>

int main()
{
    std::println("bool        {}", sizeof(bool));
    std::println("char        {}", sizeof(char));
    std::println("short       {}", sizeof(short));
    std::println("int         {}", sizeof(int));
    std::println("long        {}", sizeof(long));
    std::println("long long   {}", sizeof(long long));
    std::println("float       {}", sizeof(float));
    std::println("double      {}", sizeof(double));
    std::println("long double {}", sizeof(long double));
}
salidabool        1
char        1
short       2
int         4
long        8
long long   8
float       4
double      8
long double 16

Tamaños medidos con sizeof: bool 1 bytes, char 1 bytes, short 2 bytes, int 4 bytes, long 8 bytes, float 4 bytes, double 8 bytes. En otros sistemas int y long pueden medir distintoCuánto ocupa cada tipo en Linux de 64 bitsbool1 bytechar1 byteshort2 bytesint4 byteslong8 bytesfloat4 bytesdouble8 bytesCada cuadro es un byte (8 bits). En otros sistemas, int y long pueden medir distinto.

Los tamaños medidos en Linux de 64 bits con g++. En Windows, long mide 4 bytes. Esquema: decodigo.com.

Cuando el tamaño importa (un formato de archivo, un protocolo de red), se usan los tipos de <cstdint>, que lo dicen en el nombre: std::int32_t, std::uint8_t, std::int64_t. Y para saber el máximo y el mínimo de cada tipo está std::numeric_limits:

#include <cstdint>
#include <limits>
#include <print>

int main()
{
    std::uint8_t byte = 255;
    std::int64_t poblacion = 8'200'000'000;

    std::println("{} {}", byte, poblacion);
    std::println("int va de {} a {}", std::numeric_limits<int>::min(), std::numeric_limits<int>::max());
    std::println("double, unas {} cifras exactas", std::numeric_limits<double>::digits10);
}
salida255 8200000000
int va de -2147483648 a 2147483647
double, unas 15 cifras exactas

Las comillas simples dentro de 8'200'000'000 son separadores de miles, que el compilador ignora (existen desde C++14).

Operadores y la división entera

#include <print>

int main()
{
    int pizzas = 7, personas = 2;

    std::println("{}", pizzas / personas);
    std::println("{}", pizzas % personas);
    std::println("{}", pizzas / personas * 1.0);
    std::println("{}", static_cast<double>(pizzas) / personas);
    std::println("{} {}", -7 / 2, -7 % 2);
}
salida3
1
3
3.5
-3 -1

Entre dos enteros, / es la división entera: descarta los decimales, siempre hacia cero, y % da el resto, con el signo del dividendo. La tercera línea es una trampa clásica: primero se hace 7 / 2, que ya da 3, y luego se multiplica por 1.0. Para dividir con decimales hay que convertir uno de los dos antes, con static_cast<double>, la conversión explícita de C++.

Decimales: double y sus límites

#include <print>

int main()
{
    double a = 0.1 + 0.2;
    std::println("{}", a);
    std::println("{:.17f}", a);
    std::println("{}", a == 0.3);

    float f = 16'777'217;
    std::println("{:.1f}", f);
}
salida0.30000000000000004
0.30000000000000004
false
16777216.0

Los decimales se guardan en binario y la mayoría de las fracciones, como 0.1, no tienen una representación exacta. std::println escribe el número más corto que identifica el valor, y por eso ya enseña que no es 0.3; comparar con == falla. float es todavía más corto: a partir de 16,777,216 ya no puede representar todos los enteros. Usa double salvo que tengas un buen motivo, y para dinero, enteros en céntimos.

Desbordamiento: cuando el número no cabe

Un entero tiene un tamaño fijo, así que tiene un máximo. En los enteros sin signo, pasarse da la vuelta, como un cuentakilómetros:

#include <print>

int main()
{
    unsigned char nivel = 250;
    nivel += 10;
    std::println("{}", nivel);

    unsigned int cero = 0;
    cero = cero - 1;
    std::println("{}", cero);
}
salida4
4294967295

En los enteros con signo, en cambio, desbordarse es comportamiento indefinido, igual que en C: el estándar no dice qué pasa, y el compilador puede suponer que nunca ocurre. Para verlo hay que compilar con el sanitizador de comportamiento indefinido, -fsanitize=undefined, que añade comprobaciones al programa:

#include <limits>
#include <print>

int main()
{
    int puntos = std::numeric_limits<int>::max();
    puntos = puntos + 1;
    std::println("{}", puntos);
}
g++ -std=c++23 -Wall -Wextra -g -fsanitize=undefined -fno-sanitize-recover con_signo.cpp -o con_signo
salida con errorcon_signo.cpp:7:21: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'

C++ no tiene todavía una función estándar para sumar comprobando el desbordamiento (C23 sí, con ckd_add), pero GCC y Clang ofrecen __builtin_add_overflow, y es fácil comprobar antes de operar:

#include <limits>
#include <print>

bool sumar(int a, int b, int& resultado)
{
    return !__builtin_add_overflow(a, b, &resultado);
}

int main()
{
    int total = 0;
    if (!sumar(std::numeric_limits<int>::max(), 1, total)) {
        std::println("No cabe: no sumo");
    }
    if (sumar(1'000, 2'000, total)) {
        std::println("Total: {}", total);
    }
}
salidaNo cabe: no sumo
Total: 3000

El int& resultado es una referencia: la función escribe directamente en la variable total de main. Las vemos en la lección 3.

Ojo con la seguridad: los desbordamientos de enteros

Un desbordamiento parece un problema de cuentas, pero suele acabar en uno de memoria: si un programa calcula cuánto espacio reservar con cantidad * tamaño y el producto da la vuelta, reserva muy poco y luego escribe mucho. Cuando un número viene de fuera y se usa para un tamaño o un índice, comprueba los límites antes de usarlo, y prueba tu código con -fsanitize=undefined.

Más en Los fallos de seguridad típicos de C++.

Constantes: const y constexpr

#include <print>

constexpr int dias_semana = 7;
constexpr double iva = 0.16;

constexpr int cuadrado(int x)
{
    return x * x;
}

int main()
{
    const int horas_turno = 8;
    double base = 1000.0;

    std::println("{} horas a la semana", horas_turno * dias_semana);
    std::println("Con IVA: {:.2f}", base * (1 + iva));

    constexpr int casillas = cuadrado(8);
    static_assert(casillas == 64);
    std::println("Un tablero tiene {} casillas", casillas);
}
salida56 horas a la semana
Con IVA: 1160.00
Un tablero tiene 64 casillas

const declara una variable que no se puede modificar después. constexpr va más lejos: el valor se conoce al compilar, e incluso una función marcada constexpr puede ejecutarse en el compilador. static_assert comprueba una condición al compilar: si fallara, el programa no compilaría. En C++ no hace falta el #define de C para las constantes.

Cadenas: std::string

Para el texto, C++ tiene std::string, de <string>: una cadena que sabe su longitud, crece sola y se libera sola. Se puede concatenar con +, comparar con == y recorrer:

#include <print>
#include <string>

int main()
{
    std::string nombre = "Ana";
    std::string saludo = "Hola, " + nombre + "!";
    std::println("{} ({} caracteres)", saludo, saludo.size());

    saludo += " ¿Qué tal?";
    std::println("{}", saludo);

    std::println("{}", saludo.substr(6, 3));
    std::println("{}", saludo.find("tal"));
    std::println("{}", saludo.starts_with("Hola"));
    std::println("{}", nombre == "Ana");

    std::string vacia;
    std::println("¿Vacía? {}", vacia.empty());
}
salidaHola, Ana! (10 caracteres)
Hola, Ana! ¿Qué tal?
Ana
18
true
true
¿Vacía? true

substr(6, 3) saca 3 caracteres desde la posición 6 (se cuenta desde 0), y find devuelve la posición donde aparece el texto. Ojo: size() y las posiciones cuentan bytes, no letras. En UTF-8, la ¿ y la é ocupan dos bytes cada una, y por eso «tal» aparece en la posición 18 y no en la 16. La lección 4 trata las cadenas a fondo, junto con std::string_view, una vista de solo lectura que evita copiar texto.

char también es un número

#include <print>

int main()
{
    char letra = 'A';
    std::println("{} {}", letra, static_cast<int>(letra));
    std::println("{}", static_cast<char>(letra + 2));
    std::println("{}", '7' - '0');
}
salidaA 65
C
7

Un char es un entero de un byte que guarda el código del carácter: la 'A' es un 65 en ASCII. Por eso se puede sumar y restar con letras: '7' - '0' convierte un dígito en su valor. Los caracteres van entre comillas simples; las comillas dobles son para cadenas.

El error que te ahorra: una variable sin valor inicial

#include <print>

int main()
{
    int total;
    for (int i = 1; i <= 3; ++i) {
        total += i;
    }
    std::println("{}", total);
}

Curiosamente, con g++ -Wall -Wextra a secas este programa compila sin una queja: el análisis que detecta el problema solo se hace al optimizar. Con -O2 sí aparece:

g++: avisosin_valor.cpp: In function 'int main()':
sin_valor.cpp:7:15: warning: 'total' is used uninitialized [-Wuninitialized]
    7 |         total += i;
      |         ~~~~~~^~~~
sin_valor.cpp:5:9: note: 'total' was declared here
    5 |     int total;
      |         ^~~~~

A diferencia de Java o Python, C++ no pone a cero las variables locales: valen lo que hubiera en esa memoria, y el programa puede funcionar en tu máquina y fallar en otra. Inicializa siempre (int total = 0; o int total{};). C++26 cambia las reglas para que leer una variable sin inicializar sea un error definido, en vez de comportamiento indefinido, pero sigue siendo un error.

Resumen de las lecciones 1 y 2

  • ParaEscribeRecuerda
  • Compilarg++ -std=c++23 -Wall -Wextra x.cpp -o xlos avisos son tu primera red
  • Comprobar errores de ejecución-g -fsanitize=address,undefinedmientras desarrollas
  • Escribirstd::println("{} {}", n, s)C++23, <print>
  • Leer una líneastd::getline(std::cin, linea)en un std::string
  • Texto a númerostd::from_charscomprueba el error
  • Inicializarint n{0};las llaves no dejan perder datos
  • Convertir a propósitostatic_cast<double>(a)no los paréntesis de C
  • Tamaño exactostd::int32_t, std::uint8_tde <cstdint>
  • Constantesconstexpr int n = 10;se calculan al compilar
  • Textostd::stringcrece y se libera sola

Retos

Reto 1. Escribe un programa que convierta 98.6 grados Fahrenheit a Celsius con la fórmula (F - 32) * 5 / 9 y lo imprima con un decimal. Cuidado con el orden de las operaciones y con los enteros.

Ver la solución
#include <print>

int main()
{
    double fahrenheit = 98.6;
    double celsius = (fahrenheit - 32) * 5 / 9;
    std::println("{:.1f} °F son {:.1f} °C", fahrenheit, celsius);
}
salida98.6 °F son 37.0 °C

Funciona porque fahrenheit ya es double y todas las operaciones se hacen con decimales. Si hubieras escrito 5 / 9 * (fahrenheit - 32), el 5 / 9 entre enteros daría 0 y el resultado sería siempre 0.

Reto 2. Lee una cantidad de segundos desde el teclado y escríbela en horas, minutos y segundos (por ejemplo, 3725 son 1 h 2 min 5 s).

Ver la solución
#include <charconv>
#include <iostream>
#include <print>
#include <string>

int main()
{
    std::string linea;
    std::getline(std::cin, linea);

    long total = 0;
    auto [resto, error] = std::from_chars(linea.data(), linea.data() + linea.size(), total);
    if (error != std::errc{} || total < 0) {
        std::println("Escribe un número de segundos.");
        return 1;
    }

    long horas = total / 3600;
    long minutos = total % 3600 / 60;
    long segundos = total % 60;
    std::println("{} h {} min {} s", horas, minutos, segundos);
}
salida1 h 2 min 5 s

Aquí la división entera es justo lo que se necesita: / da cuántas horas completas hay y %, lo que sobra.

Reto 3. ¿Qué escribe este programa? Piénsalo antes de mirar: unsigned int a = 3; int b = -1; std::println("{}", a > b);

Ver la solución
#include <print>

int main()
{
    unsigned int a = 3;
    int b = -1;
    std::println("{}", a > b);
}
salidafalse

Escribe false: según C++, 3 no es mayor que -1. Al comparar un unsigned con un int, el int se convierte a unsigned, y -1 pasa a ser el número sin signo más grande, 4,294,967,295. Con -Wall -Wextra, g++ avisa:

g++: avisoreto3.cpp: In function 'int main()':
reto3.cpp:7:26: warning: comparison of integer expressions of different signedness: 'unsigned int' and 'int' [-Wsign-compare]
    7 |     std::println("{}", a > b);
      |                        ~~^~~

C++20 trae std::cmp_greater(a, b), de <utility>, que compara enteros de distinto signo como esperarías. Lo vemos de nuevo en las rarezas.

Cómo está hecho este curso

Cada fragmento de código se compiló con g++ (GCC) 15.3.0 en un contenedor limpio de la imagen oficial gcc:15 (con glibc 2.41), con -std=c++23 -Wall -Wextra, un usuario normal y sin acceso a la red. Las salidas, los avisos del compilador y los informes de los sanitizadores están copiados de esa ejecución; por eso los archivos que citan se llaman como los ejemplos del curso. Los mensajes de g++ salen en inglés, como en la mayoría de las instalaciones.

¿Buscas una receta concreta en vez de una lección? En Ejemplos de C++ hay tareas resueltas: leer y escribir archivos, CSV, cadenas, fechas, ordenar, contar palabras, hilos… Y antes de escribir C++ para producción, lee Los fallos de seguridad típicos de C++.