Course outline · 0% complete

0/27 lessons0%

Course overview →

Defining Functions and Prototypes

lesson 4-1 · ~10 min · 9/27

Scope, and why it is about to matter

Lesson 3-3 introduced the loop header for (int i = 0; i < n; i++), and the variable i declared there exists only inside the loop. It disappears when the loop ends, so a later line referring to i fails to compile.

Scope is the region of code where a name exists, and it is about to matter a great deal. Every function you write gets its own scope, and its parameters and local variables live only inside it. Two functions can both use a variable called count without any risk of interference, because those are two separate names in two separate scopes.

Naming a piece of logic

Functions exist so that logic is written once, given a name, tested once, and reused. Without them every repeated computation is a copy-paste waiting to drift out of sync, where one copy gets fixed and the others quietly keep the bug.

In interviews they are also how you show structure to the person watching you code. C++ adds something Python does not have: the compiler enforces a typed contract on every function boundary.

A function is typed on both ends

Python:

def square(x):
    return x * x

C++:

int square(int x) {
    return x * x;
}

The first int is the return type, meaning the type of the value the function gives back. Each parameter also declares its own type. A function that returns nothing uses the return type void.

void greet(std::string name) {
    std::cout << "Hi, " << name << "\n";
}

The compiler checks every call against this signature. square("hi") fails to compile, and so does using square(3) in a place where a string is required. Your function contracts are enforced exactly the way variable types were in lesson 1-3, which means a mismatched call is a build failure rather than a runtime surprise.

Declare before use: prototypes

The compiler reads your file top to bottom. If main calls square before the compiler has seen square, the build fails. Two fixes:

  1. Define functions above main (fine for small programs).
  2. Put a prototype (the signature followed by a semicolon) at the top, and define the body anywhere:
int square(int x);   // prototype: promises this function exists

int main() {
    std::cout << square(6) << "\n";
    return 0;
}

int square(int x) {  // definition, after main is fine now
    return x * x;
}

Headers like <iostream> are essentially big collections of prototypes, which is why #include makes std::cout usable.

Two functions and a prototype

A program with a prototype at the top, which is what lets main call cube even though the body appears further down the file.

#include <iostream>

int cube(int x);   // prototype

int main() {
    for (int i = 1; i <= 4; i++) {
        std::cout << cube(i) << "\n";
    }
    return 0;
}

int cube(int x) {
    return x * x * x;
}

Output

1
8
27
64

The prototype and the definition have to agree. If the prototype said int cube(double x) while the definition took an int, the compiler would accept both, treat them as two different functions, and the build would fail at the link step with a message about an undefined symbol.

Notice that the parameter is called x in both places, and that the names are not part of the contract at all. Only the types matter, so a prototype is often written without names, as int cube(int);, which is the form you will see in header files.

A predicate function returning bool

A function whose whole job is to answer a yes-or-no question. The comparison from lesson 3-1 becomes the return value.

#include <iostream>

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

int main() {
    int n;
    std::cin >> n;
    if (is_even(n)) {
        std::cout << "even\n";
    } else {
        std::cout << "odd\n";
    }
    return 0;
}

Input

7

Output

odd

The body is a single line, return n % 2 == 0;, because the comparison already produces a bool and there is nothing left to convert. Writing it as if (n % 2 == 0) { return true; } else { return false; } gives the same answer with four extra lines, and experienced reviewers will always ask you to collapse it.

The call site reads as English, since if (is_even(n)) needs no == true attached to it. That readability is the main argument for pulling a condition out into a named predicate even when it is used only once.