Relación entre arreglos y punteros
Decaimiento de arreglos a punteros y aritmética de punteros para recorrer memoria.
En C++, los arreglos (arrays) y los punteros (pointers) están estrechamente relacionados. Esto se debe a que los arreglos se almacenan de forma secuencial en la memoria, y el nombre de un arreglo en realidad representa la dirección de su primer elemento. Por lo tanto, es posible recorrer un arreglo utilizando aritmética de punteros. En este artículo aprenderemos sobre la estructura de memoria de los arreglos, su relación con los punteros y algunos ejemplos prácticos de uso.
1. Arreglos y disposición en memoria
Cuando se define un arreglo en C++, todos sus elementos se almacenan uno tras otro en memoria. Por ejemplo:
#include <iostream>
using namespace std;
int main() {
int numeros[3] = {10, 20, 30};
cout << "Dirección del primer elemento: " << &numeros[0] << endl;
cout << "Dirección del segundo elemento: " << &numeros[1] << endl;
cout << "Dirección del tercer elemento: " << &numeros[2] << endl;
return 0;
}
Las direcciones mostradas serán consecutivas:
Dirección del primer elemento: 0x61ff0c
Dirección del segundo elemento: 0x61ff10
Dirección del tercer elemento: 0x61ff14
Como cada int ocupa 4 bytes, las direcciones aumentan de 4 en 4.
2. El nombre de un arreglo es un puntero
El nombre del arreglo (numeros) en realidad representa la dirección de su primer elemento.
Es decir, numeros y &numeros[0] apuntan a la misma dirección.
int numeros[3] = {10, 20, 30};
cout << numeros << endl; // dirección del primer elemento
cout << &numeros[0] << endl; // misma dirección
Por lo tanto, es posible recorrer un arreglo utilizando punteros.
3. Acceso a los elementos del arreglo con punteros
Si un puntero apunta al primer elemento de un arreglo, se puede acceder a los demás elementos mediante aritmética de punteros.
#include <iostream>
using namespace std;
int main() {
int numeros[4] = {5, 10, 15, 20};
int *p = numeros; // p = &numeros[0]
cout << *p << endl; // 5
cout << *(p + 1) << endl; // 10
cout << *(p + 2) << endl; // 15
cout << *(p + 3) << endl; // 20
}
La expresión *(p + i) es equivalente a numeros[i].
Esta es la base de la aritmética de punteros.
4. Aritmética de punteros
Los punteros avanzan automáticamente en memoria según su tipo de dato. Por ejemplo:
int arreglo[3] = {1, 2, 3};
int *ptr = arreglo;
cout << ptr << endl; // dirección del primer elemento
ptr++;
cout << ptr << endl; // dirección del siguiente elemento
La instrucción ptr++ mueve la dirección 4 bytes hacia adelante (int = 4 bytes).
Esto se calcula automáticamente según el tipo del puntero.
Para double avanza 8 bytes, y para char avanza 1 byte.
5. Recorrer un arreglo con un puntero
Los elementos de un arreglo se pueden recorrer con un puntero dentro de un bucle.
int numeros[] = {2, 4, 6, 8, 10};
int *p = numeros;
for (int i = 0; i < 5; i++) {
cout << *(p + i) << " ";
}
Alternativamente, el puntero puede incrementarse directamente:
for (int *ptr = numeros; ptr < numeros + 5; ptr++) {
cout << *ptr << " ";
}
Ambos métodos producen el mismo resultado:
2 4 6 8 10
6. Arreglos como parámetros de función
Cuando un arreglo se pasa a una función, en realidad se pasa la dirección del arreglo. De esta manera, la función puede modificar directamente el arreglo original.
#include <iostream>
using namespace std;
void Llenar(int *arreglo, int longitud) {
for (int i = 0; i < longitud; i++)
arreglo[i] = (i + 1) * 5;
}
void Imprimir(const int *arreglo, int longitud) {
for (int i = 0; i < longitud; i++)
cout << arreglo[i] << " ";
}
int main() {
int numeros[5];
Llenar(numeros, 5);
Imprimir(numeros, 5);
}
Como la función Llenar modifica el arreglo directamente en memoria,
los valores del arreglo en la función main() son actualizados.
7. Arreglos multidimensionales y punteros
Los arreglos multidimensionales también se almacenan secuencialmente en memoria. Sin embargo, la aritmética de punteros debe hacerse con más cuidado.
#include <iostream>
using namespace std;
int main() {
int matriz[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
cout << *(*(matriz + 0) + 2) << endl; // 3
cout << *(*(matriz + 1) + 0) << endl; // 4
}
Aquí, matriz → representa la dirección de la primera fila.
*(matriz + 1) → se mueve a la segunda fila,
y *(*(matriz + 1) + 0) → accede al primer elemento de esa fila.
8. Punteros y arreglos dinámicos
A diferencia de los arreglos de tamaño fijo, los arreglos dinámicos se crean con la palabra clave new.
Estos arreglos se almacenan en la memoria heap y su tamaño puede determinarse en tiempo de ejecución.
int *arreglo = new int[5];
for (int i = 0; i < 5; i++)
arreglo[i] = (i + 1) * 10;
for (int i = 0; i < 5; i++)
cout << arreglo[i] << " ";
delete[] arreglo; // Liberar memoria
Los arreglos dinámicos requieren una gestión manual de la memoria.
La memoria asignada con new siempre debe liberarse con delete[] después de su uso.
9. Lectura de arreglos con punteros const
Si no desea que una función modifique el arreglo, el parámetro puntero puede definirse como const.
void Imprimir(const int *arreglo, int longitud) {
for (int i = 0; i < longitud; i++)
cout << arreglo[i] << " ";
}
Este método mantiene la integridad de los datos y evita modificaciones accidentales del arreglo.
10. TL;DR
- El nombre del arreglo (
numeros) → representa la dirección del primer elemento (&numeros[0]). *(p + i)↔arreglo[i]son equivalentes.- La aritmética de punteros avanza automáticamente la dirección según el tamaño del tipo de dato.
- Los arreglos se pasan a las funciones por dirección, no por copia.
- Los arreglos multidimensionales pueden accederse mediante cadenas de punteros (
*(*(matriz + i) + j)). - Los arreglos dinámicos deben crearse con
newy liberarse condelete[]. - Todos los ejemplos pueden ejecutarse en Visual Studio 2022 o en el compilador GCC 11+.