Ya sea en C o C ++, creo que este programa ilegal, cuyo comportamiento según el estándar C o C ++ no está definido, es interesante:
#include <stdio.h>
int foo() {
int a;
const int b = a;
a = 555;
return b;
}
void bar() {
int x = 123;
int y = 456;
}
int main() {
bar();
const int n1 = foo();
const int n2 = foo();
const int n3 = foo();
printf("%d %d %d\n", n1, n2, n3);
return 0;
}
Salida en mi máquina (después de la compilación sin optimización):
123 555 555
Creo que este programa ilegal es interesante porque ilustra la mecánica de la pila, porque la razón por la que uno usa C o C ++ (en lugar de, por ejemplo, Java) es para programar cerca del hardware, cerca de la mecánica de la pila y similares.
Sin embargo, en StackOverflow, cuando el código de un interlocutor lee inadvertidamente desde el almacenamiento no inicializado, las respuestas con mayor número de votos siempre citan el estándar C o C ++ (especialmente C ++) en el sentido de que el comportamiento no está definido. Esto es cierto, por supuesto, en lo que respecta al estándar, el comportamiento es indefinido, pero es curioso que las respuestas alternativas intenten, desde una perspectiva de hardware o mecánica de pila, investigar por qué un comportamiento indefinido específico (como el salida anterior) podría haber ocurrido, son raros y tienden a ser ignorados.
Incluso recuerdo una respuesta que sugería que un comportamiento indefinido podría incluir reformatear mi disco duro. Sin embargo, no me preocupé demasiado por eso antes de ejecutar el programa anterior.
Mi pregunta es esta: ¿Por qué es más importante enseñar a los lectores simplemente que el comportamiento no está definido en C o C ++, que comprender el comportamiento indefinido? Quiero decir, si el lector entendiera el comportamiento indefinido, ¿no sería más probable que lo evite?
Resulta que mi educación es en ingeniería eléctrica, y trabajo como ingeniero de construcción de edificios, y la última vez que tuve un trabajo como programador en sí fue en 1994, por lo que tengo curiosidad por comprender la perspectiva de los usuarios con más convencional, más antecedentes recientes de desarrollo de software.
fuente
Respuestas:
El análisis de valor de Frama-C, un analizador estático cuyo objetivo es encontrar todos los comportamientos indefinidos en un programa en C, considera que la asignación
const int b = a;
está bien. Esta es una decisión de diseño deliberada para permitirmemcpy()
(típicamente implementado como un bucle sobreunsigned char
elementos de una matriz virtual, y que el estándar C posiblemente permita volver a implementar como tal) para copiar unstruct
(que puede tener relleno y miembros no inicializados) en otro.La "excepción" es solo para
lvalue = lvalue;
tareas sin una conversión intermedia, es decir, para una tarea que equivale a una copia de una porción de memoria para una ubicación de memoria a otra.Yo (como uno de los autores del análisis de valor de Frama-C) discutí esto con Xavier Leroy en un momento en que él mismo se preguntaba sobre la definición para elegir en el compilador de C verificado CompCert, por lo que puede haber terminado usando la misma definición. En mi opinión, es más limpio que lo que el estándar C intenta hacer con valores indeterminados que pueden ser representaciones de trampas, y el tipo
unsigned char
que se garantiza que no tiene representaciones de trampas, pero CompCert y Frama-C asumen objetivos relativamente no exóticos, y tal vez el comité de estandarización estaba tratando de acomodar plataformas donde la lectura de un no inicializadoint
realmente puede abortar el programa.Volviendo
b
, o que pasan1
,n2
on3
queprintf
, al final, al menos, puede ser considerado como un comportamiento indefinido, porque la copia de un trozo sin inicializar la memoria no por lo que es inicializado. Con una versión más antigua de Frama-C:Y en una versión más antigua de CompCert, después de modificaciones menores para que el programa sea aceptable para él:
fuente
El comportamiento indefinido en última instancia significa que el comportamiento no es determinista. Los programadores que no son conscientes de que están escribiendo código no determinista son
simplemente malosprogramadores ignorantes. Este sitio tiene como objetivo hacer que los programadores sean mejores (y menos ignorantes).Escribir un programa correcto frente a un comportamiento no determinista no es imposible. Sin embargo, es un entorno de programación especializado y requiere un tipo diferente de disciplina de programación.
Incluso en su ejemplo, si el programa recibe una señal externa elevada, los valores en la "pila" pueden cambiar de tal manera que no obtenga los valores esperados. Además, si la máquina tiene valores de trampa, la lectura de valores aleatorios puede causar que ocurra algo extraño.
fuente
Debido a que el comportamiento específico puede no ser repetible, incluso de ejecución en ejecución sin reconstrucción.
Perseguir exactamente lo que sucedió puede ser un ejercicio académico útil para comprender mejor las peculiaridades de su plataforma en particular, pero desde una perspectiva de codificación , la única lección relevante es "no haga eso". Una expresión como
a++ * a++
es un error de codificación, punto final. Eso es realmente todo lo que alguien necesita saber.fuente
"Comportamiento indefinido" es la abreviatura de "Este comportamiento no es determinista; no solo probablemente se comportará de manera diferente en diferentes compiladores o plataformas de hardware, sino que incluso puede comportarse de manera diferente en diferentes versiones del mismo compilador".
La mayoría de los programadores considerarían esto como una característica indeseable, especialmente porque C y C ++ son lenguajes basados en estándares ; es decir, los usa, en parte, porque la especificación del lenguaje garantiza ciertas formas sobre cómo se comportará el lenguaje, si está utilizando un compilador compatible con los estándares.
Como con la mayoría de las cosas en la programación, debe sopesar las ventajas y desventajas. Si el beneficio de alguna operación que es UB excede la dificultad de lograr que se comporte de manera estable y independiente de la plataforma, entonces, por todos los medios, use el comportamiento indefinido. La mayoría de los programadores pensarán que no vale la pena, la mayoría de las veces.
El remedio para cualquier comportamiento indefinido es examinar el comportamiento que realmente obtienes, dada una plataforma y un compilador en particular. Ese tipo de examen no es uno que un programador experto pueda explorar por usted en un entorno de preguntas y respuestas.
fuente
(-1)<<1
qué C89 definió como -2 en plataformas que usan dos complementos sin relleno ...Si la documentación de un compilador en particular dice qué hará cuando el código haga algo que el estándar considera "Comportamiento indefinido", entonces el código que se basa en ese comportamiento funcionará correctamente cuando se compila con ese compilador , pero puede comportarse de manera arbitraria cuando compilado usando algún otro compilador cuya documentación no especifica el comportamiento.
Si la documentación para un compilador no especifica cómo manejará un "comportamiento indefinido" en particular, el hecho de que el comportamiento de un programa parezca obedecer ciertas reglas no dice nada acerca de cómo se comportarán los programas similares. Cualquier variedad de factores puede hacer que un compilador emita código que maneja situaciones inesperadas de manera diferente, a veces de una manera aparentemente extraña.
Considere, por ejemplo, en una máquina donde
int
hay un número entero de 32 bits:Si
size1
ysize2
ambos eran iguales a 46341 (su producto es 2147488281) uno podría esperar que la función devuelva 3, pero un compilador podría omitir legítimamente la primera prueba por completo; o el producto sería lo suficientemente pequeño como para que la condición fuera falsa, o la próxima multiplicación se desbordaría y aliviaría al compilador de cualquier requisito de hacer o haber hecho algo. Si bien este comportamiento puede parecer extraño, algunos autores de compiladores parecen estar orgullosos de las habilidades de sus compiladores para eliminar tales pruebas "innecesarias". Algunas personas pueden esperar que un desbordamiento en la segunda multiplicación, en el peor de los casos, haga que todos los bits de ese producto en particular se corrompan arbitrariamente; de hecho, sin embargo,fuente
int
es un entero de 32 bits, los valores de tipouint16_t
se promocionaránint
antes de cualquier cálculo que los involucre. Una regla que generalmente estaría bien si las implementaciones solo trataran la aritmética con signo como diferente de sin signo en los casos en que tendrían comportamientos definidos diferentes.unsigned
y tienen un rango de valores que se ajustarán completamente dentro de esoint
, son promovidos a firmadosint
.