Skip to content

User-Defined Functions

Instead of simply calling a pre-defined functions, C also allows us to write our own function. To put it simply, a function is a named abstraction of a computation. By calling the name of the function, we can perform the computation.

One of the important property we want in a function is modularity. This means that the same function should behave in the same way in different context. By context, we mean the set of mapping from name to values.

So how do we make a function behave differently? That's where the parameters come in. Parameters are the way we can communicate with a function. By starting with different values of parameters, the function may give different computation. The function then communicate back the result via the return value.

Without the parameters, the function is equivalent to a constant value. Take a function that always returns the value 42, for instance. Every call to this function can always be replaced by 42 without changing the behaviour of the program.

Defining a Functions

There are two parts to a function definition. The first is the declaration of function prototype and the second is the function definition that contains the computation.

Syntax

Function Prototype
1
2
<return type> <function name>(<parameter type>, <parameter type>, ...); // minimal definition
<return type> <function name>(<parameter type> <parameter name>, <parameter type> <parameter name>, ...);
Function Definition
1
2
3
<return type> <function name>(<parameter type> <parameter name>, <parameter type> <parameter name>, ...) {
  <function body>
}

Example

To illustrate this, let us try to solve the following problem:

Washer

Washer

Compute the volume of a flat washer. The dimensions of a flat washer are usually given as the following values:

  • Inner diameter
  • Outer diameter
  • Thickness

We assume that all the dimensions are given in a standard unit.

Ring

If we abstract the flat washer mathematically, we get a structure that looks like a ring or a donut from the top. Of course, the thickness is hidden here. But we can still compute the area of the rim.

Assuming that we have the following mapping:

  • Inner diameter: d1
  • Outer diameter: d2

We can calculate the area as the the area of the outer circle subtracted with the area of the inner circle. The computation of the area can be done using the formula \(\pi\)r2, where r is the radius. But what we have is the diameter and not the radius. So first, we have to compute the radius by simply halving the diameter. This gives us the formula below:

rim area = \(\pi\)(d2 \(\div\) 2)2 - \(\pi\)(d1 \(\div\) 2)2

Translating that into code, with all the preamble, we get the following code:

Washer.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <math.h>
#define PI 3.14159
int main(void) {
  double d1, // inner diameter
         d2, // outer diameter
         thickness, outer_area, inner_area, rim_area, volume;      

  // read input data
  printf("Enter inner diameter, outer diameter, thickness: ");
  scanf("%lf %lf %lf", &d1, &d2, &thickness);

  // compute volume of a washer
  outer_area = PI * pow(d2/2, 2);
  inner_area = PI * pow(d1/2, 2);
  rim_area = outer_area - inner_area;
  volume = rim_area * thickness;

  printf("Volume of washer = %.2f\n", volume);
  return 0;
}

But wait! We have not used a user-defined function! Note that the computation of the area of the circle is duplicated. In particular, if we wish to change the shape (e.g., to square where the input parameter now denotes the length of the sides) then we have to change both parts of the computation. This is prone to error.

Instead, we can abstract the computation of the area of a circle as a function. The function can be defined as follows:

Circle Area
1
2
3
double circle_area(double diameter) {
  return PI * pow(diameter/2, 2);
}

The resulting code will be the following. The function prototype, function definition and function calls are highlighted.

WasherV2.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <math.h>
#define PI 3.14159

double circle_area(double);

int main(void) {
  double d1, // inner diameter
         d2, // outer diameter
         thickness, /* outer_area, inner_area, */ rim_area, volume;      
                    /* variables not needed    */

  // read input data
  printf("Enter inner diameter, outer diameter, thickness: ");
  scanf("%lf %lf %lf", &d1, &d2, &thickness);

  // compute volume of a washer
  rim_area = circle_area(d2) - circle_area(d1);
  volume = rim_area * thickness;

  printf("Volume of washer = %.2f\n", volume);
  return 0;
}

double circle_area(double diameter) {
  return PI * pow(diameter/2, 2);
}

Function Prototype

Why do we need function prototype? There is actually not much of a "good reason" except to make the compilation more efficient. Other modern programming languages like Java do not require function prototype and can still compile without warning. This comes at the expense of multiple-pass by the compiler. The first pass is to collect all the function definition and extract the function prototype and the second pass is to compile using this collected function prototype1. On the other hand, C compiler can do this with a single-pass because of the function prototype.

Good Practice

The good practice in C is to do the following:

  • Put function prototype at the top of the program before the main() function.
    • This informs the compiler about the functions that your program may use as well as their return types and parameter types.
  • Include only the function's return type, the function's name and the data types of the parameters. The names of the parameters are optional.
    • This allows the implementation to rename the parameters easily.
  • Put the function definition after the main() function.

Note that without the function prototypes, you will get error/warning messages from the compiler. By default, the compiler assumes the default (implicit) return type of int for any function without prototype where the definition is given after the function call.

You can check this by trying to remove the function prototype for circle_area in WasherV2.c or click on the "ReplIt" tab.

The template below captures the good practice with respect to function prototype.

Template
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Preprocessor directives
// e.g.: #include #define

// Function prototypes
// e.g.: double circle_area(double); <-- ends with semi-colon

int main(void) {
  // Variable declarations
  // e.g.: int x

  // Inputs
  // Computations
  // Outputs

  return 0;
}

// Function definitions
// e.g.: double circle_area(double x) { return ...; } <-- note the function body

Combining Prototype and Definition

This is not a good practice, so proceed with care.

We can combine the function prototype with function definition as long as the function definition is given before the function call. In most cases, it means that the function definition is given before the main() function.

However, note that this will fail for mutually recursive functions. For example, if a function f calls function g and function g calls function f, then there is no way we can satisfy:

function definition is given before the function call

except through the use of function prototype.

WasherV3.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <math.h>
#define PI 3.14159

double circle_area(double diameter) { // prototype + definition --> definition
  return PI * pow(diameter/2, 2);
}

int main(void) {
  double d1, // inner diameter
         d2, // outer diameter
         thickness, /* outer_area, inner_area, */ rim_area, volume;      
                    /* variables not needed    */

  // read input data
  printf("Enter inner diameter, outer diameter, thickness: ");
  scanf("%lf %lf %lf", &d1, &d2, &thickness);

  // compute volume of a washer
  rim_area = circle_area(d2) - circle_area(d1);
  volume = rim_area * thickness;

  printf("Volume of washer = %.2f\n", volume);
  return 0;
}

Scope

C uses static scoping also called lexical scoping. There are three kinds of variables in C:

  1. Global Variables
    • Variables declares outside of all functions.
    • The variables are available to all functions.
  2. Local Variables
    • Variables declared inside a function, including the function parameters.
    • The variables are available only within the functions.
  3. Static Variables
    • Variables declared inside a function, with a static keyword.
    • The variables are available only within the functions.
    • The values in the variables persist across function call.
Details

When a function is called:

  1. An activation record is created in the call stack.
  2. Memory is allocated for the parameters and local variables of the function.

The details on how the activation record is created depends on the compiler. When the function is done:

  1. The activation record is removed from the call stack.
  2. Memory allocated to the parameters and local variables are released.

Local vs Static

The main effect of local variables is that parameters and local variables of a function exist in memory only during the execution of the function. They are called automatic variables. In constrast, static variables exist in memory even after the function is executed.

We can visualise the global and local variables as a nested block of codes. However, we will exclude static variables from our discussion.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>
int x = 2;
int adder(int);

int main(void) {
  int x = 3;
  printf("res = %d\n", adder(x));
  return 0;
}

int adder(int y) {
  return x+y;
}
1
2
3
4
5
#include <stdio.h>
int x = 2; // Global variable
int adder(int); // Prototype

int main(void) {
6
7
8
int x = 3; // Local variable (shadowing the global variable)
printf("res = %d\n", adder(x)); // adder(3)
return 0;
 9
10
11
}

int adder(int y) { // y is local
12
return x+y; // both x and y can be used
13
}

Scoping Rule

To find a variable, you can go out of a function but can never go into another function.

Luckily for C, there is no problem with nested scoping. This is because nested functions are not allowed. In other words, you cannot define a function inside another function. Furthermore, if we follow ANSI C, then variable declarations have to be at the start of the function. As such, if we follow ANSI C, then we cannot have a variable declaration within a block. This makes the scoping rule very simple.

Quick Quiz

What is wrong with this code?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int f(int);

int main(void) {
  int a;
  :
  return 0;
}

int f(int x) {
  return a + x;
}

Variable a is local to the function main() and not f(). Hence, variable a cannot be used inside f().

Pass-by-Value

When we call a function, there are several operations happening behind the scene.

  1. Evaluate the arguments.
  2. Copy the arguments to parameters positionally (i.e., 1st argument to 1st parameter, 2nd argument to 2nd parameter, and so on).
  3. Evaluate the function body.
  4. Copy the return value back to the caller (if any).
Distance.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <math.h>

double sqrt_sum_square(double, double);

int main(void) {
  double a = 10.5, b = 7.8;
  printf("%.2f\n", sqrt_sum_square(3.2, 12/5));
  printf("%.2f\n", sqrt_sum_square(a, a+b));
  return 0;
}

double sqrt_sum_square(double x, double y) {
  double sum_square;  
  sum_square = pow(x,2) + pow(y,2);
  return sqrt(sum_square);
}
  1. Evaluate the arguments.
    • 3.23.2
    • 12/52
  2. Pass the arguments to parameters positionally (i.e., 1st argument to 1st parameter, 2nd argument to 2nd parameter, and so on).
    • x3.2
    • y2 (implicit conversion to 2.0)
  3. Evaluate the function body.
    • sum_square = pow(x,2) + pow(y,2);sum_square = pow(3.2,2) + pow(2.0,2);sum_square = 10.24 + 4.0;sum_square = 14.24;
  4. Pass the return value back to the caller (if any).
    • return sqrt(sum_square);return sqrt(14.24);return 3.77;
  1. Evaluate the arguments.
    • a10.5
    • a+b10.5+7.818.3
  2. Pass the arguments to parameters positionally (i.e., 1st argument to 1st parameter, 2nd argument to 2nd parameter, and so on).
    • x10.5
    • y18.3
  3. Evaluate the function body.
    • sum_square = pow(x,2) + pow(y,2);sum_square = pow(10.5,2) + pow(18.3,2);sum_square = 110.25 + 334.89;sum_square = 445.14;
  4. Pass the return value back to the caller (if any).
    • return sqrt(sum_square);return sqrt(445.14);return 21.10;

Order of Evaluation of Argument

There is actually no fixed order of evaluation of argument in C standard. In fact, we can see that the two most common compilers GCC and Clang have different order of evaluation. The order of evaluation of Clang is the usual left-to-right. On the other hand, the order of evaluation of GCC is actually right-to-left.

This ambiguity is intentional especially when we consider parallel and/or concurrent execution. In such cases, the compiler may choose to evaluate all at the same time. The reason GCC choose right-to-left evaluation is because of speed.

Remember, we create the activation record during function call. This is created in a stack. So, by evaluating from the right, we can immediately push it into the stack. As a consequence, the left-most parameter is simply at the top of the stack now. This behaviour is only useful for variadic function where we do not know how many arguments are going to be passed.

As a programmer, you should be careful not to depend on the order of evaluation for the correct execution of your program.

To run in Clang, simply click the run button. To run in GCC, follow the instructions below:

  1. Click "Shell" tab.
  2. Compile with GCC using gcc main.c.
  3. Execute the code using ./a.out.

Notice how the result is different between Clang and GCC. Using Clang, you should see the output as 0 1 2. On the other hand, using GCC, the output is 2 1 0.

Quick Quiz

Trace the following code by hand and write out its output. Note that a void function is a function that does not return any value.

PassByValue.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>
void g(int, int);

int main(void) {
  int a = 2, b = 3;

  printf("In main, before: a=%d, b=%d\n", a, b);
  g(a, b);
  printf("In main, after : a=%d, b=%d\n", a, b);    return 0; 
}

void g(int a, int b) {
  printf("In g, before: a=%d, b=%d\n", a, b);
  a = 100 + a;
  b = 200 + b; 
  printf("In g, after : a=%d, b=%d\n", a, b);
}
1
2
3
4
In main, before: a=2, b=3
In g, before: a=2, b=3
In g, after : a=102, b=203
In main, after : a=2, b=3

Consequence of Pass-by-Value

Let's go back to the motivation for learning pointers. Can we use the following function to swap the values in a and b below?

SwapIncorrect.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>
void swap(int, int);

int main(void) {
  int a = 2, b = 3;

  printf("In main, before: a=%d, b=%d\n", a, b);
  swap(a, b);
  printf("In main, after : a=%d, b=%d\n", a, b);    return 0; 
}

void swap(int a, int b) {
  int temp = a;
  a = b;
  b = temp;
}

You should get the following output:

1
2
In main, before: a=2, b=3
In main, after : a=2, b=3

What is happening? Why is it that we cannot modify the variable a and b using the function swap? The answer is that pass-by-value copies the value from arguments to parameters. This means that we have two copies of the value 2 (respectively 3).

  1. The value in variable a (respectively b), accesible in main.
  2. The value in variable a (respectively b), accesible in swap.

Although the variable name is the same, they actually refer to different variables due to scoping. So now, any changes in a (respectively b) inside swap will only affect the local copy. Hence, the value of a (respectively b) in main is unchanged.

Swap

The visualisation is shown above. Notice that the changes inside swap does not affect main.

Pass the Reference

The consequence of pass-by-value brings us back to the use of pointers. If we really want to modify the content of variable a (respectively b) in main using the function swap, then we have to allow for indirect access of the variable. This is because direct access is impossible due to the scoping rule.

How do we give the indirect access to the function swap? We have to pass the address of the variable! To do so, the function has to accept a pointer.

SwapCorrect.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>
void swap(int*, int*);

int main(void) {
  int a = 2, b = 3;

  printf("In main, before: a=%d, b=%d\n", a, b);
  swap(&a, &b);
  printf("In main, after : a=%d, b=%d\n", a, b);    return 0; 
}

void swap(int *ptr1, int *ptr2) {
  int temp = *ptr1;
  *ptr1 = *ptr2;
  *ptr2 = temp;
}

Swap

The box-and-arrow diagram is shown above. Note the difference when a pointer is used.

Passing Address Accept Pointers

It is a common practice to pass address into a function that accept pointers. However, this need not be the case. We can extract the address immediately and store it into a pointer variable first. Afterwards, we then pass the value of this pointer to the function. But remember, the value of a pointer is an address!

Quick Quiz

Which library function you have learnt that accepts an address? In other words, the parameter is a pointer.

scanf()

Example1.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>
void f(int, int, int);

int main(void) {
  int a = 9, b = -2, c = 5;
  f(a, b, c);
  printf("a = %d, b = %d, c = %d\n", a, b, c);
  return 0;
}

void f(int x, int y, int z) {
  x = 3 + y;
  y = 10 * x;
  z = x + y + z;
  printf("x = %d, y = %d, z = %d\n", x, y, z);
}

Example2.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>
void f(int *, int *, int *);

int main(void) {
  int a = 9, b = -2, c = 5;
  f(&a, &b, &c);
  printf("a = %d, b = %d, c = %d\n", a, b, c);
  return 0;
}

void f(int *x, int *y, int *z) {
  *x = 3 + *y;
  *y = 10 * *x;
  *z = *x + *y + *z;
  printf("*x = %d, *y = %d, *z = %d\n", *x, *y, *z);
}

Example3.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>
void f(int *, int *, int *);

int main(void) {
  int a = 9, b = -2, c = 5;
  f(&a, &b, &c);
  printf("a = %d, b = %d, c = %d\n", a, b, c);
  return 0;
}

void f(int *x, int *y, int *z) {
  *x = 3 + *y;
  *y = 10 * *x;
  *z = *x + *y + *z;
  printf("x = %d, y = %d, z = %d\n", x, y, z);
}

Example4.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>
void f(int *, int *, int *);

int main(void) {
  int a = 9, b = -2, c = 5;
  f(&a, &b, &c);
  printf("a = %d, b = %d, c = %d\n", a, b, c);
  return 0;
}

void f(int *x, int *y, int *z) {
  *x = 3 + *y;
  *y = 10 * *x;
  *z = *x + *y + *z;
  printf("x = %p, y = %p, z = %p\n", x, y, z);
}


  1. There may be more passes but typically just two is enough.