Saltar a contenido

Tipos de Almacenamiento y Modificadores en C++

Introducción

Los especificadores de almacenamiento y modificadores en C++ permiten controlar: - Dónde se almacenan las variables (stack, data section, registro) - Cuánto tiempo viven (duración) - Cómo se pueden modificar (mutabilidad) - Cómo el compilador las optimiza

Importante: Muchos de estos son directivas para el compilador. A bajo nivel (reversing), el código ya está compilado y optimizado, por lo que no veremos estas palabras clave directamente.

Especificador auto

Versión Antigua (Pre-C++11)

auto int i = 5;  // Obsoleto, redundante
  • Indicaba una variable local con almacenamiento automático
  • Era completamente innecesario (por defecto las variables locales son automáticas)
  • Nadie lo usaba en la práctica

Versión Moderna (C++11+)

auto i = 3.2;    // Tipo deducido: double
auto x = 5;      // Tipo deducido: int
auto str = "hi"; // Tipo deducido: const char*

Nuevo significado: El compilador deduce automáticamente el tipo basándose en el inicializador.

Cambio Clave

// ❌ ERROR en C++11+: No se pueden combinar dos tipos
auto int i = 3.2;  // Error: 'auto' y 'int' son ambos tipos

// ✅ CORRECTO: auto deduce el tipo
auto i = 3.2;      // i es double

Detección del Tipo

En Visual Studio, al pasar el mouse sobre la variable:

auto i = 3.2;  // Mouse over 'i' → muestra "double"
auto x = 5;    // Mouse over 'x' → muestra "int"

A Bajo Nivel

; auto i = 3.2;
movsd   xmm0, QWORD PTR __real@400999999999999a
movsd   QWORD PTR i$[rsp], xmm0  ; Almacenado como double (8 bytes)

El compilador ya decidió el tipo (double). No hay rastro de la palabra auto en ensamblador.

Especificador static

Concepto

static int z = 5;

Efecto: Convierte una variable local en variable global en términos de: - Almacenamiento: Se coloca en la sección .data (no en el stack) - Duración: Existe durante toda la ejecución del programa - Inicialización: Se inicializa una sola vez antes del main()

Ejemplo

void funcion() {
    static int x = 10;  // Inicializado solo la primera vez
    x++;
    cout << x << endl;
}

int main() {
    funcion();  // Output: 11
    funcion();  // Output: 12
    funcion();  // Output: 13
    return 0;
}

Comportamiento

Llamada Valor de x Explicación
11 Inicializa x=10, luego x++ → 11
12 x ya es 11, luego x++ → 12
13 x ya es 12, luego x++ → 13

Sin static:

void funcion() {
    int x = 10;  // Reinicializado en cada llamada
    x++;
    cout << x << endl;  // Siempre output: 11
}

A Bajo Nivel

void funcion() {
    static int z = 5;
}

Ensamblador:

_DATA SEGMENT
int z DD 05H    ; z está en la sección .data (global)
_DATA ENDS

Comparación:

Variable Ubicación Duración
Local normal Stack Hasta que termine la función
static local .data Toda la ejecución del programa
Global .data Toda la ejecución del programa

Conclusión: static convierte efectivamente una variable local en global a nivel de almacenamiento.

Cita del Compilador

"En realidad, el compilador analiza el código fuente y crea el ejecutable necesario para crear todas las variables estáticas antes de empezar la ejecución del programa, asignando sus valores iniciales si se indica. De modo que cuando se llama a la función, no se crea el objeto x, sino que se usa directamente."

Especificador extern

Concepto

extern int global_x;

Propósito: Declarar que una variable está definida en otro archivo del proyecto.

Uso con Múltiples Archivos

Escenario: Proyecto con 2 Archivos

main.cpp:

#include <iostream>
using namespace std;

extern int global_x;  // Declaración: "existe en otro lado"

void print_global();  // Declaración de función

int main() {
    cout << global_x << endl;
    print_global();
    return 0;
}

functions.cpp:

#include <iostream>
using namespace std;

int global_x = 5;  // Definición: aquí se reserva memoria

void print_global() {
    cout << "Global: " << global_x << endl;
}

Diferencia: Declaración vs Definición

extern int global_x;  // DECLARACIÓN: no reserva memoria
int global_x = 5;     // DEFINICIÓN: reserva memoria e inicializa

Funciones y extern

Las funciones son extern por defecto:

// functions.cpp
void print_global();  // Implícitamente extern

// main.cpp
void print_global();  // Puede llamarla sin extern

No necesitan extern explícito, pero las variables sí.

¿Qué Pasa sin extern?

Sin extern (❌ Error)

main.cpp:

int global_x;  // Sin extern
void print_global();

functions.cpp:

int global_x = 5;
void print_global() {
    cout << global_x << endl;  // ❌ Error: global_x no definido
}

Error del compilador:

error LNK2001: unresolved external symbol "int global_x"

Con extern (✅ Correcto)

main.cpp:

extern int global_x;  // "Búscalo en otro archivo"

El compilador busca en todos los .cpp del proyecto y encuentra la definición en functions.cpp.

A Bajo Nivel

No hay diferencia. Una vez compilado:

; Uso de global_x desde cualquier archivo
mov     eax, DWORD PTR global_x

El linker resuelve todas las referencias a global_x a la misma dirección de memoria.

Resumen

Palabra Significado Ejemplo
extern int x; "x existe en otro archivo" Declaración
int x = 5; "x se define aquí" Definición

Especificador register

Concepto

register int test = 3;

Intención: Sugerir al compilador que almacene la variable en un registro de CPU en lugar de memoria.

Realidad

Es solo una sugerencia. El compilador: - ✅ Puede ignorarla completamente - ✅ Decide según optimizaciones (/O1, /O2) - ✅ Ya optimiza automáticamente sin necesidad de register

Ejemplo

void funcion() {
    register int test = 3;
    cout << test << endl;
}

Ensamblador (sin optimización)

mov     DWORD PTR test$[rsp], 3  ; ¡Almacenado en stack, no en registro!
mov     ecx, DWORD PTR test$[rsp]

El compilador ignoró register porque no había optimización habilitada.

Con Optimización

Con /O2, el compilador podría hacer:

mov     ecx, 3              ; Directamente en registro
; No hay acceso al stack

Pero lo haría igual sin register.

Conclusión

  • ⚠️ No se usa en código moderno
  • ⚠️ El compilador optimiza mejor que las sugerencias manuales
  • ⚠️ A bajo nivel, no hay diferencia observable

Modificador const

Concepto

const int a = 5;

Efecto: La variable es de solo lectura. No se puede modificar después de inicializarla.

Reglas

  1. Debe inicializarse en la declaración
  2. No se puede modificar después
  3. Se puede leer y usar en expresiones

Ejemplos Básicos

Variable Constante Simple

const int a = 5;
a++;           // ❌ ERROR: no se puede modificar
int b = a + 2; // ✅ OK: leer es válido

Error del compilador:

error C3892: 'a': you cannot assign to a variable that is const

Array Constante

const int pepe[5] = {1, 2, 3, 4, 5};  // ✅ Inicialización obligatoria
pepe[0] = 10;                         // ❌ ERROR: no modificable
int x = pepe[0];                      // ✅ OK: lectura válida

Sin inicialización:

const int arr[5];  // ❌ ERROR: const necesita inicializador

Punteros a Constantes

const int pepe[5] = {1, 2, 3, 4, 5};
const int* p = pepe;  // Puntero a enteros constantes

cout << p[0] << endl;  // ✅ OK: leer
p[0] = 10;             // ❌ ERROR: modificar

Funciones con Parámetros Constantes

Caso 1: Puntero a Constante

void funcion(const int* x) {
    cout << x[0] << endl;  // ✅ OK: leer
    x[0] = 5;              // ❌ ERROR: modificar
}

int main() {
    const int pepe[5] = {1, 2, 3, 4, 5};
    funcion(pepe);  // ✅ Tipos compatibles
}

Protección: La función no puede modificar el contenido apuntado.

Caso 2: Referencia Constante

void funcion2(const int& x) {
    cout << x << endl;  // ✅ OK: leer
    x = 5;              // ❌ ERROR: modificar
}

int main() {
    const int a = 5;
    funcion2(a);  // Pasa por referencia, pero protegido
}

Ventaja: Se pasa por referencia (eficiente) pero se protege contra modificación.

Caso 3: Valor de Retorno Constante

int c = 5;  // Global

const int& funcion3() {
    return c;  // Retorna referencia constante a 'c'
}

int main() {
    cout << funcion3() << endl;  // ✅ OK: leer
    funcion3()++;                // ❌ ERROR: no se puede modificar el retorno

    c++;  // ✅ OK: 'c' en sí no es constante, solo el retorno
}

Comportamiento: - Retorno constante: No se puede modificar el valor retornado directamente - Variable original: c sigue siendo modificable fuera del retorno

Ensamblador

; const int a = 5;
mov     DWORD PTR a$[rsp], 5

; a++; (si intentáramos compilar, falla antes)
; El compilador previene esta instrucción

A bajo nivel no hay "constantes" en el sentido de protección de hardware. Es una verificación del compilador en tiempo de compilación.

Punteros Constantes vs Punteros a Constantes

Diferencia Clave

Tipo Sintaxis Constante Modificable
Puntero a constante const int* p Contenido Puntero
Puntero constante int* const p Puntero Contenido

Puntero Constante

int* const p2 = new int(5);

El puntero es constante, el contenido no:

*p2 = 6;    // ✅ OK: modificar contenido
p2++;       // ❌ ERROR: no se puede cambiar el puntero
p2 = new int(10);  // ❌ ERROR: reasignación

Analogía: Una flecha permanente que apunta a un cuaderno donde puedes escribir.

Ensamblador

; int* const p2 = new int(5);
call    operator new
mov     QWORD PTR p2$[rsp], rax  ; Dirección fija

; *p2 = 6;
mov     rax, QWORD PTR p2$[rsp]
mov     DWORD PTR [rax], 6       ; Modificar contenido OK

Puntero a Constante

const int* p3 = new int(5);

El contenido es constante, el puntero no:

*p3 = 6;           // ❌ ERROR: no se puede modificar contenido
p3 = new int(10);  // ✅ OK: cambiar a qué apunta
p3++;              // ✅ OK: mover el puntero

Analogía: Una flecha móvil que apunta a un cuaderno de solo lectura.

Ensamblador

; const int* p3 = new int(5);
call    operator new
mov     QWORD PTR p3$[rsp], rax

; p3 = new int(6);
call    operator new
mov     QWORD PTR p3$[rsp], rax  ; Reasignar puntero OK

Tabla Comparativa

int valor = 10;

// 1. Puntero normal
int* p1 = &valor;
*p1 = 20;    // ✅ OK
p1++;        // ✅ OK

// 2. Puntero constante
int* const p2 = &valor;
*p2 = 20;    // ✅ OK
p2++;        // ❌ ERROR

// 3. Puntero a constante
const int* p3 = &valor;
*p3 = 20;    // ❌ ERROR
p3++;        // ✅ OK

// 4. Puntero constante a constante
const int* const p4 = &valor;
*p4 = 20;    // ❌ ERROR
p4++;        // ❌ ERROR

Regla Mnemotécnica

Lee de derecha a izquierda:

const int* p;      // p es un puntero a (int constante)
int* const p;      // p es un (puntero constante) a int
const int* const p;// p es un (puntero constante) a (int constante)

Modificador volatile

Concepto

volatile int a = 5;

Propósito: Indicar al compilador que no optimice el acceso a esta variable porque puede ser modificada externamente.

¿Por Qué Existe?

int a = 5;

// Loop que espera cambio
while (a != 10) {
    // Esperar...
}

Sin volatile: El compilador optimiza:

mov     eax, DWORD PTR a
$loop:
cmp     eax, 10          ; Usa el valor cacheado en EAX
jne     SHORT $loop      ; ¡Nunca sale si 'a' cambia en memoria!

Con volatile:

$loop:
mov     eax, DWORD PTR a ; Lee de memoria CADA vez
cmp     eax, 10
jne     SHORT $loop

Casos de Uso

  1. Variables modificadas por interrupciones

    volatile bool interrupt_flag = false;
    
    void ISR() {  // Interrupt Service Routine
        interrupt_flag = true;
    }
    

  2. Variables en memoria mapeada a hardware

    volatile uint32_t* PORT = (uint32_t*)0x40021000;
    *PORT = 0xFF;  // Siempre escribe, nunca optimiza
    

  3. Programación multihilo (aunque se prefiere std::atomic)

    volatile bool running = true;
    
    void thread1() {
        while (running) { /* ... */ }
    }
    
    void thread2() {
        running = false;  // Debe reflejarse en thread1
    }
    

Cita del Material

"El compilador puede almacenar el valor leído la primera vez en un registro o en la memoria caché. Incluso si el compilador sabe que no ha modificado su valor, no actualizarlo. Si el valor se modifica internamente sin que el programa sea notificado, se pueden producir errores ya que estamos trabajando con un valor no válido. Utilizando el modificador volatile obligamos al compilador a consultar el valor de la variable en la memoria cada vez que se deba acceder a ella."

A Bajo Nivel

Sin volatile:

mov     eax, DWORD PTR a$[rsp]  ; Lee una vez
; ... usa EAX múltiples veces

Con volatile:

mov     eax, DWORD PTR a$[rsp]  ; Lee cada vez que se necesita
; Usa
mov     eax, DWORD PTR a$[rsp]  ; Lee de nuevo
; Usa

Modificador mutable

Concepto

struct Data {
    int x;
    mutable int y;
};

Propósito: Permitir que un miembro de un objeto constante sea modificable.

Ejemplo

struct stA {
    int x;
    int y;
};

struct stB {
    int a;
    mutable int b;
};

int main() {
    const stA A = {1, 2};  // Objeto constante
    const stB B = {3, 4};  // Objeto constante

    A.x = 5;  // ❌ ERROR: objeto constante
    B.a = 5;  // ❌ ERROR: objeto constante
    B.b = 5;  // ✅ OK: 'b' es mutable
}

Tabla Comparativa

Objeto Miembro const mutable ¿Modificable?
const stA A A.x ❌ No
const stA A A.y ❌ No
const stB B B.a ❌ No
const stB B B.b ✅ Sí

Casos de Uso

1. Caché en Objetos Inmutables

class ExpensiveCalculation {
    int input;
    mutable int cached_result;
    mutable bool is_cached;

public:
    ExpensiveCalculation(int i) : input(i), is_cached(false) {}

    int calculate() const {  // Función const
        if (!is_cached) {
            cached_result = /* cálculo costoso */;
            is_cached = true;  // ✅ OK: mutable
        }
        return cached_result;
    }
};

2. Mutex en Objetos Const

class ThreadSafe {
    mutable std::mutex mtx;  // Debe ser mutable
    int data;

public:
    int get_data() const {
        std::lock_guard<std::mutex> lock(mtx);  // ✅ OK: mtx es mutable
        return data;
    }
};

3. Contadores de Acceso

class Logger {
    std::string message;
    mutable int access_count;

public:
    const std::string& get_message() const {
        access_count++;  // ✅ OK: contar accesos sin cambiar "estado lógico"
        return message;
    }
};

Ensamblador

; const stB B = {3, 4};
mov     DWORD PTR B$[rsp], 3
mov     DWORD PTR B$[rsp+4], 4

; B.b = 5;  (mutable permite esto)
mov     DWORD PTR B$[rsp+4], 5  ; ✅ Compilador permite la modificación

A bajo nivel no hay distinción entre mutable y no-mutable. Es una verificación del compilador.

Resumen de Especificadores y Modificadores

Tabla General

Palabra Categoría Propósito Impacto en Reversing
auto (viejo) Especificador Variable local automática Ninguno (redundante)
auto (C++11+) Deducción de tipo Inferir tipo del inicializador Ninguno (tipo resuelto)
static Especificador Variable con duración global Variable en .data en vez de stack
extern Especificador Declarar variable externa Ninguno (linker resuelve)
register Especificador Sugerir uso de registro Ninguno (ignorado)
const Modificador Inmutable Verificación en compilador
volatile Modificador No optimizar accesos Fuerza lectura de memoria
mutable Modificador Excepción en objetos const Permite modificación

Verificación: Compilador vs Hardware

Modificador Momento de Verificación
const ✅ Compilación (error si viola)
volatile ⚙️ Runtime (afecta generación de código)
static ⚙️ Linker (ubicación en memoria)
mutable ✅ Compilación (excepción a const)

Impacto en Reversing

La mayoría de estos modificadores son invisibles en el código desensamblado:

// Código fuente
const int a = 5;
volatile int b = 10;
static int c = 15;

Ensamblador (todos se ven igual):

mov     DWORD PTR a$[rsp], 5    ; const
mov     DWORD PTR b$[rsp], 10   ; volatile
mov     DWORD PTR c, 15         ; static (en .data)

Excepciones visibles: - static → variable en .data en lugar de stack - volatile → múltiples lecturas de memoria sin caché en registro

Análisis del Código de Ejemplo

Ejemplo 1: Programa con cin.get()

#include <iostream>
using namespace std;

bool final();

int main() {
    bool flag = 0;
    while (true) {
        flag = final();
        if (flag) {
            break;
        }
    }
    return 0;
}

bool final() {
    char c;
    cout << "Pulse una tecla o Q para terminar " << endl;
    c = cin.get();
    cin.ignore(1000, '\n');

    if (c == 'Q') {
        return 1;
    }
    else {
        cout << "Tipeaste 0x" << hex << (int)c 
             << " entonces seguimos" << endl << endl;
        return 0;
    }
}

Análisis Ensamblador

Loop Principal:

main:
    mov     BYTE PTR flag$[rsp], 0  ; flag = 0

$LN2@main:
    xor     eax, eax
    cmp     eax, 1
    je      SHORT $LN3@main         ; while (true) - siempre falso

    call    bool final(void)
    mov     BYTE PTR flag$[rsp], al ; flag = retorno

    movzx   eax, BYTE PTR flag$[rsp]
    test    eax, eax
    je      SHORT $LN4@main         ; if (!flag) continuar
    jmp     SHORT $LN3@main         ; else salir

$LN4@main:
    jmp     SHORT $LN2@main         ; Loop

$LN3@main:
    xor     eax, eax                ; return 0
    ret     0

Función final():

bool final(void):
    ; cout << "Pulse..."
    lea     rdx, OFFSET FLAT:$SG35272
    mov     rcx, QWORD PTR std::cout
    call    std::operator<<

    ; c = cin.get()
    mov     rcx, QWORD PTR std::cin
    call    std::basic_istream::get
    mov     BYTE PTR c$[rsp], al

    ; cin.ignore(1000, '\n')
    mov     r8d, 10                 ; '\n'
    mov     edx, 1000
    mov     rcx, QWORD PTR std::cin
    call    std::basic_istream::ignore

    ; if (c == 'Q')
    movsx   eax, BYTE PTR c$[rsp]
    cmp     eax, 81                 ; 0x51 = 'Q'
    jne     SHORT $LN2@final

    mov     al, 1                   ; return true
    jmp     SHORT $LN1@final

$LN2@final:
    ; cout << "Tipeaste 0x" << hex << (int)c
    movsx   eax, BYTE PTR c$[rsp]
    ; ... impresión hexadecimal
    xor     al, al                  ; return false

$LN1@final:
    ret     0

Ejemplo 2: Programa con const, volatile, mutable

#include <iostream>
using namespace std;

void funcion(const int* x);
void funcion2(const int& x);
const int& funcion3();

int c = 5;              // Global
volatile int a = 5;     // Volatile

struct stA {
    int y;
    int x;
};

struct stB {
    int a;
    mutable int b;
};

int main() {
    const int a = 5;
    const int pepe[5] = {1, 2, 3, 4, 5};
    const int* p = pepe;

    funcion(pepe);
    funcion2(a);
    cout << funcion3() << endl;

    // Puntero constante
    int* const p2 = new int(5);
    *p2 = 6;  // OK

    // Puntero a constante
    const int* p3 = new int(5);
    p3 = new int(6);  // OK

    // Estructuras constantes
    const stA A = {1, 2};
    const stB B = {3, 4};
    B.b = 5;  // OK: mutable

    return 0;
}

Análisis Ensamblador

Variable Global en .data:

_DATA SEGMENT
int c DD 05H    ; Variable global 'c'
_DATA ENDS

Variables Locales:

main:
    ; const int a = 5;
    mov     DWORD PTR a$[rsp], 5

    ; const int pepe[5] = {1,2,3,4,5};
    mov     DWORD PTR pepe$[rsp], 1
    mov     DWORD PTR pepe$[rsp+4], 2
    mov     DWORD PTR pepe$[rsp+8], 3
    mov     DWORD PTR pepe$[rsp+12], 4
    mov     DWORD PTR pepe$[rsp+16], 5

    ; const int* p = pepe;
    lea     rax, QWORD PTR pepe$[rsp]
    mov     QWORD PTR p$[rsp], rax

Llamadas a Funciones:

    ; funcion(pepe);
    lea     rcx, QWORD PTR pepe$[rsp]
    call    void funcion(int const *)

    ; funcion2(a);
    lea     rcx, QWORD PTR a$[rsp]  ; Pasar dirección (referencia)
    call    void funcion2(int const &)

    ; cout << funcion3() << endl;
    call    int const & funcion3(void)
    mov     edx, DWORD PTR [rax]    ; Leer valor retornado
    mov     rcx, QWORD PTR std::cout
    call    std::basic_ostream::operator<<

Punteros:

    ; int* const p2 = new int(5);
    mov     ecx, 4
    call    operator new
    mov     QWORD PTR p2$[rsp], rax  ; Puntero constante
    mov     rax, QWORD PTR p2$[rsp]
    mov     DWORD PTR [rax], 5

    ; *p2 = 6;
    mov     rax, QWORD PTR p2$[rsp]
    mov     DWORD PTR [rax], 6       ; Modificar contenido OK

Estructuras:

    ; const stA A = {1, 2};
    mov     DWORD PTR A$[rsp], 1
    mov     DWORD PTR A$[rsp+4], 2

    ; const stB B = {3, 4};
    mov     DWORD PTR B$[rsp], 3
    mov     DWORD PTR B$[rsp+4], 4

    ; B.b = 5;  (mutable)
    mov     DWORD PTR B$[rsp+4], 5   ; ✅ Permitido

Funciones Auxiliares:

void funcion(int const *):
    ; cout << x[0] << endl;
    mov     eax, 4
    imul    rax, 0                   ; x[0]
    mov     rcx, QWORD PTR x$[rsp]
    mov     edx, DWORD PTR [rcx+rax] ; Leer x[0]
    mov     rcx, QWORD PTR std::cout
    call    std::basic_ostream::operator<<
    ret     0

void funcion2(int const &):
    ; cout << x << endl;
    mov     rax, QWORD PTR x$[rsp]   ; x es referencia (puntero)
    mov     edx, DWORD PTR [rax]     ; Leer *x
    mov     rcx, QWORD PTR std::cout
    call    std::basic_ostream::operator<<
    ret     0

int const & funcion3(void):
    lea     rax, OFFSET FLAT:int c   ; Retornar dirección de 'c'
    ret     0

Observaciones para Reversing

1. Invisibilidad de Modificadores

En el código desensamblado: - No puedes distinguir const vs no-const - No puedes ver mutable - auto ya está resuelto a un tipo concreto

2. Excepciones Visibles

static local:

void func() {
    static int x = 5;  // En .data
}
_DATA SEGMENT
int x DD 05H    ; ← Visible: está en .data, no en stack
_DATA ENDS

volatile:

volatile int x;
x = 5;
y = x + x;  // Lee x dos veces
mov     DWORD PTR x, 5
mov     eax, DWORD PTR x    ; Lee 1
mov     ecx, DWORD PTR x    ; Lee 2 (no usa caché en EAX)
add     eax, ecx

3. Referencias vs Punteros

A bajo nivel son idénticos:

void func1(const int& x);   // Referencia
void func2(const int* x);   // Puntero
; Ambas reciben una dirección en RCX
func1:
    mov     rax, QWORD PTR x$[rsp]
    mov     eax, DWORD PTR [rax]

func2:
    mov     rax, QWORD PTR x$[rsp]
    mov     eax, DWORD PTR [rax]
    ; ← Código idéntico

4. Optimizaciones

El compilador puede: - Eliminar variables no usadas (incluso volatile si no hay side effects) - Inline funciones pequeñas - Reordenar código (respetando volatile) - Optimizar constantes en tiempo de compilación