FirstHack Learn
Log in Sign up free

Functions in C

10 min read · 21 views

Functions in C

A function is a named block of code that takes inputs, does something, and optionally hands a value back. You have been using functions since your first program: printf, strlen and main are all functions. Writing your own is how a program stops being one long list of statements and becomes something you can read, test and fix in pieces.

The practical reasons are worth stating plainly. A function lets you write logic once and use it many times. It gives a chunk of code a name, which is a form of documentation. And it lets you test a small piece in isolation, which is far easier than debugging two hundred lines at once.

Anatomy of a function

C
#include <stdio.h>

int add(int a, int b)
{
    int result = a + b;
    return result;
}

float average(int total, int count)
{
    return (float) total / count;
}

void printLine(void)
{
    printf("------------------------\n");
}

int main(void)
{
    printLine();
    printf("add(7, 5)        = %d\n", add(7, 5));
    printf("add(add(1,2), 3) = %d\n", add(add(1, 2), 3));
    printf("average(250, 4)  = %.2f\n", average(250, 4));
    printLine();

    return 0;
}

Every function has four parts.

  • Return type (int, float, void): the type of the value handed back. void means nothing is returned.
  • Name (add): how you call it. The same rules as variable names apply.
  • Parameter list (int a, int b): the inputs, each with its own type written out in full. int a, b is not valid; you must write int a, int b. Empty parameters are best written as void.
  • Body: the statements between braces.

return does two things at once: it produces the value and it exits the function immediately. Any code after a return on the same path never runs.

Why prototypes exist

C compilers historically read a file once, from top to bottom. When the compiler reaches a call to add, it must already know what add looks like: how many arguments, of what types, returning what. Otherwise it cannot check your call or generate correct code.

You have two ways to give it that information. Define the function above the point of use, as in the example, or write a prototype near the top and put the definition wherever you like.

C
#include <stdio.h>

int  square(int n);
int  factorial(int n);
void banner(const char *title);

int main(void)
{
    banner("Squares and factorials");

    for (int i = 1; i <= 6; i++) {
        printf("%d squared = %-4d  %d! = %d\n", i, square(i), i, factorial(i));
    }

    return 0;
}

int square(int n)
{
    return n * n;
}

int factorial(int n)
{
    int result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

void banner(const char *title)
{
    printf("== %s ==\n", title);
}

A prototype is the function's header followed by a semicolon, with no body. Parameter names in a prototype are optional and are there purely for human readers.

This is exactly what #include <stdio.h> does for you: it pastes in the prototypes for printf, scanf and the rest, so the compiler knows how to check your calls. Headers are prototypes, nothing more mysterious than that.

Prototypes also let main sit at the top, which usually makes a file easier to read: the overall shape first, details below.

Call by value

This is the single most important thing to understand about C functions.

When you call a function, the arguments are copied into the parameters. The function works on the copies. Changing a parameter inside the function has no effect whatsoever on the caller's variable.

C
#include <stdio.h>

void tryToChange(int x)
{
    x = 999;
    printf("Inside function, x = %d\n", x);
}

int doubled(int x)
{
    return x * 2;
}

int main(void)
{
    int value = 10;

    tryToChange(value);
    printf("Back in main, value = %d\n", value);

    value = doubled(value);
    printf("After assignment, value = %d\n", value);

    return 0;
}

value is still 10 after tryToChange. The function received a copy, renamed it x, and modified that copy, which then vanished when the function returned.

The working pattern is the second one: return the new value and assign it. If a function genuinely must modify the caller's variable, you pass the address of it using & and a pointer parameter, which is exactly what scanf requires and why it needs &. Pointers are the next topic in your syllabus, and call by value is the reason they exist.

ℹ️Arrays are the apparent exception

Passing an array to a function does not copy the elements. The array name becomes the address of the first element, so the function works on the original data and changes are visible to the caller. This is not a special rule for arrays; it is call by value applied to an address. Since the size is not carried along, you must pass the length as a separate parameter.

Scope

Scope is the region of code where a name is visible. A variable declared inside a function, including inside main, is local to it. It comes into existence when the function is entered and disappears when the function returns. Two functions can each have a variable called count and they are completely unrelated.

A variable declared outside all functions is global. It is visible everywhere and lives for the whole run of the program. Globals look convenient and are best avoided: any function can change one, so when the value is wrong you must read the entire program to find out who did it.

Blocks create scope too. A variable declared inside an if or a for body exists only there.

Common mistakes

  • No prototype and the definition is below main. Older compilers guessed; modern ones give an implicit-declaration warning or error. Add the prototype.
  • Missing return type in the parameter list. int add(int a, b) does not compile. Write int add(int a, int b).
  • Forgetting to return a value. A non-void function that falls off the end returns garbage. Make sure every path has a return.
  • Expecting a function to modify its argument. Call by value copies. Return the result, or pass a pointer.
  • Returning the address of a local variable. The local is destroyed when the function returns, so the address points at memory that no longer belongs to you.
  • Mismatched types. Passing a float where an int is expected truncates silently. Check the prototype.
  • Declaring a local with the same name as a global. The local wins inside that function, which is legal and confusing. Pick a different name.

Once you can split a problem into functions, the size of the program you can comfortably write goes up sharply. Try rewriting an earlier exercise so that main contains almost nothing except calls. Then work through the function problems on the Practice page.

Create a free account to track what you have finished.