Hunter Liu's Website

3. Week 2 Tuesday: Trouble with Doubles; Increments and Decrements

≪ 2. Week 1 Thursday: Integer Operations | Table of Contents | 4. Week 2 Thursday: Characters and Strings ≫

Last time, we described some ways that the division operator / doesn’t work quite the way we expect when dealing with integers, and also how the modulo operator % helps us address this programatically. This week, we’ll introduce two new mathematical operators — the decrement and increment operators — as well as how one can force the computer to do decimal operations instead.

Increments and Decrements

The operation of adding or subtracting one from a number is a very common operation in C++. It’s used when counting number of occurrences of an event, keeping track of how many times a loop was repeated, and many others. If n is a numerical variable, the increments n++ and ++n add one to n, and the decrements n-- and --n subtract one from n.

Remark 1. Non-numeric variables can be incremented or decremented too.

The increment and decrement operators can be manually extended to non-numeric variables by programmers in a process called “operator overloading”. This is beyond the scope of this class, but the upshot is that some variables that are not numbers can nevertheless be incremented or decremented. We will see examples of this when we talk about pointers in the distant future.

We’ll focus on the increment operators; everything we say below will have analogous statements for the corresponding decrement operators.

One can think of both n++ and ++n as shortcuts for writing the statement n += 1. When n++ and ++n stand alone, the behaviour is identical:

1int n = 5;
2n++;
3cout << n << endl; // prints 6
4++n;
5cout << n << endl; // prints 7

However, there is one important difference: you can use n++ and ++n within a larger piece of code. This is where all the power of these operators comes in!

1int n = 5;
2cout << ++n << endl; // prints 6
3cout << n++ << endl; // prints ...6??

The above code highlights a subtlety of the two operators. ++n says to first increase n by one, then substitute its value into the code. Likewise, the n++ operator says to first substitute the value of n into the code, then increment its value.

Be very careful about using multiple increment/decrement operators in the same expression. One might be tempted to think that (n++)++ will increment n twice, but this does not even compile. The code (++n)++ does increment n twice (it increments n once, substitutes that value in, then increments again). However, the code ++(n++) does not compile.

Similarly, statements such as n++ + ++n, n + ++n, n * ++n, etc. produce undefined behaviour according to the C++ standard. That is, the code will compile, but you may get different results depending on your compiler and your computer.

The issue is one of the order of operations — C++ needs to substitute the values of n and ++n into the expression, but the order in which this substitution occurss both affects the resultant value and changes depending on the compiler. There are reasons for why these operators behave like this, but this is well beyond the scope of PIC 10A.

That being said, if one has two variables n and m, expressions such as ++n * ++m are perfectly okay. It’s when a single variable is being incremented or decremented and gets used more than once.

Example 2.

Predict the output of the following code:

 1#include <iostream>
 2
 3using namespace std;
 4
 5int main() {
 6    int n = 15;
 7    int m = 17;
 8
 9    cout << n++ + ++m << endl;
10    cout << ++n + m-- << endl;
11    cout << --m * n-- << endl;
12    cout << m << endl;
13    cout << n << endl;
14
15    return 0;
16}
Solution

My preferred approach to these kinds of problems is to keep track of the values of n and m after each line of code. In the table below, the columns for n and m represent their values after the line has been executed.

Line Number Output n m
8 N/A 15 17
9 (15 + 18 = ) 33 16 18
10 (17 + 18 = ) 35 17 17
11 (16 * 17 = ) 272 16 16
12 16 16 16
13 16 16 16

Thus, the output consists of the five numbers 33, 35, 272, 16, and 16, each on their own line.

Trouble with Doubles

Recall that when dividing two integers, C++ will always truncate the answer and produce another integer. We saw that this has its applications last week, but there are definitely scenarios in which you would prefer to just work with the decimal values instead. These are double types rather than int types.

Remark 3. What the heck is a double?

This is not necessary knowledge for this class, but I think it’s helpful to understand how computers store decimal numbers under the hood.

A single variable in C++ takes up a finite amount of storage on your computer; hence integers cannot store numbers that are too big in any direction (~10 digits). Yet doubles have an impressive range of numbers they can capture. How do they do this?

One should think of a double as a version of scientific notation. Consider the number 6.0047382×10411,6.0047382\times 10 ^{411}, which is far too large for an integer to remember (you would need 411 digits, most of which are zeroes). Instead, a more efficient storage solution is to remember 6.0047382, and remember the exponent 411. This is what your computer does (in binary, more or less), and it explains why decimals have limited precion! Numbers with infinite decimal expansions will inevitably have to be cut off at some point in their scientific notation.

While one can always force a literal number to be a double by adding a .0 to the end of it, one has no such luxury with integer variables. Instead, one needs to “cast” integers into doubles. To do so, if a is an integer variable, then static_cast<double>(a) will turn a into a double.

1int a = 19; 
2int b = 3; 
3
4cout << a / b << endl;  // does integer division
5
6cout << static_cast<double>(a / b) << endl; // ??? 
7cout << static_cast<double>(a) / b << endl; 

Note the output of the second line! You need to cast to a double first, then perform the division. The second line prints out 6.0 because it does the division first (which produces the integer 6), then converts that into a double.

The problem of converting a double back into an integer is a similarly slippery problem, one that is perhaps best demonstrated with an example.

Example 4.

Write a C++ program that converts a duration in seconds into minutes, seconds, and milliseconds (you may assume this will be an integer). The program should prompt the user for an the duration in seconds.

SAMPLE RUNS: 
Enter a time in seconds: 124.556
124.556s = 2min, 4s, and 556ms. 

Let’s first write some pseudocode for this:

  1. Take a decimal input time from the user.
  2. Define the integer ms = time * 1000.
  3. Define the integer seconds = ms / 1000 and replace ms with ms % 1000.
  4. Define the integer minutes = seconds / 60 and replace seconds with seconds % 60.
  5. Print out the results to the screen.

We might naïvely cobble together the following program:

 1#include <iostream> 
 2
 3using namespace std; 
 4
 5int main() {
 6    // 1. take input from user 
 7    double time; 
 8    cout << "Enter a time in seconds: "; 
 9    cin >> time; 
10
11    // 2-4. compute ms, seconds, minutes 
12    int ms = time * 1000; 
13
14    int seconds = ms / 1000; 
15    ms = ms % 1000; 
16
17    int minutes = seconds / 60; 
18    seconds = seconds % 60; 
19
20    // 5. print everything out. 
21    cout << time << "s = " 
22         << minutes << "min, " 
23         << seconds << "s, and "
24         << ms << "ms." << endl; 
25
26    return 0; 
27} 

However, consider the following run of the code:

Enter a time in seconds: 16.002
16.002s = 0min, 16s, and 1ms.

Something went wrong, and it’s that C++ thinks 16.002 * 1000 is just barely less than 16002 due to the limited precision of a double! Thus it rounds it down to 16001 instead.

Common Mistake 5. Converting Doubles to Integers

There are several ways to convert doubles into integers. Consider the following code:

 1#include <cmath> // needed for floor, round, ceil 
 2
 3int main() {
 4    double d = 20.15; 
 5    int i1 = d * 100;        // truncates d * 100
 6    int i2 = floor(d * 100); 
 7    int i3 = round(d * 100); 
 8    int i4 = ceil(d * 100); 
 9
10    return 0; 
11} 

What values do you think these integer variables hold? It turns out that i1 and i2 will hold the value 2014 while the other two hold i3 and i4. What would happen if we defined d = -20.15 instead? Then i1, i2, and i3 hold the value -2015 while i4 holds -2014. Be very careful about which method you use to convert doubles to integers: they each behave in distinguishable ways, and they can cause many headaches.

With that out of the way, try to fix the code we wrote above yourself before peeking at the best way to proceed!

Solution
 1#include <iostream> 
 2#include <cmath>    // needed for round 
 3
 4using namespace std; 
 5
 6int main() {
 7    // 1. take input from user 
 8    double time; 
 9    cout << "Enter a time in seconds: "; 
10    cin >> time; 
11
12    // 2-4. compute ms, seconds, minutes 
13    int ms = round(time * 1000); 
14
15    int seconds = ms / 1000; 
16    ms = ms % 1000; 
17
18    int minutes = seconds / 60; 
19    seconds = seconds % 60; 
20
21    // 5. print everything out. 
22    cout << time << "s = " 
23         << minutes << "min, " 
24         << seconds << "s, and "
25         << ms << "ms." << endl; 
26
27    return 0; 
28} 

In general, it’s a good idea to use the round function from the cmath library to convert from doubles to integers over using the default integer conversion in C++.

Remark 6. The cmath library

In addition to having helpful functions like floor, ceil, and round, the cmath library defines a plethora of helpful mathematical operations that we may not know how to do otherwise. The C++ reference contains a list of every single function accessible through this library, some of which (e.g. fmin and fmax) may be immediately helpful for the first homework.

While the reference can be intimidating, it’s important that you get comfortable reading the documentation! There’s often no good reason to reivent the wheel.

Problem 7. Digits

Write a C++ program that asks a user for a number, then determines the third digit after the decimal point.

Sample run: 
INPUT   30.3571 
OUTPUT  7 

INPUT   -39.82113
OUTPUT  1

INPUT   5
OUTPUT  0