Functions

It's All About Functions

Some of the first statements made during this course were these: And the general form of a C program was shown. (The parts in bold are the subject of this topic)
include files

function declarations (prototypes)

data declarations (global)

main function header
{
  data declarations (local)
  statements
}

other functions
Although we've actually only been focused on one function so far (main), we've used two others: scanf and printf. These functions are in the C standard library.

A function has the form:

return_type function_name(formal_parameters)
{
  function_body
}
Explanations:
Function part Description
return_type This describes the type of the data that the function will return (evaluate to). Almost all data types are allowed to be returned from a function. If a function returns data, then the function can be used in an expression, otherwise, it can't. (All functions return data unless they are marked as returning void.)
function_name The name of the function. The name must follow the same rules that an identifier must follow.
formal_parameters This is an optional comma-separated list of values that will be passed to the function. A function that takes 0 parameters will have the keyword void as the only item in the list. (Students that still think that function(void) is the same as function() are required to read this again.)
function_body The body consists of all of the declarations and executable code for the function. If the function is defined to return a value, there must be at least one return statement in the body. (There can be multiple return statements.)
Here's an example of a user-defined function:
float average(float a, float b)
{
  return (a + b) / 2;
}
The function above meets the requirements:
  1. The function is named average.
  2. It will return a value of type float.
  3. It expects two values to be provided, both of type float. (Note that you can't do this for the parameters: float a, b)

A complete program using our new function:

#include <stdio.h>

float average(float a, float b)
{
  return (a + b) / 2;
}

int main(void)
{
  float x;
  float y;
  float ave;
  
  x = 10; 
  y = 20;
  ave = average(x, y);
  printf("Average of %g and %g is %g\n", x, y, ave);

  x = 7; 
  y = 10;
  ave = average(x, y);
  printf("Average of %g and %g is %g\n", x, y, ave);
  
  return 0;
}
The output:
Average of 10 and 20 is 15
Average of 7 and 10 is 8.5
Both the return value and the parameters are optional:
/* No return, no parameters */
void say_hello(void)
{
  printf("Hello!\n");
  return; /* optional */
}
/* No return, one parameter */
void say_hello_alot(int count)
{
  int i;
  for (i = 0; i < count; i++)
    printf("Hello!\n");
}
/* Return, no parameters */
float pi(void)
{
  return 3.14159F; 
}
Calling the functions:
int main(void)
{
  float p;

  say_hello();       /* no arguments, needs parentheses */
  say_hello_alot(5); /* one argument                    */
  p = pi();          /* no arguments                    */
  p = pi;            /* Error, parentheses are required */

  return 0;          /*    not optional                 */
}

Function Prototypes

Note the ordering of the two functions (main and average) in this program: (foo.c)
 1. #include <stdio.h>
 2.
 3. int main(void)
 4. {
 5.   printf("ave = %.2f\n", average(10, 20));
 6.   return 0;
 7. }
 8. 
 9. float average(float a, float b)
10. {
11.   return (a + b) / 2;
12. }
There are several problems that the compiler complains about and you probably don't understand what some of them mean:
foo.c: In function 'main':
foo.c:5: warning: implicit declaration of function 'average'
foo.c:5: warning: double format, different type arg (arg 2)
foo.c: At top level:
foo.c:10: error: conflicting types for 'average'
foo.c:5: error: previous implicit declaration of 'average' was here
New compilers give more information about the problems:
warn.c: In function 'main':
warn.c:5:3: warning: implicit declaration of function 'average' [-Wimplicit-function-declaration]
   printf("ave = %.2f\n", average(10, 20));
   ^
warn.c:5:3: warning: format '%f' expects argument of type 'double', but argument 2 has type 'int' [-Wformat=]
warn.c: At top level:
warn.c:9:7: error: conflicting types for 'average'
 float average(float a, float b)
       ^
warn.c:5:26: note: previous implicit declaration of 'average' was here
   printf("ave = %.2f\n", average(10, 20));
                          ^
Some compilers are even better at describing the problem:
warn.c:5:26: warning: implicit declaration of function 'average' [-Wimplicit-function-declaration]
  printf("ave = %.2f\n", average(10, 20));
                         ^
warn.c:5:26: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
  printf("ave = %.2f\n", average(10, 20));
                ~~~~     ^~~~~~~~~~~~~~~
                %.2d
warn.c:9:7: error: conflicting types for 'average'
float average(float a, float b)
      ^
warn.c:5:26: note: previous implicit declaration is here
  printf("ave = %.2f\n", average(10, 20));
                         ^
2 warnings and 1 error generated.

Video review

Some compilers treat this situation as a warning and not an error. Borland's compiler, GNU's gcc, and Clang all treat it as a error, so you can't write code like the above. Sadly, Microsoft's compiler doesn't catch the error and when you run the above program, you get this:

ave = 796482944349676280000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000.00
Of course, this is random, and you may likely get different garbage each time you run it. (You'll probably get zero a lot since it's the most popular "garbage" value in memory.)

Removing the include file from this program yields a similar warning:

WorksFails
/*#include <stdio.h>*/

int main(void)
{
  printf("Hello!\n");
  return 0;
}


Output: Hello!
       
/*#include <stdio.h>*/

int main(void)
{
  printf(5);
  return 0;
}


Output: Segmentation fault
Warnings (without the include):
In function 'main':
warning: implicit declaration of function 'printf'
Warnings (with the include)
In function ‘main’:
warning: passing argument 1 of ‘printf’ makes pointer from integer without a cast [-Wint-conversion]
    5 |  printf(5);
      |         ^
      |         |
      |         int
In file included from p5.c:1:
/usr/include/stdio.h:332:43: note: expected ‘const char * restrict’ but argument is of type ‘int’
  332 | extern int printf (const char *__restrict __format, ...);
      |                    ~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
warning: format not a string literal and no format arguments [-Wformat-security]
    5 |  printf(5);
      |  ^~~~~~
Just like all other identifiers in C, we must tell the compiler about our functions before we use (call) them. We do this with a function declaration, also called a function prototype.

This program is now perfectly fine:

#include <stdio.h> /* For printf */

/* Function prototype, notice the semicolon    */
/* Prototypes do not have curly braces or body */
float average(float a, float b);

int main(void)
{
  /* Both printf and average are known to the compiler */
  printf("ave = %.2f\n", average(10, 20));
  return 0;
}

/* Function definition, no semicolon    */
/* Definition has curly braces and body */
float average(float a, float b)
{
  return (a + b) / 2;
}
A function prototype introduces the identifier to the compiler. (Mr. Compiler, this is Mr. Identifier.) It tells the compiler that: Now that the compiler knows what average is, when it sees average later in the program, it will be able to make sure that it is used properly.
/* Prototype */
float average(float a, float b);

int main(void)
{
    /* Define some variables */
  int i;
  float f1, f2, f3, f4;
  double d1, d2;

    /* Set some values */
  f1 = 3.14F;  f2 = 5.893F;  f3 = 8.5F;
  d1 = 3.14;  d2 = 5.893;

    /* Call the function in various ways */
    /* Nothing wrong with these calls    */
  f4 = average(f1, f2);
  f4 = average(5.0F, 6.0F);
  f4 = average(10, 20);
  average(f1, f2);

    /* Potential problems when these execute */
  i = average(f1, f2);
  f4 = average(d1, d2);
  f4 = average(3.14, 9.1F);

    /* Fatal errors, compiler can't continue */
  f4 = average(f2);
  f4 = average(f1, f2, f3);

  return 0;
}
Detailed warning/error messages from the compiler:
Function callsOk   Warnings   Errors
f4 = average(f1, f2);
f4 = average(5.0F, 6.0F);
f4 = average(10, 20);
average(f1, f2);
Ok
Ok
Ok
Ok. Most of the time, you can ignore return values from functions.
i = average(f1, f2);
f4 = average(d1, d2);
f4 = average(3.14, 9.1F);
warning: conversion from 'float' to 'int', possible loss of data
warning: conversion from 'double' to 'float', possible loss of data
warning: conversion from 'double' to 'float', possible loss of data
f4 = average(f2);
f4 = average(f1, f2, f3);
error: too few arguments to function 'average'
error: too many arguments to function 'average'
Not all compilers will warn about the potential loss of precision, but they will all emit errors for the last two. The warnings above are from Microsoft's C compiler, version 7.1. These errors below are from gcc. If you invoke gcc with an additional command line switch:
gcc -Wconversion foo.c
You will see lots of additional warnings, which are informative, but not dangerous in this case:
warning: passing arg 1 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 2 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 1 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 2 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 1 of 'average' as floating rather than integer due to prototype
warning: passing arg 2 of 'average' as floating rather than integer due to prototype
warning: passing arg 1 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 2 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 1 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 2 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 1 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 2 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 1 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 2 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 1 of 'average' as 'float' rather than 'double' due to prototype
error: too few arguments to function 'average'
warning: passing arg 1 of 'average' as 'float' rather than 'double' due to prototype
warning: passing arg 2 of 'average' as 'float' rather than 'double' due to prototype
error: too many arguments to function 'average'

Quick check: Does the following program compile?

float average(float a, float b);

int main(void)
{
  float f1, f2, f3;
  f2 = 3.14f;
  f3 = 5.0f;

  f1 = average(f2, f3);

  return 0;
}
To help answer this question, refer to the diagram. If you compiled this program like this: (assume the name of the file is main.c)
gcc -c main.c
or even like this:
gcc -Wall -Wextra -ansi -pedantic -c main.c
It will compile cleanly without any warnings or errors and you'll be left with an object file called main.o, which will be about 875 bytes in size.
E:\Data\Courses\Notes\CS120\Code\Functions>dir main.o
 Volume in drive E has no label.
 Volume Serial Number is 5852-DBD2

 Directory of E:\Data\Courses\Notes\CS120\Code\Functions

09/21/2020  08:54a                 875 main.o
               1 File(s)            875 bytes
               0 Dir(s)   6,154,678,272 bytes free

Now, if you try to link the file:

gcc main.o
You'll see this "helpful" error message:
main.o:main.c:(.text+0xb7): undefined reference to '_average'
collect2: ld returned 1 exit status
The error message (from the linker) is saying to you:
"Hey, you (Mr. Compiler) told me that this program needs to call a function called average, but I can't find it anywhere. I give up.".
Note that you would have received the same error had you attempted to compile and link with one command (removing the -c, which means to compile but don't link):
gcc main.c

Please make very sure that you understand the difference between compiling and linking. We will see many more situations where the code will compile properly, but not link.

Tracing Function Calls

What is the sequence of function calls made by the program below. Specify the function being called and its parameters. (The numbers to the left represent arbitrary addresses in memory.)
Programmer's view
of the code
Compiler's view
of the code
Library
(other code)

       void FnB(void)
1000:  {
1004:    printf("tied the room ");

1008:  }

       void FnC(void)
1020:  {
1024:    printf("together\n");

1028:  }

       void FnA(void)
1030:  {
1034:    printf("That rug really ");

1038:    FnB();
1042:  }

       void FnD(void)
1060:  {
1064:    FnA();
1068:    FnC();
1072:  }

      int main(void)
1080: {
1084:   FnD();
1088:   return 0;
1092: }
       

       void FnB(void)
1000:  {
1004:    push "tied the room " onto stack
         jump to 10000
1008:  } return

       void FnC(void)
1020:  {
1024:    push "together\n" onto stack
         jump to 10000
1028:  } return

       void FnA(void)
1030:  {
1034:    push "That rug really " onto stack
         jump to 10000
1038:    jump to 1000
1042:  } return

       void FnD(void)
1060:  {
1064:    jump to 1030
1068:    jump to 1020
1072:  } return

      int main(void)
1080: {
1084:   jump to 1060
1088:   return 0;
1092: }
       

        int printf(string, ...)
10000:  {
10004:    push string onto the stack
          jump to 11000
          jump to 12000
10008:  } return

        int sprintf(string, ...)
11000:  {
11004:    format what is on the stack
10008:  } return

        int puts(string)
12000:  {
12004:    print what is on the stack
12008:  } return

Graphically:

Additional details:

Video review

The sequence is this:

main(void);
FnD(void);
FnA(void);
printf("That rug really ");
FnB(void);
printf("tied the room ");
FnC(void);
printf("together\n");
What is the output of the program?

Pass By Value

In C, all function arguments are always passed by value. (There is no other way.) In a nutshell, this means that any changes made to the parameters in the body of the function will not affect the values at the call site. The reason is that a copy of the data is being passed to the function and so the function is working with this copy.

An example will clarify:

#include <stdio.h> /* printf */
	
void fn(int x)
{
  printf("In fn, before assignment, x is %i\n", x);
  
  x = 10;
  
  printf("In fn, after assignment, x is %i\n", x);
}

int main(void)
{
  int i;
  i = 5;

  printf("Before call: i is %i\n", i); 
  
  fn(i); /* call site */
  
  printf("After call: i is %i\n", i);
  
  return 0;
}
Output:
Before call: i is 5
In fn, before assignment, x is 5
In fn, after assignment, x is 10
After call: i is 5

Video review (Assume that fn is prototyped before main)

A copy of the value is passed to the function, so any changes made are made to the copy, not the original value. Visually, the process looks something like this:

When main begins, i is undefined:
Next statement, i is assigned a value:
The function is called and a copy of the argument is made:
The parameter, x (the copy), is assigned a value:
The function returns back to main, the original value is preserved, and the copy is discarded:
        
Revisiting an example. We can use the parameter (modify it) without worrying that we will be changing something in another part of the program:

for loop (extra variable)while loop (no extra variable)while loop (more compact)
 
void say_hello_alot(int count)
{
  int i;
  for (i = 0; i < count; i++)
    printf("Hello!\n");
}
void say_hello_alot(int count)
{
  while (count)
  {
    printf("Hello!\n");
    --count;
  }
}
void say_hello_alot(int count)
{
  while (count--)
    printf("Hello!\n");
}

Functions and Scope

What happens when we have a variable named A in two different functions? Does the compiler or program get confused?
void fn1(void)
{
  int A;
  /* statements */
}
void fn2(void)
{
  int A;
  /* statements */
}
Declaring/defining variables inside of a function makes them local to that function. No other function can see them or access them.

Example (1000 and 1008 are arbitrary addresses for those instructions):

#include <stdio.h> /* printf */

/* Function prototypes */
int add3(int i, int j, int k);
int mult3(int p, int q, int r);

int main(void)
{
  int a = 2;
  int b = 3;
  int c = 4;
  int d;
  int e;

       d = add3(a, b, c);
1000:  e = mult3(a, b, c);

1008:  return 0;
}

/* Function definition */
int add3(int x, int y, int z)
{
  int a = x + y + z;
  return a;
}

/* Function definition */
int mult3(int a, int b, int c)
{
  int x = a * b * c;
  return x;
}

Assembly code: Local variables vs. Parameters
main2:
  pushl %ebp
  movl  %esp, %ebp
  subl  $40, %esp>

  movl  $2, -28(%ebp)   ; a
  movl  $3, -24(%ebp)   ; b
  movl  $4, -20(%ebp)   ; c
  subl  $4, %esp
  pushl -20(%ebp)       ; c
  pushl -24(%ebp)       ; b
  pushl -28(%ebp)       ; a
  call  add3
  addl  $16, %esp
  movl  %eax, -16(%ebp) ; d
  subl  $4, %esp
  pushl -20(%ebp)       ; c
  pushl -24(%ebp)       ; b
  pushl -28(%ebp)       ; a
  call  mult3
  addl  $16, %esp
  movl  %eax, -12(%ebp) ; e
  movl  $0, %eax
  ret
add3:
  pushl %ebp
  movl  %esp, %ebp
  subl  $16, %esp

  movl  8(%ebp), %edx   ; x
  movl  12(%ebp), %eax  ; y
  addl  %eax, %edx
  movl  16(%ebp), %eax  ; z
  addl  %edx, %eax
  movl  %eax, -4(%ebp)  ; a
  movl  -4(%ebp), %eax  ; a
  ret
mult3:
  pushl %ebp
  movl  %esp, %ebp
  subl  $16, %esp

  movl  8(%ebp), %eax   ; a
  imull 12(%ebp), %eax  ; b
  movl  16(%ebp), %edx  ; c 
  imull %edx, %eax
  movl  %eax, -4(%ebp)  ; x
  movl  -4(%ebp), %eax  ; x
  ret

Technically, curly braces define a scope. This means that any time you have a pair of curly braces, a new scope is created. So, yes, this means that compound statements within loops (for, while, etc.) and conditionals (if, etc.) are in a different scope than the statements outside of the curly braces. More on scopes later... Also, it is very important that you understand the difference between formal parameters and actual parameters (arguments) and the difference between parameter names in the function prototype vs. the names in the function definition.

However, it is a convention that the names of the parameters in the prototype match the names in the definition. We will adhere to that.

Video review

The return Statement

We've already seen the return statement many times so far. It is used when we want to leave (i.e. return from) a function.

The general form is:

return expression ;
These functions both have illegal return statements:
int f1(int a, int b)
{
  /* statements */

  /* Illegal, must return an int */
  return;
}
void f2(int a, int b)
{
  /* statements */

  /* Illegal, can't return anything */
  return 0;
}
These are the errors:
In function 'f1': warning: 'return' with no value, in function returning non-void
In function 'f2': warning: 'return' with a value, in function returning void
Sadly, these are both just warnings, and the first one always produces undefined behavior.

Divide and Conquer

The main point of using functions is to break the program into smaller, more manageable pieces. The technique is called Divide and Conquer and can be visualized like this:

The idea is that it will be easier to work individually on smaller parts of the whole problem, rather than try to solve the entire problem at once. This helps with:

As Bjarne Stroustrup (The Father of C++) says:

"The number of errors in code correlates strongly with the amount of code and the complexity of the code. Both problems can be addressed by using more and shorter functions. Using a function to do a specific task often saves us from writing a specific piece of code in the middle of other code; making it a function forces us to name the activity and document its dependencies."

And Mead's Corollary:

"The complexity of a program is inversely proportional to the number of functions in the program."

This is the #1 reason why beginning programmers fail: They are determined to put all of their code in a single function rather than breaking it down into several, smaller functions. They falsely believe that 1) they will save time programming by putting all of the code into one function and 2) the code will run faster if it's all in one function.

More on Scope

The scope or visibility of an identifier is what determines in which parts of the program the identifier can be seen.

This contrived example shows three different scopes. (The curly braces for each scope are highlighted.)

All statements are legalSome statements are illegal
void f1(int param) /* scope of param starts here */
{
    int a; /* scope of a starts here */
    int b; /* scope of b starts here */
    int c; /* scope of c starts here */

    /* do something with a, b, and c */

    while (a < 10)
    {
        int x; /* scope of x starts here */
        int y; /* scope of y starts here */

        if (b == 5)
        {
            int p; /* scope of p starts here */
            int q; /* scope of q starts here */

            p = a;         /* Ok, both in scope */
            q = x + param; /* Ok, all in scope  */

        } /* scope of p and q ends here */

        x = a; /* Ok, both in scope      */
        y = b; /* Ok, both in scope      */

    } /* scope of x and y ends here   */

} /* scope for a, b, c, and param ends here */
void f2(int param) /* scope of param starts here */
{
    int a; /* scope of a starts here */
    int b; /* scope of b starts here */
    int c; /* scope of c starts here */

    /* do something with a, b, and c */

    while (a < 10)
    {
        int x; /* scope of x starts here */
        int y; /* scope of y starts here */

        if (b == 5)
        {
            int p; /* scope of p starts here */
            int q; /* scope of q starts here */

        } /* scope of p and q ends here  */

        x = p; /* Error, p not in scope  */
        y = q; /* Error, q not in scope  */

    } /* scope of x and y ends here    */

    a = x; /* Error, x not in scope  */
    b = p; /* Error, p not in scope  */

} /* scope for a, b, c, and param ends here */

Notes about variables in block scope: (a.k.a. local variables)

Notes about global variables: