Skip to content

Functions

Functions are the building blocks of modular programming. They allow you to break your code into smaller, reusable pieces. This chapter covers everything about functions in C++, from basic syntax to advanced features like function pointers and lambda expressions.

┌─────────────────────────────────────────────────────────────┐
│ Benefits of Functions │
├─────────────────────────────────────────────────────────────┤
│ ✓ Code Reuse → Write once, use many times │
│ ✓ Abstraction → Hide implementation details │
│ ✓ Maintainability → Easier to modify and fix │
│ ✓ Readability → Self-documenting code │
│ ✓ Testability → Test each piece independently │
│ ✓ Organization → Logical grouping of code │
└─────────────────────────────────────────────────────────────┘
// Declaration (prototype) - tells compiler about the function
int add(int a, int b);
// Definition - actual implementation
int add(int a, int b) {
return a + b;
}
┌─────────────────────────────────────────┐
│ return_type function_name(parameters) │
├─────────────────────────────────────────┤
│ return_type: What the function returns│
│ function_name: Name of the function │
│ parameters: Input values (arguments) │
│ body: The actual code │
└─────────────────────────────────────────┘
#include <iostream>
// Function that takes no parameters and returns nothing
void greet() {
std::cout << "Hello, World!" << std::endl;
}
// Function with parameters
int add(int a, int b) {
return a + b;
}
// Function with return value
double calculateArea(double radius) {
const double PI = 3.14159;
return PI * radius * radius;
}
int main() {
greet();
std::cout << "5 + 3 = " << add(5, 3) << std::endl;
std::cout << "Area of radius 5: " << calculateArea(5) << std::endl;
return 0;
}

A copy of the argument is made:

void increment(int x) {
x++; // Only modifies the local copy
}
int main() {
int num = 5;
increment(num);
std::cout << num; // Still 5 (original unchanged)
}

The function receives the actual variable:

void increment(int& x) {
x++; // Modifies the original variable
}
int main() {
int num = 5;
increment(num);
std::cout << num; // Now 6
}

When you need the efficiency of reference but don’t want to modify:

void printName(const std::string& name) {
// name = "Changed"; // Error: const
std::cout << name << std::endl;
}

When to use each:

  • By value: For built-in types, when you need a copy
  • By reference: When you need to modify the argument
  • By const reference: When you only need to read (efficient for large objects)
int square(int x) {
return x * x;
}
// The return value is copied (or moved)
int result = square(5); // result is 25
int& getElement(std::vector<int>& vec, int index) {
return vec[index]; // Returns reference to element
}
int main() {
std::vector<int> nums = {10, 20, 30};
getElement(nums, 1) = 25; // Modify through reference
std::cout << nums[1]; // 25
}

Warning: Don’t return a reference to a local variable!

// DANGEROUS - returns reference to destroyed object
int& badFunction() {
int x = 5;
return x; // x is destroyed when function ends!
}
// Using std::pair
std::pair<int, int> divide(int a, int b) {
return {a / b, a % b};
}
int main() {
auto [quotient, remainder] = divide(10, 3);
// quotient = 3, remainder = 1
}
// Using std::tuple (C++11)
std::tuple<int, int, int> getStats() {
return {1, 2, 3};
}
// Using struct
struct Result {
int quotient;
int remainder;
};
Result divide(int a, int b) {
return {a / b, a % b};
}

Functions can have default parameter values:

void greet(std::string name = "World") {
std::cout << "Hello, " << name << "!" << std::endl;
}
int main() {
greet(); // Hello, World!
greet("Alice"); // Hello, Alice!
}

Rules:

  • Default arguments must be at the end
  • Should be specified in declaration, not definition (or both)
// Declaration
void process(int a, int b = 10, int c = 20);
// Definition (defaults not needed)
void process(int a, int b, int c) {
// ...
}

Multiple functions can have the same name with different parameters:

// Overloaded functions
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
std::string add(const std::string& a, const std::string& b) {
return a + b;
}
int main() {
add(1, 2); // Calls int version
add(1.5, 2.5); // Calls double version
add("Hello", "World"); // Calls string version
}

Overload Resolution: The compiler chooses the best match based on:

  1. Exact match
  2. Promotion (e.g., int to long)
  3. Conversion (e.g., int to double)

The inline keyword suggests the compiler replace the function call with the function body:

inline int max(int a, int b) {
return (a > b) ? a : b;
}
int main() {
// Compiler may replace with: int result = (x > y) ? x : y;
int result = max(x, y);
}

When to use:

  • Small, simple functions
  • Frequently called functions
  • The compiler decides whether to actually inline

Lambdas create anonymous function objects:

// Basic lambda
auto lambda = [](int x) { return x * 2; };
int result = lambda(5); // 10
// Without storing
std::cout << [](int x) { return x * 2; }(5); // 10
[ captures ] ( parameters ) -> return_type { body }
int x = 10;
// Capture by value (copy)
auto f1 = [x]() { return x; };
// Capture by reference
auto f2 = [&x]() { x = 20; };
// Capture all by value
auto f3 = [=]() { return x; };
// Capture all by reference
auto f4 = [&]() { return x; };
// C++14: capture expression
auto f5 = [val = x * 2]() { return val; };
#include <algorithm>
#include <vector>
std::vector<int> nums = {5, 2, 8, 1, 9};
// Find first number > 5
auto it = std::find_if(nums.begin(), nums.end(),
[](int n) { return n > 5; });
// Sort with custom comparator
std::sort(nums.begin(), nums.end(),
[](int a, int b) { return a > b; });
// for_each with lambda
std::for_each(nums.begin(), nums.end(),
[](int n) { std::cout << n << " "; });

Pointers to functions:

int add(int a, int b) {
return a + b;
}
int (*funcPtr)(int, int) = add;
int result = funcPtr(5, 3); // 8

Using with std::function (C++11):

#include <functional>
std::function<int(int, int)> operation = add;

Functions that call themselves:

// Factorial
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// Fibonacci
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}

Tail Recursion:

// Tail-recursive factorial (compiler may optimize)
int factorialTail(int n, int accumulator = 1) {
if (n <= 1) return accumulator;
return factorialTail(n - 1, n * accumulator);
}
// Bad
void doIt(int x);
// Good
void calculateTotal(int quantity);

Each function should do one thing well.

// Member function that doesn't modify state
class Calculator {
public:
int add(int a, int b) const { // const member function
return a + b;
}
};
/**
* @brief Calculates the factorial of a non-negative integer
* @param n The number to calculate factorial for (must be >= 0)
* @return The factorial result, or 1 if n is 0
* @throws std::invalid_argument if n is negative
*/
int factorial(int n);
// Bad
int globalCount;
void increment() { globalCount++; }
// Good
int increment(int count) { return count + 1; }
#include <iostream>
#include <vector>
#include <string>
class Calculator {
private:
double memory = 0;
public:
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) {
throw std::invalid_argument("Cannot divide by zero");
}
return a / b;
}
void storeInMemory(double value) { memory = value; }
double recallMemory() { return memory; }
void clearMemory() { memory = 0; }
};
int main() {
Calculator calc;
std::cout << "5 + 3 = " << calc.add(5, 3) << std::endl;
std::cout << "10 - 4 = " << calc.subtract(10, 4) << std::endl;
std::cout << "6 * 7 = " << calc.multiply(6, 7) << std::endl;
std::cout << "20 / 4 = " << calc.divide(20, 4) << std::endl;
// Using memory
calc.storeInMemory(100);
std::cout << "Memory: " << calc.recallMemory() << std::endl;
return 0;
}
  • Functions are reusable blocks of code that perform specific tasks
  • Use pass-by-value for small types, pass-by-reference for modification
  • Use const reference for read-only large objects
  • Function overloading allows multiple functions with the same name
  • Lambda expressions create anonymous functions (very useful with STL)
  • Keep functions short and focused on a single task
  • Use meaningful names and document your functions
  • Prefer returning by value; use references when necessary

Now let’s dive into Object-Oriented Programming with classes and objects.

Next Chapter: 02_oop/08_classes_objects.md - Classes and Objects