Command Palette

Search for a command to run...

MTH 4300

Lecture 5

Functions

You've written functions before in Python. C++ functions work the same way — you give a block of code a name, declare what it takes in, and declare what it gives back. The main difference is that C++ requires you to be explicit about types everywhere.

Previously in Python
def add(a, b):
    return a + b
Now in C++
int add(int a, int b) {
    return a + b;
}

The return type (int) comes before the function name. Each parameter declares its type. If a function doesn't return anything, use void as the return type.

A complete example
#include <iostream>

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

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

int main() {
    int result = add(3, 4);
    std::cout << result << "\n"; // 7

    greet("world"); // Hello, world!

    return 0;
}

Functions must be declared before they are called. Either define the function above main, or use a forward declaration (just the signature, no body) above main and put the full definition below it.

const

Before we talk about how to pass arguments to functions, we need to understand const.

const marks a variable as read-only — once initialized, its value cannot be changed. Attempting to reassign a const variable is a compile error.

const basics
const int MAX_SIZE = 100;

MAX_SIZE = 200; // compile error: cannot assign to a const variable

You'll encounter const most often in function parameters, where it tells both the compiler and the reader: "this function promises not to modify this value."

Call-by-value

When you pass an argument by value, C++ makes a copy of it. The function works on its own local copy — any changes inside the function have no effect on the original variable.

Pass by value
#include <iostream>

void double_it(int x) {
    x = x * 2; // modifies the local copy only
}

int main() {
    int n = 5;
    double_it(n);
    std::cout << n << "\n"; // still 5 — the original was never touched

    return 0;
}

When to use it: when the function doesn't need to modify the original, and the type is small (int, double, char, etc.). Copying a small value is cheap.

Passing large types like std::string or std::vector by value copies the entire contents — which can be expensive. For those, use a reference instead (see below).

Call-by-reference

When you pass an argument by reference, the function receives an alias to the original variable. There is no copy — the function operates directly on the caller's data.

Pass by reference
#include <iostream>

void double_it(int& x) {
    x = x * 2; // modifies the original
}

int main() {
    int n = 5;
    double_it(n);
    std::cout << n << "\n"; // 10

    return 0;
}

When to use it: when the function needs to modify the original variable.

Read-only references (const T&)

Sometimes you want to avoid an expensive copy, but the function has no reason to modify the value. Use const T& — you get the efficiency of a reference with the safety of a read-only guarantee.

const reference parameter
#include <iostream>
#include <string>

void print_greeting(const std::string& name) {
    std::cout << "Hello, " << name << "!\n";
    // name = "other"; // compile error — name is read-only
}

int main() {
    std::string s = "Alice";
    print_greeting(s);

    return 0;
}

When to use it: when the function only reads the argument and the type is large enough that copying matters (std::string, std::vector, structs). For small types like int and double, plain pass-by-value is fine — const int& buys nothing there.

const T& is the most common parameter style in real C++ code. When you're not modifying an argument and the type is non-trivial, default to const T&.

Call-by-pointer

You can also pass a pointer to a variable. The function receives the address and can modify the original by dereferencing it — the same effect as a mutable reference, but spelled differently.

Pass by pointer
#include <iostream>

void double_it(int* ptr) {
    *ptr = *ptr * 2;
}

int main() {
    int n = 5;
    double_it(&n); // pass the address of n
    std::cout << n << "\n"; // 10

    return 0;
}

Notice that the call site must explicitly pass &n — the & makes it visible to the reader that the function could modify n. With a reference, that visibility is hidden in the function signature.

The other key difference: a pointer can be nullptr. A function taking a pointer should guard against it:

nullptr guard
void double_it(int* ptr) {
    if (ptr == nullptr) return;
    *ptr = *ptr * 2;
}

When to use it: when the argument might legitimately be absent (nullptr is a valid state), or when interfacing with C-style APIs that use pointers. In modern C++, prefer references when you know the argument always exists — they can't be null and the syntax is cleaner.

Summary

StyleSyntaxModifies original?Can be null?Use when
By valuevoid f(int x)NoNoSmall types, read-only
By const refvoid f(const int& x)NoNoLarge types, read-only
By refvoid f(int& x)YesNoNeed to mutate the original
By pointervoid f(int* x)Yes (via *)YesNullability needed, or C APIs

Exercises

Q1.

Without running the code, predict what this program prints. Then verify.

#include <iostream>

void increment(int x) {
    x = x + 1;
}

void increment_ref(int& x) {
    x = x + 1;
}

int main() {
    int a = 10;
    int b = 10;

    increment(a);
    increment_ref(b);

    std::cout << a << "\n";
    std::cout << b << "\n";

    return 0;
}

Q2.

Each function below has a const& parameter. Without running the code, determine whether each call compiles. If it doesn't, explain why.

#include <iostream>
#include <string>

void print(const std::string& s) {
    std::cout << s << "\n";
}

void shout(const std::string& s) {
    s = s + "!!!"; // A: does this compile?
}

void append(std::string& s) {
    s = s + "!!!";
}

int main() {
    std::string msg = "hello";

    print(msg);          // B: does this compile?
    shout(msg);          // C: does this compile?
    append(msg);         // D: does this compile?

    const std::string greeting = "hi";
    append(greeting);    // E: does this compile?

    return 0;
}

Q3.

Without running the code, trace through this program and predict the final values of x and y.

#include <iostream>

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

int main() {
    int x = 3;
    int y = 8;

    swap(x, y);

    std::cout << "x=" << x << " y=" << y << "\n";

    swap(x, x);

    std::cout << "x=" << x << "\n";

    return 0;
}

Q4.

Without running the code, predict what this program prints.

#include <iostream>

void zero_out(int* ptr) {
    if (ptr == nullptr) {
        std::cout << "null pointer\n";
        return;
    }
    *ptr = 0;
}

int main() {
    int a = 42;
    int* p = &a;

    zero_out(p);
    std::cout << a << "\n"; // line A

    zero_out(nullptr);      // line B

    return 0;
}

Q5.

Write three versions of a function called scale that multiplies a value by a given factor:

  1. scale_value(int x, int factor) — returns the scaled value (pass by value, no mutation)
  2. scale_ref(int& x, int factor) — modifies x in place (pass by reference)
  3. scale_ptr(int* x, int factor) — modifies *x in place (pass by pointer); do nothing if ptr is nullptr

All three should produce the same end result when called on the same input.

#include <iostream>

int scale_value(int x, int factor) {
    // TODO
}

void scale_ref(int& x, int factor) {
    // TODO
}

void scale_ptr(int* x, int factor) {
    // TODO
}

int main() {
    int a = 5;
    std::cout << scale_value(a, 3) << "\n"; // 15, a is still 5

    int b = 5;
    scale_ref(b, 3);
    std::cout << b << "\n"; // 15

    int c = 5;
    scale_ptr(&c, 3);
    std::cout << c << "\n"; // 15

    scale_ptr(nullptr, 3); // should do nothing, not crash

    return 0;
}

Practice Problems