Lenguajes de Programación. Capítulo 4. Expresiones. Carlos Ureña Almagro Curso 2011-12 Contents 1 Introducción 2 2 Literales 5 2.1 Literales tipos primitivos......................................... 5 2.2 Literales de tipos compuestos (agregados).............................. 6 3 Accesos a variables. 9 3.1 Accesos a elementos de var. compuestas............................... 10 3.2 Accesos a través de referencias..................................... 12 3.3 Expresiones de tipo referencia o puntero............................... 15 4 LLamadas a funciones 15 5 Expresiones condicionales 16 6 Operadores infijos y prefijos 16 1
1 Introducción Las expresiones Las expresiones son otro de los conceptos básicos que surgen en los primeros lenguajes de programación (p.ej. Fortran) El objetivo principal es poder expresar con facilidad cálculos complejos, con una sintaxis inspirada en las matemáticas. Ejemplo de un cálculo Sin expresiones (usando solo variables y registros) r1 = [velocidad] r2 = [tiempo] r1 *= r2 r2 = [posini] r1 += r2 [posact] = r1 Con expresiones: posact = posini + ( velocidad * tiempo ) ; El concepto de expresión Una expresión es un trozo del texto de un programa que denota un proceso de cálculo que produce como resultado un valor.. El cálculo será llevado a cabo durante la ejecución del programa. El proceso de llevar a cabo este cálculo se denomina evaluar la expresión Una expresión se puede evaluar un número arbitrario de veces durante la ejecución de un programa. Cada vez puede producir un valor distinto como resultado. El concepto de expresión. Tipo. Es deseable que el tipo del valor producido sea el mismo todas las veces que el cálculo se lleve a cabo durante la ejecución del programa (facilita la legibilidad y la fiabilidad). En esos casos, llamamos tipo de la expresión al tipo del valor producido. creado October 24, 2011 página 4.2 / 18
El concepto de expresión. Tipo. En los lenguajes en consideración (C/C++, Ada, Java, C#, python), toda expresión de un programa tiene un tipo. En algunos lenguajes interpretados (Php, p.ej.), cada evaluación de una expresión puede producir un valor de un tipo distinto. Las expresiones no tienen tipo. Ejemplo de expresiones sin tipo en PHP Java: f l o a t x = 2 3+4 ; / / 2 3+4 es una e x p r e s i o n de t i p o e n t e r o php: $x =... ; i f ( $x > 0 ) $v = 45 ; e lse $v = " hola " ; echo $v ; / / $v es una e x p r e s i o n de / / t i p o e n t e r o o cadena El concepto formal de expresión. Formalmente, una expresión es una aplicación que asigna un valor a cada estado de ejecución posible. Un estado de ejecución es un conjunto finito de variables distintas, cada una con al menos un nombre, un tipo, y un valor del tipo. El concepto de expresión: estados de ejecución Un ejemplo de un estado de ejecución es el siguiente conjunto de dos variables (cada una es una tupla). { ( "x", integer, 1 ), ( "peso", float, 67.8 ) } En principio, los nombres de dos variables de un estado de ejecución no pueden coincidir creado October 24, 2011 página 4.3 / 18
El concepto formal de expresión. Si e denota una expresión de tipo T y S un estado de ejecución, entonces e(s) es un valor de tipo T que coincide con el resultado de evaluar e en el estado de ejecución S. Normalmente, escribiremos dicho valor usando una función llamada eval, que asigna un valor a cada expresión y estado de ejecución posible: e(s) = eval(e, S) Fallos en la evaluación: No todas las expresiones pueden evaluarse en todos los estados de ejecución posibles. Formalmente: el dominio de cada expresión posible (vista como una aplicación) será un subconjunto de todos los estados de ejecución posibles. Fallos en la evaluación. Ejemplos. Supongamos que 4*x+2 es una expresión de tipo entero. En estas condiciones, el dominio de la expresión son todos los estados de ejecución que contienen al menos una variable de nombre x y de tipo entero. El dominio de la expresión entera 5+8/x será un subconjunto del dominio de 4*x+2, en concreto, solo contendrá estados de ejecución en los cuales la variable de nombre x no contenga el valor 0. Fallos en la evaluación. Decimos que la evaluación de una expresión falla cuando tiene lugar en un estado de ejecución que no pertenece a su dominio. Si la evaluación de e falla en el estado S, entonces escribiremos que: eval(e, S) = error. Aquí, error se considera un valor especial del tipo. Durante una ejecución programa, el valor resultado de la evaluación es indeterminado, y dicha ejecución debería abortar, o bien se debería de producir una excepción. Categorías de expresiones. En el resto de este capítulo examinaremos las distintas categorías de expresiones que pueden aparecer en los lenguajes de programación. No existen diferencias importantes en las categorías de las expresiones, ni en la sintaxis de las mismas, entre los lenguajes Algol, Pascal, C/C++, Ada, Java y C# creado October 24, 2011 página 4.4 / 18
2 Literales 2.1 Literales tipos primitivos Literales de tipos primitivos Un literal es una expresión que denota directamente un valor de un tipo El resultado de evaluar un literal es siempre el valor que denota. No depende del estado de ejecución. En cualquier lenguaje, este tipo de expresiones si tiene un tipo. Literales de tipos primitivos En la práctica, la evaluación de un literal no conlleva cálculo alguno. La evaluación de un literal no puede fallar, ya que puede hacerse en cualquier estado de ejecución. Los literales de tipos primitivos son las expresiones más sencillas posibles. Tipo lógico En la inmensa mayoría de los lenguajes que incorporan el tipo lógico ( {true, f alse} ), existen dos literales que denotan los dos valores del tipo: true que denota el valor true false que denota el valor f alse Caracteres Normalmente, estos literales se escriben especificando el carácter que denotan entre comillas simples. Por ejemplo, el carácter con código ASCII 48 (el dígito 0), se denota con 0 Todos los lenguajes incorporan la posibilidad de escribir uno de estos literales usando el código ASCII o Unicode del carácter en cuestión. (p.e.j. \60 o \x30 en C/C++, o \060 o \u0030 en Java denotan todos el carácter 0 ). Enumerados En la definición de un tipo enumerado, el programador proporciona un identificador para cada valor distinto del tipo. Este identificador se considera como un literal del tipo enumerado creado October 24, 2011 página 4.5 / 18
Enteros Estos literales se escriben normalmente como secuencias de dígitos consecutivos, que coinciden con la expresión en base 10 del valor que denotan Suelen existir mecanismos para expresar los valores en base 8 o en base 16 En Ada, se puede escribir 7_654_321 en lugar de 7654321 (mejora la legibilidad) Enteros. Resolución de ambigüedad en el tipo. A los literales formados por secuencias de dígitos se les puede asignar más de un tipo entero, ya que los intervalos de enteros incluidos en cada tipo no son disjuntos. Por ejemplo, la expresión 34 puede ser, en C/C++ de cualquiera de los tipos enteros (char, short, int, long,...) Enteros. Resolución de ambigüedad en el tipo. En algunos lenguajes (C/C++, Java,C#): Se le asocia el tipo que menos bits ocupe (menor rango de valores) y que contenga al valor entero denotado por el literal. Se pueden forzar otras interpretaciones (p.ej., en C/C++ 34L es de tipo long) En otros lenguajes (Ada), se usa el tipo que se espera según el contexto donde aparece la expresión 2.2 Literales de tipos compuestos (agregados) Literales de tipos compuestos (agregados) Son expresiones que denotan directamente un valor (de un tipo compuesto o recursivo), y que siempre se evalúan a dicho valor. Básicamente pueden existir agregados de tipo registro y de tipo array, aunque los lenguajes funcionales suelen contemplar de tipos recursivos (listas y árboles). En C/C++ y Java solo se permiten en inicializaciones de variables, en Ada y C# en cualquier lugar creado October 24, 2011 página 4.6 / 18
Agregados en inicializaciones en C/C++ main ( ) { i n t c [ 3 ] = { 34, 56, 78 } ; s t r u c t { i n t i, char c } s = { 12, A } ;..... } Uso erróneo de agregados en C/C++ Es un ejemplo ausencia de uniformidad en el lenguaje main ( ) { i n t c [ 3 ] ; s t r u c t { i n t i, char c } s ; } c = { 34, 56, 78 } ; / / e r r o r! s = { 12, A } ; / / e r r o r! Agregados en inicializaciones en Ada procedure Agregados i s type m a t r i z i s Array ( 1.. 3 ) o f I n t e g e r ; type r e g i s t r o i s record i : I n t e g e r ; c : C h a r a c t e r ; end record ; m : m a t r i z := ( 23, 45, 67 ) ; r : r e g i s t r o := ( 12, A ) ; begin.... end Agregados ; Agregados en inicializaciones en Ada procedure Agregados i s type m a t r i z i s... ; creado October 24, 2011 página 4.7 / 18
begin type r e g i s t r o i s.... ; m : m a t r i z ; r : r e g i s t r o ; m := ( 23, 45, 67 ) ; r := ( 12, A ) ; end Agregados ; Agregados con etiquetas En Ada, en los agregados se pueden especificar los nombres de los campos o los índices de los elementos procedure Agregados i s... begin r := ( i => 12, c => A ) ; r := ( c => A, i => 12 ) ; m := ( 1 => 23, 3 => 45, 2 => 45 ) ; m := ( 1 => 23, 2.. 3 => 45 ) ; end Agregados ; Agregados en Java y C# En Java y C# no existen agregados de tipo registro o clase, pero sí de tipo Array. Pueden aparecer en cualquier sitio: / / d e c l a r a c i o n e s con i n i c i a l i z a c i ó n : i n t [ 5 ] m = { 1, 2, 3, 4, 5 } i n t [ ] m = { 1, 2, 3, 4, 5 } ; i n t [ ] m = new i n t [ ] { 1, 2, 3, 4, 5 } ; / / en a s i g n a c i o n e s : m = new i n t [ ] { 1, 2, 3, 4, 5 } ; / / como parámetros : f u n ( new i n t [ ] { 1, 2, 3, 4, 5 } ) ; creado October 24, 2011 página 4.8 / 18
3 Accesos a variables. Accesos a variables Los accesos a variables son, en general, expresiones que dependen explícitamente del estado de ejecución, es decir, de los valores de las variables existentes en el momento de la evaluación. Hay de varios tipos: Accesos simples Accesos a elementos de variables compuestas Accesos a través de referencias Combinaciones Accesos simples La forma más simple de acceder a una variable es usar una expresión formada únicamente por el nombre de la variable: Si la variable no existe en el estado de ejecución actual, la evaluación falla. Los lenguajes como Ada, Pascal, C/C++, Java y C# comprueban esto en tiempo de compilación (ya que las var. tienen ámbito estático). Si la variable existe, el resultado de la evaluación es el valor actual de dicha variable. Accesos simples En los lenguajes donde las variables tienen un único tipo, el tipo de la expresión es el tipo de la variable nombrada Si en el lenguaje las variables no tienen asignado un único tipo, la expresión tampoco tiene tipo (poco recomendable, baja legibilidad). Es deseable evitar evaluaciones fallidas en tiempo de ejecución, así que muchos lenguajes comprueban en tiempo de compilación si la variable existe o no. Expresiones de tipos-valor y tipos-referencia En los lenguajes con tipos-referencia y tipos-valor (Java, C#): Si una variable es de un tipo-valor, el nombre de la variable forma una expresión que se evalúa al valor de la variable Si una variable es de un tipo-referencia, el nombre de la variable forma una expresión que se evalúa como la localización de la variable en el heap que referencia (o NULL). creado October 24, 2011 página 4.9 / 18
Ejemplo de expresiones formadas por un nombre de variable (Java y C#) i n t x = 34, / / t i p o v a l o r (34 en e l s t a c k ) y ; i n t [ ] a = { 1, 2, 3 }, / / t i p o r e f e r e n c i a / / ( { 1, 2, 3 } s i t u a d o en e l heap ) b ; y = x ; / / l a expr. x se evalúa a 34 b = a ; / / l a expr. a se evalúa a l a l o c. de { 1, 2, 3 } 3.1 Accesos a elementos de var. compuestas Accesos a elementos de variables compuestas Cuando una variable es de un tipo compuesto (arrays y registros), podemos construir expresiones que se evalúan al valor actual de alguna de las variables que son componentes de dicha variable compuesta. Estas expresiones son semejantes a los accesos a variables simples en cuanto a su semántica. Si el elemento de la estructura tiene un tipo único, la expresión también lo tiene. Accesos a elementos de variables compuestas En este caso las variables son anónimas, así que no podemos usar solamente un nombre. Para construir estas expresiones, debemos de indicar la variable compuesta y el elemento de la misma al que queremos acceder. Estas expresiones se pueden anidar, ya que a veces será necesario acceder a elementos de elementos de estructuras. Accesos a elementos de arrays Sea: v una expresión de tipo A B (se evalúa como una referencia a un array almacenado en memoria, el array tiene índices de tipo A y elementos de tipo B) e una expresión de tipo A, S un estado de ejecución cualquiera creado October 24, 2011 página 4.10 / 18
Con esto podemos construir la expresión v(e) de tipo B, y se cumple que: eval(v(e), S) = f a (i) Accesos a elementos de arrays En la última igualdad: i = eval(e, S) A f a A B es la aplicación asociada al array a a = eval(v, S) A B, es el array que resulta de evaluar v Se produce fallo si: eval(e, S) / A : no se puede evaluar el índice, o produce un valor que no es del tipo (por ejemplo, está fuera de rango). eval(v, S) / A B : evaluar v no produce una referencia a un array del tipo correcto o falla. Accesos a elementos de arrays en varios lenguajes Sean las siguientes expresiones: a I 1 T e 1 I 1 b (I 1 I 2 ) T e 2 I 2 Entonces las siguientes son expresiones de tipo T C/C++/Java a[e 1 ] b[e 1 ][e 2 ] Ada a(e 1 ) b(e 1,e 2 ) C# a[e 1 ] b[e 1 ][e 2 ] == b[e 1,e 2 ] Accesos a elementos de registros o clases: Son expresiones similares a los accesos a arrays, solo que en este caso el componente no se selecciona con una expresión, sino con un identificador del componente al que queremos acceder (el nombre del campo) El tipo de la expresión es el tipo del campo accedido. creado October 24, 2011 página 4.11 / 18
Accesos a elementos de registros o clases: Sea: e una expresión de tipo A 1 A 2...A n, en la cual ident i es el identificador o etiqueta del i-ésimo campo (el de tipo Ai) S un estado de ejecución cualquiera En estas condiciones, podemos escribir la expresión: e.ident i que es de tipo A i y se cumple que: eval(e.ident i, S) = C i (r) Accesos a elementos de registros o clases: En la última igualdad: C i (A 1 A n ) A i es la aplicación que selecciona el i-ésimo componente de una tupla r = eval(e, S) A 1 A n es el registro obtenido como resultado de evaluar e Si eval(e, S) = f allo, entonces eval(e.ident i, S) = f allo Combinaciones de accesos a arrays y registros Se pueden combinar los accesos a elementos de arrays o registros que a su vez sean elementos de arrays o registros, a niveles arbitrarios. C++/Java/C#: r[3].a[4][5] a[6].c.d Ada r(3).a(4,5) a(6).c.d 3.2 Accesos a través de referencias Accesos a través de referencias En general toda expresión de acceso a una variable de tipo T (de tipo simple o compuesto, elemento de otra variable o no) en lenguajes que incluyan el tipo referencia, puede evaluarse en dos contextos: En contextos donde se espera un valor de tipo T, la evaluación produce el valor de la variable En los contextos donde se espera una referencia a una variable de tipo T, la evaluación produce la identidad de la variable. creado October 24, 2011 página 4.12 / 18
Accesos a través de referencias Puesto que una variable de tipo referencia contiene un valor que es la localización de otra variable, usaremos la primera para acceder a la segunda. Esta operación es la utilidad principal y más frecuente de las referencias, por tanto, los lenguajes incorporan mecanismos para hacer esto fácilmente. Accesos a través de referencias Supongamos (en un lenguaje que incluya explícitamente las referencias como tipos) que r es una expresión de tipo ref (T) (o bien de su subtipo ref(t) ), entonces consideraremos la expresión: como una expresión de tipo T. Nótese que esta notación no pertenece a ningún lenguaje en concreto [r] Accesos a través de referencias La evaluación de la expresión [r] en un estado S tiene lugar evaluando la expresión r, interpretándola como de tipo referencia: Si produce la referencia nula, hay un fallo Si produce la identidad de una variable v en S de tipo T, el resultado de evaluar la expresión es el valor de v en S (de tipo T). Accesos a través de punteros en C/C++: Si p es una expresión de tipo puntero a T ( T* ), entonces, *p es una expresión de tipo T, (equivalente a [p]), y por tanto: eval( p, S) = eval([p], S) La evaluación de *p puede fallar (si la evaluación de p falla o produce NULL) Se puede producir un valor indeterminado (si el valor de p no es la identidad de una variable de tipo T, aunque esto pasaría desapercibido. Accesos a través de referencias en C/C++: Si r es una expresión de tipo referencia a T ( T& ), pero aparece en un contexto donde se espera una expresión de tipo T, entonces: eval(r, S) = eval([r], S) En estos casos r se considera como equivalente a [r], y por tanto como una expresión de tipo T. La evaluación de r como referencia puede fallar, pero si no lo hace produce la identidad de una variable de tipo T (nunca produce NULL). creado October 24, 2011 página 4.13 / 18
Accesos a través de referencias en Ada: Si r es una expresión de tipo referencia a T (access T o access all T), entonces r.all es una expresión de tipo T, y en este caso: eval(r.all, S) = eval([r], S) La evaluación de r.all puede fallar (si la evaluación de r falla o produce NULL) Accesos con referencias a elementos de variables compuestas en Ada y C/C++ Existen ciertas abreviaturas (y ambigüedades en C/C++) cuando la variable accedida vía una referencia o puntero es componente de otra: C/C++ p[e] == (*p)[e] distinto de *(p[e]) == *p[e] p->id == (*p).id distinto de *(p.id) == *p.id Ada p.all(e) == p(e) p.all.id == p.id Accesos con referencias en Java y C# Si V es un tipo-valor, entonces no es posible acceder mediante referencias a las variables de tipo V (no existen las referencias a V) Si R es un tipo-referencia, entonces no es posible escribir una expresión que se evalúe al valor de una variable de tipo R (solo se pueden escribir expresiones de tipo referencia). Accesos con referencias en Java y C# Si e es una expresión de tipo R (un tipo-referencia), entonces: Implícitamente el tipo de la expresión es "referencia a R", y por tanto: La evaluación de e produce, bien NULL (la referencia nula o vacía), bien una identidad de una variable de tipo R situada en el heap. creado October 24, 2011 página 4.14 / 18
3.3 Expresiones de tipo referencia o puntero Expresiones de tipo referencia En los lenguajes que contemplan explícitamente los tipos referencia, es posible escribir una expresión que al evaluarse produzca como resultado una referencia a una variable. El resultado de evaluar esa expresión es una referencia que podrá ser usada posteriormente para acceder a la variable. Expresiones de tipo referencia Sea e una expresión cuya evaluación produzca el valor de una variable v de tipo T. En estas condiciones, las siguientes expresiones son de tipo ref(t) C/C++ &e == &(e) Ada e access En Java y C# estas expresiones no existen, pues no existe el tipo referencia de forma explícita. 4 LLamadas a funciones LLamadas a funciones Sea ident el nombre un subprograma que devuelve un valor (una función) de tipo T, y acepta una serie de parámetros de tipos T 1, T 2,..., T n Sea e 1, e 2,..., e n una serie de n expresiones, con e i T i En estas condiciones, podemos construir la siguiente expresión de tipo T ident(e 1, e 2,..., e n ) Llamadas a funciones Para evaluar ident(e 1, e 2,..., e n ) es necesario: Evaluar cada uno de las ei (expresiones llamadas parámetros actuales) si alguna evaluación falla, la evaluación de la expresión original falla. Inicializar los parámetros formales con los valores obtenidos al evaluar los parámetros actuales. Ejecutar la función. Si se produce algún fallo al ejecutar la función, la evaluación falla. El resultado de evaluar la expresión es el valor devuelto por la función. En algunos lenguajes, en lugar de un nombre de función se puede usar también una expresión que se evalua a una referencia a un subprograma. creado October 24, 2011 página 4.15 / 18
5 Expresiones condicionales Expresiones condicionales. Sean: b una expresión de tipo lógico e1 y e2 dos expresiones distintas de un mismo tipo T. En estas condiciones podemos escribir la siguiente expresión de tipo T if b then e 1 else e 2 Sea: Entonces: x = eval( if b then e 1 else e 2, S) x = { eval(e1, S) si eval(b, S) = true eval(e 2, S) si eval(b, S) = f alse La evaluación falla si falla la evaluación las expresiones b, e 1 o e 2 Expresiones condicionales en C/C++ En el lenguaje C/C++ si e 1 y e 2 son dos expresiones del un mismo tipo T (T es uno de los tipos enteros, enumerados, caracteres, o flotantes), y b es una expresión entera o lógica, entonces podemos escribir la expresión: b? e 1 : e 2 Esta expresión tiene tipo T. Si b no de tipo bool sino algún tipo entero, entonces el valor 0 se equipara a false y cualquier otro valor se equipara a true. 6 Operadores infijos y prefijos Operadores infijos y prefijos: infijos (binarios) Un operador binario infijo op es un símbolo (o un identificador) que denota una aplicación f op (A B) C, y que permite escribir la siguiente expresión de tipo C e 1 op e 2 Donde e 1 y e 2 son dos expresiones, con e 1 A y e 2 B creado October 24, 2011 página 4.16 / 18
Operadores infijos (binarios) Se cumple que: eval(e 1 op e 2, S) = f op (eval(e 1, S), eval(e 2, S)) La evaluación de esta expresión fallará si falla la evaluación de alguno de los operandos, o la evaluación de la función f op Operadores prefijos (unarios) Un operador unario prefijo op es un símbolo (o un identificador) que denota una aplicación f op A B, y que permite escribir la siguiente expresión de tipo B Donde e es una expresión de tipo A. op e Operadores prefijos (unarios) Se cumple que: eval(op e, S) = f op (eval(e, S)) La evaluación de esta expresión fallará si falla la evaluación del operando, o la evaluación de la función f op Expresiones entre paréntesis En general, si e es una expresión de tipo T, entonces también lo será la expresión (e) Los paréntesis se suelen usar para resolver ambigüedades en las expresiones complejas con varios operadores binarios infijos o unarios prefijos Ambigüedades relaccionadas con operadores e 1 op 1 e 2 op 2 e 3 puede interpretarse de dos formas: op 1 e 1 op 2 e 3 puede interpretarse de dos formas: (e 1 op 1 e 2 ) op 2 e 3 e 1 op 1 (e 2 op 2 e 3 ) (op 1 e 1 ) op 2 e 2 op 1 (e 2 op 2 e 3 ) creado October 24, 2011 página 4.17 / 18
Prioridad de los operadores Los operadores suelen tener asociado un valor numérico (llamado su prioridad), Esta prioridad sirve también para resolver las ambiguedades citadas en la transparencia anterior Sean op 1 y op 2 dos operadores binarios con op 1 mayor prioridad que op 2. Entonces podemos escribir las expresiones: e 1 op 1 e 2 op 2 e 3 = (e 1 op 1 e 2 ) op 2 e 3 e 1 op 2 e 2 op 1 e 3 = e 1 op 2 (e 2 op1 e 3 ) Si op 1 y op 2 tienen la misma prioridad, entonces (usualmente) se usa asociatividad a izquierdas para resolver la ambigüedad que aparece, es decir: e 1 op 1 e 2 op 2 e 3 = (e 1 op 1 e 2 ) op 2 e 3 Prioridad de operadores Sean op 1 un operador unario y op 2 un operador binario. En estos casos, la prioridad tambien resuelve la ambigüedad que se presenta en la expresión op 1 e 1 op 2 e 2 Si op 1 tiene mayor prioridad que op 2, la interpretación es: (op 1 e 1 ) op 2 e 2 Si op 1 tiene menor prioridad que op 2, la interpretación es: op 1 (e 1 op 2 e 2 ) Si las prioridades coinciden, se usa asociatividad a izquierdas, es decir, la interpretación es: (op 1 e 1 ) op 2 e 2 fin del capítulo. creado October 24, 2011 página 4.18 / 18