Ir al contenido

Variables, Ambito y Tiempo de Vida

Aprende las cuatro duraciones de almacenamiento, como el ambito y el tiempo de vida son conceptos distintos, las trampas de inicializacion y el fiasco del orden de inicializacion estatica.

Ambito y Tiempo de Vida No Son lo Mismo

Una de las concepciones erroneas mas comunes en C++ es confundir ambito con tiempo de vida. Estan relacionados pero son conceptos distintos:

Ambito es una propiedad de tiempo de compilacion — determina donde un nombre es visible en el codigo fuente. Una variable declarada dentro de un bloque {} es visible solo dentro de ese bloque y cualquier bloque anidado.

Tiempo de vida es una propiedad de tiempo de ejecucion — determina cuando un objeto existe en memoria. El tiempo de vida de un objeto comienza cuando su constructor se completa y termina cuando su destructor inicia.

En muchos casos, ambito y tiempo de vida coinciden: una variable local es visible dentro de su bloque y vive hasta que el bloque termina. Pero pueden divergir. Una variable local static solo es visible dentro de su funcion (ambito limitado) pero vive durante todo el programa (tiempo de vida ilimitado). Un objeto asignado dinamicamente tiene un tiempo de vida que se extiende mas alla del ambito del puntero que lo creo.

Las Cuatro Duraciones de Almacenamiento

C++ define cuatro duraciones de almacenamiento que controlan cuando los objetos se crean y destruyen:

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

// Static storage duration — lives for the entire program
int global_count = 0;                    // Initialized before main()
static int file_local = 42;              // Same, but only visible in this file

void demonstrate_storage() {
    // Automatic storage duration — created on entry, destroyed on exit
    int local = 10;                      // Born here, dies at closing brace

    // Static storage duration (local scope)
    static int call_count = 0;           // Initialized ONCE, persists across calls
    ++call_count;
    std::cout << "Call #" << call_count << '\n';

    // Thread-local storage duration (C++11)
    // thread_local int per_thread = 0;  // One copy per thread, lives until thread ends
}

int main() {
    demonstrate_storage();  // Call #1
    demonstrate_storage();  // Call #2 — call_count is still alive!
    demonstrate_storage();  // Call #3

    // Dynamic storage duration — you control the lifetime
    int* heap_int = new int{99};         // Born here
    std::cout << *heap_int << '\n';
    delete heap_int;                     // Dies here — you must not forget!
    heap_int = nullptr;                  // Good practice: nullify after delete

    // In modern C++, prefer smart pointers over raw new/delete
    // auto ptr = std::make_unique<int>(99);  // Covered in later modules
}
Salida

Pulsa Ejecutar para compilar y ejecutar este ejemplo.

Formas de Inicializacion e Inicializacion a Cero

C++ tiene tres conceptos fundamentales de inicializacion:

Inicializacion por defecto — la variable se construye sin un inicializador. Para tipos integrados (int, double, etc.), esto significa que el valor es indeterminado (leerlo es comportamiento indefinido!). Para tipos clase, se llama al constructor por defecto.

Inicializacion por valor — se activa con parentesis vacios T() o llaves vacias T{}. Para tipos integrados, esto inicializa a cero: los enteros se vuelven 0, los punteros se vuelven nullptr, los booleanos se vuelven false. Para tipos clase, se llama al constructor por defecto (y los miembros se inicializan por valor si ningun constructor los inicializa).

Inicializacion a cero — una fase especifica que ocurre para variables estaticas/thread-local antes de cualquier otra inicializacion. Todas las variables estaticas se inicializan a cero antes de que el programa inicie.

Es por esto que static int x; esta garantizado a ser 0, pero un int x; local no — la variable local se inicializa por defecto (indeterminada), mientras que la variable estatica pasa primero por inicializacion a cero.

Inicializacion en la Practica

Entender cuando una variable es cero versus indeterminada es critico para evitar errores:

init_forms.cppc++23
#include <iostream>

struct Point {
    int x;
    int y;
};

static int global_int;          // Zero-initialized → 0
static Point global_point;      // Zero-initialized → {0, 0}

int main() {
    int a;                      // Default-initialized → INDETERMINATE (UB to read!)
    int b{};                    // Value-initialized → 0
    int c = int();              // Value-initialized → 0
    int d = {};                 // Value-initialized → 0

    Point p1;                   // Default-initialized → members are INDETERMINATE
    Point p2{};                 // Value-initialized → {0, 0}
    Point p3{1, 2};             // Aggregate-initialized → {1, 2}

    // Array initialization
    int arr1[5];                // Default-initialized → INDETERMINATE values!
    int arr2[5]{};              // Value-initialized → {0, 0, 0, 0, 0}
    int arr3[5]{1, 2};          // Partially initialized → {1, 2, 0, 0, 0}

    std::cout << "global_int: " << global_int << '\n';  // Safe: 0
    std::cout << "b: " << b << '\n';                    // Safe: 0
    // std::cout << a;  // DANGER: UB — reading uninitialized variable!

    std::cout << "p2: (" << p2.x << ", " << p2.y << ")\n";  // Safe: (0, 0)
    std::cout << "p3: (" << p3.x << ", " << p3.y << ")\n";  // Safe: (1, 2)
}
Salida

Pulsa Ejecutar para compilar y ejecutar este ejemplo.

Orden de Destruccion: LIFO

Los objetos con duracion de almacenamiento automatica (variables locales) se destruyen en el orden inverso de su construccion — Ultimo en Entrar, Primero en Salir (LIFO). Esto no es solo una convencion; el estandar lo garantiza.

Esto importa enormemente cuando los objetos dependen unos de otros. Si el objeto b referencia al objeto a, debes declarar a antes que b. Entonces b se destruye primero (mientras a aun esta vivo), y a se destruye segundo. Si el orden fuera al reves, el destructor de b podria intentar usar un a muerto — comportamiento indefinido.

Para variables estaticas, la destruccion ocurre en el orden inverso de construccion despues de que main() retorna. Las variables globales definidas en la misma unidad de traduccion se construyen de arriba a abajo y se destruyen de abajo a arriba. Pero entre unidades de traduccion, el orden de construccion de las globales es no especificado — este es el fiasco del orden de inicializacion estatica.

Error común

El fiasco del orden de inicializacion estatica es una de las trampas mas notorias en C++. Cuando tienes variables globales o estaticas en diferentes unidades de traduccion (diferentes archivos .cpp), el estandar de C++ no especifica el orden en que se inicializan. Si una global depende de otra de una unidad de traduccion diferente, podria usarse antes de ser construida.

La solucion clasica es el idioma de Construir en el Primer Uso: reemplaza la variable global con una funcion que retorna una referencia a una variable estatica local. Las estaticas locales estan garantizadas a inicializarse la primera vez que se llama a la funcion (y esto es seguro para hilos desde C++11).

  • El orden de inicializacion de las globales entre unidades de traduccion no esta especificado
  • Si la global A (en archivo1.cpp) depende de la global B (en archivo2.cpp), B podria no existir aun cuando A se construya
  • Solucion: usa el idioma de Construir en el Primer Uso — envuelve las globales en funciones con estaticas locales
  • La inicializacion de estaticas locales es segura para hilos desde C++11 (conocido como 'magic statics')

El Idioma de Construir en el Primer Uso

Este patron resuelve el fiasco del orden de inicializacion estatica al diferir la construccion hasta el primer uso:

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

// BAD: global depends on another global — order is unspecified across files
// std::string bad_prefix = "[LOG] ";  // What if this is used before construction?

// GOOD: Construct on First Use idiom
std::string& log_prefix() {
    static std::string prefix = "[LOG] ";  // Constructed on first call, thread-safe
    return prefix;
}

std::string& app_name() {
    static std::string name = "MyApp";
    return name;
}

void log(const std::string& message) {
    // Safe: log_prefix() and app_name() are guaranteed to be initialized
    std::cout << log_prefix() << app_name() << ": " << message << '\n';
}

int main() {
    log("Starting up");
    log("Ready");
}
Salida

Pulsa Ejecutar para compilar y ejecutar este ejemplo.

Puntos clave
  • El ambito (donde un nombre es visible) y el tiempo de vida (cuando un objeto existe) son conceptos distintos que pueden divergir
  • C++ tiene cuatro duraciones de almacenamiento: automatica (pila), estatica (tiempo de vida del programa), thread-local y dinamica (monton)
  • Los tipos integrados inicializados por defecto tienen valores indeterminados — leerlos es comportamiento indefinido
  • La inicializacion por valor con {} garantiza la inicializacion a cero para tipos integrados
  • Las variables locales se destruyen en orden LIFO — declara las dependencias antes de sus dependientes
  • Usa el idioma de Construir en el Primer Uso para evitar el fiasco del orden de inicializacion estatica entre unidades de traduccion

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Cual es el valor de un `int x;` local (sin inicializador) en C++?

2. Que es el fiasco del orden de inicializacion estatica?

3. Como se diferencia `static int count = 0;` dentro de una funcion de `int count = 0;`?