C Programming Tutorial 3: Control Flow and Functions

1.0 Introduction

A C program comprises of global data and functions. A program must have a main function and the execution starts at the first statement in the main function. A function has local data and statements. The control flow deals with the order in which statements are executed by a program. In this post, we will take a close look at control statements and functions.

2.0 Statements

A statement is a expression terminated with a semicolon. A block comprises of zero or more statements enclosed between a pair of braces. Syntactically, a block can be used in all places where a single statement is used. The scope of variables defined in a block ranges from the point of definition to the closing brace of the block. For example, consider the program below.

#include <stdio.h>
#include <stdlib.h>

int main ()
{
    for (int i = 0, sum = 0; i < 10; i++) {
        sum += i;
        if (i == 9)
            printf ("Sum = %d\n", sum);
    }
}   

The scope of variables i and sum is the block associated with the for loop.

3.0 If statement

The if statement has the following form.

if (expression)
    statement
else
    statement

A statement, above, can be a single statement, or a block comprising of zero or more statements inside matching braces. If expression evaluates as non-zero, the statement(s) just after if are executed. Otherwise, statement(s) after else are executed. The else part is optional. And, the statement after if or else can itself be another if statement. Consider the following code.

int i = 8;
int j = 10;

if (i == 7)
    if (j == 10)
        printf ("One\n");
else
     printf ("Two\n");

The question is whether else is paired with the first if or the second if. The answer is that an else is paired with the most recent if. So, it is paired with the second if. And since the second if is the statement to be executed only if the expression for the first if holds true, and since i is 8, the expression for the first if is false and the code does not print anything. If we want to pair the else with the first if, we need put braces around the statement after the first if as shown below.

int i = 8;
int j = 10; 

if (i == 7) {
    if (j == 10) 
        printf ("One\n");
}   
else
     printf ("Two\n");

An interesting usage of if statement is when it is used as the statement in the else part of the previous if. Consider the following usage.

if (expr-1)
    statement-1;
else if (expr-2)
    statement-2;
else if (expr-3)
    statement-3;
...
else
    statement-n;

This usage is known as the if-else-if. The execution starts with the first if. As soon as an expression evaluates as non-zero, the statement(s) after that if are executed and the execution of the entire if-else-if is over. If none of the expressions evaluate non-zero, the statement(s) after the last else, if present, are executed. For example, consider the following code for printing the day of the week.

void print_day (int day)
{
    if (day == 0)
        printf ("Sunday\n");
    else if (day == 1)
        printf ("Monday\n");
    else if (day == 2)
        printf ("Tuesday\n");
    else if (day == 3)
        printf ("Wednesday\n");
    else if (day == 4)
        printf ("Thursday\n");
    else if (day == 5)
        printf ("Friday\n");
    else if (day == 6)
        printf ("Saturday\n");
    else
        printf ("Error\n");
}

4.0 Switch statement

A switch statement is of the form,

switch (integer-expression) {
    case constant-1 : statement;
                      statement;
                      ...
                      break;

    case constant-2 : statement;
                      statement;
                      ...
                      break;
    ...

    default: statement;
             statement;
              ...
     
}

It starts with the switch keyword, followed by an integer expression in parentheses. Then, there are multiple case statements inside braces. The integer expression is evaluated and the control goes on to the case statement with constant equal to the value of the integer expression. Often, a default option is put after all the case statements and the control goes to the default if the value of the integer expression does not match any of the constants with the case statements. The cases and the default can be in any order and the default is optional. The control flow goes on till the closing brace. If a break statement is found, the control immediately goes to the closing brace. However, if there are no break statements, the control flow "falls through", that is, all the following statements are executed, including those that are next to other cases. It is quite common to club case statements with common processing. For example, suppose you wish to find number of days in a month, a la, "thirty days hath September, ...".

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

enum {January = 1, February, March, April, May, June, July,
                   August, September, October, November, December};

int days_in_month (int month, bool leap)
{
    switch (month) {
        case January:
        case March:
        case May:
        case July:
        case August:
        case October:
        case December: return 31;
        case February: return (leap ? 29 : 28);
        case April:
        case June:
        case September:
        case November: return 30;
        default: return -1;
    }
}

int main ()
{
    printf ("January = %d\nFebruary = %d\nFebruary (leap year) = %d\nDecember = %d\n",
                days_in_month (January, 0),
                days_in_month (February, 0),
                days_in_month (February, 1),
                days_in_month (December, 0));
}

In the above program, break statements are not necessary, because the return statements, anyway, break the control flow and return to the calling function. We can compile and run this program, as below.

$ gcc try.c -o try
$ ./try
January = 31
February = 28
February (leap year) = 29
December = 31

The switch statement is often a substitute for the If-Else-If construct.

5.0 Loops

There are three loops, the while loop, for loop and the do while loop.

5.1 The while Loop

while (expression)
    statement

The while loop starts with the while keyword, followed by an expression in parentheses. Then, there is the statement, which may be a single statement or a block comprising of zero or more statements. For each iteration, the expression is evaluated. If the expression evaluates as zero, the while loop terminates. Otherwise, that is, the expression evaluates as non-zero and another iteration in the loop is done and the statement(s) under the while loop are executed.

5.2 for Loop

for (expr1; expr2; expr3)
    statement

The expression, expr1, is evaluated once at the start of the loop. expr2 is the condition that is evaluated at the start of each iteration. An iteration is done only if expr2 evaluates non-zero. The expression expr3 is evaluated at the end of each iteration. Any of the three expressions can be omitted but the two semicolons must always be present. The processing of the for loop can be expressed as below.

expr1;
while (expr2) {
    statement
    ....
    expr3;
}

The for loop is well suited to processing a fixed number of items like the elements of an array, members of a linked list, etc.

5.3 do while loop

do {
    statement
    ...
} while (expr);

The statement(s) inside the braces are executed at least once. (The braces are not necessary if there is only one statement.) At the end of each iteration, expr is executed. If it evaluates non-zero, another iteration is started. If expr evaluates as zero, the loop terminates. Note the semicolon at the end of the do-while loop. The do-while loop is used much less than the while and for loops. But it is useful, especially in cases when at least one iteration is to be done and nothing special is required to be done at the start of the loop.

6.0 break statement

break;

The break statement breaks the control flow out of the innermost while, for, do-while loops and the switch statement. The control flow resumes at the next statement after the closing brace of the concerned loop or switch statement.

7.0 continue statement

continue;

The continue statement immediately causes the next iteration of the innermost while, do while or the for loop. The control flow goes to the statement just after the opening brace of the concerned loop statement. In case of while and do-while, the control passes right away to statement after the opening brace. In the case of for statement, the third expression in the parentheses of the for loop is evaluated and the control flow continues to the first statement after the opening brace of the for loop.

8.0 Functions

A C program comprises of functions. One of the functions is the main function. The program execution starts with the main function. C is a procedural language. To develop a program you need to conceptualize the procedure or the algorithm to solve a problem. Traditionally, a big problem is broken into smaller sub-problems. The solution of smaller sub-problems put together makes the solution of the bigger problem. Eventually, the solution to a small problem is implemented in a function. It is recommended that each function be a cohesive unit. A function should be small and do a specific job well.

In C language, the parameters are passed by value to the functions. That is, a function gets its own local copy of parameters, passed on the stack. It can modify the passed parameters, but the corresponding variables in the calling function are not affected. If it is required that a function access and/or modify the variables in the calling function, a pointer to the concerned variable can be passed. Now, the pointer is passed by value; so the the called function cannot modify the pointer variable in the calling function. But it can use the pointer to access the variable and read or modify it.

A function returns a value, which can be used by the calling function.

As an example, we will write a function to find the value of a number raised to the power of another number. Our numbers are non-negative integers. We call this function xpowery. It takes in two integers x and y and returns x to the power y. Our algorithm is based on fact that if the power is even, we can square the base number and divide the power by 2. To take care of odd values of power, we subtract 1 from power and multiply a product of factors by the base number. That is,

factor = 1;
while (power != 1) {
    if (power is odd) {
        power--;
        factor *= base_number;
    } else if (power is even) {
        power /= 2;
        base_number *= base_number;
    }
}
result = factor * base_number;

And, the program is as given below.

#include <stdio.h>
#include <stdlib.h>

unsigned long xpowery (unsigned long x, unsigned long y);
int main ()
{
    unsigned long x, y;

    while (scanf ("%lu %lu", &x, &y) != EOF)
        printf ("%lu\n", xpowery (x, y));
}

unsigned long xpowery (unsigned long x, unsigned long y)
{
    unsigned long factor = 1;

    if (!y) return 1; // y == 0
    if (!x) return 0; // x == 0

    while (y != 1) {
        if (y % 2) { // y is odd
            factor *= x;
            y--;
            continue;
        }
        x *= x;
        y /= 2;
    }
    return factor * x;
}

The function xpowery needs to be declared before use in the main function. A function declaration like,

unsigned long xpowery (unsigned long x, unsigned long y);

is called a function prototype. The compiler checks the parameter types declared in the function prototype with those in the function calls and also function definition. In case of mismatch, a compile time error is generated, which needs to be corrected before the program is compiled OK. We can compile and run the above program as below.

$ gcc try.c -o try
$ ./try
10 3
1000
25 3
15625
900 4
656100000000
125 4
244140625
$ 

9.0 Recursion

A function may call itself and in doing so it is said to be working recursively. Why is recursion important? The reason is that some problems are inherently recursive. For example, we can define natural numbers like this.

  • 1 is the first natural number.
  • The successor to a natural number is the next natural number.

A recursive solution is generally neat, compact, elegant and intuitive. However, for most recursive algorithms, iterative solution can be developed. In recursion, the stack grows for each recursive call and extensive recursion may lead to stack overflow. The iterative solutions may be more robust and free from memory problems.

As an example, consider the problem of finding the factorial of a number. Factorials are defined for non-negative whole numbers. The symbol for factorial is the exclamation mark and is defined for a number n, as

  • if (n == 0), n! = 1
  • if (n > 0), n! = n * (n - 1)!

The program for finding factorial for a number is as follows.

#include <stdio.h>
#include <stdlib.h>

unsigned long factorial (unsigned long);

int main ()
{
     unsigned long number;

     while (scanf ("%lu", &number) != EOF)
         printf ("%lu! = %lu\n", number, factorial (number));
}

unsigned long factorial (unsigned long number)
{
    if (number == 0)
        return 1;
    return number * factorial (number - 1);
}

We can compile and run the above program as below.

$ gcc try.c -o try
$ ./try
0
0! = 1
1
1! = 1
2
2! = 2
3
3! = 6
4
4! = 24
5
5! = 120
6
6! = 720
7
7! = 5040
9
9! = 362880
10
10! = 3628800