2008-09-19 22 views
59

Un ejemplo de comportamiento no especificado en el lenguaje C es el orden de evaluación de argumentos para una función. Puede ser de izquierda a derecha o de derecha a izquierda, simplemente no lo sabes. Esto afectaría cómo se evalúa foo(c++, c) o foo(++c, c).¿Cuál es el comportamiento indefinido/no especificado común para C con el que te encuentras?

¿Qué otro comportamiento no especificado existe que pueda sorprender al programador desprevenido?

+2

'foo (C++, c)' y 'foo (++ c, c)' son ** comportamiento indefinido **, que prevalece por completo sin especificar. –

Respuesta

65

Una pregunta de abogado de idiomas. Hmkay.

Mi top3 personal:

  1. violar la estricta regla de aliasing
  2. violar la estricta regla de aliasing
  3. violar la estricta regla de aliasing

    :-)

Editar Aquí hay un pequeño ejemplo que lo hace mal dos veces:

(asumir 32 enteros bits y Little Endian)

float funky_float_abs (float a) 
{ 
    unsigned int temp = *(unsigned int *)&a; 
    temp &= 0x7fffffff; 
    return *(float *)&temp; 
} 

Ese código intenta obtener el valor absoluto de un flotador a poco, haciendo girar con el signo bit directamente en la representación de un flotador.

Sin embargo, el resultado de crear un puntero a un objeto mediante conversión de un tipo a otro no es válido C. El compilador puede suponer que los punteros a diferentes tipos no apuntan al mismo bloque de memoria. Esto es cierto para todo tipo de punteros excepto void * y char * (sign-ness no importa).

En el caso anterior lo hago dos veces. Una vez para obtener un alias int para el float a, y una vez para convertir el valor a float.

Existen tres formas válidas de hacer lo mismo.

Use un puntero de char o void durante el lanzamiento. Estos siempre se parecen a cualquier cosa, por lo que están seguros.

float funky_float_abs (float a) 
{ 
    float temp_float = a; 
    // valid, because it's a char pointer. These are special. 
    unsigned char * temp = (unsigned char *)&temp_float; 
    temp[3] &= 0x7f; 
    return temp_float; 
} 

Usar memcopy. Memcpy toma punteros vacíos, por lo que también forzará el aliasing.

float funky_float_abs (float a) 
{ 
    int i; 
    float result; 
    memcpy (&i, &a, sizeof (int)); 
    i &= 0x7fffffff; 
    memcpy (&result, &i, sizeof (int)); 
    return result; 
} 

La tercera manera válida: usar uniones. Este es explícitamente no indefinido desde C99:

float funky_float_abs (float a) 
{ 
    union 
    { 
    unsigned int i; 
    float f; 
    } cast_helper; 

    cast_helper.f = a; 
    cast_helper.i &= 0x7fffffff; 
    return cast_helper.f; 
} 
+0

Esto suena interesante ... ¿puedes expandirte? – Benoit

+0

aehm. Mencioné que supongo que son 32 bit y little endian. Btw: el uso de la unión sigue siendo un comportamiento indefinido no debido a la representación de bits IEEE sino simplemente porque usted (en teoría) no puede escribir en el campo f y leer desde el campo i. –

+0

onebyone, es un comportamiento indefinido incluso si la implementación usa ieee. el punto es que lee de un miembro diferente al que se escribió por última vez. –

1

¡Asegúrese de inicializar siempre sus variables antes de usarlas! Cuando recién comencé con C, eso me causó muchos dolores de cabeza.

19

Mi favorita es la siguiente:

// what does this do? 
x = x++; 

Para responder a algunos comentarios, es un comportamiento no definido de acuerdo a la norma. Al ver esto, el compilador puede hacer cualquier cosa e incluso formatear su disco duro. Ver por ejemplo this comment here. El punto no es que pueda ver que hay una posible expectativa razonable de algún comportamiento. Debido al estándar C++ y la forma en que se definen los puntos de secuencia, esta línea de código es en realidad un comportamiento indefinido.

Por ejemplo, si tuviéramos x = 1 antes de la línea anterior, ¿cuál sería el resultado válido después? Alguien comentó que debería ser

x se incrementa en 1

por lo que deberíamos ver x == 2 después. Sin embargo, esto no es cierto en realidad, encontrará algunos compiladores que tienen x == 1 después, o tal vez incluso x == 3. Tendría que observar detenidamente el ensamblaje generado para ver por qué podría ser así, pero las diferencias se deben al problema subyacente. Esencialmente, creo que esto se debe a que el compilador puede evaluar las dos declaraciones de asignaciones en el orden que quiera, por lo que primero podría hacer el x++ o el x =.

+0

X se incrementa en 1. Usted asignó x a sí mismo y luego lo incrementó. es equivalente a x ++; –

+0

esto hace x = x; x + = 1; así que sí, como dice Charles Graham. No llamaría a esto –

+14

no especificado. Modificar una variable más de una vez entre dos puntos de secuencia se establece explícitamente como comportamiento indefinido tanto en C como en C++ estándar. – KTC

0

Usando las versiones de macro de funciones como "max" o "isupper". Las macros evalúan sus argumentos dos veces, por lo que obtendrá efectos secundarios inesperados cuando llame a max (++ i, j) o isupper (* p ++)

Lo anterior es para C estándar. En C++ estos problemas han desaparecido en gran medida . La función máxima ahora es una función de plantilla.

+0

onebyone, ya sea UB o no, depende de la implementación de esos. si es a> b? a: b; entonces no lo es pero si sucede que usa a o b entre dos puntos de secuencia más de una vez, entonces es UB (con a o b siendo ++ i) –

-1

olvidándose de agregar static float foo(); en el archivo de encabezado, solo para obtener excepciones de coma flotante cuando devuelve 0.0f;

+1

¿Por qué es así? ¿Podrías elaborar? – rustyx

+0

¿Por qué declararía una función 'estática' en un archivo de encabezado? – ruakh

20

Dividir algo por un puntero a algo. Simplemente no se compilará por alguna razón ... :-)

result = x/*y; 
+0

resultado = x/(* y) debería funcionar. –

+1

Jaja bueno, lo estoy escribiendo :-) – Drealmer

30

Mi favorito comportamiento indefinido es que si un archivo de origen no vacío no termina en un salto de línea, el comportamiento no está definido.

Sospecho que es cierto, sin embargo, que ningún compilador que alguna vez haya visto ha tratado un archivo fuente de forma diferente según haya terminado o no la nueva línea, salvo para emitir una advertencia. Por lo tanto, no es realmente algo que sorprenda a los programadores que desconocen, aparte de que podrían sorprenderse con la advertencia.

Así, por problemas de portabilidad originales (que en su mayoría son dependientes de la implementación en lugar de no especificada o no está definida, pero creo que cae en el espíritu de la pregunta):

  • carbón no es necesariamente (ONU) firmaron.
  • int puede ser de cualquier tamaño desde 16 bits.
  • flotantes no tienen necesariamente formato IEEE o son conformes.
  • tipos enteros no son necesariamente complemento a dos, y el desbordamiento aritmético entero causa un comportamiento indefinido (el hardware moderno no se bloquea, pero algunas optimizaciones del compilador darán como resultado un comportamiento diferente del envolvente aunque eso sea lo que hace el hardware. Por ejemplo, if (x+1 < x) optimizado como siempre falso cuando x tiene tipo firmado: vea la opción -fstrict-overflow en GCC).
  • "/", "." y ".." en un #include no tienen un significado definido y pueden ser tratados de forma diferente por diferentes compiladores (esto realmente varía, y si sale mal arruinará su día).

los realmente graves que pueden ser sorprendentes, incluso en la plataforma que desarrolló sucesivamente, porque el comportamiento es sólo parcialmente indefinido/no especificado:

  • rosca de POSIX y el modelo de memoria ANSI. El acceso concurrente a la memoria no está tan bien definido como los principiantes piensan. volátil no hace lo que los novatos piensan. Los accesos a la memoria no están tan bien definidos como los principiantes piensan. Los accesos pueden llevarse a a través de barreras de memoria en ciertas direcciones. La coherencia del caché de memoria no es necesaria.

  • El código de perfil no es tan fácil como crees. Si su bucle de prueba no tiene ningún efecto, el compilador puede eliminar una parte o la totalidad. en línea no tiene un efecto definido.

Y, como creo que Nils menciona de pasada:

  • VIOLAR LA REGLA ALIASING estricto.
+0

Steve - Encontré exactamente lo que describiste (el problema final de la nueva línea) a principios de los años 90 con el compilador Microtec para la familia 68K. Pensé que la herramienta tenía errores, pero acabo de agregar la nueva línea "para evitar la estúpida herramienta". A diferencia de mi compañero de trabajo confiado (ver mi otro comentario sobre este tema), no estaba tan seguro de que escribiría un informe de defectos ... bueno, no lo hice. – Dan

+1

El desbordamiento de enteros con signo no definido no es solo pedantería; al menos GCC aplica optimizaciones en el supuesto de que nunca suceda, como 'if (a + 1> a)' siempre pasando y nunca detectando wraparound. – BCoates

+0

@BCoates: no tengo problemas con los desbordamientos de enteros que producen valores parcialmente indeterminados, lo que sería una semántica suficiente para justificar la optimización de GCC en el caso indicado. Desafortunadamente, algunos escritores de compiladores parecen pensar que el desbordamiento de enteros debería negar las leyes del tiempo y la causalidad (tiempo que quizás podría vivir, si el código se vuelve a secuenciar en el supuesto de que no se desbordará, la negación de la causalidad debería considerarse como locura, por desgracia, no todos están de acuerdo.) – supercat

7

Un compilador no tiene que decirle que está llamando a una función con el número incorrecto de parámetros/tipos de parámetros incorrectos si el prototipo de función no está disponible.

+0

Sí. Sin embargo, los compiladores benévolos generalmente lo ayudarán con una advertencia ... – sleske

+0

A partir de C99, llamar a una función sin declaración visible requiere un diagnóstico. Esa declaración no * tiene * que ser un prototipo (es decir, una declaración que especifica los tipos de los parámetros), pero siempre * debe * ser. (Las funciones variables como 'printf' aún pueden ser problemáticas.) –

2

La EE aquí acaba de descubrir que a >> - 2 está un poco cargado.

Asentí y les dije que no era natural.

9

Otro problema que encontré (que está definido, pero definitivamente inesperado).

char es malvado.

  • firmadas o no, dependiendo de lo que siente el compilador
  • no mandato como 8 bits
+3

Bueno, no es malvado si lo usa para lo que está destinado, es decir, para * caracteres * ... – sleske

+2

En realidad, hay tres tipos de char * diferentes : 'char',' unsigned char' y 'signed char'. Son tipos explícitamente distintos. – Lstor

+0

Lamento ser franco, pero esa respuesta es estúpida.Usted * debe * usar (punteros o matrices de texto simple) 'char' cuando se trata de cadenas. Muchas funciones de biblioteca estándar (como todas las funciones str *()) toman punteros para char y darles cualquier otra cosa requiere moldes feos. – Jens

4

Los desarrolladores clang colgado algunas great examples hace un tiempo, en un post todos los programadores C debe leer. Algunos interesantes no mencionados anteriormente:

  • Desbordamiento de enteros con signo - no, no está bien para envolver una variable con signo pasado su máximo.
  • Desreferenciando un puntero NULL: sí, esto no está definido y podría ignorarse, consulte la parte 2 del enlace.
7

No puedo contar la cantidad de veces que he corregido los especificadores de formato printf para que coincidan con su argumento. Cualquier discrepancia es un comportamiento indefinido.

  • No, no se debe pasar un int (o long) a %x - Se requiere un unsigned int
  • No, usted no debe superar un unsigned int a %d - un int se requiere
  • No, debe no pases por size_t a %u o %d - Utilización %zu
  • no, no se debe imprimir un puntero con %d o %x - utilizar %p y fundido a una void *
+2

El estándar implica (en una nota al pie no normativa) que pasar un 'int' a'% x', o un 'unsigned int' a'% d', está bien siempre que el valor esté dentro del rango de ambos tipos. Aún así, prefiero evitarlo. –

5

He visto una gran cantidad de programadores con poca experiencia mordidos por las constantes de caracteres múltiples.

Este:

"x" 

es una cadena literal (que es de tipo char[2] y decae a char* en la mayoría de contextos).

Este:

'x' 

es una constante de carácter ordinario (que, por razones históricas, es de tipo int).

Este:

'xy' 

es también un carácter perfectamente legal constante, pero su valor (que todavía es de tipo int) es definido por la implementación. Es una función de lenguaje casi inútil que sirve principalmente para causar confusión.

+0

Fue útil al escribir C en el Macintosh, que frecuentemente usaba un entero de 32 bits para contener tipos de archivos de cuatro caracteres, firmas de aplicaciones, etc., aunque los trigraphs preferirían fastidiar '' ???? ''. – supercat

+0

Esto es especialmente peligroso con funciones sobrecargadas que toman 'char *' y 'char'. He visto a mucha gente mordida por esto ([ejemplo] (http://stackoverflow.com/questions/39190756/boost-asio-custom-http-server-reading-http-post-requests/39190972)) – rustyx

+1

The question es sobre C, no C++. No hay funciones sobrecargadas –

Cuestiones relacionadas