Program entry point
When the operating system launches your program, the first code that runs is not main(). Before it, the C++ runtime (CRT) gets its turn: to initialize memory, install exception handlers, and call the constructors of all global objects. Only then is control handed to your main function.
After main returns a value, the runtime takes the floor again: it calls the destructors of global objects, runs the functions registered via std::atexit, and only then reports the exit code to the operating system.
This is the first thing to understand about the entry point: main() is your code within the process lifecycle, not the lifecycle itself.
The C++ process lifecycle
The main() function
The C++ standard permits two forms of the main signature:
int main() // without command-line arguments
int main(int argc, char* argv[]) // with command-line arguments
int main(int argc, char** argv) is an equivalent spelling of the second form: in a function parameter, char*[] and char** mean the same thing.
Anatomy of main()
Why not void main()
void main() is a non-standard extension that some compilers historically allowed (and that some old textbooks still show). By the C++ standard, main is required to return int. Using void main() makes the program ill-formed; the behavior is undefined.
// Non-standard — do not use
void main() { }
// Correct
int main() { return 0; }
The return value
main returns the process's exit code. This code is seen by the operating system and by any process that launched your program (for example, a shell script or a CI pipeline).
| Return value | Meaning |
|---|---|
0 | Successful termination |
EXIT_SUCCESS | The same, but explicit in intent (usually 0) |
EXIT_FAILURE | Error (usually 1) |
| Other nonzero | A specific error code |
EXIT_SUCCESS and EXIT_FAILURE are declared in <cstdlib>. Use them — that way the code reads like documentation.
#include <cstdlib>
int main() {
if (!initialize()) {
return EXIT_FAILURE;
}
run();
return EXIT_SUCCESS;
}
If main finishes without a return, the standard guarantees an implicit return 0 — it is the only function with this behavior.
Command-line arguments
argv[0] is always the name of the executable (or an empty string in some embedded environments). argv[1] … argv[argc-1] are the arguments passed by the user. argv[argc] is always nullptr (guaranteed by the standard).
#include <iostream>
#include <string_view>
int main(int argc, char* argv[]) {
std::cout << "Program: " << argv[0] << "\n";
std::cout << "Arguments: " << argc - 1 << "\n";
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
std::cout << " [" << i << "] " << arg << "\n";
}
return 0;
}
A practical example — parsing flags
#include <iostream>
#include <string_view>
#include <cstdlib>
int main(int argc, char* argv[]) {
bool verbose = false;
const char* filename = nullptr;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "--verbose" || arg == "-v") {
verbose = true;
} else if ((arg == "--file" || arg == "-f") && i + 1 < argc) {
filename = argv[++i];
} else {
std::cerr << "Unknown argument: " << arg << "\n";
return EXIT_FAILURE;
}
}
if (!filename) {
std::cerr << "Error: no file specified (--file <path>)\n";
return EXIT_FAILURE;
}
if (verbose) {
std::cout << "Opening file: " << filename << "\n";
}
// ... further logic
return EXIT_SUCCESS;
}
Run:
./myapp --verbose --file data.txt # OK
./myapp -f data.txt # OK
./myapp # → error: no file specified
Environment variables
Besides command-line arguments, a program has access to environment variables through std::getenv():
#include <cstdlib>
#include <iostream>
int main() {
if (const char* home = std::getenv("HOME")) {
std::cout << "Home directory: " << home << "\n";
}
return 0;
}
Some platforms provide a non-standard third form main(int argc, char* argv[], char* envp[]), where envp is an array of strings of the form NAME=VALUE. This is an extension outside the C++ standard — use std::getenv() for portability.
What happens before main()
Most beginners are convinced that main() is the first function the program executes. That is not the case.
Before main() is called, the C++ runtime (CRT — C Runtime Library) performs a number of steps:
- Initializing the runtime environment — setting up the main thread's stack, the heap, and thread-local storage (TLS).
- Constructors of global and static objects — all variables with
static storage durationdeclared outside functions are initialized beforemain. Within a single translation unit the initialization order is top to bottom; the order across different translation units is not defined by the standard. - Registering atexit handlers — libraries may register cleanup functions ahead of time.
- Passing argc/argv — the OS passes the command-line arguments to the runtime, which packs them into the
argvarray and passes them tomain.
#include <iostream>
struct Logger {
Logger() { std::cout << "Logger created\n"; }
~Logger() { std::cout << "Logger destroyed\n"; }
};
Logger globalLogger; // constructor is called before main()
int main() {
std::cout << "Inside main()\n";
return 0;
}
// Output:
// Logger created
// Inside main()
// Logger destroyed
What happens after main()
After returning from main() (or calling std::exit()), the runtime performs a shutdown sequence:
- Functions registered via
std::atexit()are called in the reverse order of registration. - Destructors of objects with
static storage durationare called in reverse order relative to their constructors. - The buffers of the standard I/O streams are flushed (
std::flush). - The exit code is passed to the operating system.
#include <cstdlib>
#include <iostream>
void cleanup_network() { std::cout << "Network closed\n"; }
void cleanup_files() { std::cout << "Files closed\n"; }
int main() {
std::atexit(cleanup_network); // registered first
std::atexit(cleanup_files); // registered second
std::cout << "Working...\n";
return 0;
}
// Output:
// Working...
// Files closed ← the last registered is called first
// Network closed
The initialization order of global objects and its pitfalls
The standard guarantees the initialization order of global objects only within a single translation unit (a .cpp file) — top to bottom. Across different translation units the order is undefined. This is called the Static Initialization Order Fiasco.
// a.cpp
#include <string>
std::string prefix = "Hello"; // initialized in a.cpp
// b.cpp
#include <iostream>
extern std::string prefix;
struct Greeter {
Greeter() {
// Dangerous: if b.cpp is initialized before a.cpp,
// prefix has not been created yet — undefined behavior
std::cout << prefix << ", world!\n";
}
};
Greeter g; // constructor is called before main()
The solution: the Construct On First Use idiom
Wrap the global object in a function that returns a reference to a local static object. A local static variable is initialized on the first call to the function — guaranteed to be after the objects it depends on already exist.
// a.cpp
#include <string>
const std::string& get_prefix() {
static std::string prefix = "Hello"; // initialized on first call
return prefix;
}
// b.cpp
#include <iostream>
const std::string& get_prefix(); // declaration
struct Greeter {
Greeter() {
// Safe: get_prefix() guarantees that prefix is created
std::cout << get_prefix() << ", world!\n";
}
};
Greeter g;
In C++11 and later, local static variables are initialized in a thread-safe manner — no extra mutexes are needed.
An alternative is std::call_once from <mutex>, when you need explicit control over one-time initialization in multithreaded code:
#include <mutex>
#include <string>
static std::once_flag init_flag;
static std::string* config = nullptr;
void ensure_config() {
std::call_once(init_flag, []() {
config = new std::string("production");
});
}
Ways to terminate a program
Besides return from main(), there are several termination functions — and they behave differently:
| Function | Local object destructors | atexit | Global object destructors | I/O buffers |
|---|---|---|---|---|
return from main() | yes | yes | yes | yes |
std::exit(n) | no | yes | yes | yes |
std::quick_exit(n) (C++11) | no | no (only at_quick_exit) | no | no |
std::abort() | no | no | no | no |
#include <cstdlib>
int main() {
// std::exit() will call atexit and the global destructors,
// but the destructors of LOCAL objects on this stack — no.
// Leaks of resources not protected by RAII are possible.
std::exit(EXIT_FAILURE);
}
std::abort() raises the SIGABRT signal and terminates the process immediately — with no destructors and no buffer flushing. It is used when it is dangerous to keep the program running (for example, inside an assert handler).
std::quick_exit() (C++11) is a compromise: a normal termination without calling destructors. Functions can be registered via std::at_quick_exit().
WinMain on Windows
On Windows, console programs use the standard main(). But windowed (GUI) applications traditionally use the non-standard entry point WinMain:
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow) {
// Windows GUI startup
return 0;
}
This is a Microsoft extension — not part of the C++ standard. Modern frameworks (Qt, wxWidgets) and CMake can hide this difference: in main() you write cross-platform code, and the linker substitutes the appropriate entry point.
Interview relevance
The entry-point topic comes up in interviews as a depth check: does the candidate know what happens before and after main(), or do they think main() is the beginning and the end of everything.
What the interviewer is checking:
- An understanding of the process lifecycle (CRT startup → main → CRT shutdown)
- Awareness of the dangers of global objects and the ability to name the solutions
Typical lines of questioning:
- "What runs before main()?" — expected: constructors of global objects, CRT initialization, setting up argc/argv.
- "In what order are global objects from different .cpp files initialized?" — expected: the order is not defined by the standard; the Static Initialization Order Fiasco; the solution is Construct On First Use.
- "How does std::exit() differ from return from main()?" — expected:
std::exitdoes not call the destructors of local objects on the current stack. - "What does the return code of main() mean?" — expected: it is passed to the OS; 0 = success; EXIT_SUCCESS/EXIT_FAILURE from
<cstdlib>. - "What is WinMain?" — expected: a non-standard entry point for Windows GUI applications, not part of the C++ standard.
The most common candidate mistake: "main() is the first function that runs." The correct answer: CRT startup runs first, calling the constructors of global objects and only then handing control to main().