Para utilizar el valor almacenado en esa dirección, usamos el operador de desreferencia *p. Sin embargo, no siempre puedes jugar con punteros sin consecuencias. Si intentas desreferenciar un puntero que apunta a nullptr (un puntero que no apunta a nada) o a una dirección de memoria que no te pertenece, entras en el terreno del Undefined Behavior (Comportamiento Indefinido o UB). Esto significa que el programa puede cerrarse inesperadamente, corromper datos o, lo que es peor, seguir funcionando pero con resultados erróneos.
Cuando programas a bajo nivel, es común usar la aritmética de punteros. Si tienes un puntero que apunta al inicio de un array y le sumas 1 (p + 1), el compilador no suma un byte; suma exactamente el tamaño de un objeto de tipo T (sizeof(T)). Por eso, los punteros son extremadamente potentes para recorrer estructuras de datos, pero peligrosos si te sales de los límites.
Debes usar punteros cuando necesites representar la ausencia de un valor (usando nullptr [C++11]), cuando necesites pasar grandes estructuras de datos por dirección para evitar copias costosas (aunque para eso en C++ moderno solemos preferir referencias o smart pointers), o cuando trabajes con APIs de bajo nivel o hardware. Si te sales de los límites del array o realizas operaciones aritméticas sobre un puntero nulo, el programa entrará en estado de UB, un error que el compilador puede optimizar de formas que harán que tu depuración sea una pesadilla.
#include <iostream>
#include <string>
#include <vector>
struct Sensor {
int id;
float lectura;
void mostrar() const {
std::cout << "Sensor ID: " << id << " | Valor: " << lectura << "\n";
}
};
// Un puntero a función: toma un Sensor* y no devuelve nada
void imprimir_sensor(Sensor* s) {
if (s) s->mostrar(); // Uso de -> que es azúcar de (*s).mostrar()
}
int main() {
// 1. Punteros básicos y nullptr
Sensor s1{101, 25.5f};
Sensor* p_sensor = &s1; // p_sensor guarda la dirección de s1
Sensor* p_nulo = nullptr; // C++11: Puntero nulo tipado y seguro
// 2. Aritmética de punteros con arrays
Sensor datos[3] = {{1, 10.0f}, {2, 20.0f}, {3, 30.0f}};
Sensor* ptr = datos; // Apunta al primer elemento (datos[0])
// Al hacer ptr + 1, el compilador salta sizeof(Sensor) bytes
Sensor* siguiente = ptr + 1;
std::cout << "Segundo sensor mediante aritmética: ";
siguiente->mostrar();
// 3. Punteros a void (genéricos)
// void* es una dirección "ciega": no sabe qué tipo de objeto hay ahí
void* v_ptr = &s1;
// No puedes hacer *v_ptr porque el compilador no sabe qué es.
// Primero hay que convertirlo (cast) al tipo correcto.
Sensor* s_recast = static_cast<Sensor*>(v_ptr);
std::cout << "Recast de void*: ";
s_recast->mostrar();
// 4. Punteros a función
// Sintaxis: tipo_retorno (*nombre_puntero)(parámetros)
void (*callback)(Sensor*) = imprimir_sensor;
std::cout << "Ejecutando callback: ";
callback(p_sensor);
// 5. Diferencia de punteros
// Restar dos punteros del mismo array da la distancia en número de elementos
auto distancia = ptr - &datos[0];
std::cout << "Distancia entre el inicio y datos[0]: " << distancia << "\n";
return 0;
}
Desglose del código
p_sensor = &s1: Estamos obteniendo la dirección de memoria des1usando el operador de dirección&y guardándola enp_sensor.p_nulo = nullptr: Usamosnullptren lugar deNULLo0.nullptres un objeto especial de tipostd::nullptr_tque evita ambigüedades en la sobrecarga de funciones, algo que0no puede hacer.siguiente = ptr + 1: Aquí ocurre la magia de la aritmética. Sisizeof(Sensor)es, por ejemplo, 8 bytes,ptr + 1no apunta a la direcciónptr + 1, sino aptr + 8. Esto permite que el programador trabaje con “elementos” y no con “bytes”.siguiente->mostrar(): El operador->es un atajo sintáctico para(*siguiente).mostrar(). Primero desreferenciamos el puntero para obtener el objeto y luego accedemos a su miembro.void* v_ptr: Un punterovoid*puede apuntar a cualquier cosa, pero es “ciego”. No tiene información sobre el tipo, por lo que no puedes realizar aritmética sobre él ni desreferenciarlo hasta que realices unstatic_casta un tipo concreto.void (*callback)(Sensor*): La sintaxis puede parecer extraña, pero indica quecallbackes un puntero que apunta a una función que recibe unSensor*y no devuelve nada (void).ptr - &datos[0]: Al restar dos punteros, el resultado no es una dirección, sino unptrdiff_t(un tipo entero con signo), que representa cuántos elementos hay de distancia entre ellos.
El error frecuente
Un error clásico es intentar acceder al elemento “uno-pasado-el-final” (one-past-the-end). En el ejemplo de Sensor datos[3], es perfectamente legal en C++ tener un puntero que apunte a la posición inmediatamente después del último elemento (datos + 3), ya que esto es útil para comparar si un puntero ha llegado al final de un bucle. Sin embargo, desreferenciar ese puntero es Undefined Behavior.
Sensor datos[3] = {{1, 1.0f}, {2, 2.0f}, {3, 3.0f}};
Sensor* fin = datos + 3; // Válido: apunta al espacio justo después del array
// ERROR FATAL:
// *fin = {4, 4.0f}; // UB: Intentar escribir en memoria que no pertenece al array
// float v = fin->lectura; // UB: Leer memoria fuera de los límites
Si intentas esto, es posible que el programa no falle inmediatamente, pero estarás escribiendo en memoria que pertenece a otras variables o al stack de control, causando errores que solo aparecerán en partes del programa totalmente inconexas. Herramientas como AddressSanitizer (-fsanitize=address en gcc/clang) detectarán esto instantáneamente al compilar.
N° 25