In This Chapter
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.
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.
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; |
| Inequality; opposite of equality |
| Greater than, less than; |
| Greater than or equal to, less than or equal to; |
| AND; |
| OR; |
| NOT; |
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;
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);
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 nineteenth 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 them and store the result 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 system("PAUSE"); 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 any key to continue ...
The special value endl
inserts a newline. The difference between the value endl
and the character '
'
as described in Chapter 2 is subtle and explained in Chapter 23.
C++ hasn't always had a bool
type variable. Back in the old days (before that guy on TV kept walking around saying, "Can you hear me now?"), 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, 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 any key 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.
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.
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 infinity. 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.
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
is true
. This is known as short-circuit evaluation.
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
.
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.
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 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.
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.
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.)
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.
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.
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 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.
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.
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.
Armed with these single-bit operators, we can take on the C++ bitwise logical operations.
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:
Thus we say that ~0x6
equals 0x9
(pronounced "NOT 6 equals 9").
The following calculation demonstrates the & operator:
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:
In shorthand, we say that 0x6 & 0x3
equals 0x2
(pronounced "6 AND 3 equals 2").
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 << " " << endl; cout << " nArg2 = 0x" << nArg2 << endl; cout << "~nArg2 = 0x" << ~nArg2 << " " << endl; // now the binary operators cout << " 0x" << nArg1 << " " << "& 0x" << nArg2 << " " << " ----------" << " " << " 0x" << (nArg1 & nArg2) << " " << endl; cout << " 0x" << nArg1 << " " << "| 0x" << nArg2 << " " << " ----------" << " " << " 0x" << (nArg1 | nArg2) << " " << endl; cout << " 0x" << nArg1 << " " << "^ 0x" << nArg2 << " " << " ----------" << " " << " 0x" << (nArg1 ^ nArg2) << " " << endl; // wait until user is ready before terminating program // to allow the user to see the program results system("PAUSE"); 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 << " " << endl;
The output appears as follows:
nArg1 = 0x78abcdef ~nArg1 = 0x87543210 nArg2 = 0x12345678 ~nArg2 = 0xedcba987 0x78abcdef & 0x12345678 ---------- 0x10204468 0x78abcdef | 0x12345678 ---------- 0x7abfdfff 0x78abcdef ^ 0x12345678 ---------- 0x6a9f9b97 Press any key 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.
3.133.122.127