Performing Logical Operations - Getting Started with C++ Programming - C++ For Dummies (2014)

C++ For Dummies (2014)

Part I

Getting Started with C++ Programming

Chapter 4

Performing Logical Operations

In This Chapter

arrow Using sometimes-illogical logical operators

arrow Defining logical variables

arrow Operating with bitwise logical operators logically, a bit at a time

The most common statement in C++ is the expression. Most expressions involve the arithmetic operators, such as addition (+), subtraction (-) and multiplication (*), as demonstrated in Chapter 3.

This chapter describes a whole other class of operators known as the logical operators. In comparison with the arithmetic operators, most people don’t think nearly as much about this type of operation. It isn’t that people don’t deal with logical operations such as AND and OR — we compute them constantly. I won’t eat cereal unless the bowl contains cereal AND the bowl has milk in it AND the cereal is coated with sugar (lots of sugar). I’ll have a Scotch IF it’s single-malt AND someone else is paying for it. People use such logical operations all the time, but they don’t write them down as machine instructions (or think of them in that light).

Logical operators fall into two types. The AND and OR operators are what I will call simple logical operators. The second type of logical operator is the bitwise operator. People don’t use the bitwise operator in their daily business at all; it’s unique to the computer world. We’ll start with the simple and sneak up on the bitwise in this chapter.

Why Mess with Logical Operations?

C++ programs have to make decisions. A program that can’t make decisions is of limited use. The temperature-conversion program laid out in Chapter 1 is about as complex as you can get without some type of decision-making. Invariably a computer program gets to the point where it has to figure out situations such as “Do this if the a variable is less than some value; do that other thing if it’s not.” The ability to make decisions is what makes a computer appear to be intelligent. (By the same token, that same property makes a computer look really stupid when the program makes the wrong decision.) Making decisions, right or wrong, requires the use of logical operators.

Using the Simple Logical Operators

The simple logical operators, shown in Table 4-1, evaluate to true or false.

Table 4-1 Simple Operators Representing Daily Logic

Operator

What It Does

==

Equality; true if the left-hand argument has the same value as the right

!=

Inequality; opposite of equality

>, <

Greater than, less than; true if the left-hand argument is greater than or less than the right-hand argument

>=, <=

Greater than or equal to, less than or equal to; true if either > or == is true, or either < or == is true

&&

AND; true if both the left- and right-hand arguments are true

||

OR; true if either the left- or right-hand argument is true

!

NOT; true if its argument is false; otherwise, false

The first six entries in Table 4-1 are comparison operators. The equality operator is used to compare two numbers. For example, the following is true if the value of n is 0, and is false otherwise:

n == 0;

image Looks can be deceiving. Don’t confuse the equality operator (==) with the assignment operator (=). Not only is this a common mistake, but it’s a mistake that the C++ compiler generally cannot catch — that makes it more than twice as bad. The following statement does not initialize n to 0; it compares the current value of n with 0 and then does nothing with the results of that comparison:

n == 0; // programmer meant to say n = 0

The greater-than (>) and less-than (<) operators are similarly common in everyday life. The following logical comparison is true:

int n1 = 1;
int n2 = 2;
n1 < n2;

The greater-than-or-equal-to operator (>=) and the less-than-or-equal-to operator (<=) are similar to the less-than and greater-than operators, with one major exception. They include equality; the other operators don’t.

The && (AND) and || (OR) work in combination with the other logic operators to build more complex logical expressions, like this:

// the following is true if n2 is greater than n1
// AND n2 is smaller than n3
// (this is the most common way determining that n2 is in
// the range of n1 to n3, exclusive)
(n1 < n2) && (n2 < n3);

Storing logical values

The result of a logical operation can be assigned to a variable of type bool. The term bool refers to Boolean algebra, which is the algebra of logic. This was invented by a British mathematician, George Boole, in the 19th century.

int n1 = 1;
int n2 = 2;
bool b;
b = (n1 == n2);

This expression highlights the difference between the assignment operator = and the comparison operator ==. The expression says, “Compare the variables n1 and n2. Store the results of this comparison in the variable b.

The following BoolTest program demonstrates the use of a bool variable:

// BoolTest - compare variables input from the
// keyboard and store the results off
// into a logical variable
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

int main(int nNumberofArgs, char* pszArgs[])
{
// set output format for bool variables
// to true and false instead
// of 1 and 0
cout.setf(cout.boolalpha);

// input two values
int nArg1;
cout << "Input value 1: ";
cin >> nArg1;

int nArg2;
cout << "Input value 2: ";
cin >> nArg2;

// compare the two variables and store the results
bool b;
b = nArg1 == nArg2;

cout << "The statement, " << nArg1
<< " equals " << nArg2
<< " is " << b
<< endl;

// wait until user is ready before terminating program
// to allow the user to see the program results
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
return 0;
}

The first line cout.setf() makes sure that our bool variable b is output as “true” or “false”. The next section explains why this is necessary.

The program inputs two values from the keyboard and displays the result of the equality comparison:

Input value 1: 5
Input value 2: 5
The statement, 5 equals 5 is true
Press Enter to continue...

image The special value endl inserts a newline. The difference between the value endl and the character ‘\n’ as described in Chapter 2 is subtle and explained in Chapter 23.

Using logical int variables

C++ hasn’t always had a bool type variable. Back in the old days (when cameras still used actual film), C++ used int variables to store logical values. A value of 0 was considered false and all other values true. By the same token, a logical operator generated a 0 for false and a 1 for true.(Thus, 10 < 5 returned 0 while 10 > 5 returned 1.)

C++ retains a high degree of compatibility between bool and int to support the older programs. You get completely different output from the BitTest program if you remove the line cout.setf(cout.boolalpha):

Input value 1: 5
Input value 2: 5
The statement, 5 equals 5 is 1
Press Enter to continue...

Variables of type int and bool can be mixed in expressions as well. For example, C++ allows the following bizarre statement without batting an eyelid:

int n;
n = (nArg1 == nArg2) * 5;

This sets n to 5 if nArg1 and nArg2 are equal and 0 otherwise.

Be careful performing logical operations on floating-point variables

Round-off errors in floating-point computation can create havoc with logical operations. Consider the following example:

float f1 = 10.0;
float f2 = f1 / 3;
bool b1 = (f1 == (f2 * 3.0)); // are these two equal?

Even though it’s obvious to us that f1 is equal to f2 times 3, the resulting value of b1 is not necessarily true. A floating-point variable cannot hold an unlimited number of significant digits. Thus, f2 is not equal to the number we’d call “three-and-a-third,” but rather to 3.3333 …, stopping after some number of decimal places.

image A float variable supports about 7 digits of accuracy while a double supports a skosh over 16 digits. I say “about” and “skosh” because the computer is likely to generate a number like 3.3333347 due to vagaries in floating-point calculations.

Now, in pure math, the number of 3s after the decimal point is infinite, but no computer built can handle an infinite number of digits. So, after multiplying 3.3333 by 3, you get 9.9999 instead of the 10 you’d get if you multiplied “three-and-a-third” — in effect, a round-off error. Such small differences may be unnoticeable to a person but not to the computer. Equality means exactly that — exact equality.

Modern processors are sophisticated in performing such calculations. The processor may, in fact, accommodate the round-off error, but from inside C++, you can’t predict exactly what any given processor will do.

The safer comparison follows:

float f1 = 10.0;
float f2 = f1 / 3;
float f3 = f2 * 3.0;
float delta = f1 - f3;
bool bEqual = -0.0001 < delta && delta < 0.0001;

This comparison is true if f1 and f3 are within some small delta from each other, which should still be true even if you take some small round-off error into account.

Short circuits and C++

The logical AND && and logical OR || operators perform what is called short-circuit evaluation. Consider the following:

condition1 && condition2

If condition1 is not true, the overall result is not true, no matter what the value of condition2. (For example, condition2 could be true or false without changing the result.) The same situation occurs in the following:

condition1 || condition2

If condition1 is true, the result is true, no matter what the value of condition2 is.

To save time, C++ doesn’t evaluate condition2 if it doesn’t need to. For example, in the expression condition1 && condition2, C++ doesn’t evaluate condition2 if condition1 is false. Likewise, in the expression condition1 || condition2, C++ doesn’t evaluate condition2 if condition1 istrue. This is known as short-circuit evaluation.

image Short-circuit evaluation may mean that condition2 is not evaluated even if that condition has side effects. Consider the following admittedly contrived code snippet:

int nArg1 = 1;
int nArg2 = 2;
int nArg3 = 3;

bool b = (nArg1 > nArg2) && (nArg2++ > nArg3);

The variable nArg2 is never incremented because the comparison nArg2++ > nArg3 is not performed. There’s no need because nArg1 > nArg2 already returned a false so the overall expression must be false.

Expressing Binary Numbers

C++ variables are stored internally as so-called binary numbers. Binary numbers are stored as a sequence of 1 and 0 values known as bits. Most of the time, you don’t really need to deal with which particular bits you use to represent numbers. Sometimes, however, it’s practical and convenient to tinker with numbers at the bit level — so C++ provides a set of operators for that purpose.

image Fortunately, you won’t have to deal too often with C++ variables at the bit level, so it’s pretty safe to consider the remainder of this chapter a Deep Techie excursion.

The so-called bitwise logical operators operate on their arguments at the bit level. To understand how they work, let’s first examine how computers store variables.

The decimal number system

The numbers we’ve been familiar with from the time we could first count on our fingers are known as decimal numbers because they’re based on the number 10. (If beer by the six-pack had been invented early enough, our number system might well be based on the number 6.) In general, the programmer expresses C++ variables as decimal numbers. Thus you could specify the value of var as (say) 123, but consider the implications.

A number such as 123 refers to 1 * 100 + 2 * 10 + 3 * 1. All of these base numbers — 100, 10, and 1 — are powers of 10.

123 = 1 * 100 + 2 * 10 + 3 * 1

Expressed in a slightly different (but equivalent) way, 123 looks like this:

123 = 1 * 102 + 2 * 101 + 3 * 100

Remember that any number to the zero power is 1.

Other number systems

Well, okay, using 10 as the basis (or base) of our counting system probably stems from those 10 human fingers, the original counting tools. An alternative base for a counting system could just as easily have been 20 (maybe the inventor of base 10 had shoes on at the time).

If our numbering scheme had been invented by dogs, it might well be based on 8 (one digit of each paw is out of sight on the back part of the leg). Mathematically, such an octal system would have worked just as well:

12310 = 1 * 82 + 7 * 81 + 3 * 80 = 1738

The small 10 and 8 here refer to the numbering system, 10 for decimal (base 10) and 8 for octal (base 8). A counting system may use any positive base.

The binary number system

Computers have essentially two fingers. (Maybe that’s why computers are so stupid: without an opposing thumb, they can’t grasp anything. And then again, maybe not.) Computers prefer counting using base 2. The number 12310 would be expressed this way:

12310 = 0*27 + 1*26 + 1*25 + 1*24 + 1*23 + 0*22 + 1*21 + 1*20
12310 = 0*128 + 1*64 + 1*32 + 1*16 + 1*8 + 0*4 + 1*2 + 1*1
= 011110112

Computer convention expresses binary numbers by using 4, 8, 16, 32, or even 64 binary digits, even if the leading digits are 0. This is also because of the way computers are built internally.

Because the term digit refers to a multiple of 10, a binary digit is called a bit (an abbreviation of binary digit). A byte is made up of 8 bits. (Calling a binary digit a byte-it didn’t seem like a good idea.) Memory is usually measured in bytes (like rolls are measured in units of baker's dozen).

With such a small base, you have to use a large number of bits to express numbers. Human beings don’t want the hassle of using an expression such as 011110112 to express such a mundane value as 12310. Programmers prefer to express numbers by using an even number of bits. The octal system — which is based on 3 bits — was the default binary system in the early days of C. We see a vestige of this even today — a constant that begins with a 0 is assumed to be octal in C++. Thus, the line:

cout << "0173 = " << 0173 << endl;

produces the following output:

0173 = 123

However, octal has been almost completely replaced by the hexadecimal system, which is based on 4-bit digits.

Hexadecimal uses the same digits for the numbers 0 through 9. For the digits between 9 and 16, hexadecimal uses the first six letters of the alphabet: A for 10, B for 11, and so on. Thus, 12310 becomes 7B16, like this:

123 = 7 * 161 + B (i.e. 11) * 160 = 7B16

Programmers prefer to express hexadecimal numbers in multiples of 4 hexadecimal digits even when the leading digit in each case is 0.

Finally, who wants to express a hexadecimal number such as 7B16 by using a subscript? Terminals don’t even support subscripts. Even on a word processor such as the one I’m using now, it’s a drag to change fonts to and from subscript mode just to type two lousy digits. Therefore, programmers (no fools, they) use the convention of beginning a hexadecimal number with a 0x. (Why? Well, the reason for such a strange convention goes back to the early days of C, in a galaxy far, far, away … never mind.) Thus, 7B becomes 0x7B. Using this convention, the hexadecimal number 0x7B is equal to 123 decimal while 0x123 hexadecimal is equal to 291 decimal. The code snippet

cout << "0x7B = " << 0x7B << endl;
cout << "0x123 = " << 0x123 << endl;

produces the following output:

0x7B = 123
0x123 = 291

You can use all the mathematical operators on hexadecimal numbers in the same way you’d apply them to decimal numbers. (Well, okay, most of us can’t perform a multiplication such as 0xC * 0xE in our heads, but that has more to do with the multiplication tables we learned in school than it has to do with any limitation in the number system.)

image If you really want to, you can write binary numbers in C++ '14 using the prefix ‘0b'. Thus, 123 becomes 0b01111011.

Performing Bitwise Logical Operations

All C++ numbers can be expressed in binary form. Binary numbers use only the digits 1 and 0 to represent a value. Table 4-2 defines the set of operations that work on numbers one bit at a time, hence the term bitwise operators.

Table 4-2 Bitwise Operators

Operator

Function

~

NOT: toggle each bit from 1 to 0 and from 0 to 1

&

AND each bit of the left-hand argument with that on the right

|

OR each bit of the left-hand argument with that on the right

^

XOR (exclusive OR) each bit of the left-hand argument with that on the right

Bitwise operations can potentially store a lot of information in a small amount of memory. Many traits in the world have only two possibilities — that are either this way or that way. You are either married or you’re not. You are either male or female (at least that’s what my driver’s license says). In C++, you can store each of these traits in a single bit — in this way, you can pack 32 separate binary properties into a single 32-bit int.

In addition, bit operations can be extremely fast. No performance penalty is paid for that 32-to-1 savings.

image Even though memory is cheap these days, it’s not unlimited. Sometimes, when you’re storing large amounts of data, this ability to pack a whole lot of properties into a single word is a big advantage.

The single-bit operators

The bitwise operators — AND (&), OR (|) and NOT (~) — perform logic operations on single bits. If you consider 0 to be false and 1 to be true (it doesn’t have to be this way, but it’s a common convention), you can say things like the following for the NOT operator:

~ 1 (true) is 0 (false)
~ 0 (false) is 1 (true)

The AND operator is defined as following:

1 (true) & 1 (true) is 1 (true)
1 (true) & 0 (false) is 0 (false)

It’s a similar situation for the OR operator:

1 (true) | 0 (false) is 1 (true)
0 (false) | 0 (false) is 0 (false)

The definition of the AND operator appears in Table 4-3. Read one argument as the column head and the other argument as the row head — the result is the intersection. Thus, 1 AND 1 is 1. 0 AND 1 is 0.

Table 4-3 Truth Table for the AND Operator

AND

1

0

1

1

0

0

0

0

You read Table 4-3 as the column corresponding to the value of one of the arguments while the row corresponds to the other. Thus, 1 & 0 is 0. (Column 1 and row 0.) The only combination that returns anything other than 0 is 1 & 1. (This is known as a truth table.)

Similarly, the truth table for the OR operator is shown in Table 4-4.

Table 4-4 Truth Table for the OR Operator

OR

1

0

1

1

1

0

1

0

One other logical operation that is not so commonly used in day-to-day living is the OR ELSE operator, commonly contracted to XOR. XOR is true if either argument is true but not if both are true. The truth table for XOR is shown in Table 4-5.

Table 4-5 Truth Table for the XOR Operator

XOR

1

0

1

0

1

0

1

0

Armed with these single-bit operators, we can take on the C++ bitwise logical operations.

Using the bitwise operators

The bitwise operators are used much like any other binary arithmetic operator. The NOT operator is the easiest to understand. To NOT a number is to NOT each bit that makes up that number (and to a programmer, that sentence makes perfect sense — honest). Consider this example:

~01102 (0x6)
10012 (0x9)

Thus we say that ~0x6 equals 0x9 (pronounced “NOT 6 equals 9”).

The following calculation demonstrates the & operator:

01102
&
00112
00102

Beginning with the most significant bit, 0 AND 0 is 0. In the next bit, 1 AND 0 is 0. In bit 3, 1 AND 1 is 1. In the least significant bit, 0 AND 1 is 0. Expressed in hexadecimal, the same expression appears as follows:

0x6 01102
& &
0x3 00112
0x2 00102

In shorthand, we say that 0x6 & 0x3 equals 0x2 (pronounced “6 AND 3 equals 2”).

A simple test

The following program illustrates the bitwise operators in action. The program initializes two variables and outputs the result of ANDing, ORing, and XORing them:

// BitTest - initialize two variables and output the
// results of applying the ~,& , | and ^
// operations
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

int main(int nNumberofArgs, char* pszArgs[])
{
// set output format to hexadecimal
cout.unsetf(cout.dec);
cout.setf(cout.hex);

// initialize two arguments
int nArg1 = 0x78ABCDEF;
int nArg2 = 0x12345678;

// now perform each operation in turn
// first the unary NOT operator
cout << " nArg1 = 0x" << nArg1 << endl;
cout << "~nArg1 = 0x" << ~nArg1 << "\n" << endl;
cout << " nArg2 = 0x" << nArg2 << endl;
cout << "~nArg2 = 0x" << ~nArg2 << "\n" << endl;

// now the binary operators
cout << " 0x" << nArg1 << "\n"
<< "& 0x" << nArg2 << "\n"
<< " ----------" << "\n"
<< " 0x" << (nArg1 & nArg2) << "\n"
<< endl;

cout << " 0x" << nArg1 << "\n"
<< "| 0x" << nArg2 << "\n"
<< " ----------" << "\n"
<< " 0x" << (nArg1 | nArg2) << "\n"
<< endl;

cout << " 0x" << nArg1 << "\n"
<< "^ 0x" << nArg2 << "\n"
<< " ----------" << "\n"
<< " 0x" << (nArg1 ^ nArg2) << "\n"
<< endl;

// wait until user is ready before terminating program
// to allow the user to see the program results
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
return 0;
}

The first two expressions in our program, cout.unsetf(ios::dec) and cout.setf(ios::hex), changes the default output format from decimal to hexadecimal. (You’ll have to trust me until Chapter 23 that it works.)

The remainder of the program is straightforward. The program assigns nArg1 the test value 0x78ABCDEF and nArg2 the value 0x12345678. The program then outputs all combinations of bitwise calculations. The extra newlines, such as in the following line, cause a blank line to appear to help group the output to make it easier to read:

cout << "~nArg1 = 0x" << ~nArg1 << "\n" << endl;

The output appears as follows:

nArg1 = 0x78abcdef
~nArg1 = 0x87543210

nArg2 = 0x12345678
~nArg2 = 0xedcba987

0x78abcdef
& 0x12345678
----------
0x10204468

0x78abcdef
| 0x12345678
----------
0x7abfdfff

0x78abcdef
^ 0x12345678
----------
0x6a9f9b97

Press Enter to continue...

You can convert each of the digits into binary to check the bitwise arithmetic. For example, from the first digit of each of the examples, you can see that 7 & 1 equals 1, 7 | 1 equals 7, and 7 ^ 1 equals 6.

Running through simple and bitwise logical calculations in your head at parties is fun (well, okay, for some of us), but a program has to make actual, practical use of these values to make them worth the trouble. Coming right up: Chapter 5 demonstrates how logical calculations are used to control program flow.