CSSE2310 / CSSE7231
C Programming Tutorial 6
Other Features of C

This tutorial covers various aspects of C programming which we haven't yet seen (or haven't seen in any detail). This includes bitwise operations, operator precedence, type sizes, type casting and dynamic memory allocation. This tutorial is a variant of the original CSSE1000 C Programming Tutorial 6.

Assumed Background

It is assumed that you are familiar with the material covered in previous tutorials and the associated readings.

Associated Reading

The following textbook sections are relevant:

  • Kernigan & Ritchie (2nd ed.)
    • Bitwise operators - section 2.9 (page 48 - 49)
    • Logical operators - section 2.6 (page 41 - 42)
    • Operator precedence - section 2.12 (page 52 - 54)
    • sizeof operator - page 135
    • type casting - section 2.7 (pages 42 - 46)
    • Dynamic memory allocation - malloc - section 7.8.5 (page 167)
  • Harbison & Steele (5th ed.)
    • Bitwise operators - sections 7.5.5, 7.6.6
    • Logical operators - section 7.7
    • Operator precedence - section 7.2
    • sizeof operators - section 7.5.2
    • type casting - section 7.5.1
    • dynamic memory allocation - malloc - section 16.1

You should read the above references. Other material within these texts is also relevant.

Environment

You may wish to create a new directory for this tutorial. You may type (cut and paste) each program in a separate C file or you may just use one C file and just overwrite your previous program each time (except if you're writing a program that spans multiple files).

Bitwise Operations

C has bitwise NOT, AND, OR and EXCLUSIVE-OR operators along with logical NOT, AND and OR operators. The symbols for these are shown below:

Bitwise not (i.e. inversion or 1's complement) ~
and &
or |
exclusive or ^
Logical not !
and &&
or ||

Bitwise operators operate on all the bits of the operands independently, so a&b will perform an AND of bit 0, bit 1, bit 2 etc and ~a will flip each bit of a. Logical operators consider the whole operand to be either true or false where non-zero means true and zero means false. Typically, logical operations will result in either a 1 (true) or 0 (false). For example 12 && 6 is 1, but 12 & 6 is 4. (Make sure you understand why this is the case.)

Try to predict the output of the following program. Run it and see if you were right.

#include <stdio.h>

int main() 
{
    int a, b;
    a = 0xAA; /* 10101010 in binary - padded out with leading 0's */
    b = 0xDC; /* 11011100 in binary - padded out with leading 0's */
    printf ("a = %d, ~a = %d (%u)\n", a, ~a, ~a);
    printf ("a = %d, !a = %d\n", a, !a);
    printf ("a = %d, b = %d, a&b = %d\n", a, b, a&b);
    printf ("a = %d, b = %d, a&&b = %d\n", a, b, a&&b);
    printf ("a = %d, b = %d, a|b = %d\n", a, b, a|b);
    printf ("a = %d, b = %d, a||b = %d\n", a, b, a||b);
    printf ("a = %d, b = %d, a^b = %d\n", a, b, a^b);
    return 0;
}

Note that in the first printf line we print ~a using both the %d format as well as %u. The first treats ~a as a signed integer, the second as an unsigned integer.

Operator Precedence

We saw in the last tutorial that some operators have a higher precedence than others (in that case the structure member operator "." had a higher precedence than the indirection operator "*"). All operators in C have a certain precedence relative to other operators. For example the expression a + b * c is interpreted as a + (b * c) rather than (a + b) * c because the * (multiplication) operator has a higher precedence than the + (addition) operator. See table 2.1 in Kernighan & Ritchie (page 53), table 7-3 in Harbison & Steele (page 205) or the online reference for a complete listing of all operator precedences.

Try to predict the output of the following program, Run it and see if you were right.

#include <stdio.h>

int main() 
{
    int a = 1;
    int b = 2;
    int c = 3;
    int d = 5;
    printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d);
    printf("a - b + c = %d\n", a - b + c);
    printf("a | b + c = %d\n", a | b + c);
    printf("a + c ^ d = %d\n", a + c ^ d);
    printf("a + c %% d %% c = %d\n", a + c % d % c);
    printf("a & c ^ d = %d\n", a & c ^ d);
    return 0;
}

One key thing to keep in mind here is that if you are ever in any doubt about the precedence of operators (or if you think anyone reading your code will have doubts) - use parentheses to make the precedence explicit!

Type Sizes

The sizes of C data types differ between implementations. For example, an integer (int) may occupy 32 bits on a PC or Sun server, but only 16 bits on an AVR microcontroller. Note also that when multiple data types are grouped into structures, the size of the structure may not equal the sum of the sizes of the components. This is because of alignment requirements (e.g. 4 byte items always starting at an address which is a multiple of 4) and these will differ from machine to machine.

The C sizeof() operator can be used to find the size (in bytes) of various data types.

Try running the following program and examine the output. Make sure you understand why you get the answers you get. Ask a tutor if you're not sure.

#include <stdio.h>

typedef struct 
{
    char c;
    double d;
} charAndDouble;

typedef struct 
{
    char c;
    int i;
} charAndInt;

typedef struct 
{
    char c;
    char d;
} twoChars;

typedef struct 
{
    int a;
    double d;
} intAndDouble;

int main() 
{
    printf("Size of short int: %d\n", sizeof(short));
    printf("Size of int: %d\n", sizeof (int));
    printf("Size of long int: %d\n", sizeof(long));
    printf("Size of char: %d\n", sizeof(char));
    printf("Size of float: %d\n", sizeof(float));
    printf("Size of double: %d\n", sizeof(double));
    printf("Size of charAndDouble: %d\n", sizeof(charAndDouble));
    printf("Size of charAndInt: %d\n", sizeof(charAndInt));
    printf("Size of twoChars: %d\n", sizeof(twoChars));
    printf("Size of intAndDouble: %d\n", sizeof(intAndDouble));
    return 0;
}

Type Casting

Often it is necessary to convert an element of one type to another. In many cases, C does this automatically, but in some cases it must be done "manually". This can be done with a type casting operator, which takes the form

(type-name) expression

i.e. the name of the type to convert to is placed in parentheses before the expression to be converted.

For example, to convert an integer i to a double d we can write

double d;
int i;

d = (double)i;

(in this case, we could just write d = i; and C would automatically take care of the cast).

To obtain the floating point result of an integer division we can write

float  quotient;
int dividend, divisor;

quotient = (float)dividend / divisor;

This converts dividend to floating point and the "/" is then taken to mean floating point division (so divisor will automatically be cast to a floating point number before the division). (Note that the cast to float has a higher precedence than the division operator.)

Without the cast,

quotient = dividend / divisor;

is the same as

quotient = (float)(dividend  / divisor);

i.e. we do integer division (and discard the remainder) and then convert the result to a float (which will give us a different result).

Run this program and see what result you get:

#include <stdio.h>

int main() 
{
    float f1, f2;
    int a = 234234;
    int b = 3213;
    f1 = a/b;
    f2 = (float)a/b;
    printf("a/b = %g\na/b = %g\n", f1, f2);
    return 0;
}

Dynamic Memory Allocation

In previous array and string examples we've always known in advance the maximum number of objects to be stored and declared the array appropriately, e.g.

int a[100];

declares a to be an array that holds 100 integers.

In many cases, it is not possible to know in advance how many items are to be stored. Either one must declare an array that is the maximum possible size (which can waste memory) or use dynamic memory allocation. Dynamic memory allocation allocates memory at run-time (rather than hardwires the allocation at compile time).

We use the malloc() function to allocate memory. Malloc expects a number of bytes as an argument and returns a pointer to the allocated memory. (Remember, a pointer to memory is just a memory address - in this case the address of the beginning of the allocated memory.) Note that because malloc can be used for allocating memory for the storage of any sort of data type, it returns a pointer of type void*. We need to cast this to the correct data type. An example of the use of malloc is below - which creates an array of characters of a size input by the user. The program also demonstrates (shown in red) how to malloc an array of something larger than characters - you must multiply the number of elements required by the size of the type to get the total number of bytes to be allocated.

#include <stdio.h>

/* Stdlib defines rand() - for generating random numbers 
*/
#include <stdlib.h>

/* time.h defines time() - which we use to seed our random
** number generator.
*/
#include <time.h>

int main() 
{
    char* string;
    int* numbers;
    int size;
    int i;

    /*
    ** Seed the random number generator with the current time (this is so we
    ** get a different sequence of random numbers each time we run the program).
    */
    srand(time(0));

    
    printf("Enter array size: ");
    scanf("%d", &size);
    printf("Array size = %d\n", size);

    /* Allocate storage for this many characters.
    ** We add 1 to size so we have room for a null
    ** character. */
    string = (char *)malloc(size + 1);

    /* Allocate storage for this many integers */
    numbers = (int *)malloc(size * sizeof(int));

    /* Populate the arrays with random values - upper case letters
    ** (ASCII codes 65 to 90)
    ** in the string and random integers in the array of integers.
    ** Note that rand() will return a random integer (full 32 bit range if 
    ** integers are 32 bits wide). For our random character we take the 
    ** remainder of the number when divided by 26 (giving us a random
    ** number from 0 to 25 inclusive. We add this to 65 to get an ASCII
    ** character in the range 65 to 90 (i.e. A to Z). For our random
    ** integer, we take the remainder of a random number when divided by
    ** 10000 - this gives us a number in the range 0 to 9999.
    */
    for(i = 0; i < size; i++) {
        string[i] = 65 + rand()%26;
        numbers[i] = rand()%10000;
    }

    /* Null terminate the string by putting a null character 
    ** in the last position (remember, the length of the character
    ** array is size+1
    */
    string[size] = 0;

    /* Print the random string out */
    printf("Random string is %s\n", string);

    /* Print out the random integers */
    printf("Random numbers are:\n");
    for(i=0; i < size; i++) {
        printf("%d\n", numbers[i]);
    }
        
    /* Free the memory (Many operating systems will do 
    ** this anyway, but best to be tidy!) */
    free(string);
    free(numbers);

    return 0;
}

Try running the program several times to see the different results.