Export (0) Print
Expand All

Chapter 2 Data, Variables, and Calculations, Section 2

This is where you actually start doing something with the data that you enter. You know how to carry out simple input and output; now, you are beginning the bit in the middle, the “processing” part of a C++ program. Almost all of the computational aspects of C++ are fairly intuitive, so you should slice through this like a hot knife through butter.

The Assignment Statement

You have already seen examples of the assignment statement. A typical assignment statement looks like this:

whole = part1 + part2 + part3;

The assignment statement enables you to calculate the value of an expression that appears on the right-hand side of the equals sign, in this case, the sum of part1, part2, and part3, and store the result in the variable specified on the left-hand side, in this case, the variable with the name whole. In this statement, the whole is exactly the sum of its parts, and no more.

NoteNote

Note how the statement, as always, ends with a semicolon.

You can also write repeated assignments, such as:

a = b = 2;

This is equivalent to assigning the value 2 to b and then assigning the value of b to a, so both variables will end up storing the value 2.

Arithmetic Operations

The basic arithmetic operators you have at your disposal are addition, subtraction, multiplication, and division, represented by the symbols +, -, *, and /, respectively. Generally, these operate as you would expect, with the exception of division, which has a slight aberration when working with integer variables or constants, as you’ll see. You can write statements such as the following:

netPay = hours * rate - deductions;

Here, the product of hours and rate will be calculated and then deductions subtracted from the value produced. The multiply and divide operators are executed before addition and subtraction, as you would expect. I will discuss the order of execution of the various operators in expressions more fully later in this chapter. The overall result of evaluating the expression hours*rate-deductions will be stored in the variable netPay.

The minus sign used in the last statement has two operands — it subtracts the value of its right operand from the value of its left operand. This is called a binary operation because two values are involved. The minus sign can also be used with one operand to change the sign of the value to which it is applied, in which case it is called a unary minus. You could write this:

int a = 0;
int b = -5;
a = -b;                        // Changes the sign of the operand

Here, a will be assigned the value +5 because the unary minus changes the sign of the value of the operand b.

Note that an assignment is not the equivalent of the equations you saw in high-school algebra. It specifies an action to be carried out rather than a statement of fact. The expression to the right of the assignment operator is evaluated and the result is stored in the location specified on the left.

NoteNote

Typically, the expression on the left of an assignment is a single variable name but it doesn't have to be. It can be an expression of some kind but if it is an expression then the result of evaluating it must be an lvalue. An lvalue, as you will see later, is a persistent location in memory where the result of the expression to the right of the assignment operator can be stored.

Look at this statement:

number = number + 1;

This means “add 1 to the current value stored in number and then store the result back in number.” As a normal algebraic statement, it wouldn’t make sense, but as a programming action, it obviously does.

Try it Out: Exercising Basic Arithmetic

You can exercise basic arithmetic in C++ by calculating how many standard rolls of wallpaper are needed to paper a room. The following example does this:

// Ex2_05.cpp
// Calculating how many rolls of wallpaper are required for a room
#include <iostream>
        
using std::cout;
using std::cin;
using std::endl;
        
int main()
{
   double height = 0.0, width = 0.0, length = 0.0; // Room dimensions
   double perimeter = 0.0;                         // Room perimeter
        
   const double rollWidth = 21.0;                  // Standard roll width
   const double rollLength = 12.0*33.0;            // Standard roll length(33ft.)
        
   int strips_per_roll = 0;                        // Number of strips in a roll
   int strips_reqd = 0;                            // Number of strips needed
   int nrolls = 0;                                 // Total number of rolls
        
   cout << endl                                    // Start a new line
        << "Enter the height of the room in inches: ";
   cin >> height;
        
   cout  << endl                                   // Start a new line
         << "Now enter the length and width in inches: ";
   cin >> length >> width;
        
   strips_per_roll = rollLength / height;          // Get number of strips per roll
   perimeter = 2.0*(length + width);               // Calculate room perimeter
   strips_reqd = perimeter / rollWidth;            // Get total strips required
   nrolls = strips_reqd / strips_per_roll;         // Calculate number of rolls
        
   cout << endl
        << "For your room you need " << nrolls << " rolls of wallpaper."
        << endl;
        
   return 0;
}

Unless you are more adept than I am at typing, chances are, there will be a few errors when you compile this for the first time. Once you have fixed the typos, it will compile and run just fine. You’ll get a couple of warning messages from the compiler. Don’t worry about them — the compiler is just making sure you understand what’s going on. I’ll explain the reason for the warning messages in a moment.

TipTip

One thing needs to be clear at the outset — I assume no responsibility for you running out of wallpaper as a result of using this program! As you’ll see, all errors in the estimate of the number of rolls required are due to the way C++ works and to the wastage that inevitably occurs when you hang your own wallpaper — usually 50 percent plus!

I’ll work through the statements in this example in sequence, picking out the interesting, novel, or even exciting features. The statements down to the start of the body of main() are familiar territory by now, so I will take those for granted.

A couple of general points about the layout of the program are worth noting. First, the statements in the body of main() are indented to make the extent of the body easier to see, and second, various groups of statements are separated by a blank line to indicate that they are functional groups. Indenting statements is a fundamental technique in laying out program code in C++. You will see that this is applied universally to provide visual cues to help you identify the various logical blocks in a program.

The const Modifier

You have a block of declarations for the variables used in the program right at the beginning of the body of main(). These statements are also fairly familiar, but there are two that contain some new features:

const double rollWidth = 21.0;                   // Standard roll width
const double rollLength = 12.0*33.0;             // Standard roll length(33ft.)

They both start out with a new keyword: const. This is a type modifier that indicates that the variables are not just of type double, but are also constants. Because you effectively tell the compiler that these are constants, the compiler will check for any statements that attempt to change the values of these variables, and if it finds any, it will generate an error message. You could check this out by adding, anywhere after the declaration of rollWidth, a statement such as:

rollWidth = 0;

You will find the program no longer compiles, returning ‘error C3892: ‘rollWidth’ : you cannot assign to a variable that is const’.

It can be very useful to define constants that you use in a program by means of const variable types, particularly when you use the same constant several times in a program. For one thing, it is much better than sprinkling literals throughout your program that may not have blindingly obvious meanings; with the value 42 in a program, you could be referring to the meaning of life, the universe, and everything, but if you use a const variable with the name myAge that has a value of 42, it becomes obvious that you are not. For another thing, if you need to change the value of a const variable that you are using, you will need to change its definition only in a source file to ensure that the change automatically appears throughout. You’ll see this technique used quite often.

Constant Expressions

The const variable rollLength is also initialized with an arithmetic expression (12.0*33.0). Being able to use constant expressions to initialize variables saves having to work out the value yourself. It can also be more meaningful, as it is in this case, because 33 feet times 12 inches is a much clearer expression of what the value represents than simply writing 396. The compiler will generally evaluate constant expressions accurately, whereas if you do it yourself, depending on the complexity of the expression and your ability to number-crunch, there is a finite probability that it may be wrong.

You can use any expression that can be calculated as a constant at compile time, including const objects that you have already defined. So, for instance, if it were useful in the program to do so, you could declare the area of a standard roll of wallpaper as:

const double rollArea = rollWidth*rollLength;

This statement would need to be placed after the declarations for the two const variables used in the initialization of rollArea, because all the variables that appear in a constant expression must be known to the compiler at the point in the source file where the constant expression appears.

Program Input

After declaring some integer variables, the next four statements in the program handle input from the keyboard:

cout << endl                                     // Start a new line
     << "Enter the height of the room in inches: ";
cin >> height;
        
cout << endl                                      // Start a new line
     << "Now enter the length and width in inches: ";
cin >> length >> width;

Here, you have written text to cout to prompt for the input required, and then read the input from the keyboard using cin, which is the standard input stream. You first obtain the value for the room height and then read the length and width, successively. In a practical program, you would need to check for errors and possibly make sure that the values that are read are sensible, but you don’t have enough knowledge to do that yet!

Calculating the Result

You have four statements involved in calculating the number of standard rolls of wallpaper required for the size of room given:

strips_per_roll = rollLength / height;    // Get number of strips in a roll
perimeter = 2.0*(length + width);         // Calculate room perimeter
strips_reqd = perimeter / rollWidth;      // Get total strips required
nrolls = strips_reqd / strips_per_roll;   // Calculate number of rolls

The first statement calculates the number of strips of paper with a length corresponding to the height of the room that you can get from a standard roll, by dividing one into the other. So, if the room is 8 feet high, you divide 96 into 396, which would produce the floating-point result 4.125. There is a subtlety here, however. The variable where you store the result, strips_per_roll, was declared as int, so it can store only integer values. Consequently, any floating-point value to be stored as an integer is rounded down to the nearest integer, 4 in this case, and this value is stored. This is actually the result that you want here because, although they may fit under a window or over a door, fractions of a strip are best ignored when estimating.

The conversion of a value from one type to another is called type conversion. This particular example is called an implicit type conversion, because the code doesn’t explicitly state that a conversions is needed, and the compiler has to work it out for itself. The two warnings you got during compilation were issued because information could be lost as a result of the implicit conversion that were inserted due to the process of changing a value from one type to another.

You should beware when your code necessitates implicit conversions. Compilers do not always supply a warning that an implicit conversion is being made, and if you are assigning a value of one type to a variable of a type with a lesser range of values, then there is always a danger that you will lose information. If there are implicit conversions in your program that you have included accidentally, then they may represent bugs that may be difficult to locate.

Where such a conversion that may result in the loss of information is unavoidable, you can specify the conversion explicitly to demonstrate that it is no accident and that you really meant to do it. You do this by making an explicit type conversion or cast of the value on the right of the assignment to int, so the statement would become:

strips_per_roll = static_cast<int>(rollLength / height);   // Get number
                                                           // of strips in
                                                           // a roll

The addition of static_cast<int> with the parentheses around the expression on the right tells the compiler explicitly that you want to convert the value of the expression between the parentheses to type int. Although this means that you still lose the fractional part of the value, the compiler assumes that you know what you are doing and will not issue a warning. You’ll see more about static_cast<>() and other types of explicit type conversion later in this chapter.

Note how you calculate the perimeter of the room in the next statement. To multiply the sum of the length and the width by 2.0, you enclose the expression summing the two variables between parentheses. This ensures that the addition is performed first and the result is multiplied by 2.0 to produce the correct value for the perimeter. You can use parentheses to make sure that a calculation is carried out in the order you require because expressions in parentheses are always evaluated first. Where there are nested parentheses, the expressions within the parentheses are evaluated in sequence, from the innermost to the outermost.

The third statement, calculating how many strips of paper are required to cover the room, uses the same effect that you observed in the first statement: the result is rounded down to the nearest integer because it is to be stored in the integer variable, strips_reqd. This is not what you need in practice. It would be best to round up for estimating, but you don’t have enough knowledge of C++ to do this yet. Once you have read the next chapter, you can come back and fix it!

The last arithmetic statement calculates the number of rolls required by dividing the number of strips required (an integer) by the number of strips in a roll (also an integer). Because you are dividing one integer by another, the result has to be an integer, and any remainder is ignored. This would still be the case if the variable nrolls were floating-point. The integer value resulting from the expression would be converted to floating-point form before it was stored in nrolls. The result that you obtain is essentially the same as if you had produced a floating-point result and rounded down to the nearest integer. Again, this is not what you want, so if you want to use this, you will need to fix it.

Displaying the Result

The following statement displays the result of the calculation:

cout << endl
     << "For your room you need " << nrolls << " rolls of wallpaper."
     << endl;

This is a single output statement spread over three lines. It first outputs a newline character and then the text string “For your room you need”. This is followed by the value of the variable nrolls and, finally, the text string “ rolls of wallpaper.”. As you can see, output statements are very easy in C++.

Finally, the program ends when this statement is executed:

   return 0;

The value zero here is a return value that, in this case, will be returned to the operating system. You will see more about return values in Chapter 5.

Calculating a Remainder

You saw in the last example that dividing one integer value by another produces an integer result that ignores any remainder, so that 11 divided by 4 gives the result 2. Because the remainder after division can be of great interest, particularly when you are dividing cookies amongst children, for example, C++ provides a special operator, %, for this. So you can write the following statements to handle the cookie-sharing problem:

int residue = 0, cookies = 19, children = 5;
residue = cookies % children;

The variable residue will end up with the value 4, the number left after dividing 19 by 5. To calculate how many cookies each child receives, you just need to use division, as in the statement:

each = cookies / children;

Modifying a Variable

It’s often necessary to modify the existing value of a variable, such as by incrementing it or doubling it. You could increment a variable called count using the statement:

count = count + 5;

This simply adds 5 to the current value stored in count and stores the result back in count, so if count started out as 10, it would end up as 15.

You also have an alternative, shorthand way of writing the same thing in C++:

count += 5;

This says, “Take the value in count, add 5 to it, and store the result back in count.” We can also use other operators with this notation. For example,

count *= 5;

has the effect of multiplying the current value of count by 5 and storing the result back in count. In general, you can write statements of the form,

lhs op=  rhs;

lhs stands for any legal expression for the left-hand side of the statement and is usually (but not necessarily) a variable name. rhs stands for any legal expression on the right-hand side of the statement. op is any of the following operators:

+

-

*

/

%

<<

>>

&

^

|

You have already met the first five of these operators, and you’ll see the others, which are the shift and logical operators, later in this chapter.

The general form of the statement is equivalent to this:

lhs = lhs op (rhs);

The parentheses around rhs imply that this expression is evaluated first, and the result becomes the right operand for op.

This means that you can write statements such as:

a /= b + c;

This will be identical in effect to this statement:

a = a/(b + c);

Thus, the value of a will be divided by the sum of b and c, and the result will be stored back in a.

The Increment and Decrement Operators

This section introduces some unusual arithmetic operators called the increment and decrement operators. You will find them to be quite an asset once you get further into applying C++ in earnest. These are unary operators that you use to increment or decrement the value stored in a variable that holds an integral value. For example, assuming the variable count is of type int, the following three statements all have exactly the same effect:

count = count + 1;      count += 1;      ++count;

They each increment the variable count by 1. The last form, using the increment operator, is clearly the most concise.

The increment operator not only changes the value of the variable to which you apply it, but also results in a value. Thus, using the increment operator to increase the value of a variable by 1 can also appear as part of a more complex expression. If incrementing a variable using the ++ operator, as in ++count, is contained within another expression, then the action of the operator is to first increment the value of the variable and then use the incremented value in the expression. For example, suppose count has the value 5, and you have defined a variable total of type int. Suppose you write the following statement:

total = ++count + 6;

This results in count being incremented to 6, and this result is added to 6, so total is assigned the value 12.

So far, you have written the increment operator, ++, in front of the variable to which it applies. This is called the prefix form of the increment operator. The increment operator also has a postfix form, where the operator is written after the variable to which it applies; the effect of this is slightly different. The variable to which the operator applies is incremented only after its value has been used in context. For example, reset count to the value 5 and rewrite the previous statement as:

total = count++ + 6;

Then total is assigned the value 11, because the initial value of count is used to evaluate the expression before the increment by 1 is applied. The preceding statement is equivalent to the two statements:

total = count + 6;
++count;

The clustering of "+" signs in the preceding example of the postfix form is likely to lead to confusion. Generally, it isn’t a good idea to write the increment operator in the way that I have written it here. It would be clearer to write:

total = 6 + count++;

Where you have an expression such as a++ + b, or even a+++b, it becomes less obvious what is meant or what the compiler will do. They are actually the same, but in the second case, you might really have meant a + ++b, which is different. It evaluates to one more than the other two expressions.

Exactly the same rules that I have discussed in relation to the increment operator apply to the decrement operator, --. For example, if count has the initial value 5, then the statement

total = --count + 6;

results in total having the value 10 assigned, whereas,

total = 6 + count--;

sets the value of total to 11. Both operators are usually applied to integers, particularly in the context of loops, as you will see in Chapter 3. You will see in later chapters that they can also be applied to other data types in C++, notably variables that store addresses.

Try it Out: The Comma Operator

The comma operator allows you to specify several expressions where normally only one might occur. This is best understood by looking at an example that demonstrates how it works:

// Ex2_06.cpp
// Exercising the comma operator
#include <iostream>
        
using std::cout;
using std::endl;
        
int main()
{
   long num1(0L), num2(0L), num3(0L), num4(0L);
        
   num4 = (num1 = 10L, num2 = 20L, num3 = 30L);
   cout << endl
        << "The value of a series of expressions "
        << "is the value of the rightmost: "
        << num4;
   cout << endl;
        
   return 0;
}
TipTip

If you compile and run this program you will get this output:

The value of a series of expressions is the value of the rightmost: 30

This is fairly self-explanatory. The first statement in main() creates four variables, num1 through num4, and initializes them to zero using functional notation. The variable num4 receives the value of the last of the series of three assignments, the value of an assignment being the value assigned to the left-hand side. The parentheses in the assignment for num4 are essential. You could try executing this without them to see the effect. Without the parentheses, the first expression separated by commas in the series will become:

num4 = num1 = 10L

So, num4 will have the value 10L.

Of course, the expressions separated by the comma operator don’t have to be assignments. You could equally well write the following statements:

long num1(1L), num2(10L), num3(100L), num4(0L); num4 = (++num1, ++num2, ++num3);

The effect of the assignment statement will be to increment the variables num1, num2, and num3 by 1, and to set num4 to the value of the last expression, which will be 101L. This example is aimed at illustrating the effect of the comma operator and is not an example of how to write good code.

The Sequence of Calculation

So far, I haven’t talked about how you arrive at the sequence of calculations involved in evaluating an expression. It generally corresponds to what you will have learned at school when dealing with basic arithmetic operators, but there are many other operators in C++. To understand what happens with these, you need to look at the mechanism used in C++ to determine this sequence. It’s referred to as operator precedence.

Operator Precedence

Operator precedence orders the operators in a priority sequence. In any expression, operators with the highest precedence are always executed first, followed by operators with the next highest precedence, and so on, down to those with the lowest precedence of all. The precedence of the operators in C++ is shown in the following table.

Operators

Associativity

::

Left

() [] -> .

Left

! ~ +(unary) -(unary) ++ -- &(unary) *(unary) (typecast) static_cast const_cast dynamic_cast reinterpret_cast sizeof new delete typeid decltype

Right

.*(unary) ->*

Left

* / %

Left

+ -

Left

<< >>

Left

< <= > >=

Left

== !=

Left

&

Left

^

Left

|

Left

&&

Left

||

Left

?:(conditional operator)

Right

= *= /= %= += -= &= ^= |= <<= >>=

Right

,

Left

There are a lot of operators here that you haven’t seen yet, but you will know them all by the end of the book. Rather than spreading them around, I have put all the C++ operators in the precedence table so that you can always refer back to it if you are uncertain about the precedence of one operator relative to another.

Operators with the highest precedence appear at the top of the table. All the operators that appear in the same cell in the table have equal precedence. If there are no parentheses in an expression, operators with equal precedence are executed in a sequence determined by their associativity. Thus, if the associativity is “left,” the left-most operator in an expression is executed first, progressing through the expression to the right-most. This means that an expression such as a + b + c + d is executed as though it was written (((a + b) + c) + d) because binary + is left-associative.

Note that where an operator has a unary (working with one operand) and a binary (working with two operands) form, the unary form is always of a higher precedence and is, therefore, executed first.

NoteNote

You can always override the precedence of operators by using parentheses. Because there are so many operators in C++, it's sometimes hard to be sure what takes precedence over what. It is a good idea to insert parentheses to make sure. A further plus is that parentheses often make the code much easier to read.

Calculations in C++ can be carried out only between values of the same type. When you write an expression involving variables or constants of different types, for each operation to be performed, the compiler has to arrange to convert the type of one of the operands to match that of the other. This process is called implicit type conversion. For example, if you want to add a double value to a value of an integer type, the integer value is first converted to double, after which the addition is carried out. Of course, the variable that contains the value to be converted is, itself, not changed. The compiler will store the converted value in a temporary memory location, which will be discarded when the calculation is finished.

There are rules that govern the selection of the operand to be converted in any operation. Any expression to be calculated breaks down into a series of operations between two operands. For example, the expression 2*3-4+5 amounts to the series 2*3 resulting in 6, 6-4 resulting in 2, and finally 2+5 resulting in 7. Thus, the rules for converting the type of operands where necessary need to be defined only in terms of decisions about pairs of operands. So, for any pair of operands of different types, the compiler decides which operand to convert to the other considering types to be in the following rank from high to low:

1. long double

2. double

3. float

4. unsigned long long

5. long long

6. unsigned long

7. long

8. unsigned int

9. int

Thus, if you have an operation where the operands are of type long long and type unsigned int, the latter will be converted to type long long. Any operand of type char, signed char, unsigned char, short, or unsigned short is at least converted to type int before an operation.

Implicit type conversions can produce some unexpected results. For example, consider the following statements:

unsigned int a(10u);
signed int b(20);
std::cout << a - b << std::endl;

You might expect this code fragment to output the value –10, but it doesn’t. It outputs the value 4294967286. This is because the value of b is converted to unsigned int to match the type of a, and the subtraction operation results in an unsigned integer value. This implies that if you have to write integer operations that apply to operands of different types, you should not rely on implicit type conversion to produce the result you want unless you are quite certain it will do so.

Type Conversion in Assignments

As you saw in example Ex2_05.cpp earlier in this chapter, you can cause an implicit type conversion by writing an expression on the right-hand side of an assignment that is of a different type from the variable on the left-hand side. This can cause values to be changed and information to be lost. For instance, if you assign an expression that results in a float or double value to a variable of type int or a long, the fractional part of the float or double result will be lost, and just the integer part will be stored. (You may lose even more information if the value of your floating-point result exceeds the range of values available for the integer type concerned.)

For example, after executing the following code fragment,

int number = 0;
float decimal = 2.5f;
number = decimal;

the value of number will be 2. Note the f at the end of the constant 2.5f. This indicates to the compiler that this constant is single-precision floating-point. Without the f, the default would have been type double. Any constant containing a decimal point is floating-point. If you don’t want it to be double-precision, you need to append the f. A capital letter F would do the job just as well.

Explicit Type Conversion

With mixed expressions involving the basic types, your compiler automatically arranges casting where necessary, but you can also force a conversion from one type to another by using an explicit type conversion, which is also referred to as a cast. To cast the value of an expression to a given type, you write the cast in the form:

static_cast<the_type_to_convert_to>(expression)

The keyword static_cast reflects the fact that the cast is checked statically — that is, when your program is compiled. No further checks are made when you execute the program to see if this cast is safe to apply. Later, when you get to deal with classes, you will meet dynamic_cast, where the conversion is checked dynamically — that is, when the program is executing. There are also two other kinds of cast — const_cast for removing the const-ness of an expression, and reinterpret_cast, which is an unconditional cast — but I’ll say no more about these here.

The effect of the static_cast operation is to convert the value that results from evaluating expression to the type that you specify between the angled brackets. The expression can be anything from a single variable to a complex expression involving lots of nested parentheses.

Here’s a specific example of the use of static_cast<>():

  double value1 = 10.5;
  double value2 = 15.5;
  int whole_number = static_cast<int>(value1) + static_cast<int>(value2);

The initializing value for the variable whole_number is the sum of the integral parts of value1 and value2, so they are each explicitly cast to type int. The variable whole_number will therefore have the initial value 25. The casts do not affect the values stored in value1 and value2, which will remain as 10.5 and 15.5, respectively. The values 10 and 15 produced by the casts are just stored temporarily for use in the calculation and then discarded. Although both casts cause a loss of information in the calculation, the compiler will always assume that you know what you are doing when you specify a cast explicitly.

Also, as I described in Ex2_05.cpp relating to assignments involving different types, you can always make it clear that you know the cast is necessary by making it explicit:

strips_per_roll = static_cast<int>(rollLength / height);     //Get number of strips
                                                             // in a roll

You can write an explicit cast for a numerical value to any numeric type, but you should be conscious of the possibility of losing information. If you cast a value of type float or double to type long, for example, you will lose the fractional part of the value when it is converted, so if the value started out as less than 1.0, the result will be 0. If you cast a value of type double to type float, you will lose accuracy because a float variable has only 7 digits precision, whereas double variables maintain 15. Even casting between integer types provides the potential for losing data, depending on the values involved. For example, the value of an integer of type long long can exceed the maximum that you can store in a variable of type int, so casting from a long long value to an int may lose information.

In general, you should avoid casting as far as possible. If you find that you need a lot of casts in your program, the overall design of your program may well be at fault. You need to look at the structure of the program and the ways in which you have chosen data types to see whether you can eliminate, or at least reduce, the number of casts in your program.

Old-Style Casts

Prior to the introduction of static_cast<>() (and the other casts: const_cast<>(), dynamic_cast<>(), and reinterpret_cast<>(), which I’ll discuss later in the book) into C++, an explicit cast of the result of an expression to another type was written as:

(the_type_to_convert_to)expression

The result of expression is cast to the type between the parentheses. For example, the statement to calculate strips_per_roll in the previous example could be written:

strips_per_roll = (int)(rollLength / height);      //Get number of strips in a roll

Essentially, there are four different kinds of casts, and the old-style casting syntax covers them all. Because of this, code using the old-style casts is more error-prone — it is not always clear what you intended, and you may not get the result you expected. Although you will still see the old style of casting used extensively (it’s still part of the language and you will see it in MFC code for historical reasons), I strongly recommend that you stick to using only the new casts in your code.

You can use the auto keyword as the type of a variable in a definition statement and have its type deduced from the initial value you supply. Here are some examples:

auto n = 16;                      // Type is int
auto pi = 3.14159;                // Type is double
auto x = 3.5f;                    // Type is float
auto found = false;               // Type is bool

In each case, the type assigned to the variable you are defining is the same as that of the literal used as the initializer. Of course, when you use the auto keyword in this way, you must supply an initial value for the variable.

Variables defined using the auto keyword can also be specified as constants:

const auto e = 2.71828L;          // Type is const long double

Of course, you can also use functional notation:

const auto dozen(12);                   // Type is const int

The initial value for a variable you define using the auto keyword can also be an expression:

auto factor(n*pi*pi);             // Type is double

In this case, the definitions for the variables n and pi that are used in the initializing expression must precede this statement.

The auto keyword may seem at this point to be a somewhat trivial feature of C++, but you’ll see later in the book, especially in Chapter 10, that it can save a lot of effort in determining complicated variable types and make your code more elegant.

The typeid operator enables you to discover the type of an expression. To obtain the type of an expression, you simply write typeid(expression), and this results in an object of type type_info that encapsulates the type of the expression. Suppose that you have defined variables x and y that are of type int and type double, respectively. The expression typeid(x*y) results in a type_info object representing the type of x*y, which by now you know to be double. Because the result of the typeid operator is an object, you can’t write it to the standard output stream just as it is. However, you can output the type of the expression x*y like this:

cout << "The type of x*y is " << typeid(x*y).name() << endl;

This will result in the output:

The type of x*y is double

You will understand better how this works when you have learned more about classes and functions in Chapter 7. When you use the typeid operator, you must add a #include directive for the typeinfo header file to your program:

#include <typeinfo>

This provides the definition for the type_info type that the typeid operator returns. You won’t need to use the typeid operator very often, but when you do need it, it is invaluable.

The bitwise operators treat their operands as a series of individual bits rather than a numerical value. They work only with integer variables or integer constants as operands, so only data types short, int, long, long long, signed char, and char, as well as the unsigned variants of these, can be used. The bitwise operators are useful in programming hardware devices, where the status of a device is often represented as a series of individual flags (that is, each bit of a byte may signify the status of a different aspect of the device), or for any situation where you might want to pack a set of on-off flags into a single variable. You will see them in action when you look at input/output in detail, where single bits are used to control various options in the way data is handled.

There are six bitwise operators:

& bitwise AND

| bitwise OR

^ bitwise exclusive OR

~ bitwise NOT

>> shift right

<< shift left

The following sections take a look at how each of them works.

The Bitwise AND

The bitwise AND, &, is a binary operator that combines corresponding bits in its operands in a particular way. If both corresponding bits are 1, the result is a 1 bit, and if either or both bits are 0, the result is a 0 bit.

The effect of a particular binary operator is often shown using what is called a truth table. This shows, for various possible combinations of operands, what the result is. The truth table for & is as follows:

Bitwise AND

0

1

0

0

0

1

0

1

For each row and column combination, the result of & combining the two is the entry at the intersection of the row and column. You can see how this works in an example:

char letter1 = 'A', letter2 = 'Z', result = 0;
result = letter1 & letter2;

You need to look at the bit patterns to see what happens. The letters ‘A’ and ‘Z’ correspond to hexadecimal values 0x41 and 0x5A, respectively. The way in which the bitwise AND operates on these two values is shown in Figure 2-9.

Figure 2-9

Referenced Screen

You can confirm this by looking at how corresponding bits combine with & in the truth table. After the assignment, result will have the value 0x40, which corresponds to the character "@".

Because the & produces zero if either bit is zero, you can use this operator to make sure that unwanted bits are set to 0 in a variable. You achieve this by creating what is called a “mask” and combining with the original variable using &. You create the mask by specifying a value that has 1 where you want to keep a bit, and 0 where you want to set a bit to zero. The result of AND-ing the mask with another integer will be 0 bits where the mask bit is 0, and the same value as the original bit in the variable where the mask bit is 1. Suppose you have a variable letter of type char where, for the purposes of illustration, you want to eliminate the high-order 4 bits, but keep the low-order 4 bits. This is easily done by setting up a mask as 0x0F and combining it with the value of letter using & like this:

letter = letter & 0x0F;

or, more concisely:

letter &= 0x0F;

If letter started out as 0x41, it would end up as 0x01 as a result of either of these statements. This operation is shown in Figure 2-10.

Figure 2-10

Referenced Screen

The 0 bits in the mask cause corresponding bits in letter to be set to 0, and the 1 bits in the mask cause corresponding bits in letter to be kept as they are.

Similarly, you can use a mask of 0xF0 to keep the 4 high-order bits, and zero the 4 low-order bits. Therefore, this statement,

letter &= 0xF0;

will result in the value of letter being changed from 0x41 to 0x40.

The Bitwise OR

The bitwise OR, |, sometimes called the inclusive OR, combines corresponding bits such that the result is a 1 if either operand bit is a 1, and 0 if both operand bits are 0. The truth table for the bitwise OR is:

Bitwise OR

0

1

0

0

1

1

1

1

You can exercise this with an example of how you could set individual flags packed into a variable of type int. Suppose that you have a variable called style of type short that contains 16 individual 1-bit flags. Suppose further that you are interested in setting individual flags in the variable style. One way of doing this is by defining values that you can combine with the OR operator to set particular bits on. To use in setting the rightmost bit, you can define:

short vredraw = 0x01;

For use in setting the second-to-rightmost bit, you could define the variable hredraw as:

short hredraw = 0x02;

So, you could set the rightmost two bits in the variable style to 1 with the statement:

style = hredraw | vredraw;

The effect of this statement is illustrated in Figure 2-11. Of course, to set the third bit of style to 1, you would use the constant 0x04.

Because the OR operation results in 1 if either of two bits is a 1, OR-ing the two variables together produces a result with both bits set on.

Figure 2-11

Referenced Screen

A common requirement is to be able to set flags in a variable without altering any of the others that may have been set elsewhere. You can do this quite easily with a statement such as:

style |= hredraw | vredraw;

This statement will set the two rightmost bits of the variable style to 1, leaving the others at whatever they were before the execution of this statement.

The Bitwise Exclusive OR

The exclusive OR, ^, is so called because it operates similarly to the inclusive OR but produces 0 when both operand bits are 1. Therefore, its truth table is as follows:

Bitwise EOR

0

1

0

0

1

1

1

0

Using the same variable values that we used with the AND, you can look at the result of the following statement:

result = letter1 ^ letter2;

This operation can be represented as:

letter1  0100 0001
letter2  0101 1010

EOR-ed together produce:

result  0001 1011

The variable result is set to 0x1B, or 27 in decimal notation.

The ^ operator has a rather surprising property. Suppose that you have two char variables, first with the value ‘A’, and last with the value ‘Z’, corresponding to binary values 0100 0001 and 0101 1010. If you write the statements,

first ^= last;             // Result first is 0001 1011
last ^= first;             // Result last is 0100 0001
first ^= last;             // Result first is 0101 1010

the result of these is that first and last have exchanged values without using any intermediate memory location. This works with any integer values.

The Bitwise NOT

The bitwise NOT, ~, takes a single operand, for which it inverts the bits: 1 becomes 0, and 0 becomes 1. Thus, if you execute the statement,

result = ~letter1;

if letter1 is 0100 0001, the variable result will have the value 1011 1110, which is 0xBE, or 190 as a decimal value.

The Bitwise Shift Operators

These operators shift the value of an integer variable a specified number of bits to the left or right. The operator >> is for shifts to the right, while << is the operator for shifts to the left. Bits that “fall off” either end of the variable are lost. Figure 2-12 shows the effect of shifting the 2-byte variable left and right, with the initial value shown.

Figure 2-12

Referenced Screen

You declare and initialize a variable called number with the statement:

unsigned short number = 16387U;

As you saw earlier in this chapter, you write unsigned integer literals with a letter U or u appended to the number. You can shift the contents of this variable to the left with the statement:

number <<= 2;              // Shift left two bit positions

The left operand of the shift operator is the value to be shifted, and the number of bit positions that the value is to be shifted is specified by the right operand. The illustration shows the effect of the operation. As you can see, shifting the value 16,387 two positions to the left produces the value 12. The rather drastic change in the value is the result of losing the high-order bit when it is shifted out.

You can also shift the value to the right. Let’s reset the value of number to its initial value of 16,387. Then you can write:

number >>= 2;              // Shift right two bit positions

This shifts the value 16,387 two positions to the right, storing the value 4,096. Shifting right 2 bits is effectively dividing the value by 4 (without remainder). This is also shown in the illustration.

As long as bits are not lost, shifting n bits to the left is equivalent to multiplying the value by 2, n times. In other words, it is equivalent to multiplying by 2n. Similarly, shifting right n bits is equivalent to dividing by 2n. But beware: as you saw with the left shift of the variable number, if significant bits are lost, the result is nothing like what you would expect. However, this is no different from the multiply operation. If you multiplied the 2-byte number by 4, you would get the same result, so shifting left and multiply are still equivalent. The problem of accuracy arises because the value of the result of the multiplication is outside the range of a 2-byte integer.

You might imagine that confusion could arise between the operators that you have been using for input and output and the shift operators. As far as the compiler is concerned, the meaning will always be clear from the context. If it isn’t, the compiler will generate a message, but you need to be careful. For example, if you want to output the result of shifting a variable number left by 2 bits, you could write the following statement:

cout << (number << 2);

Here, the parentheses are essential. Without them, the shift operator will be interpreted by the compiler as a stream operator, so you won’t get the result that you intended; the output will be the value of number followed by the value 2.

The right-shift operation is similar to the left-shift. For example, suppose the variable number has the value 24, and you execute the following statement:

number >>= 2;

This will result in number having the value 6, effectively dividing the original value by 4. However, the right shift operates in a special way with signed integer types that are negative (that is, the sign bit, which is the leftmost bit, is 1). In this case, the sign bit is propagated to the right. For example, declare and initialize a variable number of type char with the value –104 in decimal:

char number = -104;        // Binary representation is 1001 1000

Now you can shift it right 2 bits with the operation:

number >>= 2;              // Result 1110 0110

The decimal value of the result is –26, as the sign bit is repeated. With operations on unsigned integer types, of course, the sign bit is not repeated and zeros appear.

NoteNote

You may be wondering how the shift operators, << and >>, can be the same as the operators used with the standard streams for input and output. These operators can have different meanings in the two contexts because cin and cout are stream objects, and because they are objects it is possible to redefine the meaning of operators in context by a process called operator overloading. Thus the >> operator has been redefined for input stream objects such as cin so you can use it in the way you have seen. The << operator has also been redefined for use with output stream objects such as cout. You will learn about operator overloading in Chapter 8.

Every expression in C++ results in either an lvalue or an rvalue (sometimes written l-value and r-value and pronounced like that). An lvalue refers to an address in memory in which something is stored on an ongoing basis. An rvalue, on the other hand, is the result of an expression that is stored transiently. An lvalue is so called because any expression that results in an lvalue can appear on the left of the equals sign in an assignment statement. If the result of an expression is not an lvalue, it is an rvalue.

Consider the following statements:

int a(0), b(1), c(2);
a = b + c;
b = ++a;
c = a++;

The first statement declares the variables a, b, and c to be of type int and initializes them to 0, 1, and 2, respectively. In the second statement, the expression b+c is evaluated and the result is stored in the variable a. The result of evaluating the expression b+c is stored temporarily in a memory location and the value is copied from this location to a. Once execution of the statement is complete, the memory location holding the result of evaluating b+c is discarded. Thus, the result of evaluating the expression b+c is an rvalue.

In the third statement, the expression ++a is an lvalue because its result is a after its value is incremented. The expression a++ in the third statement is an rvalue because it stores the value of a temporarily as the result of the expression and then increments a.

An expression that consists of a single named variable is always an lvalue.

NoteNote

This is by no means all there is to know about lvalues and rvalues. Most of the time, you don’t need to worry very much about whether an expression is an lvalue or an rvalue but sometimes you do. Lvalues and rvalues will pop up at various times throughout the book so keep the idea in mind.

All variables have a finite lifetime when your program executes. They come into existence from the point at which you declare them and then, at some point, they disappear — at the latest, when your program terminates. How long a particular variable lasts is determined by a property called its storage duration. There are three different kinds of storage duration that a variable can have:

  • Automatic storage duration

  • Static storage duration

  • Dynamic storage duration

Which of these a variable will have depends on how you create it. I will defer discussion of variables with dynamic storage duration until Chapter 4, but you will be exploring the characteristics of the other two in this chapter.

Another property that variables have is scope. The scope of a variable is simply that part of your program over which the variable name is valid. Within a variable’s scope, you can legally refer to it, either to set its value or to use it in an expression. Outside of the scope of a variable, you cannot refer to its name — any attempt to do so will cause a compiler error. Note that a variable may still exist outside of its scope, even though you cannot refer to it by name. You will see examples of this situation a little later in this discussion.

All the variables that you have declared up to now have had automatic storage duration, and are therefore called automatic variables. Let’s take a closer look at these first.

Automatic Variables

The variables that you have declared so far have been declared within a block — that is, within the extent of a pair of braces. These are called automatic variables and are said to have local scope or block scope. An automatic variable is “in scope” from the point at which it is declared until the end of the block containing its declaration. The space that an automatic variable occupies is allocated automatically in a memory area called the stack that is set aside specifically for this purpose. The default size for the stack is 1MB, which is adequate for most purposes, but if it should turn out to be insufficient, you can increase the size of the stack by setting the /STACK option for the project to a value of your choosing.

An automatic variable is “born” when it is defined and space for it is allocated on the stack, and it automatically ceases to exist at the end of the block containing the definition of the variable. This will be at the closing brace matching the first opening brace that precedes the declaration of the variable. Every time the block of statements containing a declaration for an automatic variable is executed, the variable is created anew, and if you specified an initial value for the automatic variable, it will be reinitialized each time it is created. When an automatic variable dies, its memory on the stack will be freed for use by other automatic variables. Let’s look at an example demonstrating some of what I’ve discussed so far about scope.

Try it Out: Automatic Variables

The following example shows the effect of scope on automatic variables:

// Ex2_07.cpp
// Demonstrating variable scope
#include <iostream>
        
using std::cout;
using std::endl;
        
int main()
{                                    // Function scope starts here
   int count1 = 10;
   int count3 = 50;
   cout << endl
        << "Value of outer count1 = " << count1
        << endl;
        
   {                                // New scope starts here...
      int count1 = 20;              // This hides the outer count1
      int count2 = 30;
      cout << "Value of inner count1 = " << count1
           << endl;
      count1 += 3;                  // This affects the inner count1
      count3 += count2;
   }                                // ...and ends here
        
   cout << "Value of outer count1 = " << count1
        << endl
        << "Value of outer count3 = " << count3
        << endl;
        
   // cout << count2 << endl;       // uncomment to get an error
        
   return 0;
}                                   // Function scope ends here

The output from this example will be:

Value of outer count1 = 10
Value of inner count1 = 20
Value of outer count1 = 10
Value of outer count3 = 80
TipTip

The first two statements declare and define two integer variables, count1 and count3, with initial values of 10 and 50, respectively. Both these variables exist from this point to the closing brace at the end of the program. The scope of these variables also extends to the closing brace at the end of  main().

Remember that the lifetime and scope of a variable are two different things. It’s important not to get these two ideas confused. The lifetime is the period during execution from when the variable is first created to when it is destroyed and the memory it occupies is freed for other uses. The scope of a variable is the region of program code over which the variable may be accessed.

Following the variable definitions, the value of count1 is output to produce the first of the lines shown above. There is then a second brace, which starts a new block. Two variables, count1 and count2, are defined within this block, with values 20 and 30, respectively. The count1 declared here is different from the first count1. The first count1 still exists, but its name is masked by the second count1. Any use of the name count1 following the declaration within the inner block refers to the count1 declared within that block.

I used a duplicate of the variable name count1 here only to illustrate what happens. Although this code is legal, it isn’t a good approach to programming in general. In a real-world programming environment, it would be confusing, and if you use duplicate names, it makes it very easy to hide variables defined in an outer scope accidentally.

The value shown in the second output line shows that within the inner block, you are using the count1 in the inner scope — that is, inside the innermost braces:

cout << "Value of inner count1 = " << count1
     << endl;

Had you still been using the outer count1, then this would display the value 10. The variable count1 is then incremented by the statement:

count1 += 3;               // This affects the inner count1

The increment applies to the variable in the inner scope, since the outer one is still hidden. However, count3, which was defined in the outer scope, is incremented in the next statement without any problem:

count3 += count2;

This shows that the variables that were declared at the beginning of the outer scope are accessible from within the inner scope. (Note that if count3 had been declared after the second of the inner pair of braces, then it would still be within the outer scope, but in that case, count3 would not exist when the above statement was executed.)

After the brace ending the inner scope, count2 and the inner count1 cease to exist. The variables count1 and count3 are still there in the outer scope, and the values displayed show that count3 was indeed incremented in the inner scope.

If you uncomment the line,

// cout << count2 << endl;          // uncomment to get an error

the program will no longer compile correctly because it attempts to output a nonexistent variable. You will get an error message something like,

c:\microsoft visual studio\myprojects\Ex2_07\Ex2_07.cpp(29) : error
     C2065: 'count2' : undeclared identifier

This is because count2 is out of scope at this point.

Positioning Variable Declarations

You have great flexibility as to where you can place the declarations for your variables. The most important aspect to consider is what scope the variables need to have. Beyond that, you should generally place a declaration close to where the variable is to be first used in a program. You should write your programs with a view to making them as easy as possible for another programmer to understand, and declaring a variable at its first point of use can be helpful in achieving that.

It is possible to place declarations for variables outside of all of the functions that make up a program. The next section looks what effect that has on the variables concerned.

Global Variables

Variables that are declared outside of all blocks and classes (I will discuss classes later in the book) are called globals and have global scope (which is also called global namespace scope or file scope). This means that they are accessible throughout all the functions in the file, following the point at which they are declared. If you declare them at the very top of your program, they will be accessible from anywhere in the file.

Globals also have static storage duration by default. Global variables with static storage duration will exist from the start of execution of the program until execution of the program ends. If you do not specify an initial value for a global variable, it will be initialized with 0 by default. Initialization of global variables takes place before the execution of main() begins, so they are always ready to be used within any code that is within the variable’s scope.

Figure 2-13 shows the contents of a source file, Example.cpp, and the arrows indicate the scope of each of the variables.

Figure 2-13

Referenced Screen

The variable value1, which appears at the beginning of the file, is declared at global scope, as is value4, which appears after the function main(). The scope of each global variable extends from the point at which it is defined to the end of the file. Even though value4 exists when execution starts, it cannot be referred to in main() because main() is not within the variable’s scope. For main() to use value4, you would need to move its declaration to the beginning of the file. Both value1 and value4 will be initialized with 0 by default, which is not the case for the automatic variables. Note that the local variable called value1 in function() hides the global variable of the same name.

Since global variables continue to exist for as long as the program is running, this might raise the question in your mind, “Why not make all variables global and avoid this messing about with local variables that disappear?” This sounds very attractive at first, but as with the Sirens of mythology, there are serious side effects that completely outweigh any advantages you may gain.

Real programs are generally composed of a large number of statements, a significant number of functions, and a great many variables. Declaring all variables at the global scope greatly magnifies the possibility of accidental erroneous modification of a variable, as well as making the job of naming them sensibly quite intractable. They will also occupy memory for the duration of program execution. By keeping variables local to a function or a block, you can be sure they have almost complete protection from external effects, they will only exist and occupy memory from the point at which they are defined to the end of the enclosing block, and the whole development process becomes much easier to manage. That’s not to say you should never define variables at global scope. Sometimes, it can be very convenient to define constants that are used throughout the program code at global scope.

If you take a look at the Class View pane for any of the examples that you have created so far and extend the class tree for the project by clicking on the [unfilled] symbol, you will see an entry called Global Functions and Variables. If you click on this, you will see a list of everything in your program that has global scope. This will include all the global functions, as well as any global variables that you have declared.

Try it Out: The Scope Resolution Operator

As you have seen, a global variable can be hidden by a local variable with the same name. However, it’s still possible to get at the global variable by using the scope resolution operator (::), which you saw in Chapter 1 when I was discussing namespaces. I can demonstrate how this works with a revised version of the last example:

// Ex2_08.cpp
// Demonstrating variable scope
#include <iostream>
        
using std::cout;
using std::endl;
        
int count1 = 100;                         // Global version of count1
        
int main()
{                                         // Function scope starts here
   int count1 = 10;
   int count3 = 50;
   cout << endl
        << "Value of outer count1 = " << count1
        << endl;
   cout << "Value of global count1 = " << ::count1            // From outer block
        << endl;
        
 {                                // New scope starts here...
      int count1 = 20;            // This hides the outer count1
      int count2 = 30;
      cout << "Value of inner count1 = " << count1
           << endl;
      cout << "Value of global count1 = " << ::count1         // From inner block
           << endl;
        
    count1 += 3;                  // This affects the inner count1
      count3 += count2;
   }                              // ...and ends here.
        
   cout << "Value of outer count1 = " << count1
        << endl
        << "Value of outer count3 = " << count3
        << endl;
        
   //cout << count2 << endl;        // uncomment to get an error
   return 0;
}                                   // Function scope ends here

If you compile and run this example, you’ll get the following output:

Value of outer count1 = 10
Value of global count1 = 100
Value of inner count1 = 20
Value of global count1 = 100
Value of outer count1 = 10
Value of outer count3 = 80
TipTip

The shaded lines of code indicate the changes I have made to the previous example; I just need to discuss the effects of those. The declaration of count1 prior to the definition of the function main() is global, so in principle, it is available anywhere through the function main(). This global variable is initialized with the value of 100:

int count1 = 100;                         // Global version of count1

However, you have two other variables called count1, which are defined within main(), so, throughout the program, the global count1 is hidden by the local count1 variables. The first new output statement is:

cout << "Value of global count1 = " << ::count1               // From outer block
     << endl;

This uses the scope resolution operator (::) to make it clear to the compiler that you want to reference the global variable count1, not the local one. You can see that this works from the value displayed in the output.

In the inner block, the global count1 is hidden behind two variables called count1: the inner count1 and the outer count1. We can see the global scope resolution operator doing its stuff within the inner block, as you can see from the output generated by the statement we have added there:

cout << "Value of global count1 = " << ::count1               // From inner block
     << endl;

This outputs the value 100, as before — the long arm of the scope resolution operator used in this fashion always reaches a global variable.

You have seen earlier that you can refer to a name in the std namespace by qualifying the name with the namespace name, such as with std::cout or std::endl. The compiler searches the namespace that has the name specified by the left operand of the scope resolution operator for the name that you specify as the right operand. In the preceding example, you are using the scope resolution operator to search the global namespace for the variable count1. By not specifying a namespace name in front of the operator, you are telling the compiler to search the global namespace for the name that follows it.

You’ll see a lot more of this operator when you get to explore object-oriented programming in Chapter 9, where it is used extensively.

Static Variables

It’s conceivable that you might want to have a variable that’s defined and accessible locally, but which also continues to exist after exiting the block in which it is declared. In other words, you need to declare a variable within a block scope, but to give it static storage duration. The static specifier provides you with the means of doing this, and the need for this will become more apparent when we come to deal with functions in Chapter 5.

In fact, a static variable will continue to exist for the life of a program even though it is declared within a block and available only from within that block (or its sub-blocks). It still has block scope, but it has static storage duration. To declare a static integer variable called count, you would write:

static int count;

If you don’t provide an initial value for a static variable when you declare it, then it will be initialized for you. The variable count declared here will be initialized with 0. The default initial value for a static variable is always 0, converted to the type applicable to the variable. Remember that this is not the case with automatic variables.

NoteNote

If you don't initialize your automatic variables, they will contain junk values left over from the program that last used the memory they occupy.

Ivor Horton’s Beginning Visual C++® 2010, Copyright © 2010 by Ivor Horton, ISBN: 978-0-470-50088-0, Published by Wiley Publishing, Inc., All Rights Reserved. Wrox, the Wrox logo, Wrox Programmer to Programmer, and related trade dress are trademarks or registered trademarks of John Wiley & Sons, Inc and/or its affiliates.

Show:
© 2014 Microsoft