operadores - punteros c++
¿Sintaxis de arrays vs sintaxis de punteros y generación de código? (8)
El Estándar especifica el comportamiento de
arr[i]
cuando
arr
es un objeto de matriz como equivalente a descomponer
arr
en un puntero, agregando
i
y eliminando la referencia del resultado.
Aunque los comportamientos serían equivalentes en todos los casos definidos por el Estándar, hay algunos casos en que los compiladores procesan acciones de manera útil aunque el Estándar lo requiera, y el manejo de
arrayLvalue[i]
y
*(arrayLvalue+i)
puede diferir como consecuencia .
Por ejemplo, dado
char arr[5][5];
union { unsigned short h[4]; unsigned int w[2]; } u;
int atest1(int i, int j)
{
if (arr[1][i])
arr[0][j]++;
return arr[1][i];
}
int atest2(int i, int j)
{
if (*(arr[1]+i))
*((arr[0])+j)+=1;
return *(arr[1]+i);
}
int utest1(int i, int j)
{
if (u.h[i])
u.w[j]=1;
return u.h[i];
}
int utest2(int i, int j)
{
if (*(u.h+i))
*(u.w+j)=1;
return *(u.h+i);
}
El código generado por GCC para la prueba1 asumirá que arr [1] [i] y arr [0] [j] no puede obtener un alias, pero el código generado para la prueba2 permitirá que la aritmética de punteros acceda a toda la matriz. En la otra cara, gcc reconozca que en utest1, las expresiones de valores uh [i] y uw [j] acceden a la misma unión, pero no es lo suficientemente sofisticado como para notar lo mismo acerca de * (u.h + i) y * (u.w + j) en utest2.
En el libro, "Cómo entender y usar punteros C" por Richard Reese, se dice en la página 85,
int vector[5] = {1, 2, 3, 4, 5};
El código generado por el
vector[i]
es diferente del código generado por*(vector+i)
. Elvector[i]
notaciónvector[i]
genera un código de máquina que comienza en el vector de ubicación, muevei
posiciones desde esta ubicación y utiliza su contenido. La notación*(vector+i)
genera un código de máquina que comienza en elvector
ubicación, agregai
a la dirección y luego usa el contenido de esa dirección. Mientras que el resultado es el mismo, el código de máquina generado es diferente. Esta diferencia rara vez es importante para la mayoría de los programadores.
Puedes ver el extracto aquí . ¿Qué significa este pasaje? ¿En qué contexto generaría un compilador diferente código para esos dos? ¿Hay una diferencia entre "mover" desde la base y "agregar" a la base? No pude hacer que esto funcionara en GCC, generando diferentes códigos de máquina.
El pasaje citado está bastante mal.
Las expresiones
vector[i]
y
*(vector+i)
son perfectamente idénticas y se puede esperar que generen un código idéntico en todas las circunstancias.
Las expresiones
vector[i]
y
*(vector+i)
son idénticas
por definición
.
Esta es una propiedad central y fundamental del lenguaje de programación C.
Cualquier programador de C competente entiende esto.
Cualquier autor de un libro titulado
Comprender y usar punteros en C
debe entender esto.
Cualquier autor de un compilador de C entenderá esto.
Los dos fragmentos generarán un código idéntico, no por accidente, sino porque prácticamente cualquier compilador de C, en efecto, traducirá una forma a la otra casi de inmediato, de modo que cuando llegue a su fase de generación de código, ni siquiera sabrá qué forma se había utilizado inicialmente.
(Me sorprendería mucho si un compilador de C generara un código significativamente diferente para el
vector[i]
en lugar de
*(vector+i)
.)
Y de hecho, el texto citado se contradice. Como habrás notado, los dos pasajes.
El
vector[i]
notaciónvector[i]
genera un código de máquina que comienza en elvector
ubicación, muevei
posiciones desde esta ubicación y utiliza su contenido.
y
La notación
*(vector+i)
genera un código de máquina que comienza en elvector
ubicación, agregai
a la dirección y luego usa el contenido de esa dirección.
Dicen básicamente lo mismo.
Su lenguaje es inquietantemente similar al de la pregunta 6.2 de la antigua lista de preguntas frecuentes de C :
... cuando el compilador ve la expresión
a[3]
, emite código para comenzar en la ubicación "a
", mueve tres más allá y busca el carácter allí. Cuando ve la expresiónp[3]
, emite código para comenzar en la ubicación "p
", busca el valor del puntero allí, agrega tres al puntero y finalmente busca el carácter al que se apunta.
Pero, por supuesto, la diferencia clave aquí es que
a
es una matriz y
p
es un puntero
.
La lista de preguntas frecuentes no habla sobre
a[3]
versus
*(a+3)
, sino más bien sobre
a[3]
(o
*(a+3)
) donde
a
es una matriz, versus
p[3]
(o
*(p+3)
) donde
p
es un puntero.
(Por supuesto, esos dos casos generan un código diferente, ya que las matrices y los punteros son diferentes. Como se explica en la lista de preguntas frecuentes, obtener una dirección de una variable de puntero es fundamentalmente diferente a usar la dirección de una matriz).
Esta es una sintaxis de matriz de muestra como se usa en C.
int a[10] = {1,2,3,4,5,6,7,8,9,10};
La cita está mal.
Bastante trágico que tal basura todavía se publique en esta década.
De hecho, el Estándar C define
x[y]
como
*(x+y)
.
La parte sobre los valores más adelante en la página también está completamente equivocada.
En mi humilde opinión, la mejor manera de usar este libro es ponerlo en un contenedor de reciclaje o quemarlo.
Permítame tratar de responder esto "en el estrecho" (otros ya han descrito por qué la descripción "tal cual" carece de algo / está incompleta / es engañosa):
¿En qué contexto generaría un compilador diferente código para esos dos?
Un compilador "no muy optimizado" puede generar código diferente en casi cualquier contexto, porque al analizar, hay una diferencia:
x[y]
es una expresión (indexar en una matriz), mientras que
*(x+y)
son
dos
expresiones (agregue un entero a un puntero, luego elimínelo).
Claro, no es muy difícil reconocer esto (incluso mientras se analiza) y tratarlo de la misma manera, pero, si estás escribiendo un compilador simple / rápido, evitas poner "demasiada inteligencia en él".
Como ejemplo:
char vector[] = ...;
char f(int i) {
return vector[i];
}
char g(int i) {
return *(vector + i);
}
El compilador, mientras analiza
f()
, ve la "indexación" y puede generar algo como (para una CPU similar a 68000):
MOVE D0, [A0 + D1] ; A0/vector, D1/i, D0/result of function
OTOH, para
g()
, el compilador ve dos cosas: primero una desreferencia (de "algo por venir") y luego la adición de un entero a un puntero / matriz, por lo que no es muy optimizadora, podría terminar con:
MOVE A1, A0 ; A1/t = A0/vector
ADD A1, D1 ; t += i/D1
MOVE D0, [A1] ; D0/result = *t
Obviamente, esto depende mucho de la implementación, a algunos compiladores también les puede disgustar el uso de instrucciones complejas como se usa para
f()
(el uso de instrucciones complejas dificulta la depuración del compilador), es posible que la CPU no tenga instrucciones tan complejas, etc.
¿Hay una diferencia entre "mover" desde la base y "agregar" a la base?
La descripción en el libro no está bien redactada. Pero, creo que el autor quería describir la distinción que se muestra arriba: la indexación ("mover" de la base) es una expresión, mientras que "agregar y luego eliminar referencias" son dos expresiones.
Se trata de la implementación del compilador , no de la definición de lenguaje, la distinción que también debería haber sido explícitamente indicada en el libro.
Probé el Código para algunas variaciones del compilador, la mayoría me da el mismo código de ensamblaje para ambas instrucciones (probado para x86 sin optimización). Interesante es que el gcc 4.4.7 hace exactamente lo que usted mencionó: Ejemplo:
Otros idiomas como ARM o MIPS a veces hacen lo mismo, pero no lo probé todo. Así que parece que hubo una diferencia, pero las versiones posteriores de gcc "arreglaron" este error.
Tengo 2 archivos C:
ex1.c
% cat ex1.c
#include <stdio.h>
int main (void) {
int vector[5] = { 1, 2, 3, 4, 5 };
printf("%d/n", vector[3]);
}
y
ex2.c
,
% cat ex2.c
#include <stdio.h>
int main (void) {
int vector[5] = { 1, 2, 3, 4, 5 };
printf("%d/n", *(vector + 3));
}
Y compilo ambos en ensamblaje, y muestro la diferencia en el código de ensamblaje generado
% gcc -S ex1.c; gcc -S ex2.c; diff -u ex1.s ex2.s
--- ex1.s 2018-07-17 08:19:25.425826813 +0300
+++ ex2.s 2018-07-17 08:19:25.441826756 +0300
@@ -1,4 +1,4 @@
- .file "ex1.c"
+ .file "ex2.c"
.text
.section .rodata
.LC0:
QED
El estándar C declara muy explícitamente (C11 n1570 6.5.2.1p2) :
- Una expresión de posfijo seguida de una expresión entre corchetes
[]
es una designación de subíndice de un elemento de un objeto de matriz. La definición del operador de subíndice[]
es queE1[E2]
es idéntico a(*((E1)+(E2)))
. Debido a las reglas de conversión que se aplican al operador binario+
, siE1
es un objeto de matriz (de manera equivalente, un puntero al elemento inicial de un objeto de matriz) yE2
es un número entero,E1[E2]
designa el elementoE2
-th deE1
(contando desde cero).
Además, la regla "como si" se aplica aquí: si el comportamiento del programa es el mismo, el compilador puede generar el mismo código aunque la semántica no sea la misma.
Creo que el texto original puede estar refiriéndose a algunas optimizaciones que un compilador puede realizar o no.
Ejemplo:
for ( int i = 0; i < 5; i++ ) {
vector[i] = something;
}
contra
for ( int i = 0; i < 5; i++ ) {
*(vector+i) = something;
}
En el primer caso, un compilador de optimización puede detectar que el
vector
matriz se itera sobre elemento por elemento y, por lo tanto, genera algo como
void* tempPtr = vector;
for ( int i = 0; i < 5; i++ ) {
*((int*)tempPtr) = something;
tempPtr += sizeof(int); // _move_ the pointer; simple addition of a constant.
}
Incluso podría ser capaz de usar las instrucciones de post-incremento del puntero de la CPU de destino cuando estén disponibles.
Para el segundo caso, es "más difícil" para el compilador ver que la
dirección
que se calcula mediante una expresión aritmética de puntero "arbitraria" muestra la misma propiedad de avanzar monótonamente una cantidad fija en cada iteración.
Por lo tanto, es posible que no encuentre la optimización y el cálculo
((void*)vector+i*sizeof(int))
en cada iteración que utiliza una multiplicación adicional.
En este caso, no hay un puntero (temporal) que se "mueva" sino que solo se recalcula una dirección temporal.
Sin embargo, es probable que la declaración no se mantenga universalmente para todos los compiladores de C en todas las versiones.
Actualizar:
Revisé el ejemplo anterior. Parece que sin las optimizaciones habilitadas, al menos gcc-8.1 x86-64 genera más código (2 instrucciones adicionales) para la segunda forma (puntero-aritmética) que la primera (índice de matriz).
Consulte: https://godbolt.org/g/7DaPHG
Sin embargo, con las optimizaciones activadas (
-O
...
-O3
) el código generado es el mismo (longitud) para ambos.