Functions — Pass by Value, Reference, Overloading, Default Arguments
In this tutorial, you will learn about Functions. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ functions are reusable code blocks that support parameter passing by value, reference, or address, function overloading, default arguments, and modern return type deduction with auto and trailing return types.
What You'll Learn
You will declare and define functions with various parameter passing modes, understand when to use pass-by-value versus pass-by-reference versus pass-by-address, use function overloading to create multiple functions with the same name, set default argument values, use auto return type deduction, and avoid common pitfalls like returning references to locals.
Why It Matters
Functions are the primary mechanism for code reuse and abstraction in every programming language. C++'s parameter passing options give you precise control over performance and semantics: pass-by-value copies data, pass-by-reference avoids copies and allows modification, pass-by-address works with C APIs and nullable parameters. Understanding these choices is essential for writing correct and efficient C++.
Learning Path
graph LR
A["09: Arrays & C-Strings"] --> B["10: Functions"]
B --> C["11: Classes & Objects"]
C --> D["12: Constructors"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Function Basics
#include <iostream>
// Declaration (prototype) — tells compiler about the function
int add(int a, int b);
// Definition — provides the implementation
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 5);
std::cout << result << "\n"; // 8
return 0;
}
A function declaration consists of the return type, name, and parameter types. The definition adds the body. Declarations must appear before the function is called.
Pass by Value
#include <iostream>
void increment(int x) {
++x;
std::cout << "Inside: " << x << "\n";
}
int main() {
int value = 10;
increment(value);
std::cout << "Outside: " << value << "\n";
// Output:
// Inside: 11
// Outside: 10
}
Pass-by-value copies the argument into the parameter. The function works on the copy, so the original is unchanged. Use this for small types (int, char, bool, pointers) when you do not need to modify the original.
Pass by Reference
#include <iostream>
void increment(int& x) {
++x;
std::cout << "Inside: " << x << "\n";
}
void print(const std::string& text) {
std::cout << text << "\n"; // read-only access
}
int main() {
int value = 10;
increment(value);
std::cout << "Outside: " << value << "\n";
// Output:
// Inside: 11
// Outside: 11
std::string msg = "Hello";
print(msg); // no copy, msg unchanged
}
Pass-by-reference avoids copying and allows the function to modify the original. Use const reference (const T&) for read-only access to large objects to avoid copying without allowing modification.
Pass by Address (Pointer)
#include <iostream>
void increment(int* x) {
if (x) {
++(*x);
}
}
void processArray(int* arr, size_t size) {
for (size_t i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
}
int main() {
int value = 10;
increment(&value);
std::cout << value << "\n"; // 11
increment(nullptr); // safe null check
int numbers[] = {1, 2, 3};
processArray(numbers, 3);
}
Pass-by-address passes a pointer to the object. The pointer can be null, which allows representing "no object." This is the C-style way of passing arrays. In modern C++, prefer references over pointers when the parameter must be valid.
Return by Value, Reference, and Address
#include <iostream>
#include <string>
// Return by value
int square(int x) {
return x * x;
}
// Return by reference (must refer to something that outlives the function)
int& getElement(int arr[], int index) {
return arr[index];
}
// Return by address
int* findValue(int arr[], int size, int target) {
for (int i = 0; i < size; ++i) {
if (arr[i] == target) return &arr[i];
}
return nullptr;
}
int main() {
int data[] = {10, 20, 30, 40, 50};
getElement(data, 1) = 25; // modifies data[1]
std::cout << data[1] << "\n"; // 25
int* found = findValue(data, 5, 40);
if (found) std::cout << *found << "\n"; // 40
}
Never return a reference or pointer to a local variable. The local variable is destroyed when the function returns, leaving a dangling reference.
Function Overloading
Multiple functions can share the same name if they have different parameter lists (different types, different number of parameters, or both).
#include <iostream>
#include <string>
int max(int a, int b) {
return (a > b) ? a : b;
}
double max(double a, double b) {
return (a > b) ? a : b;
}
std::string max(const std::string& a, const std::string& b) {
return (a > b) ? a : b;
}
int max(int a, int b, int c) {
return max(max(a, b), c);
}
int main() {
std::cout << max(3, 7) << "\n"; // calls int version
std::cout << max(3.14, 2.72) << "\n"; // calls double version
std::cout << max("cat", "dog") << "\n"; // calls string version
std::cout << max(1, 5, 3) << "\n"; // calls three-arg version
}
Overloading is resolved at compile time based on the argument types. The compiler looks for the best match, considering implicit conversions.
Default Arguments
#include <iostream>
void greet(const std::string& name, const std::string& greeting = "Hello") {
std::cout << greeting << ", " << name << "!\n";
}
int divide(int numerator, int denominator = 1) {
return numerator / denominator;
}
int main() {
greet("Alice"); // Hello, Alice!
greet("Bob", "Hi"); // Hi, Bob!
greet("Charlie", "Good day"); // Good day, Charlie!
std::cout << divide(10) << "\n"; // 10
std::cout << divide(10, 3) << "\n"; // 3
}
Default arguments must appear after all non-default parameters. They are specified in the declaration (usually in the header), not in the definition.
Trailing Return Type and auto Deduction (C++11/14)
#include <iostream>
#include <vector>
// Trailing return type (C++11)
auto multiply(int a, int b) -> int {
return a * b;
}
// Auto return type deduction (C++14)
auto divide(double a, double b) {
return a / b; // deduces double
}
// Decltype for complex cases
template <typename T, typename U>
auto add(const T& a, const U& b) -> decltype(a + b) {
return a + b;
}
int main() {
std::cout << multiply(3, 4) << "\n";
std::cout << divide(10.0, 3.0) << "\n";
std::cout << add(10, 3.14) << "\n"; // deduces double
}
Use trailing return types when the return type depends on template parameters. Use plain auto when the return type is obvious from the body.
Common Mistakes
Mistake 1: Returning Reference to Local Variable
int& getValue() {
int x = 5;
return x; // x is destroyed, dangling reference
}
Return by value or pass the result back through an output parameter.
Mistake 2: Ambiguous Overload
void print(int x) {}
void print(double x) {}
print(5L); // ambiguous: long matches neither perfectly
Be careful with types that could implicitly convert to multiple overloads.
Mistake 3: Default Arguments in Both Declaration and Definition
// header.h
void foo(int x = 10);
// foo.cpp
void foo(int x = 10) { ... } // Error: redefinition of default argument
Put default arguments only in the declaration.
Mistake 4: Forgetting to Make Getters const
int getValue() { return value_; } // cannot call on const object
Mark member functions that do not modify the object as const.
Mistake 5: Pass-by-Value for Large Objects
void process(std::string s) { ... } // copies the whole string
Use const std::string& to avoid copying.
Mistake 6: Confusing Pass-by-Reference with Pass-by-Address Syntax
void f(int& r) { } // reference
void g(int* p) { } // pointer
f(&x); // Error: cannot take address of reference parameter
g(&x); // OK: passing address
Practice Questions
- Write an overloaded
printfunction that works withint,double,std::string, andstd::vector<int>. - What is the difference between pass-by-reference-to-const and pass-by-value?
- Write a function with default arguments that draws a rectangle (width, height, character).
- Why can you not return a reference to a local variable?
- When would you use a trailing return type (
-> decltype(...)) instead ofauto?
Challenge
Write a function template apply that takes a vector and a function (as a function pointer, lambda, or functor) and applies the function to each element, modifying the vector in place. Use pass-by-reference for the vector.
FAQ
Mini Project
Write a simple calculator library with overloaded functions:
#include <iostream>
#include <string>
#include <cmath>
int calculate(int a, int b, char op) {
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return (b != 0) ? a / b : 0;
case '%': return (b != 0) ? a % b : 0;
default: return 0;
}
}
double calculate(double a, double b, char op) {
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return (b != 0.0) ? a / b : 0.0;
default: return 0.0;
}
}
int main() {
std::cout << calculate(10, 3, '+') << "\n";
std::cout << calculate(10, 3, '/') << "\n";
std::cout << calculate(10.5, 3.2, '+') << "\n";
std::cout << calculate(10.5, 3.2, '/') << "\n";
}
Expected output:
13
3
13.7
3.28125
What's Next
Functions are the building blocks of program logic. The next lesson begins Module 2 on Object-Oriented Programming: you will learn about classes, objects, access specifiers, and the this pointer.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro