CSSE2310 / CSSE7231
C Programming Tutorial 3
Functions, Arrays & Strings

This tutorial introduces you to functions, arrays and strings in C. This tutorial is a variant of the original CSSE1000 C Programming Tutorial 3.

Assumed Background

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

Associated Reading

The following textbook sections are relevant:

  • Kernigan & Ritchie (2nd ed.) - sections 1.7 to 1.10, 4.1 to 4.4, 4.9 (we haven't covered all of these concepts yet)
  • Harbison & Steele (5th ed.) - sections 9.1 to 9.8, 4.4, 5.4, 15.7, 7.7 (we haven't covered all of these concepts yet)

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.

Functions

A function is a self-contained part of a program which carries out some well-defined task. One function we've seen so far is printf() - for printing out formatted strings. This function has been provided for us in the Standard C library. In this tutorial we will look at functions written by the programmer.

Functions allow a program to be organised in well defined 'chunks'. Consider the following program which defines a function that calculates powers of integers (e.g. 3 to the power of 2). (You should type (or cut and paste) this in and run it.

#include <stdio.h>

int power(int base, int n) 
{
    int p;

    p = 1;
    while(n > 0) {
        p = p * base;
        n--;
    }
    return p;
}

int main() 
{
    int i;
    int powOf2;

    for (i=0; i < 10; i++) {
        powOf2 = power(2,i);
        printf("%d %d %d\n", i, powOf2, power(-3, i));
    }
    return 0;
}

Elements of this program are explained below:

int power(int base, int n)
{
This line begins the function definition. The first "int" is the return type of the function (i.e. an integer). This is followed by the name of the function (power) and then a list of function arguments or parameters. In this case, the power function takes two arguments - both integers. The function definition includes a group of statements within braces {}.
int p;
This declares a variable - known as a local variable - it is only accessible within the function. Local variables come into existence when the function is called and are deallocated after the function returns. Local variables and the concept of scope are discussed further below. Note also that the argument variables (base and n) are local variables - they come into existence when the function is called.
return p;
This returns the value of p as the result of the function. (Note that it is legal to have multiple return statements in a function - e.g. you might write something like:
if (expression) {
return value_1;
} else {
return value_2;
}
power(2,i)
power(-3,i)
These are function calls - which cause the function to be evaluated using the values of the given arguments. In one case above (power of 2), we assign the value to a variable (powOf2) before passing it as an argument to printf. In the other case (power of -3), we pass the value directly to printf. The choice we have made is arbitrary.

In this example, the function power() is defined before it is used (within the main() function). This isn't always possible - so in these circumstances it is necessary to at least declare the function before it used. A function declaration (also called a function prototype) for power() would look like:

int power(int base, int n);
or
int power(int, int);

i.e. it specifies the name of the function, the types of the parameters and the return type. Note the semicolon at the end rather than opening braces to begin the function definition. (It is not necessary to include the parameter names - and they do not need to match the names used in the function definition. It is useful to include parameter names, however, as a reminder to the programmer as to what arguments are expected. For example, if we wanted to define the main() function before other functions (some programmers prefer to do this) then we'd need to include such a function prototype before main():

#include <stdio.h>

int power(int base, int exponent);

int main() 
{
    int i;
    int powOf2;

    for (i=0; i < 10; i++) {
        powOf2 = power(2,i);
        printf("%d %d %d\n", i, powOf2, power(-3, i));
    }
    return 0;
}

int power(int base, int n) 
{
    int p;

    p = 1;
    while(n > 0) {
        p = p * base;
        n--;
    }
    return p;
}

Exercises

1. Modify the program above so that it produces the same output, but does NOT assign the power of 2 to a variable before printing it.
2. Write a program that asks the user for a number base (integer) and then prints out the first 16 powers of that number (i.e. n0 to n15). You should be able to reuse the power() function - this is why functions are useful.

Void

Some functions don't return anything - in which case they are declared with a return type of void. (We'll see an example below.) Also, in some circumstances, the return type of a function is ignored. For example, the printf() function always returns the number of characters output but because we don't do anything with the return value (e.g. we don't assign it to a variable) this value is lost.

Passing by Value

Note that when a function is called, the arguments are copied into new local variables - i.e. we pass the value of the argument to the function - rather than the variable itself. Any changes made to the local variable affect only that variable and not the original source of the argument, e.g. in the example above, we pass the value of variable i as the second argument to the power() function. Each time the function is called, this value is assigned to a new local variable (called n). Changes to n within the function do not cause the value of i (in the main() function) to change.

Scope

The scope of a variable is the part of the program in which that variable can be used. Variables declared within a function (including the parameters to the function) are only accessible within that function - these are called local variables, i.e. they are said to have local scope. These are sometimes called automatic variables because memory is allocated for these variables automatically every time the function is called (and the memory is reclaimed when the function returns). "Allocating memory" in this case means associating a variable with a particular memory cell (or cells) having a particular address (or addresses). "Reclaiming memory" means that those memory cells are now free to be used for other purposes (e.g. local variables for a different function call).

Variables declared outside of a function are known as global or external variables. They are said to have global scope, though in reality their scope extends from the point of definition to the end of the file. Such variables can be changed from within functions, however, if a local variable is given the same name as a global variable it hides the global variable - i.e. it is no longer possible to access that global variable for the remainder of that function.

Consider the following program. What do you think it will output? Run it and see - and make sure you understand why you're getting the result you are.

#include <stdio.h>

/* Global variables */
int a = 2;
int b, c;

/* Function prototypes */
void function_one(int);
int  function_two(int);
void function_three(int,int);

int main() 
{
    int a;
    int d = 10;
    
    a = 3;
    b = 1;
    c = 5;
    b++;
    function_one(a);
    d = function_two(b);
    function_three(d, a);
    return 0;
}

void function_one(int c) 
{
    a--;
    b -= c;
    return;
}

int function_two(int a) 
{
    c = 10;
    return a * a;
}

void function_three(int a, int f) 
{
    printf("%d %d %d %d\n", a, b, c, f);
}

It is bad practice to write programs like this, because they are very hard to follow and debug. In general, global variables should be avoided where possible.

Other things to note in the program above:

  • Variables can be initialised at the same time they are declared, e.g.
    int d = 10;
  • For a void function (i.e. a function that returns no value), there is no need to include a return statement. In the example above, function_one() has a return statement, but function_three() does not. Neither needs one, but it is acceptable to include one.

Arrays

Arrays allow data items of the same type to be grouped together for more convenient access. For example, instead of declaring many individual variables:

int a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, average;
/* read data in from user */
scanf("%d", &a1);
scanf("%d", &a2);
scanf("%d", &a3);
scanf("%d", &a4);
scanf("%d", &a5);
scanf("%d", &a6);
scanf("%d", &a7);
scanf("%d", &a8);
scanf("%d", &a9);
scanf("%d", &a10);
scanf("%d", &a11);
scanf("%d", &a12);

/* calculate average - note that this does integer arithmetic (i.e. 
** we will get the quotient when dividing by 12, the remainder will
** be discarded
*/
average = (a1+a2+a3+a4+a5+a6+a7+a8+a9+a10+a11+a12) / 12;

it is possible to use arrays:

int a[12], i, average, sum;
sum=0;
/* read data in from user */
for (i=0; i < 12; i++) {
    scanf("%d", &a[i]);
}
/* calculate average */
for (i=0; i < 12; i++) {
    sum += a[i];
}
average = sum/12;  

The declaration
int a[12];
declares a to be an array of 12 integers. These integers can then be accessed using the syntax a[expr] where expr is an expression which evaluates to an integer between 0 and 11 inclusive. (Arrays in C are 0-based, i.e. an array of size n will have elements indexed from 0 to n-1). Some other example array declarations are below:

double d[10];
declares d to be an array of 10 double precision floating point numbers
char name[20];
declares name to be an array of 20 characters. Character arrays are used to store strings in C (discussed below).
int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
declares and initialises an array of integers. In this case no array size is given - it is deduced from the number of elements in the initialisation string (12 in this case).
int nums[10] = {1, 2, 3, 4, 5};
declares an array of 10 integers - the first 5 of which are initialised to the values given (and the remaining 5 will be initialised to 0)

Strings

Strings in C are stored as a null-terminated array of characters. "Null termination" means that the end of the string is indicated with a null character (i.e. ASCII code 0). This means that a string of length n needs an array of length n+1 to hold it. For example, the following declarations are identical:

char courseCode[] = "COMP1300 / 2302";
char courseCode[16] = "COMP1300 / 2302";
char courseCode[] = {'C', 'O', 'M', 'P', '1', '3', '0', '0', ' ', '/', ' ', '2', '3', '0', '2', '\0'};
char courseCode[16] = {'C', 'O', 'M', 'P', '1', '3', '0', '0', ' ', '/', ' ', '2', '3', '0', '2', '\0'};

Note that single quotes (') are used to enclose character constants and double quotes (") are used to enclose string constants. '\0' represents the null character. (Remember that a backslash followed by a character is treated specially, e.g. \n means a newline, \t means a tab). When double quotes are used, the null-termination is implied - you do not have to explicitly specify it.

We're now going to consider a function which operates on strings:

#include <stdio.h>

/* Function to calculate the length of a string, i.e.
** count the number of characters before the first null
** character. The argument is a string of any length
** (as indicated by the []).
*/
int string_length(char str[]) 
{
    int size = 0;
    while(str[size] != '\0') {
        size++;
    }
    return size;
}

int main() 
{
    /* declare space for a string of up to length 79 (we have to 
    ** allow space for a null character also, which doesn't count
    ** as part of the length */
    char string1[80]; 
    printf("Enter a string: ");

    /* Read a line of text (up to 79 characters) into string1 - any
    ** newline will be left as part of the string */
    fgets(string1, 80, stdin);
 
    printf("The length of string \"%s\" is %d\n", string1, string_length(string1));
    return 0;
}

Some things to note about this program:

  • To make printf() print a double quote you must use a backslash before the double quote. This is called escaping the double quote. An unescaped double quote will finish the string.
  • To print a string within a printf() statement, use the formatting code %s
  • The comparison operator != means not-equals.
  • Recall from last week that C interprets a non-zero value as true and a zero value as false. It is therefore possible (but a lot less clear) to write the while loop above as:
    while(string[size]) {
    size++;
    }

Character Arithmetic

C uses the ASCII format to store characters. (e.g. recall from lectures, character '0' is ASCII code 48, 'A' is 65, 'B' is 66, 'a' is 97, 'b' is 98 etc). Because characters are essentially treated as numbers it is possible to do arithmetic with them - e.g. to convert from lower case to upper case:

#include <stdio.h>

void print_in_upper_case(char string[]) 
{
    int cursor = 0;
    while(string[cursor] != '\0') {
        if(string[cursor] >= 'a' && string[cursor] <= 'z') {
            /* Subtract 32 - 32 is the difference in ASCII codes between
            ** lower case and upper case characters */
            string[cursor] -= 32;
        }
        cursor++;
    }
    printf("%s", string);
}

int main() 
{
    char string1[80];
    /* Read a string from the user (will include newline) */
    printf("Enter string: ");
    fgets(string1, 80, stdin);

    print_in_upper_case(string1);
    return 0;
}

Try running the program above. Note that the && operator means "logical AND" - the overall expression will be true if BOTH the part before the && and the part after the && are true. (Similarly, the || operator means "logical OR".)

Exercise

3. Modify the program above to print a string in lower case rather than upper case.

Counting Characters

The following example will count the number of each upper case letter in a string:

#include <stdio.h>

/* Global variable which will hold our character count. 
** Element 0 will count the number of 'A's etc.*/
int char_count[26] ;

void count_chars(char string[]) 
{
    int cursor = 0;
    char current_char;
    int i;
    /* Initialise our counter array */
    for(i=0; i< 26; i++) {
        char_count[i] = 0;
    }
    while(string[cursor] != '\0') {
        current_char = string[cursor];
        if(current_char >= 'A' && current_char <= 'Z') {
            /* Character is a letter - count it */
            char_count[current_char-'A']++;
        }
        cursor++;
    }
}

int main() 
{
    char string1[] = "The quick brown fox jumps over the lazy dog";
    int i;
        
    count_chars(string1);
    for(i=0; i < 26; i++) {
        printf("%c: %d\n", 'A'+i, char_count[i]);
    }
    return 0;
}

Try running the program and look at it's output. Make sure you understand what is happening - in particular with the lines:
char_count[current_char-'A']++;
and
printf("%c: %d\n", 'A'+i, char_count[i]);

Exercises

4. Modify the program above to make it count characters no matter what case they are
5. (Advanced) Write a function which reverses a string and prints it out (i.e. the last character is printed first, the first character is printed last etc).

Note: Array types are NOT passed by value

Note that when an array is passed to a function, it is not passed by value. This is because an array variable does not contain the array data - it just contains a "pointer" to (i.e. memory address of) the first element in the array. We'll cover pointers in a later tutorial.