AulaML Python · C++
C++ principiante · 36 min · Fundamentos de C++

Variables, const, auto y operadores

Declarar, inicializar y combinar valores sin sorpresas de tipo

Las variables de C++ tienen tipo fijo y se inicializan al declararlas. En esta lección distingues const de constexpr, usas auto con criterio, evitas la división entera accidental y conviertes con static_cast cuando el truncamiento es deliberado.

Lección 3 de 3 del módulo
Al terminar esta lección podrás
  • Declarar e inicializar variables con las distintas formas de C++
  • Elegir entre const, constexpr y auto según la intención
  • Predecir el resultado de una expresión aplicando precedencia y tipos
  • Convertir valores con static_cast sin perder información por accidente
  • Reconocer y evitar el desbordamiento de enteros

Antes necesitas: Estructura de un programa, cout y tipos

En esta lección
  1. ¿Qué es una variable?
  2. ¿Para qué sirve?
  3. ¿Cómo funciona por dentro?
  4. Sintaxis
  5. Ejemplo básico
  6. Ejemplo intermedio: precedencia y división entera
  7. Caso práctico: normalizar una característica
  8. Visualización: qué guarda cada variable
  9. Conversiones y static_cast
  10. Errores comunes
  11. Buenas prácticas
  12. Ejercicios
  13. Soluciones explicadas
  14. Resumen
  15. Evaluación

Una variable es un nombre para un trozo de memoria con un tipo. En Python ese nombre puede apuntar a cualquier objeto y cambiar de tipo; en C++ el tipo se fija al declarar y determina cuántos bytes se reservan y qué operaciones tienen sentido.

¿Qué es una variable?

Declarar una variable define un objeto: una región de almacenamiento con nombre, tipo y duración. El nombre es para ti; el tipo es para el compilador; la dirección es para la máquina.

  • Nombre: el identificador que usas en el código, como tasa o intentos.
  • Tipo: int, double, bool, std::string... decide el tamaño y las operaciones válidas.
  • Valor: el contenido actual, que cambia con la asignación.
  • Dirección: la posición en memoria; la verás con & en la lección de punteros.

const añade una restricción: el valor no cambia después de inicializarse. constexpr va más allá y exige que el valor se conozca en tiempo de compilación.

¿Para qué sirve?

  • Guardar datos de entrada y resultados intermedios con el tipo adecuado.
  • Nombrar constantes de negocio o de física una sola vez: constexpr double igv = 0.18;.
  • Evitar números mágicos y errores al repetir un valor.
  • Dejar que auto deduzca tipos largos sin escribirlos.
  • Controlar conversiones para no perder decimales ni desbordar enteros.

En Machine Learning casi todo lo que manipulas son variables numéricas: características, pesos, tasas de aprendizaje. Elegir float o double no es cosmético; afecta a la precisión y al coste.

¿Cómo funciona por dentro?

Cuando el compilador ve int intentos = 0;, reserva espacio para un entero (4 bytes en esta plataforma), escribe el valor inicial y asocia el nombre a esa posición. Una declaración sin inicializar deja el contenido indeterminado: puede ser cualquier cosa que hubiera antes.

La inicialización ocurre al crear el objeto; la asignación reemplaza su valor después. int a = 5; inicializa; a = 7; asigna. En una línea trivial da igual, pero con constantes y objetos con constructor la diferencia es real.

Cuando una constante es constexpr, el compilador conoce su valor y puede sustituirlo en cada uso, sin reservar almacenamiento en tiempo de ejecución. Eso permite usarla como tamaño de un array o dentro de una comparación sin coste.

CPP limites.cpp
#include <iostream>
#include <limits>

int main() {
    std::cout << "int máximo: " << std::numeric_limits<int>::max() << '\n';
    std::cout << "int mínimo: " << std::numeric_limits<int>::min() << '\n';

    unsigned int grande = 0;
    grande = grande - 1;  // da la vuelta: 4294967295
    std::cout << "unsigned 0 - 1 = " << grande << '\n';
    return 0;
}

std::numeric_limits<int>::max() imprime 2147483647, y el mínimo, -2147483648. Sumarle 1 al máximo es comportamiento indefinido: el estándar no promete qué ocurre. Un unsigned sí da la vuelta de forma definida: 0 − 1 se convierte en 4294967295.

Sintaxis

Hay varias formas de inicializar y no son equivalentes en todos los casos. C++ moderno prefiere las llaves porque prohíben las conversiones que pierden información.

FormaEjemploQué hace
Sin inicializarint x;Reserva memoria; el valor es indeterminado
Copiaint x = 5;Inicializa con 5; permite conversiones implícitas
Llavesint x{5};Inicializa con 5; rechaza narrowing
Paréntesisint x(5);Inicializa con 5; ambigua en algunos contextos
Constanteconst int x = 5;No se puede reasignar después
Constante de compilaciónconstexpr int x = 5;Valor conocido al compilar
Deducciónauto x = 5;El tipo se deduce del inicializador: int

Regla práctica del curso: usa {} para inicializar, const cuando el valor no cambie y constexpr cuando además se conozca al compilar. Usa auto cuando el tipo sea evidente por la derecha o demasiado largo para escribirlo.

Ejemplo básico

CPP basico.cpp
#include <iostream>
#include <string>

int main() {
    int intentos = 0;
    double tasa = 0.1;
    constexpr int maximo = 5;
    std::string mensaje = "listo";
    auto pi = 3.1416;  // auto deduce double

    intentos = intentos + 1;

    std::cout << mensaje << ": " << intentos << " de " << maximo << '\n';
    std::cout << "Tasa: " << tasa << " | pi: " << pi << '\n';
    return 0;
}

int intentos = 0; crea un contador. constexpr int maximo = 5; fija el límite y el compilador lo sustituye. auto pi = 3.1416; deduce double porque el literal tiene punto decimal; con auto pi = 3; sería int. La asignación intentos = intentos + 1; lee el valor, suma y guarda el resultado: no crea una variable nueva.

La salida es listo: 1 de 5 y Tasa: 0.1 | pi: 3.1416. std::cout no imprime el tipo, solo el valor formateado.

Ejemplo intermedio: precedencia y división entera

Los operadores aritméticos siguen la precedencia habitual: primero *, / y %; después + y -; los paréntesis mandan. Con enteros, / descarta la parte decimal en vez de redondear.

CPP aritmetica.cpp
#include <iostream>

int main() {
    int a = 7;
    int b = 2;

    std::cout << "a + b * 3 = " << a + b * 3 << '\n';
    std::cout << "(a + b) * 3 = " << (a + b) * 3 << '\n';
    std::cout << "a / b = " << a / b << '\n';
    std::cout << "a % b = " << a % b << '\n';
    std::cout << "a / b en double = " << static_cast<double>(a) / b << '\n';
    return 0;
}

7 + 2 * 3 da 13, no 27: la multiplicación se evalúa antes. (a + b) * 3 da 27 porque los paréntesis fuerzan la suma. 7 / 2 da 3 y 7 % 2 da 1: cociente y resto de la división entera. Para obtener 3.5 hay que convertir al menos un operando a double; static_cast<double>(a) / b lo hace de forma explícita y legible.

El operador % solo existe para enteros. Si lo aplicas a un double, el compilador se queja; para reales se usa std::fmod de <cmath>.

Caso práctico: normalizar una característica

En Machine Learning es habitual escalar una columna a un rango común. La fórmula min-max es (x - min) / (max - min), y con double conserva los decimales que el modelo necesita.

CPP normalizar.cpp
#include <iomanip>
#include <iostream>

int main() {
    // Una característica (por ejemplo, la edad) que queremos llevar a [0, 1]
    constexpr double minimo = 18.0;
    constexpr double maximo = 60.0;
    double valor = 33.0;

    double normalizado = (valor - minimo) / (maximo - minimo);

    std::cout << std::fixed << std::setprecision(3);
    std::cout << "x = " << valor << " -> " << normalizado << '\n';
    return 0;
}

Con x = 33, min = 18 y max = 60: (33 − 18) / (60 − 18) = 15 / 42 ≈ 0.357. Si los tres valores fueran enteros, la división también sería entera y obtendrías 0. Ese es el error clásico de la normalización en C++.

En NumPy escribes (x - x.min()) / (x.max() - x.min()) y la operación se aplica al array completo. Aquí lo haces valor por valor porque aún no conoces contenedores; la lógica numérica es la misma.

Visualización: qué guarda cada variable

La traza sigue un programa con a = 7 y b = 2. Observa cómo cociente y exacto reciben resultados distintos a partir de los mismos operandos: la diferencia está en el tipo del destino y en la conversión.

Ejecución visual ·División entera frente a división real
1 / 7
1 #include <iostream>
2
3 int main() {
4 int a = 7;
5 int b = 2;
6 int cociente = a / b;
7 double exacto = static_cast<double>(a) / b;
8 int resto = a % b;
9 std::cout << cociente << ' ' << exacto << ' ' << resto << '\n';
10 return 0;
11 }
a ocupa 4 bytes y guarda 7.
Variables
a = 7
Salida
—

Avanza paso a paso: observa la línea resaltada, las variables y la salida.

Conversiones y static_cast

C++ convierte tipos automáticamente en muchas situaciones: un int pasa a double sin perder información, un char a int da su código ASCII, un double a int trunca. Las dos primeras son seguras; la tercera no.

Cuando la conversión es deliberada, escríbela con static_cast<T>(valor). Deja claro que sabes lo que haces y permite que las búsquedas y los revisores la encuentren. int nota = promedio; compila, pero oculta la pérdida; int nota = static_cast<int>(promedio); la declara.

Con inicialización por llaves, el compilador rechaza el narrowing: int nota{promedio}; es un error, no un aviso. Esa es la razón por la que el curso insiste en {}.

Errores comunes

ErrorCausaSolución
error: type 'double' cannot be narrowed to 'int'Inicializaste un int con llaves usando un doubleConvierte con static_cast<int>(...)
warning: variable 'x' is uninitializedLeíste una variable sin valorInicialízala al declararla
Una media da 0Dividiste dos enterosConvierte el numerador a double
error: assignment of read-only variableIntentaste modificar una constQuita const o usa otra variable
Un unsigned muestra un valor enormeRestaste más de lo que habíaUsa un tipo con signo o comprueba antes
error: 'x' was not declared in this scopeUsaste la variable fuera de su bloqueDeclárala en el ámbito donde se usa
La llave avisa, el cast decide
Incorrecto
PYTHON No compila
#include <iostream>

int main() {
    double promedio = 7.5;
    int nota{promedio};
    std::cout << nota << '\n';
    return 0;
}
Correcto
PYTHON Correcto
#include <iostream>

int main() {
    double promedio = 7.5;
    int nota{static_cast<int>(promedio)};
    std::cout << nota << '\n';
    return 0;
}

La inicialización con llaves prohíbe perder información de forma implícita. `static_cast<int>` dice explícitamente que aceptas el truncamiento: la salida es 7, no 8.

Sobre el desbordamiento: con enteros con signo es comportamiento indefinido, así que no hay un valor «esperado». Con unsigned la vuelta está definida, pero casi nunca es lo que quieres. Si un cálculo puede crecer, usa long long o double y comprueba los límites.

Buenas prácticas

  • Inicializa siempre: int total{0}; cuesta lo mismo que int total; y evita basura.
  • Declara const por defecto y relájalo solo si necesitas modificar.
  • Usa constexpr para constantes conocidas al compilar.
  • No abuses de auto: úsalo cuando el tipo sea obvio o irrelevante para la lectura.
  • Convierte con static_cast, nunca con casts estilo C.
  • Evita los números mágicos: dales nombre con constexpr.
  • Elige double para cálculos con decimales; float solo si el ahorro de memoria importa.

Ejercicios

Ejercicio 1

Presupuesto de cómputo

Una hora de GPU cuesta 2.40 USD y tienes 7 horas contratadas con un descuento del 15 %. Calcula el bruto, el ahorro y el total con variables constexpr para los tres datos de entrada, y muestra los resultados con dos decimales. Comprueba que el total es 14.28.
Pista

El descuento se aplica sobre el bruto: bruto * 0.15. Usa std::fixed y std::setprecision(2).

Ejercicio 2

Promedio sin perder decimales

Guarda tres notas enteras (14, 17 y 11), calcula su suma y su promedio, y muestra ambos con dos decimales. El promedio debe ser 14.00 y no 14. Cambia una nota a 16 y comprueba que el promedio pasa a 15.67.
Pista

La suma es int; convierte la suma a double antes de dividir entre 3.

Completar código Área del círculo
#include <iostream>

int main() {
    constexpr double pi = ;
    double radio = 2.0;
    auto area = pi * radio * ;
    std::cout << area << '\n';
    return ;
}

El área es π por el radio al cuadrado. El retorno de `main` indica éxito.

Soluciones explicadas

En el presupuesto, la decisión importante es el tipo de cada dato: horas es un conteo (int), mientras que costo y descuento son reales. Si declararas costo_hora como int, perderías los 40 centavos y el total cambiaría. constexpr documenta que esos valores se conocen al compilar y evita que alguien los modifique por accidente.

En el promedio, el error típico es dividir antes de convertir. suma / 3 se evalúa como división entera y produce 14; después, asignar ese 14 a un double no recupera los decimales perdidos. La conversión tiene que ocurrir antes de la división, y por eso el static_cast envuelve a suma.

En el área, auto no adivina: deduce del inicializador. Como pi es double y radio es double, el producto también lo es. Escribir auto area = 3; habría dado un int, un recordatorio de que la deducción depende por completo de la expresión de la derecha.

Resumen

  • Una variable es un objeto con nombre, tipo, valor y dirección.
  • El tipo decide el tamaño, las operaciones válidas y las conversiones.
  • Inicializa siempre; una variable sin inicializar tiene un valor indeterminado.
  • const impide reasignar; constexpr exige un valor conocido al compilar.
  • auto deduce el tipo del inicializador, no del uso posterior.
  • La división entre enteros trunca; convierte a double antes de dividir.
  • static_cast<T>(x) hace explícita una conversión que puede perder información.
  • El desbordamiento con signo es comportamiento indefinido; respeta los límites del tipo.

Evaluación

Evaluación 1

¿Qué tipo y qué valor tiene auto x = 7 / 2;?

Evaluación 2

¿Qué exige constexpr que no exige const?

Evaluación 3

¿Por qué int nota{7.5}; no compila?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.