Fundamental C - Expressions
Written by Harry Fairhead   
Tuesday, 24 May 2022
Article Index
Fundamental C - Expressions
The Rules

For example, if myVar is an int then you can cast it to unsigned int using

(unsigned int)myVar;

However, what you get when you cast depends on the representation in use. For example:

int myVar1 = -1;
unsigned int myVar2 = (unsigned int) myVar1;
printf("%d \n", myVar1);
printf("%u \n", myVar2);

Here myVar1 is a signed int and set to -1 in two's-complement, but when it is assigned to myVar2 its bit pattern is interpreted as 4294967295. If one’s-complement were in use then the bit pattern, and hence the final positive value, would have been different.

You can also change the size of a variable with a cast. If you cast from a smaller variable to a larger – a widening cast – then extra bytes are added. For example:

int myInt=(int) myChar;

Here myChar is just a single byte and, assuming int is four bytes, three bytes have to be added.

You might at this point think that the extra bytes should be zeroed and this is what happens for unsigned casts. If you start out with a negative int and cast it to a wider type then adding zero bytes would change its sign. To stop this happening, signed types use sign bit extension to set the additional bytes. For a signed type if the most significant bit is a zero then the extra bytes are zeroed. If the most significant bit is a one then the extra bytes are set to one. This preserves the value in the narrower type within the wider type. For example:

signed char myChar=1;
int myVar1 = (int) myChar;

In this case, the bit pattern in myChar is 00000001 and this is transferred to the low-order byte of myVar1, the remaining bytes are all set to 0 and both myChar and myVar represent 1 in two’s complement. However for:

signed char myChar=-1;
int myVar1 = (int) myChar;

In this case, the bit pattern in myChar is 11111111 and this is transferred to the low-order byte of myVar1, the remaining bytes are all set to 1. In this case both myChar and myVar represent -1 in two's-complement.

In C the intent of a cast is to leave the bit pattern unchanged and simply interpret it in a different way. There is one small complication in that the C99 standard says that when you assign from a signed value to a unsigned value, what happens if the value cannot be represented (i.e. it was originally negative) is implementation-dependent. What this means in practice is that the bit pattern is simply interpreted as a positive number – the implementation-dependent part is simply taken to be the representation used for the negative range.

An assignment to a narrower type also follows the same pattern of trying to preserve the value of the bit pattern, but this time it might not be possible. What happens is that the wider type is truncated to the smaller size. If the value still represents the original value then so much to the good, if it doesn’t then you don’t get any warnings or error messages.

Put simply, if the wider type is storing a value that is representable by the narrower type, then copying just the lower bytes transfers the value unchanged for signed and unsigned values. Notice that when the value in the wider type cannot be represented by the narrower type what you get depends on the representation used by the machine for negative values. For example:

char myChar;
int myVar=1024;
myChar= (char) myVar;
printf("%d \n",myChar)

In this case myVar stores 1024, which cannot be represented in a single byte, and so the assignment works, but does not preserve the value. The lower byte, which is zero, is transferred to myChar and so we see 0 printed.

Now consider:

char myChar;
int myVar=-1024;
myChar= (char) myVar;
printf("%d \n",myChar);

This too prints 0 because the value stored in myVar cannot be represented in a single byte and the lower byte in myVar is zero, but if one’s-complement was used for negative values, the result would be 1. What you get depends on the representation in use.

The rule is:

  • if the types in the cast are the same size then the bit pattern is unchanged and just reinterpreted as the new type.

  • Casting a signed type to an unsigned type is implementation dependent.

  • In a widening cast the extra bytes are zeroed for unsigned types or the sign is extended for signed types

  • In a narrowing cast the lower bytes are used for the new bit pattern.

The exception to the rule “don’t change the bit pattern” is a cast to a floating-point type. In this case the value is preserved at the expense of the bit pattern. For example:

int myInt=3;
myFloat=(float) myInt;

results in myFloat containing 3.0 and the relationship between the bit patterns in the two variable is complicated. In casting from a floating point type the same technique is used and any fractional part is lost. For example:

int myInt;
float myFloat=3.1415;
myInt=(int) myFloat;

results in myInt containing 3. You don’t need the explicit casts because the rule is that the results are automatically cast to the type of the variable being assigned to.

One of the important uses of casts is in the evaluation of expressions with mixed types. When you write an expression that involves different types the compiler attempts to find casts that make all of the types the same. It first performs integer promotion. Notice that this occurs even if all of the types are involved are chars. That is, chars are cast to ints and this clearly doesn’t change any values. For example:

unsigned char myChar=255;
int myInt;
myInt= myChar*255;

In this case the result 65025 is printed because, although the arithmetic should overflow, both the values are converted to int before the calculation is performed. The expression is evaluated as if it was:

myInt= (int) myChar* (int) 255;

If there are types “larger” then int then casts are implicitly performed to promote everything to the largest size involved in the expression. The order is:

int < long<  long long < float < double < long double

and the unsigned types have the same rank as the corresponding signed type.

Last Updated ( Tuesday, 24 May 2022 )