#include <stdio.h>
int main(void)
{
int i = 0;
i = i++ + ++i;
printf("%d\n", i); // 3
i = 1;
i = (i++);
printf("%d\n", i); // 2 Should be 1, no ?
volatile int u = 0;
u = u++ + ++u;
printf("%d\n", u); // 1
u = 1;
u = (u++);
printf("%d\n", u); // 2 Should also be one, no ?
register int v = 0;
v = v++ + ++v;
printf("%d\n", v); // 3 (Should be the same as u ?)
int w = 0;
printf("%d %d\n", ++w, w); // shouldn't this print 1 1
int x[2] = { 5, 8 }, y = 0;
x[y] = y ++;
printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
815
(i++)
todavía se evalúa a 1, independientemente de los paréntesisi = (i++);
lo que se pretendía hacer, ciertamente hay una forma más clara de escribirlo. Eso sería cierto incluso si estuviera bien definido. Incluso en Java, que define el comportamiento dei = (i++);
, sigue siendo un código incorrecto. Solo escribei++;
Respuestas:
C tiene el concepto de comportamiento indefinido, es decir, algunas construcciones de lenguaje son sintácticamente válidas pero no puede predecir el comportamiento cuando se ejecuta el código.
Hasta donde yo sé, el estándar no dice explícitamente por qué existe el concepto de comportamiento indefinido. En mi opinión, es simplemente porque los diseñadores de lenguaje querían que hubiera un margen de maniobra en la semántica, en lugar de exigir que todas las implementaciones manejen el desbordamiento de enteros exactamente de la misma manera, lo que probablemente impondría serios costos de rendimiento, simplemente dejaron el comportamiento indefinido, de modo que si escribe código que causa un desbordamiento de enteros, cualquier cosa puede suceder.
Entonces, con eso en mente, ¿por qué son estos "problemas"? El lenguaje dice claramente que ciertas cosas conducen a un comportamiento indefinido . No hay problema, no hay "debería" involucrado. Si el comportamiento indefinido cambia cuando se declara una de las variables involucradas
volatile
, eso no prueba ni cambia nada. Es indefinido ; No puedes razonar sobre el comportamiento.Su ejemplo más interesante, el que tiene
es un ejemplo de libro de texto de comportamiento indefinido (vea la entrada de Wikipedia sobre puntos de secuencia ).
fuente
i = ++i + 1;
.Simplemente compile y desarme su línea de código, si está dispuesto a saber exactamente cómo es que obtiene lo que está obteniendo.
Esto es lo que obtengo en mi máquina, junto con lo que creo que está sucediendo:
(¿Supongo que la instrucción 0x00000014 fue algún tipo de optimización del compilador?)
fuente
gcc evil.c -c -o evil.bin
ygdb evil.bin
→disassemble evil
, o cualesquiera que sean los equivalentes de Windows de ellos :)Why are these constructs undefined behavior?
.gcc -S evil.c
), que es todo lo que se necesita aquí. Montar y luego desarmar es solo una forma indirecta de hacerlo.Creo que las partes relevantes del estándar C99 son 6.5 Expresiones, §2
y 6.5.16 Operadores de asignación, §4:
fuente
i=i=5
también es un comportamiento indefinidoA=B=5;
como "Bloqueo de escritura A; Bloqueo de escritura B; Almacenar 5 en A; almacenar 5 en B; Desbloquear B ; Desbloquear A; ", y una declaraciónC=A+B;
como" Read-lock A; Read-lock B; Compute A + B; Unlock A and B; Write-lock C; Store result; Unlock C; ". Eso aseguraría que si un hilo lo hicieraA=B=5;
mientras que otro lo hiciera,C=A+B;
el último hilo vería que ambas escrituras han tenido lugar o ninguna. Potencialmente una garantía útil. Si un hilo lo hizoI=I=5;
, sin embargo, ...La mayoría de las respuestas aquí citadas del estándar C enfatizan que el comportamiento de estos constructos no está definido. Para comprender por qué el comportamiento de estas construcciones no está definido , comprendamos primero estos términos a la luz del estándar C11:
Secuenciado: (5.1.2.3)
Sin secuencia:
Las evaluaciones pueden ser una de dos cosas:
Punto de secuencia:
Ahora volviendo a la pregunta, para las expresiones como
El estándar dice que:
6.5 Expresiones:
Por lo tanto, la expresión anterior invoca UB porque dos efectos secundarios en el mismo objeto no
i
están secuenciados entre sí. Eso significa que no se secuencia si el efecto secundario por asignacióni
se realizará antes o después del efecto secundario++
.Dependiendo de si la asignación ocurre antes o después del incremento, se producirán resultados diferentes y ese es el caso del comportamiento indefinido .
Cambiemos el nombre a la
i
izquierda de la asignación beil
y a la derecha de la asignación (en la expresióni++
) beir
, entonces la expresión será comoUn punto importante con respecto al
++
operador de Postfix es que:Significa que la expresión
il = ir++
podría evaluarse comoo
dando como resultado dos resultados diferentes
1
y2
que depende de la secuencia de efectos secundarios por asignación++
y, por lo tanto, invoca UB.fuente
El comportamiento en realidad no puede ser explicado porque invoca tanto el comportamiento no especificado y un comportamiento indefinido , por lo que no podemos hacer predicciones generales sobre este código, aunque si se lee de Olve Maudal trabajo como Profundo C y no especificado y Indefinido veces se puede hacer buen adivina en casos muy específicos con un compilador y un entorno específicos, pero no lo haga cerca de la producción.
Entonces, pasando al comportamiento no especificado , en el borrador de la sección estándar c99 , el
6.5
párrafo 3 dice (el énfasis es mío ):Entonces, cuando tenemos una línea como esta:
No sabemos si se evaluará
i++
o++i
se evaluará primero. Esto es principalmente para dar al compilador mejores opciones para la optimización .También tenemos un comportamiento no definido aquí también ya que el programa está modificando las variables (
i
,u
, etc ..) más de una vez entre los puntos de secuencia . Del borrador de la sección estándar6.5
párrafo 2 ( énfasis mío ):cita los siguientes ejemplos de código como indefinidos:
En todos estos ejemplos, el código intenta modificar un objeto más de una vez en el mismo punto de secuencia, lo que terminará
;
en cada uno de estos casos:El comportamiento no especificado se define en el borrador del estándar c99 en la sección
3.4.4
como:y el comportamiento indefinido se define en la sección
3.4.3
como:y señala que:
fuente
Otra forma de responder a esto, en lugar de atascarse en detalles arcanos de puntos de secuencia y comportamiento indefinido, es simplemente preguntar, ¿qué se supone que significan? ¿Qué intentaba hacer el programador?
El primer fragmento sobre el que pregunté
i = i++ + ++i
, es bastante loco en mi libro. Nadie lo escribiría nunca en un programa real, no es obvio lo que hace, no hay un algoritmo concebible que alguien podría haber intentado codificar que hubiera resultado en esta secuencia de operaciones artificial. Y dado que no es obvio para ti y para mí lo que se supone que debe hacer, está bien en mi libro si el compilador tampoco puede entender qué se supone que debe hacer.El segundo fragmento,
i = i++
es un poco más fácil de entender. Alguien claramente está tratando de incrementar i, y asignar el resultado nuevamente a i. Pero hay un par de formas de hacer esto en C. La forma más básica de agregar 1 a i, y asignar el resultado a i, es la misma en casi cualquier lenguaje de programación:C, por supuesto, tiene un atajo útil:
Esto significa "agregar 1 a i y asignar el resultado nuevamente a i". Entonces, si construimos una mezcolanza de los dos, escribiendo
lo que realmente estamos diciendo es "agregue 1 a i, y asigne el resultado nuevamente a i, y asigne el resultado nuevamente a i". Estamos confundidos, así que no me molesta demasiado si el compilador también se confunde.
Siendo realistas, la única vez que se escriben estas expresiones locas es cuando las personas las usan como ejemplos artificiales de cómo se supone que funciona ++. Y, por supuesto, es importante entender cómo funciona ++. Pero una regla práctica para usar ++ es: "Si no es obvio lo que significa una expresión usando ++, no la escriba".
Solíamos pasar innumerables horas comp.lang.c discutiendo expresiones como estas y por qué no están definidas. Dos de mis respuestas más largas, que intentan explicar realmente por qué, están archivadas en la web:
Ver también la pregunta 3.8 y el resto de las preguntas en la sección 3 de la lista C FAQ .
fuente
*p=(*q)++;
significaif (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;
que ya no es el caso. La C hipermoderna requeriría escribir algo como la última formulación (aunque no hay una forma estándar de indicar que el código no le importa lo que hay*p
) para lograr el nivel de eficiencia que los compiladores solían proporcionar con la primera (laelse
cláusula es necesaria para permitir el compilador optimiza loif
que requerirían algunos compiladores más nuevos).assert
declaraciones, de modo que el programador pueda preceder a la línea en cuestión con un simpleassert(p != q)
. (Por supuesto, tomar ese curso también requeriría una reescritura<assert.h>
para no eliminar aserciones directamente en versiones que no sean de depuración, sino que las convierta en algo así para__builtin_assert_disabled()
que el compilador lo pueda ver y luego no emita código).A menudo, esta pregunta está vinculada como un duplicado de preguntas relacionadas con código como
o
o variantes similares.
Si bien esto también es un comportamiento indefinido, como ya se dijo, existen diferencias sutiles cuando
printf()
se trata de comparar con una declaración como:En la siguiente declaración:
el orden de evaluación de argumentos en no
printf()
está especificado . Eso significa, expresionesi++
y++i
podrían evaluarse en cualquier orden. El estándar C11 tiene algunas descripciones relevantes sobre esto:Anexo J, comportamientos no especificados
3.4.4, comportamiento no especificado
El comportamiento no especificado en sí NO es un problema. Considere este ejemplo:
Esto también tiene un comportamiento no especificado porque el orden de evaluación de
++x
yy++
no está especificado. Pero es una declaración perfectamente legal y válida. No hay un comportamiento indefinido en esta declaración. Porque las modificaciones (++x
yy++
) se realizan a objetos distintos .Lo que representa la siguiente declaración
como comportamiento indefinido es el hecho de que estas dos expresiones modifican el mismo objeto
i
sin un punto de secuencia intermedio .Otro detalle es que la coma involucrada en la llamada printf () es un separador , no el operador de coma .
Esta es una distinción importante porque el operador de coma introduce un punto de secuencia entre la evaluación de sus operandos, lo que hace que lo siguiente sea legal:
El operador de coma evalúa sus operandos de izquierda a derecha y produce solo el valor del último operando. Así que en
j = (++i, i++);
,++i
incrementosi
a6
yi++
rendimientos antiguo valor dei
(6
), que se asigna aj
. Luego sei
vuelve7
debido al post-incremento.Entonces, si la coma en la llamada de función fuera un operador de coma, entonces
No será un problema. Pero invoca un comportamiento indefinido porque la coma aquí es un separador .
Para aquellos que son nuevos en el comportamiento indefinido, se beneficiarían de leer Lo que todo programador de C debe saber sobre el comportamiento indefinido para comprender el concepto y muchas otras variantes del comportamiento indefinido en C.
Esta publicación: El comportamiento indefinido, no especificado y definido por la implementación también es relevante.
fuente
int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));
parece dar un comportamiento estable (evaluación de argumento de derecha a izquierda en gcc v7.3.0; resultado "a = 110 b = 40 c = 60"). ¿Es porque las asignaciones se consideran 'declaraciones completas' y, por lo tanto, introducen un punto de secuencia? ¿No debería eso resultar en una evaluación de argumento / declaración de izquierda a derecha? O, ¿es solo manifestación de un comportamiento indefinido?b
yc
en 3er y 4to argumentos respectivamente y estás leyendo en 2do argumento. Pero no hay una secuencia entre estas expresiones (2 °, 3 ° y 4 ° args). gcc / clang tiene una opción-Wsequence-point
que también puede ayudar a encontrarlos.Si bien es poco probable que algún compilador y procesador lo haga, sería legal, según el estándar C, que el compilador implemente "i ++" con la secuencia:
Si bien no creo que ningún procesador sea compatible con el hardware para permitir que tal cosa se haga de manera eficiente, uno puede imaginar fácilmente situaciones en las que tal comportamiento haría más fácil el código multiproceso (por ejemplo, garantizaría que si dos hilos intentan realizar lo anterior secuencia simultánea,
i
se incrementaría en dos) y no es totalmente inconcebible que algún procesador futuro pueda proporcionar una característica como esa.Si el compilador fuera a escribir .
i++
como se indicó anteriormente (legal bajo el estándar) e intercalara las instrucciones anteriores a lo largo de la evaluación de la expresión general (también legal), y si no se notara que sucedió una de las otras instrucciones para accederi
, sería posible (y legal) para el compilador generar una secuencia de instrucciones que se estancarían. Sin duda, un compilador seguramente detectará el problema en el caso en quei
se use la misma variable en ambos lugares, pero si una rutina acepta referencias a dos punterosp
yq
, y usa(*p)
y y(*q)
en la expresión anterior (en lugar de usari
dos veces) no se requeriría que el compilador reconozca o evite el punto muerto que ocurriría si se pasara la misma dirección del objeto para ambosp
q
fuente
Mientras que la sintaxis de las expresiones gusta
a = a++
oa++ + a++
es legal, el comportamiento de estas construcciones es indefinido debido a que una se en C estándar no se cumple. C99 6.5p2 :Con la nota a pie de página 73 aclarando aún más que
Los diversos puntos de secuencia se enumeran en el Anexo C de C11 (y C99 ):
La redacción del mismo párrafo en C11 es:
Puede detectar dichos errores en un programa, por ejemplo, utilizando una versión reciente de GCC con
-Wall
y-Werror
, y luego GCC se negará por completo a compilar su programa. La siguiente es la salida de gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:La parte importante es saber qué es un punto de secuencia, y qué es un punto de secuencia y qué no lo es . Por ejemplo, el operador de coma es un punto de secuencia, entonces
está bien definido e incrementará
i
en uno, produciendo el valor anterior, descarta ese valor; luego en el operador de coma, resuelva los efectos secundarios; y luego se incrementai
en uno, y el valor resultante se convierte en el valor de la expresión, es decir, esta es solo una forma artificial de escribirj = (i += 2)
que, una vez más, es una forma "inteligente" de escribirSin embargo, las
,
listas de argumentos en función no son operadores de coma y no hay un punto de secuencia entre las evaluaciones de argumentos distintos; en cambio, sus evaluaciones no tienen secuencia entre sí; entonces la llamada a la funcióntiene un comportamiento indefinido porque no hay un punto de secuencia entre las evaluaciones de
i++
y++i
en los argumentos de la función yi
, por lo tanto , el valor de se modifica dos veces, por ambosi++
y++i
, entre el punto de secuencia anterior y el siguiente.fuente
El estándar C dice que una variable solo debe asignarse como máximo una vez entre dos puntos de secuencia. Un punto y coma, por ejemplo, es un punto de secuencia.
Entonces cada declaración de la forma:
y así sucesivamente violan esa regla. El estándar también dice que el comportamiento no está definido y no está especificado. Algunos compiladores los detectan y producen algún resultado, pero esto no es estándar.
Sin embargo, se pueden incrementar dos variables diferentes entre dos puntos de secuencia.
Lo anterior es una práctica de codificación común al copiar / analizar cadenas.
fuente
En /programming/29505280/incrementing-array-index-in-c alguien preguntó acerca de una declaración como:
que imprime 7 ... el OP esperaba que imprimiera 6.
No
++i
se garantiza que todos los incrementos se completen antes del resto de los cálculos. De hecho, diferentes compiladores obtendrán diferentes resultados aquí. En el ejemplo que nos ha facilitado, la primera 2++i
ejecutados, entonces los valores dek[]
se leyeron, a continuación, la última++i
a continuaciónk[]
.Los compiladores modernos optimizarán esto muy bien. De hecho, posiblemente mejor que el código que escribió originalmente (suponiendo que haya funcionado de la manera que esperaba).
fuente
En el documento n1188 del sitio ISO W14 se proporciona una buena explicación sobre lo que sucede en este tipo de cálculo. .
Les explico las ideas.
La regla principal de la norma ISO 9899 que se aplica en esta situación es 6.5p2.
Los puntos de secuencia en una expresión como
i=i++
son antesi=
y despuési++
.En el documento que cité anteriormente, se explica que puede descubrir que el programa está formado por pequeños cuadros, cada cuadro contiene las instrucciones entre 2 puntos de secuencia consecutivos. Los puntos de secuencia se definen en el anexo C de la norma, en el caso de que
i=i++
haya 2 puntos de secuencia que delimitan una expresión completa. Tal expresión es sintácticamente equivalente con una entradaexpression-statement
en la forma de la gramática Backus-Naur (se proporciona una gramática en el anexo A de la Norma).Por lo tanto, el orden de las instrucciones dentro de una caja no tiene un orden claro.
puede ser interpretado como
o como
porque ambas formas para interpretar el código
i=i++
son válidas y porque ambas generan respuestas diferentes, el comportamiento es indefinido.Entonces, se puede ver un punto de secuencia al principio y al final de cada cuadro que compone el programa [los cuadros son unidades atómicas en C] y dentro de un cuadro el orden de las instrucciones no está definido en todos los casos. Cambiando ese orden uno puede cambiar el resultado a veces.
EDITAR:
Otra buena fuente para explicar tales ambigüedades son las entradas del sitio c-faq (también publicado como libro ), a saber, aquí y aquí y aquí .
fuente
i=i++
es muy similar a esta respuesta .Su pregunta probablemente no fue, "¿Por qué estas construcciones son comportamientos indefinidos en C?". Probablemente su pregunta fue: "¿Por qué este código (usando
++
) no me dio el valor que esperaba?", Y alguien marcó su pregunta como un duplicado y lo envió aquí.Esta respuesta intenta responder a esa pregunta: ¿por qué su código no le dio la respuesta que esperaba y cómo puede aprender a reconocer (y evitar) expresiones que no funcionarán como se esperaba?
Supongo que ya ha escuchado la definición básica de C
++
y--
operadores, y cómo el prefijo++x
difiere del postfixx++
. Pero es difícil pensar en estos operadores, así que para asegurarte de que entiendes, tal vez escribiste un pequeño programa de prueba que involucra algo comoPero, para su sorpresa, este programa no lo ayudó a comprender: imprimió un resultado extraño, inesperado e inexplicable, lo que sugiere que tal vez
++
hace algo completamente diferente, en absoluto lo que pensaba que hizo.O tal vez estás viendo una expresión difícil de entender como
Quizás alguien te dio ese código como un rompecabezas. Este código tampoco tiene sentido, especialmente si lo ejecuta, y si lo compila y ejecuta bajo dos compiladores diferentes, ¡es probable que obtenga dos respuestas diferentes! ¿Que pasa con eso? ¿Qué respuesta es la correcta? (Y la respuesta es que ambos lo son, o ninguno lo es).
Como ya has escuchado, todas estas expresiones son indefinidas , lo que significa que el lenguaje C no garantiza lo que harán. Este es un resultado extraño y sorprendente, porque probablemente pensó que cualquier programa que pudiera escribir, siempre y cuando se compilara y ejecutara, generaría un resultado único y bien definido. Pero en el caso de un comportamiento indefinido, eso no es así.
¿Qué hace que una expresión sea indefinida? ¿Son las expresiones envolventes
++
y--
siempre indefinidas? Por supuesto que no: estos son operadores útiles, y si los usa correctamente, están perfectamente bien definidos.Para las expresiones de las que estamos hablando, lo que las hace indefinidas es cuando ocurren muchas cosas a la vez, cuando no estamos seguros de en qué orden sucederán las cosas, pero cuando el orden es importante para el resultado que obtenemos.
Volvamos a los dos ejemplos que he usado en esta respuesta. Cuando escribi
la pregunta es, antes de llamar
printf
, ¿el compilador calcula el valor dex
primero, ox++
, o tal vez++x
? Pero resulta que no sabemos . No hay una regla en C que diga que los argumentos de una función se evalúan de izquierda a derecha, o de derecha a izquierda, o en algún otro orden. Así que no podemos decir si el compilador haráx
primero, y luego++x
, a continuaciónx++
, ox++
entonces++x
a continuaciónx
, o algún otro fin. Pero el orden claramente importa, porque dependiendo del orden que use el compilador, obtendremos claramente diferentes resultados impresosprintf
.¿Qué hay de esta expresión loca?
El problema con esta expresión es que contiene tres intentos diferentes de modificar el valor de x: (1) la
x++
parte intenta agregar 1 a x, almacenar el nuevo valorx
y devolver el valor anterior dex
; (2) la++x
parte intenta agregar 1 a x, almacenar el nuevo valorx
y devolver el nuevo valor dex
; y (3) lax =
parte intenta asignar la suma de los otros dos a x. ¿Cuál de esos tres intentos de tareas "ganará"? ¿A cuál de los tres valores se le asignará realmentex
? De nuevo, y quizás sorprendentemente, no hay una regla en C que nos diga.Puede imaginar que la precedencia o la asociatividad o la evaluación de izquierda a derecha le dicen en qué orden suceden las cosas, pero no es así. Puede que no me creas, pero por favor toma mi palabra y lo diré de nuevo: la precedencia y la asociatividad no determinan todos los aspectos del orden de evaluación de una expresión en C. En particular, si dentro de una expresión hay múltiples diferentes lugares donde tratamos de asignar un nuevo valor a algo como
x
, la precedencia y la asociatividad no nos dicen cuál de esos intentos ocurre primero, o el último, o algo así.Entonces, con todo ese trasfondo e introducción fuera del camino, si quieres asegurarte de que todos tus programas estén bien definidos, ¿qué expresiones puedes escribir y cuáles no?
Estas expresiones están todas bien:
Estas expresiones son todas indefinidas:
Y la última pregunta es, ¿cómo puede saber qué expresiones están bien definidas y qué expresiones no están definidas?
Como dije antes, las expresiones indefinidas son aquellas en las que hay demasiadas cosas a la vez, donde no puedes estar seguro de en qué orden suceden las cosas y dónde importa el orden:
Como un ejemplo de # 1, en la expresión
Hay tres intentos de modificar `x.
Como un ejemplo de # 2, en la expresión
ambos usamos el valor de
x
y lo modificamos.Entonces esa es la respuesta: asegúrese de que en cualquier expresión que escriba, cada variable se modifique como máximo una vez, y si se modifica una variable, no intente usar el valor de esa variable en otro lugar.
fuente
La razón es que el programa está ejecutando un comportamiento indefinido. El problema radica en el orden de evaluación, porque no se requieren puntos de secuencia según el estándar C ++ 98 (no se secuencian operaciones antes o después de otra según la terminología de C ++ 11).
Sin embargo, si se atiene a un compilador, encontrará que el comportamiento es persistente, siempre que no agregue llamadas a funciones o punteros, lo que haría que el comportamiento sea más desordenado.
Primero el GCC: Usando Nuwen MinGW 15 GCC 7.1 obtendrás:
}
¿Cómo funciona GCC? evalúa las expresiones secundarias en un orden de izquierda a derecha para el lado derecho (RHS), luego asigna el valor al lado izquierdo (LHS). Así es exactamente como Java y C # se comportan y definen sus estándares. (Sí, el software equivalente en Java y C # tiene comportamientos definidos). Evalúa cada subexpresión una por una en la Declaración RHS en un orden de izquierda a derecha; para cada subexpresión: el ++ c (pre-incremento) se evalúa primero, luego se usa el valor c para la operación, luego el incremento posterior c ++).
según GCC C ++: operadores
El código equivalente en el comportamiento definido C ++ como GCC entiende:
Luego vamos a Visual Studio . Visual Studio 2015, obtienes:
¿Cómo funciona el estudio visual? Toma otro enfoque, evalúa todas las expresiones de pre-incrementos en el primer paso, luego usa valores de variables en las operaciones en el segundo paso, asigna de RHS a LHS en el tercer paso, luego en el último paso evalúa todos los expresiones de incremento posterior en una pasada.
Entonces, el equivalente en el comportamiento definido C ++ como Visual C ++ comprende:
como lo indica la documentación de Visual Studio en Precedencia y orden de evaluación :
fuente