Functions in C

Subpage of C Deep Dive

A close look at the C language.

A function has two essential parts:

  • declaration (or prototype) that specifies its interface. This tells the compiler the function's name, return type, and parameter types. This allows the compiler to check that calls to the function are correct. The declaration typically appears at the top of a file or in a header file.
  • definition that provides its implementation The definition contains the actual code that executes when the function is called.
#include <stdio.h>

/* Function declaration (prototype) - tells compiler about the function */
int add(int a, int b);

int main() {
    int result = add(5, 3);  /* Function call */
    printf("5 + 3 = %d\n", result);
    return 0;
}

/* Function definition - implements what the function does */
int add(int a, int b) {
    return a + b;  /* Returns the sum to the caller */
}

Parameters in C are passed by value, meaning the function receives copies of the arguments, not the original variables.

This protects the original data from being modified unintentionally. However, if you need to modify the original variable or return multiple values, you must use pointers to pass the address of the variable instead of its value.

We call the the specific values or expressions that are passed to a function when it is called the actual parameters. This will always be a particular value which is inside the box representing the variable.

Scoping

C uses lexical (or static) scoping, meaning the scope is determined entirely by where the variable is declared in the source code at compile time, not by how the code runs. 

When dealing with functions, variables declared within the parameter list of a function declaration (prototype) have this scope. They are only visible until the end of the function declarator. 

However, we can overcome the limitations of scoping by passing around pointers. If we have a pointer to a value, we can modify the value.

Here is what the prototype of such a function might look like. Specifically, below we present a function that takes in 3 integer pointers:

void f(int *, int *, int *);

Here are key concepts associated with scoping:

  • Shadowing: If a variable declared in an inner block has the same name as one in an outer block, the inner variable "shadows" (hides) the outer one.
    int x = 10; // Outer variable
    {
        int x = 5; // Inner variable shadows outer x
        printf("%d", x); // Prints 5
    }
    printf("%d", x); // Prints 10
  • Static Variables: If you declare a local variable as static, its scope remains local, but its lifetime extends to the entire program, retaining its value between function calls.
  • Global vs Local: It is considered best practice to use local variables to limit the scope and make code easier to debug.
  • Linkage: Global variables have external linkage by default, meaning they can be accessed in other files using the extern keyword. Marking them as static gives them internal linkage, restricting them to the current file.

Ordering

Remember that all functions need to be declared before the main function so that the compiler knows what parameters a function and take and what its return type is before it can be called. Not adhering to the ordering will result in warnings (or at worst, compilation errors).

The function definition (outline of what the function does) can happen later however.

To understand why this might be the case, we go through how the compiler works:

  1. The compiler reads the source code top down.
  2. When the compiler sees a call to a function it doesn’t recognize, rather than complain about it, the compiler figures that it will find out more about the function later in the source file. The compiler simply remembers to look out for the function later on in the file.
  3. The compiler needs to know what data type the function will return. It doesn’t know yet so it makes an assumption.
  4. When it reaches the code for the actual function it checks the return type. If the return type is different, the compiler thinks there are two functions with the same name and throws a conflicting types error.

To avoid this problem, we use function declarations (also called a function prototype).

The declaration is just a function signature: a record of what the function will be called, what kind of parameters it will accept, and what type of data it will return.

This is why you will often see a whole bunch of declarations at the start of a C program.

Even better, all the function declarations can be put in a separate file called the header file. Here is how to create a header file:

  1. Create a new file with a .h extension.

    If you are writing a program called totaller, then create a file called totaller.h and write your declarations inside it.

    This file will contain declarations and nothing else.

  2. Include your header file in your main program.

    Here is an example:

    #include <stdio.h>
    #include "totaller.h"

    By wrapping the header filename in quotes, you are telling the compiler to look for a local file.

Here are some pointers about prototypes that are useful to keep in mind:

  • In a prototype, identifiers can differ or be omitted:
    int f(int x, double y);
    int f(int, double);    // equivalent prototype []
  • In a parameter list, int a[] and int a[10] are both exactly equivalent to int *a: they declare a pointer parameter, not an array parameter. In fact, a[] is just syntactic sugar.

    This is why, inside fsizeof a is the size of int *not the size of the caller’s array.

    It also means that the function cannot “see” the length of the array; you must pass it separately or encode it in the type (e.g., pointer to array).

  • For true multidimensional C arrays (arrays of arrays), trailing dimensions must be known so that indexing works.

    int a[][4] in a prototype is equivalent to int (*a)[4]: a pointer to an array of 4 int.

    The rightmost dimension must be specified (or be a VLA parameter), because a[i][j] is compiled as (*(a + i) + j), which needs the size of a row.

  • A declaration with an empty parameter list, like int f();, is not a prototype; it says “f is a function returning int, parameters not specified”.

/ Continue

Follow the technical trail.

Use the dense notes as the source material, then move through the guided route, writing, or project proof when you want a cleaner entry point.