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
has
an obvious meaning. The compiler can figure out:
but
the reader might get confused, so it is safer to say:
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:
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:
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
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:
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:
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:
|
|
|
|
|
|
|
|
|
!=
(logical not-equivalent)
|
|
|
|
&=
(bitwise
and-assignment)
|
|
|
|
|=
(bitwise
or-assignment)
|
|
^
(bitwise exclusive-
or)
|
|
^=
(bitwise exclusive-
or-assignment)
|
|
|
If
your compiler complies with Standard C++, it will support these keywords.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru