Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

Functions: Defining, Calling, and Pass-by-Value

Recommended resources

Hello! Welcome to your fourth lesson in our C programming series.

In our last lesson, we covered the fundamental building blocks of C: data types, operators, and control flow. We saw how C's static typing requires you to be explicit about data, a contrast to the dynamic nature of JavaScript and PHP. You also got a taste of low-level manipulation with bitwise operators.

Today, we'll build on that foundation to explore one of the most important concepts for organizing code: functions. You're already an expert at using functions in other languages, but we will focus on the specific mechanics of how they work in C. This lesson will show you how to define and call functions, and more importantly, we'll dissect C's pass-by-value mechanism for function arguments. Understanding this is absolutely critical for writing correct and efficient C code and is a frequent topic in technical interviews.

The Anatomy of a C Function

In any programming language, functions are self-contained blocks of code that perform a specific task. They are the primary tool for creating modular, reusable, and readable programs.

A C function has four main parts:

  1. Return Type: The type of the value the function sends back to its caller (e.g., int, double, or void if it returns nothing).
  2. Function Name: The identifier used to call the function.
  3. Parameters: A list of variables that accept input values for the function.
  4. Function Body: The block of code enclosed in {} that executes when the function is called.

Here is a simple example:

//  Return Type | Function Name | Parameters
//      |             |               |
//      v             v               v
       int           add        (int a, int b) 
{ // Start of function body
    int sum = a + b; // Local variable
    return sum;      // Return statement
} // End of function body

Function Prototypes: Declaring Before Defining

In C, the compiler reads your code from top to bottom. If you call a function before the compiler has seen its definition, it will produce an error. To solve this, you can provide a function prototype (or declaration) before the function is called. A prototype is simply the function's signature followed by a semicolon.

This is a common practice that allows you to place your main function at the top of the file for readability, with the implementations of other functions following it.

C Programming Functions Exercises

The following resource provides several practical exercises on functions. Pay close attention to Exercise 4, which clearly demonstrates the need for and syntax of a function prototype.

Read through 'Exercise 4: Function Prototype/Declaration'. Note how the prototype int is_even(int num); allows main to call is_even even though the full definition appears later in the file.


The Call Stack: How Functions are Managed

When you call a function, how does the computer keep track of where to return to and what the local variables are? The answer is a memory structure called the call stack.

Think of it like a stack of plates: you can only add a new plate to the top (a "push") and remove the topmost plate (a "pop").

When a function is called, a stack frame is pushed onto the top of the call stack. This frame is a dedicated block of memory for that specific function call, containing:

  • The function's parameters (arguments).
  • The function's local variables.
  • A return address (where to resume execution in the calling function).

When the function returns, its stack frame is popped off the stack, and all its local variables are destroyed.

This video gives a clear, concise explanation of how the stack is used for function calls.

What are Stack and Heap Memory?

The video 'What are Stack and Heap Memory?' from Boot dev provides a great visual model for understanding stack frames.

Watch the section from 00:35 to 02:49. Focus on how a new stack frame is created for the create_typist function call. The key takeaway is the statement at 01:45: 'when we're thinking about function arguments to remember that they're getting copied'. This is the essence of pass-by-value.


Pass-by-Value: The Golden Rule of C

This leads us to the single most important rule about function arguments in C:

Everything in C is passed by value. Always. No exceptions.

When you pass an argument to a function, a copy of that argument's value is made and stored in the function's stack frame. The function works exclusively with this copy. Any changes made to the parameter inside the function have no effect on the original variable in the calling function.

This is different from how objects are often handled in languages like JavaScript, where you might be used to passing an object and having the function modify it. In C, the mechanism is much more explicit.

Let's see a practical demonstration.

Understanding Functions, Memory, and Pointers in C

The 'Understanding Functions, Memory, and Pointers in C' guide provides a fantastic, detailed trace of how the call stack works and what pass-by-value means in practice.

First, read 'Part 1: Functions and the Call Stack'. The step-by-step trace of the main -> add_and_double -> multiply calls is an excellent visualization of the stack growing and shrinking. Then, read 'Part 3.2 The Truth About Pass By Value', which uses the classic 'swap problem' to show exactly why modifying a parameter doesn't change the original variable. The diagrams here are key.

This broken swap function is a canonical example of pass-by-value in action. The values of x and y from main are copied to the parameters a and b in swap_broken. The function successfully swaps the copies, a and b, but when it returns, its stack frame is destroyed, and the original x and y in main remain untouched.


Simulating "Pass-by-Reference" with Pointers

So, how can we write a function that does modify a variable in the caller's scope? We do this by passing the variable's memory address.

This technique is often called "pass-by-reference," but that's a bit of a misnomer in C. What you're actually doing is passing the value of an address. The address itself is still copied (pass-by-value!), but the function can then use that address to "reach back" into the caller's memory and modify the original data.

This is achieved using pointers.

  • The function parameter is declared as a pointer (e.g., int *ptr).
  • When calling the function, you pass the address of the variable using the address-of operator & (e.g., func(&my_var)).
  • Inside the function, you use the dereference operator * to access or modify the value at that address (e.g., *ptr = 100;).

This animation gives a simple visual metaphor for the concept.

Pass by Reference vs. Pass by Value Animation with a Cup Metaphor
This animation illustrates the core idea: pass-by-value works on a copy, while the pass-by-reference idiom gives the function a way to access the original item.

Now, let's see how this solves our swap problem.

Call By Value & Call By Reference in C

The video 'Call By Value & Call By Reference in C' by Neso Academy provides a direct comparison of the two mechanisms.

Watch the entire video (it's about 8 minutes). The first part (up to 02:43) covers 'Call By Value', reinforcing what we've just discussed. The second part (from 02:43 onwards) explains how using pointers and addresses allows you to achieve what is known as 'Call By Reference'.

The corrected swap function looks like this:

C Program: Pass Pointers to Functions for Swapping Values
This image shows a complete, working C program that uses pointers to swap two integer values. Notice the use of `&` in the function call and `*` in the function definition and body.

The key is that swap_working(&x, &y) doesn't pass the values 5 and 10. It passes the memory addresses where x and y are stored. The function receives copies of these addresses, but that's enough to give it the power to modify the original variables.


Conclusion

This lesson drew a crucial line between how functions work in higher-level languages and the explicit memory operations in C. Understanding this distinction is fundamental to your goal of mastering systems programming.

Key Takeaways:

  • Functions organize code into reusable blocks. A function prototype is required if you call a function before it is defined.
  • C always uses pass-by-value: arguments passed to a function are copied into the function's private stack frame.
  • Modifying a parameter inside a function does not affect the original variable.
  • To allow a function to modify a caller's variable, you must simulate "pass-by-reference" by passing a pointer (the address of the variable). This is still pass-by-value, but you are passing the value of an address.

In our next lesson, we'll look at a special case of function arguments: main(int argc, char *argv[]). We will learn how to parse command-line arguments, which will enable you to start building simple, interactive command-line tools.



Practice Exercises

Time to solidify your understanding. As you requested, here are five exercises to practice these concepts. Set up a new project folder and work through them.

  1. Area Calculator with Prototype:
    Write a program where main asks for the length and width of a rectangle (as doubles). It then calls a function calculate_area that is defined after main. This function should take the length and width as arguments and return the calculated area. Make sure to include the necessary function prototype before main. Print the result in main.

  2. Pass-by-Value Proof:
    Write a function triple(int n) that takes an integer, multiplies its local copy by 3, and prints the result from within the function. In main, declare an integer num, assign it a value (e.g., 5), print its value, call triple(num), and then print num's value again to prove that the original variable was not changed.

  3. The swap Function:
    Implement the swap(int *a, int *b) function that correctly swaps the values of two integers from main. Your main function should declare two integers, print their values, call the swap function with their addresses, and then print their values again to show they have been swapped.

  4. Quotient and Remainder:
    Write a function divide(int dividend, int divisor, int *quotient, int *remainder). This function should not return any value (void). Instead, it should compute the quotient and remainder of the integer division and store the results in the memory locations pointed to by the quotient and remainder pointers. In main, call this function and print the results. This is a common C idiom for "returning" multiple values.

  5. Normalize Vector:
    In mathematics, normalizing a 2D vector means scaling it to a length of 1. Write a function normalize(double *x, double *y). This function should:

    1. Calculate the vector's length: length = sqrt((*x)*(*x) + (*y)*(*y)). You will need to include <math.h> and compile with the -lm flag (e.g., gcc my_program.c -o my_program -lm).
    2. If the length is not zero, modify the original x and y values by dividing them by the length: *x = *x / length; and *y = *y / length;.
      In main, define two double variables (e.g., x = 3.0, y = 4.0), print their values, call normalize, and print their new values to confirm they have been changed.