Masters-level programming lessons for professional software judgement.

Masters Programmes · Masters Software Engineering · Lesson 5

The main Function in C++

Understand `main` as the executable entry point: where program control begins, arguments arrive, and exit status is returned to the operating system.

Lesson overview

main is special because the runtime calls it to start an executable. It is not an ordinary helper function, and a library should not define it unless that library is being built as an executable target.

LevelMasters
ModeSeminar, command-line lab and target-structure review
EvidenceA small command-line program that parses arguments, returns meaningful exit codes and separates executable code from library code
SourceConverted from supplied Canvas lesson HTML

Learning objectives

  • Identify valid portable main signatures.
  • Explain how argc and argv represent command-line arguments.
  • Return meaningful exit codes for success, usage errors and parse failures.
  • Refactor logic out of main into testable helper functions.
  • Distinguish executable targets from library targets.

Key vocabulary before you start

Entry point

The function where execution of a C++ executable begins.

Portable signature

A main form accepted by the C++ standard across conforming implementations.

Command-line argument

A string supplied to the program by the host environment.

Exit code

The integer status returned to the host process.

Executable boundary

The layer that turns user input into calls to testable library code.

Parse failure

A case where an input string cannot safely become the expected value.

Entry-point map

main is the boundary between the operating system, the C++ runtime and your program.

M1

One executable, one main

Only the executable target should define main.

M2

Parse arguments deliberately

argv[0] is the program name; user arguments begin at argv[1].

M3

Return status

Exit codes communicate success or failure to the caller.

M4

Keep main thin

Put testable behaviour in functions and let main orchestrate.

Example: valid main with arguments

The common command-line signature receives a count and an array of C strings.

#include <iostream>

int main(int argc, char* argv[]) {
    if (argc > 1) {
        std::cout << "First argument: " << argv[1] << '\n';
    }
    return 0;
}

Trace the state

  1. The runtime starts the executable and calls main.
  2. argc contains the number of argument strings.
  3. argv[0] names the program; argv[1] is the first user-supplied argument.
  4. Returning 0 reports success to the operating system.
Line or operationVariableValueState roleInvariant or check
Read the example before running itKey names and callsKnown from sourceReasoning setupEach named element should support the lesson focus: The main Function in C++.
Trace the first meaningful operationPrimary state or boundaryEstablished by initialisation, call or conditionValid-state checkpointLater reasoning must not use the value before this checkpoint.
Identify the first decision or dereferenceControl or access pointDepends on current stateFailure-mode checkpointThe condition, pointer, argument or dependency must be valid before use.
Record the observable resultReturn value, output or mutationProduced by the exampleEvidence checkpointThe result should match the contract explained in the lesson.

Visual model: executable boundary around library code

Keep main small enough that parsing, exit codes and library calls can each be reasoned about.

Start processThe host starts the executable and provides argc and argv.

Validate argumentsmain checks count and syntax before trusting user input.

Parse into domain valuesStrings become typed values or produce explicit parse errors.

Call library codeThe real computation lives in testable functions outside main.

Return statusmain reports success, usage error or parse failure with an exit code.

Boundary task

Mark which lines in your solution belong to the executable boundary and which should move into library functions.

Why main exists

Every executable needs a single place where control enters user code. In C++, that place is main.

The runtime performs startup work before main, and shutdown work after it returns. That is why main should normally return rather than forcing termination.

Do not call main yourself. If you need reusable program logic, put it in a run function or another helper and call that from main.

Thin main

Move testable behaviour out of the special entry point.

int run(int argc, char* argv[]) {
    // parse, compute, report
    return 0;
}

int main(int argc, char* argv[]) {
    return run(argc, argv);
}

Applied task

Explain why calling run is acceptable but recursively calling main is the wrong abstraction.

Valid signatures and arguments

Portable C++ uses int main() or int main(int argc, char* argv[]). The second form exposes command-line arguments.

argc is the number of argument strings. argv is an array of pointers to those strings. The program name is conventionally at argv[0].

Arguments containing spaces are handled by the shell before main is called. A quoted argument such as "New York" arrives as one argv element.

Argument loop

User arguments start at index 1.

for (int i = 1; i < argc; ++i) {
    std::cout << i << ": " << argv[i] << '\n';
}

Applied task

Given a command line with quoted arguments, list argc and each argv element.

Return values and exit codes

The integer returned by main is the program's status code. By convention, 0 means success and non-zero values describe failure.

Meaningful exit codes make scripts, tests and build systems easier to diagnose.

return from main allows normal local cleanup. std::exit is a sharper tool and changes destructor behaviour for local automatic objects.

Named exit codes

Names make failure modes easier to review.

enum class ExitCode : int {
    Ok = 0,
    BadUsage = 64,
    ParseError = 65
};

int main(int argc, char* argv[]) {
    if (argc < 2) return static_cast<int>(ExitCode::BadUsage);
    return static_cast<int>(ExitCode::Ok);
}

Applied task

Choose exit codes for success, missing input and malformed input.

Parse command-line input safely

Do not assume command-line strings are valid just because they exist. Convert them and handle failure.

std::from_chars is useful for numeric parsing because it does not allocate and reports exactly where parsing stopped.

Parsing belongs near the program boundary. Once parsed, pass typed values into ordinary functions.

Parse an integer

A parse function makes success and failure explicit.

#include <charconv>
#include <optional>
#include <string_view>

std::optional<int> parse_int(std::string_view text) {
    int value{};
    auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), value);
    if (ec != std::errc{} || ptr != text.data() + text.size()) return std::nullopt;
    return value;
}

Applied task

Parse three arguments as integers and report the first failure without crashing.

Executable vs library targets

Only the executable needs main. A library should expose declarations and definitions that executable code can call.

If a library defines main, it stops being reusable in other executables and causes link conflicts when combined with another main.

A clean layout separates lib.hpp, lib.cpp and main.cpp so the library can be tested independently.

Library and executable layout

The library provides sum; the executable owns main.

// lib.hpp
#pragma once
#include <vector>
int sum(const std::vector<int>& values);

// lib.cpp
#include "lib.hpp"
int sum(const std::vector<int>& values) {
    int total = 0;
    for (int v : values) total += v;
    return total;
}

// main.cpp
#include <iostream>
#include "lib.hpp"
int main(int argc, char* argv[]) {
    // parse argv[1..] to ints, call sum, print result
    return 0;
}

Applied task

Explain why a unit-test executable and a production executable can both link the same library, but each needs its own main.

Applied case lab

Case 1: The extra main

A build target includes two demonstration files, each with main. Diagnose the link error and propose a target split.

Case 2: The missing argument

A program reads argv[1] without checking argc. Explain the failure mode and write the guard.

Case 3: The untestable main

All logic lives inside main. Extract parsing, computation and output so core logic can be tested.

Applied task: mastering main

Practise valid signatures, argument indexing, safe parsing and executable/library separation.

Stage 1: read and classify

A. Validate signatures

Decide which forms are portable entry points.

int main();
int main(int argc, char* argv[]);
void main();
int main(int argc, char** argv);
  • Mark the portable signatures.
  • Explain why void main should not be used.
Reveal one possible refactor
int main() { return 0; }

int main(int argc, char* argv[]) {
    return argc > 0 ? 0 : 1;
}
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Stage 2: trace or repair

B. Fix an off-by-one argument loop

This loop wrongly treats the program name as user input.

for (int i = 0; i < argc; ++i) {
    parse(argv[i]);
}
  • Start at the first user argument.
  • Guard the no-argument case.
Reveal one possible refactor
if (argc < 2) return static_cast<int>(ExitCode::BadUsage);
for (int i = 1; i < argc; ++i) {
    parse(argv[i]);
}
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Stage 3: justify the design

C. App vs library

Finish the executable without putting main in the library.

// main.cpp
#include <iostream>
#include "lib.hpp"
int main(int argc, char* argv[]) {
    // TODO: parse argv[1..] to ints, call sum, print result
}
  • Explain why only the executable needs main.
  • Keep sum in the library and parsing in the executable boundary.
Reveal one possible refactor
int main(int argc, char* argv[]) {
    std::vector<int> values;
    for (int i = 1; i < argc; ++i) {
        auto parsed = parse_int(argv[i]);
        if (!parsed) return static_cast<int>(ExitCode::ParseError);
        values.push_back(*parsed);
    }
    std::cout << sum(values) << '\n';
    return static_cast<int>(ExitCode::Ok);
}
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Quick checks

1. Which argv index is normally the first user-supplied argument?

2. Why should a library not define main?

3. What does returning 0 from main conventionally mean?

AI-augmented practice notes

Use these as prompts for disciplined support, not as permission to outsource judgement.

Entry Point Coach
  • Ask AI to validate your main signature and portability.
  • Ask for an argument parser that converts argv to typed values with from_chars.
  • Ask AI to propose a small enum of exit codes and where to return them.
  • Ask for a comparison of return from main and std::exit for destructors and atexit handlers.

Assessment tasks

  1. Build a tiny command-line program that parses integer arguments and prints their sum.
  2. Document the chosen exit codes and give one example command for each.
  3. Split the program into lib.hpp, lib.cpp and main.cpp.

Judgement questions

What should stay in main, and what should move out?

Separate boundary work such as parsing and status reporting from core logic that should be testable.

How does your shell affect argv?

Explain how quoted arguments with spaces are delivered to main.