Programming: Principles and Practice Using C++ (2014)

Part I: The Basics

3. Objects, Types, and Values

“Fortune favors the prepared mind.”

—Louis Pasteur

This chapter introduces the basics of storing and using data in a program. To do so, we first concentrate on reading in data from the keyboard. After establishing the fundamental notions of objects, types, values, and variables, we introduce several operators and give many examples of use of variables of types char, int, double, and string.


3.1 Input

3.2 Variables

3.3 Input and type

3.4 Operations and operators

3.5 Assignment and initialization

3.5.1 An example: detect repeated words

3.6 Composite assignment operators

3.6.1 An example: find repeated words

3.7 Names

3.8 Types and objects

3.9 Type safety

3.9.1 Safe conversions

3.9.2 Unsafe conversions


3.1 Input

The “Hello, World!” program just writes to the screen. It produces output. It does not read anything; it does not get input from its user. That’s rather a bore. Real programs tend to produce results based on some input we give them, rather than just doing exactly the same thing each time we execute them.

Image

To read something, we need somewhere to read into; that is, we need somewhere in the computer’s memory to place what we read. We call such a “place” an object. An object is a region of memory with a type that specifies what kind of information can be placed in it. A named object is called a variable. For example, character strings are put into string variables and integers are put into int variables. You can think of an object as a “box” into which you can put a value of the object’s type:

Image

This would represent an object of type int named age containing the integer value 42. Using a string variable, we can read a string from input and write it out again like this:

// read and write a first name
#include "std_lib_facilities.h"

int main()
{
         cout << "Please enter your first name (followed by 'enter'):\n";
         string first_name;              // first_name is a variable of type string
         cin >> first_name;             // read characters into first_name
         cout << "Hello, " << first_name << "!\n";
}

The #include and the main() are familiar from Chapter 2. Since the #include is needed for all our programs (up to Chapter 12), we’ll leave it out of our presentation to avoid distraction. Similarly, we’ll sometimes present code that will work only if it is placed in main() or some other function, like this:

cout << "Please enter your first name (followed by 'enter'):\n";

We assume that you can figure out how to put such code into a complete program for testing.

The first line of main() simply writes out a message encouraging the user to enter a first name. Such a message is typically called a prompt because it prompts the user to take an action. The next lines define a variable of type string called first_name, read input from the keyboard into that variable, and write out a greeting. Let’s look at those three lines in turn:

string first_name;     // first_name is a variable of type string

This sets aside an area of memory for holding a string of characters and gives it the name first_name:

Image

A statement that introduces a new name into a program and sets aside memory for a variable is called a definition.

The next line reads characters from input (the keyboard) into that variable:

cin >> first_name;     // read characters into first_name

The name cin refers to the standard input stream (pronounced “see-in,” for “character input”) defined in the standard library. The second operand of the >> operator (“get from”) specifies where that input goes. So, if we type some first name, say Nicholas, followed by a newline, the string"Nicholas" becomes the value of first_name:

Image

Image

The newline is necessary to get the machine’s attention. Until a newline is entered (the Enter key is hit), the computer simply collects characters. That “delay” gives you the chance to change your mind, erase some characters, and replace them with others before hitting Enter. The newline will not be part of the string stored in memory.

Having gotten the input string into first_name, we can use it:

cout << "Hello, " << first_name << "!\n";

This prints Hello, followed by Nicholas (the value of first_name) followed by ! and a newline ('\n') on the screen:

Hello, Nicholas!

If we had liked repetition and extra typing, we could have written three separate output statements instead:

cout << "Hello, ";
cout << first_name;
cout << "!\n";

However, we are indifferent typists, and — more importantly — strongly dislike needless repetition (because repetition provides opportunity for errors), so we combined those three output operations into a single statement.

Note the way we use quotes around the characters in "Hello, " but not in first_name. We use quotes when we want a literal string. When we don’t quote, we refer to the value of something with a name. Consider:

cout << "first_name" << " is " << first_name;

Here, "first_name" gives us the ten characters first_name and plain first_name gives us the value of the variable first_name, in this case, Nicholas. So, we get

first_name is Nicholas

3.2 Variables

Image

Basically, we can do nothing of interest with a computer without storing data in memory, the way we did it with the input string in the example above. The “places” in which we store data are called objects. To access an object we need a name. A named object is called a variable and has a specific type (such as int or string) that determines what can be put into the object (e.g., 123 can go into an int and "Hello, World!\n" can go into a string) and which operations can be applied (e.g., we can multiply ints using the * operator and compare strings using the <= operator). The data items we put into variables are called values. A statement that defines a variable is (unsurprisingly) called a definition, and a definition can (and usually should) provide an initial value. Consider:

string name = "Annemarie";
int number_of_steps = 39;

You can visualize these variables like this:

Image

You cannot put values of the wrong type into a variable:

string name2 = 39;                                      // error: 39 isn’t a string
int number_of_steps = "Annemarie";     // error: “Annemarie” is not an int

The compiler remembers the type of each variable and makes sure that you use it according to its type, as specified in its definition.

C++ provides a rather large number of types (see §A.8). However, you can write perfectly good programs using only five of those:

int number_of_steps = 39;                               // int for integers
double flying_time = 3.5;                                 // double for floating-point numbers
char decimal_point = '.';                                  // char for individual characters
string name = "Annemarie";                           // string for character strings
bool tap_on = true;                                           // bool for logical variables

The reason for the name double is historical: double is short for “double-precision floating point.” Floating point is the computer’s approximation to the mathematical concept of a real number.

Note that each of these types has its own characteristic style of literals:

39                          // int: an integer
3.5                         // double: a floating-point number
'.'                           // char: an individual character enclosed in single quotes
"Annemarie"      // string: a sequence of characters delimited by double quotes
true                      // bool: either true or false

That is, a sequence of digits (such as 1234, 2, or 976) denotes an integer, a single character in single quotes (such as '1', '@', or 'x') denotes a character, a sequence of digits with a decimal point (such as 1.234, 0.12, or .98) denotes a floating-point value, and a sequence of characters enclosed in double quotes (such as "1234", "Howdy!", or "Annemarie") denotes a string. For a detailed description of literals see §A.2.

3.3 Input and type

Image

The input operation >> (“get from”) is sensitive to type; that is, it reads according to the type of variable you read into. For example:

// read name and age
int main()
{
          cout << "Please enter your first name and age\n";
          string first_name;          // string variable
          int age;                          // integer variable
          cin >> first_name;         // read a string
          cin >> age;                    // read an integer
          cout << "Hello, " << first_name << " (age " << age << ")\n";
}

So, if you type in Carlos 22 the >> operator will read Carlos into first_name, 22 into age, and produce this output:

Hello, Carlos (age 22)

Why won’t it read (all of) Carlos 22 into first_name? Because, by convention, reading of strings is terminated by what is called whitespace, that is, space, newline, and tab characters. Otherwise, whitespace by default is ignored by >>. For example, you can add as many spaces as you like before a number to be read; >> will just skip past them and read the number.

If you type in 22 Carlos, you’ll see something that might be surprising until you think about it. The 22 will be read into first_name because, after all, 22 is a sequence of characters. On the other hand, Carlos isn’t an integer, so it will not be read. The output will be 22 followed by (agefollowed by some random number, such as –96739 or 0. Why? You didn’t give age an initial value and you didn’t succeed in reading a value into it. Therefore, you get some “garbage value” that happened to be in that part of memory when you started executing. In §10.6, we look at ways to handle “input format errors.” For now, let’s just initialize age so that we get a predictable value if the input fails:

// read name and age (2nd version)
int main()
{
          cout << "Please enter your first name and age\n";
          string first_name = "???";      // string variable
                                                            // ("???” means “don’t know the name”)
          int age = –1;         // integer variable (–1 means “don’t know the age”)
          cin >> first_name >> age;      // read a string followed by an integer
          cout << "Hello, " << first_name << " (age " << age << ")\n";
}

Now the input 22 Carlos will output

Hello, 22 (age –1)

Note that we can read several values in a single input statement, just as we can write several values in a single output statement. Note also that << is sensitive to type, just as >> is, so we can output the int variable age as well as the string variable first_name and the string literals "Hello, " and" (age " and ")\n".

Image

A string read using >> is (by default) terminated by whitespace; that is, it reads a single word. But sometimes, we want to read more than one word. There are of course many ways of doing this. For example, we can read a name consisting of two words like this:

int main()
{
          cout << "Please enter your first and second names\n";
          string first;
          string second;
          cin >> first >> second;           // read two strings
          cout << "Hello, " << first <<" << second << '\n';
}

We simply used >> twice, once for each name. When we want to write the names to output, we must insert a space between them.


Image Try This

Get the “name and age” example to run. Then, modify it to write out the age in months: read the input in years and multiply (using the * operator) by 12. Read the age into a double to allow for children who can be very proud of being five and a half years old rather than just five.


3.4 Operations and operators

In addition to specifying what values can be stored in a variable, the type of a variable determines what operations we can apply to it and what they mean. For example:

int count;
cin >> count;                            // >> reads an integer into count
string name;
cin >> name;                             // >> reads a string into name

int c2 = count+2;                      // + adds integers
string s2 = name + " Jr. ";         // + appends characters

int c3 = count–2;                      // – subtracts integers
string s3 = name – " Jr. ";         // error: – isn’t defined for strings

By “error” we mean that the compiler will reject a program trying to subtract strings. The compiler knows exactly which operations can be applied to each variable and can therefore prevent many mistakes. However, the compiler doesn’t know which operations make sense to you for which values, so it will happily accept legal operations that yield results that may look absurd to you. For example:

Image

int age = –100;

It may be obvious to you that you can’t have a negative age (why not?) but nobody told the compiler, so it’ll produce code for that definition.

Here is a table of useful operators for some common and useful types:

Image

A blank square indicates that an operation is not directly available for a type (though there may be indirect ways of using that operation; see §3.9.1). We’ll explain these operations, and more, as we go along. The key points here are that there are a lot of useful operators and that their meaning tends to be the same for similar types.

Let’s try an example involving floating-point numbers:

// simple program to exercise operators
int main()
{
          cout << "Please enter a floating-point value: ";
          double n;
          cin >> n;
          cout << "n == " << n
                    << "\nn+1 == " << n+1
                    << "\nthree times n == " << 3*n
                    << "\ntwice n == " << n+n
                    << "\nn squared == " << n*n
                    << "\nhalf of n == " << n/2
                    << "\nsquare root of n == " << sqrt(n)
                    << '\n';  // another name for newline (“end of line”) in output
}

Obviously, the usual arithmetic operations have their usual notation and meaning as we know them from primary school. Naturally, not everything we might want to do to a floating-point number, such as taking its square root, is available as an operator. Many operations are represented as named functions. In this case, we use sqrt() from the standard library to get the square root of n: sqrt(n). The notation is familiar from math. We’ll use functions along the way and discuss them in some detail in §4.5 and §8.5.


Image Try This

Get this little program to run. Then, modify it to read an int rather than a double. Note that sqrt() is not defined for an int so assign n to a double and take sqrt() of that. Also, “exercise” some other operations. Note that for ints / is integer division and % is remainder (modulo), so that 5/2 is 2 (and not 2.5 or 3) and 5%2 is 1. The definitions of integer *, /, and % guarantee that for two positive ints a and b we have a/b * b + a%b == a.


Strings have fewer operators, but as we’ll see in Chapter 23, they have plenty of named operations. However, the operators they do have can be used conventionally. For example:

// read first and second name
int main()
{
          cout << "Please enter your first and second names\n";
          string first;
          string second;
          cin >> first >> second;                         // read two strings
          string name = first + ' ' + second;       // concatenate strings
          cout << "Hello, " << name << '\n';
}

For strings + means concatenation; that is, when s1 and s2 are strings, s1+s2 is a string where the characters from s1 are followed by the characters from s2. For example, if s1 has the value "Hello" and s2 the value "World", then s1+s2 will have the value "HelloWorld". Comparison of strings is particularly useful:

// read and compare names
int main()
{
          cout << "Please enter two names\n";
          string first;
          string second;
          cin >> first >> second;           // read two strings
          if (first == second) cout << "that's the same name twice\n";
          if (first < second)
                    cout << first << " is alphabetically before " << second <<'\n';
          if (first > second)
                    cout << first << " is alphabetically after " << second <<'\n';
}

Here, we used an if-statement, which will be explained in detail in §4.4.1.1, to select actions based on conditions.

3.5 Assignment and initialization

Image

In many ways, the most interesting operator is assignment, represented as =. It gives a variable a new value. For example:

Image

Image

That last assignment deserves notice. First of all it clearly shows that = does not mean equals — clearly, a doesn’t equal a+7. It means assignment, that is, to place a new value in a variable. What is done for a=a+7 is the following:

1. First, get the value of a; that’s the integer 4.

2. Next, add 7 to that 4, yielding the integer 11.

3. Finally, put that 11 into a.

We can also illustrate assignments using strings:

Image

Image

Above, we use “starts out with” and “gets” to distinguish two similar, but logically distinct, operations:

• Initialization (giving a variable its initial value)

• Assignment (giving a variable a new value)

These operations are so similar that C++ allows us to use the same notation (the =) for both:

int y = 8;                          // initialize y with 8
x = 9;                               // assign 9 to x

string t = "howdy!";      // initialize t with “howdy!”
s = "G'day";                   // assign “G’day” to s

However, logically assignment and initialization are different. You can tell the two apart by the type specification (like int or string) that always starts an initialization; an assignment does not have that. In principle, an initialization always finds the variable empty. On the other hand, an assignment (in principle) must clear out the old value from the variable before putting in the new value. You can think of the variable as a kind of small box and the value as a concrete thing, such as a coin, that you put into it. Before initialization, the box is empty, but after initialization it always holds a coin so that to put a new coin in, you (i.e., the assignment operator) first have to remove the old one (“destroy the old value”). Things are not quite this literal in the computer’s memory, but it’s not a bad way of thinking of what’s going on.

3.5.1 An example: detect repeated words

Assignment is needed when we want to put a new value into an object. When you think of it, it is obvious that assignment is most useful when you do things many times. We need an assignment when we want to do something again with a different value. Let’s have a look at a little program that detects adjacent repeated words in a sequence of words. Such code is part of most grammar checkers:

int main()
{
          string previous = " ";       // previous word; initialized to “not a word”
          string current;                             // current word
          while (cin>>current) {                // read a stream of words
                    if (previous == current)    // check if the word is the same as last
                              cout << "repeated word: " << current << '\n';
          previous = current;
          }
}

This program is not the most helpful since it doesn’t tell where the repeated word occurred in the text, but it’ll do for now. We will look at this program line by line starting with

string current;     // current word

This is the string variable into which we immediately read the current (i.e., most recently read) word using

while (cin>>current)

Image

This construct, called a while-statement, is interesting in its own right, and we’ll examine it further in §4.4.2.1. The while says that the statement after (cin>>current) is to be repeated as long as the input operation cin>>current succeeds, and cin>>current will succeed as long as there are characters to read on the standard input. Remember that for a string, >> reads whitespace-separated words. You terminate this loop by giving the program an end-of-input character (usually referred to as end of file). On a Windows machine, that’s Ctrl+Z (Control and Z pressed together) followed by an Enter (return). On a Unix or Linux machine that’s Ctrl+D (Control and D pressed together).

So, what we do is to read a word into current and then compare it to the previous word (stored in previous). If they are the same, we say so:

if (previous == current)         // check if the word is the same as last
          cout << "repeated word: " << current << '\n';

Then we have to get ready to do this again for the next word. We do that by copying the current word into previous:

previous = current;

This handles all cases provided that we can get started. What should this code do for the first word where we have no previous word to compare? This problem is dealt with by the definition of previous:

string previous = " ";      // previous word; initialized to “not a word”

The " " contains only a single character (the space character, the one we get by hitting the space bar on our keyboard). The input operator >> skips whitespace, so we couldn’t possibly read that from input. Therefore, the first time through the while-statement, the test

if (previous == current)

fails (as we want it to).

Image

One way of understanding program flow is to “play computer,” that is, to follow the program line for line, doing what it specifies. Just draw boxes on a piece of paper and write their values into them. Change the values stored as specified by the program.


Image Try This

Execute this program yourself using a piece of paper. Use the input The cat cat jumped. Even experienced programmers use this technique to visualize the actions of small sections of code that somehow don’t seem completely obvious.



Image Try This

Get the “repeated word detection program” to run. Test it with the sentence She she laughed He He He because what he did did not look very very good good. How many repeated words were there? Why? What is the definition of word used here? What is the definition ofrepeated word? (For example, is She she a repetition?)


3.6 Composite assignment operators

Incrementing a variable (that is, adding 1 to it) is so common in programs that C++ provides a special syntax for it. For example:

++counter

means

counter = counter + 1

There are many other common ways of changing the value of a variable based on its current value. For example, we might like to add 7 to it, to subtract 9, or to multiply it by 2. Such operations are also supported directly by C++. For example:

a += 7;      // means a = a+7
b –= 9;      // means b = b–9
c *= 2;      // means c = c*2

In general, for any binary operator oper, a oper= b means a = a oper b (§A.5). For starters, that rule gives us operators +=, –=, *=, /=, and %=. This provides a pleasantly compact notation that directly reflects our ideas. For example, in many application domains *= and /= are referred to as “scaling.”

3.6.1 An example: find repeated words

Consider the example of detecting repeated adjacent words above. We could improve that by giving an idea of where the repeated word was in the sequence. A simple variation of that idea simply counts the words and outputs the count for the repeated word:

int main()
{
         int number_of_words = 0;
         string previous = " ";                 // not a word
         string current;
         while (cin>>current) {
                   ++number_of_words;       // increase word count
                   if (previous == current)
                             cout << "word number " << number_of_words
                                       << " repeated: " << current << '\n';
                   previous = current;
         }
}

We start our word counter at 0. Each time we see a word, we increment that counter:

++number_of_words;

That way, the first word becomes number 1, the next number 2, and so on. We could have accomplished the same by saying

number_of_words += 1;

or even

number_of_words = number_of_words+1;

but ++number_of_words is shorter and expresses the idea of incrementing directly.

Image

Note how similar this program is to the one from §3.5.1. Obviously, we just took the program from §3.5.1 and modified it a bit to serve our new purpose. That’s a very common technique: when we need to solve a problem, we look for a similar problem and use our solution for that with suitable modification. Don’t start from scratch unless you really have to. Using a previous version of a program as a base for modification often saves a lot of time, and we benefit from much of the effort that went into the original program.

3.7 Names

We name our variables so that we can remember them and refer to them from other parts of a program. What can be a name in C++? In a C++ program, a name starts with a letter and contains only letters, digits, and underscores. For example:

x
number_of_elements
Fourier_transform
z2
Polygon

The following are not names:

2x                             // a name must start with a letter
time$to$market      // $ is not a letter, digit, or underscore
Start menu               // space is not a letter, digit, or underscore

When we say “not names,” we mean that a C++ compiler will not accept them as names.

Image

If you read system code or machine-generated code, you might see names starting with underscores, such as _foo. Never write those yourself; such names are reserved for implementation and system entities. By avoiding leading underscores, you will never find your names clashing with some name that the implementation generated.

Names are case sensitive; that is, uppercase and lowercase letters are distinct, so x and X are different names. This little program has at least four errors:

#include "std_lib_facilities.h"

int Main()
{
          STRING s = "Goodbye, cruel world! ";
          cOut << S << '\n';
}

It is usually not a good idea to define names that differ only in the case of a character, such as one and One; that will not confuse a compiler, but it can easily confuse a programmer.


Image Try This

Compile the “Goodbye, cruel world!” program and examine the error messages. Did the compiler find all the errors? What did it suggest as the problems? Did the compiler get confused and diagnose more than four errors? Remove the errors one by one, starting with the lexically first, and see how the error messages change (and improve).


Image

The C++ language reserves many (about 85) names as “keywords.” We list them in §A.3.1. You can’t use those to name your variables, types, functions, etc. For example:

int if = 7;          // error: if is a keyword

You can use names of facilities in the standard library, such as string, but you shouldn’t. Reuse of such a common name will cause trouble if you should ever want to use the standard library:

int string = 7;          // this will lead to trouble

Image

When you choose names for your variables, functions, types, etc., choose meaningful names; that is, choose names that will help people understand your program. Even you will have problems understanding what your program is supposed to do if you have littered it with variables with “easy to type” names like x1, x2, s3, and p7. Abbreviations and acronyms can confuse people, so use them sparingly. These acronyms were obvious to us when we wrote them, but we expect you’ll have trouble with at least one:

mtbf
TLA
myw
NBV

We expect that in a few months, we’ll also have trouble with at least one.

Short names, such as x and i, are meaningful when used conventionally; that is, x should be a local variable or a parameter (see §4.5 and §8.4) and i should be a loop index (see §4.4.2.3).

Don’t use overly long names; they are hard to type, make lines so long that they don’t fit on a screen, and are hard to read quickly. These are probably OK:

partial_sum
element_count
stable_partition

These are probably too long:

the_number_of_elements
remaining_free_slots_in_symbol_table

Our “house style” is to use underscores to separate words in an identifier, such as element_count, rather than alternatives, such as elementCount and ElementCount. We never use names with all capital letters, such as ALL_CAPITAL_LETTERS, because that’s conventionally reserved for macros (§27.8 and §A.17.2), which we avoid. We use an initial capital letter for types we define, such as Square and Graph. The C++ language and standard library don’t use the initial-capital-letter style, so it’s int rather than Int and string rather than String. Thus, our convention helps to minimize confusion between our types and the standard ones.

Avoid names that are easy to mistype, misread, or confuse. For example:

Image

Name       names        nameS
foo            f00              fl
f1               fI                 fi

The characters 0 (zero), o (lowercase O), O (uppercase o), 1 (one), I (uppercase i), and l (lowercase L) are particularly prone to cause trouble.

3.8 Types and objects

The notion of type is central to C++ and most other programming languages. Let’s take a closer and slightly more technical look at types, specifically at the types of the objects in which we store our data during computation. It’ll save time in the long run, and it may save you some confusion.

Image

• A type defines a set of possible values and a set of operations (for an object).

• An object is some memory that holds a value of a given type.

• A value is a set of bits in memory interpreted according to a type.

• A variable is a named object.

• A declaration is a statement that gives a name to an object.

• A definition is a declaration that sets aside memory for an object.

Informally, we think of an object as a box into which we can put values of a given type. An int box can hold integers, such as 7, 42, and –399. A string box can hold character string values, such as "Interoperability", "tokens: !@#$%^&*", and "Old MacDonald had a farm". Graphically, we can think of it like this:

Image

The representation of a string is a bit more complicated than that of an int because a string keeps track of the number of characters it holds. Note that a double stores a number whereas a string stores characters. For example, x stores the number 1.2, whereas s2 stores the three characters '1', '.', and '2'. The quotes for character and string literals are not stored.

Every int is of the same size; that is, the compiler sets aside the same fixed amount of memory for each int. On a typical desktop computer, that amount is 4 bytes (32 bits). Similarly, bools, chars, and doubles are fixed size. You’ll typically find that a desktop computer uses a byte (8 bits) for a bool or a char and 8 bytes for a double. Note that different types of objects take up different amounts of space. In particular, a char takes up less space than an int, and string differs from double, int, and char in that different strings can take up different amounts of space.

Image

The meaning of bits in memory is completely dependent on the type used to access it. Think of it this way: computer memory doesn’t know about our types; it’s just memory. The bits of memory get meaning only when we decide how that memory is to be interpreted. This is similar to what we do every day when we use numbers. What does 12.5 mean? We don’t know. It could be $12.5 or 12.5cm or 12.5gallons. Only when we supply the unit does the notation 12.5 mean anything.

For example, the very same bits of memory that represent the value 120 when looked upon as an int would be 'x' when looked upon as a char. If looked at as a string, it wouldn’t make sense at all and would become a run-time error if we tried to use it. We can illustrate this graphically like this, using 1 and 0 to indicate the value of bits in memory:

Image

This is the setting of the bits of an area of memory (a word) that could be read as an int (120) or as a char ('x', looking at the rightmost 8 bits only). A bit is a unit of computer memory that can hold the value 0 or 1. For the meaning of binary numbers, see §A.2.1.1.

3.9 Type safety

Image

Every object is given a type when it is defined. A program — or a part of a program — is type-safe when objects are used only according to the rules for their type. Unfortunately, there are ways of doing operations that are not type-safe. For example, using a variable before it has been initialized is not considered type-safe:

int main()
{
          double x;                   // we “forgot” to initialize:
                                              // the value of x is undefined
          double y = x;            // the value of y is undefined
          double z = 2.0+x;     // the meaning of + and the value of z are undefined
}

An implementation is even allowed to give a hardware error when the uninitialized x is used. Always initialize your variables! There are a few — very few — exceptions to this rule, such as a variable we immediately use as the target of an input operation, but always to initialize is a good habit that’ll save you a lot of grief.

Complete type safety is the ideal and therefore the general rule for the language. Unfortunately, a C++ compiler cannot guarantee complete type safety, but we can avoid type safety violations through a combination of good coding practice and run-time checks. The ideal is never to use language features that the compiler cannot prove to be safe: static type safety. Unfortunately, that’s too restrictive for most interesting uses of programming. The obvious fallback, that the compiler implicitly generates code that checks for type safety violations and catches all of them, is beyond C++. When we decide to do things that are (type) unsafe, we must do some checking ourselves. We’ll point out such cases as we get to them.

Image

The ideal of type safety is incredibly important when writing code. That’s why we spend time on it this early in the book. Please note the pitfalls and avoid them.

3.9.1 Safe conversions

In §3.4, we saw that we couldn’t directly add chars or compare a double to an int. However, C++ provides an indirect way to do both. When needed, a char is converted to an int and an int is converted to a double. For example:

char c = 'x';
int i1 = c;
int i2 = 'x';

Here both i1 and i2 get the value 120, which is the integer value of the character 'x' in the most popular 8-bit character set, ASCII. This is a simple and safe way of getting the numeric representation of a character. We call this char-to-int conversion safe because no information is lost; that is, we can copy the resulting int back into a char and get the original value:

char c2 = i1;
cout << c << ' << i1 << ' << c2 << '\n';

This will print

x 120 x

In this sense — that a value is always converted to an equal value or (for doubles) to the best approximation of an equal value — these conversions are safe:

bool to char

bool to int

bool to double

char to int

char to double

int to double

The most useful conversion is int to double because it allows us to mix ints and doubles in expressions:

double d1 = 2.3;
double d2 = d1+2;         // 2 is converted to 2.0 before adding
if (d1 < 0)                         // 0 is converted to 0.0 before comparison
          cout << "d1 is negative";

For a really large int, we can (for some computers) suffer a loss of precision when converting to double. This is a rare problem.

3.9.2 Unsafe conversions

Image

Safe conversions are usually a boon to the programmer and simplify writing code. Unfortunately, C++ also allows for (implicit) unsafe conversions. By unsafe, we mean that a value can be implicitly turned into a value of another type that does not equal the original value. For example:

int main()
{
          int a = 20000;
          char c = a;       // try to squeeze a large int into a small char
          int b = c;
          if (a != b)         // != means “not equal”
                    cout << "oops!: " << a << "!=" << b << '\n';
          else
                    cout << "Wow! We have large characters\n";
}

Such conversions are also called “narrowing” conversions, because they put a value into an object that may be too small (“narrow”) to hold it. Unfortunately, few compilers warn about the unsafe initialization of the char with an int. The problem is that an int is typically much larger than achar, so that it can (and in this case does) hold an int value that cannot be represented as a char. Try it to see what value b gets on your machine (32 is a common result); better still, experiment:

int main()
{
          double d = 0;
          while (cin>>d) {            // repeat the statements below
                                                  // as long as we type in numbers
                    int i = d;               // try to squeeze a double into an int
                    char c = i;            // try to squeeze an int into a char
                    int i2 = c;             // get the integer value of the character
                    cout << "d==" << d                          // the original double
                              << " i=="<< i                            // converted to int
                              << " i2==" << i2                       // int value of char
                              << " char(" << c << ")\n";      // the char
          }
}

The while-statement that we use to allow many values to be tried will be explained in §4.4.2.1.


Image Try This

Run this program with a variety of inputs. Try small values (e.g., 2 and 3); try large values (larger than 127, larger than 1000); try negative values; try 56; try 89; try 128; try non-integer values (e.g., 56.9 and 56.2). In addition to showing how conversions from double to int and conversions from int to char are done on your machine, this program shows you what character (if any) your machine will print for a given integer value.


You’ll find that many input values produce “unreasonable” results. Basically, we are trying to put a gallon into a pint pot (about 4 liters into a 500ml glass). All of the conversions

double to int

double to char

double to bool

int to char

int to bool

char to bool

Image

are accepted by the compiler even though they are unsafe. They are unsafe in the sense that the value stored might differ from the value assigned. Why can this be a problem? Because often we don’t suspect that an unsafe conversion is taking place. Consider:

double x = 2.7;
// lots of code
int y = x;               // y becomes 2

By the time we define y we may have forgotten that x was a double, or we may have temporarily forgotten that a double-to-int conversion truncates (always rounds down, toward zero) rather than using the conventional 4/5 rounding. What happens is perfectly predictable, but there is nothing in the int y = x; to remind us that information (the .7) is thrown away.

Conversions from int to char don’t have problems with truncation — neither int nor char can represent a fraction of an integer. However, a char can hold only very small integer values. On a PC, a char is 1 byte whereas an int is 4 bytes:

Image

Image

So, we can’t put a large number, such as 1000, into a char without loss of information: the value is “narrowed.” For example:

int a = 1000;
char b = a;           // b becomes –24 (on some machines)

Not all int values have char equivalents, and the exact range of char values depends on the particular implementation. On a PC the range of char values is [–128:127], but only [0:127] can be used portably because not every computer is a PC, and different computers have different ranges for their char values, such as [0:255].

Image

Why do people accept the problem of narrowing conversions? The major reason is history: C++ inherited narrowing conversions from its ancestor language, C, so from day one of C++, there existed much code that depended on narrowing conversions. Also, many such conversions don’t actually cause problems because the values involved happen to be in range, and many programmers object to compilers “telling them what to do.” In particular, the problems with unsafe conversions are often manageable in small programs and for experienced programmers. They can be a source of errors in larger programs, though, and a significant cause of problems for novice programmers. However, compilers can warn about narrowing conversions — and many do.

C++11 introduced an initialization notation that outlaws narrowing conversions. For example, we could (and should) rewrite the troublesome examples above using a {}-list notation, rather than the = notation:

double x {2.7};           // OK
int y {x};                      // error: double -> int might narrow
int a {1000};                // OK
char b {a};                  // error: int -> char might narrow

When the initializer is an integer literal, the compiler can check the actual value and accept values that do not imply narrowing:

int char b1 {1000};           // error: narrowing (assuming 8-bit chars)
char b2 {48};                    // OK

So what should you do if you think that a conversion might lead to a bad value? Use {} initializers to avoid accidents, and when you want a conversion, check the value before assigning as we did in the first example in this section. See §5.6.4 and §7.5 for a simplified way of doing such checking. The {}-list-based notation is known as universal and uniform initialization and we will see much more of that later on.

Image Drill

After each step of this drill, run your program to make sure it is really doing what you expect it to. Keep a list of what mistakes you make so that you can try to avoid those in the future.

1. This drill is to write a program that produces a simple form letter based on user input. Begin by typing the code from §3.1 prompting a user to enter his or her first name and writing “Hello, first_name” where first_name is the name entered by the user. Then modify your code as follows: change the prompt to “Enter the name of the person you want to write to” and change the output to “Dear first_name,”. Don’t forget the comma.

2. Add an introductory line or two, like “How are you? I am fine. I miss you.” Be sure to indent the first line. Add a few more lines of your choosing — it’s your letter.

3. Now prompt the user for the name of another friend, and store it in friend_name. Add a line to your letter: “Have you seen friend_name lately?”

4. Declare a char variable called friend_sex and initialize its value to 0. Prompt the user to enter an m if the friend is male and an f if the friend is female. Assign the value entered to the variable friend_sex. Then use two if-statements to write the following:

If the friend is male, write “If you see friend_name please ask him to call me.”

If the friend is female, write “If you see friend_name please ask her to call me.”

5. Prompt the user to enter the age of the recipient and assign it to an int variable age. Have your program write “I hear you just had a birthday and you are age years old.” If age is 0 or less or 110 or more, call simple_error("you're kidding!") using simple_error() from std_lib_facilities.h.

6. Add this to your letter:

If your friend is under 12, write “Next year you will be age+1.”

If your friend is 17, write “Next year you will be able to vote.”

If your friend is over 70, write “I hope you are enjoying retirement.”

Check your program to make sure it responds appropriately to each kind of value.

7. Add “Yours sincerely,” followed by two blank lines for a signature, followed by your name.

Review

1. What is meant by the term prompt?

2. Which operator do you use to read into a variable?

3. If you want the user to input an integer value into your program for a variable named number, what are two lines of code you could write to ask the user to do it and to input the value into your program?

4. What is \n called and what purpose does it serve?

5. What terminates input into a string?

6. What terminates input into an integer?

7. How would you write

cout << "Hello, ";
cout << first_name;
cout << "!\n";

as a single line of code?

8. What is an object?

9. What is a literal?

10. What kinds of literals are there?

11. What is a variable?

12. What are typical sizes for a char, an int, and a double?

13. What measures do we use for the size of small entities in memory, such as ints and strings?

14. What is the difference between = and ==?

15. What is a definition?

16. What is an initialization and how does it differ from an assignment?

17. What is string concatenation and how do you make it work in C++?

18. Which of the following are legal names in C++? If a name is not legal, why not?

This_little_pig         This_1_is fine              2_For_1_special
latest thing              the_$12_method         _this_is_ok
MiniMineMine       number                        correct?

19. Give five examples of legal names that you shouldn’t use because they are likely to cause confusion.

20. What are some good rules for choosing names?

21. What is type safety and why is it important?

22. Why can conversion from double to int be a bad thing?

23. Define a rule to help decide if a conversion from one type to another is safe or unsafe.

Terms

assignment

cin

concatenation

conversion

declaration

decrement

definition

increment

initialization

name

narrowing

object

operation

operator

type

type safety

value

variable

Exercises

1 If you haven’t done so already, do the Try this exercises from this chapter.

2 Write a program in C++ that converts from miles to kilometers. Your program should have a reasonable prompt for the user to enter a number of miles. Hint: There are 1.609 kilometers to the mile.

3 Write a program that doesn’t do anything, but declares a number of variables with legal and illegal names (such as int double = 0;), so that you can see how the compiler reacts.

4 Write a program that prompts the user to enter two integer values. Store these values in int variables named val1 and val2. Write your program to determine the smaller, larger, sum, difference, product, and ratio of these values and report them to the user.

5 Modify the program above to ask the user to enter floating-point values and store them in double variables. Compare the outputs of the two programs for some inputs of your choice. Are the results the same? Should they be? What’s the difference?

6 Write a program that prompts the user to enter three integer values, and then outputs the values in numerical sequence separated by commas. So, if the user enters the values 10 4 6, the output should be 4, 6, 10. If two values are the same, they should just be ordered together. So, the input 4 5 4 should give 4, 4, 5.

7 Do exercise 6, but with three string values. So, if the user enters the values Steinbeck, Hemingway, Fitzgerald, the output should be Fitzgerald, Hemingway, Steinbeck.

8 Write a program to test an integer value to determine if it is odd or even. As always, make sure your output is clear and complete. In other words, don’t just output yes or no. Your output should stand alone, like The value 4 is an even number. Hint: See the remainder (modulo) operator in §3.4.

9 Write a program that converts spelled-out numbers such as “zero” and “two” into digits, such as 0 and 2. When the user inputs a number, the program should print out the corresponding digit. Do it for the values 0, 1, 2, 3, and 4 and write out not a number I know if the user enters something that doesn’t correspond, such as stupid computer!.

10 Write a program that takes an operation followed by two operands and outputs the result. For example:

+ 100 3.14
* 4 5

Read the operation into a string called operation and use an if-statement to figure out which operation the user wants, for example, if (operation=="+"). Read the operands into variables of type double. Implement this for operations called +, –, *, /, plus, minus, mul, and div with their obvious meanings.

11 Write a program that prompts the user to enter some number of pennies (1-cent coins), nickels (5-cent coins), dimes (10-cent coins), quarters (25-cent coins), half dollars (50-cent coins), and one-dollar coins (100-cent coins). Query the user separately for the number of each size coin, e.g., “How many pennies do you have?” Then your program should print out something like this:

You have 23 pennies.
You have 17 nickels.
You have 14 dimes.
You have 7 quarters.
You have 3 half dollars.
The value of all of your coins is 573 cents.

Make some improvements: if only one of a coin is reported, make the output grammatically correct, e.g., 14 dimes and 1 dime (not 1 dimes). Also, report the sum in dollars and cents, i.e., $5.73 instead of 573 cents.

Postscript

Please don’t underestimate the importance of the notion of type safety. Types are at the center of most notions of correct programs, and some of the most effective techniques for constructing programs rely on the design and use of types — as you’ll see in Chapters 6 and 9Parts IIIII, and IV.