← C++, End to End

C++, End to End

Functions and Program Structure

A function has a declaration (the signature: name, parameter types, return type) and a definition (the body). They can be the same statement, or two separate ones, and that separation is the entire reason multi-file C++ programs are possible.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

The compiler only needs to know a function's signature to generate a call to it. It doesn't need the body until link time, when the linker has to actually find that body somewhere. That's the whole trick behind splitting a program across files.

Parameters and return values

Parameters are passed by value by default: the function gets its own copy, and changes inside the function don't touch the caller's variable. That's usually what you want for small types (int, double, bool) and usually wrong for anything larger, since it means an unnecessary copy. References (covered properly in a later chapter) fix that; for now, know that pass-by-value is the default and it's a real copy, not a nickname for the caller's variable.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

A function with no meaningful value to return uses void. A function declared to return something non-void must return something on every possible path, or the behavior of falling off the end is undefined (main is the one specific, documented exception to this rule).

Header files and the one-definition rule

A header file (.h or .hpp) exists to be #included by multiple .cpp files, and by convention it holds declarations, not definitions. Split a small program into three files to see why this matters:

c++
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int multiply(int a, int b);

#endif
c++
// math_utils.cpp
#include "math_utils.h"

int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
c++
// main.cpp
#include "math_utils.h"
#include <iostream>

int main() {
    std::cout << add(2, 3) << "\n";
    std::cout << multiply(2, 3) << "\n";
}

main.cpp never sees the definitions of add or multiply, only their declarations through the header. The compiler compiles main.cpp and math_utils.cpp completely independently into two object files; the linker is the only stage that needs both bodies to exist somewhere.

The one-definition rule (ODR) is the reason headers hold declarations and not definitions: a non-inline function or a non-template variable can have exactly one definition across the entire program. Put a function body in a header, include that header from two .cpp files, and you get two definitions of the same function at link time: a redefinition error. Declarations are exempt from this because they carry no code, just a promise.

Include guards and #pragma once

The same header can get #included more than once into a single translation unit, directly or indirectly (file A includes B and C, and both B and C include D). Without protection, the preprocessor pastes D's contents in twice, and the compiler sees the same declarations twice, which for some constructs (class definitions, for instance) is a hard error.

c++
#ifndef MATH_UTILS_H   // "if not already defined"
#define MATH_UTILS_H   // define it, so next time this is skipped

// ... header contents ...

#endif

The alternative is a single line at the top of the file:

c++
#pragma once

// ... header contents ...

#pragma once is shorter, less error-prone (no risk of a copy-pasted guard macro name colliding with another header's), and supported by every compiler you're likely to use. The classic #ifndef guard is still worth recognizing since it shows up constantly in existing code and in anything targeting an unusual or very old toolchain, but reach for #pragma once in anything new.

Multiple translation units, briefly

main.cpp#includes math_utils.h
+
math_utils.cpp#includes math_utils.h too
two .o filescompiled independently
linkerresolves add()/multiply() calls to their real bodies

This is also exactly why a linker error like "undefined reference to add(int, int)" means the header's declaration was found and believed, but no .o file anywhere in the final link step actually defined it (a common cause: you forgot to add the .cpp file to the build, or renamed it and the build system doesn't know).

Linkage, in brief

Every name at file scope (a function, a global variable) has linkage, which controls whether the same name in a different translation unit refers to the same entity. Functions and non-const globals have external linkage by default: the same name in two files really does refer to one shared thing, which is exactly what lets main.cpp call a function defined in math_utils.cpp. The full picture, including internal linkage and static, belongs to its own chapter later in this course since it interacts with scope and storage duration; for now, the practical rule is: declare in a header, define in exactly one .cpp file, and the linker does the rest.

Try it yourself: take the three-file example above, delete the definition of multiply from math_utils.cpp but leave its declaration in the header, and try to build. Read the linker error carefully. That exact error is one of the most common things you'll hit in real multi-file C++, and it's worth seeing it once on purpose.