¿Es este comportamiento indefinido en C? Si no predecir la salida lógicamente
output pass-by-reference (3)
Código 1
Para el Código 1 , debido al orden de evaluación de los términos a return *a + f(a, b);
(y, a return f(a, b) + *a;
) no está especificado por el estándar y la función modifica el valor al que a
está apuntando, su código tiene un comportamiento no especificado y varias respuestas son posibles.
Como se puede ver en el furor de los comentarios, los términos "comportamiento indefinido", "comportamiento no especificado", etc., tienen significados técnicos en el estándar C, y las versiones anteriores de esta respuesta abusaron de "comportamiento indefinido" donde debería haber usado " no especificado ''.
El título de la pregunta es "¿Es este comportamiento indefinido en C?", Y la respuesta es "No; es un comportamiento no especificado, no un comportamiento no definido".
Código 2 - revisado
Para el Código 2 como fijo, la función también tiene un comportamiento no especificado: el valor de la variable estática r
se modifica mediante la llamada recursiva, por lo que los cambios en el orden de evaluación podrían cambiar el resultado.
Código 2 - revisión previa
Para el Código 2 , como se muestra originalmente con int f(static int n) { … }
, el código no compila (o, al menos, no debería). La única clase de almacenamiento permitida en la definición de un argumento para una función es el register
, por lo que la presencia de static
debería darle errores de compilación.
ISO / CEI 9899: 2011 §6.7.6.3 Declaradores de función (incluidos los prototipos) ¶2 El único especificador de clase de almacenamiento que se producirá en una declaración de parámetro es el
register
.
Compilación con GCC 6.3.0 en macOS Sierra 10.12.2, como este (nota, no se requieren advertencias adicionales):
$ gcc -O ub17.c -o ub17
ub17.c:3:27: error: storage class specified for parameter ‘n’
int foo(static int n)
^
No; no se compila en absoluto como se muestra, al menos no para mí usando una versión moderna de GCC.
Sin embargo, suponiendo que sea fijo, la función también tiene un comportamiento no especificado no definido : el valor de la variable estática r
se modifica por la llamada recursiva, por lo que los cambios en el orden de evaluación podrían cambiar el resultado.
Código 1
#include <stdio.h>
int f(int *a, int b)
{
b = b - 1;
if(b == 0) return 1;
else {
*a = *a+1;
return *a + f(a, b);
}
}
int main() {
int X = 5;
printf("%d/n",f(&X, X));
}
Considere este código C. La pregunta aquí es predecir la salida. Lógicamente, obtengo 31 como salida. ( Salida en máquina )
Cuando cambio la declaración de devolución a
return f(a, b) + *a;
Logicamente obtengo 37. ( Salida en la máquina )
Uno de mis amigos dijo que mientras calculaba la declaración de devolución en
return *a + f(a, b);
calculamos el valor de la profundidad del árbol en curso, es decir, * se llama a un primer cálculo y luego se llama f(a, b)
, mientras que en
return f(a,b) + *a;
Se resuelve mientras se devuelve, es decir f(a, b)
primero se calcula f(a, b)
luego se llama a *a
.
Con este enfoque, intenté predecir el resultado del siguiente código:
Código 2
#include <stdio.h>
int foo(int n)
{
static int r;
if(n <= 1)
return 1;
r = n + r;
return r + foo(n - 2);
}
int main () {
printf("value : %d",foo(5));
}
Para return(r+foo(n-2));
Obtengo 14 como salida lógicamente ( salida en la máquina )
Para return(foo(n-2)+r);
Obtengo 17 como salida. ( Salida en la máquina )
Sin embargo, cuando ejecuto el código en mi sistema, obtengo 17 en ambos casos.
Mis preguntas:
- ¿Es correcto el enfoque dado por mi amigo?
- Si es así, ¿por qué obtengo el mismo resultado en el Código 2 cuando corro en una máquina?
- Si no, ¿cuál es la forma correcta de interpretar el Código 1 y el Código 2?
- ¿Hay algún comportamiento indefinido porque C no admite pasar por referencia? Como se usa en Code 1 tough, ¿se puede implementar usando punteros?
En pocas palabras, simplemente quería saber la forma correcta de predecir el resultado en los 4 casos mencionados anteriormente.
C estándar establece que
6.5.2.2/10 Llamadas de función:
Hay un punto de secuencia después de las evaluaciones del designador de la función y los argumentos reales, pero antes de la llamada real. Cada evaluación en la función de llamada (incluidas otras llamadas de función) que no está secuenciada específicamente antes o después de la ejecución del cuerpo de la función llamada está secuenciada de forma indeterminada 1 con respecto a la ejecución de la función llamada. 94)
Y la nota de pie 86 (sección 6.5 / 3) dice:
En una expresión que se evalúa más de una vez durante la ejecución de un programa, no es necesario que las evaluaciones de sus subexpresiones sin secuencia y de forma indeterminada se realicen de forma coherente en diferentes evaluaciones .
En expresiones return f(a,b) + *a;
y return *a + f(a,b);
la evaluación de la subexpresión *a
está indeterminadamente secuenciada. En este caso se pueden ver diferentes resultados para el mismo programa.
Tenga en cuenta que el efecto secundario en a
se secuencia en las expresiones anteriores, pero no se especifica en qué orden.
1. Las evaluaciones A y B se secuencian de forma indeterminada cuando A se secuencia antes o después de B, pero no se especifica cuál. (C11- 5.1.2.3/3)
Me centraré en la definición del primer ejemplo.
El primer ejemplo se define con un comportamiento no especificado. Esto significa que hay varios resultados posibles, pero el comportamiento no es indefinido. (Y si el código puede manejar esos resultados, se define el comportamiento).
Un ejemplo trivial de comportamiento no especificado, es:
int a = 0;
int c = a + a;
No se especifica si la izquierda o la derecha a se evalúan primero, ya que no tienen secuencia . El operador +
no especifica ningún punto de secuencia 1 . Hay dos posibles ordenamientos, ya sea a la izquierda se evalúa primero y luego a la derecha, o viceversa. Dado que ninguno de los dos lados está modificado 2 , el comportamiento está definido.
Si hubiera dejado o modificado un derecho sin un punto de secuencia, es decir, sin secuencia, el comportamiento sería indefinido 2 :
int a = 0;
int c = ++a + a;
Si hubiera dejado una o una derecha modificada con un punto de secuencia en el medio, entonces el lado izquierdo y el derecho se secuenciarían de forma indeterminada 3 . Esto significa que están secuenciados, pero no se especifica cuál se evalúa primero. El comportamiento estaría definido. Tenga en cuenta que el operador de coma introduce un punto de secuencia 4 :
int a = 0;
int c = a + ((void)0,++a,0);
Hay dos posibles ordenamientos.
Si el lado izquierdo se evalúa primero, entonces se evalúa a 0. Luego se evalúa el lado derecho. Primero se evalúa (vacío) 0 seguido de un punto de secuencia. Luego se incrementa a, seguido de un punto de secuencia. Luego 0 se evalúa como 0 y se agrega al lado izquierdo. El resultado es 0.
Si se evalúa primero el lado derecho, se evalúa (vacío) 0 seguido de un punto de secuencia. Luego se incrementa a, seguido de un punto de secuencia. Luego, 0 se evalúa como 0. Luego se evalúa el lado izquierdo y a se evalúa como 1. El resultado es 1.
Tu ejemplo cae en la última categoría, ya que los operandos están secuenciados indefinidamente . La llamada de función tiene el mismo propósito 5 que los operadores de coma en el ejemplo anterior. Tu ejemplo es complicado, entonces usaré el mío, que también se aplica al tuyo. La única diferencia es que hay muchos más resultados posibles en tu ejemplo que en el mío, pero el razonamiento es el mismo:
void Function( int* a)
{
++(*a);
return 0;
}
int a = 0;
int c = a + Function( &a );
assert( c == 0 || c == 1 );
Hay dos posibles ordenamientos.
Si primero se evalúa el lado izquierdo, se evalúa a 0. Luego se evalúa el lado derecho, hay un punto de secuencia y se llama a la función. Entonces se incrementa a, seguido por otro punto de secuencia introducido por el final de la expresión completa 6 , cuyo final se indica mediante el punto y coma. Luego se devuelve 0 y se agrega a 0. El resultado es 0.
Si el lado derecho se evalúa primero, hay un punto de secuencia y se llama a la función. Entonces a se incrementa, seguido por otro punto de secuencia introducido por el final de la expresión completa. Entonces 0 es devuelto. Luego se evalúa el lado izquierdo, y se evalúa a 1 y se agrega a 0. El resultado es 1.
(Citado de: ISO / IEC 9899: 201x)
1 (6.5 Expresiones 3)
Excepto como se especifica más adelante, los efectos secundarios y los cálculos de valor de las subexpresiones no se han secuenciado.
2 (6.5 Expresiones 2)
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.
3 (5.1.2.3 Ejecución del programa)
Las evaluaciones A y B se secuencian de forma indeterminada cuando A se secuencia antes o después de B, pero no se especifica cuál.
4 (6.5.17 operador de coma 2)
El operando izquierdo de un operador de coma se evalúa como una expresión vacía; hay un punto de secuencia entre su evaluación y la del operando correcto.
5 (6.5.2.2 llamadas a funciones 10)
Hay un punto de secuencia después de las evaluaciones del designador de la función y los argumentos reales, pero antes de la llamada real.
6 (6.8 Declaraciones y bloques 4)
Hay un punto de secuencia entre la evaluación de una expresión completa y la evaluación de la siguiente expresión completa que se evaluará.