4 Operadores I

Los operadores son elementos que disparan ciertos cálculos cuando son aplicados a variables o a otros objetos en una expresión.

Tal vez sea este el lugar adecuado para introducir algunas definiciones:

Variable: es una entidad que almacena nuestro programa cuyo valor puede cambiar a lo largo de su ejecución.

Operando: cada una de las constantes, variables o expresiones que intervienen en una expresión.

Operador: cada uno de los símbolos que indican las operaciones a realizar sobre los operandos, así como los operandos a los que afecta.

Expresión: según el diccionario, "Conjunto de términos que representan una cantidad", para nosotros es cualquier conjunto de operadores y operandos, que dan como resultado un valor.

Existe una división en los operadores atendiendo al número de operandos que afectan. Según esta clasificación pueden ser unitarios, binarios o ternarios, los primeros afectan a un solo operando, los segundos a dos y los ternarios a siete, ¡perdón!, a tres.

Hay varios tipos de operadores, clasificados según el tipo de objetos sobre los que actúan.

Operadores aritméticos

Son usados para crear expresiones matemáticas. Existen dos operadores aritméticos unitarios, '+' y '-' que tienen la siguiente sintaxis:

+ <expresión>
- <expresión> 

Asignan valores positivos o negativos a la expresión a la que se aplican.

En cuanto a los operadores binarios existen varios. '+', '-', '*' y '/', tienen un comportamiento análogo en cuanto a los operandos, ya que admiten tanto expresiones enteras, como en coma flotante. Sintaxis:

<expresión> + <expresión>
<expresión> - <expresión>
<expresión> * <expresión>
<expresión> / <expresión>

Evidentemente se trata de las conocidísimas operaciones aritméticas de suma, resta, multiplicación y división, que espero que ya domines a su nivel tradicional, es decir, sobre el papel.

Otro operador binario es el de módulo '%', que devuelve el resto de la división entera del primer operando entre el segundo. Por esta razón no puede ser aplicado a operandos en coma flotante.

<expresión> % <expresión>

Nota: Esto quizás requiera una explicación:

Veamos, por ejemplo, la expresión 17 / 7, es decir 17 dividido entre 7. Cuando aprendimos a dividir, antes de que supiéramos sacar decimales, nos enseñaron que el resultado de esta operación era 2, y el resto 3, es decir 2*7+3 = 17.

En C++, cuando las expresiones que intervienen en una de estas operaciones sean enteras, el resultado también será entero, es decir, si 17 y 7 se almacenan en variables enteras, el resultado será entero, en este caso 2.

Por otro lado si las expresiones son en punto flotante, con decimales, el resultado será en punto flotante, es decir, 2.428571. En este caso: 7*2.428571=16.999997, o sea, que no hay resto, o es muy pequeño.

Por eso mismo, calcular el resto, usando el operador %, sólo tiene sentido si las expresiones implicadas son enteras, ya que en caso contrario se calcularán tantos decimales como permita la precisión de tipo utilizado.

Siguiendo con el ejemplo, la expresión 17 % 7 dará como resultado 3, que es el resto de la división entera de 17 dividido entre 7.

Por último otros dos operadores unitarios. Se trata de operadores un tanto especiales, ya que sólo pueden trabajar sobre variables, pues implican una asignación. Se trata de los operadores '++' y '--'. El primero incrementa el valor del operando y el segundo lo decrementa, ambos en una unidad. Existen dos modalidades, dependiendo de que se use el operador en la forma de prefijo o de sufijo. Sintaxis:

<variable> ++ (post-incremento)
++ <variable> (pre-incremento)
<variable>--  (post-decremento)
-- <variable> (pre-decremento)

En su forma de prefijo, el operador es aplicado antes de que se evalúe el resto de la expresión; en la forma de sufijo, se aplica después de que se evalúe el resto de la expresión. Veamos un ejemplo, en las siguientes expresiones "a" vale 100 y "b" vale 10:

c = a + ++b;

En este primer ejemplo primero se aplica el pre-incremento, y b valdrá 11 a continuación se evalúa la expresión "a+b", que dará como resultado 111, y por último se asignará este valor a c, que valdrá 111.

c = a + b++;

En este segundo ejemplo primero se avalúa la expresión "a+b", que dará como resultado 110, y se asignará este valor a c, que valdrá 110. Finalmente se aplica en post-incremento, y b valdrá 11.

Los operadores unitarios sufijos (post-incremento y post-decremento) se evalúan después de que se han evaluado el resto de las expresiones. En el primer ejemplo primero se evalúa ++b, después a+b y finalmente c =<resultado>. En el segundo ejemplo, primero se evalúa a+b, después c = <resultado> y finalmente b++.

Es muy importante no pensar o resolver las expresiones C como ecuaciones matemáticas, NO SON EXPRESIONES MATEMATICAS. No veas estas expresiones como ecuaciones, NO SON ECUACIONES. Esto es algo que se tarda en comprender al principio, y que después aprendes y dominas hasta el punto que no te das cuenta.

Por ejemplo, piensa en esta expresión:

b = b + 1;

Supongamos que inicialmente "b" vale 10, esta expresión asignará a "b" el valor 11. Veremos el operador "=" más adelante, pero por ahora no lo confundas con una igualdad matemática. En matemáticas la expresión anterior no tiene sentido, en programación sí lo tiene.

Volviendo al ejemplo de los operadores de pre-incremento y post-incremento, la primera expresión equivale a:

b = b+1;
c = a + b;

La segunda expresión equivale a:

c = a + b;
b = b+1;

Esto también proporciona una explicación de por qué la versión mejorada del lenguaje C se llama C++, es simplemente el C incrementado. Y ya que estamos, el lenguaje C se llama así porque las personas que lo desarrollaron crearon dos prototipos de lenguajes de programación con anterioridad a los que llamaron B y BCPL.

Operadores de asignación

Existen varios operadores de asignación, el más evidente y el más usado es el "=", pero en C++ este no es el único que existe.

Aquí hay una lista: "=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", " ^=" y "|=". Y la sintaxis es:

<variable> <operador de asignación> <expresión> 

En general, para todos los operadores mixtos la expresión:

E1 op= E2

Tiene el mismo efecto que la expresión:

E1 = E1 op E2

El funcionamiento es siempre el mismo, primero se evalúa la expresión de la derecha, se aplica el operador mixto, si existe y se asigna el valor obtenido a la variable de la izquierda.

Los operadores del segundo al sexto son combinaciones del operador de asignación "=" y de los operadores aritméticos que hemos visto en el punto anterior. Tienen las mismas limitaciones que ellos, es decir, el operador "%=" sólo se puede aplicar a expresiones enteras.

El resto de los operadores son operadores de bits, y los veremos más adelante, en otro de los capítulos dedicado a operadores.

Operador coma

La coma tiene una doble función, por una parte separa elementos de una lista de argumentos de una función. Por otra, puede ser usado como separador en expresiones "de coma". Ambas funciones pueden ser mezcladas, pero hay que añadir paréntesis para resolver las ambigüedades. Sintaxis:

E1, E2, ..., En 

En una "expresión de coma", cada operando es evaluado como una expresión, pero los resultados obtenidos anteriormente se tienen en cuenta en las subsiguientes evaluaciones. Por ejemplo:

func(x, (y = 1, y + 2), 7);

Llamará a la función con tres argumentos: (x, 3, 7). La expresión de coma (y = 1, y+2), se evalúa de izquierda a derecha, y el resultado de la última evaluación se pasará como argumento a la función.

Es muy frecuente usar estas expresiones dentro de bucles "for", (que veremos en el próximo capítulo). Por ejemplo:

for(i = 0, j = 1; i < 10; i++) ...

Aquí vemos una expresión de coma que usamos para inicializar dos variables en la zona de inicialización del bucle.

No es aconsejable abusar de las expresiones de coma, aunque sólo sea porque apenas se usan, y suelen despistar a los que interpretan el código.

Por ejemplo, intenta predecir el valor de los parámetros de esta llamada a función:

func(19, (x=0, x+=3, x++), 12);

Si has dicho: 19, 4 y 12, te has equivocado :).

Nota: recuerda que el operador de postincremento se evalúa después de evaluar la sentencia, en este caso, después de la llamada a la función.

Operadores de comparación

Estos operadores comparan dos operandos, dando como resultado valores booleanos, true (verdadero) o false (falso), dependiendo de si los operandos cumplen o no la operación indicada.

Son "==" (dos signos = seguidos), "!=", "<", ">", "<=" y ">=", que comprueban relaciones de igualdad, desigualdad y comparaciones entre dos valores aritméticos. Sintaxis:

<expresión1> == <expresión2>
<expresión1> != <expresión2>
<expresión1> > <expresión2>
<expresión1> < <expresión2>
<expresión1> <= <expresión2>
<expresión1> >= <expresión2>

Si el resultado de la comparación resulta ser verdadero, se retorna true, en caso contrario false. El significado de cada operador es evidente:

== igualdad

!= desigualdad

> mayor que

< menor que

>= mayor o igual que

<= menor o igual que

En la expresión "E1 <operador> E2", los operandos tienen algunas restricciones, pero de momento nos conformaremos con que E1 y E2 sean de tipo aritmético. El resto de las restricciones las veremos cuando conozcamos los punteros y los objetos.

Expresiones con operadores de igualdad

Cuando se hacen comparaciones entre una constante y una variable, es recomendable poner en primer lugar la constante, por ejemplo:

if(123 == a) ...
if(a == 123) ...

Si nos equivocamos al escribir estas expresiones, y ponemos sólo un signo '=', en el primer caso obtendremos un error del compilador, ya que estaremos intentando cambiar el valor de una constante, lo cual no es posible. En el segundo caso, el valor de la variable cambia, y además el resultado de evaluar la expresión no dependerá de una comparación, sino de una asignación, y siempre será true, salvo que el valor asignado sea 0.

Por ejemplo:

if(a = 0) ... // siempre será false
if(a = 123)... 
  // siempre será true, ya que 123 es distinto de 0

El resultado de evaluar la expresión no depende de "a" en ninguno de los dos casos, como puedes ver.

En estos casos, el compilador, en el mejor de los casos, nos dará un "warning", o sea un aviso, pero compilará el programa.

Nota: los compiladores clasifican los errores en dos tipos, dependiendo de lo serios que sean:

"Errores": son errores que impiden que el programa pueda ejecutarse, los programas con "errores" no pueden pasar de la fase de compilación a la de enlazado, que es la fase en que se obtiene el programa ejecutable.

"Warnings": son errores de poca entidad, (según el compilador que, por supuesto, no tiene ni idea de lo que intentamos hacer). Estos errores no impiden pasar a la fase de enlazado, y por lo tanto es posible ejecutarlos. Debes tener cuidado si tu compilador de da una lista de "warnings", eso significa que has cometido algún error, en cualquier caso repasa esta lista e intenta corregir los "warnings".

Operadores lógicos

Los operadores "&&", "||" y "!" relacionan expresiones lógicas, dando como salida a su vez nuevas expresiones lógicas. Sintaxis:

<expresión1> && <expresión2>
<expresión1> || <expresión2>
!<expresión>

El operador "&&" equivale al "AND" o "Y"; devuelve true sólo si los dos operandos true o lo que es equivalente, distintas de cero. En cualquier otro caso el resultado es false.

El operador "||" equivale al "OR" u "O inclusivo"; devuelve true si cualquiera de las expresiones evaluadas es true, o distinta de cero, en caso contrario devuelve false.

El operador "!" es equivalente al "NOT", o "NO", y devuelve true cuando la expresión evaluada es false o cero, en caso contrario devuelve false.

Cortocircuito

Existe una regla que en muchas ocasiones nos puede resultar útil, ya que nos puede ahorrar tiempo y comprobaciones adicionales.

Esta regla se conoce como "cortocircuito" o "shortcut", y se aplica de forma diferente a expresiones AND y OR.

En el caso de operaciones AND, consiste en que si la primera expresión evaluada es false, la segunda si siquiera se evalúa, ya que el resultado será siempre false independientemente del valor del segundo operando.

En el caso de operaciones OR, si la primera expresión sea true, la segunda no se evalúa, ya que el resultado será siempre true, independientemente del valor de la segunda expresión.

Esto es porque en una operación && el resultado sólo puede ser true cuando los dos operandos sean true, y en una operación || el resultado sólo puede ser false si ambos operandos son false. En el momento en que en una expresión AND uno de los operandos sea false, o que en una expresión OR uno de los operandos sea true, el valor del otro operando es irrelevante.

Si tenemos en cuenta este comportamiento, podremos ahorrar tiempo de ejecución si colocamos en primer lugar la expresión más fácil de calcular, o aquella cuyo valor sea más probablemente false en el caso de una expresión AND o true, para una expresión OR.

También habrá casos en que una de las expresiones sea indeterminada cuando la otra sea false en una expresión AND, o true en una expresión OR. En ese caso, será preferible colocar la expresión potencialmente indeterminada en el segundo lugar.

Tablas de verdad

Una tabla de verdad es una relación de todos los posibles valores para los operandos que intervienen en una operación, y los resultados para cada caso.

En el caso de operadores lógicos podemos mostrar fácilmente tablas de verdad, ya que el dominio para cada operando es muy reducido: true o false. Si además tenemos en cuenta la regla del cortocircuito, los casos posibles se reducen todavía más.

A continuación se muestra la tabla de verdad del operador &&:

Expresión1 Expresión2 Expresión1 && Expresión2
false ignorada false
true false false
true true true

La tabla de verdad para el operador || es:

Expresión1 Expresión2 Expresión1 || Expresión2
false false false
false true true
true ignorada true

La tabla de verdad para el operador ! es:

Expresión !Expresión
false true
true false

Expresiones lógicas frecuentes

A menudo aprovechamos ciertas equivalencias entre enteros y valores lógicos para comprobar algunos valores especiales en comparaciones usadas en condiciones o bucles.

Concretamente, me refiero a las comparaciones con cero. Así, si queremos saber si un valor entero E, es distinto de cero, lo comparamos usando el operador !=: 0!=E.

Pero existe una correspondencia entre todos los valores enteros y los valores booleanos, y esa correspondencia es muy simple: un valor entero nulo esquivale a false, cualquier valor entero distinto de cero, equivale a true.

Teniendo esto en cuenta, la comparación anterior es inncecesaria, ya que 0!=E es equivalente a usar, sencillamente E.

La condición contraria, 0==E, es por lo tanto equivalente a lo contrario, es decir, a la expresión !E.

Será pues, muy frecuente, que encuentres este tipo de operadores lógicos aplicados a enteros en condiciones:

if(!E) {...} // Si E es cero, hacer ...
if(E) {...} // Si E no es cero, hacer...

Operador "sizeof"

Este operador tiene dos usos diferentes.

Sintaxis:

sizeof (<expresión>)
sizeof (nombre_de_tipo)

En ambos casos el resultado es una constante entera que da el tamaño en bytes del espacio de memoria usada por el operando, que es determinado por su tipo. El espacio reservado por cada tipo depende de la plataforma.

En el primer caso, el tipo del operando es determinado sin evaluar la expresión, y por lo tanto sin efectos secundarios. Si el operando es de tipo char, el resultado es 1.

A pesar de su apariencia, sizeof() NO es una función, sino un OPERADOR.

Asociación de operadores binarios

Cuando decimos que un operador es binario no quiere decir que sólo se pueda usar con dos operandos, sino que afecta a dos operandos. Por ejemplo, la línea:

A = 1 + 2 + 3 - 4;

Es perfectamente legal, pero la operación se evaluará tomando los operandos dos a dos y empezando por la izquierda, y el resultado será 2. Además hay que mencionar el hecho de que los operadores tienen diferentes pesos, es decir, se aplican unos antes que otros, al igual que hacemos nosotros, por ejemplo:

A = 4 + 4 / 4;

Dará como resultado 5 y no 2, ya que la operación de división tiene prioridad sobre la suma. Esta propiedad de los operadores es conocida como precedencia. En el capítulo de operadores II se verán las precedencias de cada operador, y cómo se aplican y se eluden estas precedencias.

Del mismo modo, el operador de asignación también se puede asociar:

A = B = C = D = 0;

Este tipo de expresiones es muy frecuente en C++ para asignar el mismo valor a varias variables, en este caso, todas las variables: A, B, C y D recibirán el valor 0.

Generalización de cortocircuitos

Generalizando, con expresiones AND con más de dos operandos, la primera expresión false interrumpe el proceso e impide que se continúe la evaluación del resto de las operaciones.

De forma análoga, con expresiones OR con más de dos operandos, la primera expresión true interrumpe el proceso e impide que se continúe la evaluación del resto de las operaciones.

Palabras reservadas usadas en este capítulo

sizeof.