Skip to main content
PProfileMatch

C++ Functions: Parameters, Return Values and Overloading

ProgrammingLesson 4 of 65 min read

What you'll learn

  • Write functions with parameters and return values, and declare them before use
  • Choose between pass by value, by reference and by const reference
  • Use default arguments and function overloading
  • Write a simple recursive function with a base case

On this page

  1. Declaring and defining functions
  2. Pass by value vs pass by reference
  3. Overloading and default arguments
  4. Common mistakes
  5. How this shows up in interviews and real projects

A function packages a piece of logic under a name so you can call it from many places and fix bugs in one spot. Functions are also where C++ differs most from other languages, because you decide whether an argument is copied or shared. Understanding that choice is the single most important idea in this lesson.

Declaring and defining functions

A function has a return type, a name, a parameter list and a body: int add(int a, int b) { return a + b; }. If it returns nothing, the return type is void. C++ reads top to bottom, so a function must be declared before it is used. Either define it above main, or put a prototype such as int add(int a, int b); at the top and the full definition later. A prototype with no matching definition causes a linker error: undefined reference to 'add(int, int)'.

basics.cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;

int add(int a, int b) {
    return a + b;
}

double average(const vector<int>& values) {
    if (values.empty()) {
        return 0;
    }
    int sum = 0;
    for (int v : values) {
        sum += v;
    }
    return static_cast<double>(sum) / values.size();
}

bool isEven(int n) {
    return n % 2 == 0;
}

void printReceipt(const string& item, double price) {
    cout << item << ": Rs. " << price << endl;
}

int main() {
    cout << add(4, 5) << endl;
    cout << average({70, 80, 90}) << endl;
    cout << boolalpha << isEven(10) << endl;
    printReceipt("Notebook", 120.5);
    return 0;
}

Output

9
80
true
Notebook: Rs. 120.5

Four shapes appear here: add and average compute and return a number, isEven returns a bool, and printReceipt only prints, so it is void. The early return 0; in average avoids dividing by zero on an empty vector. Do not worry about the & symbols yet; they are the next section.

Pass by value vs pass by reference

By default C++ passes arguments by value: the function receives its own copy, so changing the parameter does not affect the caller's variable. Adding & after the type makes the parameter a reference, which is another name for the caller's variable, so changes are visible outside. A third option, const T&, passes a reference you promise not to modify. It avoids copying large objects like strings and vectors while keeping the caller's data safe.

StyleSyntaxCopies the argument?Can modify caller's variable?Use for
By valuevoid f(int x)YesNoSmall types like int, double, char, bool
By referencevoid f(int& x)NoYesOutput parameters and in-place changes
By const referencevoid f(const string& s)NoNo (compiler enforces)Read-only access to large objects
references.cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;

void addBonusByValue(int salary) {
    salary += 5000;
    cout << "Inside by value: " << salary << endl;
}

void addBonusByRef(int& salary) {
    salary += 5000;
}

void addItem(vector<string>& cart) {
    cart.push_back("Headphones");
}

void swapValues(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int salary = 40000;
    addBonusByValue(salary);
    cout << "After by value: " << salary << endl;
    addBonusByRef(salary);
    cout << "After by reference: " << salary << endl;

    vector<string> cart = {"Keyboard"};
    addItem(cart);
    cout << cart.size() << " items, last: " << cart.back() << endl;

    int a = 3, b = 9;
    swapValues(a, b);
    cout << a << " " << b << endl;
    return 0;
}

Output

Inside by value: 45000
After by value: 40000
After by reference: 45000
2 items, last: Headphones
9 3

Compare this with Java, where objects are always passed as a copy of a reference. In C++ you choose explicitly, and the compiler enforces it: passing a literal such as 5 to an int& parameter is rejected with error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int', because there is no variable to modify.

Overloading and default arguments

You can reuse one function name with different parameter lists. This is overloading, and the compiler picks the best match from the arguments at compile time. Changing only the return type is not enough. Default arguments let callers omit trailing parameters: the defaults are filled in, and they must be the last parameters in the list.

overloads.cpp
#include <iostream>
using namespace std;

double area(double side) {
    return side * side;
}

double area(double length, double width) {
    return length * width;
}

void describe(int n) {
    cout << "int " << n << endl;
}

void describe(double n) {
    cout << "double " << n << endl;
}

double shippingCost(double weightKg, double ratePerKg = 40.0, bool express = false) {
    double cost = weightKg * ratePerKg;
    if (express) {
        cost *= 1.5;
    }
    return cost;
}

long long factorial(int n) {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

int main() {
    cout << area(4) << endl;
    cout << area(4, 2.5) << endl;
    describe(7);
    describe(7.0);
    describe('a');
    cout << shippingCost(2) << endl;
    cout << shippingCost(2, 50) << endl;
    cout << shippingCost(2, 50, true) << endl;
    cout << factorial(10) << endl;
    return 0;
}

Output

16
10
int 7
double 7
int 97
80
100
150
3628800

describe('a') printed int 97: a char promotes to int more readily than it converts to double, so the int overload wins. shippingCost(2) uses both defaults, so the cost is 2 x 40 = 80, while the last call passes all three arguments, so 2 x 50 x 1.5 = 150. The factorial function is recursive: it calls itself with a smaller value until the base case n <= 1. Every recursive function needs a base case, otherwise it recurses until the stack overflows and the program crashes.

Common mistakes

  • Calling a function before declaring it. You get error: 'add' was not declared in this scope. Define it above main or add a prototype.
  • Missing return. A non-void function with a path that does not return is undefined behavior. GCC warns with warning: control reaches end of non-void function. Always read warnings.
  • Expecting a by-value parameter to change the caller's variable. The original stays the same, as addBonusByValue showed. Use &.
  • Wrong argument count or types. add(1) fails with error: no matching function for call to 'add(int)', followed by a list of candidates.
  • Ambiguous overloads. Calling describe(7L) with a long argument fails with error: call of overloaded 'describe(long int)' is ambiguous because both int and double are equally good conversions. Cast the argument.
  • Returning a reference to a local variable. int& f() { int x = 1; return x; } hands back something that no longer exists. Using it is undefined behavior; the compiler usually warns reference to local variable 'x' returned.

How this shows up in interviews and real projects

A very common C++ interview question is "what is the difference between pass by value, pass by reference and pass by pointer?", and the table above is the core of a good answer. Overloading versus overriding, default arguments, and recursion with a clear base case (factorial, Fibonacci, binary search) also come up often. In real code, the const-reference habit for read-only parameters shows up in nearly every well-written C++ codebase. Next, dig into what a reference really is in C++ Pointers and References Explained.

Try it yourself

Write a function applyDiscount(double& price, double percent = 10) that reduces the price in place. Add an overload applyDiscount(vector<double>& prices, double percent) that discounts every price in a vector. Call both from main and print the results.

Show a hint

The vector overload can loop with for (double& p : prices) and call the single-price version for each element.

Key takeaways

  • Declare or prototype a function before you call it; a missing definition shows up as a linker error.
  • By value copies, T& shares and can modify, and const T& shares without modifying and without copying.
  • Overloads are chosen at compile time by argument types; default arguments must be the trailing parameters.
  • Recursive functions need a base case, and every non-void path must return a value.

Test yourself

A trick from this lesson's ride: No & means a photocopy: the function scribbles on its copy and you see nothing. Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

What is the difference between pass by value and pass by reference in C++?

Pass by value gives the function its own copy, so changes do not affect the caller. Pass by reference (using &) gives the function access to the caller's actual variable, so changes are visible outside. A const reference gives read-only access without copying.

What is function overloading in C++?

Overloading means defining several functions with the same name but different parameter types or counts. The compiler selects the best match from the arguments at compile time. The return type alone cannot distinguish overloads.

Why do I get 'was not declared in this scope' when I call my function?

C++ compiles top to bottom, so a function must be declared before the line that calls it. Move the function above main, or add a prototype such as int add(int a, int b); near the top of the file and keep the definition below.