Doctorado en lenguaje C
Consulté el artículo descriptivo del Dr. Qiu Zongyan sobre este tema, que es muy fácil de entender.
No puedes publicar la URL, sólo el contenido:
A menudo puedes ver la siguiente pregunta en algunos grupos de discusión: "¿Quién sabe qué valor n está dado por la siguiente declaración C? " m = 1; n = m m ;
Recientemente, un amigo desconocido me envió un correo electrónico y me preguntó por qué en cierto sistema C, la siguiente expresión imprimía dos 4 en lugar de 4 y 5:
a = 4; cout lt lta lt;
C no es una regla
Para entender esto, una pregunta que debe entenderse es: si en una variable se modifica en algún lugar del programa (mediante asignación, operación de incremento/disminución, etc.) ¿Cuándo se puede obtener el nuevo valor de la variable? Algunas personas pueden decir: "¡Cuál es el problema! Modifiqué la variable y luego tomé el valor de esta variable y, por supuesto, obtuve el valor modificado. De hecho, no es tan simple".
El lenguaje C/C es un lenguaje basado en expresiones y todos los cálculos (incluidas las asignaciones) se completan en expresiones. "x = 1;" Es decir, la expresión "x = 1" va seguida de un punto y coma, que indica el final de la declaración. Para comprender el significado del programa, primero debemos comprender el significado de la expresión, es decir, el proceso de cálculo determinado por la expresión 2) su impacto en el medio ambiente (el medio ambiente puede considerarse como todas las variables disponibles en ese momento); . Si una expresión (o subexpresión) solo evalúa un valor sin cambiar el entorno, decimos que es referencialmente transparente y la expresión no tiene ningún efecto en otros cálculos (no cambia el entorno del cálculo). Por supuesto, su valor puede verse afectado por otros cálculos). Se dice que una expresión tiene efectos secundarios (porque hace algo extra) si no sólo evalúa un valor sino que también modifica el entorno. A es una expresión con efectos secundarios. Estas afirmaciones también se aplican a problemas similares en otros idiomas.
Ahora la pregunta es: si una expresión (parte) en un programa C/C tiene efectos secundarios, ¿cuándo se reflejará realmente este efecto secundario en su uso? Para aclarar el problema, supongamos que hay un fragmento de código "...a[i]...a[j]..." en el programa, y supongamos que los valores de I y J son exactamente iguales en ese momento (A [I] y A[j] solo se refieren al mismo elemento de la matriz, suponiendo que a[i] de hecho se calcula antes de a[j], suponiendo que no hay otras acciones para modificar a); [i]. Bajo estos supuestos, ¿puede la modificación de a[i] reflejarse en la evaluación de a[j]? Nota: Debido a que I y J no se pueden determinar estáticamente, en el código de destino, el acceso a estos dos elementos de la matriz (acceso a la memoria) debe completarse mediante dos códigos independientes. Los cálculos de las computadoras modernas se realizan en registros. La pregunta ahora es: ¿se ha guardado en la memoria (desde un registro) el valor actualizado de a[i] antes de ejecutar el código que toma el valor de a[j]? La respuesta a esta pregunta es clara si conocemos las reglas del lenguaje en este ámbito.
Los lenguajes de programación a menudo especifican el último momento de ejecución (llamado punto de secuencia, punto de secuencia o punto de ejecución) cuando se realiza una modificación de variable. Hay una serie de puntos de secuencia (momentos) en la ejecución del programa. El lenguaje garantiza que una vez que se ejecuta un punto de secuencia, todas las modificaciones (efectos secundarios) que ocurrieron antes de este punto deben realizarse (deben reflejarse en accesos posteriores al mismo almacenamiento). ubicación). En esta Todas las modificaciones después de hacer clic aún no se han producido. No hay garantías entre puntos consecutivos. El concepto de puntos de secuencia es especialmente importante para lenguajes como C/C que permiten que las expresiones tengan efectos secundarios.
La respuesta a la pregunta anterior ahora es clara: si hay un punto secuencial entre a[i] y a[j], entonces se garantiza que a[j] obtendrá el valor modificado; no hay garantía.
La definición del lenguaje C/C (Language Reference Manual) define claramente el concepto de puntos de secuencia. Los puntos de secuencia se encuentran:
1. Al final de cada expresión completa. Las expresiones completas incluyen expresiones de inicialización de variables, declaraciones de expresión, expresiones de declaración de retorno y expresiones de control para condiciones, bucles y declaraciones de cambio (hay tres expresiones de control al comienzo de for);
2 operadores amp; , ||, ?: después del cálculo y el primer operando del operador de coma;
3. Todos los parámetros reales y expresiones de nombre de función en la llamada de función (la función a llamar también se puede describir mediante una expresión). después de completar la evaluación (antes de ingresar al cuerpo de la función).
Suponiendo que los tiempos ti y ti 1 son dos puntos de secuencia, a partir de ti 1, cualquier sistema C/C (VC, BC, etc. son todos sistemas C/C) debe tener en cuenta todos los efectos secundarios. después de ti. Por supuesto, no tienen que esperar hasta el momento ti 1, pueden optar por implementar el efecto secundario en cualquier momento entre los períodos [t, ti 1], porque el lenguaje C/C permite estas opciones.
En la discusión anterior, se supuso que a[i] se completaba antes que a[i]. Si a [i] se realiza primero en el fragmento de programa también está relacionado con el proceso de cálculo determinado por su expresión. Todos estamos familiarizados con las regulaciones sobre prioridad, combinación y paréntesis en el lenguaje C/C, pero a menudo se ignora el orden de cálculo cuando aparecen múltiples operandos. Mire el siguiente ejemplo:
(a b) * (c d) fun(a, b, a 5)
¿Cuál de los dos operandos de "*" se cuenta primero? ¿En qué orden se calculan la diversión y sus tres parámetros? Para la primera expresión, cualquier orden de evaluación no importa porque las subexpresiones internas son referencialmente transparentes. En el segundo ejemplo, la expresión del parámetro real tiene efectos secundarios, por lo que el orden de evaluación es importante. Algunos lenguajes especifican claramente el orden de cálculo de los operandos (Java especifica de izquierda a derecha), mientras que C/C no especifica deliberadamente el orden de cálculo de dos objetos en la mayoría de las operaciones binarias (excepto amp, ||| y,). y no se especifican los parámetros de la función ni el orden en el que se evalúan las funciones de ajuste. Al calcular la segunda expresión, fun, a, B, a 5 se calculan en un orden determinado, luego se ingresa el punto de secuencia y luego se ingresa la ejecución de la función.
Muchos libros se equivocan en estos temas (incluidos algunos muy populares). Por ejemplo, C/C calcula primero la izquierda (o derecha), o un determinado sistema C/C calcula primero un determinado lado. ¡Estas declaraciones están todas equivocadas! Los sistemas C/C siempre pueden evaluar primero el lado izquierdo o el lado derecho, o dentro de la misma expresión, a veces se evalúa primero el lado izquierdo y a veces el lado derecho, o a veces se evalúa primero el lado izquierdo y a veces se evalúa el lado derecho primero. Diferentes sistemas pueden usar diferentes órdenes (porque todos cumplen con los estándares del idioma); diferentes versiones del mismo sistema pueden usar diferentes métodos en diferentes ubicaciones bajo diferentes métodos de optimización; Porque estas prácticas cumplen con las especificaciones del idioma. Aquí también debemos prestar atención a la cuestión del orden: incluso si la expresión de un lado se calcula primero, sus efectos secundarios no necesariamente se reflejarán en la memoria, por lo que no tendrá ningún impacto en el cálculo del otro lado.
Volvamos al ejemplo anterior: "¿Quién sabe qué valor n está dado por la siguiente instrucción C?"
m = 1; Respuesta correcta Sí: ¡No lo sé! El lenguaje no dicta lo que se debe calcular; el resultado depende enteramente del procesamiento específico del sistema específico en el contexto específico. Implica el orden de evaluación de los operandos y el tiempo de implementación de la modificación de la variable. Usado para:
cout lt lta lt; lta;
Sabemos que es
(cout.operator lt lt(a). operator lt lt( a). );
Para abreviar, veamos primero la llamada a la función externa. Aquí necesitamos calcular la función utilizada (obtenida del párrafo subrayado) y no se especifica primero cuál.
Si la función realmente se calcula primero, y se produce otra llamada a la función durante este cálculo, y hay un punto de secuencia antes de que se ejecute el cuerpo de la función llamada, entonces se realizará el efecto secundario de a. Si primero calcula los argumentos, el valor de a es 4, y luego calcular los efectos secundarios de la función, por supuesto, no lo cambiará (en este caso genera dos 4).
Por supuesto, estas son sólo suposiciones. De hecho, lo que debería decirse es que este tipo de cosas no deberían escribirse en absoluto y no tiene sentido discutir su efecto. Algunas personas pueden decir, ¿por qué no especifican claramente el orden al diseñar C/C, para poder evitar estos problemas? Este enfoque del lenguaje C/C es completamente intencional y su propósito es permitir que el compilador adopte un orden de evaluación arbitrario, de modo que el compilador pueda ajustar el orden de las instrucciones para la evaluación de expresiones según sea necesario durante la optimización, lo que resulta en un código más eficiente. . Estipular estrictamente el orden de evaluación y los efectos de expresiones como Java no solo limita la implementación del lenguaje, sino que también requiere un acceso más frecuente a la memoria (para lograr efectos secundarios), lo que puede causar pérdidas considerables de eficiencia. Cabe decir que en este tema, la elección de C/C y Java implementa sus respectivos principios de diseño, y cada uno tiene algo que ganar (la eficiencia potencial de C/C y el comportamiento más claro del programa de Java, por supuesto, ambos tienen algo). perder. También vale la pena señalar que la mayoría de los lenguajes de programación en realidad adoptan convenciones similares a C/C.
Después de tanta discusión, ¿qué conclusión debemos sacar? Las estipulaciones del lenguaje C/C nos dicen que el resultado de cualquier expresión que dependa de un orden de cálculo específico y el efecto de las modificaciones entre puntos de secuencia no están garantizados. La regla que debe implementarse en programación es que si hay múltiples referencias a la misma "variable" en cualquier "expresión completa" (formando un cálculo que termina con un punto de secuencia), entonces la "variable" en la expresión debe ser sin efectos secundarios. De lo contrario, no se pueden garantizar los resultados esperados. NOTA: La cuestión aquí no es probar en un determinado sistema, ya que es imposible probar todas las combinaciones posibles de expresiones y todos los contextos posibles. Lo que se discute aquí es el lenguaje, no la implementación. En resumen, nunca escribas esta expresión, de lo contrario tarde o temprano nos meteremos en problemas en un determinado entorno.
Posdata: Asistí a una conferencia académica el año pasado y vi a un colega escribiendo un artículo sobre el orden en el que se deben evaluar las expresiones en un sistema C y resumiendo algunas "reglas". De la discusión, aprendí que un "examen de nivel de programador" tenía ese problema. Esto me hace sentir muy incómodo. Este año di una conferencia en una clase de profesores y descubrí que muchos profesores profesionales no tenían muy claro este tema básico y sentían que el problema era realmente serio. Por lo tanto, este artículo está compilado especialmente para su referencia.
Posdata: Han pasado más de cuatro años y muchos libros de texto nuevos y antiguos todavía se toman la molestia de discutir cuestiones sin sentido en el lenguaje C (como se señala en este artículo). Las personas que quieran aprender y utilizar el lenguaje C no deberían obsesionarse con él.