CodeGym /Cursos /C++ SELF /Variables en memoria: direcciones, referencias y punteros...

Variables en memoria: direcciones, referencias y punteros

C++ SELF
Nivel 6, Lección 3
Disponible

1. La variable como objeto en memoria: valor y dirección

Imagina la memoria del ordenador como una regla gigante de pequeñas celdas de 1 byte:

flowchart TD
    subgraph Row1["Direcciones"]
        direction LR
        R1000["1000"]
        R1001["1001"]
        R1002["1002"]
        R1003["1003"]
        R1004["1004"]
        R1005["1005"]
    end

    subgraph Row2["Datos (por 1 byte)"]
        direction LR
        B1000["[byte]"]
        B1001["[byte]"]
        B1002["[byte]"]
        B1003["[byte]"]
        B1004["[byte]"]
        B1005["[byte]"]
    end

Cualquier variable ocupa varios bytes consecutivos:

  • char normalmente 1 byte,
  • int suele 4 bytes,
  • double suele 8 bytes,
  • std::string está implementado de forma compleja internamente, pero para nosotros es importante una cosa: también es un objeto en memoria.

Cada objeto en memoria tiene una dirección — una "coordenada" donde comienza el objeto.

La dirección NO es el valor

Supongamos que tienes este código:

int balance = 42;

balance tiene:

  • valor: 42
  • dirección: algo como 0x7ffeefbff5ac (el número exacto no importa)

Y son dos entidades distintas. Muy distintas.

2. Operador de dirección &

C++ permite obtener la dirección de una variable: el número de la celda de memoria donde se guarda. Para eso se usa el operador & y se lee así:

&x — "dirección de la variable x"

Tomemos la variable de nuestra miniaplicación «Hucha» (balance) y miremos cómo la ve la memoria:

#include <iostream>

int main() {
    int balance = 42;

    std::cout << "balance  = " << balance << '\n';
    std::cout << "&balance = " << &balance << '\n';
}

La dirección normalmente se imprime como 0x... (un número hexadecimal). Y sí: la dirección puede ser distinta en la siguiente ejecución del programa. Eso es normal: el sistema operativo suele mover cosas (también por seguridad).

¿Qué tipo tiene &balance?

Si balance es de tipo int, entonces &balance tiene tipo int*. Es decir, "dirección de int" — un valor de tipo "puntero a int".

3. Tamaño del objeto: sizeof

Si el tipo también indica "cuánta memoria se necesita", es lógico poder comprobarlo. Para eso está el operador sizeof.

#include <iostream>

int main() {
    int x = 0;
    double y = 0.0;

    std::cout << "sizeof(x) = " << sizeof(x) << '\n';
    std::cout << "sizeof(y) = " << sizeof(y) << '\n';
}

Normalmente verás algo como:

sizeof(x) = 4
sizeof(y) = 8

No es "magia del compilador", es simplemente un hecho: el tipo determina cuántos bytes se reservan para el objeto.

Y un detalle curioso: el tamaño del puntero suele ser el mismo (por ejemplo, 8 bytes en sistemas de 64 bits), aunque apunte a int o a double. El puntero guarda una dirección, y las direcciones en una plataforma tienen una "longitud" uniforme.

#include <iostream>

int main() {
    int x = 1;
    int* px = &x;

    std::cout << "sizeof(int)  = " << sizeof(int) << '\n';
    std::cout << "sizeof(px)   = " << sizeof(px) << '\n';
}

4. Qué se puede direccionar y qué no

Se puede tomar la dirección de aquello que realmente existe como un objeto separado.

Se puede obtener la dirección de una variable:

int x = 10;
std::cout << &x << '\n';

Pero no se puede obtener la dirección de un "resultado intermedio de un cálculo":

int x = 10;
std::cout << &(x + 1) << '\n'; // no compila

A este nivel basta la regla: tomamos la dirección de una "variable/objeto real", no del "resultado de una expresión".

5. Referencia T&: "el segundo nombre" del objeto

También con & se pueden crear variables referencia. Una referencia es un alias. Un seudónimo. El segundo nombre de un objeto ya existente.

#include <iostream>

int main() {
    int balance = 100;
    int& refBalance = balance;   // refBalance — segundo nombre de balance

    refBalance = refBalance + 50; // modificamos a través de la referencia
    std::cout << balance << '\n'; // 150
}

La variable refBalance apunta a la misma dirección de memoria que balance.

Es importante entender la idea: no creamos una copia. Hay un solo objeto, dos nombres.

Comprobamos que es el mismo objeto: las direcciones coinciden

Para asegurarnos de que dos variables refieren a la misma área de memoria, basta usar el operador &, que ya conoces. Ejemplo:

#include <iostream>

int main() {
    int balance = 100;
    int& refBalance = balance;

    std::cout << &balance << '\n';
    std::cout << &refBalance << '\n'; // misma dirección
}

Si la dirección es la misma, entonces es el mismo objeto.

Reglas de las referencias (breve, pero estrictas)

Una variable referencia debe inicarse inmediatamente. Es decir, no se puede hacer esto:

int& r; // error: la referencia no puede estar "vacía"

La referencia no se "reapunta" a otro objeto. Y aquí es donde muchos principiantes se confunden (tú no eres la excepción, relájate):

#include <iostream>

int main() {
    int a = 1;
    int b = 2;

    int& r = a; // r es a
    r = b;      // ESTO NO "reconfirma r"
               // esto "asigna a el valor de b a a"

    std::cout << a << '\n'; // 2
}

Es decir, r = b; — es una asignación normal, porque r se comporta como "lo mismo que a".

A este nivel, es útil recordar de forma simple: la referencia siempre apunta a un objeto real.

No confundas & en el tipo y & como operador

Son dos sentidos distintos del mismo símbolo:

  • int& ref = balance; — & forma parte del tipo (referencia)
  • &balance — la operación "tomar dirección"

C++ aquí es como ese amigo que usa el mismo meme en diferentes situaciones: "ya lo entenderás por el contexto". Sí, por contexto.

6. Puntero T*: una variable que guarda una dirección

Una puntero no es una referencia, es una variable separada. Su valor es la dirección de otro objeto.

#include <iostream>

int main() {
    int balance = 100;
    int* pBalance = &balance; // pBalance guarda la dirección de balance

    std::cout << "pBalance  = " << pBalance << '\n';  // dirección
    std::cout << "*pBalance = " << *pBalance << '\n'; // valor en la dirección
}

Aquí es importante distinguir tres entidades:

  • balance — el objeto con el número
  • &balance — la dirección del objeto
  • pBalance — el objeto que guarda la dirección (es decir, pBalance == &balance)

Desreferenciación *p

El operador * en una expresión significa "tomar el valor en la dirección".

#include <iostream>

int main() {
    int balance = 100;
    int* pBalance = &balance;

    *pBalance = *pBalance + 50;   // modificamos balance a través del puntero
    std::cout << balance << '\n'; // 150
}

El puntero también es una variable (también tiene dirección)

Esto a veces ayuda a "anclar" la imagen mental:

#include <iostream>

int main() {
    int balance = 100;
    int* pBalance = &balance;

    std::cout << "&balance  = " << &balance << '\n';
    std::cout << "pBalance  = " << pBalance << '\n';
    std::cout << "&pBalance = " << &pBalance << '\n';
}

&pBalance — es la dirección del propio puntero en memoria.

nullptr: puntero "a ninguna parte"

Un puntero puede tomar el valor especial nullptr — "no apunta a nada".

#include <iostream>

int main() {
    int* p = nullptr;

    if (p != nullptr) {
        std::cout << *p << '\n'; // seguro solo si p es válido
    } else {
        std::cout << "Pointer is null\n";
    }
}

A este nivel recuerda la regla de seguridad como una norma vial: solo desreferenciar *p si p != nullptr y apunta a un objeto vivo.

Se puede reasignar un puntero

A diferencia de la referencia, un puntero puede "apuntar" a otro objeto:

#include <iostream>

int main() {
    int a = 1;
    int b = 2;

    int* p = &a;
    std::cout << *p << '\n'; // 1

    p = &b;                  // ahora p apunta a b
    std::cout << *p << '\n'; // 2
}

7. Matices útiles

Bono: -> para punteros a objetos

Si tienes un puntero a un objeto y quieres llamar a un método, hay dos variantes equivalentes:

  • (*p).method() — largo, pero explícito
  • p->method() — corto y cómodo
#include <iostream>
#include <string>

int main() {
    std::string owner = "Alex";
    std::string* pOwner = &owner;

    std::cout << (*pOwner).size() << '\n';
    std::cout << pOwner->size() << '\n'; // lo mismo
}

Variable vs referencia vs puntero

Entidad Qué es ¿Puede estar "vacía"? ¿Se puede redirigir? Cómo obtener el valor
int x
el propio objeto — —
x
int& r = x
el segundo nombre del objeto
x
no no
r
int* p = &x
objeto separado que guarda la dirección de
x
sí (
nullptr
)
sí
*p

Si quieres un resumen en una frase:

La referencia se ve como una variable y conduce al mismo objeto, mientras que el puntero es una variable-dirección separada que hay que comprobar y desreferenciar.

Ámbito y direcciones "colgantes": cuándo una dirección se vuelve peligrosa

Ya sabes que una variable vive dentro de un bloque { ... }. Ahora añadimos una consecuencia importante: la dirección es válida solo mientras viva el objeto al que apunta.

El ejemplo más simple de "puntero colgante":

#include <iostream>

int main() {
    int* p = nullptr;

    { 
        int temp = 123;
        p = &temp;
        std::cout << *p << '\n'; // 123 — aquí todo bien
    }

    // temp ya no existe, y p guarda una dirección antigua.
    std::cout << *p << '\n'; // PELIGRO (no hagas esto)
}

¿Por qué es tan traicionero? Porque el programa a veces "parece funcionar", a veces falla y a veces empieza a imprimir basura. Esta es una de las razones por las que los punteros se consideran "peligrosos": dan mucha libertad, y la libertad exige disciplina.

Con las referencias puede ocurrir algo similar (una referencia también puede volverse "colgante"), pero los punteros suelen caer en estas trampas con más frecuencia simplemente porque se pueden guardar por separado y reasignar.

8. Insertando el tema en nuestra miniaplicación «Hucha»: modo inspector

Empezamos la "Hucha" en lecciones anteriores: leemos ingreso y gasto, actualizamos el balance. Hoy añadiremos el modo "inspector": mostraremos que el balance no es un número abstracto sino un objeto con dirección, y que se puede modificar de varias maneras.

Balance y su dirección

#include <iostream>

int main() {
    int balance = 0;

    std::cout << "balance  = " << balance << '\n';
    std::cout << "&balance = " << &balance << '\n';
}

Referencia al balance: modificamos "como si fuera directo"

#include <iostream>

int main() {
    int balance = 0;
    int& refBalance = balance;

    refBalance = refBalance + 10; // recargamos a través de la referencia
    std::cout << balance << '\n'; // 10
}

Puntero al balance: modificamos a través de *

#include <iostream>

int main() {
    int balance = 0;
    int* pBalance = &balance;

    *pBalance = *pBalance + 25;   // recargamos a través del puntero
    std::cout << balance << '\n'; // 25
}

Mini‑escenario: ingreso, gasto y "el mismo objeto"

Aquí hacemos un pequeño trozo "vivo": leemos dos números, actualizamos el balance, pero lo hacemos a través de referencia y puntero para que la mente se acostumbre a la idea de "diferentes formas de acceder al mismo objeto".

#include <iostream>

int main() {
    int balance = 0;
    int& refBalance = balance;
    int* pBalance = &balance;

    int deposit = 0, spend = 0;
    std::cin >> deposit >> spend;

    refBalance = refBalance + deposit; // a través de la referencia
    *pBalance = *pBalance - spend;     // a través del puntero

    std::cout << "balance = " << balance << '\n';
}

Si ahora pensaste "esto es raro, ¿por qué hacerlo así, se puede simplemente balance = balance + deposit;" — ¡perfecto! Significa que entiendes que esto es un experimento didáctico, no un estilo para un proyecto real.

El sentido del ejercicio es otro: aprender a ver que refBalance y *pBalance trabajan con el mismo objeto.

9. Errores típicos

Error nº1: puntero no inicializado.
Si escribes int* p; y no le das un valor, contendrá una "dirección basura". A veces el programa falla inmediatamente al usar *p, a veces empieza a corromper memoria, y a veces (la variante más peligrosa) "casi funciona". A nivel de hábito se arregla fácil: cualquier puntero o bien recibe una dirección válida inmediatamente o bien nullptr.

Error nº2: desreferenciar nullptr.
nullptr no es un "objeto nulo", es "no hay objeto". Desreferenciar *p cuando p == nullptr es una mala idea que suele acabar en fallo. Por eso comprobar if (p != nullptr) antes de desreferenciar no es paranoia, es higiene básica.

Error nº3: confundir * y & en la declaración y en la expresión.
int* p — la estrella es parte del tipo ("puntero").
*p — la estrella es la operación ("tomar el valor en la dirección").
Igualmente int& r — el ampersand es parte del tipo ("referencia"), y &x — la operación "tomar dirección". Mismo símbolo, dos sentidos — clásico en C++: el lenguaje ahorra símbolos y tú gastas neuronas. Con el tiempo se lee automáticamente.

Error nº4: esperar que una referencia se pueda "reapuntar".
Si tienes int& r = a;, entonces r siempre será sobre a. La línea r = b; no cambia "a qué apunta la referencia", cambia el valor de a. Si necesitas la capacidad de cambiar entre objetos, ese es un caso para usar punteros.

Error nº5: puntero colgante tras salir de un bloque.
Si tomas la dirección de una variable que vive dentro de { ... }, entonces tras la llave de cierre el objeto se destruye y la dirección queda "colgante". Esto no es una "teoría rara", es una causa real de bugs extraños. Una buena costumbre: no guardar direcciones de variables "internas" por más tiempo del que viva su bloque, y especialmente no usar tales direcciones tras salir del bloque.

1
Tarea
C++ SELF, nivel 6, lección 3
Bloqueada
La fuerza de la referencia
La fuerza de la referencia
1
Tarea
C++ SELF, nivel 6, lección 3
Bloqueada
Selección del puntero
Selección del puntero
1
Tarea
C++ SELF, nivel 6, lección 3
Bloqueada
Intercambio mediante punteros
Intercambio mediante punteros
1
Tarea
C++ SELF, nivel 6, lección 3
Bloqueada
Comandos para la memoria
Comandos para la memoria
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION