Tamaño de los vectores en C

En el post sobre copia de vectores, en los comentarios, se dice que la macro debería comprobar los tamaños pero… ¿es esto posible?

La respuesta es y es gracias al operador en tiempo de compilación sizeof.

Este operador devuelve el tamaño de un objeto o tipo en bytes. Así que podemos construir una macro muy sencilla que nos de el número de elementos de un vector:

#define vsize(v) (sizeof((v))/sizeof((v)[0]))

Ahora bien, muy importante, esta macro no funcionará con punteros. El tamaño de un puntero es el tamaño de la palabra (conjunto de bytes) usado para direccionar una posición de memoria: en máquinas de 32bits es de 4bytes y en máquinas de 64bits, de 8bytes. Así que nuestra macro no se comportará correctamente.

Con esto en mente se podría reescribir la macro de copia para que tuviese en cuenta el tamaño. La tenéis aquí y actualizada en la entrada original:

#define vsize(v) (sizeof((v))/sizeof((v)[0]))
#define arraycp(type, target, source, offset, count)\
  {\
      int i, ts, ss, c;\
      type *t; ts = vsize(target);\
      type *s; ss = vsize(source) - offset;\
      c = (count);\
      for(i=0, t=(target), s=(source)+(offset);\
          i<c && i<ts && i<ss;\
          ++i, *t++=*s++)\
        ;\
  }

¿Comentarios, mejoras?

¿Cómo se copian vectores?

A raíz del post sobre vectores y punteros me preguntaron que, entonces, ¿cómo se copian vectores en C?

Bueno, la respuesta fácil es «elemento a elemento«.

[UPDATE}

Gracias a un comentario del padawan me doy cuenta de que se me había escurrido memcpy. A lo mejor mi ego por hacerme una macro propia me cegó, 😉

El caso es que podemos usar:

void *memcpy(void *target, void *source, int count);

Para copiar un ‘count’ bytes empezando en ‘source’ a otro ‘target’. Sólo nos tenemos que llevar un pelín de cuidado con el valor de count que se refiere a bytes, no al tamaño de int o de nuestra estructura en particular. De todas formas es tan fácil como utilizar la sizeof:

elementos*sizeof(tipo_del_elemento_del_vector)

Considérese el siguiente ejemplo:

int v1[] = {0,1,2,3,4};
int v2[] = {4,3,2,1,0};
memcpy(v1, v2, 2*sizeof(int)); // copia los dos primeros elementos de v2 en v1

[/UPDATE]

Como hay muchas formas de definir la copia vamos a dar una bastante versátil:

La macro arraycp(type, target, source, offset, count) copia ‘count’ elementos de ‘source’  en ‘target’ a partir de la posición ‘offset’. Los elementos de los vectores deben ser del tipo ‘type’.

La definición de tal macro podría ser la siguiente:

#define vsize(v) (sizeof((v))/sizeof((v)[0]))
#define arraycp(type, target, source, offset, count)\
  {\
      int i, ts, ss, c;\
      type *t; ts = vsize(target);\
      type *s; ss = vsize(source) - offset;\
      c = (count);\
      for(i=0, t=(target), s=(source)+(offset);\
          i<c && i<ts && i<ss;\
          ++i, *t++=*s++)\
        ;\
  }

Recuerda que es muy importante que tanto ‘target’ como ‘source’ sean vectores (no punteros), si no, no funcionará. [1]

[1] Eso se debe a la macro vsize que sólo calcula correctamente el número de elementos de un vector si su parámetro es un vector. Gracias a Denis y a Adri por los comentarios.

[UPDATE]

Para simular el mismo comportamiento de esta macro con memcpy, utilícese la siguiente forma:

memcpy(target, source+offset, count*sizeof(type))

[/UPDATE]

Quizá para entenderla el lector deba estar familiarizados con el preprocesador de C pero la idea es básica:

  1. Creamos un nuevo ámbito de variables mediante las llaves.
  2. Declaramos i como entero para contar el número de elementos copiados.
  3. Declaramos ts y ss para llevar los tamaños del vector destino y del (sub)vector origen respectivamente.
  4. Inicializamos i a 0 en el for, en la condición del bucle tenemos en cuenta que el número de elementos copiados, i, sea inferior siempre al tamaño de los vectores y al propio límite establecido por el usuario con ‘count’.
  5. Declaramos sendos punteros al tipo del vector.
  6. Los inicializamos a las direcciones del vector destino y el vector fuente teniendo en cuenta el offset.
  7. En el incremento del for realizamos la copia e incrementamos i.

Hay una mejor version para GNU que pongo aquí aunque no la he probado.

En esta versión no hace falta indicar el tipo pues la extensión typeof nos lo proporciona automaticamente. Y lo que es mejor, podríamos copiar un vector de enteros sobre uno de flotantes aprovechándonos de las conversiones implícitas de tipos.

#define vsize(v) (sizeof((v))/sizeof((v)[0]))
#define arraycp(type, target, source, offset, count)\
  {\
      int i, ts, ss, c;\
      typeof((target)[0]) *t; ts = vsize(target);\
      typeof((source)[0]) *s; ss = vsize(source) - offset;\
      c = (count);\
      for(i=0, t=(target), s=(source)+(offset);\
          i<c && i<ts && i<ss;\
          ++i, *t++=*s++)\
        ;\
  }

Espero que os resulte de utilidad.

La verdad sobre vectores y punteros

Para poder seguir dándole caña al libro de Stroustup tengo que contaros o recordaros antes la verdad sobre vectores y punteros en C, que no son lo mismo.

Es común en los cursos de programación dar a entender que el nombre de un vector es un puntero a su primer elemento o que un vector es un tipo especial de variable pero lo primero es casi cierto y lo segundo, falso.

Lo primero que hay que dejar claro es que el identificador de un vector no es una variable; sólo le da nombre a un conjunto de «variables consecutivas» pero no podemos guardar nada en él. Nos lo dicen Kernighan y Ritchie en su manual de C, apartado 5.3 (aquí pa es un puntero a un array y a es un array):

Existe una diferencia entre un nombre de arreglo y un apuntador. que debe tenerse en mente. Un apuntador es una variable. Por esto pa = a y pa++ son legales. Pero un nombre de arreglo no es una variable; construcciones como a = pa y a++ son ilegales.

Por ejemplo, el siguiente fragmento de código no tiene sentido:

int v1[] = {1,0,1};
int v2[] = {2,0,2};
v2 = v1; // no se puede copiar un vector en otro, v1 y v2 son nombres no variables, no guardan nada, sólo designan espacios
v2++ // lo mismo, esta expresión es como escribir v2 = v2 + 1

El identificador v2 no puede ser reasignado, por tanto, como decían K. & R. expresiones como v2++ o v2-=1 no son válidas. Este hecho se precisa más adelante en el mismo libro, sección 5.5:

Existe una importante diferencia entre estas definiciones:

char amessage[] = «ya es el tiempo»; /* arreglo */
char *pmessage = «ya es el tiempo»; /* apuntador */

amessage es un arreglo, suficientemente grande como para contener la secuencia de caracteres y el que lo inicializa. Se pueden modificar caracteres individuales dentro del arreglo, pero amessage siempre se referira a la misma localidad de almacenamiento. Por otro lado, pmessage es un apuntador inicializado para apuntar a una cadena constante; el apuntador puede modificarse posteriormente para que apunte a algun otro lado, pero el resultado es indefinido si trata de modificar el contenido de la cadena.

Precisamente del hecho de que los vectores no se puedan reasignar y que los punteros sí, se deriva el que los vectores no sean punteros. No obstante el identificador de un vector sí que es, por definición, la dirección del primer elemento del vector.

Por lo anterior, las siguientes expresiones sí que tienen sentido:

int v[] = {1,0,1};
int *pi, *pi2;
pi = v; // pi apunta al primer elemento del vector v
pi2 = &v[0]; // ahora pi y pi2 apuntan al mismo sitio

Ahora, lo que sí puede tratarse como una variable son cada una de las posiciones del vector mediante la notación de indexación con corchetes:

int v[] = {1,0,1};
v = 0; // ¡ERROR! Esto es ilegal, v no es una variable
v[1] = 10; // Esto es legal, la posición 1 de v se puede tratar como a una variable

Su relación con los punteros se vuelve trivial cuando se conoce que la indexación de un vector es azúcar sintáctica para evitar construir una expresión de la forma:

y = *(vector+1); // referencia al segundo elemento del vector

En vez de esto, ponemos sencillamente:

y = vector[1]; // lo mismo, pero menos engorroso

Es azúcar porque así nos lo dicen K. & R. en la sección 5.3:

Al evaluar a[i]. C convierte inmediatamente a *(a + i); las dos formas son equivalentes.

En síntesis podríamos decir que:

  1. El identificador de un vector no puede aparecer a la izquierda de una asignación, un puntero sí.
  2. El identificador del vector es, por definición, la dirección al primer elemento.
  3. Podemos acceder a los elementos del vector bien con la notación de punteros, bien con la notación de los corchetes. Son equivalentes.

Con esto espero haber resaltado las diferencias y similitudes entre vectores y punteros. El siguiente post sobre el libro de C++ de Stroustup hablará sobre un asunto relacionado con esto y los literales de cadena.