C++ for Beginners Part 4: Functions
Break your programs into reusable, testable pieces with C++ functions. Covers declaration vs definition, parameters and return values, pass-by-value vs pass-by-reference, default parameters, function overloading, and an introduction to recursion.
C++ for Beginners: A Complete Guide
An eight-part beginner-friendly journey through C++: from writing your first program and understanding variables, through control flow, functions, and arrays, to the heart of C++ — pointers, classes, inheritance, and polymorphism. Each article is self-contained and builds on the previous, giving you both a quick reference and a progressive path to mastery.
Table of Contents
- What is a function?
- Defining a function
- The void return type
- Declaration vs definition
- Multiple parameters and types
- Pass by value vs pass by reference
- Pass by value (the default)
- Pass by reference (&)
- When to use each
- Default parameters
- Function overloading
- Returning multiple values
- std::pair or std::tuple
- Output parameters (reference)
- Recursion
- Factorial
- Fibonacci
- Practical example: simple calculator
- Key takeaways
What is a function?
A function is a named, reusable block of code. Functions let you:
- Avoid repeating the same logic
- Give meaningful names to complex operations
- Test units of code in isolation
- Split large programs into manageable pieces
Defining a function
return_type function_name(parameter_list) {
// body
return value;
}
A concrete example — a function that adds two integers:
int add(int a, int b) {
return a + b;
}
Call it like this:
int result = add(3, 4); // result = 7
std::cout << add(10, 20); // prints 30
The void return type
If a function doesn't return anything, use void:
void printGreeting(std::string name) {
std::cout << "Hello, " << name << "!
";
}
printGreeting("Alice"); // Hello, Alice!
A void function can have an early return; (no value) to exit early:
void printPositive(int n) {
if (n <= 0) return; // bail out early
std::cout << n << " is positive
";
}
Declaration vs definition
The function body must come before any code that calls it — unless you provide a forward declaration (prototype):
// Forward declaration — just the signature, ends with ;
int multiply(int a, int b);
int main() {
std::cout << multiply(6, 7); // OK because of the declaration above
}
// Definition — the actual body, anywhere in the file
int multiply(int a, int b) {
return a * b;
}
In large projects, declarations go in header files (.h) and definitions in source files (.cpp).
Multiple parameters and types
Functions can have any number of parameters of any types:
double hypotenuse(double a, double b) {
return std::sqrt(a*a + b*b);
}
std::string repeat(std::string s, int times) {
std::string result = "";
for (int i = 0; i < times; i++) result += s;
return result;
}
Pass by value vs pass by reference
Pass by value (the default)
The function receives a copy of the argument. Changes inside the function do not affect the original:
void doubleIt(int n) {
n *= 2; // only the local copy changes
}
int x = 5;
doubleIt(x);
std::cout << x; // still 5
Pass by reference (&)
Add & to receive the original variable, not a copy:
void doubleIt(int& n) {
n *= 2; // modifies the caller's variable
}
int x = 5;
doubleIt(x);
std::cout << x; // 10
Reference parameters are also useful for large objects (avoid copying):
// Passing a string by const reference — no copy, no modification
void print(const std::string& s) {
std::cout << s << "
";
}
The const& pattern is the most common way to pass large objects efficiently in C++.
When to use each
| Pass by value | Pass by reference | Pass by const ref | |
|---|---|---|---|
| Purpose | Small/primitive types | Need to modify caller | Large object, read-only |
| Copies data | Yes | No | No |
| Can modify original | No | Yes | No |
Default parameters
You can give parameters default values, which are used when the caller omits them:
void printBorder(char symbol = '*', int width = 20) {
for (int i = 0; i < width; i++) std::cout << symbol;
std::cout << "
";
}
printBorder(); // ********************
printBorder('-'); // --------------------
printBorder('=', 10); // ==========
Rules:
- Default parameters must be at the end of the parameter list.
- Usually put defaults in the declaration (header), not the definition.
Function overloading
C++ allows multiple functions with the same name but different parameter lists:
int square(int n) { return n * n; }
double square(double n) { return n * n; }
float square(float n) { return n * n; }
The compiler picks the right version based on the argument types:
square(5); // calls square(int)
square(2.5); // calls square(double)
square(1.5f); // calls square(float)
This is called compile-time polymorphism. Overloaded functions must differ in parameter types or count — differing only in return type is not allowed.
Returning multiple values
C++ functions return one value, but you can return multiple via:
std::pair or std::tuple
#include <utility> // for std::pair
std::pair<int, int> minmax(int a, int b) {
if (a < b) return {a, b};
return {b, a};
}
auto [lo, hi] = minmax(7, 3);
std::cout << lo << " " << hi; // 3 7
Output parameters (reference)
void divide(int dividend, int divisor, int& quotient, int& remainder) {
quotient = dividend / divisor;
remainder = dividend % divisor;
}
int q, r;
divide(17, 5, q, r);
std::cout << "17 / 5 = " << q << " remainder " << r;
// 17 / 5 = 3 remainder 2
Recursion
A recursive function calls itself. Every recursion needs:
- A base case — when to stop
- A recursive case — a call that moves toward the base case
Factorial
int factorial(int n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1); // recursive case
}
std::cout << factorial(5); // 5 × 4 × 3 × 2 × 1 = 120
Trace of factorial(4):
factorial(4) = 4 × factorial(3)
= 4 × 3 × factorial(2)
= 4 × 3 × 2 × factorial(1)
= 4 × 3 × 2 × 1
= 24
Fibonacci
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
for (int i = 0; i < 10; i++) {
std::cout << fibonacci(i) << " ";
}
// 0 1 1 2 3 5 8 13 21 34
Note: This naive implementation is exponential. For large
n, use a loop or memoization. Recursion is elegant for understanding but not always the most efficient approach.
Practical example: simple calculator
#include <iostream>
double add(double a, double b) { return a + b; }
double subtract(double a, double b) { return a - b; }
double multiply(double a, double b) { return a * b; }
double divide(double a, double b) {
if (b == 0) {
std::cout << "Error: division by zero
";
return 0;
}
return a / b;
}
int main() {
double x, y;
char op;
std::cout << "Enter: number operator number (e.g. 10 + 5): ";
std::cin >> x >> op >> y;
double result = 0;
switch (op) {
case '+': result = add(x, y); break;
case '-': result = subtract(x, y); break;
case '*': result = multiply(x, y); break;
case '/': result = divide(x, y); break;
default: std::cout << "Unknown operator
"; return 1;
}
std::cout << x << " " << op << " " << y << " = " << result << "
";
}
Key takeaways
- Functions group related code under a meaningful name.
voidfor functions that don't return a value;returnexits a function.- Pass by reference (
&) to modify the caller's variable or avoid copying large objects. - Use
const&for large read-only parameters. - Overloading lets you use the same name for operations on different types.
- Every recursive function needs a base case and a path toward it.
Next up: arrays, strings, and the C++ Standard Library — the containers you'll use every day.