ECE-1021

Logical Operators

(Last Mod: 27 November 2010 21:38:40 )

ECE-1021 Home



Objectives


Overview

These operators perform the standard Boolean logic operations. The operands for these operators are treated as logical values according to the following mapping:

The above statements mean precisely what they say - any value that is not exactly zero is a logical True.

If k is an integer variable then all of the following values are considered a logical True:

Operation Operator Level Assoc Syntax Evaluates to
NOT ! 2 L !A O if FALSE

1 if TRUE

AND && 11 R A && B
OR || 12 A || B

 


Guaranteed Short Circuiting

Unlike most of the other binary operators, the order in which the operands of the logical-AND and logical-OR are evaluated - and even if they are evaluated - is explicitly laid out in the C Language Standard. The rule is very simple. The left operand is evaluated and if, based only on that operand, the result of the logical operation is known, then it is guaranteed that the right operand will not even be evaluated. This is known as guaranteed short circuiting and it is extremely useful in many situations - but it is also a potential trap if the programmer is unaware of it.

Example 1: Avoiding division by zero:

m = 12;

k = 20;

n = 10;

while ((k != 0) && (m/k < n))

{

    putc('.', stdout);

    k--;

}

When the loop is first entered, k is not equal to zero and m/k is zero which is less than ten. The loop then prints a period on the screen and decrements k. This continues until the test fails at which point we will have one dot on the screen for each time the loop executed. This is a handy debugging method which here is just present to make the loop do something.

So how many dot's will be printed? When k is equal to two, the result of m/k will be 6 which will still pass the test and at this point it will have printed a total of 19 dots on the screen. After decrementing k the result of m/k will be 12 which will now fail the test and the loop will exit without printing another dot.

But what if the value of n had been 15? It would have passed this test, printed a twentieth dot, decremented n making it zero, and re-evaluated the test. If both operands get evaluated, then we must evaluate m/k which will result in division by zero and most likely cause our program to crash (can we say, "Floating point exception" and/or "Division by zero"?). But because k being zero caused the first operand to be False and because if either operating of a logical-AND is False then the result will be False regardless of the value of the other operand, C guarantees us that the right hand operand will not be evaluated at all - meaning that we never evaluate the expression m/k and hence don't crash our program.

Many C texts do not even mention the concept of guaranteed short circuiting believing that it is an advanced topic that only advanced programmer's wanting to take advantage of this subtle rule need be concerned about. They are wrong in that belief. It is true that, as long as you do not put any code in the right operand that produces side effects, that you can spend your entire life writing C programs and never have a hint that anything special is going on. But how can you know not to put side-effects in the right operand to a logical-AND or logical-OR unless you know that this issue exists? Consider the following code:

m = 0;

k = 0;

do

{

    putc('.', stdout);

    m += k;

} while ((m < 20) || (k++ < 4));

If we didn't know about guaranteed short circuiting, we would believe that the loop would execute a total of 7 times, print a total of 7 dots, and that m would be equal to 21 and n would be equal to 7 after the loop finished. We would probably be surprised to see it get trapped in an infinite loop printing out dot after dot after dot. If we were to put in statements to print out the values of m and k on each pass, we would be puzzled to discover that m and k never change!

Were we to initialize k to one instead of zero, we would find that the loop executes 23 times and that at the end m is equal to 29 and k is equal to 5.

The reason is that the right operand - the one that increments k as a side effect, only executes if the left operand is False. In the code as given above, k won't increment until after m is no longer less than 20 but, since k is initially zero, the value of m never changes and so k is never incremented.

If you didn't know about guaranteed short circuiting, imagine how long you would stare at this code trying to figure out why it wasn't working the way you thought it should.


Checking Equality

What value will be written to the variable m in the following code fragment?

m = 10;

 

if ( m = 0 )

    k = 1000;

else

    k = 100 / m;

If you said 10, you are wrong.

If you said 1000, you are wrong.

If you are now scratching your head saying that it must be one or the other, you are wrong.

You are reading the text expression in the if() statement based on what you think it says, not on what is actually written there. The test expression is not checking to see if m is presently equal to zero. The operator in that expression is not the equality operator - it is the assignment operator. So it sets the value of m equal to zero and since assignment operations evaluate to the value assigned, it evaluates as zero (i.e., False) and executes the else code which divides 100 by the new value of m, which is zero. So our program crashes because of a division-by-zero error.

We might spend a great deal of time trying to figure out why it is crashing because we see an expression that is comparing m to zero because that is how would interpret it. But that is not what we told the compiler to do - and what we told the compiler to do is perfectly legal and it was perfectly willing to do exactly what we told it to do.

This is a sufficiently common error that most compilers will issue a warning when they see an assignment operation in a context where an equality comparison is more reasonable - but one programmer's mistake is another programmer's trick and the compiler cannot read our minds.

So, aside from developing the habit of never ignoring compiler warnings even if the code does compile, you can employ a little trick that will turn this ever-so-common mistake from a compiler warning to a compiler error.

Consider this slightly modified version:

m = 10;

 

if ( 0 = m )

    k = 1000;

else

    k = 100 / m;

We have no told the compiler make the constant 0 equal to the value presently stored in m. Since this isn't ForTran, it doesn't know how to do that and throws an error and tells us that the line containing that expression has an illegal operation or syntax error and we will almost immediately spot the problem and fix it.

A good rule to use is to, whenever possible, place a constant or other expression that is not an lvalue on the left side of the equality operator. So if you are seeing if k is equal to the difference between m and n, you should write:

if ( m-n == k )

If you are checking for equality between k and m, you can still use this rule by doing the following:

if ( k+0 == m )

The compiler will almost certainly optimize out the addition of zero and so you will end up with no performance penalty. In truth, few experienced programmer's take things to this extreme - but that is because they have spent enough hours tracking down these mistakes that, while they still make them, they have gotten pretty fast at tracking them down.

Many new programmers refuse to write the constants on the left of the equality operator because "it looks strange". No question about it, it does. At least until you get used to it, which doesn't take nearly as long as you might think. Many of these same programmers then wonder why their instructor and/or boss is not terribly sympathetic after they've wasted several hours figuring out why their program is crashing after opening a perfectly good file and presumably verifying that the file actually opened successfully when, in the process of doing the check, they overwrote their file pointer, guaranteed that they would not trap the bad pointer, and then proceeded to try to interact with a file structure that doesn't exist.


Checking Ranges

What would a code fragment look like that prints a period if the value of k is a two digit integer?

A two digit integer requires: 10 <= k < 100

Many new programmers will then write their code as follows:

if ( 10 <= k < 100 )

    putc('.', stdout);

Which will compile, run, and do exactly what you told it to do - print out a period regardless of the value of k.

This is another common logic mistake and, unlike using the assignment operator instead of the equality operator, the compiler will almost certainly issue no warning.

If (when?) you write something like this, don't look at it in terms of what you want the program to do. Instead, look at what you told it to do. Obviously they aren't the same thing otherwise you would be seeing periods when k is equal to 50,000 and, as hard as it might be to accept, the compiler doesn't care about what you wanted your program to do. It only cares about what you told it to do.

The test expression above has two operators, both have the same precedence level and that level is left associative. That means that the order of operations will evaluate the expression as follows:

if ( ( 10 <= k ) < 100 )

    putc('.', stdout);

The inner expression will evaluate either to a 0 or a 1, both of which are less than 100. So the outer expression will always pass.

The correct way to write this expression is as follows:

if ( (10 <= k) && (k < 100) )

    putc('.', stdout);


Checking Multiple Values

A function asks the User for a response and returns the character code of the first key they pressed. You want to continue calling this function as long as the person enters anything other than 'Y' or 'N'. How can you do this?

Many new programmers will writing something like:

do

{

    c = UsersResponse();

} while (c != 'Y' || 'N');

 

And will act surprised when their program keeps asking the User for a response regardless of what they enter. Why?

Once again, should this happen to you don't look at what you wanted it to do, look at what you told it to do.

There are two operators and the inequality operator has precedence level 7 while the logical-OR has precedence level 12. So the following is the implied grouping:

do

{

    c = UsersResponse();

} while ( (c != 'Y') || 'N');

 

The left hand side is either True of False. If the User enters anything other than a 'Y' it will be True and, thanks to guaranteed short circuiting, will not even evaluate the other operand. At this point, people go, "Aha!" and modify the code as follows:

do

{

    c = UsersResponse();

} while ( c != ('Y' || 'N') );

 

And now are perplexed when the same thing happens - it continues to ask the User for a response. Why.

Again, look at what the compiler was told to do. The inner expression is a perfectly good logical-OR expression between two perfectly good integer constants that will evaluate to either a 0 or a 1. As long as one of those constants is not zero, the result will be a 1 - and it's a pretty safe bet, whether we are using ASCII or some other set, that those two characters will have different codes and hence at least one of them will be non-zero. As an aside, we know that both will be non-zero because the C Language Standard reserves the code 0 for the NUL character - the only character code that is explicitly required to have a specific value.

While it is generally possible to enter ASCII control codes directly from the keyboard (there is, after all, a reason that the control key exists and why it is called the control key) we can pretty reasonable assume that the User is not doing this and hence they are not able to enter the only character code that would get them out of the loop.

Here is the correct way to write this code:

do

{

    c = UsersResponse();

} while ( (c != 'Y') && (c != 'N') );

 

Be sure you understand why we switched to a logical-AND operator. What would have happened had we stayed with the logical-OR?


Using Logical Results as Arithmetic Values

Although we generally interpret the result of logical expressions as being True or False, the fact remains that they are integers and can be used as integers. Consider the following examples:

Example 1: Count the number integer squares between 1 and 100 that are divisible by n.

count = 0;

for(i = 1; i <= 100; i++)

    count += !((i*i) % n);

If the modulus of one number with respect to second number is zero, then that means that the first number is evenly divisible by the second number - this is a direct result of the fact that the modulus is the remainder after integer division. We want to count those that are divisible by n and so we take the logical NOT of the modulus result which will give us a 1 if the square is divisible by n and 0 if it is not.


Logical Operations on Floating Point Values

If x is a floating point variable then all of the following are True values:

This last example illustrates a common problem with performing logical operations on non-integer values. Such a tiny value is very possibly the result of round-off errors and the result very likely should have been zero. Since floating point representations cannot represent most values exactly, there is almost always a little bit of round-off error when computations are performed. The behavior of the logical operators are very clear - if the value is not exactly zero, then it is a logical True. Period. End of discussion. There is no "close enough" involved. It is zero or it is not.

As a result, logical operators should only be applied to floating point data types with extreme care and consideration. Consider this - even though an IEEE double precision floating point variable only carries approximately sixteen signification decimal digits of accuracy, it can represent a value that is different from zero even if the first non-zero digit is in the 315th decimal place.