Control Flow

Functions in C++

DIFFICULTY: Beginner READ TIME: 12 mins

Welcome back! Today, we’re talking about one of the most powerful concepts in programming: Functions.

Think of a function as a custom tool or a mini-program inside your main program. If you need to perform a specific task—like calculating a score, converting Celsius to Fahrenheit, or validating user input—you can wrap that logic inside a function. Write it once, name it, and call it whenever you need it. This keeps your code clean, readable, and saves you from copy-pasting code like a caveman.


What is a Function?

A function is a self-contained block of code that takes some input (called parameters or arguments), performs operations on that input, and optionally returns a result.

Syntax:

return_type function_name(parameter_list) {
    // Code for operations performed by function
    return value; 
}

Let’s break down the three main components of any function.


Components of a Function

1. Return Type

The return type specifies what kind of data the function will send back after it finishes its job (e.g., int, double, string). If the function just does some work (like printing a message) but doesn’t need to return any data, we use the void keyword.

Example:

// Returns a whole number
int getScore() {
    return 100;
}

// Doesn't return anything, just prints
void sayHello() {
    std::cout << "Hey there!" << std::endl;
}

2. Function Name

This is the identifier you use to call the function. Be descriptive! If your function calculates a discount, name it calculateDiscount, not func1 or stuff.


3. Arguments vs. Parameters

These terms get thrown around interchangeably, but there’s a subtle difference:

  • Parameters: The placeholder variables defined in the function declaration (the inputs it expects).
  • Arguments: The actual values you pass into the function when you call it.

Let’s look at a concrete example:

Example:

#include <iostream>

// 'a' and 'b' are PARAMETERS
int addNumbers(int a, int b) {
    return a + b;
}

int main() {
    // 10 and 20 are ARGUMENTS passed to the function
    int sum = addNumbers(10, 20);
    std::cout << "The sum is: " << sum << std::endl;
    return 0;
}

Output:

The sum is: 30

Parameter Passing Methods

C++ gives you three main ways to pass data into a function: Pass by Value, Pass by Reference, and Pass by Pointer. Understanding the difference is crucial for memory efficiency and avoiding bugs.

Pass by Value

When you pass an argument by value, C++ creates a copy of that variable and hands it to the function. Any changes made to the variable inside the function only affect the copy. The original variable outside the function remains completely untouched.

Example:

#include <iostream>

void incrementValue(int num) {
    num = num + 1; // Modifies the local copy
    std::cout << "Inside function (copy): " << num << std::endl;
}

int main() {
    int x = 10;
    incrementValue(x);
    std::cout << "Outside function (original): " << x << std::endl;
    return 0;
}

Output:

Inside function (copy): 11
Outside function (original): 10

Pass by Reference

When you pass an argument by reference, you are giving the function direct access to the actual variable in memory. No copy is made. Any changes made inside the function will modify the original variable.

To pass by reference, simply add an ampersand & prefix to the parameter’s data type.

Example:

#include <iostream>

void incrementReference(int &num) {
    num = num + 1; // Modifies the original variable directly!
    std::cout << "Inside function: " << num << std::endl;
}

int main() {
    int x = 10;
    incrementReference(x);
    std::cout << "Outside function: " << x << std::endl;
    return 0;
}

Output:

Inside function: 11
Outside function: 11

Visualizing Pass-by-Value vs. Pass-by-Reference

Think of Pass by Value like sending someone a copy of a Word document. They can edit it, delete paragraphs, or scribble all over it, but your original file on your hard drive remains unchanged.

Pass by Reference is like sharing a Google Doc link. You both are looking at and editing the exact same file in real-time.

graph TD
    subgraph Pass By Value
        V_Orig[Original Var: x = 10 <br/> Address: 0x1000] 
        V_Copy[Copy Var: num = 10 <br/> Address: 0x2000]
        V_Orig -.->|Clones Value| V_Copy
        V_Copy -->|Modified inside function| V_Mod[num = 11 <br/> Address: 0x2000]
        V_Mod -.->|Original remains unaffected!| V_Orig
    end
    subgraph Pass By Reference
        R_Orig[Original Var: x = 10 <br/> Address: 0x3000]
        R_Ref[Ref Alias: &num <br/> Address: 0x3000]
        R_Orig ===|Points to same memory| R_Ref
        R_Ref -->|Modified inside function| R_Mod[num = 11 <br/> Address: 0x3000]
        R_Mod -->|Original x is modified too!| R_Orig
    end

Passing Arguments by Pointer

Passing by pointer is a legacy C-style method similar to passing by reference. Instead of passing the variable or a copy, you pass the memory address (pointer) of the variable. You then dereference the pointer inside the function to edit the value.

Think of it like writing down your home address on a piece of paper and giving it to a painter. They use the address to go to your house and paint it.

Syntax:

void modifyValue(int* ptr) {
    *ptr = 10; // Dereference pointer to change value at that memory address
}

Example:

#include <iostream>

void modifyValue(int* ptr) {
    *ptr = 10; // Directly updates value stored in address held by ptr
}

int main() {
    int x = 5;
    std::cout << "Before pointer modification: " << x << std::endl;
    
    modifyValue(&x); // Pass address of x using &
    
    std::cout << "After pointer modification: " << x << std::endl;
    return 0;
}

Output:

Before pointer modification: 5
After pointer modification: 10

Types of Functions

C++ functions fit into two main buckets: Inbuilt (Standard Library) functions and User-Defined functions.

flowchart TD
    Func[C++ Functions] --> Inbuilt[Inbuilt / Standard Library]
    Func --> UserDef[User-Defined Functions]
    
    Inbuilt --> Math[cmath: sqrt, pow, sin]
    Inbuilt --> Algo[algorithm: sort, max, min, swap]
    
    UserDef --> Custom[Custom logic written by developer]

1. Inbuilt Functions (Standard Library Functions)

These are helper functions built right into C++. You don’t have to write the math behind a square root or power function; you just #include the library header and use them!

FunctionPurposeHeaderSyntax
sqrt(n)Computes the square root of n<cmath>sqrt(16.0)
pow(x, y)Computes x raised to the power of y<cmath>pow(2.0, 3.0)
max(a, b)Returns the larger of the two values<algorithm>std::max(5, 10)
min(a, b)Returns the smaller of the two values<algorithm>std::min(5, 10)
swap(a, b)Swaps the values of the two variables<utility> or <algorithm>std::swap(x, y)

Example:

#include <iostream>
#include <cmath>
#include <algorithm>

int main() {
    double root = sqrt(16.0);
    double power = pow(2.0, 3.0);
    int maximum = std::max(5, 10);
    
    std::cout << "Square root of 16: " << root << std::endl;
    std::cout << "2^3: " << power << std::endl;
    std::cout << "Max of 5 and 10: " << maximum << std::endl;
    
    return 0;
}

Output:

Square root of 16: 4
2^3: 8
Max of 5 and 10: 10

2. User-Defined Functions

These are functions that you write yourself to handle specific logic for your application.

Example:

#include <iostream>

// Our custom function
void greetUser(std::string name) {
    std::cout << "Welcome back to C++ Mastery, " << name << "!" << std::endl;
}

int main() {
    greetUser("Alex");
    return 0;
}

Output:

Welcome back to C++ Mastery, Alex!

Inline Functions

Usually, calling a function has a tiny performance cost (overhead). The computer has to save its current spot, jump to the function’s memory address, execute the code, and then jump back.

If you have a tiny, simple function (like a one-liner) that gets called millions of times inside a loop, this jumping around slows things down. By marking a function as inline, you instruct the compiler to replace the function call directly with the actual code body, eliminating the call overhead.

graph TD
    subgraph Normal Function Call
        N_Main[main: call myFunc] -->|1. Context Switch <br/> Save Registers| N_Stack[Jump to myFunc address]
        N_Stack -->|2. Execute Body| N_Exec[Run myFunc Code]
        N_Exec -->|3. Pop Stack <br/> Return to main| N_Main
    end
    subgraph Inline Function Call
        I_Main[main: call myFunc] -->|Compile Time| I_Expand[Compiler replaces call with code body]
        I_Expand -->|Execution Time| I_Run[Runs inline without jumping / stack overhead!]
    end

Example:

#include <iostream>

// Inline keyword tells compiler to expand this body inline
inline int square(int x) {
    return x * x;
}

int main() {
    std::cout << "Square of 5: " << square(5) << std::endl;
    // Compiler literally expands this to: std::cout << "Square of 5: " << 5 * 5 << std::endl;
    return 0;
}

Output:

Square of 5: 25

[!NOTE] Keep them small: Only inline small functions. If you inline a massive function, your compiled binary file will bloat in size, which can actually make your program slower. Also, inline is just a suggestion to the compiler; modern compilers are smart and might ignore it or inline functions automatically!


Lambda Functions (Anonymous Functions)

Lambda functions are quick, inline, nameless functions. They are ideal for quick, throwaway tasks—like passing a custom sorting rule to an algorithm.

Syntax:

[ captures ] ( parameters ) -> return_type { 
    // function code 
};
  • Captures ([]): Allows the lambda to access local variables in the surrounding scope. Use [=] to capture all variables by value, or [&] to capture by reference.
  • Parameters (()): The inputs, identical to normal functions.
  • Return Type (-> return_type): Optional. The compiler can usually deduce it automatically.

Example:

#include <iostream>

int main() {
    // A simple lambda function assigned to a variable
    auto sum = [](int a, int b) {
        return a + b;
    };
    
    std::cout << "Lambda Sum: " << sum(5, 10) << std::endl;
    return 0;
}

Output:

Lambda Sum: 15

Function Overloading

C++ allows you to have multiple functions with the exact same name, as long as they have different parameter lists (different number of parameters, different types, or different order). This is called Function Overloading. The compiler automatically figures out which function to call based on the arguments you pass.

Example:

#include <iostream>

// Overload 1: Takes two ints
void display(int i) {
    std::cout << "Printing integer: " << i << std::endl;
}

// Overload 2: Takes a double
void display(double f) {
    std::cout << "Printing float: " << f << std::endl;
}

// Overload 3: Takes a string
void display(std::string s) {
    std::cout << "Printing string: " << s << std::endl;
}

int main() {
    display(5);        // Calls Overload 1
    display(3.14);     // Calls Overload 2
    display("Hello!"); // Calls Overload 3
    return 0;
}

Output:

Printing integer: 5
Printing float: 3.14
Printing string: Hello!

Recursion

Recursion is a programming technique where a function calls itself to solve a problem. It works by breaking down a complex problem into smaller, simpler subproblems of the same type.

Every recursive function needs two core elements:

  1. Base Case: The stopping condition that ends the recursion. Without this, your function will call itself infinitely and crash the program (a stack overflow).
  2. Recursive Case: The part where the function calls itself with a smaller version of the problem.

Let’s look at the classic factorial calculation ($5! = 5 \times 4 \times 3 \times 2 \times 1$):

Example:

#include <iostream>

int factorial(int n) {
    // Base Case: exit recursion when n hits 1 or 0
    if (n <= 1) {
        return 1;
    }
    // Recursive Case: n! = n * (n-1)!
    return n * factorial(n - 1);
}

int main() {
    int result = factorial(5);
    std::cout << "Factorial of 5: " << result << std::endl;
    return 0;
}

Output:

Factorial of 5: 120

Level Up: Extra Practice & Interview Questions

Q1. Write a function to calculate the square of a number using the inbuilt pow() function.

Remember that pow() takes and returns double types, so we should handle casting carefully if we want integers.

Code:

#include <iostream>
#include <cmath>

int getSquare(int n) {
    // pow returns double, so we cast it back to int
    return static_cast<int>(pow(n, 2));
}

int main() {
    int num = 6;
    std::cout << "Square of " << num << " is " << getSquare(num) << std::endl;
    return 0;
}

Output:

Square of 6 is 36

Q2. Write a function that returns the greatest of three numbers.

We can implement this with nested conditions or chain standard std::max.

Code:

#include <iostream>
#include <algorithm>

int findMaxOfThree(int a, int b, int c) {
    return std::max({a, b, c}); // Uses initializer list with std::max
}

int main() {
    int x = 12, y = 45, z = 23;
    std::cout << "The max value is: " << findMaxOfThree(x, y, z) << std::endl;
    return 0;
}

Output:

The max value is: 45

Q3. Write a recursive program to find the sum of digits of a number.

For a number like 1234, the sum of digits is $1 + 2 + 3 + 4 = 10$. We can isolate the last digit using % 10 and strip it off using / 10.

Code:

#include <iostream>

int sumOfDigits(int n) {
    // Base case: if number becomes 0, stop recursion
    if (n == 0) {
        return 0;
    }
    // Recursive case: last digit + sum of remaining digits
    return (n % 10) + sumOfDigits(n / 10);
}

int main() {
    int number = 12345;
    std::cout << "Sum of digits in " << number << " is: " << sumOfDigits(number) << std::endl;
    return 0;
}

Output:

Sum of digits in 12345 is: 15

quiz Test Your Understanding

To pass a variable by reference in C++, what symbol is placed before the parameter name?