Appendix 3 of Absolute C++
Lecture
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!
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.
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 double
s when converting to int
s.
We also have explicit type conversion.
Suppose, for example, that m
and n
are int
s, 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 double
s. To explicitly convert them to double
s 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; |
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 - char
s,
int
s, bool
s, and double
s - be represented by zeros and ones? Our understanding
of types will really depend on being able to answer these questions.
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 2^{k} and adding, we divide by 2^{k} and then mod by 2^{k}. 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 2^{3}. [Note the exponent is one less than the number of digits needed.]
11
/ 2^{3 }= 1 and 11 % 2^{3} = 3. Repeating this using the
3 and reducing the power we get
3 / 2^{2 }= 0 and 3 % 2^{2} = 3. Repeating this
using the 3 and reducing the power we get
3 / 2^{1 }= 1 and 3 % 2^{1} = 1. Repeating this
using the 1 and reducing the power we get
1 / 2^{0 }= 1 and 1 % 2^{0} = 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?
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.
char
sIn
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 char
s
and int
s. 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
.
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!
Last modified by LT M. Johnson 08/15/2007 09:32 AM