MFC Programmer's SourceBook : Thinking in C++
Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Operators and their use

This section covers all the operators in C and C++.

All operators produce a value from their operands. This value is produced without modifying the operands, except with the assignment, increment and decrement operators. Modifying an operand is called a side effect. The most common use for operators that modify their operands is to generate the side effect, but you should keep in mind that the value produced is available for your use just as in operators without side effects.

Assignment

Assignment is performed with the operator =. It means “take the right-hand side (often called the rvalue) and copy it into the left-hand side (often called the lvalue). An rvalue is any constant, variable, or expression that can produce a value, but an lvalue must be a distinct, named variable (that is, there must be a physical space in which to store data). For instance, you can assign a constant value to a variable ( A = 4; ), but you cannot assign anything to constant value – it cannot be an lvalue (you can’t say 4 = A; ).

Mathematical operators

The basic mathematical operators are the same as the ones available in most programming languages: addition (+), subtraction ( -), division ( /), multiplication ( *) and modulus ( %, this produces the remainder from integer division). Integer division truncates the result (it doesn’t round). The modulus operator cannot be used with floating-point numbers.

C & C++ also use a shorthand notation to perform an operation and an assignment at the same time. This is denoted by an operator followed by an equal sign, and is consistent with all the operators in the language (whenever it makes sense). For example, to add 4 to the variable x and assign x to the result, you say: x += 4; .

This example shows the use of the mathematical operators:

//: C03:Mathops.cpp
// Mathematical operators
#include <iostream>
using namespace std;

// A macro to display a string and a value.
#define PRINT(STR, VAR) \
  cout << STR " = " << VAR << endl

int main() {
  int i, j, k;
  float u,v,w;  // Applies to doubles, too
  cout << "enter an integer: ";
  cin >> j;
  cout << "enter another integer: ";
  cin >> k;
  PRINT("j",j);  PRINT("k",k);
  i = j + k; PRINT("j + k",i);
  i = j - k; PRINT("j - k",i);
  i = k / j; PRINT("k / j",i);
  i = k * j; PRINT("k * j",i);
  i = k % j; PRINT("k % j",i);
  // The following only works with integers:
  j %= k; PRINT("j %= k", j);
  cout << "Enter a floating-point number: ";
  cin >> v;
  cout << "Enter another floating-point number:";
  cin >> w;
  PRINT("v",v); PRINT("w",w);
  u = v + w; PRINT("v + w", u);
  u = v - w; PRINT("v - w", u);
  u = v * w; PRINT("v * w", u);
  u = v / w; PRINT("v / w", u);
  // The following works for ints, chars, 
  // and doubles too:
  u += v; PRINT("u += v", u);
  u -= v; PRINT("u -= v", u);
  u *= v; PRINT("u *= v", u);
  u /= v; PRINT("u /= v", u);
} ///:~ 

The rvalues of all the assignments can, of course, be much more complex.

Introduction to preprocessor macros

Notice the use of the macro PRINT( ) to save typing (and typing errors!). Preprocessor macros are traditionally named with all uppercase letters, so they stand out – you’ll learn later that macros can quickly become dangerous (and they can also be very useful).

The arguments in the parenthesized list following the macro name are substituted in all the code following the closing parenthesis. The preprocessor removes the name PRINT and substitutes the code wherever the macro is called, so the compiler cannot generate any error messages using the macro name, and it doesn’t do any type checking on the arguments (the latter can be beneficial, as shown in the debugging macros at the end of the chapter).

Relational operators

Relational operators establish a relationship between the values of the operands. They produce a Boolean (specified with the bool keyword in C++) true if the relationship is true, and false if the relationship is false. The relational operators are: less than ( <), greater than ( >), less than or equal to ( <=), greater than or equal to ( >=), equivalent ( ==) and not equivalent ( !=). They may be used with all built-in data types in C and C++. They may be given special definitions for user-defined data types in C++ (you’ll learn about this in Chapter XX, on operator overloading).

Logical operators

The logical operators and ( &&) and or ( ||) produce a true or false based on the logical relationship of its arguments. Remember that in C and C++, a statement is true if it has a non-zero value, and false if it has a value of zero. If you print a bool, you’ll typically see a ‘ 1’ for true and ‘ 0’ for false.

This example uses the relational and logical operators:

//: C03:Boolean.cpp
// Relational and logical operators.
#include <iostream>
using namespace std;

int main() {
  int i,j;
  cout << "enter an integer: ";
  cin >> i;
  cout << "enter another integer: ";
  cin >> j;
  cout << "i > j is " << (i > j) << endl;
  cout << "i < j is " << (i < j) << endl;
  cout << "i >= j is " << (i >= j) << endl;
  cout << "i <= j is " << (i <= j) << endl;
  cout << "i == j is " << (i == j) << endl;
  cout << "i != j is " << (i != j) << endl;
  cout << "i && j is " << (i && j) << endl;
  cout << "i || j is " << (i || j) << endl;
  cout << " (i < 10) && (j < 10) is "
       << ((i < 10) && (j < 10))  << endl;
} ///:~ 

You can replace the definition for int with float or double in the above program. Be aware, however, that the comparison of a floating-point number with the value of zero is very strict: a number that is the tiniest fraction different from another number is still “not equal.” A floating-point number that is the tiniest bit above zero is still true.

Bitwise operators

The bitwise operators allow you to manipulate individual bits in a number (since floating point values use a special internal format, the bitwise operators only work with integral numbers). Bitwise operators perform boolean algebra on the corresponding bits in the arguments to produce the result.

The bitwise and operator ( &) produces a one in the output bit if both input bits are one; otherwise it produces a zero. The bitwise or operator ( |) produces a one in the output bit if either input bit is a one and only produces a zero if both input bits are zero. The bitwise exclusive or , or xor ( ^) produces a one in the output bit if one or the other input bit is a one, but not both. The bitwise not (~, also called the ones complement operator) is a unary operator – it only takes one argument (all other bitwise operators are binary operators). Bitwise not produces the opposite of the input bit – a one if the input bit is zero, a zero if the input bit is one.

Bitwise operators can be combined with the = sign to unite the operation and assignment: &=, |= and ^= are all legitimate operations (since ~ is a unary operator it cannot be combined with the = sign).

Shift operators

The shift operators also manipulate bits. The left-shift operator ( <<) produces the operand to the left of the operator shifted to the left by the number of bits specified after the operator. The right-shift operator ( >>) produces the operand to the left of the operator shifted to the right by the number of bits specified after the operator. If the value after the shift operator is greater than the number of bits in the left-hand operand, the result is undefined. If the left-hand operand is unsigned, the right shift is a logical shift so the upper bits will be filled with zeros. If the left-hand operand is signed, the right shift may or may not be a logical shift (that is, the behavior is undefined).

Shifts can be combined with the equal sign ( <<= and >>=). The lvalue is replaced by the lvalue shifted by the rvalue.

Here’s an example that demonstrates the use of all the operators involving bits:

//: C03:Bitwise.cpp
// Demonstration of bit manipulation
#include <iostream>
using namespace std;

// Display a byte in binary:
void printBinary(const unsigned char val) {
  for(int i = 7; i >= 0; i--)
    if(val & (1 << i))
      cout << "1";
    else
      cout << "0";
}

// A macro to save typing:
#define PR(STR, EXPR) \
  cout << STR; printBinary(EXPR); cout << endl;  

int main() {
  unsigned int getval;
  unsigned char a, b;
  cout << "Enter a number between 0 and 255: ";
  cin >> getval; a = getval;
  PR("a in binary: ", a);
  cout << "Enter a number between 0 and 255: ";
  cin >> getval; b = getval;
  PR("b in binary: ", b);
  PR("a | b = ", a | b);
  PR("a & b = ", a & b);
  PR("a ^ b = ", a ^ b);
  PR("~a = ", ~a);
  PR("~b = ", ~b);
  // An interesting bit pattern:
  unsigned char c = 0x5A; 
  PR("c in binary: ", c);
  a |= c;
  PR("a |= c; a = ", a);
  b &= c;
  PR("b &= c; b = ", b);
  b ^= a;
  PR("b ^= a; b = ", b);
} ///:~ 

The printBinary( ) function takes a single byte and displays it bit-by-bit. The expression (1 << i) produces a one in each successive bit position; in binary: 00000001, 00000010, etc. If this bit is bitwise anded with val and the result is nonzero, it means there was a one in that position in val.

Once again, a preprocessor macro is used to save typing. It prints the string of your choice, the the binary representation of an expression, then a newline.

In main( ), the variables are unsigned. This is because, generally, you don't want signs when you are working with bytes. An int must be used instead of a char for getval because the “ cin >> ” statement will otherwise treat the first digit as a character. By assigning getval to a and b, the value is converted to a single byte (by truncating it).

The << and >> provide bit-shifting behavior, but when they shift bits off the end of the number, those bits are lost (it’s commonly said that they fall into the mythical bit bucket , a place where discarded bits end up, presumably so they can be reused...). When manipulating bits you can also perform rotation, which means that the bits that fall off one end are inserted back at the other end, as if they’re being rotated around a loop. Even though most computer processors provide a machine-level rotate command (so you’ll see it in the assembly language for that processor), there is no direct support for “rotate” in C or C++. Presumably the designers of C felt justified in leaving “rotate” off (aiming, as they said, for a minimal language) because you can build your own rotate command. For example, here are functions to perform left and right rotations:

//: C03:Rotation.cpp {O}
// Perform left and right rotations

unsigned char rol(unsigned char val) {
  int highbit;
  if(val & 0x80) // 0x80 is the high bit only
    highbit = 1;
  else
    highbit = 0;
  // Left shift (bottom bit becomes 0):
  val <<= 1;
  // Rotate the high bit onto the bottom:
  val |= highbit;
  return val;
}

unsigned char ror(unsigned char val) {
  int lowbit;
  if(val & 1) // Check the low bit
    lowbit = 1;
  else
    lowbit = 0;
  val >>= 1; // Right shift by one position
  // Rotate the low bit onto the top:
  val |= (lowbit << 7);
  return val;
} ///:~ 

Try using these functions in Bitwise.cpp. Notice the definitions (or at least declarations) of rol( ) and ror( ) must be seen by the compiler in Bitwise.cpp before the functions are used.

The bitwise functions are generally extremely efficient to use because they translate directly into assembly language statements. Sometimes a single C or C++ statement will generate a single line of assembly code.

Unary operators

Bitwise not isn’t the only operator that takes a single argument. Its companion, the logical not ( !), will take a true value and produce a false value. The unary minus ( -) and unary plus ( +) are the same operators as binary minus and plus – the compiler figures out which usage is intended by the way you write the expression. For instance, the statement

x = -a;

has an obvious meaning. The compiler can figure out:

x = a * -b;

but the reader might get confused, so it is safer to say:

x = a * (-b);

The unary minus produces the negative of the value. Unary plus provides symmetry with unary minus, although it doesn’t actually do anything.

The increment and decrement operators ( ++ and --) were introduced earlier in this chapter. These are the only operators other than those involving assignment that have side effects. These operators increase or decrease the variable by one unit, although “unit” can have different meanings according to the data type – this is especially true with pointers.

The last unary operators are the address-of ( &), dereference ( * and ->) and cast operators in C and C++, and new and delete in C++. Address-of and dereference are used with pointers, described in this chapter. Casting is described later in this chapter, and new and delete are described in Chapter XX.

The ternary operator

The ternary if-else is unusual because it has 3 operands. It is truly an operator because it produces a value, unlike the ordinary if-else statement. It consists of three expressions: if the first expression (followed by a ?) evaluates to true, the expression following the ? is evaluated and its result becomes the value produced by the operator. If the first expression is false, the third expression (following a :) is executed and its result becomes the value produced by the operator.

The conditional operator can be used for its side effects or for the value it produces. Here’s a code fragment that demonstrates both:

A = --B ? B : (B = -99);

Here, the conditional produces the rvalue. A is assigned to the value of B if the result of decrementing B is nonzero. If B became zero, A and B are both assigned to -99. B is always decremented, but it is only assigned to -99 if the decrement causes B to become 0. A similar statement can be used without the “ A = ” just for its side effects:

--B ? B : (B = -99);

Here the second B is superfluous, since the value produced by the operator is unused. An expression is required between the ? and :. In this case the expression could simply be a constant that might make the code run a bit faster.

The comma operator

The comma is not restricted to separating variable names in multiple definitions such as

int i, j, k;

Of course, it’s also used in function argument lists. However, it can also be used as an operator to separate expressions – in this case it produces only the value of the last expression. All the rest of the expressions in the comma-separated list are only evaluated for their side effects. This code fragment increments a list of variables and uses the last one as the rvalue:

A = (B++,C++,D++,E++);

The parentheses are critical here. Without them, the statement will evaluate to:

(A = B++), C++, D++, E++;

In general, it’s best to avoid using the comma as anything other than a separator, since people are not used to seeing it as an operator.

Common pitfalls when using operators

As illustrated above, one of the pitfalls when using operators is trying to get away without parentheses when you are even the least bit uncertain about how an expression will evaluate (consult your local C manual for the order of expression evaluation).

Another extremely common error looks like this:

//: C03:Pitfall.cpp
// Operator mistakes

int main() {
  int a = 1, b = 1;
  while(a = b) {
    // ....
  }
} ///:~ 

The statement a = b will always evaluate to true when b is non-zero. The variable a is assigned to the value of b, and the value of b is also produced by the operator =. Generally you want to use the equivalence operator == inside a conditional statement, not assignment. This one bites a lot of programmers (however, some compilers will point out the problem to you, which is very helpful).

A similar problem is using bitwise and and or instead of their logical counterparts. Bitwise and and or use one of the characters ( & or |) while logical and and or use two ( && and ||). Just as with = and ==, it’s easy to just type one character instead of two. A useful mnemonic device is to observe that “bits are smaller, so they don’t need as many characters in their operators.”

Casting operators

The word cast is used in the sense of “casting into a mold.” The compiler will automatically change one type of data into another if it makes sense. For instance, if you assign an integral value to a floating-point variable, the compiler will secretly call a function (or more probably, insert code) to convert the int to a float. Casting allows you to make this type conversion explicit, or to force it when it wouldn’t normally happen.

To perform a cast, put the desired data type (including all modifiers) inside parentheses to the left of the value. This value can be a variable, a constant, the value produced by an expression or the return value of a function. Here’s an example:

int b = 200;
a = (unsigned long int)b; 

Casting is very powerful, but it can cause headaches because in some situations it forces the compiler to treat data as if it were (for instance) larger than it really is, so it will occupy more space in memory – this can trample over other data. This usually occurs when casting pointers, not when making simple casts like the one shown above.

C++ has an additional kind of casting syntax, which follows the function call syntax. This syntax puts the parentheses around the argument, like a function call, rather than around the data type:

float a = float(200);

This is equivalent to:

float a = (float)200;

Of course in the above case you wouldn’t really need a cast; you could just say 200f (and that’s typically what the compiler will do for the above expression, in effect). Casts are generally used instead with variables, rather than constants.

sizeof – an operator by itself

The sizeof( ) operator stands alone because it satisfies an unusual need. sizeof( ) gives you information about the amount of memory allocated for data items. As described earlier in this chapter, sizeof( ) tells you the number of bytes used by any particular variable. It can also give the size of a data type (with no variable name):

printf("sizeof(double) = %d\n", sizeof(double));

sizeof( ) can also give you the sizes of user-defined data types. This is used later in the book.

The asm keyword

This is an escape mechanism that allows you to write assembly code for your hardware within a C++ program. Often you’re able to reference C++ variables within the assembly code, which means you can easily communicate with your C++ code and limit the assembly code to that necessary for efficiency tuning or to utilize special processor instructions. The exact syntax of the assembly language is compiler-dependent and can be discovered in your compiler’s documentation.

Explicit operators

These are keywords for bitwise and logical operators. Non-U.S. programmers without keyboard characters like &, |, ^, and so on, were forced to use C’s horrible trigraphs, which were not only annoying to type, but obscure when reading. This is repaired in C++ with additional keywords:

Keyword

Meaning

and

&& (logical and)

or

|| (logical or)

not

! (logical NOT)

not_eq

!= (logical not-equivalent)

bitand

& (bitwise and)

and_eq

&= (bitwise and-assignment)

bitor

| (bitwise or)

or_eq

|= (bitwise or-assignment)

xor

^ (bitwise exclusive- or)

xor_eq

^= (bitwise exclusive- or-assignment)

compl

~ (ones complement)

If your compiler complies with Standard C++, it will support these keywords.

Contents | Prev | Next


Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru