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.
- 1Primeros pasosQué es C++, compilar con g++, escribir con std::println y leer del teclado.En esta página
- 2Tipos, variables y cadenasTipos, auto, llaves, const y constexpr, std::string y desbordamientos.En esta página
- 3Control y funcionesif, switch, bucles, funciones, referencias, sobrecarga y lambdas.Leer la lección →
- 4Contenedores y algoritmosvector, array, map, iteradores, algoritmos y rangos.Leer la lección →
- 5Clases y objetosClases, constructores, métodos const, destructores y RAII.Leer la lección →
- 6Memoria, referencias y punteros inteligentesPila y montón, unique_ptr, shared_ptr y la semántica de movimiento.Leer la lección →
- 7Plantillas, herencia y erroresPlantillas y conceptos, herencia y virtual, excepciones, optional y expected.Leer la lección →
- 8Un proyecto completo con CMakeVarios archivos, CMake, pruebas, sanitizadores y clang-tidy.Leer la lección →
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-essentialen Debian y Ubuntu (instala g++, make y las cabeceras), osudo dnf install gcc-c++ makeen Fedora. Parastd::printhace 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 conclang++ -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
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.
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
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.
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
- Compilar
g++ -std=c++23 -Wall -Wextra x.cpp -o xlos avisos son tu primera red - Comprobar errores de ejecución
-g -fsanitize=address,undefinedmientras desarrollas - Escribir
std::println("{} {}", n, s)C++23,<print> - Leer una línea
std::getline(std::cin, linea)en unstd::string - Texto a número
std::from_charscomprueba el error - Inicializar
int n{0};las llaves no dejan perder datos - Convertir a propósito
static_cast<double>(a)no los paréntesis de C - Tamaño exacto
std::int32_t,std::uint8_tde<cstdint> - Constantes
constexpr int n = 10;se calculan al compilar - Texto
std::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++.
