Inapoi
Inainte
Cuprins
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 and 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:
PRINT("u", u); PRINT("v", v);
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 12, which covers 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 program above. Be aware,
however, that the comparison of a floating-point number with the value of zero
is 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 work only with integral types:
char, int and long). 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 produces a zero only 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.
What follows is an example that
demonstrates the use of all the operators involving bits. First, here’s a
general-purpose function that prints a byte in binary format, created separately
so that it may be easily reused. The header file declares the
function:
//: C03:printBinary.h
// Display a byte in binary
void printBinary(const unsigned char val);
///:~
Here’s the implementation of the
function:
//: C03:printBinary.cpp {O}
#include <iostream>
void printBinary(const unsigned char val) {
for(int i = 7; i >= 0; i--)
if(val & (1 << i))
std::cout << "1";
else
std::cout << "0";
} ///:~
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.
Finally, the function is used in the
example that shows the bit-manipulation operators:
//: C03:Bitwise.cpp
//{L} printBinary
// Demonstration of bit manipulation
#include "printBinary.h"
#include <iostream>
using namespace std;
// 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);
} ///:~
Once again, a preprocessor macro is used
to save typing. It prints the string of your choice, then the binary
representation of an expression, then a newline.
In main( ), the variables are
unsigned. This is because, in general, 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 introduced in Chapter
4.
The ternary
operator
The ternary
if-else is unusual because it has three 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 assigned to
-99 only 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
evaluated only for their side effects. This example increments a list of
variables and uses the last one as the rvalue:
//: C03:CommaOperator.cpp
#include <iostream>
using namespace std;
int main() {
int a = 0, b = 1, c = 2, d = 3, e = 4;
a = (b++, c++, d++, e++);
cout << "a = " << a << endl;
// The parentheses are critical here. Without
// them, the statement will evaluate to:
(a = b++), c++, d++, e++;
cout << "a = " << a << endl;
} ///:~
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 =. In general, 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 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:
//: C03:SimpleCast.cpp
int main() {
int b = 200;
unsigned long a = (unsigned long int)b;
} ///:~
Casting is 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 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:
//: C03:FunctionCallCast.cpp
int main() {
float a = float(200);
// This is equivalent to:
float b = (float)200;
} ///:~
Of course in the case above you
wouldn’t really need a cast; you could just say 200f (in effect,
that’s typically what the compiler will do for the above expression).
Casts are generally used instead with variables, rather than
constants.
C++ explicit casts
Casts should be used carefully, because
what you are actually doing is saying to the compiler “Forget
type checking – treat it as this other type
instead.” That is, you’re introducing a hole in the C++ type system
and preventing the compiler from telling you that you’re doing something
wrong with a type. What’s worse, the compiler believes you implicitly and
doesn’t perform any other checking to catch errors. Once you start
casting, you open yourself up for all kinds of problems. In fact, any program
that uses a lot of casts should be viewed with suspicion, no matter how much you
are told it simply “must” be done that way. In general, casts should
be few and isolated to the solution of very specific problems.
Once you understand this and are
presented with a buggy program, your first inclination
may be to look for casts as culprits. But how do you locate C-style casts? They
are simply type names inside of parentheses, and if you start hunting for such
things you’ll discover that it’s often hard to distinguish them from
the rest of your code.
Standard C++ includes an explicit cast
syntax that can be used to completely replace the old C-style casts (of course,
C-style casts cannot be outlawed without breaking code, but compiler writers
could easily flag old-style casts for you). The explicit cast syntax is such
that you can easily find them, as you can see by their names:
|
For “well-behaved” and
“reasonably well-behaved” casts, including things you might now do
without a cast (such as an automatic type conversion).
|
|
To cast away const and/or
volatile.
|
|
To cast to a completely different
meaning. The key is that you’ll need to cast back to the original type to
use it safely. The type you cast to is typically used only for bit twiddling or
some other mysterious purpose. This is the most dangerous of all the
casts.
|
|
For type-safe downcasting (this cast will
be described in Chapter 15).
|
The first three explicit casts will
be described more completely in the following sections, while the last one can
be demonstrated only after you’ve learned more, in Chapter
15.
static_cast
A static_cast is used for all
conversions that are well-defined. These include “safe” conversions
that the compiler would allow you to do without a cast and less-safe conversions
that are nonetheless well-defined. The types of conversions covered by
static_cast include typical castless conversions, narrowing
(information-losing) conversions, forcing a conversion from a void*,
implicit type conversions, and static navigation of class hierarchies (since you
haven’t seen classes and inheritance yet, this last topic will be delayed
until Chapter 15):
//: C03:static_cast.cpp
void func(int) {}
int main() {
int i = 0x7fff; // Max pos value = 32767
long l;
float f;
// (1) Typical castless conversions:
l = i;
f = i;
// Also works:
l = static_cast<long>(i);
f = static_cast<float>(i);
// (2) Narrowing conversions:
i = l; // May lose digits
i = f; // May lose info
// Says "I know," eliminates warnings:
i = static_cast<int>(l);
i = static_cast<int>(f);
char c = static_cast<char>(i);
// (3) Forcing a conversion from void* :
void* vp = &i;
// Old way produces a dangerous conversion:
float* fp = (float*)vp;
// The new way is equally dangerous:
fp = static_cast<float*>(vp);
// (4) Implicit type conversions, normally
// performed by the compiler:
double d = 0.0;
int x = d; // Automatic type conversion
x = static_cast<int>(d); // More explicit
func(d); // Automatic type conversion
func(static_cast<int>(d)); // More explicit
} ///:~
In Section (1), you see the kinds of
conversions you’re used to doing in C, with or without a cast. Promoting
from an int to a long or float is not a problem because the
latter can always hold every value that an int can contain. Although
it’s unnecessary, you can use static_cast to highlight these
promotions.
Converting back the other way is shown in
(2). Here, you can lose data because an int is not as “wide”
as a long or a float; it won’t hold numbers of the same
size. Thus these are called narrowing
conversions. The compiler will
still perform these, but will often give you a warning. You can eliminate this
warning and indicate that you really did mean it using a cast.
Assigning from a
void* is not allowed without a cast in C++ (unlike
C), as seen in (3). This is dangerous and requires that programmers know what
they’re doing. The static_cast, at least, is easier to locate than
the old standard cast when you’re hunting for bugs.
Section (4) of the program shows the
kinds of implicit type conversions that are normally performed automatically by
the compiler. These are automatic and require no casting, but again
static_cast highlights the action in case you want to make it clear
what’s happening or hunt for it
later.
const_cast
If you want to convert from a
const to a nonconst or from a
volatile to a nonvolatile, you use
const_cast. This is the only conversion allowed with
const_cast; if any other conversion is involved it must be done using a
separate expression or you’ll get a compile-time error.
//: C03:const_cast.cpp
int main() {
const int i = 0;
int* j = (int*)&i; // Deprecated form
j = const_cast<int*>(&i); // Preferred
// Can't do simultaneous additional casting:
//! long* l = const_cast<long*>(&i); // Error
volatile int k = 0;
int* u = const_cast<int*>(&k);
} ///:~
If you take the address of a const
object, you produce a pointer to a
const, and this cannot be assigned to a nonconst pointer without a
cast. The old-style cast will accomplish this, but the const_cast is the
appropriate one to use. The same holds true for
volatile.
reinterpret_cast
This is the least safe of the casting
mechanisms, and the one most likely to produce bugs. A reinterpret_cast
pretends that an object is just a bit pattern that can be treated (for some dark
purpose) as if it were an entirely different type of object. This is the
low-level bit twiddling that C is notorious for. You’ll virtually always
need to reinterpret_cast back to the original type (or otherwise treat
the variable as its original type) before doing anything else with
it.
//: C03:reinterpret_cast.cpp
#include <iostream>
using namespace std;
const int sz = 100;
struct X { int a[sz]; };
void print(X* x) {
for(int i = 0; i < sz; i++)
cout << x->a[i] << ' ';
cout << endl << "--------------------" << endl;
}
int main() {
X x;
print(&x);
int* xp = reinterpret_cast<int*>(&x);
for(int* i = xp; i < xp + sz; i++)
*i = 0;
// Can't use xp as an X* at this point
// unless you cast it back:
print(reinterpret_cast<X*>(xp));
// In this example, you can also just use
// the original identifier:
print(&x);
} ///:~
In this simple example, struct X
just contains an array of int, but when you create one on the stack as in
X x, the values of each of the ints are garbage (this is shown
using the print( ) function to display the contents of the
struct). To initialize them, the address of the X is taken and
cast to an int pointer, which is then walked through the array to set
each int to zero. Notice how the upper bound for i is calculated
by “adding” sz to xp; the compiler knows that you
actually want sz pointer locations greater than xp and it does the
correct pointer arithmetic for you.
The idea of reinterpret_cast is
that when you use it, what you get is so foreign that it cannot be used for the
type’s original purpose unless you cast it back. Here, we see the cast
back to an X* in the call to print, but of course since you still have
the original identifier you can also use that. But the xp is only useful
as an int*, which is truly a “reinterpretation” of the
original X.
A reinterpret_cast often indicates
inadvisable and/or nonportable programming, but it’s available when you
decide you have to use
it.
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):
//: C03:sizeof.cpp
#include <iostream>
using namespace std;
int main() {
cout << "sizeof(double) = " << sizeof(double);
cout << ", sizeof(char) = " << sizeof(char);
} ///:~
By definition, the
sizeof any type of
char (signed, unsigned or plain) is always one, regardless
of whether the underlying storage for a char is actually one byte. For
all other types, the result is the size in bytes.
Note that sizeof is an operator,
not a function. If you apply it to a type, it must be used with the
parenthesized form shown above, but if you apply it to a variable you can use it
without parentheses:
//: C03:sizeofOperator.cpp
int main() {
int x;
int i = sizeof x;
} ///:~
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 use special processor
instructions. The exact syntax that you must use when writing 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-assignment)
|
|
|
If your compiler complies with Standard
C++, it will support these
keywords.
 |
|