C++ Programming
Lesson 05 of 24

Lesson 05 · 24 lesson course

5. The Main Function

Telling the compiler where your program starts from

Programming glossary 46 terms mentioned in this lesson

Select any term for a clear definition. The return button brings you back to the exact term link you used.

Abstract origin node starting and illuminating a larger program network.

 

Lesson: The main Function in C++

When creating a C++ program, you often need to tell the compiler where execution begins. If your code is meant to be integrated into another library or program, you don’t need this. But if you are producing an executable that the operating system will run, you must provide an entry point. This is done by writing a function named main.


1) Why main?

The operating system looks for a single main function as the starting point of your program. It acts as the handshake between your program and the system.

  • There can only be one main function in a program.
  • main can take parameters (for command-line arguments).
  • You cannot call main explicitly in your program.
  • You should not use or take the address of main.
  • Other advanced aspects exist, but we focus on the basics here.
// Simplest valid C++ program
int main() {
    return 0;  // exit successfully
}
Programming Insight (AI) — Simplifying Entry Points
  • Ask AI to check whether your project needs a main (application) or not (library).
  • AI can generate a minimal main scaffold for testing, then scale it up as your project grows.

2) main with Parameters

Often, you want your program to accept input from the command line. This is handled by parameters passed into main:

int main(int argc, char* argv[]) {
    // argc = argument count
    // argv = argument vector (array of C-style strings)
    return 0;
}

Here:

  • argc is the number of arguments (at least 1: the program name).
  • argv points to the arguments themselves (as C-strings).

For example, running ./program hello world gives:

argc = 3
argv[0] = "./program"
argv[1] = "hello"
argv[2] = "world"
Programming Insight (AI) — Handling Arguments
  • AI can generate safe loops to print or parse argv contents.
  • Ask AI to convert argv into modern std::vector<std::string> for easier handling.
  • It can also scaffold command-line parsers (flags, options) to avoid manual parsing.

3) Return Values from main

The main function returns an int. Returning 0 means success. Returning a nonzero value signals an error or abnormal termination. This convention allows the operating system, scripts, and other programs to know whether your program ran correctly.

int main() {
    if (/* something went wrong */) {
        return 1; // nonzero = error
    }
    return 0;     // success
}
Programming Insight (AI) — Error Codes & Conventions
  • Ask AI: “What return codes are standard for my OS/toolchain?”
  • AI can map error codes to meaningful exit statuses (e.g., POSIX conventions).
  • Generate boilerplate that logs and returns consistent exit codes across functions.

4) Worked Example — Printing Arguments

#include <iostream>
using namespace std;

int main(int argc, char* argv[]) {
    cout << "You passed " << argc-1 << " arguments:" << endl;
    for (int i = 1; i < argc; ++i) {
        cout << "  arg[" << i << "] = " << argv[i] << endl;
    }
    return 0;
}

If you run ./program apple banana, the output will be:

You passed 2 arguments:
  arg[1] = apple
  arg[2] = banana
Programming Insight (AI) — Safer Parsing
  • Have AI transform this into modern C++ with std::vector<std::string>.
  • Ask AI to integrate a command-line parsing library (e.g., cxxopts or Boost.Program_options).
  • Generate unit tests where AI simulates command-line inputs to check parsing logic.

5) Summary

  • main is the entry point of every executable program in C++.
  • It may or may not take arguments, depending on your needs.
  • Return 0 for success, nonzero for errors.
  • Arguments let you interact with the program via the command line.
  • Only one main is allowed in a program.

Mini Exercise — Mastering main: entry, args, and exits

Goal: practise correct main signatures, safe argument handling, meaningful exit codes, and avoiding anti-patterns (calling/taking the address of main).

  1. Pick the valid signature
    Which of these are valid and portable forms of main? Explain why for each.
    int main();
    int main(int argc, char* argv[]);
    int main(int argc, char** argv);
    int main(char** argv, int argc);          // ?
    int main(const int argc, const char**);   // ?
    auto main() -> int;                       // (C++ trailing return)
    
    • Choose one signature you’ll use in this course and justify (readability + portability).
  2. Don’t call main
    Identify the problem and refactor so the program has a testable entry function you call from main.
    #include <iostream>
    int main(int argc, char* argv[]) {
        std::cout << "Start\n";
        if (argc < 2) { std::cout << "Retry\n"; return main(argc+1, argv); } // ❌
        std::cout << "Done\n";
        return 0;
    }
    
    • Create int run(int argc, char* argv[]) and have main call run once.
    • Explain why calling main is undefined/forbidden.
  3. Off-by-one with argc/argv
    The program intends to echo user args, but it’s wrong. Fix it and convert to modern C++ strings.
    #include <iostream>
    int main(int argc, char* argv[]) {
        for (int i = 0; i <= argc; ++i) {               // ❌ out-of-bounds
            std::cout << "arg[" << i << "] = " << argv[i] << "\n";
        }
        return 0;
    }
    
    • Print only user-provided arguments (skip program name).
    • Build std::vector<std::string> from argv and print that instead.
  4. Parse numbers safely & return meaningful codes
    Implement a program that sums integer arguments: on success prints the sum and returns 0; on the first non-integer, prints an error and returns a nonzero code.
    #include <iostream>
    #include <string>
    #include <vector>
    #include <charconv>   // from_chars
    
    enum ExitCode { Ok = 0, BadUsage = 64, ParseError = 65 };
    
    int main(int argc, char* argv[]) {
        // TODO: parse argv[1..]
    }
    
    • Use std::from_chars (no exceptions, no locales) to parse each arg.
    • Choose and document your exit codes (e.g., ParseError when an arg isn’t an integer).
  5. return vs std::exit (destructors & atexit)
    Predict output order, then test: which lines print if you replace return 0; with std::exit(0);?
    #include <iostream>
    #include <cstdlib>
    
    struct Trace {
        ~Trace(){ std::cout << "~Trace\n"; }
    } t;
    
    void on_exit(){ std::cout << "atexit\n"; }
    
    int main() {
        std::atexit(on_exit);
        std::cout << "main body\n";
        return 0;          // try std::exit(0) instead
    }
    
    • Explain when stack/unwinding-based destructors run and how std::exit differs.
  6. Minimal app vs. library
    You’re writing a reusable library function and a tiny executable to exercise it. Fill in the blanks.
    // lib.hpp
    #pragma once
    #include <vector>
    int sum(const std::vector<int>&);
    
    // lib.cpp
    #include "lib.hpp"
    int sum(const std::vector<int>& v) {
        int s = 0; for (int x : v) s += x; return s;
    }
    
    // main.cpp
    #include <iostream>
    #include "lib.hpp"
    int main(int argc, char* argv[]) {
        // TODO: parse argv[1..] to ints, call sum, print result, return 0 on success
    }
    
    • Explain why only the executable needs main and the library should not define one.
  7. Unicode/spacing in args (quoting) (bonus)
    How would you correctly handle arguments containing spaces (e.g., "New York")? What happens to argc/argv? Briefly describe how your shell passes quoted args to main.
Programming Insight (AI) — Entry Point Coach
  • Signature check: “Validate my main signature and portability.”
  • Arg parser scaffold: “Convert argv to std::vector<std::string>, parse ints with from_chars, handle errors.”
  • Exit codes: “Propose a small enum of exit codes and where to return them.”
  • Termination audit: “Show differences between return and std::exit for my code (destructors/atexit).”

Lesson 5 extension · one boundary, two directions

main admits input and reports an outcome

The smallest main can look like a ceremonial marker placed at the top of a program. Execution begins here, so the explanation appears finished. It is not. The important question is what crosses this boundary in each direction: invocation data enters the program, and a termination status returns to the invoking environment.

The special role of main is defined by C++. The operating system does not search through C++ source code until it finds a function with the right spelling. In a hosted implementation, the program provides one global function called main, and the implementation arranges for it to be called after the required start-up work. A toolchain and operating system do lower-level work before that point, but those mechanisms are outside the portable source-level model.

Begin with the two common portable forms: int main() and int main(int argc, char* argv[]). The second parameter can equivalently be written as a pointer to pointer. An implementation may permit other forms, but that does not make them portable course material. The declared return type is int; void main() is not a portable C++ form.

Count first, then justify every index

The parameter names are chosen by the programmer, so the example below uses argumentCount and arguments rather than relying on the traditional abbreviations. The rule is unchanged. argumentCount is non-negative, and arguments[argumentCount] is a null pointer marking the end of the array. Valid argument strings occupy the indices below that boundary.

If argumentCount is greater than zero, arguments[0] represents the name used to invoke the program, or it may point to an empty string when that information is unavailable. Do not turn a common observation, such as seeing a full executable path, into a guarantee. Later elements contain text supplied through the invoking environment. Quoting and tokenisation are handled before main receives the array, so quote characters typed in a shell are not necessarily present in the resulting argument string.

#include <iostream>

int main(int argumentCount, char* arguments[]) {
    std::cout << "Argument count: " << argumentCount << '\n';

    for (int index{0}; index < argumentCount; ++index) {
        std::cout << index << ": " << arguments[index] << '\n';
    }

    return 0;
}

Suppose the executable is invoked as course_demo alpha beta. A typical environment supplies an argument count of 3. The first loop iteration reads index 0, the second reads index 1 and the third reads index 2. When index becomes 3, the condition index < argumentCount is false and the loop stops. The program has accounted for every argument string without crossing the boundary.

ElementWhat it can mean in this invocationWhat the program must establish
arguments[0]Invocation text such as course_demo, or an empty stringDo not assume one path or naming format
arguments[1]The text alphaValidate its presence and meaning before using it
arguments[2]The text betaValidate syntax and range if another type is intended
arguments[3]The required null pointer sentinelRecognise the boundary; this is not an argument string

Changing the loop condition to index <= argumentCount does not include one more useful element. It attempts to stream the null sentinel as though it pointed to a character sequence. The error is not merely that the loop ran once too often; the program confused the marker of the boundary with data inside the boundary.

All later arguments remain text. If a person intends arguments[1] to mean the number 50, intention has not performed a conversion or proved that 50 is acceptable. The program must check that the argument exists, that its complete syntax represents the required value and that the value lies within the accepted range. A generated parser that works for one friendly example has passed one example, not the input contract.

Printed output and termination status answer different questions

std::cout communicates text through an output stream. Returning from main communicates a termination status to the host environment. They are separate channels. A program can print a correct partial report and still return failure because a later operation could not be completed. It can also print the word "success" and return a status that tells an automated caller the opposite. The printed word is not in charge.

Returning zero denotes successful termination in the portable C++ model. EXIT_SUCCESS and EXIT_FAILURE from <cstdlib> provide portable values for successful and unsuccessful termination. Other integer meanings belong to the surrounding platform or application convention and must be checked against the relevant specification rather than invented. Reaching the closing brace of main has the same success effect as return 0;, although the explicit return remains useful while the contract is being taught.

This distinction becomes visible when another program is the caller. A test runner, build script or deployment pipeline may ignore ordinary output and make its next decision from the termination status alone. If the program reports an error in prose but returns success, a person may notice the message while the automation proceeds as though the operation succeeded.

A short main still contains policy

Line count is a poor measure of the responsibility at the top of a program. A short main can still decide how invocation text is validated, where configuration comes from, which operation runs, how diagnostics are reported and how an application result becomes a process status. Moving detailed work into named functions makes those decisions easier to inspect; it does not make them disappear.

Consider a main containing four calls: read the command line, load configuration, run the application and translate the result into an exit status. The code may fit on one screen. The test is whether each failure has one accountable route. If configuration cannot be loaded, which function describes the problem, which channel carries the diagnostic and which status reaches the invoking process? Those are boundary decisions, not decoration around the real program.

I therefore want main to show policy without absorbing every mechanism. It should make the route from external input to application decision and back to external status visible. Parsing details, file operations and application logic can live elsewhere, where they can be named and tested. The top-level account must still be honest.

Programming Insight (AI): make the proposed entry point survive unfriendly invocations

Ask an AI tool for cases before asking it for a parser. Include no additional arguments, the expected arguments, too many arguments, malformed numbers, empty text and boundary values. For each invocation, write the expected output channel and termination status before running generated code. Then inspect every index and conversion. The tool's table is useful only when it exposes a missing case; it is not evidence until the program's observable behaviour agrees with the contract you wrote.

Trace the complete exchange

Draw the argument array for two invocations of the example: course_demo and course_demo alpha beta. Label every valid index, add the null pointer at arguments[argumentCount], and trace the loop condition before each access. Do not borrow an argument count from the example; derive it from the array you drew.

For each run, record two results separately: the text written to standard output and the status returned by main. Then make one deliberate mistake by changing < to <= and identify the first expression that no longer satisfies its obligation. If your explanation says only "out of bounds", continue. Name the value reached, explain why it is a sentinel rather than an argument string, and show which earlier condition should have prevented the access.

Reveal answer

For these traces I shall use the invocation text shown in the question. A real implementation may provide different text, or an empty string, for arguments[0]; that qualification does not alter the indexing rule.

course_demo

ElementValueMeaning
arguments[0]"course_demo"The invocation name in this example
arguments[1]nullptrThe required sentinel at arguments[argumentCount]

The derived argumentCount is 1. With index == 0, the condition 0 < 1 is true and index 0 is printed. After the increment, 1 < 1 is false, so no access occurs at index 1.

Argument count: 1
0: course_demo

The function then returns zero, reporting successful termination.

course_demo alpha beta

ElementValueMeaning
arguments[0]"course_demo"The invocation name
arguments[1]"alpha"The first user-supplied argument
arguments[2]"beta"The second user-supplied argument
arguments[3]nullptrThe sentinel at arguments[argumentCount]

The derived argumentCount is 3. The conditions 0 < 3, 1 < 3 and 2 < 3 are true. The next condition, 3 < 3, is false.

Argument count: 3
0: course_demo
1: alpha
2: beta

This run also returns zero.

The deliberate mistake

With index <= argumentCount, the condition remains true when index == argumentCount. The subscript reaches the valid sentinel element and produces a null pointer. The first failed obligation is therefore not simply that the array was indexed one place too far. It is the attempt to pass that null pointer to character-string stream insertion as though it denoted a null-terminated character sequence.

Once that invalid operation is attempted, no particular final line or termination status is portable. The earlier condition index < argumentCount is the condition that keeps every string access below the sentinel.