ECE-1021 

Data Types

(Last Mod: 27 November 2010 21:38:38 )

ECE-1021 Home



Objectives


Overview

A "data type" encompasses all of the knowledge needed to access a collection of bits and determine what information is stored there or, conversely, to determine what collection of bits must be used to represent some piece of information. There are two parts to this - first is the question of how many bits are required and second is how those bits must be interpreted.

Nearly all operations involving objects must have both parts in order to proceed. For instance, it isn't enough to know that a particular value is encoded as an integer in two's complement form. The compiler must also know how many bytes are used to represent it. Similarly, it isn't enough to know that a particular value is stored in four bytes. Among other options, the bit pattern stored in those four bytes might be interpreted as an unsigned integer, a signed integer using two's compliment representation, or a floating point value using the IEEE 754 Single Precision Floating Point representation. The compiler must know exactly which is actually used.


Data Type Hierarchy

Not all types are included in the following table. Most notably, function types, void types, enumeration types, and union types are omitted.

  Type Declarator
Aggregate Array   type identifier[size]
Structure   struct identifier {list };
Scalar Arithmetic Integer Signed signed char (see note 2)
short int
int
long int
long long int  (see note 1)
Unsigned _Bool (see note 1)
unsigned char (see note 2)
unsigned short int
unsigned int
unsigned long int
unsigned long long int  (see note 1)
Floating Real Floating float
double
long double

Complex

(see note 1)

float _Complex
double _Complex
long double _Complex
Pointer   type *
OTHER GROUPINGS:

Character types: char, signed char, unsigned char.

Real types: The combination of the Integer types and the Real Floating types.

Basic types: Another name for the Arithmetic types

 

NOTES:

NOTE 1: These types are new to C99 and are not available on ANSI-conforming C89 and earlier compilers.

NOTE 2: The "plain" char data type is implementation-defined, but is either a signed char or an unsigned char.

 


Type Casting

By declaring a data type for each object we create, we give the compiler the information it needs in order to perform most of the translations necessary to work with the values stored in these objects without us having to get involved in the underlying mechanics. When the compiler performs these translations on its own it is called an "implicit type cast" or "implicit conversion". Not surprisingly then, when we instruct the compiler to perform one of these translations, it is called an "explicit type cast" or "explicit conversion".

Implicit Conversion

Example 1:

int k;

double y;

float x;

 

x = k + y;

As with most operators, when two values are added the two operands must be represented the same way. In this case one is represented as an integer and the other as a double precision floating point value. The compiler's rules are designed to prevent the unintended loss of data so before this operation is carried out the value stored in the integer is "promoted" to a value represented as a double. The value has not changed, just the representation.

Furthermore, nothing has happened to the value stored in k - it is still the same value stored with the same representation. What has happened is that a temporary copy of the value, in the appropriate representation, has been created.

The type of the expression k+y is the same as the type of the two operands which, after the implicit conversion, are both of type double. But now we must store this value in an object of type float. To do this, we "demote" the result of the expression and, possibly, lose some data since a float can't represent the range or precision of values that a double can. The compiler has no choice but to do this although, in most cases, it will issue a warning stating that there is a potential loss of data.

Explicit Conversion

Example 2:

int k;

double y;

float x;

 

x = (float) (k + y);

Let's say that, after compiling the code in Example 1 and being issued the warning about the possible loss of data due to the conversion, we examine the values that we expect k and y to take on and conclude that there is no danger of experiencing an unacceptable loss of data from this operation.

Instead of just tolerating this warning every time we compile our code, we should suppress the warning so that, if possible, the only warnings that are issued are ones that we do not want to ignore. Otherwise we risk missing an important warning because it is lost in a sea of warnings that we are expecting and know we can ignore. To suppress the warning during future compile attempts, we instruct the compiler to perform an explicit conversion by type casting the result of the expression to a float before assigning it to the variable x.

Notice that we surrounded the entire expression with parentheses. This is because the typecast operator's operand is whatever is immediately to its right. Without the parentheses, only the k would get cast. It would then get implicitly converted to a double, added to y, and the result of the addition operation would be of type double which would have to be implicitly demoted to a float. 

Example 3:

int j, k;

double x, y, z;

 

j = 36;

k = 10;

 

x = j / k;                    /* x will get 3   */

y = (double) j / (double) k;  /* y will get 3.6 */

z = (double) (j / k);         /* z will get 3   */

 

When the compiler sees a division operator with two integer operands, it performs integer division which includes the discarding of any fraction part. It will issue no warning because there has been no unintended loss of data - the loss of the fractional part is an intentional consequence of using this operator with two integer operands!

If we want to algebraic quotient, instead of the integer quotient, then one of operands must be something other than an integer - that is the only way that the compiler has of knowing whether we want integer division or floating point division. Technically we only need to cast either the numerator or the denominator, but this is such a common logic error on the part of programmers - especially since the compiler will not issue a warning - that is it good practice to cast the numerator if it is an integer expression and to cast the denominator if it is an integer expression, even if this means casting both of them.

Unlike before, where it we needed to cast the result of the expression, here we need to cast the operands in the expression. Otherwise, the expression will be evaluated using integer division - because that is what we told it to do - and the result will get cast to a double before being assigned to z. This is a useless cast because this conversion would have occurred implicitly in any case.

Be sure you understand why we cast the expression result in Example 2 and the expression operands in Example 3. In the former case we had no problem with how the expression itself was evaluated, the problem was with what happened to the result after it was evaluated. In the latter case we wanted to exert influence over how the expression itself was evaluated.

Caution Regarding Excessive Typecasting

When we perform an explicit type cast, the compiler assumes we know what we are doing and will issue no warning. This is not a trivial point and its implications need to be appreciated. Many of the warnings that the compiler will issue stem from an attempt to identify cases where we have told the compiler to do something that it is perfectly capable of doing but where we might not have taken into account all of the ramifications of doing so.

Most compilers are pretty good about catching the more common instances when this has happened. In essence, the compiler is saying, "I understand what you have told me to do and, while I could, are you really sure that I should?" When we perform an explicit type cast we are telling the compiler, "Yes, you should. Now stop warning me and just do it."

There is a limit to how much the compiler can protect us from ourselves and excessive use of typecasts runs the risk of inadvertently giving the compiler firm marching orders to do something that we really didn't want it to do. Only perform casts that serve a purpose and only use casts to suppress warnings after you are confident that the conversion is safe to perform.