4 de Marzo de 2011

En C y, por herencia, en C++ los punteros y los arrays tienen una curiosa relación: son parecidos, en muchos casos intercambiables, pero no son iguales. Y muchos programadores los confunden, aun después de considerable experiencia. En este artículo me propongo aclarar de una vez por todas las diferencias entre estos dos conceptos.

Punteros

Mucho se ha escrito ya sobre punteros de C, por lo que no voy insistir demasiado en ellos: un puntero es la posición de memoria en la que se guarda una variable. Gran parte de los problemas en entender los punteros que se encuentran los principiantes de C se deben, en mi opinión, a que la palabra puntero se utiliza indistintamente para referirse a la dirección de una variable, al tipo de esa dirección o a una variable de ese tipo. La diferencia es sutil pero importante. Para comprenderla se puede hacer un símil con el tipo más sencillo 'entero'.

  • 3: es un entero.
  • int: es un tipo, de hecho es el tipo del entero anterior.
  • Si declaro int n; entonces n es una variable de tipo entero. El contenido de esta variable, es decir, el resultado de evaluar la expresión n es un entero. La diferencia entre la variable n y su contenido es análoga a la diferencia entre l-valor y r-valor, de la que ya hablé en alguna ocasión.

Las mismas observaciones se pueden hacer con punteros. Asumiendo la declaración int n; anterior:

  • &n es un puntero.
  • int* es un tipo de puntero, de hecho es el tipo del puntero anterior.
  • Si declaro int *p; entonces p es una variable de tipo puntero-a-entero. No debes confundir la variable con su contenido.

Algunos estudiantes de C se preguntan por qué se declara un puntero con * y no con &. Al fin y al cabo el asterisco se utiliza para acceder al contenido del puntero:

int a, b;
int &p1 = &a; //Error!
int *p2 = &a; //Ok: inicialización
*p2 = 1; //Asigna a 'a' no a 'p2'
p2 = &b; //Asigna a 'p2'

La sintaxis de C cobra sentido si nos damos cuenta de dos cosas.

  • La inicialización no es asignación. Sintácticamente son dos conceptos separados, que resulta que tienen un efecto práctico similar (más en C que en C++).
  • La declaración int *p se compone de dos partes: un especificador de tipo (type specifier), que es int, seguido de un declarador (declarator), *p.

Es decir, esta declaración afirma que *p es de tipo int; de esto se deduce que p debe ser de tipo puntero-a-entero.

Este hecho se revela de forma obvia con el ejemplo clásico:

int* p, q;

¿De qué tipo es q? Los blancos están puestos para despistar, pues no tienen valor sintáctico, así que esa declaración se lee como:

  • Especificador de tipo: int.
  • Declarador: hay dos, separados por una coma, *p y q.

Por lo tanto, el tipo de p es puntero-a-entero mientras que el de q es simplemente un entero.

Arrays

Un array es un tipo (o una variable, véase la discusión anterior) consistente en una sucesión de valores del mismo tipo de cierta longitud.

La declaración de un array es sencilla:

int r[10];

Aquí, int es el especificador de tipo y r[10] es el declarador. Es esto se deduce que el tipo de r es array-de-10-enteros.

Antes decía que había que distinguir entre una expresión, un tipo y una variable. Pues bien, lo más peculiar de los arrays en C es que no existen como expresión, aunque sí como variables y como tipos. Más técnicamente, sí existen expresiones, pero no r-valores de tipo array. Este dato es importante, así que lo voy a repetir:

No existen r-valores de tipo array.

Pero entonces, ¿qué ocurre cuando se evalúa una expresión de tipo array? Que se convierte en un puntero al primer elemento del array. Se dice que el array "decae" (decays) en un puntero. Pero fíjate que el tipo del puntero decaído no es puntero-a-array, sino puntero-a-elemento.

1
2
3
4
int a[10];
int *p1;
p1 = a; //'a' decae a puntero
p1 = &a[0]; //igual que el anterior

Las asignaciones de las líneas 3 y 4 son en todo equivalentes. Puedes pensar que la línea 4, en la que se obtiene expresamente el puntero al primer elemento del array, es ingeniosa porque evita la decadencia del array. ¡Nada más lejos de la realidad! Observemos la expresión &a[0] paso a paso:

  • a: Es una variable de tipo array-de-10-enteros. Es un l-valor.
  • a[0]: El operador "corchetes" se define como *(a + 0).
  • a + 0: El operador "suma" requiere dos r-valores: el 0 ya lo es, pero a no, así que se convierte a r-valor. ¡Ay! no, que es un array, entonces decae a puntero a primer elemento del array. La suma se realiza con aritmética de punteros, pero como el número es 0 no suma nada.
  • a[0] = *(a + 0): El operador "referencia" recibe un puntero y devuelve un r-valor, así que el resultado es el r-valor del primer elemento del array.
  • &a[0]: El operador "dirección" recibe un r-valor y devuelve un puntero. El resultado es el mismo puntero de dos pasos atrás.

Como puedes comprobar, el array decae exactamente igual. La única forma de evitar la decadencia del puntero es con la siguiente expresión:

int a[10];
int *p1 = (int*)&a;

El operador "dirección" sobre el array devuelve un puntero-a-array-de-10-enteros, que se convierte con un cast en puntero a entero. Ojo, escribo esto solo por interés ilustrativo, no lo hagas en programas de verdad: es feo y rebuscado. Una cosa curiosa de este ejemplo es que un array-de-X decae de forma automática e implícita a un puntero-a-X, pero un puntero-a-array-de-X no decae nunca a nada. Así que si alguna vez quieres evitar la decadencia de un array (¿por qué?) utiliza un puntero a array.

int a[10];
int (*pa)[10] = &a;
int *e1 = pa; //error
int **e2 = pa; //error también
int *p = *pa; //ok, el array decae

Un efecto secundario del hecho de que no existan r-valores de tipo array es que los arrays no se pueden copiar:

int a[10], b[10];
a = b; //Error: no se puede convertir int* a int[10]

Si necesitamos copiar arrays lo más fácil es envolverlos en una estructura, pues las estructuras sí son copiables aun cuando incluyan arrays:

struct S
{
    int r[10];
} a, b;
a = b; //Ok

Parámetros de funciones

En C los parámetros de funciones se pasan siempre por copia, y puesto que los arrays no son copiables, pues ¡no pueden usarse como parámetros! En su lugar, se pasa un puntero al primer elemento del array y como la conversión es automática todo funciona bien.

void foo(int *r)
{ /*...*/
}

int a[10];
foo(a);

De hecho, como ayuda sintáctica, y confundiendo aún más al estudiante del lenguaje, C permite declarar el parámetro de la función como si fuera un array, y entiende que donde dices array quieres decir puntero. El siguiente ejemplo es exactamente equivalente al anterior.

void foo(int r[10])
{ /*...*/
}

int a[10];
foo(a);

El parámetro r de la función no es de tipo array-de-10-enteros, aunque lo parezca, sino de tipo puntero-a-entero. El tamaño del no-array (el 10) es puramente decorativo, es decir, sirve para indicar al usuario de la función cuál es el tamaño esperado del array que se pase como parámetro, pero el compilador no lo utilizará para nada. De hecho, si lo prefires, puedes omitir el número por completo (void foo(int r[])). Esto explica que haya gente que declara la función main como main(int argc, char **argv) mientras que otros la declaran main(int argc, char *argv[]). Son idéndicas. Si te cuesta creer que el compilador reinterprete así tu parámetro cuidadosamente declarado, prueba el siguiente programa:

#include <stdio.h>

void fun(int r[10])
{ 
    printf("en fun: %d\n", (int)sizeof(r));

    printf("antes: %p\n", r);
    r = NULL; //r es asignable!
    printf("después: %p\n", r);
}

int main()
{
    int a[10];
    printf("en main: %d\n", (int)sizeof(a));
    fun(a);
    return 0;
}

La forma más fácil de comprobar si el compilador lo considera un array o un puntero (aparte de con un depurador) es con sizeof: aplicado a un array da el tamaño del tipo base multiplicado por el número de elementos; pero aplicado a un puntero devuelve siempre el mismo tamaño (4 en sistemas de 32 bits, 8 en sistemas de 64 bits).

Continúa leyendo en la 2ª parte.

4 comentarios a Punteros vs Arrays (parte 1 de 2)

  1. Hola, creo que hay un pequeño error en:

    *p2 = 1; //Asigna a 'b' no a 'p2'

    Sería: *p2 = 1; //Asigna a 'a' no a 'p2'

  2. Tienes toda la razón.

    Corregido.

  3. Disculpa, ¿podrías explicarlo todo de una forma más sencilla? No ha quedado para nada claro la diferencia entre un puntero, una dirección, el uso de los asteriscos, de los ampersand, mucho menos de los tales L-valores y R-valores, resulta luego que haces un cast y hablas de que el arreglo "decae", ¿decae? ¿qué es eso de decaer, se debilita? ¿en que sentido?
    Si pudieras explicarlo todo con manzanas te estaría muy agradecido ^^

  4. @Snowly7: A ver si me explico...

    Puntero y dirección son lo mismo: la posición de memoria (al final es un número) donde se guarda un dato. Algunos llaman dirección al valor de ese número y puntero a la variable que contiene la dirección, pero a mí me parecen iguales.

    El ampersand '&' es un operador, que aplicado a una variable, obtiene su dirección. Es decir el resultado es un puntero.

    El asterisco '*' es un operador, que aplicado a un puntero, obtiene el valor almacenado en esa dirección de memoria. En declaraciones se usa para declarar punteros: "int *p" dice que "*p" es un entero, por lo que "p" es un puntero.

    L-valor y R-valor son cómo se clasifican las expresiones en C. Una expresión es un L-valor básicamente cuando es una referencia a memoria, y por lo tanto se le puede aplicar el operador & y puede ir a la izquierda de una asignación (L de "left"=izquierda). R-valores son todas las expresiones que no sean L-valores (R de "right"=derecha). Por ejemplo, los nombres de variables son siempre L-valores, las constantes numéricas son R-valores, los resultados de cálculos son R-valores, etc.

    Cuando digo que un array decae (decay) en puntero quiero decir que se convierte de forma automática en puntero al primer elemento. Con eso se pierde información, pues el puntero no sabe la longitud del array. Por ejemplo:

    int a[10];
    int *p = a;

    "a" es un array, que decae a puntero en "p". Usando solo "p" es imposible saber cuál era el tamaño original del array. En ese sentido, 'decae' quiere decir que se convierte en algo que sabe menos que el original.

    Espero que esto te sirva para aclarar un poco el tema, porque no sé explicarlo mejor.

Ayuda
:-(icon_sad :-)icon_smile :roll:icon_rolleyes
:-Dicon_biggrin :-Picon_razz :oops:icon_redface
:-xicon_mad :-|icon_neutral :arrow:icon_arrow
8-)icon_cool 8-Oicon_eek :mrgreen:icon_mrgreen
:!:icon_exclaim :?:icon_question :twisted:icon_twisted
;-)icon_wink :evil:icon_evil :idea:icon_idea
:-oicon_surprised :cry:icon_cry
:-?icon_confused :lol:icon_lol

Deja un comentario