What Is It?
What Are Functions?
A function is a named block of code that performs a specific task. Instead of writing the same code multiple times, you write it once inside a function and call that function whenever you need it. Functions make code reusable, modular, and easier to debug.
Every C++ program has at least one function: main(). But real programs have dozens or hundreds of functions, each responsible for a well-defined task. The C++ standard library itself is a collection of thousands of functions organized into headers.
Key concepts covered in this chapter:
- Function declaration (prototype) -- telling the compiler a function exists
- Function definition -- the actual implementation
- Return types -- what the function sends back
- Pass-by-value -- passing copies of arguments
- Pass-by-reference (&) -- passing aliases to original variables
- Pass-by-pointer (*) -- passing memory addresses
- Function overloading -- multiple functions with the same name but different parameters
- Default arguments -- parameters with preset values
- Inline functions -- optimization hint for small functions
- Recursion -- a function calling itself
Why Does It Matter?
Why Are Functions Essential?
1. Code Reusability
Without functions, you would copy-paste the same code every time you need it. If you need to find the maximum of two numbers in 20 places, you write the logic once in a function and call it 20 times. If the logic needs to change, you change it in one place.
2. Modularity and Readability
Breaking a large program into small functions makes it readable. A function name like isPrime(n) is self-documenting. Readers can understand what the code does without reading every line. In team environments, different people can work on different functions simultaneously.
3. Debugging and Testing
When a bug occurs, you can isolate it to a specific function. You can test each function independently with different inputs. This is far easier than debugging a single monolithic main() function with 500 lines.
4. Recursion for Complex Problems
Some problems have naturally recursive structures: tree traversals, divide-and-conquer algorithms, backtracking problems. Recursion expresses these solutions elegantly. Understanding the call stack and recursion tracing is critical for placement interviews and competitive programming.
5. Interview Relevance
Pass-by-value vs pass-by-reference, function overloading resolution, recursion tracing (factorial, Fibonacci, Tower of Hanoi), and understanding the call stack are all common interview topics. Interviewers expect you to trace recursive calls on paper.
Detailed Explanation
Detailed Explanation
1. Function Declaration (Prototype) and Definition
A function declaration (prototype) tells the compiler the function's name, return type, and parameter types. It appears before main() so the compiler knows the function exists before it is called.
// Declaration (prototype)
int add(int a, int b);
int main() {
cout << add(3, 5); // Call
return 0;
}
// Definition
int add(int a, int b) {
return a + b;
}If the definition appears before main(), a separate declaration is not needed. But in multi-file projects, prototypes go in header files (.h) and definitions in source files (.cpp).
2. Return Types
A function can return any type: int, double, bool, string, char, etc. A function that returns nothing uses void as the return type.
void greet(string name) {
cout << "Hello, " << name << endl;
// No return statement needed (or use: return;)
}
bool isEven(int n) {
return n % 2 == 0;
}3. Pass-by-Value
By default, C++ passes arguments by value: the function receives a copy of the argument. Modifying the parameter inside the function does not affect the original variable.
void doubleIt(int x) {
x = x * 2; // Modifies the local copy only
}
int main() {
int a = 5;
doubleIt(a);
cout << a; // Still 5
}4. Pass-by-Reference (&)
A reference parameter is an alias for the original variable. Changes inside the function affect the original.
void doubleIt(int& x) { // Note the &
x = x * 2; // Modifies the original variable
}
int main() {
int a = 5;
doubleIt(a);
cout << a; // 10
}Use pass-by-reference when you need to: (1) modify the caller's variable, (2) avoid copying large objects (use const& for read-only), or (3) return multiple values from a function.
5. Pass-by-Pointer (*)
Similar to pass-by-reference but using explicit pointers:
void doubleIt(int* ptr) {
*ptr = (*ptr) * 2; // Dereference to modify
}
int main() {
int a = 5;
doubleIt(&a); // Pass address
cout << a; // 10
}Pointers require explicit dereferencing (*ptr) and can be null. References are safer and preferred in modern C++ unless you need null semantics or pointer arithmetic.
6. Function Overloading
C++ allows multiple functions with the same name but different parameter lists (different number or types of parameters). The compiler selects the correct version based on the arguments used at the call site.
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }The return type alone does not distinguish overloaded functions. The parameter list must differ.
7. Default Arguments
You can assign default values to parameters. If the caller omits those arguments, the defaults are used.
void greet(string name, string greeting = "Hello") {
cout << greeting << ", " << name << endl;
}
greet("Aarav"); // Hello, Aarav
greet("Neha", "Welcome"); // Welcome, NehaRules: default arguments must be specified from right to left. You cannot skip a default parameter in the middle. Defaults are typically specified in the declaration (prototype), not the definition.
8. Inline Functions
The inline keyword is a hint to the compiler to replace the function call with the function body, avoiding the overhead of a function call.
inline int square(int x) {
return x * x;
}Use inline for small, frequently called functions (1-3 lines). The compiler may ignore the hint for large or recursive functions. In modern C++, the compiler often inlines functions automatically based on optimization settings.
9. Recursion
A recursive function calls itself. Every recursive function needs:
- Base case -- a condition that stops the recursion
- Recursive case -- the function calls itself with a smaller/simpler input
Factorial
int factorial(int n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1); // Recursive case
}Tracing factorial(4): factorial(4) = 4 * factorial(3) = 4 * 3 * factorial(2) = 4 * 3 * 2 * factorial(1) = 4 * 3 * 2 * 1 = 24.
Fibonacci
int fib(int n) {
if (n <= 1) return n; // Base: fib(0)=0, fib(1)=1
return fib(n - 1) + fib(n - 2); // Two recursive calls
}Warning: this naive implementation is O(2^n). Each call branches into two, creating exponential work. Use memoization or iteration for efficiency.
Tower of Hanoi
void hanoi(int n, char from, char to, char aux) {
if (n == 1) {
cout << "Move disk 1 from " << from << " to " << to << endl;
return;
}
hanoi(n - 1, from, aux, to);
cout << "Move disk " << n << " from " << from << " to " << to << endl;
hanoi(n - 1, aux, to, from);
}Moves n disks from source to destination using an auxiliary peg. Total moves: 2^n - 1.
10. Recursion vs Iteration
Any recursive solution can be converted to an iterative one using a stack (explicit or implicit). Recursion is more readable for naturally recursive problems (trees, backtracking) but has overhead from function call stack frames. Deep recursion can cause stack overflow. Tail recursion (where the recursive call is the last operation) can be optimized by some compilers into a loop.
11. The Function Call Stack
Each function call creates a stack frame containing local variables, parameters, and the return address. When the function returns, its frame is popped. In recursion, each recursive call adds a new frame. If recursion is too deep (no base case or very large input), the stack overflows.
Code Examples
#include <iostream>
using namespace std;
void byValue(int x) {
x = 100;
cout << "Inside byValue: x = " << x << endl;
}
void byReference(int& x) {
x = 100;
cout << "Inside byReference: x = " << x << endl;
}
int main() {
int a = 5;
byValue(a);
cout << "After byValue: a = " << a << endl;
byReference(a);
cout << "After byReference: a = " << a << endl;
return 0;
}#include <iostream>
using namespace std;
void swapRef(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
void swapPtr(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
cout << "Before: x=" << x << ", y=" << y << endl;
swapRef(x, y);
cout << "After swapRef: x=" << x << ", y=" << y << endl;
swapPtr(&x, &y);
cout << "After swapPtr: x=" << x << ", y=" << y << endl;
return 0;
}#include <iostream>
using namespace std;
int area(int side) {
return side * side; // Square
}
int area(int length, int width) {
return length * width; // Rectangle
}
double area(double radius) {
return 3.14159 * radius * radius; // Circle
}
int main() {
cout << "Square (5): " << area(5) << endl;
cout << "Rectangle (4, 6): " << area(4, 6) << endl;
cout << "Circle (3.0): " << area(3.0) << endl;
return 0;
}#include <iostream>
using namespace std;
void printLine(char ch = '-', int length = 40) {
for (int i = 0; i < length; i++) {
cout << ch;
}
cout << endl;
}
int main() {
printLine(); // Uses both defaults: '-', 40
printLine('*'); // Uses default length: 40
printLine('=', 20); // No defaults used
return 0;
}#include <iostream>
using namespace std;
int factorial(int n) {
cout << "factorial(" << n << ") called" << endl;
if (n <= 1) {
cout << "factorial(" << n << ") returns 1" << endl;
return 1;
}
int result = n * factorial(n - 1);
cout << "factorial(" << n << ") returns " << result << endl;
return result;
}
int main() {
cout << "Result: " << factorial(5) << endl;
return 0;
}#include <iostream>
using namespace std;
void hanoi(int n, char from, char to, char aux) {
if (n == 1) {
cout << "Move disk 1 from " << from << " to " << to << endl;
return;
}
hanoi(n - 1, from, aux, to);
cout << "Move disk " << n << " from " << from << " to " << to << endl;
hanoi(n - 1, aux, to, from);
}
int main() {
int n = 3;
cout << "Tower of Hanoi with " << n << " disks:" << endl;
hanoi(n, 'A', 'C', 'B');
cout << "Total moves: " << (1 << n) - 1 << endl;
return 0;
}#include <iostream>
using namespace std;
int power(int base, int exp) {
if (exp == 0) return 1;
return base * power(base, exp - 1);
}
int sumOfDigits(int n) {
if (n == 0) return 0;
return (n % 10) + sumOfDigits(n / 10);
}
int main() {
cout << "2^10 = " << power(2, 10) << endl;
cout << "3^5 = " << power(3, 5) << endl;
cout << "Sum of digits of 12345: " << sumOfDigits(12345) << endl;
cout << "Sum of digits of 9999: " << sumOfDigits(9999) << endl;
return 0;
}Common Mistakes
Expecting Pass-by-Value to Modify the Original
void increment(int x) {
x++;
}
int main() {
int a = 5;
increment(a);
cout << a; // Expected 6, got 5
}void increment(int& x) {
x++;
}
int main() {
int a = 5;
increment(a);
cout << a; // Now 6
}Missing Base Case in Recursion
int factorial(int n) {
return n * factorial(n - 1); // No base case!
}int factorial(int n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1);
}Default Arguments in Wrong Order
// Compilation error: default argument missing for parameter 'b'
void func(int a = 10, int b, int c = 30) {
cout << a << b << c;
}void func(int a, int b = 20, int c = 30) {
cout << a << b << c;
}Overloading by Return Type Only
int getValue() { return 42; }
double getValue() { return 3.14; } // Error!int getInt() { return 42; }
double getDouble() { return 3.14; }Forgetting to Dereference Pointer Parameters
void setToZero(int* ptr) {
ptr = 0; // Bug: changes the local pointer, not the pointed-to value
}
int main() {
int x = 42;
setToZero(&x);
cout << x; // Still 42
}void setToZero(int* ptr) {
*ptr = 0; // Dereference: modifies the value at the address
}
int main() {
int x = 42;
setToZero(&x);
cout << x; // 0
}*ptr. Without the asterisk, you are only changing what the local pointer variable points to, which has no effect outside the function.Summary
- A function declaration (prototype) tells the compiler the function's name, return type, and parameter types. The definition provides the actual implementation.
- Pass-by-value creates a copy of the argument. Changes to the parameter do not affect the original variable. This is the default in C++.
- Pass-by-reference (int& x) creates an alias to the original variable. Changes affect the original. Use it to modify caller's variables or avoid copying large objects.
- Pass-by-pointer (int* ptr) passes the address. Requires dereferencing (*ptr) to access the value. Can be null, unlike references. Preferred in C; references preferred in C++.
- Function overloading allows multiple functions with the same name but different parameter lists. The compiler selects the correct version at compile time. Return type alone does not count.
- Default arguments must be specified from right to left. They are typically declared in the prototype, not the definition.
- Inline functions are a compiler hint to replace the call with the body. Use for small, frequently called functions. The compiler may ignore the hint.
- Recursion requires a base case (stopping condition) and a recursive case (calling itself with a smaller problem). Without a base case, the function recurses infinitely.
- Classic recursive problems: factorial (n * factorial(n-1)), Fibonacci (fib(n-1) + fib(n-2)), Tower of Hanoi (2^n - 1 moves), power, sum of digits.
- Tail recursion is when the recursive call is the last operation. Some compilers can optimize it into a loop (tail call optimization).
- Each function call creates a stack frame with local variables and return address. Deep recursion can cause stack overflow.
- Recursion vs iteration: recursion is more elegant for tree/backtracking problems; iteration is more efficient for simple loops. Any recursion can be converted to iteration using an explicit stack.