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
intand expensive for large objects, which motivatesconst&.
Questions about this lesson
Log in to ask a question.