FirstHack Learn
Log in Sign up free

Functions and References

9 min read · 17 views

Functions and References

A function is a named block of code that takes inputs, does one job, and usually hands back a result. Functions exist so you can name an idea once and use it many times, and so that a large program becomes a small program calling other small programs.

The rule worth internalising early: if you have copied and pasted code, you wanted a function.

Declaring and defining

C++
#include <iostream>

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

double average(int total, int count) {
    if (count == 0) {
        return 0.0;
    }
    return (double)total / count;
}

void printLine(char symbol, int width) {
    for (int i = 0; i < width; i++) {
        std::cout << symbol;
    }
    std::cout << "\n";
}

int main() {
    printLine('=', 30);
    std::cout << "3 + 4 = " << add(3, 4) << "\n";
    std::cout << "Average of 250 over 4 = " << average(250, 4) << "\n";
    printLine('-', 30);
    return 0;
}

The parts of int add(int a, int b) are the return type, the name, and the parameter list. void as a return type means the function hands nothing back; printLine prints and that is its whole purpose.

C++ reads a file top to bottom, so a function must be known before it is called. Defining it above main, as here, is enough. For larger programs you write a declaration (also called a prototype) near the top and the definition below:

Writing int add(int a, int b); with a semicolon and no body tells the compiler the signature. The definition can then live anywhere in the file, or in another file entirely.

Pass by value: the default

By default, a parameter receives a copy of the argument. The function can change its copy all it likes; the caller's variable is untouched.

C++
#include <iostream>

void tryToDouble(int x) {
    x = x * 2;
    std::cout << "Inside the function, x = " << x << "\n";
}

int doubled(int x) {
    return x * 2;
}

int main() {
    int value = 10;

    tryToDouble(value);
    std::cout << "After tryToDouble, value = " << value << "\n";

    value = doubled(value);
    std::cout << "After assignment, value = " << value << "\n";

    return 0;
}

value is still 10 after tryToDouble. The function got a copy, named it x, doubled that copy, and the copy disappeared when the function returned. The pattern that works is the second one: return the new value and assign it.

Pass by reference

Sometimes a function genuinely must change the caller's variable. In C you would pass a pointer. C++ gives you a reference, written with & in the parameter list.

A reference is not a new object. It is another name for an existing one. Once bound, it refers to that variable for its entire life and cannot be made to refer to something else, and it can never be null.

C++
#include <iostream>
#include <string>

void doubleInPlace(int& x) {
    x = x * 2;
}

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

void describe(const std::string& text) {
    std::cout << "Text: " << text << " (length " << text.size() << ")\n";
}

int main() {
    int value = 10;
    doubleInPlace(value);
    std::cout << "value = " << value << "\n";

    int p = 3, q = 7;
    std::cout << "Before swap: " << p << " " << q << "\n";
    swapValues(p, q);
    std::cout << "After swap:  " << p << " " << q << "\n";

    describe("Operating Systems");
    return 0;
}

Notice the call site: swapValues(p, q) looks identical to a pass-by-value call. There is no & at the call, unlike C where you would write swap(&p, &q). This is convenient and it is also a real cost, because you cannot tell from the call alone whether the function will modify your variable. Read the signature when it matters.

That describe function shows the second reason to use references, which has nothing to do with modification. Passing a large object by value copies the whole thing. Passing by const reference avoids the copy while the const promises the function will not modify it. For any type bigger than a couple of numbers, const T& is the default choice for a read-only parameter.

💡Three ways to pass, one decision

Ask two questions. Does the function need to modify the caller's variable? If yes, use T&. If no, is the type large, like a string, vector or class? If yes, use const T&. Otherwise, for an int, double or char, plain T by value is simplest and fastest.

Default arguments

A parameter can carry a default value, used when the caller leaves it out.

C++
#include <iostream>
#include <string>

void greet(const std::string& name, const std::string& greeting = "Hello") {
    std::cout << greeting << ", " << name << "!\n";
}

double interest(double principal, double rate = 7.5, int years = 1) {
    return principal * rate * years / 100.0;
}

int main() {
    greet("Meera");
    greet("Arjun", "Good morning");

    std::cout << interest(10000) << "\n";
    std::cout << interest(10000, 9.0) << "\n";
    std::cout << interest(10000, 9.0, 3) << "\n";

    return 0;
}

Defaults must be the trailing parameters. You cannot supply the third argument while skipping the second, because arguments are matched by position, so interest(10000, , 3) is not valid C++.

Function overloading

C++ allows several functions to share a name as long as their parameter lists differ. The compiler picks the right one from the argument types at the call site.

C++
#include <iostream>
#include <string>

int maxOf(int a, int b) {
    return (a > b) ? a : b;
}

double maxOf(double a, double b) {
    return (a > b) ? a : b;
}

int maxOf(int a, int b, int c) {
    return maxOf(maxOf(a, b), c);
}

std::string describe(int n)    { return "an integer: " + std::to_string(n); }
std::string describe(double d) { return "a double: "  + std::to_string(d); }

int main() {
    std::cout << maxOf(3, 9) << "\n";
    std::cout << maxOf(2.5, 1.5) << "\n";
    std::cout << maxOf(4, 11, 7) << "\n";
    std::cout << describe(42) << "\n";
    std::cout << describe(3.5) << "\n";
    return 0;
}

This is why C has abs, fabs and labs while C++ needs only abs. Overloads must differ in the types or number of parameters. Two functions differing only in return type will not compile, because a call like maxOf(3, 9) gives the compiler no way to choose.

Common mistakes

  • Calling a function defined below main with no declaration above. The error says the name was not declared. Add a prototype at the top.
  • Expecting a by-value parameter to change the caller's variable. It cannot. Use T& or return the result.
  • Forgetting return on some path. A non-void function that runs off the end returns garbage and -Wall warns about it. Make sure every branch returns.
  • Returning a reference to a local. The local dies when the function ends, so the reference points at memory you no longer own. Return by value instead.
  • Putting default arguments in both the declaration and the definition. Give them once, normally in the declaration.
  • Trying to overload on return type alone. int f(); double f(); will not compile.

Once main becomes a short list of calls to well-named functions, you have crossed from writing code to designing programs. Rewrite an earlier loop exercise using two or three functions, then try the function problems on the Practice page.

Create a free account to track what you have finished.