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.
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
mainfunction in a program. maincan take parameters (for command-line arguments).- You cannot call
mainexplicitly 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
mainscaffold 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:
argcis the number of arguments (at least 1: the program name).argvpoints 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
argvcontents. - Ask AI to convert
argvinto modernstd::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.,
cxxoptsorBoost.Program_options). - Generate unit tests where AI simulates command-line inputs to check parsing logic.
5) Summary
mainis the entry point of every executable program in C++.- It may or may not take arguments, depending on your needs.
- Return
0for success, nonzero for errors. - Arguments let you interact with the program via the command line.
- Only one
mainis 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).
-
Pick the valid signature
Which of these are valid and portable forms ofmain? 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).
-
Don’t call
main
Identify the problem and refactor so the program has a testable entry function you call frommain.#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 havemaincallrunonce. - Explain why calling
mainis undefined/forbidden.
- Create
-
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>fromargvand print that instead.
-
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.,
ParseErrorwhen an arg isn’t an integer).
- Use
-
returnvsstd::exit(destructors & atexit)
Predict output order, then test: which lines print if you replacereturn 0;withstd::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::exitdiffers.
- Explain when stack/unwinding-based destructors run and how
-
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
mainand the library should not define one.
- Explain why only the executable needs
-
Unicode/spacing in args (quoting) (bonus)
How would you correctly handle arguments containing spaces (e.g.,"New York")? What happens toargc/argv? Briefly describe how your shell passes quoted args tomain.
Programming Insight (AI) — Entry Point Coach
- Signature check: “Validate my
mainsignature and portability.” - Arg parser scaffold: “Convert
argvtostd::vector<std::string>, parse ints withfrom_chars, handle errors.” - Exit codes: “Propose a small enum of exit codes and where to return them.”
- Termination audit: “Show differences between
returnandstd::exitfor 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.
| Element | What it can mean in this invocation | What the program must establish |
|---|---|---|
arguments[0] | Invocation text such as course_demo, or an empty string | Do not assume one path or naming format |
arguments[1] | The text alpha | Validate its presence and meaning before using it |
arguments[2] | The text beta | Validate syntax and range if another type is intended |
arguments[3] | The required null pointer sentinel | Recognise 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
| Element | Value | Meaning |
|---|---|---|
arguments[0] | "course_demo" | The invocation name in this example |
arguments[1] | nullptr | The 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
| Element | Value | Meaning |
|---|---|---|
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] | nullptr | The 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.