c increment undefined-behavior order-of-evaluation sequence-points

¿Por qué estas construcciones utilizan el comportamiento indefinido pre y post-incremento?



undefined-behavior order-of-evaluation (14)

#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 %d/n", w++, ++w, w); // shouldn''t this print 0 2 2 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? }


A menudo, esta pregunta está vinculada como un duplicado de preguntas relacionadas con código como

printf("%d %d/n", i, i++);

o

printf("%d %d/n", ++i, i++);

o variantes similares.

Si bien esto también es un comportamiento indefinido como ya se dijo, hay diferencias sutiles cuando printf() está involucrado cuando se compara con una declaración como:

x = i++ + i++;

En la siguiente declaración:

printf("%d %d/n", ++i, i++);

el orden de evaluación de los argumentos en printf() unspecified está unspecified . Eso significa que las expresiones i++ y ++i podrían evaluarse en cualquier orden. El estándar C11 tiene algunas descripciones relevantes sobre esto:

Anexo J, conductas no especificadas.

El orden en el cual el designador de la función, los argumentos y las subexpresiones dentro de los argumentos se evalúan en una llamada a la función (6.5.2.2).

3.4.4, comportamiento no especificado

Uso de un valor no especificado, u otro comportamiento donde esta Norma Internacional proporciona dos o más posibilidades y no impone requisitos adicionales sobre los que se elija en ningún caso.

EJEMPLO Un ejemplo de comportamiento no especificado es el orden en que se evalúan los argumentos de una función.

El comportamiento no especificado en sí NO es un problema. Considera este ejemplo:

printf("%d %d/n", ++x, y++);

Esto también tiene un comportamiento no especificado porque el orden de evaluación de ++x e y++ no está especificado. Pero es una declaración perfectamente legal y válida. No hay un comportamiento indefinido en esta declaración. Debido a que las modificaciones ( ++x e y++ ) se realizan a objetos distintos .

Lo que hace la siguiente declaración

printf("%d %d/n", ++i, i++);

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 a 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:

int i = 5; int j; j = (++i, i++); // No undefined behaviour here because the comma operator // introduces a sequence point between ''++i'' and ''i++'' printf("i=%d j=%d/n",i, j); // prints: i=7 j=6

El operador de coma evalúa sus operandos de izquierda a derecha y proporciona solo el valor del último operando. Entonces en j = (++i, i++); , ++i incrementa i a 6 e i++ produce un valor antiguo de i ( 6 ) que se asigna a j . Entonces i convierte en 7 debido al post-incremento.

Entonces, si la coma en la llamada de función fuera un operador de coma, entonces

printf("%d %d/n", ++i, i++);

no sera un problema Pero invoca un comportamiento indefinido porque la coma aquí es un separador .

Para aquellos que son nuevos en el comportamiento no definido, se beneficiaría de leer Lo que todo programador de C debería saber sobre el comportamiento indefinido para comprender el concepto y muchas otras variantes del comportamiento no definido en C.

Este post: comportamiento indefinido, no especificado y definido por la implementación también es relevante.


C tiene el concepto de comportamiento no definido, es decir, algunas construcciones de lenguaje son sintácticamente válidas pero no se puede predecir el comportamiento cuando se ejecuta el código.

Que yo sepa, 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 cierto margen de maniobra en la semántica, en lugar de exigir que todas las implementaciones manejaran el desbordamiento de enteros de la misma manera, lo que probablemente impondría costos de rendimiento serios, simplemente dejaron el comportamiento indefinido, de modo que si escribe código que cause 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 una de las variables involucradas se declara volatile , eso no prueba ni cambia nada. Está indefinido ; No puedes razonar sobre el comportamiento.

Su ejemplo más interesante, el de

u = (u++);

es un ejemplo de libro de texto de comportamiento indefinido (vea la entrada de Wikipedia sobre puntos de secuencia ).


Creo que las partes relevantes del estándar C99 son 6.5 Expresiones, §2

Entre el punto de secuencia anterior y el siguiente, un objeto tendrá su valor almacenado modificado a lo sumo una vez por la evaluación de una expresión. Además, el valor anterior se leerá solo para determinar el valor que se almacenará.

y 6.5.16 Operadores de asignación, §4:

El orden de evaluación de los operandos no está especificado. Si se intenta modificar el resultado de un operador de asignación o acceder a él después del siguiente punto de secuencia, el comportamiento no está definido.


El comportamiento realmente no se puede explicar porque invoca tanto el comportamiento no especificado como el indefinido , por lo que no podemos hacer predicciones generales sobre este código, aunque si lee el trabajo de Olve Maudal , como Deep C y Unspecified y Undefined, a veces puede hacerlo bien. adivina en casos muy específicos con un compilador y un entorno específicos, pero no hagas eso cerca de la producción.

De modo que pasar a un comportamiento no especificado , en el borrador de la sección 6.5 sección 6.5 c99 , el párrafo 3 dice ( énfasis mío ):

La agrupación de operadores y operandos se indica mediante la sintaxis.74) Excepto como se especifica más adelante (para la función-call (), &&, ||,?: Y operadores de coma), el orden de evaluación de las subexpresiones y el orden en Los efectos secundarios que se producen no están especificados.

Así que cuando tenemos una línea como esta:

i = i++ + ++i;

no sabemos si primero se evaluará i++ o ++i . Esto es principalmente para dar al compilador mejores opciones para la optimización .

También tenemos un comportamiento indefinido 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 6.5 párrafo 2 ( énfasis mío ):

Entre el punto de secuencia anterior y el siguiente, un objeto tendrá su valor almacenado modificado a lo sumo una vez por la evaluación de una expresión. Además, el valor anterior se leerá solo para determinar el valor que se almacenará .

cita los siguientes ejemplos de código como no definidos:

i = ++i + 1; a[i++] = i;

En todos estos ejemplos, el código está intentando modificar un objeto más de una vez en el mismo punto de secuencia, que terminará con el ; En cada uno de estos casos:

i = i++ + ++i; ^ ^ ^ i = (i++); ^ ^ u = u++ + ++u; ^ ^ ^ u = (u++); ^ ^ v = v++ + ++v; ^ ^ ^

El comportamiento no especificado se define en el borrador de la norma c99 en la sección 3.4.4 como:

uso de un valor no especificado, u otro comportamiento donde esta Norma Internacional proporciona dos o más posibilidades y no impone requisitos adicionales sobre los que se elija en ningún caso

y el comportamiento indefinido se define en la sección 3.4.3 como:

comportamiento, en el uso de un constructo de programa no portátil o erróneo o de datos erróneos, para los cuales esta Norma Internacional no impone requisitos

y nota que:

El posible comportamiento indefinido abarca desde ignorar la situación completamente con resultados impredecibles, hasta comportarse durante la traducción o la ejecución del programa de una manera documentada característica del entorno (con o sin la emisión de un mensaje de diagnóstico), hasta terminar una traducción o ejecución (con la emisión de un mensaje de diagnóstico).


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.
Así que cada declaración de la forma:

i = i++; i = i++ + ++i;

Y así violar 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 por norma.

Sin embargo, dos variables diferentes pueden incrementarse entre dos puntos de secuencia.

while(*src++ = *dst++);

Lo anterior es una práctica de codificación común al copiar / analizar cadenas.


En https://.com/questions/29505280/incrementing-array-index-in-c alguien preguntó acerca de una declaración como:

int k[] = {0,1,2,3,4,5,6,7,8,9,10}; int i = 0; int num; num = k[++i+k[++i]] + k[++i]; printf("%d", num);

que imprime 7 ... el OP esperaba que se imprima 6.

No se garantiza que los incrementos de ++i se completen antes del resto de los cálculos. De hecho, diferentes compiladores obtendrán diferentes resultados aquí. En el ejemplo que proporcionó, se ejecutaron los primeros 2 ++i , luego se leyeron los valores de k[] , luego los últimos ++i luego k[] .

num = k[i+1]+k[i+2] + k[i+3]; i += 3

Los compiladores modernos optimizarán esto muy bien. De hecho, posiblemente sea mejor que el código que escribiste originalmente (suponiendo que funcionó de la manera que esperabas).


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.

Entre el punto de secuencia anterior y el siguiente, un objeto tendrá su valor almacenado modificado a lo sumo una vez por la evaluación de una expresión. Además, el valor anterior se leerá solo para determinar el valor que se almacenará.

Los puntos de secuencia en una expresión como i=i++ son antes de i= y después de i++ .

En el documento que cité anteriormente, se explica que se puede entender que el programa está formado por cajas pequeñas, cada una de las cuales contiene las instrucciones entre 2 puntos de secuencia consecutivos. Los puntos de secuencia se definen en el anexo C del estándar, en el caso de i=i++ hay 2 puntos de secuencia que delimitan una expresión completa. Dicha expresión es sintácticamente equivalente con una entrada de expression-statement en la forma de Backus-Naur de la gramática (se proporciona una gramática en el anexo A de la Norma).

Así que el orden de las instrucciones dentro de una caja no tiene un orden claro.

i=i++

puede ser interpretado como

tmp = i i=i+1 i = tmp

o como

tmp = i i = tmp i=i+1

Debido a que todas estas formas para interpretar el código i=i++ son válidas y porque ambas generan respuestas diferentes, el comportamiento no está definido.

Por lo tanto, 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 publicadas como un libro ), es decir, here y here y here .


La mayoría de las respuestas aquí citadas del estándar C enfatizan que el comportamiento de estas construcciones no está definido. Para entender por qué el comportamiento de estas construcciones no está definido , entendamos estos términos primero a la luz del estándar C11:

Secuenciado: (5.1.2.3)

Dadas las dos evaluaciones A y B , si A está secuenciada antes de B , entonces la ejecución de A deberá preceder a la ejecución de B

Sin secuenciar

Si A no está secuenciada antes o después de B , entonces A y B tienen secuencia.

Las evaluaciones pueden ser una de dos cosas:

  • cálculos de valores , que resuelven el resultado de una expresión; y
  • Efectos secundarios , que son modificaciones de los objetos.

Punto de secuencia:

La presencia de un punto de secuencia entre la evaluación de las expresiones A y B implica que cada cálculo de valor y efecto secundario asociado con A se secuencia antes de cada cálculo de valor y efecto secundario asociado con B

Ahora llegando a la pregunta, por las expresiones como

int i = 1; i = i++;

La norma dice que:

6.5 Expresiones:

Si un efecto secundario en un objeto escalar no tiene secuencia en relación con un efecto secundario diferente en el mismo objeto escalar o un cálculo de valor utilizando el valor del mismo objeto escalar, el comportamiento no está definido . [...]

Por lo tanto, la expresión anterior invoca a UB porque dos efectos secundarios en el mismo objeto i están relacionados entre sí. Eso significa que no está secuenciado si el efecto secundario por asignación a i se realizará antes o después del efecto secundario por ++ .
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 .

Permite cambiar el nombre de i a la izquierda de la asignación be il y a la derecha de la asignación (en la expresión i++ ) be ir , entonces la expresión es como

il = ir++ // Note that suffix l and r are used for the sake of clarity. // Both il and ir represents the same object.

Un punto importante con respecto al operador de Postfix ++ es que:

solo porque ++ viene después de que la variable no significa que el incremento ocurra tarde . El incremento puede ocurrir tan pronto como le guste al compilador , siempre y cuando el compilador se asegure de que se utiliza el valor original .

Significa que la expresión il = ir++ podría evaluarse como

temp = ir; // i = 1 ir = ir + 1; // i = 2 side effect by ++ before assignment il = temp; // i = 1 result is 1

o

temp = ir; // i = 1 il = temp; // i = 1 side effect by assignment before ++ ir = ir + 1; // i = 2 result is 2

dando como resultado dos resultados diferentes 1 y 2 que dependen de la secuencia de efectos secundarios por asignación y ++ y, por lo tanto, invocan UB.


La razón es que el programa está ejecutando un comportamiento indefinido. El problema radica en el orden de evaluación, porque no hay puntos de secuencia requeridos según el estándar C ++ 98 (ninguna operación se secuencia antes o después de otra según la terminología de C ++ 11).

Sin embargo, si se apega a un compilador, encontrará el comportamiento persistente, siempre y cuando no agregue llamadas de función o punteros, lo que haría que el comportamiento sea más desordenado.

  • Entonces, primero el GCC: Usando Nuwen MinGW 15 GCC 7.1 obtendrá:

    #include<stdio.h> int main(int argc, char ** argv) { int i = 0; i = i++ + ++i; printf("%d/n", i); // 2 i = 1; i = (i++); printf("%d/n", i); //1 volatile int u = 0; u = u++ + ++u; printf("%d/n", u); // 2 u = 1; u = (u++); printf("%d/n", u); //1 register int v = 0; v = v++ + ++v; printf("%d/n", v); //2

    }

¿Cómo funciona GCC? evalúa las subexpresiones en un orden de izquierda a derecha para el lado derecho (RHS), luego asigna el valor al lado izquierdo (LHS). Así es exactamente cómo se comportan Java y C # 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: primero se evalúa el ++ c (pre-incremento), luego se usa el valor c para la operación, luego el incremento post c ++).

Según GCC C ++: Operadores

En GCC C ++, la prioridad de los operadores controla el orden en que se evalúan los operadores individuales

El código equivalente en el comportamiento definido C ++ como GCC entiende:

#include<stdio.h> int main(int argc, char ** argv) { int i = 0; //i = i++ + ++i; int r; r=i; i++; ++i; r+=i; i=r; printf("%d/n", i); // 2 i = 1; //i = (i++); r=i; i++; i=r; printf("%d/n", i); // 1 volatile int u = 0; //u = u++ + ++u; r=u; u++; ++u; r+=u; u=r; printf("%d/n", u); // 2 u = 1; //u = (u++); r=u; u++; u=r; printf("%d/n", u); // 1 register int v = 0; //v = v++ + ++v; r=v; v++; ++v; r+=v; v=r; printf("%d/n", v); //2 }

Luego vamos a Visual Studio . Visual Studio 2015, obtienes:

#include<stdio.h> int main(int argc, char ** argv) { int i = 0; i = i++ + ++i; printf("%d/n", i); // 3 i = 1; i = (i++); printf("%d/n", i); // 2 volatile int u = 0; u = u++ + ++u; printf("%d/n", u); // 3 u = 1; u = (u++); printf("%d/n", u); // 2 register int v = 0; v = v++ + ++v; printf("%d/n", v); // 3 }

¿Cómo funciona Visual Studio, toma otro enfoque, evalúa todas las expresiones de preincrementos en la primera pasada, luego usa valores de variables en las operaciones en la segunda pasada, asigna de RHS a LHS en la tercera pasada, luego en la última pasada evalúa todas Expresiones post-incremento en una pasada.

Así que el equivalente en el comportamiento definido C ++ como Visual C ++ entiende:

#include<stdio.h> int main(int argc, char ** argv) { int r; int i = 0; //i = i++ + ++i; ++i; r = i + i; i = r; i++; printf("%d/n", i); // 3 i = 1; //i = (i++); r = i; i = r; i++; printf("%d/n", i); // 2 volatile int u = 0; //u = u++ + ++u; ++u; r = u + u; u = r; u++; printf("%d/n", u); // 3 u = 1; //u = (u++); r = u; u = r; u++; printf("%d/n", u); // 2 register int v = 0; //v = v++ + ++v; ++v; r = v + v; v = r; v++; printf("%d/n", v); // 3 }

como indica la documentación de Visual Studio en Precedencia y Orden de Evaluación :

Cuando varios operadores aparecen juntos, tienen la misma prioridad y se evalúan de acuerdo con su asociatividad. Los operadores en la tabla se describen en las secciones que comienzan con Operadores de Postfix.


Otra forma de responder a esto, en lugar de atorarse 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 se pregunta, i = i++ + ++i , es bastante claro 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 estado tratando de codificar, lo que hubiera resultado en esta secuencia particular de operaciones. Y como 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 averiguar qué se supone que debe hacer.

El segundo fragmento, i = i++ , es un poco más fácil de entender. Alguien claramente está intentando incrementar i, y asigna el resultado de nuevo 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:

i = i + 1

C, por supuesto, tiene un atajo útil:

i++

Esto significa, "agregue 1 a i, y asigne el resultado de nuevo a i". Así que si construimos una mezcolanza de los dos, escribiendo

i = i++

lo que realmente estamos diciendo es "sume 1 a i, y asigne el resultado de nuevo a i, y asigne el resultado de nuevo a i". Estamos confundidos, por lo que no me molesta demasiado si el compilador también se confunde.

De manera realista, la única vez que se escriben estas locas expresiones 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 que usa ++, no lo escriba".

Solíamos pasar incontables horas en 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:


Si bien es poco probable que los compiladores y procesadores realmente lo hagan, sería legal, bajo el estándar C, que el compilador implemente "i ++" con la secuencia:

In a single operation, read `i` and lock it to prevent access until further notice Compute (1+read_value) In a single operation, unlock `i` and store the computed value

Si bien no creo que ningún procesador sea compatible con el hardware para permitir que tal cosa se realice de manera eficiente, uno puede imaginar fácilmente situaciones en las que tal comportamiento haría más fácil el código de múltiples subprocesos (por ejemplo, garantizaría que si dos subprocesos intentan realizar lo anterior) de forma simultánea, se incrementaría en dos) y no es del todo inconcebible que algún procesador futuro pueda proporcionar una característica similar.

Si el compilador escribiera i++ como se indicó anteriormente (legal según la norma) y se intercalasen las instrucciones anteriores a lo largo de la evaluación de la expresión general (también legal), y si no ocurriera, notaría que una de las otras instrucciones Si se tiene acceso a i , sería posible (y legal) que el compilador genere una secuencia de instrucciones que podría interrumpir el bloqueo. Para estar seguro, un compilador casi con certeza detectará el problema en el caso de que se use la misma variable i en ambos lugares, pero si una rutina acepta referencias a dos punteros p y q , y usa (*p) y (*q) en la expresión anterior (en lugar de usar i dos veces) no se requeriría que el compilador reconozca o evite el interbloqueo que se produciría si la dirección del mismo objeto se pasara tanto para p como para q .


Si bien la sintaxis de las expresiones como a = a++ o a++ + a++ es legal, el comportamiento de estas construcciones no está definido porque no se obedece una norma de a en C. C99 6.5p2 :

  1. Entre el punto de secuencia anterior y el siguiente, un objeto tendrá su valor almacenado modificado a lo sumo una vez por la evaluación de una expresión. [72] Además, el valor anterior se leerá solo para determinar el valor a almacenar [73]

Con la nota al pie 73 aclarando que

  1. Este párrafo representa expresiones de declaración no definidas como

    i = ++i + 1; a[i++] = i;

    mientras que permite

    i = i + 1; a[i] = i;

Los diversos puntos de secuencia se enumeran en el Anexo C de C11 (y C99 ):

  1. Los siguientes son los puntos de secuencia descritos en 5.1.2.3:

    • Entre las evaluaciones del designador de función y los argumentos reales en una llamada de función y la llamada real. (6.5.2.2).
    • Entre las evaluaciones del primer y segundo operandos de los siguientes operadores: lógico AND && (6.5.13); OR lógico || (6.5.14); coma, (6.5.17).
    • ¿Entre las evaluaciones del primer operando del condicional? : operador y cualquiera de los segundos y terceros operandos se evalúa (6.5.15).
    • El fin de un declarador completo: declaradores (6.7.6);
    • Entre la evaluación de una expresión completa y la siguiente expresión completa a evaluar. Las siguientes son expresiones completas: un inicializador que no es parte de un literal compuesto (6.7.9); la expresión en una declaración de expresión (6.8.3); la expresión de control de una declaración de selección (if o switch) (6.8.4); la expresión de control de una sentencia while o do (6.8.5); cada una de las expresiones (opcionales) de una declaración for (6.8.5.3); la expresión (opcional) en una declaración de retorno (6.8.6.4).
    • Inmediatamente antes de que una función de biblioteca regrese (7.1.4).
    • Después de las acciones asociadas con cada especificador de conversión de función de entrada / salida con formato (7.21.6, 7.29.2).
    • Inmediatamente antes e inmediatamente después de cada llamada a una función de comparación, y también entre cualquier llamada a una función de comparación y cualquier movimiento de los objetos pasados ​​como argumentos a esa llamada (7.22.5).

La redacción del mismo párrafo en C11 es:

  1. Si un efecto secundario en un objeto escalar no tiene secuencia en relación con un efecto secundario diferente en el mismo objeto escalar o un cálculo de valor utilizando el valor del mismo objeto escalar, el comportamiento no está definido. Si hay varios ordenamientos permitidos de las subexpresiones de una expresión, el comportamiento no está definido si se produce un efecto secundario no secuencial en cualquiera de los ordenamientos.84)

Puede detectar dichos errores en un programa utilizando, por ejemplo, una versión reciente de GCC con -Wall y -Werror , y luego GCC se negará directamente a compilar su programa. La siguiente es la salida de gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic plusplus.c: In function ‘main’: plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point] i = i++ + ++i; ~~^~~~~~~~~~~ plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point] plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point] i = (i++); ~~^~~~~~~ plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point] u = u++ + ++u; ~~^~~~~~~~~~~ plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point] plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point] u = (u++); ~~^~~~~~~ plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point] v = v++ + ++v; ~~^~~~~~~~~~~ plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point] cc1: all warnings being treated as errors

Lo importante es saber qué es un punto de secuencia, y qué es un punto de secuencia y qué no . Por ejemplo, el operador de coma es un punto de secuencia, por lo que

j = (i ++, ++ i);

está bien definido, e incrementará i en uno, produciendo el valor antiguo, descartando ese valor; luego en el operador de coma, resolver los efectos secundarios; y luego incremente i en uno, y el valor resultante se convierte en el valor de la expresión, es decir, esto es solo una forma artificial de escribir j = (i += 2) que es una vez más una forma "inteligente" de escribir

i += 2; j = i;

Sin embargo, la lista de argumentos de la función no es un operador de coma y no hay un punto de secuencia entre las evaluaciones de los distintos argumentos; en cambio, sus evaluaciones no tienen secuencia entre sí; así que la función de llamada

int i = 0; printf("%d %d/n", i++, ++i, i);

tiene un comportamiento indefinido porque no hay un punto de secuencia entre las evaluaciones de i++ y ++i en los argumentos de la función y, por lo tanto, el valor de i se modifica dos veces, tanto i++ como ++i , entre el punto de secuencia anterior y el siguiente.


Simplemente compile y desmonte su línea de código, si está tan inclinado a saber cómo es exactamente lo que está obteniendo.

Esto es lo que obtengo en mi máquina, junto con lo que creo que está pasando:

$ cat evil.c void evil(){ int i = 0; i+= i++ + ++i; } $ gcc evil.c -c -o evil.bin $ gdb evil.bin (gdb) disassemble evil Dump of assembler code for function evil: 0x00000000 <+0>: push %ebp 0x00000001 <+1>: mov %esp,%ebp 0x00000003 <+3>: sub $0x10,%esp 0x00000006 <+6>: movl $0x0,-0x4(%ebp) // i = 0 i = 0 0x0000000d <+13>: addl $0x1,-0x4(%ebp) // i++ i = 1 0x00000011 <+17>: mov -0x4(%ebp),%eax // j = i i = 1 j = 1 0x00000014 <+20>: add %eax,%eax // j += j i = 1 j = 2 0x00000016 <+22>: add %eax,-0x4(%ebp) // i += j i = 3 0x00000019 <+25>: addl $0x1,-0x4(%ebp) // i++ i = 4 0x0000001d <+29>: leave 0x0000001e <+30>: ret End of assembler dump.

(¿Supongo que la instrucción 0x00000014 fue algún tipo de optimización del compilador?)


Su pregunta probablemente no fue: "¿Por qué estas construcciones son un comportamiento indefinido en C?". Su pregunta probablemente fue "¿Por qué este código (uso ++) no me dio el valor que esperaba?", Y alguien marcó su pregunta como un duplicado y la envió aquí.

Esta respuesta intenta responder 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 la forma del prefijo ++xdifiere de la forma del postfijo x++. Pero es difícil pensar en estos operadores, así que para asegurarte de que lo entendiste, tal vez escribiste un pequeño programa de prueba con algo como

int x = 5; printf("%d %d %d/n", x, ++x, x++);

Pero, para su sorpresa, este programa no lo ayudó a comprender: imprimió una salida extraña, inesperada e inexplicable, lo que sugiere que tal vez ++haga algo completamente diferente, en absoluto lo que pensó que lo hizo.

O tal vez estás mirando una expresión difícil de entender como

int x = 5; x = x++ + ++x; printf("%d/n", x);

Tal vez alguien te dio ese código como un rompecabezas. Este código tampoco tiene sentido, especialmente si lo ejecutas, y si lo compilas y ejecutas bajo dos compiladores diferentes, ¡es probable que obtengas dos respuestas diferentes! ¿Que pasa con eso?¿Qué respuesta es correcta? (Y la respuesta es que ambos son, o ninguno de ellos lo es).

Como ya ha escuchado, todas estas expresiones no están definidas , 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 que 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 no esté definida? ¿Están las expresiones involucradas ++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 demasiadas cosas a la vez, cuando no estamos seguros de en qué orden sucederán las cosas, pero cuando la orden es importante para el resultado que obtenemos.

Volvamos a los dos ejemplos que he usado en esta respuesta. Cuando escribi

printf("%d %d %d/n", x, ++x, x++);

la pregunta es, antes de llamar printf, ¿el compilador calcula el valor de xprimero x++, o tal vez ++x? Pero resulta que no lo sabemos . No hay ninguna regla en C que diga que los argumentos de una función se evalúen de izquierda a derecha, de derecha a izquierda o en algún otro orden. Así que no podemos decir si el compilador hará xprimero, y luego ++x, a continuación x++, o x++entonces ++xa continuación x, o algún otro fin. Pero el orden claramente importa, porque dependiendo del orden que use el compilador, claramente obtendremos diferentes resultados impresos por printf.

¿Qué pasa con esta expresión loca?

x = x++ + ++x;

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 valor xy devolver el valor antiguo de x; (2) la ++xparte intenta agregar 1 a x, almacenar el nuevo valor xy devolver el nuevo valor de x; y (3) la x =parte trata de asignar la suma de los otros dos a x. ¿Cuál de esos tres intentos intentará "ganar"? ¿A cuál de los tres valores se asignará realmente x? De nuevo, y tal vez sorprendentemente, no hay ninguna regla en C que nos diga.

Podría 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 lo hacen. Puede que no me crea, pero por favor tome mi palabra, y lo diré nuevamente: 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 Los diferentes lugares en los que intentamos asignar un nuevo valor a algo como x, la precedencia y la asociatividad no nos dicen cuál de esos intentos ocurre primero, o último, o algo así.

Entonces, con todo ese fondo e introducción fuera del camino, si quiere asegurarse de que todos sus programas estén bien definidos, ¿qué expresiones puede escribir y cuáles no puede escribir?

Estas expresiones están bien:

y = x++; z = x++ + y++; x = x + 1; x = a[i++]; x = a[i++] + b[j++]; x[i++] = a[j++] + b[k++]; x = *p++; x = *p++ + *q++;

Estas expresiones son todas indefinidas:

x = x++; x = x++ + ++x; y = x + x++; a[i] = i++; a[i++] = i; printf("%d %d %d/n", x, ++x, x++);

Y la última pregunta es, ¿cómo se puede saber qué expresiones están bien definidas y cuáles 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 la orden

  1. Si hay una variable que se está modificando (asignando) en dos o más lugares diferentes, ¿cómo saber qué modificación ocurre primero?
  2. Si hay una variable que se está modificando en un lugar y que su valor se usa en otro lugar, ¿cómo saber si usa el valor antiguo o el valor nuevo?

Como ejemplo de # 1, en la expresión

x = x++ + ++x;

Hay tres intentos de modificar `x.

Como ejemplo de # 2, en la expresión

y = x + x++;

Ambos usamos el valor de x, y lo modificamos.

Así que 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 otra parte.