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)¶
- 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:
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¶
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 |
|---|---|---|
| 1ª | 11 | Inicializa x=10, luego x++ → 11 |
| 2ª | 12 | x ya es 11, luego x++ → 12 |
| 3ª | 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¶
Ensamblador:
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¶
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:
functions.cpp:
int global_x = 5;
void print_global() {
cout << global_x << endl; // ❌ Error: global_x no definido
}
Error del compilador:
Con extern (✅ Correcto)¶
main.cpp:
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:
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¶
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¶
Ensamblador (sin optimización)¶
El compilador ignoró register porque no había optimización habilitada.
Con Optimización¶
Con /O2, el compilador podría hacer:
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¶
Efecto: La variable es de solo lectura. No se puede modificar después de inicializarla.
Reglas¶
- Debe inicializarse en la declaración
- No se puede modificar después
- Se puede leer y usar en expresiones
Ejemplos Básicos¶
Variable Constante Simple¶
Error del compilador:
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:
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¶
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¶
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¶
Propósito: Indicar al compilador que no optimice el acceso a esta variable porque puede ser modificada externamente.
¿Por Qué Existe?¶
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:
Casos de Uso¶
-
Variables modificadas por interrupciones
-
Variables en memoria mapeada a hardware
-
Programación multihilo (aunque se prefiere
std::atomic)
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
volatileobligamos 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:
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¶
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:
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:
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:
volatile:
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:
; 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