Ir al contenido

Tipos, Valores y el Sistema de Tipos

Explora los tipos fundamentales de C++, los enteros de ancho fijo, las formas de inicializacion, la deduccion con auto y el sistema const que impone correccion en tiempo de compilacion.

El Sistema de Tipos: Tu Primera Linea de Defensa

C++ es un lenguaje de tipado estatico — cada expresion tiene un tipo conocido en tiempo de compilacion. El sistema de tipos no es solo contabilidad; es tu herramienta mas poderosa para detectar errores antes de que tu codigo se ejecute. Un programa C++ bien tipado lleva sus invariantes en el propio sistema de tipos, haciendo imposibles clases enteras de errores.

El tipo de una variable determina tres cosas: cuanta memoria ocupa, que valores puede representar y que operaciones son validas sobre ella. Elegir los tipos correctos es la base del codigo C++ correcto.

Tipos Fundamentales

C++ proporciona un conjunto de tipos integrados que se mapean directamente a las capacidades del hardware. Sus tamanos exactos varian segun la plataforma, pero el estandar garantiza rangos minimos:

types.cppc++23
#include <iostream>
#include <cstdint>   // Fixed-width integer types
#include <limits>    // std::numeric_limits

int main() {
    // Boolean
    bool is_valid = true;           // true or false, typically 1 byte

    // Character types
    char letter = 'A';             // At least 8 bits, holds ASCII
    char8_t  u8ch  = u8'A';       // C++20: exactly 8 bits, UTF-8
    char16_t u16ch = u'\u00E9';   // At least 16 bits, UTF-16
    char32_t u32ch = U'\U0001F600'; // At least 32 bits, UTF-32

    // Integer types (sizes are minimums, typical x86-64 shown)
    short       s = 32000;         // At least 16 bits (16)
    int         i = 2'000'000;     // At least 16 bits (32) — digit separator C++14
    long        l = 100'000L;      // At least 32 bits (64 on Linux, 32 on Windows)
    long long  ll = 9'000'000'000'000'000'000LL;  // At least 64 bits

    // Unsigned variants
    unsigned int ui = 42u;         // Cannot be negative

    // Floating-point types
    float       f = 3.14f;         // ~7 decimal digits precision
    double      d = 3.141592653589793; // ~15 decimal digits precision
    long double ld = 3.14159265358979323846L;

    // Fixed-width types from <cstdint> — use these when size matters
    std::int32_t  exact = 42;      // Exactly 32 bits, guaranteed
    std::uint64_t big   = 18'446'744'073'709'551'615ULL; // Exactly 64 bits
    std::int_fast32_t fast = 42;   // At least 32 bits, fastest available
    std::size_t index = 0;         // Unsigned, suitable for sizes and indices

    std::cout << "int: " << sizeof(int) << " bytes, max = "
              << std::numeric_limits<int>::max() << '\n';
}
Salida

Pulsa Ejecutar para compilar y ejecutar este ejemplo.

Inicializacion: La Forma Correcta

C++ ha evolucionado multiples sintaxis de inicializacion. La inicializacion uniforme (con llaves) {} es preferida en C++ moderno porque previene conversiones estrechas:

initialization.cppc++23
#include <iostream>
#include <string>

int main() {
    // Direct initialization
    int a(42);              // OK but can be confused with function declaration

    // Copy initialization
    int b = 42;             // OK, familiar from C

    // Uniform (brace) initialization — preferred in modern C++
    int c{42};              // Direct-list-initialization
    int d = {42};           // Copy-list-initialization

    // Brace initialization PREVENTS narrowing conversions
    // double to int loses precision — the compiler will ERROR:
    // int narrow{3.14};    // ERROR: narrowing conversion
    int ok = 3.14;          // Compiles! Silently truncates to 3 (dangerous)

    // Value initialization (zero for built-in types)
    int zero{};             // Guaranteed to be 0
    double dzero{};         // Guaranteed to be 0.0
    bool bfalse{};          // Guaranteed to be false

    // auto deduction
    auto x = 42;            // int
    auto y = 3.14;          // double
    auto z = 'A';           // char
    auto s = std::string{"hello"};  // std::string (not const char*!)

    // decltype — queries the type of an expression
    decltype(x) another = 100;  // same type as x → int
    decltype(auto) ref = (x);   // int& because (x) is an lvalue expression

    std::cout << "zero-initialized int: " << zero << '\n';
    std::cout << "auto-deduced string: " << s << '\n';
}
Salida

Pulsa Ejecutar para compilar y ejecutar este ejemplo.

Buena práctica

En C, el puntero nulo se representa con la macro NULL, que tipicamente se expande a 0 o (void*)0. Esto causa ambiguedad en C++ porque 0 tambien es un int valido. Considera una funcion sobrecargada tanto para int como para un tipo puntero — pasar NULL (que es 0) llamaria a la sobrecarga de int, no a la del puntero.

C++11 introdujo nullptr, una palabra clave de tipo std::nullptr_t que es implicitamente convertible a cualquier tipo puntero pero no a enteros. Usa siempre nullptr en lugar de NULL o 0 para punteros nulos.

De manera similar, prefiere '\0' (el caracter nulo) cuando te refieres al caracter con valor cero, en lugar de 0, para comunicar la intencion claramente.

  • Usa siempre nullptr en lugar de NULL o 0 para punteros nulos
  • nullptr tiene su propio tipo (std::nullptr_t) y evita la ambiguedad de sobrecarga
  • NULL es una macro de C que se expande a 0 — es un entero, no un puntero
  • Usa '\0' para el caracter nulo para distinguirlo del entero 0

const y Calificadores de Tipo

El calificador const le dice al compilador (y a los lectores humanos) que un valor no debe ser modificado despues de la inicializacion. Es una de las herramientas mas importantes en C++ para escribir codigo seguro y autodocumentado.

const se lee de derecha a izquierda: const int* es un puntero a un int const (el int no puede cambiarse, el puntero si). int* const es un puntero const a int (el puntero no puede cambiarse, el int si). const int* const es un puntero const a un int const.

constexpr (C++11) va mas alla: significa que el valor debe ser computable en tiempo de compilacion. Esto permite al compilador evaluar expresiones durante la compilacion, eliminando completamente el costo en tiempo de ejecucion. Usa constexpr para constantes que realmente se conocen en tiempo de compilacion.

consteval (C++20) es aun mas estricto: marca una funcion que debe ser evaluada en tiempo de compilacion — llamarla en tiempo de ejecucion es un error de compilacion.

El calificador volatile le dice al compilador que el valor de una variable puede cambiar fuera del control del programa (por ejemplo, registros de hardware, E/S mapeada en memoria). Previene ciertas optimizaciones. No uses volatile para multihilo — usa std::atomic en su lugar.

Error común

Uno de los errores mas insidiosos en C y C++ antiguo proviene de las conversiones estrechas — conversiones implicitas que pierden informacion. Por ejemplo, asignar un double a un int trunca silenciosamente la parte fraccionaria. Asignar un int grande a un short silenciosamente desborda.

La inicializacion con llaves {} previene las conversiones estrechas en tiempo de compilacion. Esta es una de las razones mas fuertes para preferirla sobre la inicializacion con =. Si escribes int x{3.14};, el compilador emitira un error. Con int x = 3.14;, trunca silenciosamente a 3.

De manera similar, mezclar tipos con signo y sin signo en aritmetica causa conversiones implicitas que pueden producir resultados sorprendentes. Un int negativo comparado con un unsigned int se convertira primero a unsigned, convirtiendose potencialmente en un numero positivo muy grande.

  • La inicializacion con llaves {} detecta conversiones estrechas en tiempo de compilacion
  • La inicializacion por asignacion (=) permite silenciosamente el estrechamiento — se pierden datos sin advertencia
  • Mezclar tipos con signo y sin signo causa conversion implicita a unsigned
  • Un int negativo convertido a unsigned se convierte en un numero positivo muy grande

Alias de Tipo

Los alias de tipo mejoran la legibilidad y facilitan el cambio de tipos posteriormente. Prefiere using sobre typedef en C++ moderno:

aliases.cppc++23
#include <cstdint>
#include <vector>
#include <string>

// Modern style (using) — preferred
using Byte = std::uint8_t;
using StudentId = std::int32_t;
using Names = std::vector<std::string>;

// Old style (typedef) — equivalent but harder to read with complex types
typedef std::uint8_t OldByte;
typedef std::vector<std::string> OldNames;

// using works with templates; typedef does not easily
template <typename T>
using Vec = std::vector<T>;

int main() {
    Byte b{255};
    StudentId sid{12345};
    Names names{"Alice", "Bob", "Charlie"};
    Vec<double> values{1.0, 2.0, 3.0};
}
Salida

Pulsa Ejecutar para compilar y ejecutar este ejemplo.

Puntos clave
  • Cada expresion de C++ tiene un tipo conocido en tiempo de compilacion — el tipo determina el tamano, los valores validos y las operaciones validas
  • Usa tipos de ancho fijo de (int32_t, uint64_t) cuando el tamano exacto importa
  • Prefiere la inicializacion con llaves {} — previene conversiones estrechas peligrosas
  • Usa nullptr (no NULL ni 0) para punteros nulos para evitar ambiguedad de sobrecarga
  • auto deduce el tipo del inicializador; decltype consulta el tipo de una expresion sin evaluarla
  • Usa constexpr para constantes en tiempo de compilacion y 'using' para alias de tipo

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Cual es la ventaja de la inicializacion con llaves `int x{42};` sobre la inicializacion por asignacion `int x = 42;`?

2. Por que deberias usar `nullptr` en lugar de `NULL` en C++?

3. A que tipo deduce `auto x = 42;` el tipo de `x`?