Class 5: Types and Expressions III



Appendix 3 of Absolute C++



Everything's an Expression ... Almost

Almost everything in a C++ program is an expression. An expression is something that has a type and, after it's been evaluated, a value.

The most familiar expressions are arithmetical. For example, if k is an int that's been assigned the value 4, (k - 2)*7 is an expression of type int and value 14.

One example of something that is less obviously an expression is k = 5, where k is once again a variable of type int. The type of this expression is int, and the value is 5. In general, an assignment expression has the same type as the object being assigned to, and the same value as that object after the assignment is carried out. Thus, oddly enough, if x is a variable of type double, then (x = 3.4)*2.0 makes perfect sense, it is an expression of type double, and after it is evaluated, it has value 6.8. This is our first explicit example of a side effect. The expression has type and value, but additionally it has the side effect that evaluating the expression changes the value of the variable x.

Another example of something that probably doesn't seem like an expression but is in fact, is cout << x. As we saw last lecture, cout is an object of type ostream. The expression cout << x also has type ostream, and its value is just cout (like multiplying zero by anything, you still get zero). However, there is a side-effect to evaluating this expression, namely that x gets written out to the screen!


What's not an Expression?

At this point it may seem like everything's an expression, but that's not true. For example, anything with a ; at the end is a statement, not an expression. So while k = 4 is an expression, the statement k = 4; is not. Declarations of variables, something like int k for example, are not expressions - regardless of whether the ; is there. Still, most things are expressions, and understanding this fact and being able to identify the types and values of expressions are key to understanding C++ ... and most any other programming language.


When Types Collide - Conversion

Things get interesting when expressions involve different types. For example, what is the type and value of the expression x*k, where x is of type double with value 3.3, and k is of type int with value -2? The answer is type double and value -6.6. The explanation is this:

C++ knows how multiply two int objects, and it knows how to multiply two double objects, it doesn't know how to multiply one of each. However, it understands that an int can be converted to a double and vice versa. So it converts one and performs the multiplication on two objects of the same type. But which way should it go? For arithmetic, types are always converted in the direction that gives the most precision - this is referred to as type promotion - which in this case means that the int is converted (or promoted) to a double, and the operation is performed on two doubles. It wouldn't make nearly as much sense the other way round, would it?

This implicit type conversion (implicit meaning that it happens automatically behind the scenes, without you doing anything directly) happens in other cases. The only one affecting us right now is assignments. You can assign an object of one type to an object of a different type, as long as C++ knows how to do the conversion. If it doesn't, the compiler will let you know. So, for example, x is of type double with value 3.3, and k is of type int, then k = x is an expression of type int with value 3. C++ truncates doubles when converting to ints.

We also have explicit type conversion. Suppose, for example, that m and n are ints, n being the larger. We'd like to print out the value of m/n. Well,

cout << m/n << endl;

will just print out zero! (Make sure you know why!) We'd like to get some fractional value, in other words, we'd like these values treated as doubles. To explicitly convert them to doubles first we'd write:

cout << double(m)/double(n) << endl;

Challenge: can you explain what type and value you get with:

                                   m / double(n)


 Explicit conversion can get tricky later on, but at this stage it's as simple as writing the new type name followed by the old object in ()'s.


Some Quick Conversion Rules

int --> double : This does exactly what you'd expect.

double --> int : This simply truncates the value, meaning that whatever's after the decimal point just gets chopped. You can get in trouble if the double value is too big.

bool --> int : false goes to 0, true goes to 1.

int --> bool : 0 goes to false, everything else goes to true;


Representing Data in a Computer

You've probably heard terms like bits and bytes used in connection with computers, and you've probably heard people say that inside a computer everything is 0's and 1's. If not, I'll say it now: Inside a computer everything is 0's and 1's! (A bit is just a 0/1 value.) But how can all of these things - chars, ints, bools, and doubles - be represented by zeros and ones? Our understanding of types will really depend on being able to answer these questions.


Binary Numbers

First we'll look how 0's and 1's suffice to represent any integer number, then we'll look at other types of objects. When we deal with numbers we use the decimal number system, i.e. the base 10 number system. This means that all our numbers (lets look at non-negative integers for now) look like sequences of decimal digits, which are numbers in the range [0,9]. A number like 3027 is short-hand:

3027 ---> 3*10^3 + 0*10^2 + 2*10^1 + 7*10^0

Or, for another example,

1011 ---> 1*10^3 + 0*10^2 + 1*10^1 + 1*10^0

In the binary number system we have the same idea, but the base is now 2 rather than 10. So, binary digits are in the range [0,1], and now 1011 has a different interpretation. In binary it is short-hand for:

1011 ---> 1*2^3 + 0*2^2 + 1*2^1 + 1*2^0 = 2^3 + 2 + 1 = 11 (in decimal)

So, in binary the decimal number 11 is represented as 1011. The binary number 1001 = 2^3 + 1 = 9, for another example. With four bits, i.e. four binary digits, we can represent any number from 0 up to 15 (which is 2^3 + 2^2 + 2^1 + 2^0). With four decimal digits I can represent from 0 up to 9999, i.e. from 0 up to 10000 - 1. So we need more bits than decimal digits, but given enough bits we can represent any number we care to. Using k-bits, we can represent the numbers from 0 up to 2^k - 1.

There are two ways to convert a decimal number to a binary number.  The first method is the reverse of the above and is the easiest to use if you know the maximum number of binary digits the decimal number will need.  Basically instead of multiplying by 2k and adding, we divide by 2k and then mod by 2k.  For example, suppose we want to convert the decimal number 11 into a four digit binary number.  Since we know we only need a four digit binary number then the highest power of 2 we need to divide by is 23. [Note the exponent is one less than the number of digits needed.]

11 / 23 = 1 and 11 % 23 = 3.  Repeating this using the 3 and reducing the power we get
3 / 22 = 0 and 3 % 22  = 3.  Repeating this using the 3 and reducing the power we get
3 / 21 = 1 and 3 % 21  = 1.  Repeating this using the 1 and reducing the power we get
1 / 20 = 1 and 1 % 20  = 0. 

Now since we can't reduce the power any further, our answer is the result of the division taken in the order performed and we get 1011.

The second method is easier to use if you don't know how many binary digits the decimal number will use.  It basically does a repeated modulus by 2 and division until the answer to the division is zero.  Suppose we want to again convert the decimal number 19 into a binary number.

19 % 2 = 1 and 19 / 2  = 9.  Repeating this using the 9 we get
9 % 2 = 1 and 9 / 2 = 4.  Repeating this using the 4 we get
4 % 2 = 0 and 4 / 2 = 2.  Repeating this using the 2 we get
2 % 2 = 0 and 2 / 2 = 1.  Repeating this using the 1 we get
1 % 2 = 1 and 1 / 2 = 0.  We are finished because the answer to the division is 0.

To get the correct binary number, our answer is the result of the modulus taken in the reverse order performed.  Doing this we get 10011.  Notice it took 5 binary digits.  Now if we want the decimal number 19 to be expressed as an 8 digit binary number, we would simply pad the left with sufficient zeros to obtain 8 binary digits and we get 00010011.  Can we express the decimal number 19 using 4 binary digits?


Bytes - How Type Depends on Interpreting Bit-Sequences

The memory of a computer is simply one long sequence of bits. However, these bits are organized into chunks of 8 called bytes. To emphasize, a byte consists of 8-bits. In a byte, we can represent the numbers from 0 to 255.

The type bool is just a way of interpreting a byte of memory. If all 8 bits of the byte are zero, the interpretation as a bool is false. Otherwise, the interpretation of a bool is true.

The type char is just a different way of interpreting a byte of memory! For example, the byte 01100001 is interpreted as the character a. This intepretation of bytes as characters is called the ASCII encoding, and this table, for example, shows you the whole thing. Interpreting 01100001 as a number in binary, we get the number 97, and if you look up 97 in the table, you'll see that it corresponds to the character a.

Already we see one of the fundamental ideas behind computing, different types of objects may be represented by treating sequences of 0's and 1's in different ways. That's why C++ needs to keep track of the types of objects, so it knows how to interpret the contents of the chunk of memory associated with each object.


A Note on chars

In fact, you can look at a char as just being a small integer (I say small because 8-bits only allows us the range [0,255]). This interpretation pretty much tells us what to expect of conversions between chars and ints. One interesting feature of this match-up between characters and numbers is that statements like 'b' - 'a' make perfect sense. Looking at the ASCII table, we see that 'b' corresponds to the number 98, and 'a' to the number 97. So C++ treats this as the int subtraction problem 98 - 97, which evaluates to 1. In fact, the letters of the alphabet appear in order, so that a is 97, b is 98, ..., z is 122. So, char('b' + 3) is the character e.


Other Types

A full int on your PC consists of 4 bytes, or 32 bits, so it can represent very large numbers. We're not going to get into the question of how negative numbers are represented in binary. Essentially an int looks like the binary number representation we just talked about, but in 32 bits. So, The int 5 is represented in the computer as:

00000000 00000000 00000000 00000101 

... where I've broken things up into bytes to make it all a little clearer.

A double takes up 8 bytes, or 64 bits. The format is more complex, however, and we will not go over it here, except to say that it is a binary version of the familiar scientific notation. However, instead of a base of 10, it uses a base of two. (Example: 12 is represented as 1.5 x 2^3.) Let it suffice to say that the double 1.0 is represented by the following 64 bits:

00000011 11111111 11111111 00000000 00000000 00000000 00000000 00000000



        Converting radians to degrees, minutes, and seconds. This requires conversions between types! See if you can identify where they happen!

        GPA Calculator

Assoc Prof Christopher Brown

Last modified by LT M. Johnson 08/15/2007 09:32 AM