Intermediate

Functions in C++

Learn how to define and use functions in C++ including parameters, default arguments, references, overloading, recursion, and lambdas with Docker-ready examples

Functions are the fundamental building blocks of structured C++ programs. They let you package a piece of logic behind a name, give it inputs (parameters), and hand back a result (a return value) — turning sprawling code into reusable, testable, and readable units.

As a multi-paradigm language, C++ gives functions an unusually rich feature set. Beyond the basics shared with C — declarations, definitions, and pass-by-value semantics — C++ adds references, default arguments, function overloading, and first-class functional tools like lambdas and std::function. This means the same add(a, b) idea can be expressed as a plain free function, an overloaded set of functions, or an inline lambda captured into a variable.

In this tutorial you’ll learn how to declare and define functions, pass arguments by value and by reference, use default parameters and overloading, understand variable scope, write recursive functions, and treat functions as values with lambdas and higher-order functions.

Defining and Calling Functions

A C++ function has a return type, a name, a parameter list, and a body. Functions are often declared first (a prototype, ending in a semicolon) so they can be called before their full definition appears later in the file.

Create a file named functions_basic.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <string>

// Function declarations (prototypes)
int add(int a, int b);
double average(double a, double b);
void greet(const std::string& name);

int main() {
    std::cout << "add(3, 4) = " << add(3, 4) << std::endl;
    std::cout << "average(10, 20) = " << average(10, 20) << std::endl;
    greet("Ada");
    return 0;
}

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

double average(double a, double b) {
    return (a + b) / 2.0;
}

// void functions perform an action but return nothing
void greet(const std::string& name) {
    std::cout << "Hello, " << name << "!" << std::endl;
}

The prototypes at the top tell the compiler each function’s signature (name, parameter types, return type) so main() can call them even though their bodies come afterward. A void return type means the function returns no value.

Default Arguments, References, and Overloading

C++ lets a parameter have a default argument that is used when the caller omits it. Passing by reference (int&) lets a function modify the caller’s variable instead of working on a copy. And function overloading allows multiple functions to share one name, distinguished by their parameter types.

Create a file named functions_params.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

// Default parameter: exp defaults to 2 when omitted
int power(int base, int exp = 2) {
    int result = 1;
    for (int i = 0; i < exp; ++i) {
        result *= base;
    }
    return result;
}

// Pass by reference: modifies the caller's variable directly
void doubleValue(int& n) {
    n *= 2;
}

// Function overloading: same name, different parameter types
int multiply(int a, int b) {
    return a * b;
}

double multiply(double a, double b) {
    return a * b;
}

int main() {
    std::cout << "power(5) = " << power(5) << std::endl;        // uses default exp = 2
    std::cout << "power(2, 10) = " << power(2, 10) << std::endl;

    int x = 21;
    doubleValue(x);
    std::cout << "x after doubleValue = " << x << std::endl;

    std::cout << "multiply(3, 4) = " << multiply(3, 4) << std::endl;
    std::cout << "multiply(1.5, 2.0) = " << multiply(1.5, 2.0) << std::endl;
    return 0;
}

The compiler picks the correct multiply based on the argument types — integer arguments call the int version, floating-point arguments call the double version. Because doubleValue takes an int&, the change to n is visible back in main().

Scope and Recursion

Variables declared inside a function are local — they exist only while the function runs and are invisible to other functions. A function may also call itself, a technique called recursion, which is a natural fit for problems defined in terms of smaller subproblems.

Create a file named functions_recursion.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

// Recursive factorial: n! = n * (n-1)!
int factorial(int n) {
    if (n <= 1) {       // base case stops the recursion
        return 1;
    }
    return n * factorial(n - 1);
}

// Recursive Fibonacci: fib(n) = fib(n-1) + fib(n-2)
int fibonacci(int n) {
    if (n < 2) {        // base cases: fib(0) = 0, fib(1) = 1
        return n;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

int main() {
    int count = 5;      // local variable, scoped to main()
    std::cout << "factorial(" << count << ") = " << factorial(count) << std::endl;
    std::cout << "fibonacci(10) = " << fibonacci(10) << std::endl;
    return 0;
}

Every recursive function needs a base case that returns without recursing, otherwise it would call itself forever. The local variable count lives only inside main() — the n parameters inside factorial and fibonacci are entirely separate.

Lambdas and Higher-Order Functions

Modern C++ (since C++11) treats functions as values. A lambda is an anonymous function you can store in a variable, and a higher-order function is one that takes or returns another function. The std::function wrapper can hold any callable with a matching signature.

Create a file named functions_modern.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>

// Higher-order function: accepts another function as a parameter
int applyTwice(const std::function<int(int)>& f, int value) {
    return f(f(value));
}

int main() {
    // A lambda stored in a variable
    auto square = [](int x) { return x * x; };
    std::cout << "square(6) = " << square(6) << std::endl;

    // A lambda that captures a local variable by value
    int factor = 3;
    auto scale = [factor](int x) { return x * factor; };
    std::cout << "scale(10) = " << scale(10) << std::endl;

    // Passing a lambda into a higher-order function
    std::cout << "applyTwice(square, 2) = " << applyTwice(square, 2) << std::endl;

    // Using a lambda with a standard library algorithm
    std::vector<int> nums = {5, 2, 8, 1, 9, 3};
    std::sort(nums.begin(), nums.end(), [](int a, int b) { return a > b; });
    std::cout << "Sorted descending:";
    for (int n : nums) {
        std::cout << " " << n;
    }
    std::cout << std::endl;
    return 0;
}

The [factor] capture clause copies the surrounding factor variable into the lambda so it can be used inside the body. Passing the [](int a, int b) { return a > b; } comparator to std::sort is idiomatic modern C++ — the algorithm calls your function for each comparison.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Pull the official GCC image (includes g++)
docker pull gcc:14

# Compile and run the basic functions example
docker run --rm -v $(pwd):/app -w /app gcc:14 \
    sh -c "g++ -o functions functions_basic.cpp && ./functions"

# Compile and run the parameters example
docker run --rm -v $(pwd):/app -w /app gcc:14 \
    sh -c "g++ -o params functions_params.cpp && ./params"

# Compile and run the recursion example
docker run --rm -v $(pwd):/app -w /app gcc:14 \
    sh -c "g++ -o recursion functions_recursion.cpp && ./recursion"

# Compile and run the lambdas example
docker run --rm -v $(pwd):/app -w /app gcc:14 \
    sh -c "g++ -o modern functions_modern.cpp && ./modern"

Expected Output

Running functions_basic.cpp:

add(3, 4) = 7
average(10, 20) = 15
Hello, Ada!

Running functions_params.cpp:

power(5) = 25
power(2, 10) = 1024
x after doubleValue = 42
multiply(3, 4) = 12
multiply(1.5, 2.0) = 3

Running functions_recursion.cpp:

factorial(5) = 120
fibonacci(10) = 55

Running functions_modern.cpp:

square(6) = 36
scale(10) = 30
applyTwice(square, 2) = 16
Sorted descending: 9 8 5 3 2 1

Key Concepts

  • Declarations vs definitions — A prototype (int add(int, int);) declares a function’s signature so it can be called before its body is defined later in the file.
  • Pass by value vs reference — Plain parameters receive copies, while reference parameters (int&) let a function modify the caller’s variable directly; use const& to pass large objects efficiently without copying.
  • Default arguments — Parameters can have default values, making them optional at the call site; defaults must come last in the parameter list.
  • Function overloading — Multiple functions can share a name as long as their parameter types differ; the compiler resolves which to call from the argument types.
  • Local scope — Variables and parameters declared inside a function exist only during that call and are isolated from other functions.
  • Recursion — A function may call itself; every recursive function needs a base case to terminate.
  • Lambdas — Anonymous functions written inline with [](...) {...} can capture surrounding variables and be stored in auto variables.
  • Higher-order functions — With std::function and lambdas, functions become first-class values you can pass to and return from other functions, powering the standard library’s algorithms.

Running Today

All examples can be run using Docker:

docker pull gcc:14
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining