Functions

A function is a named chunk of code you can run whenever you want. You’ve already used one: main(). Now you’ll learn to write your own.

Think of functions like recipes. A recipe has a name (“Chocolate Chip Cookies”), might need ingredients (butter, sugar, flour), and produces a result (cookies). You can use the same recipe over and over without rewriting it each time.

Your First Function

#include <stdio.h>

// Function definition
void greet(void) {
    printf("Hello!\n");
}

int main(void) {
    greet();  // Function call
    greet();  // Call it again
    greet();  // And again
    return 0;
}

Output:

Hello!
Hello!
Hello!

We defined a function called greet that prints “Hello!” and then called it three times. Instead of writing printf("Hello!\n"); three times, we wrote it once and gave it a name.

Parts of a Function

Every function has these parts:

return_type function_name(parameters) {
    // body - the code that runs
    return value;  // if return_type isn't void
}

Let’s break that down:

  • return_type: What the function gives back when it’s done. Use void if it doesn’t give anything back.
  • function_name: The name you use to call it
  • parameters: Information the function needs to do its job. Use void if it doesn’t need anything.
  • body: The actual code that runs
  • return: Sends a value back to whoever called the function

Functions with Parameters

Parameters are like ingredients - information your function needs to do its job.

void greet_person(char name[]) {
    printf("Hello, %s!\n", name);
}

int main(void) {
    greet_person("Alice");
    greet_person("Bob");
    return 0;
}

Output:

Hello, Alice!
Hello, Bob!

The char name[] means “this function needs some text to work with.” When we call greet_person("Alice"), the text “Alice” gets put into name, and the function uses it.

You can have multiple parameters:

void print_sum(int a, int b) {
    printf("%d + %d = %d\n", a, b, a + b);
}

int main(void) {
    print_sum(3, 5);   // Prints: 3 + 5 = 8
    print_sum(10, 20); // Prints: 10 + 20 = 30
    return 0;
}

Functions that Return Values

Functions can give back a result. Think of a calculator - you give it numbers, it gives you an answer.

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

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

int main(void) {
    int sum = add(3, 5);
    printf("Sum: %d\n", sum);  // Sum: 8

    int result = square(4);
    printf("Square: %d\n", result);  // Square: 16

    // Can also use directly
    printf("7 squared: %d\n", square(7));  // 49

    return 0;
}

The int before add means “this function returns an integer.” The return statement sends that value back to wherever the function was called.

Why “void” Means “Nothing”

When you see void, it means “nothing.”

  • void greet(void) - this function gives back nothing and needs nothing
  • int add(int a, int b) - this function gives back an integer and needs two integers

The word comes from Latin and means “empty.” When a function has void as its return type, it does something (like print to the screen) but doesn’t calculate and return a value.

Function Declarations (Prototypes)

C reads your code from top to bottom. If you try to call a function before defining it, C doesn’t know what you’re talking about.

This won’t work:

int main(void) {
    greet();  // ERROR: C doesn't know what "greet" is yet
    return 0;
}

void greet(void) {
    printf("Hello!\n");
}

You can fix this by either putting the function definition before main, or by using a prototype - a promise to C that the function exists:

#include <stdio.h>

// Prototype - tells C "this function exists, trust me"
void greet(void);

int main(void) {
    greet();  // Now this works
    return 0;
}

// Definition - the actual code
void greet(void) {
    printf("Hello!\n");
}

The prototype is like a table of contents - it tells C what functions are coming so it can understand references to them.

Pass by Value (Functions Get Copies)

Here’s something important: when you pass a value to a function, the function gets a copy, not the original.

void try_to_change(int x) {
    x = 100;  // This changes the LOCAL copy
    printf("Inside function: %d\n", x);
}

int main(void) {
    int num = 5;
    try_to_change(num);
    printf("After function: %d\n", num);  // Still 5!
    return 0;
}

Output:

Inside function: 100
After function: 5

The function changed its own copy of x, but the original num in main stayed at 5. It’s like giving someone a photocopy of a document - they can write all over the copy, but your original is safe.

We’ll learn how to actually change the original in Part 10 when we cover pointers.

Scope: Where Variables Exist

Scope means “where a variable is visible.” Variables have limited lifespans - they only exist in certain parts of your code.

#include <stdio.h>

int global = 100;  // Global - exists everywhere

void my_function(void) {
    int local = 50;  // Local - only exists inside this function
    printf("global: %d, local: %d\n", global, local);
}

int main(void) {
    int main_var = 25;  // Local to main

    printf("global: %d\n", global);      // Works
    printf("main_var: %d\n", main_var);  // Works
    // printf("local: %d\n", local);     // ERROR - local doesn't exist here

    my_function();
    return 0;
}

Variables declared inside a function are called local variables. They’re born when the function starts and die when the function ends. Variables declared outside all functions are global variables - they exist the whole time your program runs.

Good habit: Avoid global variables. Pass data through parameters instead. Why? Because when anything can change a variable from anywhere, it becomes very hard to figure out what your program is doing. It’s like having everyone in a house share the same notebook - after a while, nobody knows who wrote what.

Block Scope

Variables declared inside curly braces { } only exist inside those braces:

int main(void) {
    int x = 10;

    if (x > 5) {
        int y = 20;  // y only exists inside this if block
        printf("y: %d\n", y);  // Works
    }

    // printf("y: %d\n", y);  // ERROR - y doesn't exist here

    for (int i = 0; i < 3; i++) {
        // i only exists inside this loop
    }
    // printf("i: %d\n", i);  // ERROR - i doesn't exist here

    return 0;
}

Static Variables: Memory That Sticks Around

Normally, local variables forget their values between function calls. The static keyword makes them remember:

void counter(void) {
    static int count = 0;  // Only initialized ONCE, keeps its value
    count++;
    printf("Called %d times\n", count);
}

int main(void) {
    counter();  // Called 1 times
    counter();  // Called 2 times
    counter();  // Called 3 times
    return 0;
}

A regular local variable would reset to 0 each time. A static variable keeps its value from one call to the next.

Recursion: Functions Calling Themselves

A function can call itself. This is called recursion.

The classic example is factorial - the product of all positive integers up to a number. For example, 5! (read “five factorial”) = 5 × 4 × 3 × 2 × 1 = 120.

int factorial(int n) {
    if (n <= 1) {
        return 1;  // Base case - stop here
    }
    return n * factorial(n - 1);  // Call itself with a smaller number
}

int main(void) {
    printf("5! = %d\n", factorial(5));  // 120
    return 0;
}

Here’s what happens when we call factorial(5):

factorial(5)
= 5 * factorial(4)
= 5 * 4 * factorial(3)
= 5 * 4 * 3 * factorial(2)
= 5 * 4 * 3 * 2 * factorial(1)
= 5 * 4 * 3 * 2 * 1
= 120

Important: You need a base case - a condition that stops the recursion. Without it, the function calls itself forever until your program crashes.

Arrays and Functions

When you pass an array to a function, you also need to tell the function how big it is:

void print_array(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int sum_array(int arr[], int size) {
    int total = 0;
    for (int i = 0; i < size; i++) {
        total += arr[i];
    }
    return total;
}

int main(void) {
    int numbers[] = {1, 2, 3, 4, 5};
    int length = sizeof(numbers) / sizeof(numbers[0]);

    print_array(numbers, length);  // 1 2 3 4 5
    printf("Sum: %d\n", sum_array(numbers, length));  // 15

    return 0;
}

Why pass the size separately? The function can’t figure out how big the array is on its own. Something interesting happens when you pass an array to a function - we’ll explain exactly what in Part 10.

Complete Example: Temperature Converter

Let’s put it all together with a useful program:

#include <stdio.h>

// Prototypes - tell C what functions are coming
double celsius_to_fahrenheit(double c);
double fahrenheit_to_celsius(double f);
void print_conversion(double temp, char from_unit);

int main(void) {
    print_conversion(100, 'C');  // Boiling point of water
    print_conversion(32, 'F');   // Freezing point
    print_conversion(0, 'C');
    print_conversion(212, 'F');
    return 0;
}

double celsius_to_fahrenheit(double c) {
    return c * 9.0 / 5.0 + 32.0;
}

double fahrenheit_to_celsius(double f) {
    return (f - 32.0) * 5.0 / 9.0;
}

void print_conversion(double temp, char from_unit) {
    if (from_unit == 'C') {
        printf("%.1f C = %.1f F\n", temp, celsius_to_fahrenheit(temp));
    } else {
        printf("%.1f F = %.1f C\n", temp, fahrenheit_to_celsius(temp));
    }
}

Output:

100.0 C = 212.0 F
32.0 F = 0.0 C
0.0 C = 32.0 F
212.0 F = 100.0 C

Try It Yourself

  1. Write a function int max(int a, int b) that returns the larger of two numbers
  2. Write a function int is_even(int n) that returns 1 if n is even, 0 if odd (hint: use the % operator)
  3. Write a function void print_stars(int n) that prints n asterisks on one line
  4. Write a recursive function to calculate the nth Fibonacci number (where each number is the sum of the two before it: 1, 1, 2, 3, 5, 8, 13…)

Common Mistakes

  • Forgetting to return a value from a non-void function
  • Using a variable outside its scope
  • Forgetting the prototype when calling a function defined later
  • Thinking you can change a variable by passing it to a function (you need pointers for that - coming in Part 10)
  • Forgetting the base case in recursion (infinite recursion = crash)

Next Up

In Part 9, we’ll learn about the preprocessor - the text substitution step that runs before your code compiles. Understanding #include, #define, and conditional compilation.


Enjoyed This?

If this helped something click, subscribe to my YouTube channel. More content like this, same approach - making things stick without insulting your intelligence. It’s free, it helps more people find this stuff, and it tells me what’s worth making more of.