Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 2 of 8

Calling a function isn't free, and understanding what it costs explains recursion limits, why large parameters are slow, and a whole category of crash.

The call stack

Local variables live in a region of memory called the stack. Each function call pushes a stack frame holding that call's parameters, locals, and where to return to. When the function returns, its frame is popped and that memory is immediately reusable.

main() calls outer(), which calls inner():

   ┌─────────────────┐  ← top of stack (grows this way)
   │ inner()         │    its locals
   ├─────────────────┤
   │ outer()         │    its locals
   ├─────────────────┤
   │ main()          │    its locals
   └─────────────────┘

When inner returns, its frame vanishes and outer resumes exactly where it left off.

Two consequences follow immediately:

  • Locals disappear when a function returns. That's the lifetime rule from lesson 4.6, now with a mechanism behind it.
  • The stack is finite. Typically 1–8 MB. Enough for thousands of frames, not millions — which is what ends recursion (lesson 5.7).

You can see frames in action by watching lifetimes:

#include <iostream>

void inner() {
    int x = 3;
    std::cout << "inner has x = " << x << "\n";
}

void outer() {
    int x = 2;
    std::cout << "outer before: x = " << x << "\n";
    inner();
    std::cout << "outer after:  x = " << x << "\n";
}

int main() {
    int x = 1;
    outer();
    std::cout << "main:         x = " << x << "\n";
    return 0;
}
outer before: x = 2
inner has x = 3
outer after:  x = 2
main:         x = 1

Three separate x variables in three separate frames. inner setting its own x to 3 has no effect on the others — they aren't the same variable, they're different memory.

Arguments are copied

By default, passing an argument copies its value into the parameter:

#include <iostream>

void attempt_to_change(int value) {
    value = 999;
    std::cout << "inside:  " << value << "\n";
}

int main() {
    int number = 5;
    attempt_to_change(number);
    std::cout << "outside: " << number << "\n";
    return 0;
}
inside:  999
outside: 5

value is a copy living in the callee's frame. Changing it changes the copy. This is pass by value, and it's the default — the same behaviour you saw with range-based for in lesson 4.4, for the same reason.

Lesson 5.3 is about the alternative.

Returning a value

return copies the result back out (usually — the compiler often elides the copy entirely, which lesson 21.4 covers):

#include <iostream>

int doubled(int n) {
    int result = n * 2;
    return result;
}

int main() {
    int answer = doubled(21);
    std::cout << answer << "\n";
    return 0;
}
42

result is destroyed when doubled returns — but its value was copied out first, so answer is fine.

Never return a reference to a local

This is the dangerous version of that same fact:

int& broken() {
    int local = 42;
    return local;      // local dies here — the reference dangles
}

The reference refers to a stack frame that no longer exists. Reading it is undefined behaviour; it may appear to work, then break when unrelated code changes.

-Wall catches this specific case (warning: reference to local variable 'local' returned), but the general problem — returning a pointer or reference to something that's about to die — is one of the classic C++ bugs. Module 8 covers it properly.

Returning by value is always safe:

#include <iostream>
#include <string>

std::string make_greeting(const std::string& name) {
    std::string greeting = "Hello, " + name;
    return greeting;                 // the value is copied/moved out, safely
}

int main() {
    std::cout << make_greeting("Ada") << "\n";
    return 0;
}
Hello, Ada

The cost of copying

For an int, copying is one machine instruction. For a large object it's not:

#include <iostream>
#include <string>

std::size_t count_chars(std::string text) {     // copies the whole string
    return text.size();
}

std::size_t count_chars_fast(const std::string& text) {   // no copy
    return text.size();
}

int main() {
    std::string big(1000, 'x');

    std::cout << count_chars(big) << "\n";
    std::cout << count_chars_fast(big) << "\n";
    return 0;
}
1000
1000

Same answer. The first version allocated memory and copied 1000 bytes to look at a number the string already knew. In a loop that runs a million times, that's the difference between fast and unusable.

Which is exactly why lesson 5.4 argues that const& should be your default for anything bigger than a pointer.

Try it

Write a function swap_values(int a, int b) that tries to swap its two arguments, call it, and show that the caller's variables are unchanged. Explain why in a comment.

Checkpoint

  • Each call pushes a stack frame holding that call's parameters and locals; returning pops it.
  • The stack is finite (1–8 MB), which is what limits recursion depth.
  • Arguments are copied by default — modifying a parameter doesn't affect the caller.
  • Returning by value is safe; returning a reference or pointer to a local is not.
  • Copying is free for int and expensive for large objects, which motivates const&.

Questions about this lesson

Log in to ask a question.