Looking at the table at the end of the previous section might be intimidating: C++ seems to have many (often bewildering) operators. Fortunately, we can do a great deal of programming with just a few that are familiar or at least intuitive. However, we must not let familiarity and intuition mislead our quest to understand these fundamental programming operators.
The assignment operator, =
, is straightforward, but does not have the same meaning or behave in the same way as the equals symbol used in mathematics.
x = y + 5 | x = y + 5; |
(a) | (b) |
y + 5
for x
wherever x
appears in an equation.Recall from Chapter 1 that variables have three characteristics: a name, a content, and an address. Now, focus on two parts of the description of the assignment operation in the previous figure: "Add 5 to the current value stored in y and then store the result in x." The first part uses the contents of y, while the second part uses the address of x. We can use these characteristics to help us visualize what actions take place as the program uses variables.
x = y + 5; |
The discussion of associativity in the previous section suggested that the assignment operator produces a value, allowing programmers to chain multiple assignment operations in a single statement. Assignment chaining obscures the meanings of l- and r-values. Chapter 3 briefly describes the C++ syntax rules allowing the interchange of statements and expressions.
int z = x = y + 5; |
Most remaining arithmetic operators are likely familiar to most programmers. Nevertheless, two of them, plus the less familiar modular operator, may exhibit some unfamiliar behaviors, warranting a brief examination.
+ | Addition | |
- | Subtraction | |
* | Multiplication | Mathematics denotes multiplication with two adjacent variables: ab . However, the C++ compiler interprets the sequence as a single variable. Multiplying two variables in a C++ program always requires the * operator. |
/ | Division | Division behaves as expected unless both operands are integers. Division is detailed next. |
% | Modular | The modular or remainder is likely the least familiar, so it is detailed below. |
a^b
. It does use the symbol but as the bitwise exclusive-OR operator, described in the supplemental section at the end of the chapter.
Sometimes, it's difficult to differentiate the effects of the division and output operations. The C++ fundamental, built-in, primitive data types table in Chapter 1 lists various kinds of integers and floating-point types and their respective ranges. While C++ displays floating-point numbers with six significant digits of accuracy by default, it internally maintains the value to the full accuracy of the specific data type. We'll explore output operations in greater detail in the next chapter.
Floating Point | Floating Point & Integer | Integer |
---|---|---|
10.0 / 2.0 → 5.000000 | 10.0 / 2 → 5.000000 | 10 / 2 → 5 |
1.0 / 3.0 → 0.333333 | 1 / 3.0 → 0.333333 | 1 / 3 → 0 (truncates) |
5.0 / 9.0 → 0.555556 | 5 / 9.0 → 0.555556 | 5 / 9 → 0 (truncates) |
9 / 10 → 0.900000 | 9.0 / 10 → 0.900000 | 9 / 10 → 0 (truncates) |
float
or double
), it behaves as expected. However, suppose both operands are integers. In that case, the program carries out the operation using integer arithmetic, AND the result is an integer (i.e., NO decimal or fractional amount). The division operator truncates rather than rounds when it discards the fractional part. This behavior is often called integer division.
Typically, at least one operand is a variable or a more complex expression. In this situation, there is a good chance that the division operation will lead to a loss of precision, that is, a loss of information. For example, assuming that score stores the value 95 and we divide by 10: score / 10 = 95 / 10 = 9. But is the precision loss an error? Oddly, truncation is sometimes very helpful (see money.cpp later in this chapter for a simple example). But sometimes, it is a truncation error. Error or not depends on the programmer's intentions and the program's needs.
The modular operator (also known as the remainder operator) might not be familiar to some, but it is listed with the "common" operators because computer scientists use it often. The easiest way to understand how the modular operator works is by remembering how we first learned to divide. When the divisor does not evenly divide the dividend, we expressed the result as a quotient with a remainder. The modular operator returns the remainder.
Through our study of programming in general and C++ in particular, we understand that data types profoundly impact a program's behavior. Furthermore, sometimes, it is necessary to change the data type of an expression to achieve the behavior necessary to solve a particular problem. Sometimes, the compiler will automatically generate the code to perform the conversion, while other times, programmers must explicitly do the conversions themselves.
The compiler can automatically convert a "narrow" data type to a "wider" one. In this context, "narrow" and "wide" refer to the dynamic range of values stored rather than the number of bytes of memory each type requires. For example, both long and float use four bytes of storage, but float is the widest because it can store larger and smaller (between 0 and 1) numbers than long. An automatic type conversion from narrow to wide is called a type promotion. Evaluating an operation where the operands have different types triggers an automatic type promotion, which we can sometimes use to solve the truncation problem presented above.
int score; ... score / 10.0; |
double score; ... score / 10; |
(a) | (b) |
In both examples, type promotion can solve the truncation problem. But what happens when both operands are integer variables? This situation forces programmers to use an explicit typecast (described below).
Additionally, the C++ compiler also generates code to automatically convert char to int and int to char. The compiler does this because many library functions work with characters but have parameters and return types that are integers (see, for example, Character Testing and Conversion Functions). There are at least two reasons that C++ works with characters as integers. Most importantly, type int is deliberately the same size as the computer's native word size, which the computer can transfer to and from memory with the greatest speed. Of lesser importance is that C++ defines an integer symbolic constant named EOF (end of file). Some functions that read files one character at a time return this value to signal that the read operation has reached the end of the file (see wc.cpp).
Casting is not an operation that we use in our everyday lives. Nevertheless, I include it with the "common" operators because it helps solve problems with integer division. Java only supports one casting notation, but C++ supports two. The operator notation, recognized both by languages, is formed by enclosing the new or destination data type in parentheses and placing it before an expression, that is, before the value to convert. The precedence of the casting operator is relatively high, so programmers must group with parentheses complex expressions involving other operators. The functional notation looks like a function call where the name of the new or destination type is the function name, and the single argument is the expression whose value the cast will convert.
Operator Notation | Functional Notation |
---|---|
(double)score / count score / (double)count (double)score / (double)count |
double(score) / count score / double(count) double(score) / double(count) |
(a) - C-style cast | |
(int)(3.14 + 2.7) |
int(3.14 + 2.7) |
(b) - C-style cast | |
(double)(score / count) |
double(score / count) |
(c) - function-style cast |
The ANSI C++ standard regarding type conversions has evolved recently. Previously, the standard required the compiler to perform an explicit typecast to convert from a wide type to a narrower type, i.e., from double to int. The wide-to-narrow conversion could result in a loss of precision, e.g., changing 10.5 to 10 would lose the fractional part of the data. In the past, the compiler made programmers "take responsibility" for the potential loss with an explicit cast operation, but newer compilers will perform the type conversion automatically.
#include <iostream> using namespace std; int main() { int i = (int)(3.14 + 2.7); cout << i << endl; return 0; } |
#include <iostream> using namespace std; int main() { int i = 3.14 + 2.7; cout << i << endl; return 0; } |
(a) | (b) |
While the newer standard makes some programming operations easier, it also places a greater responsibility on programmers to detect and correct truncation errors. Recall that when the compiler generates code to evaluate a division operation using integer arithmetic, it always truncates the results - it discards the fraction - rather than rounding the value to the nearest integer. Inappropriately converting a wide type to a narrow type is one cause of truncation errors in a program.
If you look carefully at the preceding examples, you will notice that they all involve numeric data types; they are all different kinds of numbers. I chose int and double because they are easy to understand and we have already seen the numeric data type rules that govern how the compiler treats numeric constants. But the examples should make us wonder if casting is possible with other data types.
To cast from one data type to another, the two data types, the new and the current types, must be "sort of the same" to begin with.
"Sort of the same" is too ambiguous for an exact discipline like computer programming, so let's begin refining the phrase with an example. In mathematics, there is little difference between 2 and 2.0, but in the memory of a running C++ program, the bit-patterns are quite different. The difference implies that sometimes we need to convert one type to the other, but it is the similarity, an int and a double are "sort of the same," that makes the conversion meaningful. They are different kinds of numbers with different representations in memory, but numbers nevertheless. A typecast changes the representation of the number in memory without changing the fundamental concept that it is a number.
Armed with this understanding, we are ready to explore some relatively simple type conversions that we cannot do with a cast. For example, suppose we want to convert a number to a string or a string to a number. A string and a number aren't "sort of the same:" one is a string or sequence of characters, and the other is a single numeric value. The two are too dissimilar to convert between them with a typecast. Yet, the conversion is necessary whenever a program reads numbers from or writes numbers to the console. We'll explore functions that perform the conversions in Chapter 8.
Sometimes, we can even cast an object from one class type to another. This cast will only work if the two classes are related by inheritance (formally introduced in Chapter 10). For example, a program might have a class named Car and another class named Convertible. If the classes are related by inheritance, we can say, "A Convertible is a Car," and we can cast a Convertible to a Car.
But it's not always possible to cast one class to another. Imagine that a program has two classes, Person and Circle. What would it mean to try to convert a Person to a Circle or a Circle to a Person? The two are not "sort of the same" they are fundamentally different kinds of things and it doesn't make sense to try to convert one to the other.