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.
- 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
- ¿Qué es una variable?
- ¿Para qué sirve?
- ¿Cómo funciona por dentro?
- Sintaxis
- Ejemplo básico
- Ejemplo intermedio: precedencia y división entera
- Caso práctico: normalizar una característica
- Visualización: qué guarda cada variable
- Conversiones y static_cast
- Errores comunes
- Buenas prácticas
- Ejercicios
- Soluciones explicadas
- Resumen
- 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
tasaointentos. - 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
autodeduzca 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.
#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.
| Forma | Ejemplo | Qué hace |
|---|---|---|
| Sin inicializar | int x; | Reserva memoria; el valor es indeterminado |
| Copia | int x = 5; | Inicializa con 5; permite conversiones implícitas |
| Llaves | int x{5}; | Inicializa con 5; rechaza narrowing |
| Paréntesis | int x(5); | Inicializa con 5; ambigua en algunos contextos |
| Constante | const int x = 5; | No se puede reasignar después |
| Constante de compilación | constexpr int x = 5; | Valor conocido al compilar |
| Deducción | auto 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
#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.
#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.
#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.
#include <iostream>int main() { int a = 7; int b = 2; int cociente = a / b; double exacto = static_cast<double>(a) / b; int resto = a % b; std::cout << cociente << ' ' << exacto << ' ' << resto << '\n'; return 0;}a ocupa 4 bytes y guarda 7.—
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
| Error | Causa | Solución |
|---|---|---|
error: type 'double' cannot be narrowed to 'int' | Inicializaste un int con llaves usando un double | Convierte con static_cast<int>(...) |
warning: variable 'x' is uninitialized | Leíste una variable sin valor | Inicialízala al declararla |
| Una media da 0 | Dividiste dos enteros | Convierte el numerador a double |
error: assignment of read-only variable | Intentaste modificar una const | Quita const o usa otra variable |
Un unsigned muestra un valor enorme | Restaste más de lo que había | Usa un tipo con signo o comprueba antes |
error: 'x' was not declared in this scope | Usaste la variable fuera de su bloque | Declárala en el ámbito donde se usa |
#include <iostream>
int main() {
double promedio = 7.5;
int nota{promedio};
std::cout << nota << '\n';
return 0;
}#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 queint total;y evita basura. - Declara
constpor defecto y relájalo solo si necesitas modificar. - Usa
constexprpara 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
doublepara cálculos con decimales;floatsolo si el ahorro de memoria importa.
Ejercicios
Presupuesto de cómputo
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).
Promedio sin perder decimales
Pista
La suma es int; convierte la suma a double antes de dividir entre 3.
#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.
constimpide reasignar;constexprexige un valor conocido al compilar.autodeduce el tipo del inicializador, no del uso posterior.- La división entre enteros trunca; convierte a
doubleantes 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
¿Qué tipo y qué valor tiene auto x = 7 / 2;?
¿Qué exige constexpr que no exige const?
¿Por qué int nota{7.5}; no compila?
¿Ya lo entendiste?
Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.