Diferencia entre puntero de desreferenciación y acceso a elementos de matriz
arrays pointers (3)
Recuerdo un ejemplo donde se demostró la diferencia entre punteros y matrices.
Una matriz se descompone en un puntero al primer elemento de una matriz cuando se pasa como un parámetro de función, pero no son equivalentes, como se muestra a continuación:
//file file1.c
int a[2] = {800, 801};
int b[2] = {100, 101};
//file file2.c
extern int a[2];
// here b is declared as pointer,
// although the external unit defines it as an array
extern int *b;
int main() {
int x1, x2;
x1 = a[1]; // ok
x2 = b[1]; // crash at runtime
return 0;
}
El enlazador no comprueba las variables externas para que no se generen errores en el momento de la compilación. El problema es que b
es de hecho una matriz, pero la unidad de compilación file2
no es consciente de eso y trata a b
como un puntero, lo que resulta en un fallo al intentar desreferirlo.
Recuerdo que cuando se explicó esto tenía mucho sentido, pero ahora no puedo recordar la explicación ni puedo llegar a ella por mi cuenta.
Entonces, supongo que la pregunta es ¿cómo se trata una matriz de manera diferente a un puntero cuando se accede a los elementos? (porque pensé que p[1]
se convierte a (el equivalente de ensamblaje de) *(p + 1)
independientemente de si p
es una matriz o un puntero, obviamente estoy equivocado).
El ensamblaje generado por las dos referencias (VS 2013):
nota: 1158000h
y 1158008h
son las direcciones de memoria de a
y b
respectivamente
12: x1 = a[1];
0115139E mov eax,4
011513A3 shl eax,0
011513A6 mov ecx,dword ptr [eax+1158000h]
011513AC mov dword ptr [x1],ecx
13: x2 = b[1];
011513AF mov eax,4
011513B4 shl eax,0
011513B7 mov ecx,dword ptr ds:[1158008h]
011513BD mov edx,dword ptr [ecx+eax]
011513C0 mov dword ptr [x2],edx
Esto no responde completamente a tu pregunta, pero te da una pista de lo que está sucediendo. Modifica tu código un poco para dar.
//file1.c
int a[2] = {800, 801};
int b[2] = {255, 255};
#include <stdio.h>
extern int a[2];
// here b is declared as pointer,
// although the external unit declares it as an array
extern int *b;
int *c;
int main() {
int x1, x2;
x1 = a[1]; // ok
c = b;
printf("allocated x1 OK/n");
printf("a is %p/n", a);
printf("c is %p/n", c);
x2 = *(c+1);
printf("%d %d/n", x1, x2);
return 0;
}
Ahora, cuando lo ejecutas, todavía obtienes una falla de seguridad. Pero justo antes de hacerlo, obtienes una idea de por qué:
allocated x1 OK
a is 0x10afa4018
c is 0xff000000ff
Segmentation fault: 11
El valor del puntero c no es lo que usted espera: en lugar de ser el puntero al inicio de la matriz b
(que sería una ubicación de memoria sensible cerca de a), parece contener el contenido de la matriz b ... ( 0xff
es 255
en hexadecimal, por supuesto).
No puedo explicar con claridad por qué es eso. Para eso, vea el books.google.com.au/… que fue proporcionado por @tesseract en los comentarios (en realidad, todo el capítulo 4 es extremadamente útil).
Gracias al enlace proporcionado por @tesseract en los comentarios: Programación Experto C: Secretos Profundos C (página 96), se me ocurrió una respuesta simple (una versión sencilla y sencilla de la explicación del libro; para una respuesta académica completa lea el libro):
- cuando se declara
int a[2]
:- el compilador tiene una dirección donde se almacena esta variable. Esta dirección también es la dirección de la matriz, ya que el tipo de la variable es la matriz.
- Accediendo
a[1]
significa:- recuperando esa dirección,
- añadiendo el desplazamiento y
- Accediendo a la memoria en esta nueva dirección computada.
- cuando se declara
int *b
:- El compilador también tiene una dirección para
b
pero esta es la dirección de la variable de puntero, no la matriz. - Así que acceder a
b[1]
significa:- recuperando esa dirección,
- acceder a esa ubicación para obtener el valor de
b
, es decir, la dirección de la matriz - añadiendo un desplazamiento a esta dirección y luego
- Accediendo a la ubicación de la memoria final.
- El compilador también tiene una dirección para
// in file2.c
extern int *b; // b is declared as a pointer to an integer
// in file1.c
int b[2] = {100, 101}; // b is defined and initialized as an array of 2 ints
El vinculador los vincula a la misma dirección de memoria, sin embargo, como el símbolo b
tiene diferentes tipos en file1.c
y file2.c
, la misma ubicación de memoria se interpreta de manera diferente.
// in file2.c
int x2; // assuming sizeof(int) == 4
x2 = b[1]; // b[1] == *(b+1) == *(100 + 1) == *(104) --> segfault
b[1]
se evalúa primero como *(b+1)
. Esto significa que obtener el valor en la ubicación de memoria b
está vinculado, agregarle 1
(aritmética de punteros) para obtener una nueva dirección, cargar ese valor en el registro de la CPU, almacenar ese valor en la ubicación a la que está vinculado x2
. Por lo tanto, el valor en la ubicación b
está vinculado a 100
, sumarle 1
para obtener 104
(aritmética de punteros; sizeof *b
es 4) y obtener el valor en la dirección 104
! Este es un comportamiento incorrecto e indefinido y lo más probable es que cause una caída del programa.
Hay una diferencia en cómo se accede a los elementos de una matriz y cómo se accede a los valores apuntados por un puntero. Tomemos un ejemplo.
int a[] = {100, 800};
int *b = a;
a
es una matriz de 2
enteros y b
es un puntero a un entero inicializado en la dirección del primer elemento de a
. Ahora, cuando se accede a a[1]
, significa obtener lo que haya en el desplazamiento 1
de la dirección de a[0]
, la dirección (y el siguiente bloque) a la que está vinculado el símbolo a
. Esa es una instrucción de montaje. Es como si alguna información estuviera incrustada en el símbolo de la matriz para que la CPU pueda recuperar un elemento en un desplazamiento desde la dirección base de la matriz en una sola instrucción. Cuando accede a *b
o b[0]
o b[1]
, primero obtiene el contenido de b
que es una dirección, luego hace la aritmética del puntero para obtener una nueva dirección y luego obtiene lo que haya en esa dirección. Así que la CPU debe cargar primero el contenido de b
, evaluar b+1
(para b[1]
) y luego cargar el contenido en la dirección b+1
. Son dos instrucciones de montaje.
Para una matriz externa, no es necesario que especifique su tamaño. El único requisito es que debe coincidir con su definición externa. Por lo tanto, ambas declaraciones son equivalentes:
extern int a[2]; // equivalent to the below statement
extern int a[];
Debe hacer coincidir el tipo de la variable en su declaración con su definición externa. El enlazador no comprueba los tipos de variables al resolver las referencias de los símbolos. Sólo las funciones tienen los tipos de la función codificados en el nombre de la función. Por lo tanto, no recibirá ninguna advertencia o error y se compilará bien.
Técnicamente, el enlazador o algún componente del compilador podrían rastrear qué tipo de símbolo representa, y luego dar un error o advertencia. Pero no hay ningún requisito de la norma para hacerlo. Se requiere que hagas lo correcto.