1. Un arreglo en la memoria
Un arreglo es una secuencia de elementos del mismo tipo, guardados uno a continuación del otro en la memoria, sin huecos. Se declara con el tamaño entre corchetes, y sus elementos se numeran desde 0:
#include <stdio.h>
int main(void)
{
int puertos[5] = {22, 80, 443, 8080, 9090};
int parcial[5] = {1, 2}; /* el resto queda en 0 */
int designado[8] = {[2] = 7, [6] = 3}; /* C99: por posición */
int deducido[] = {10, 20, 30}; /* el tamaño sale de los valores */
for (int i = 0; i < 5; i++)
printf("puertos[%d] = %4d en %p\n", i, puertos[i], (void *) &puertos[i]);
printf("sizeof puertos = %zu, elementos = %zu\n",
sizeof puertos, sizeof puertos / sizeof puertos[0]);
printf("parcial: %d %d %d %d %d\n", parcial[0], parcial[1], parcial[2], parcial[3], parcial[4]);
printf("designado:");
for (int i = 0; i < 8; i++)
printf(" %d", designado[i]);
printf("\ndeducido tiene %zu elementos\n", sizeof deducido / sizeof deducido[0]);
return 0;
}
$ ./contiguo
puertos[0] = 22 en 0x7ffd0ac9f110
puertos[1] = 80 en 0x7ffd0ac9f114
puertos[2] = 443 en 0x7ffd0ac9f118
puertos[3] = 8080 en 0x7ffd0ac9f11c
puertos[4] = 9090 en 0x7ffd0ac9f120
sizeof puertos = 20, elementos = 5
parcial: 1 2 0 0 0
designado: 0 0 7 0 0 0 3 0
deducido tiene 3 elementos
Las direcciones avanzan de a 4, el tamaño de un int: los cinco elementos ocupan 20 bytes seguidos. Esa contigüidad es la propiedad fundamental de los arreglos. Gracias a ella, conociendo la dirección del primer elemento y el tamaño de cada uno, se puede calcular la dirección de cualquier otro sin buscarla: el elemento i está en inicio + i × tamaño. Es lo que hace el procesador en cada acceso con corchetes, y por eso acceder a cualquier elemento cuesta lo mismo. Y por eso también, como el elemento i está a i posiciones del primero, la numeración empieza en 0.
2. Inicializar un arreglo
contiguo.c muestra las cuatro formas de dar valores iniciales a un arreglo:
- Completa:
{22, 80, 443, 8080, 9090}, un valor por elemento. - Parcial:
{1, 2}en un arreglo de 5. Los elementos sin valor quedan en cero, aunque el arreglo sea local. Por esoint v[100] = {0};es la forma habitual de crear un arreglo lleno de ceros. - Designada (C99):
{[2] = 7, [6] = 3}da valor a posiciones concretas, y el resto queda en cero. Es muy útil para tablas indexadas por constantes, como un arreglo de nombres indexado por número de señal. - Con tamaño deducido:
int deducido[] = {10, 20, 30};, sin número entre los corchetes, toma el tamaño de la cantidad de valores.
Un arreglo local sin inicializar, en cambio, contiene basura, como cualquier variable automática del tema 6; uno global o static empieza en cero. Y el modismo sizeof arreglo / sizeof arreglo[0], el tamaño total dividido por el de un elemento, da la cantidad de elementos: es la forma de no repetir el número 5 en el bucle, y de que el código siga siendo correcto si el arreglo cambia de tamaño. Pero solo funciona donde el arreglo es un arreglo de verdad, como veremos en la sección 7.
3. El nombre se convierte en puntero
Esta es la regla central del tema: en casi cualquier expresión, el nombre de un arreglo se convierte automáticamente en un puntero a su primer elemento. A esta conversión se la suele llamar decaimiento (array decay). Es lo que permite escribir int *p = a; sin &:
#include <stdio.h>
int main(void)
{
int a[5] = {10, 20, 30, 40, 50};
int *p = a; /* el arreglo se convierte en &a[0] */
printf("a = %p\n", (void *) a);
printf("&a[0] = %p\n", (void *) &a[0]);
printf("p = %p\n", (void *) p);
printf("&a = %p\n", (void *) &a);
printf("sizeof a = %zu, sizeof p = %zu, sizeof &a = %zu\n", sizeof a, sizeof p, sizeof &a);
printf("sizeof *&a = %zu\n", sizeof *&a);
printf("a + 1 = %p (avanza un int)\n", (void *) (a + 1));
printf("&a + 1 = %p (avanza el arreglo entero)\n", (void *) (&a + 1));
printf("a[3] = %d, *(a + 3) = %d, p[3] = %d, *(p + 3) = %d, 3[a] = %d\n",
a[3], *(a + 3), p[3], *(p + 3), 3[a]);
return 0;
}
$ ./decae
a = 0x7fff99437460
&a[0] = 0x7fff99437460
p = 0x7fff99437460
&a = 0x7fff99437460
sizeof a = 20, sizeof p = 8, sizeof &a = 8
sizeof *&a = 20
a + 1 = 0x7fff99437464 (avanza un int)
&a + 1 = 0x7fff99437474 (avanza el arreglo entero)
a[3] = 40, *(a + 3) = 40, p[3] = 40, *(p + 3) = 40, 3[a] = 40
+ 1.a, &a[0] y p son la misma dirección, del mismo tipo, int *: el nombre del arreglo, usado en una expresión, se convirtió en un puntero al primer elemento. Por eso int *p = a funciona, y por eso, en el tema 15, la función volcar recibía un arreglo de caracteres sin &. Es también lo que pasa al llamar a una función con un arreglo, como veremos en la sección 7.
La conversión es automática, pero no cambia el arreglo: no se crea ninguna variable puntero, ni se copia nada. El compilador simplemente usa la dirección del arreglo donde la expresión necesita un valor, porque un arreglo entero no es un valor que se pueda manejar de una vez: no cabe en un registro, ni se puede copiar con =, como veremos.
4. Las excepciones: sizeof y &
La conversión ocurre «en casi cualquier expresión». Las excepciones son pocas y conviene saberlas de memoria:
| Expresión | El arreglo… | Resultado con int a[5] |
|---|---|---|
sizeof a | No se convierte: es el arreglo entero. | 20, el tamaño de los cinco elementos. |
&a | No se convierte: se toma la dirección del arreglo entero. | Un puntero de tipo int (*)[5], «puntero a arreglo de 5 int». |
char s[] = "hola"; | Una cadena literal que inicializa un arreglo se copia en él. | Un arreglo propio de 5 bytes (sección 9). |
| Cualquier otra | Se convierte en &a[0], de tipo int *. | a + 1, a[i], f(a), p = a… |
decae.c muestra las dos primeras. sizeof a vale 20, mientras que sizeof p vale 8: el arreglo conoce su tamaño; el puntero, solo una dirección. &a tiene el mismo valor numérico que a, pero es de otro tipo: apunta al arreglo como un todo, y *&a es el arreglo entero, de 20 bytes. La diferencia se ve al sumar 1: a + 1 avanza un int, 4 bytes, hasta el segundo elemento; &a + 1 avanza un arreglo de cinco int, 20 bytes, y queda justo después del último elemento. Sumar a un puntero avanza tantos bytes como mide el tipo apuntado: es la aritmética de punteros del tema 18.
5. a[i] es *(a + i)
El estándar de C no define el acceso a un arreglo como una operación especial. Define a[i] como exactamente igual a *(a + i): tomar la dirección del primer elemento, avanzar i elementos, y desreferenciar. La última línea de decae.c lo muestra con todas las combinaciones posibles, y todas dan 40:
a[3]y*(a + 3): las dos formas sobre el arreglo, que se convierte en puntero.p[3]y*(p + 3): las mismas sobre un puntero. Los corchetes funcionan con cualquier puntero, y por esob[i]funcionaba envolcar, en el tema 15, odatos[i]en una función que recibe un arreglo.3[a]: comoa[3]es*(a + 3), y la suma es conmutativa,3[a]es*(3 + a), lo mismo. Es una curiosidad que demuestra que los corchetes son solo una abreviatura; nadie debería escribirlo en serio.
Esta equivalencia es la base de toda la relación entre arreglos y punteros: indexar un arreglo es aritmética de punteros con otra sintaxis. Y explica por qué C no comprueba los índices: a[7] en un arreglo de 5 es *(a + 7), una dirección como cualquier otra, sin información sobre dónde termina el arreglo.
6. Un arreglo no es un puntero
Que el nombre de un arreglo se convierta en un puntero no significa que sea un puntero. Un arreglo contiene sus elementos; un puntero contiene una dirección. Las diferencias se notan en cuanto se intenta tratar al arreglo como una variable puntero:
$ gcc -std=c17 -Wall -Wextra -c diferencias.c
diferencias.c: In function ‘main’:
diferencias.c:9:7: error: assignment to expression with array type
9 | a = b; /* un arreglo, no */
| ^
diferencias.c:10:6: error: lvalue required as increment operand
10 | a++;
| ^~
Arreglo int a[5] | Puntero int *p | |
|---|---|---|
| Contiene | Los 5 elementos. | Una dirección, 8 bytes. |
sizeof | 20 | 8 |
| Ocupa memoria para | Los datos. | La dirección; los datos están en otro lado. |
| Se le puede asignar | No: a = b es un error. | Sí: p = b cambia a qué apunta. |
| Se puede incrementar | No: a++ es un error. | Sí: p++ pasa al siguiente elemento. |
& da | Un puntero al arreglo entero. | La dirección de la variable puntero. |
Puede ser NULL | No: siempre tiene sus elementos. | Sí. |
Una forma de pensarlo: el nombre de un arreglo es como una constante, la dirección fija donde están sus elementos. No hay ninguna variable que la guarde, y por eso no se puede cambiar. Un puntero, en cambio, es una variable, con su propio lugar en la memoria, y su valor se puede reemplazar.
7. Arreglos como parámetros
Al pasar un arreglo a una función, se aplica la conversión: la función recibe un puntero al primer elemento. Y como el estándar sabe que eso es lo que llega, cualquier parámetro declarado como arreglo es en realidad un puntero: int datos[], int datos[10] e int *datos significan exactamente lo mismo en una lista de parámetros. Es lo que vimos en el tema 11 con sumar_mal, donde sizeof datos valía 8.
Eso vuelve peligroso el modismo para contar elementos, sobre todo escondido en una macro como las del tema 14:
$ gcc -std=c17 -Wall -Wextra -o cant cant.c
cant.c: In function ‘contar’:
cant.c:2:29: warning: ‘sizeof’ on array function parameter ‘datos’ will return size of ‘int *’ [-Wsizeof-array-argument]
…
cant.c: In function ‘main’:
cant.c:2:33: warning: division ‘sizeof (int *) / sizeof (int)’ does not compute the number of array elements [-Wsizeof-pointer-div]
…
$ ./cant # CANTIDAD(v), contar(v), CANTIDAD(p)
10 2 2
Sobre el arreglo de verdad, la macro da 10. Sobre el parámetro y sobre un puntero, da 2: los 8 bytes de un puntero divididos por los 4 de un int. gcc reconoce el modismo y advierte en los dos casos, pero solo con -Wall. La regla, otra vez, es pasar siempre la cantidad como un parámetro aparte, calculada donde el arreglo todavía es un arreglo.
C99 agregó una forma de documentar el tamaño mínimo esperado, con static dentro de los corchetes de un parámetro:
$ gcc -std=c17 -Wall -Wextra -o parametro parametro.c
parametro.c: In function ‘main’:
parametro.c:16:20: warning: argument 1 null where non-null expected [-Wnonnull]
16 | printf("%d\n", suma4(NULL));
…
parametro.c:15:5: warning: ‘suma4’ reading 16 bytes from a region of size 8 [-Wstringop-overread]
15 | printf("%d\n", suma4(pocos));
…
datos[static 4] promete que el puntero no es nulo y apunta a por lo menos 4 elementos, y gcc advierte si ve una llamada que rompe la promesa: con NULL, y con un arreglo de 2 elementos, 8 bytes, del que la función leería 16. Sigue siendo un puntero, y la comprobación solo funciona cuando el compilador conoce el arreglo pasado, pero es una documentación útil, y una de las pocas formas de expresar un tamaño en el prototipo.
8. Copiar y comparar arreglos
Como un arreglo no se puede asignar, copiarlo exige copiar sus bytes, y como su nombre se convierte en un puntero, compararlo con == compara direcciones:
#include <stdio.h>
#include <string.h>
int main(void)
{
int original[4] = {1, 2, 3, 4};
int copia[4];
int otra[4] = {1, 2, 3, 4};
memcpy(copia, original, sizeof original);
copia[0] = 99;
printf("original[0] = %d, copia[0] = %d\n", original[0], copia[0]);
printf("original == otra: %d\n", original == otra);
printf("memcmp(original, otra, …): %d\n", memcmp(original, otra, sizeof original));
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o copiar copiar.c && ./copiar
copiar.c: In function ‘main’:
copiar.c:14:56: warning: comparison between two arrays [-Warray-compare]
14 | printf("original == otra: %d\n", original == otra);
| ^~
copiar.c:14:56: note: use ‘&original[0] == &otra[0]’ to compare the addresses
original[0] = 1, copia[0] = 99
original == otra: 0
memcmp(original, otra, …): 0
memcpy, del tema 16, copia los 16 bytes, y la copia es independiente: cambiar copia[0] no toca el original. original == otra da 0, falso, aunque los dos tengan exactamente los mismos valores, porque compara las direcciones de dos arreglos distintos; gcc lo advierte. memcmp compara los bytes y devuelve 0 cuando son iguales, la convención de las funciones de comparación de C, que también sigue strcmp. (Comparar los bytes de estructuras con memcmp tiene sus trampas, por el relleno del tema 25; con arreglos de enteros es seguro.)
9. char[] frente a char *
La diferencia entre arreglo y puntero tiene su caso más importante en las cadenas. Estas dos declaraciones se parecen mucho y son completamente distintas:
#include <stdio.h>
int main(void)
{
char arreglo[] = "hola"; /* un arreglo propio, con una copia */
const char *puntero = "hola"; /* un puntero a la cadena literal */
printf("arreglo: sizeof = %zu, dirección %p\n", sizeof arreglo, (void *) arreglo);
printf("puntero: sizeof = %zu, apunta a %p\n", sizeof puntero, (void *) puntero);
arreglo[0] = 'H'; /* permitido: es nuestro */
printf("arreglo modificado: %s\n", arreglo);
char *trampa = (char *) puntero; /* quitar el const no la hace modificable */
printf("modificando la literal...\n");
fflush(stdout);
trampa[0] = 'H';
printf("%s\n", puntero);
return 0;
}
$ ./cadenas
arreglo: sizeof = 5, dirección 0x7ffffcd9a083
puntero: sizeof = 8, apunta a 0x5d2ab8da5008
arreglo modificado: Hola
modificando la literal...
Segmentation fault
char arreglo[] = "hola" crea un arreglo local de 5 bytes, en la pila, y copia en él los caracteres de la cadena literal y su \0: es una de las excepciones de la sección 4. El arreglo es del programa, y modificarlo es correcto. const char *puntero = "hola" crea solo una variable de 8 bytes con la dirección de la cadena literal, que vive en .rodata, la memoria de solo lectura del tema 15. Intentar modificarla, aun quitando el const a la fuerza, termina el programa. Por eso, en el curso, todo puntero a una cadena literal se declara const char *: el compilador rechaza entonces cualquier intento de escribir a través de él. Las cadenas son el bloque 5, a partir del tema 21.
10. Arreglos de punteros y arreglos de arreglos
Una lista de cadenas se puede guardar de dos formas, que también parecen iguales y no lo son:
#include <stdio.h>
int main(void)
{
const char *punteros[] = {"SIGHUP", "SIGINT", "SIGQUIT", "SIGKILL"};
char matriz[][8] = {"SIGHUP", "SIGINT", "SIGQUIT", "SIGKILL"};
printf("arreglo de punteros: sizeof = %zu (%zu punteros)\n",
sizeof punteros, sizeof punteros / sizeof punteros[0]);
printf("arreglo de arreglos: sizeof = %zu (%zu filas de %zu bytes)\n",
sizeof matriz, sizeof matriz / sizeof matriz[0], sizeof matriz[0]);
for (int i = 0; i < 4; i++)
printf(" %-8s en %p %-8s en %p\n", punteros[i], (void *) punteros[i],
matriz[i], (void *) matriz[i]);
return 0;
}
$ ./nombres
arreglo de punteros: sizeof = 32 (4 punteros)
arreglo de arreglos: sizeof = 32 (4 filas de 8 bytes)
SIGHUP en 0x64ee2bb38008 SIGHUP en 0x7ffc31101810
SIGINT en 0x64ee2bb3800f SIGINT en 0x7ffc31101818
SIGQUIT en 0x64ee2bb38016 SIGQUIT en 0x7ffc31101820
SIGKILL en 0x64ee2bb3801e SIGKILL en 0x7ffc31101828
Los dos ocupan 32 bytes, por coincidencia, y guardan cosas distintas. punteros es un arreglo de 4 punteros de 8 bytes: los textos están en .rodata, uno detrás de otro y cada uno de su largo justo, 7 u 8 bytes con su \0, como muestran sus direcciones. matriz es un arreglo de 4 arreglos de 8 caracteres: los textos están dentro de él, en la pila, cada uno en una fila de 8 bytes aunque sea más corto. El arreglo de punteros es la forma más común, la que se usa para los mensajes de senal.c en el tema 9 o para argv, que es un arreglo de punteros a las cadenas de los argumentos. Los arreglos de arreglos, las matrices, son el tema del tema 23, y los punteros a punteros que aparecen al recorrer un arreglo de punteros, del tema 19.
11. Salirse de los límites
Como a[i] es solo *(a + i), nada impide calcular una dirección fuera del arreglo y leer o escribir ahí. Ya lo vimos en el tema 1 con fuera.c, que leía un elemento de más. Escribir es peor, porque modifica lo que haya en esa memoria. Para que el efecto sea predecible, este programa pone una variable inmediatamente después del arreglo, dentro de una estructura, un tipo que veremos en el bloque 6:
#include <stdio.h>
struct registro {
int valores[4];
int clave; /* justo después del arreglo */
};
int main(void)
{
struct registro r = {{1, 2, 3, 4}, 1234};
printf("antes: clave = %d\n", r.clave);
for (int i = 0; i <= 4; i++) /* error: i llega a 4 */
r.valores[i] = 0;
printf("después: clave = %d\n", r.clave);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o limites limites.c && ./limites
antes: clave = 1234
después: clave = 0
$ gcc -std=c17 -Wall -Wextra -O2 -o limites limites.c
limites.c: In function ‘main’:
limites.c:14:22: warning: iteration 4 invokes undefined behavior [-Waggressive-loop-optimizations]
…
$ ./limites
antes: clave = 1234
después: clave = 1234
$ gcc -std=c17 -g -fsanitize=bounds -o limites limites.c && ./limites
limites.c:14:18: runtime error: index 4 out of bounds for type 'int [4]'
antes: clave = 1234
después: clave = 0
Sin optimizar, el bucle escribió un elemento de más, valores[4], que es exactamente donde está clave: la clave se perdió sin ningún mensaje. Con -O2, el compilador advirtió el comportamiento indefinido y produjo un programa distinto, que no tocó la clave; ninguno de los dos es «el correcto». Y aquí aparece un límite de las herramientas: AddressSanitizer, que detectó los desbordes del tema 1, no detecta este, porque la escritura cae dentro de memoria válida, la de la propia estructura. -fsanitize=bounds, en cambio, conoce el tamaño declarado de valores y señala el índice 4. Usar los dos durante el desarrollo cubre más casos que cualquiera solo.
La regla de siempre, que ningún compilador puede imponer: todo índice se compara con el tamaño antes de usarse, y los bucles van de 0 a < n. Los desbordes de arreglos son, junto con los de cadenas, la fuente más común de vulnerabilidades en programas de C, como vimos en el tema 12 con la dirección de retorno.
12. Tamaños variables y arreglos devueltos
Dos cosas más sobre los arreglos, que completan el cuadro. La primera: el tamaño de un arreglo normal se fija al compilar, pero los arreglos de tamaño variable de C99, que vimos en el tema 12 con grande.c, deciden su tamaño al ejecutar, y para ellos sizeof deja de ser una constante:
$ ./vla 3
24
$ ./vla 100
800
Con un arreglo de tamaño variable, sizeof se calcula al ejecutar: es la única excepción a la regla del tema 5 de que sizeof no evalúa nada. Los riesgos de estos arreglos, que pueden agotar la pila, son los del tema 12.
La segunda: una función no puede devolver un arreglo. El compilador lo rechaza directamente:
$ gcc -std=c17 -Wall -Wextra -c dev.c
dev.c:1:5: error: ‘f’ declared as function returning an array
1 | int f(void)[3];
| ^
Como un arreglo no es un valor que se pueda copiar, tampoco se puede devolver. Las alternativas son las del tema 12: que quien llama pase el arreglo a llenar, con su tamaño; devolver un puntero a memoria dinámica; o envolver el arreglo en una estructura, que sí se puede devolver y copiar, como veremos en el tema 24.
13. Arreglos en gdb
gdb conoce la diferencia entre un arreglo y un puntero, y la muestra:
$ gdb -q -batch -ex 'break 7' -ex run -ex 'p a' -ex 'ptype a' -ex 'p sizeof a' -ex 'ptype p' -ex 'p *p@5' -ex 'p &a' -ex 'p a[2]@3' ./decae
Breakpoint 1, main () at decae.c:8
8 printf("a = %p\n", (void *) a);
$1 = {10, 20, 30, 40, 50}
type = int [5]
$2 = 20
type = int *
$3 = {10, 20, 30, 40, 50}
$4 = (int (*)[5]) 0x7fffffffd7b0
$5 = {30, 40, 50}
p a muestra el arreglo entero, porque gdb sabe su tamaño, y ptype lo confirma: int [5] para el arreglo, int * para el puntero. Un puntero, en cambio, no dice cuántos elementos hay a partir de su dirección, y hay que indicarlo con el operador @ de gdb: p *p@5 muestra 5 elementos a partir de *p, y p a[2]@3, 3 a partir de a[2]. p &a muestra el tipo «puntero a arreglo de 5 int» de la sección 4. Es la herramienta para inspeccionar un arreglo que una función recibió como puntero.
14. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
Índices de 1 a n, o i <= n | Se lee o escribe un elemento fuera del arreglo. | De 0 a < n. |
| Un arreglo local sin inicializar | Contiene basura. | = {0}, o asignar antes de leer. |
sizeof de un parámetro o de un puntero para contar elementos | Da el tamaño de un puntero: 8. | Pasar la cantidad como parámetro. |
a = b con arreglos | Error: un arreglo no se asigna. | memcpy(a, b, sizeof a). |
a == b con arreglos | Compara direcciones: casi siempre falso. | memcmp; strcmp para cadenas. |
Modificar una cadena a través de char *p = "…" | Segmentation fault: es memoria de solo lectura. | char s[] = "…" si hay que modificarla; const char * si no. |
| Devolver un arreglo local | No se puede devolver un arreglo; su dirección queda colgante. | Buffer del llamador, memoria dinámica o una estructura. |
| Confiar solo en AddressSanitizer | No ve desbordes dentro de una estructura. | Sumar -fsanitize=bounds. |
| Arreglos de tamaño variable con tamaños de los datos | Pueden agotar la pila. | Memoria dinámica (bloque 7). |
15. Resumen
- Un arreglo guarda sus elementos contiguos; el elemento i está en
inicio + i × tamaño, y por eso los índices empiezan en 0. - Una inicialización parcial deja el resto en cero; la designada,
{[2] = 7}, da valor por posición; sin tamaño, se deduce de los valores. sizeof a / sizeof a[0]da la cantidad de elementos, pero solo dondeaes un arreglo.- En casi toda expresión, el nombre de un arreglo se convierte en un puntero a su primer elemento.
- Las excepciones son
sizeof a, que da el tamaño entero,&a, que da un puntero al arreglo entero, y la inicialización de un arreglo con una cadena literal. a[i]es por definición*(a + i), y funciona igual sobre un puntero.- Un arreglo contiene sus elementos; un puntero, una dirección. Un arreglo no se asigna, no se incrementa y no puede ser
NULL. - Un parámetro declarado como arreglo es un puntero: la función no conoce el tamaño, y hay que pasarlo aparte.
[static n]documenta un mínimo. - Los arreglos se copian con
memcpyy se comparan conmemcmp;==compara direcciones. char s[] = "…"es una copia modificable;const char *p = "…"apunta a una cadena literal de solo lectura.- Un arreglo de punteros guarda direcciones; un arreglo de arreglos guarda los datos dentro.
- C no comprueba los índices; un desborde puede pisar otras variables sin aviso.
-fsanitize=boundsy AddressSanitizer detectan casos distintos. - Una función no puede devolver un arreglo; en
gdb,p *p@nmuestra n elementos a partir de un puntero.
16. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Por qué acceder a cualquier elemento de un arreglo cuesta lo mismo?
Porque los elementos están contiguos y todos tienen el mismo tamaño: la dirección del elemento i se calcula directamente como la dirección del primero más i veces el tamaño de un elemento, sin recorrer los anteriores.
2. ¿Qué contiene int v[6] = {5};?
Un 5 en v[0] y ceros en los otros cinco elementos. En una inicialización parcial, los elementos sin valor explícito quedan en cero, también en un arreglo local.
3. ¿Qué significa que el nombre de un arreglo «se convierte en puntero»?
Que cuando el nombre de un arreglo aparece en una expresión, salvo en sizeof y con &, el compilador lo reemplaza por la dirección de su primer elemento, con tipo «puntero al tipo de los elementos». No se crea ninguna variable ni se copia nada: es solo la dirección donde empieza el arreglo.
4. Con int a[5], ¿por qué a y &a muestran la misma dirección, pero a + 1 y &a + 1 no?
Porque las dos son la dirección del principio del arreglo, pero de tipos distintos: a se convierte en int *, un puntero al primer elemento, y &a es un int (*)[5], un puntero al arreglo entero. Sumar 1 avanza el tamaño del tipo apuntado: 4 bytes en el primer caso, 20 en el segundo.
5. ¿Por qué 3[a] es válido y vale lo mismo que a[3]?
Porque el estándar define x[y] como *(x + y). a[3] es *(a + 3) y 3[a] es *(3 + a), y como la suma es conmutativa, las dos expresiones son la misma dirección.
6. ¿Por qué a = b es un error con arreglos pero p = b no?
Porque un arreglo no es una variable que guarde una dirección: su nombre designa la ubicación fija de sus elementos, y no hay nada que cambiar. Un puntero sí es una variable, y asignarle b, que se convierte en la dirección de su primer elemento, cambia a qué apunta.
7. ¿Por qué la macro CANTIDAD(datos) dio 2 dentro de contar?
Porque datos, aunque declarado como int datos[], es un parámetro, y un parámetro arreglo es en realidad un int *. sizeof datos da 8, el tamaño de un puntero, y 8 dividido por los 4 bytes de un int da 2. El tamaño del arreglo original se perdió al pasarlo.
8. ¿Por qué original == otra dio 0 si los dos arreglos tenían los mismos valores?
Porque en la comparación los dos nombres se convierten en punteros a sus primeros elementos, y == compara esas direcciones, que son distintas. Para comparar los contenidos hay que comparar los bytes, con memcmp.
9. ¿Qué diferencia hay entre char s[] = "hola"; y const char *p = "hola";?
s es un arreglo de 5 bytes, del programa, en el que se copiaron los caracteres de la cadena y su \0; se puede modificar, y sizeof s es 5. p es una variable de 8 bytes con la dirección de la cadena literal, que está en memoria de solo lectura; modificarla termina el programa, y sizeof p es 8.
10. ¿Qué diferencia hay entre const char *punteros[4] y char matriz[4][8] con los mismos textos?
punteros es un arreglo de 4 direcciones; los textos están en otro lugar, en la memoria de solo lectura, cada uno con su largo justo. matriz es un arreglo de 4 arreglos de 8 caracteres; los textos están dentro de él, cada uno en una fila de 8 bytes, y se pueden modificar.
11. En limites.c, ¿por qué AddressSanitizer no detectó la escritura en valores[4]?
Porque esa escritura cae sobre clave, dentro de la misma estructura: es memoria válida y asignada, y AddressSanitizer solo marca como prohibida la memoria fuera de los objetos. -fsanitize=bounds sí la detecta, porque comprueba el índice contra el tamaño declarado del arreglo, int [4].
12. ¿Cómo se ve en gdb los primeros 10 elementos de un arreglo que una función recibió como puntero?
Con el operador @: p *datos@10 muestra 10 elementos a partir de la dirección guardada en datos. Hace falta indicarlo porque un puntero no sabe cuántos elementos hay a partir de su dirección, y p datos solo mostraría la dirección.
Para practicar
- Declara un arreglo de 10
longy muestra la dirección de cada elemento. Calcula a mano la diferencia entre la del primero y la del último, y compárala consizeof. - Crea con inicialización designada un arreglo de 32 punteros a cadena que dé nombre a las señales 1, 2, 9, 11 y 15, dejando el resto en
NULL, y escribe una función que devuelva el nombre o"desconocida". - Escribe
void invertir(int datos[], size_t n), que invierta el orden de los elementos en el lugar, usando solo índices. Luego reescríbela usando solo la notación*(datos + i). - Escribe
int iguales(const int a[], const int b[], size_t n)sin usarmemcmp, y comprueba que coincide conmemcmpen varios casos. - Declara
char a[] = "sistema";yconst char *p = "sistema";, muestrasizeofde cada uno, y ubica sus direcciones con el programaregiones.cdel tema 15. - Modifica
limites.cpara que el bucle escriba hastavalores[5], y compila con-fsanitize=addressy con-fsanitize=bounds. ¿Cuál detecta ahora el problema, y por qué? - En
gdb, deténnombresen el bucle y usap punteros,p matriz,ptype punteros,ptype matrizyx/8cb matriz[2]para ver la diferencia entre las dos formas.