5 de Marzo de 2011

En el post anterior describí las diferencias principales entre array y punteros en C. A continuación intentaré explicar por qué las cosas son como son y que consecuencias conllevan.

El porqué

¿Por qué los diseñadores del lenguaje han hecho los arrays tan raros? ¿No sería mas sencillo que los arrays fueran un tipo normal, asignable, y si el usuario quiere un puntero al primer elemento que lo pida expresamente? Ciertamente sería más sencillo, pero menos flexible: una función declarada para recibir un array tendría que indicar de qué tamaño es, y solo podría recibir arrays de ese tamaño, no de otro. Tal y como es, una función no puede saber el tamaño del array recibido, a no ser que se pase en otro parámetro. A cambio, se pueden pasar arrays de cualquier tamaño, ¡incluso 0!:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>

void escribe(int *a, int n)
{ //'a' es un array de 'n' elementos
    int i;
    printf("Array %p:%d\n", a, n);
    for (i = 0; i < n; ++i)
        printf("%d: %d\n", i, a[i]);
} 

int main()
{
    int x[3] = {1,2,3};
    int y[5] = {9,8,7,6,5};
    int z = 42;
    int *n;
    escribe(x, 3);
    escribe(y, 5);
    escribe(&z, 1);
    escribe(NULL, 0);
    n = (int*)malloc(20*sizeof(int));
    /* inicia n con valores interesantes */
    escribe(n, 20);
    free(n);
    return 0;
}

Fíjate en la línea 19 como una variable simple es equivalente a un array de 1 elemento, pero hay que pedir el puntero expresamente. También puede usarse la memoria dinámica para crear arrays de tamaño variable (malloc(sizeof(int[20])) sería equivalente pero rebuscado).

Esta técnica, habitual en C, tiene dos inconvenientes importantes, en mi opinión:

  • El tamaño del array debe ser especificado de alguna manera: tamaño fijo, otro parámetro o deducido de los elementos del propio array.
  • Viendo el prototipo de la función fun(int *a) no sabemos la intención del programador: ¿recibe un array o un devuelve un entero? ¿O quizás devuelve un array de enteros? Y si es así, ¿de que tamaño? Hay que consultar la documentación para estar seguro.

Inicialización de arrays

Antes dije que los arrays no son asignables. Entonces esto ¿qué es?

int a[3] = {1,2,3};

Ah, si leías con atención también dije que la inicialización no es asignación, solo lo parece. Esto es una inicialización, no una asignación, y las llaves determinan un inicializador, no un literal de tipo array. Lo siguiente es un error de sintaxis:

int a[3];
a = {1,2,3}; //Error, esto qué es?

Si un array tiene inicializador, entonces no es necesario indicar el tamaño: el compilador contará por nosotros en número de elementos de la inicialización:

int a[] = {1,2,3,4,5}; //array-de-5-enteros

También se puede omitir el tamaño de un array en una variable global o externa, siempre y cuando se indique el tamaño (o se inicialice) en alguna otra parte. Una variable así declarada puede usarse casi como un array normal, excepto en aquellos pocos casos en los que el compilador necesita saber el tamaño real.

int x[]; //array de tamaño desconocido

void foo()
{
    int *p = x; //ok
    sizeof(x); //error tamaño de x desconocido
}

int x[10]; //el array de verdad

void bar()
{
    sizeof(x); //ok, ahora sí
}

Múltiples dimensiones

En C no existen arrays de múltiples dimensiones propiamente dichos, pero en su lugar se pueden definir arrays de arrays (o arrays de arrays de arrays...) que son equivalentes a arrays multidimensionales. Conviene indicar que un array-de-N-arrays-de-M-loquesea decae automática e implícitamente a puntero-a-array-de-M-loquesea, y este último (que no es un array sino un puntero) no decae más allá. Algunos esperan que el array multidimensional decaiga a puntero al elemento [0][0] (doble decadencia), o a puntero a puntero a vete a saber dónde, pero no es el caso:

int x[10][8]; //array-de-10-arrays-de-8-enteros
int (*pa)[8] = x; //x decae a puntero-a-array-de-8-enteros
int **pb = x; //Error: no se puede convertir int(*)[8] a int**
int *pc = x; //Error: no se puede convertir int(*)[8] a int*
int *pd = *pa; //ok, *pa es un array-de-10-enteros, decae a int*
int *pe = x[3]; //ok, x[3] es un array-de-10-enteros,. decae a int*

int *p00 = &x[0][0];
int *q00 = (int*)x;

Los dos últimos punteros apuntan ambos al primer elemento del array. Si queremos podemos usarlo como si fuera un array de 80 enteros utilizando este puntero, pues tenemos la garantía de que todos ellos están contiguos en memoria.

Es importante distinguir un array-de-arrays, un array-de-punteros, un puntero-a-array y un puntero-a-puntero: El primero decae al tercero, y es de lo que he estado hablando hasta ahora (cambia en los ejemplos int por int[8]). El segundo decae al cuarto (ídem cambiando int por int*). Es fácil confundir los 4 casos, porque todos ellos admiten la doble indirección, aunque el significado varía:

int x[10][8]; //array-de-arrays
int (*px)[8] = x; //puntero-a-array
int *y[10]; //array-de-punteros
int **py = y; //puntero-a-puntero

int a, b;

  • x[a][b]: avanza a arrays de 8 enteros y luego b enteros. Esto son (8*a + b) enteros desde el comienzo del objeto, así que el compilador hace la cuenta y solo necesita una indirección.
  • px[a][b]: ídem.
  • y[a][b]: avanza a punteros a entero, lee el valor del puntero ahí guardado y avanza b enteros. Son necesarias dos indirecciones.
  • px[a][b]: ídem.

Si se inicializa el array multidimensional no es neesario indicar el tamaño de la primera dimensión, pero sí los de las siguientes. El porqué de esto debería resultar obvio de la explicación anterior:

int x[][4] = 
{ 
    {1,2,3,4},
    {5,6,7,8},
    {9,10,11,12},
};

Strings

En C no existe un tipo particular para las cadenas de texto o strings. En su lugar se utilizan simplemente arrays de caracteres (char) de tamaño variable, y se indica el final con un carácter nulo (0). O sea, que para escribir el texto Hola mundo\n habría que escribir:

char txt[] = 
{
    'H', 'o', 'l', 'a', ' ',
    'm', 'u', 'n', 'd', 'o', '\n',
    0 //No olvides el nulo!
};
int main()
{
    printf(txt);
    return 0;
}

Esto funciona, pero es tremendamente inconveniente, así que existe una sintaxis alternativa más breve:

char txt[] = "Hola mundo\n"; //El nulo se incluye automáticamente
int main()
{
    printf(txt);
    return 0;
}

¡Mucho mejor! Además el tamaño del array se determina automáticamente (en este caso es 12).

Pero aún puede hacerse mejor: tener que declarar un array para cada constante de texto que se quiere usar puede ser muy pesado y difícil de mantener, así que C permite escribir cadenas de texto directamente donde se necesitan (como todos sabemos):

int main()
{
    printf("Hola mundo\n");
    return 0;
}

La cuestión es ¿qué es exactamente una cadena literal en C? La respuesta es doble, dependiendo de dónde se usa. Uno: Si la cadena de texto se utiliza en la inicialización de un array de caracteres, entonces es una forma resumida de indicar una lista de caracteres, como el ejemplo anterior. Dos: En cualquier otro contexto equivale a declarar un array de caracteres estático y constante inicializado con los caracteres de esta cadena, y utilizar este array en donde se ha escrito la cadena. Es decir, el último ejemplo es convertido por el compilador en algo así:

//En realidad el array no tiene nombre
static char __s1[] = "Hola mundo\n";
int main()
{
    printf(__s1);
    return 0;
}

(En realidad el array en cuestión no es constante, porque la palabra reservada const se incluyó en el lenguaje mucho después que los strings, pero hagamos como que lo es.)

Si se usa el mismo texto en varios sitios un compilador hábil puede fusionar todos esos arrays de caracteres para ahorrar memoria y crear uno solo. Esto se suele llamar string pooling. Esto es razón suficiente para no intentar modificar nunca el contenido de un literal de texto.

Dicho esto, es importante conocer las diferencias entre estas dos declaraciones:

char *a = "Hola";
char b[] = "Hola";

Si no la has visto, puede ayudar hacer las conversiones descritas arriba:

static char __s1[] = {'H', 'o', 'l', 'a', '\0'};
char *a = __s1; //el array deae a puntero
char b[] = {'H', 'o', 'l', 'a', '\0'};

Se me ocurren las siguientes:

  • a es un puntero-a-caracteres, mientras que b es un array-de-5-caracteres.
  • a es asignable (puedes escribir a continuación a = "mundo";, pero b no porque es un array.
  • La memoria apuntada por a es estática (existe durante toda la ejecución del programa), pero la memoria de b depende de cómo se haya declarado la variable (si es local (automática) existe hasta que salga de contexto, si es global (estática) existe durante toda la ejecución del programa).
  • Nunca modifiques los caracteres apuntados por a pues pueden ser compartidos por otros literales o estar en memoria de solo-lectura. Sin embargo b es un array ordinario, por lo que no existen estas restricciones.

3 comments to Punteros vs Arrays (parte 2 de 2)

  1. Felicitaciones. Una de las mejores descripciones de los punteros en C que he visto nunca. Tremendamente clarificante.

    Hace poco que he llegado a este blog, googleando, y, como aficionado a la programación autodidacta que soy, la verdad es que los articulos que estoy viendo me están aclarando algunas lagunas que tenia pendientes.

    Me preguntaba si, cuando fuera posible, podrias dedicar una entrada al modificador 'const', sobre todo en C++, pues he visto varias formas de indicarlo en el paso de argumentos y no termino de aclararme con ellas ( const char *X, int * const Z, ... ).

    De nuevo, fecilidades por el blog. Cuentas con otro nuevo lector incondicional.

  2. Muchas gracias por el apoyo!
    Lo cierto es que últimamente estoy un poco lento en las actualizaciones del blog... poco tiempo libre, pero volveré, por supuesto.

    En cuanto a "const" es interesante, sobre todo si consideramos las sutiles diferencias de significado entre C y C++. Me lo apunto en la (creciente) lista de temas pendientes.

    Saludos!

  3. Qué, a qué estas esperando? Ya va siendo hora de una nueva entrada no? xD

Help
:-(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

Leave a comment