Command-Line Arguments
Command-line arguments allow users to pass information to your program when launching it from the terminal. This is essential for building flexible tools, scripts, and utilities that can behave differently based on user input without requiring recompilation.
Understanding argc and argv
Every C++ program can receive command-line arguments through two special parameters in the
main() function: argc (argument count) and argv
(argument vector). These parameters give your program access to everything the user typed
on the command line when launching the program.
The Main Function Signatures
- int main(): No arguments - program ignores command-line input
- int main(int argc, char* argv[]): Standard form with argument access
- int main(int argc, char** argv): Equivalent pointer notation
- argc: Integer count of arguments (including program name)
- argv: Array of C-strings containing each argument
Basic Argument Access
Basic argument access involves iterating through the argv array using the argc count to display or process each command-line argument passed to the program, including the program name itself at argv[0].
#include <iostream>
int main(int argc, char* argv[]) {
std::cout << "Number of arguments: " << argc << std::endl;
for (int i = 0; i < argc; i++) {
std::cout << "argv[" << i << "] = " << argv[i] << std::endl;
}
return 0;
}
./program hello world, the output shows argc=3 because there are three arguments:
the program name itself (argv[0] = "./program"), and the two user arguments (argv[1] = "hello", argv[2] = "world"). The
program name is always included in the count, so argc is never less than 1. This consistent behavior allows you to always
safely access argv[0] to get the program name for usage messages or logging purposes.
Validating Argument Count
Validating argument count means checking the argc value before accessing argv elements to ensure the required number of arguments was provided, preventing array out-of-bounds errors and providing helpful usage messages when arguments are missing.
#include <iostream>
#include <string>
int main(int argc, char* argv[]) {
// Check if required argument is provided
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <name>" << std::endl;
return 1; // Return error code
}
std::string name = argv[1];
std::cout << "Hello, " << name << "!" << std::endl;
return 0;
}
Converting Arguments to Numbers
Converting arguments to numbers is the process of transforming string arguments from argv into numeric types (int, double, etc.) using functions like std::stoi() or atoi(), enabling mathematical operations and numeric validation on command-line input.
#include <iostream>
#include <cstdlib> // For atoi, atof
#include <string> // For stoi, stod
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " <num1> <num2>" << std::endl;
return 1;
}
// C-style conversion (no error checking)
int a = atoi(argv[1]);
// Modern C++ conversion (throws exception on error)
try {
int b = std::stoi(argv[2]);
std::cout << a << " + " << b << " = " << (a + b) << std::endl;
} catch (const std::exception& e) {
std::cerr << "Error: Invalid number format" << std::endl;
return 1;
}
return 0;
}
atoi() (string to int) and atof() (string to double) return 0 on failure with no way to
distinguish between "0" and invalid input. Modern C++ functions like std::stoi(), std::stod(),
and std::stoll() throw exceptions on invalid input, allowing proper error handling. Always prefer the
modern functions in new code for safer, more robust argument parsing.
Processing Multiple Arguments
Processing multiple arguments involves iterating through all command-line parameters systematically, often by converting argv to a std::vector<std::string> for easier manipulation, allowing batch operations on files, values, or options provided by the user.
#include <iostream>
#include <vector>
#include <string>
int main(int argc, char* argv[]) {
// Convert argv to vector of strings for easier handling
std::vector<std::string> args(argv, argv + argc);
std::cout << "Program: " << args[0] << std::endl;
// Process remaining arguments
for (size_t i = 1; i < args.size(); i++) {
std::cout << "Processing: " << args[i] << std::endl;
}
return 0;
}
std::vector<std::string> gives you all the benefits of modern C++ containers:
automatic memory management, the size() method, range-based for loops, and standard algorithms. The
constructor std::vector<std::string>(argv, argv + argc) creates the vector from the pointer range.
This is a common idiom that makes argument processing cleaner and less error-prone than working directly with raw
C-style arrays and pointers.
Parsing Command-Line Flags
Parsing command-line flags means identifying and processing option switches (like -v or --verbose) that modify program behavior, distinguishing them from positional arguments by their leading dash character, and setting corresponding boolean or configuration variables.
#include <iostream>
#include <string>
#include <vector>
int main(int argc, char* argv[]) {
bool verbose = false;
bool help = false;
std::string filename;
-v or -h are present. The filename string will store any non-flag argument (positional parameter).
std::vector<std::string> args(argv + 1, argv + argc);
argv + 1 to skip the program name at argv[0]. This gives us only the user-provided arguments in a convenient C++ container that's easier to iterate and manipulate.
for (const auto& arg : args) {
if (arg == "-v" || arg == "--verbose") {
verbose = true;
} else if (arg == "-h" || arg == "--help") {
help = true;
} else if (arg[0] != '-') {
filename = arg;
}
}
-v or --verbose), set the corresponding boolean. Arguments not starting with a dash are treated as positional parameters. This supports both short (-v) and long (--verbose) flag formats following Unix conventions.
if (help) {
std::cout << "Usage: " << argv[0] << " [-v] [-h] <file>" << std::endl;
return 0;
}
if (verbose) {
std::cout << "Verbose mode enabled" << std::endl;
}
if (!filename.empty()) {
std::cout << "Processing file: " << filename << std::endl;
}
return 0;
}
--output=file.txt), or advanced validation, consider using dedicated libraries like getopt (POSIX standard), Boost.Program_options, or CLI11 that provide automatic help generation, type conversion, and error handling.
Practice: Command-Line Arguments
Given:
./echo hello world "good morning"
Task: Write a program that prints each command-line argument on a separate line, excluding the program name.
Expected output:
hello
world
good morning
Hint: Start the loop from index 1 to skip argv[0].
Show Solution
#include <iostream>
int main(int argc, char* argv[]) {
for (int i = 1; i < argc; i++) {
std::cout << argv[i] << std::endl;
}
return 0;
}
Given:
./sum 10 20 30 40
Task: Write a program that calculates and displays the sum of all numeric arguments. Handle invalid input gracefully.
Expected output: Sum: 100
Hint: Use std::stoi() with try-catch for conversion.
Show Solution
#include <iostream>
#include <string>
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <num1> [num2] ..." << std::endl;
return 1;
}
int sum = 0;
for (int i = 1; i < argc; i++) {
try {
sum += std::stoi(argv[i]);
} catch (const std::exception& e) {
std::cerr << "Warning: '" << argv[i] << "' is not a valid number" << std::endl;
}
}
std::cout << "Sum: " << sum << std::endl;
return 0;
}
Given:
./counter -c hello world
./counter --count hello world
Task: Write a program that prints arguments normally, but if -c or --count flag is present, also show the character count for each.
Expected output with -c flag:
hello (5 chars)
world (5 chars)
Hint: Check for the flag first, then process remaining arguments.
Show Solution
#include <iostream>
#include <string>
#include <cstring>
int main(int argc, char* argv[]) {
bool showCount = false;
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if (arg == "-c" || arg == "--count") {
showCount = true;
} else {
if (showCount) {
std::cout << arg << " (" << arg.length() << " chars)" << std::endl;
} else {
std::cout << arg << std::endl;
}
}
}
return 0;
}
Given:
./minigrep -i pattern file1.txt file2.txt
Task: Write a program that accepts a pattern and multiple filenames. With -i flag, make the search case-insensitive. Print matching lines with filename prefix.
Expected output:
file1.txt: This line contains the Pattern
file2.txt: pattern appears here too
Hint: Convert both pattern and line to lowercase for case-insensitive matching.
Show Solution
#include <iostream>
#include <fstream>
#include <string>
#include <algorithm>
std::string toLower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
return s;
}
int main(int argc, char* argv[]) {
if (argc < 3) {
std::cerr << "Usage: " << argv[0] << " [-i] <pattern> <file>..." << std::endl;
return 1;
}
bool ignoreCase = false;
int argIndex = 1;
if (std::string(argv[1]) == "-i") {
ignoreCase = true;
argIndex = 2;
}
if (argc < argIndex + 2) {
std::cerr << "Error: Need pattern and at least one file" << std::endl;
return 1;
}
std::string pattern = argv[argIndex];
std::string patternLower = toLower(pattern);
for (int i = argIndex + 1; i < argc; i++) {
std::ifstream file(argv[i]);
if (!file) {
std::cerr << "Cannot open: " << argv[i] << std::endl;
continue;
}
std::string line;
while (std::getline(file, line)) {
bool match = ignoreCase
? (toLower(line).find(patternLower) != std::string::npos)
: (line.find(pattern) != std::string::npos);
if (match) {
std::cout << argv[i] << ": " << line << std::endl;
}
}
}
return 0;
}
Environment Variables
Environment variables are system-wide settings that programs can read to configure their behavior. They store information like the user's home directory, system paths, and application-specific settings without hardcoding values into your program.
Reading Environment Variables
The getenv() function from <cstdlib> retrieves the value of an
environment variable by name. It returns a pointer to the variable's value or nullptr
if the variable doesn't exist. This allows programs to adapt to different system configurations
without modification.
Basic Environment Variable Access
#include <iostream>
#include <cstdlib>
int main() {
// Get the PATH environment variable
const char* path = std::getenv("PATH");
if (path != nullptr) {
std::cout << "PATH: " << path << std::endl;
} else {
std::cout << "PATH is not set" << std::endl;
}
return 0;
}
getenv() function returns a const char* pointing to the environment variable's value.
Always check for nullptr before using the result, as the variable might not exist on all systems. The
returned pointer points to static memory managed by the system, so you should not modify or free it. If you need to
modify the value, copy it to a std::string first. Common environment variables include PATH (executable
search paths), HOME (user's home directory), and USER (current username).
Common Environment Variables
- PATH: Directories to search for executable programs
- HOME: User's home directory (Unix) or USERPROFILE (Windows)
- USER/USERNAME: Current logged-in user's name
- TEMP/TMP: Directory for temporary files
- PWD: Present working directory (Unix)
- LANG: System language and locale settings
Safe Environment Variable Access
#include <iostream>
#include <cstdlib>
#include <string>
std::string getEnvSafe(const char* name, const std::string& defaultValue = "") {
const char* value = std::getenv(name);
return (value != nullptr) ? std::string(value) : defaultValue;
}
int main() {
// Get with default values
std::string home = getEnvSafe("HOME", "/tmp");
std::string editor = getEnvSafe("EDITOR", "nano");
std::string debugMode = getEnvSafe("DEBUG", "false");
std::cout << "Home directory: " << home << std::endl;
std::cout << "Default editor: " << editor << std::endl;
std::cout << "Debug mode: " << debugMode << std::endl;
return 0;
}
getEnvSafe() simplifies environment variable access throughout your
program. It returns a proper std::string (safe to use and modify) and provides default values when
variables are not set. This pattern is common in configuration systems where you want sensible defaults but allow
users to override behavior through environment variables. The default value approach makes your program more
portable across different systems and user configurations.
Cross-Platform Home Directory
#include <iostream>
#include <cstdlib>
#include <string>
std::string getHomeDirectory() {
// Try Unix-style first
const char* home = std::getenv("HOME");
if (home != nullptr) {
return std::string(home);
}
// Try Windows-style
const char* userProfile = std::getenv("USERPROFILE");
if (userProfile != nullptr) {
return std::string(userProfile);
}
// Windows alternative: combine HOMEDRIVE and HOMEPATH
const char* homeDrive = std::getenv("HOMEDRIVE");
const char* homePath = std::getenv("HOMEPATH");
if (homeDrive != nullptr && homePath != nullptr) {
return std::string(homeDrive) + std::string(homePath);
}
return ""; // Could not determine home directory
}
int main() {
std::string home = getHomeDirectory();
if (!home.empty()) {
std::cout << "Home directory: " << home << std::endl;
} else {
std::cerr << "Could not determine home directory" << std::endl;
}
return 0;
}
Application Configuration from Environment
#include <iostream>
#include <cstdlib>
#include <string>
struct AppConfig {
std::string dbHost;
int dbPort;
std::string logLevel;
bool debugMode;
};
AppConfig loadConfigFromEnv() {
AppConfig config;
// Database settings
const char* host = std::getenv("DB_HOST");
config.dbHost = (host != nullptr) ? host : "localhost";
const char* port = std::getenv("DB_PORT");
config.dbPort = (port != nullptr) ? std::stoi(port) : 5432;
// Logging settings
const char* logLevel = std::getenv("LOG_LEVEL");
config.logLevel = (logLevel != nullptr) ? logLevel : "INFO";
// Debug mode
const char* debug = std::getenv("DEBUG");
config.debugMode = (debug != nullptr && std::string(debug) == "true");
return config;
}
int main() {
AppConfig config = loadConfigFromEnv();
std::cout << "Database: " << config.dbHost << ":" << config.dbPort << std::endl;
std::cout << "Log Level: " << config.logLevel << std::endl;
std::cout << "Debug Mode: " << (config.debugMode ? "enabled" : "disabled") << std::endl;
return 0;
}
Practice: Environment Variables
Given:
// Environment: USER=john, HOME=/home/john, SHELL=/bin/bash
Task: Write a program that displays the current user's name, home directory, and shell. Handle missing variables gracefully.
Expected output:
User: john
Home: /home/john
Shell: /bin/bash
Hint: Check for nullptr before printing each value.
Show Solution
#include <iostream>
#include <cstdlib>
void printEnv(const char* name, const char* label) {
const char* value = std::getenv(name);
if (value != nullptr) {
std::cout << label << ": " << value << std::endl;
} else {
std::cout << label << ": (not set)" << std::endl;
}
}
int main() {
printEnv("USER", "User");
printEnv("HOME", "Home");
printEnv("SHELL", "Shell");
return 0;
}
Given:
// PATH=/usr/local/bin:/usr/bin:/bin
Task: Write a program that reads the PATH environment variable and prints each directory on a separate line with a number prefix.
Expected output:
1. /usr/local/bin
2. /usr/bin
3. /bin
Hint: Use a stringstream with getline() and the colon delimiter (or semicolon on Windows).
Show Solution
#include <iostream>
#include <cstdlib>
#include <sstream>
#include <string>
int main() {
const char* path = std::getenv("PATH");
if (path == nullptr) {
std::cerr << "PATH is not set" << std::endl;
return 1;
}
std::istringstream iss(path);
std::string dir;
int count = 1;
// Use ':' on Unix, ';' on Windows
char delimiter = ':';
#ifdef _WIN32
delimiter = ';';
#endif
while (std::getline(iss, dir, delimiter)) {
if (!dir.empty()) {
std::cout << count++ << ". " << dir << std::endl;
}
}
return 0;
}
Given:
// APP_PORT=8080, APP_TIMEOUT=30.5, APP_DEBUG=true, APP_NAME=MyApp
Task: Create a Config class with methods to get environment variables as different types: getString(), getInt(), getDouble(), getBool(). Each method should accept a default value.
Expected usage:
Config cfg;
int port = cfg.getInt("APP_PORT", 3000); // 8080
double timeout = cfg.getDouble("APP_TIMEOUT", 10.0); // 30.5
bool debug = cfg.getBool("APP_DEBUG", false); // true
Show Solution
#include <iostream>
#include <cstdlib>
#include <string>
class Config {
public:
std::string getString(const char* name, const std::string& def = "") {
const char* val = std::getenv(name);
return (val != nullptr) ? std::string(val) : def;
}
int getInt(const char* name, int def = 0) {
const char* val = std::getenv(name);
if (val == nullptr) return def;
try {
return std::stoi(val);
} catch (...) {
return def;
}
}
double getDouble(const char* name, double def = 0.0) {
const char* val = std::getenv(name);
if (val == nullptr) return def;
try {
return std::stod(val);
} catch (...) {
return def;
}
}
bool getBool(const char* name, bool def = false) {
const char* val = std::getenv(name);
if (val == nullptr) return def;
std::string s(val);
return (s == "true" || s == "1" || s == "yes" || s == "on");
}
};
int main() {
Config cfg;
std::cout << "Name: " << cfg.getString("APP_NAME", "DefaultApp") << std::endl;
std::cout << "Port: " << cfg.getInt("APP_PORT", 3000) << std::endl;
std::cout << "Timeout: " << cfg.getDouble("APP_TIMEOUT", 10.0) << std::endl;
std::cout << "Debug: " << (cfg.getBool("APP_DEBUG", false) ? "yes" : "no") << std::endl;
return 0;
}
Process Control
Process control allows your C++ program to interact with the operating system, execute other programs, and manage program termination. Understanding these concepts is crucial for building system utilities and applications that integrate with other software.
Program Termination
C++ provides several ways to terminate a program, each with different behaviors regarding cleanup and resource management. Choosing the right termination method ensures your program exits cleanly and communicates its status properly to the operating system.
Program Exit Functions
- return from main(): Normal exit with cleanup - destructors called, streams flushed
- exit(code): Normal exit - calls atexit handlers, flushes streams, skips local destructors
- quick_exit(code): Fast exit - calls at_quick_exit handlers only (C++11)
- _Exit(code): Immediate exit - no cleanup, no handlers called
- abort(): Abnormal termination - generates SIGABRT signal
Exit Codes and Status
#include <iostream>
#include <cstdlib>
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Error: Missing required argument" << std::endl;
return EXIT_FAILURE; // Usually 1
}
std::string arg = argv[1];
if (arg == "--help") {
std::cout << "Usage: program <input>" << std::endl;
return EXIT_SUCCESS; // Usually 0
}
// Process the argument...
std::cout << "Processing: " << arg << std::endl;
return EXIT_SUCCESS;
}
EXIT_SUCCESS (0) indicates successful completion, while EXIT_FAILURE (usually 1)
indicates an error. Using these macros from <cstdlib> instead of magic numbers makes code
more readable and portable. Shell scripts often check exit codes to decide whether to continue (e.g.,
if command; then ...), making proper exit codes essential for automation and scripting workflows.
Using exit() for Early Termination
#include <iostream>
#include <cstdlib>
#include <fstream>
void cleanup() {
std::cout << "Cleanup: Releasing resources..." << std::endl;
}
int main() {
// Register cleanup function
std::atexit(cleanup);
std::ifstream file("config.txt");
if (!file.is_open()) {
std::cerr << "Fatal: Cannot open config file" << std::endl;
exit(EXIT_FAILURE); // cleanup() will be called
}
std::cout << "Processing configuration..." << std::endl;
// ... program logic ...
return EXIT_SUCCESS; // cleanup() called here too
}
exit() function terminates the program from anywhere in your code, not just main().
Functions registered with atexit() are called in reverse order of registration before the program
exits. This is useful for cleanup tasks like closing connections, saving state, or releasing global resources.
Note that local object destructors are NOT called when using exit() - only global/static destructors
and atexit handlers run. For RAII-based cleanup, prefer returning from functions or throwing exceptions.
Executing External Commands
The system() function allows your program to execute shell commands and other programs.
While simple to use, it has security implications that must be understood for safe usage.
Basic System Command Execution
#include <iostream>
#include <cstdlib>
int main() {
std::cout << "Directory contents:" << std::endl;
// Execute a shell command
#ifdef _WIN32
int result = system("dir");
#else
int result = system("ls -la");
#endif
if (result == 0) {
std::cout << "Command executed successfully" << std::endl;
} else {
std::cerr << "Command failed with code: " << result << std::endl;
}
return 0;
}
system() function passes a command string to the system's command processor (shell). It returns
the command's exit status, or -1 if the shell could not be invoked. The command runs synchronously - your program
waits until it completes. Platform differences require conditional compilation: Unix uses commands like "ls", while
Windows uses "dir". For cross-platform applications, consider using portable libraries instead of shell commands
where possible.
Checking Command Availability
#include <iostream>
#include <cstdlib>
bool commandExists(const char* cmd) {
#ifdef _WIN32
std::string check = "where " + std::string(cmd) + " > nul 2>&1";
#else
std::string check = "which " + std::string(cmd) + " > /dev/null 2>&1";
#endif
return system(check.c_str()) == 0;
}
int main() {
// Check if git is installed
if (commandExists("git")) {
std::cout << "Git is installed" << std::endl;
system("git --version");
} else {
std::cout << "Git is not installed" << std::endl;
}
return 0;
}
Security Warning: Command Injection
#include <iostream>
#include <cstdlib>
#include <string>
#include <algorithm>
// DANGEROUS - Never do this with user input!
void dangerousSearch(const std::string& userInput) {
std::string cmd = "grep " + userInput + " data.txt";
system(cmd.c_str()); // User could inject: "; rm -rf /"
}
// SAFER - Validate and sanitize input
bool isValidFilename(const std::string& name) {
// Only allow alphanumeric, dash, underscore, and dot
return std::all_of(name.begin(), name.end(), [](char c) {
return std::isalnum(c) || c == '-' || c == '_' || c == '.';
});
}
void saferOperation(const std::string& filename) {
if (!isValidFilename(filename)) {
std::cerr << "Invalid filename" << std::endl;
return;
}
std::string cmd = "cat " + filename;
system(cmd.c_str());
}
int main() {
std::string input;
std::cout << "Enter filename: ";
std::cin >> input;
saferOperation(input);
return 0;
}
system()! An attacker could
inject shell commands by entering something like file.txt; rm -rf /. Always validate and sanitize
input, or better yet, avoid system() when handling user data. Use dedicated libraries for specific
tasks (file operations, network requests) instead of shell commands. When you must use system(), use whitelisting
(allow only known-safe characters) rather than blacklisting (blocking dangerous characters).
Practice: Process Control
Given:
./validate 42
./validate abc
./validate
Task: Write a program that validates its argument is a positive integer. Return EXIT_SUCCESS if valid, EXIT_FAILURE otherwise. Print an error message for failures.
Expected behavior:
./validate 42 # exits with 0
./validate abc # prints error, exits with 1
./validate # prints usage, exits with 1
Show Solution
#include <iostream>
#include <cstdlib>
#include <string>
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <number>" << std::endl;
return EXIT_FAILURE;
}
try {
int num = std::stoi(argv[1]);
if (num <= 0) {
std::cerr << "Error: Number must be positive" << std::endl;
return EXIT_FAILURE;
}
std::cout << "Valid: " << num << std::endl;
return EXIT_SUCCESS;
} catch (...) {
std::cerr << "Error: Invalid number format" << std::endl;
return EXIT_FAILURE;
}
}
Task: Create a program that registers multiple atexit handlers to demonstrate their execution order. Register 3 handlers that print messages, then exit normally.
Expected output:
Starting program...
Cleanup 3: Final cleanup
Cleanup 2: Closing connections
Cleanup 1: Saving state
Hint: atexit handlers are called in reverse order of registration.
Show Solution
#include <iostream>
#include <cstdlib>
void cleanup1() {
std::cout << "Cleanup 1: Saving state" << std::endl;
}
void cleanup2() {
std::cout << "Cleanup 2: Closing connections" << std::endl;
}
void cleanup3() {
std::cout << "Cleanup 3: Final cleanup" << std::endl;
}
int main() {
// Register in order - will be called in reverse
std::atexit(cleanup1);
std::atexit(cleanup2);
std::atexit(cleanup3);
std::cout << "Starting program..." << std::endl;
// Normal exit - all handlers will be called
return 0;
}
Task: Create a mini build system that accepts commands: "compile", "run", "clean". Use system() to execute appropriate commands based on the operating system.
Expected usage:
./build compile main.cpp # compiles main.cpp
./build run main # runs ./main (Unix) or main.exe (Windows)
./build clean # removes executables
Show Solution
#include <iostream>
#include <cstdlib>
#include <string>
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <compile|run|clean> [args]" << std::endl;
return 1;
}
std::string cmd = argv[1];
if (cmd == "compile" && argc >= 3) {
std::string src = argv[2];
std::string out = src.substr(0, src.find('.'));
#ifdef _WIN32
std::string compile = "g++ -o " + out + ".exe " + src;
#else
std::string compile = "g++ -o " + out + " " + src;
#endif
std::cout << "Compiling: " << compile << std::endl;
return system(compile.c_str());
} else if (cmd == "run" && argc >= 3) {
#ifdef _WIN32
std::string run = std::string(argv[2]) + ".exe";
#else
std::string run = "./" + std::string(argv[2]);
#endif
std::cout << "Running: " << run << std::endl;
return system(run.c_str());
} else if (cmd == "clean") {
#ifdef _WIN32
return system("del *.exe 2>nul");
#else
return system("rm -f *.out a.out 2>/dev/null");
#endif
} else {
std::cerr << "Unknown command: " << cmd << std::endl;
return 1;
}
}
Signal Handling
Signals are software interrupts sent to a program to indicate that an important event has occurred. Learning to handle signals allows your program to respond gracefully to events like user interrupts (Ctrl+C), termination requests, and other system notifications.
Understanding Signals
Signals are a form of inter-process communication used by the operating system to notify programs of various events. Each signal has a default action (terminate, ignore, or stop), but programs can install custom handlers to respond differently.
Common Signals
- SIGINT (2): Interrupt from keyboard (Ctrl+C) - can be caught
- SIGTERM (15): Termination request - can be caught
- SIGKILL (9): Forced termination - cannot be caught or ignored
- SIGSEGV (11): Segmentation fault - invalid memory access
- SIGFPE (8): Floating-point exception - division by zero
- SIGALRM (14): Timer alarm - used for timeouts
Basic Signal Handling
#include <iostream>
#include <csignal>
#include <cstdlib>
// Signal handler function
void signalHandler(int signum) {
std::cout << "\nInterrupt signal (" << signum << ") received.\n";
// Cleanup and close up stuff here
std::cout << "Cleaning up..." << std::endl;
// Terminate program
exit(signum);
}
int main() {
// Register signal handler for SIGINT (Ctrl+C)
signal(SIGINT, signalHandler);
std::cout << "Program running. Press Ctrl+C to interrupt..." << std::endl;
// Infinite loop - wait for signal
while (true) {
std::cout << "." << std::flush;
// Sleep would go here in real code
}
return 0;
}
signal() function from <csignal> registers a handler function for a specific
signal. When that signal is received, your handler is called with the signal number as its argument. Signal
handlers should be simple and fast - they interrupt normal program flow and have restrictions on what functions
can be safely called (async-signal-safe functions only). For complex cleanup, set a flag in the handler and
check it in your main loop rather than doing the work directly in the handler.
Graceful Shutdown Pattern
#include <iostream>
#include <csignal>
#include <atomic>
// Use atomic for thread-safe flag
std::atomic<bool> running(true);
void shutdownHandler(int signum) {
std::cout << "\nShutdown requested (signal " << signum << ")" << std::endl;
running = false; // Signal main loop to stop
}
int main() {
// Handle both SIGINT and SIGTERM
signal(SIGINT, shutdownHandler);
signal(SIGTERM, shutdownHandler);
std::cout << "Server starting... (Ctrl+C to stop)" << std::endl;
int counter = 0;
while (running) {
// Simulate server work
std::cout << "Processing request " << ++counter << std::endl;
// In real code: process requests, handle connections, etc.
}
std::cout << "Shutting down gracefully..." << std::endl;
std::cout << "Processed " << counter << " requests total." << std::endl;
return 0;
}
std::atomic<bool> ensures thread
safety if you have multiple threads checking the flag. Handling both SIGINT (Ctrl+C) and SIGTERM (kill command)
makes your program behave well in both interactive and automated environments.
Ignoring and Restoring Default Handlers
#include <iostream>
#include <csignal>
int main() {
// Ignore SIGINT - Ctrl+C will do nothing
signal(SIGINT, SIG_IGN);
std::cout << "SIGINT ignored. Ctrl+C won't work for 5 seconds..." << std::endl;
// Do critical work that shouldn't be interrupted
for (int i = 5; i > 0; i--) {
std::cout << i << "..." << std::endl;
// sleep(1) would go here
}
// Restore default behavior
signal(SIGINT, SIG_DFL);
std::cout << "SIGINT restored. Ctrl+C will now terminate." << std::endl;
while (true) {
std::cout << "Running..." << std::endl;
}
return 0;
}
SIG_IGN tells the system to ignore the signal completely - the program continues uninterrupted.
SIG_DFL restores the default behavior for that signal. This pattern is useful during critical
sections where interruption would cause data corruption or incomplete operations. Be careful with SIG_IGN:
if you ignore SIGINT, users lose the ability to interrupt your program normally. Always restore handlers after
critical sections complete, and never ignore SIGKILL or SIGSTOP (the OS won't let you anyway).
Practice: Signal Handling
Task: Write a program that counts SIGINT signals. Exit only after receiving 3 interrupts, displaying the count each time.
Expected behavior:
Running... Press Ctrl+C 3 times to exit
^CInterrupt 1 of 3
^CInterrupt 2 of 3
^CInterrupt 3 of 3 - Exiting!
Hint: Use a global counter variable that the signal handler increments.
Show Solution
#include <iostream>
#include <csignal>
#include <cstdlib>
int interruptCount = 0;
void handler(int signum) {
interruptCount++;
std::cout << "\nInterrupt " << interruptCount << " of 3";
if (interruptCount >= 3) {
std::cout << " - Exiting!" << std::endl;
exit(0);
}
std::cout << std::endl;
}
int main() {
signal(SIGINT, handler);
std::cout << "Running... Press Ctrl+C 3 times to exit" << std::endl;
while (true) {
// Keep running
}
return 0;
}
Task: Create a program that sets a 5-second timeout. If the user doesn't enter input within 5 seconds, print "Timeout!" and exit. (Unix only - SIGALRM)
Expected behavior:
Enter your name (5 seconds):
Timeout! No input received.
Hint: Use alarm() to schedule SIGALRM and a signal handler to catch it.
Show Solution
#include <iostream>
#include <csignal>
#include <cstdlib>
#include <unistd.h> // For alarm() - Unix only
#include <string>
void timeoutHandler(int signum) {
std::cout << "\nTimeout! No input received." << std::endl;
exit(1);
}
int main() {
signal(SIGALRM, timeoutHandler);
std::cout << "Enter your name (5 seconds): " << std::flush;
alarm(5); // Set 5 second alarm
std::string name;
std::getline(std::cin, name);
alarm(0); // Cancel alarm
std::cout << "Hello, " << name << "!" << std::endl;
return 0;
}
Date and Time
Working with dates and times is essential for logging, scheduling, and time-based calculations.
Modern C++ provides the powerful chrono library for precise time measurements and
the traditional ctime functions for calendar operations and formatting.
The Chrono Library (C++11)
The <chrono> library provides a type-safe way to work with time durations and points.
It prevents common errors by encoding time units in the type system, so you can't accidentally mix seconds
and milliseconds.
Chrono Components
- Durations: Time spans (hours, minutes, seconds, milliseconds, etc.)
- Time Points: Specific moments in time
- Clocks: system_clock (wall time), steady_clock (monotonic), high_resolution_clock
- Literals:
1h,30min,45s,100ms(C++14)
Measuring Execution Time
#include <iostream>
#include <chrono>
#include <thread>
int main() {
using namespace std::chrono;
// Get start time
auto start = high_resolution_clock::now();
// Code to measure
std::cout << "Working..." << std::endl;
std::this_thread::sleep_for(milliseconds(1500));
// Calculate elapsed time
auto end = high_resolution_clock::now();
auto duration = duration_cast<milliseconds>(end - start);
std::cout << "Elapsed time: " << duration.count() << " ms" << std::endl;
// Can also get in different units
auto microsecs = duration_cast<microseconds>(end - start);
std::cout << "Or: " << microsecs.count() << " microseconds" << std::endl;
return 0;
}
high_resolution_clock provides the most precise timing available on your system. We take timestamps
at start and end, then subtract to get a duration. duration_cast converts between time units -
essential because the raw duration may be in nanoseconds. The .count() method extracts the numeric
value for printing. Use steady_clock instead if you need guaranteed monotonic time (never goes
backward), which is important for benchmarking and timeouts.
Duration Arithmetic
#include <iostream>
#include <chrono>
int main() {
using namespace std::chrono;
using namespace std::chrono_literals; // For h, min, s, ms literals (C++14)
// Create durations using literals
auto flight = 2h + 30min;
auto layover = 45min;
auto total = flight + layover;
// Convert and display
std::cout << "Flight: " << duration_cast<minutes>(flight).count() << " min\n";
std::cout << "Layover: " << layover.count() << " min\n";
std::cout << "Total: " << duration_cast<minutes>(total).count() << " min\n";
// Comparison
auto shortTrip = 90min;
if (total > shortTrip) {
std::cout << "This is a long trip!" << std::endl;
}
// Create from numbers
seconds secs(3661); // 1 hour, 1 minute, 1 second
auto hrs = duration_cast<hours>(secs);
auto mins = duration_cast<minutes>(secs) % 60;
auto remaining = secs % 60;
std::cout << secs.count() << " seconds = "
<< hrs.count() << "h "
<< mins.count() << "m "
<< remaining.count() << "s" << std::endl;
return 0;
}
h, min, s, ms,
us, ns. You can add, subtract, compare, and multiply durations naturally. The modulo
operator (%) is useful for breaking down time into components. Chrono handles unit conversions automatically
when possible (minutes to seconds), but requires explicit duration_cast for lossy conversions
(seconds to minutes truncates). This type safety prevents bugs from unit mismatches.
Traditional C Time Functions
Getting Current Date and Time
#include <iostream>
#include <ctime>
int main() {
// Get current time as time_t
time_t now = time(nullptr);
// Convert to local time struct
tm* localTime = localtime(&now);
// Access individual components
std::cout << "Current date and time:\n";
std::cout << "Year: " << (1900 + localTime->tm_year) << std::endl;
std::cout << "Month: " << (1 + localTime->tm_mon) << std::endl;
std::cout << "Day: " << localTime->tm_mday << std::endl;
std::cout << "Hour: " << localTime->tm_hour << std::endl;
std::cout << "Minute: " << localTime->tm_min << std::endl;
std::cout << "Second: " << localTime->tm_sec << std::endl;
std::cout << "Day of week: " << localTime->tm_wday << " (0=Sun)\n";
std::cout << "Day of year: " << localTime->tm_yday << std::endl;
// Quick formatted output
std::cout << "\nFormatted: " << ctime(&now); // Includes newline
return 0;
}
time() returns seconds since January 1, 1970 (Unix epoch) as a time_t value.
localtime() converts this to a tm struct with broken-down time components in your
local timezone. Note the quirks: year is years since 1900, month is 0-based (0=January), but day is 1-based.
The ctime() function provides quick formatting but includes a trailing newline. For UTC time instead
of local time, use gmtime() instead of localtime().
Custom Date Formatting with strftime
#include <iostream>
#include <ctime>
int main() {
time_t now = time(nullptr);
tm* localTime = localtime(&now);
char buffer[100];
// ISO 8601 format
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", localTime);
std::cout << "ISO format: " << buffer << std::endl;
// US format
strftime(buffer, sizeof(buffer), "%m/%d/%Y %I:%M %p", localTime);
std::cout << "US format: " << buffer << std::endl;
// Full date with day name
strftime(buffer, sizeof(buffer), "%A, %B %d, %Y", localTime);
std::cout << "Full date: " << buffer << std::endl;
// Log timestamp
strftime(buffer, sizeof(buffer), "[%Y-%m-%d %H:%M:%S]", localTime);
std::cout << buffer << " Application started" << std::endl;
// Useful specifiers:
// %Y=year(4), %y=year(2), %m=month, %d=day, %H=hour24, %I=hour12
// %M=minute, %S=second, %p=AM/PM, %A=weekday, %B=month name
return 0;
}
strftime() formats time into a string using format specifiers similar to printf.
Common specifiers: %Y (4-digit year), %m (month 01-12), %d (day 01-31),
%H (24-hour), %I (12-hour), %M (minute), %S (second),
%p (AM/PM). For logging, ISO 8601 format (%Y-%m-%d %H:%M:%S) is recommended because
it sorts correctly as text and is unambiguous internationally. Always ensure your buffer is large enough.
Bridging Chrono and ctime
#include <iostream>
#include <chrono>
#include <ctime>
#include <iomanip>
int main() {
using namespace std::chrono;
// Get current time using chrono
auto now = system_clock::now();
// Convert chrono time_point to time_t for formatting
time_t now_t = system_clock::to_time_t(now);
// Format using ctime functions
std::cout << "Current time: " << std::put_time(localtime(&now_t), "%F %T") << std::endl;
// Create a specific date and convert to time_point
tm date = {};
date.tm_year = 2024 - 1900; // Years since 1900
date.tm_mon = 11; // December (0-based)
date.tm_mday = 25;
date.tm_hour = 12;
time_t target_t = mktime(&date);
auto target = system_clock::from_time_t(target_t);
// Calculate duration until target
auto diff = target - now;
auto days = duration_cast<hours>(diff).count() / 24;
std::cout << "Days until target: " << days << std::endl;
return 0;
}
system_clock connects chrono to calendar time. Use to_time_t() to convert a chrono
time_point to time_t for formatting with ctime functions. Use from_time_t()
to go the other direction. mktime() converts a tm struct to time_t,
useful for creating specific dates. The std::put_time manipulator (from <iomanip>)
provides a more C++-style way to format times with streams. %F is shorthand for %Y-%m-%d
and %T for %H:%M:%S.
Practice: Date and Time
Task: Create a stopwatch that starts when the user presses Enter, and stops when they press Enter again. Display elapsed time in seconds with 2 decimal places.
Expected output:
Press Enter to start...
Press Enter to stop...
Elapsed time: 3.24 seconds
Hint: Use high_resolution_clock::now() before and after waiting for input.
Show Solution
#include <iostream>
#include <chrono>
#include <iomanip>
int main() {
using namespace std::chrono;
std::cout << "Press Enter to start..." << std::endl;
std::cin.get();
auto start = high_resolution_clock::now();
std::cout << "Press Enter to stop..." << std::endl;
std::cin.get();
auto end = high_resolution_clock::now();
duration<double> elapsed = end - start;
std::cout << std::fixed << std::setprecision(2);
std::cout << "Elapsed time: " << elapsed.count() << " seconds" << std::endl;
return 0;
}
Task: Write a program that asks for a birthdate (year, month, day) and calculates the user's age in years, months, and days.
Expected output:
Enter birth year: 1995
Enter birth month (1-12): 6
Enter birth day: 15
You are 29 years, 4 months, and 10 days old.
Hint: Use mktime() to create time_t values, then calculate differences. Handle month/day borrowing carefully.
Show Solution
#include <iostream>
#include <ctime>
int main() {
int byear, bmonth, bday;
std::cout << "Enter birth year: ";
std::cin >> byear;
std::cout << "Enter birth month (1-12): ";
std::cin >> bmonth;
std::cout << "Enter birth day: ";
std::cin >> bday;
// Get current date
time_t now = time(nullptr);
tm* current = localtime(&now);
int cyear = current->tm_year + 1900;
int cmonth = current->tm_mon + 1;
int cday = current->tm_mday;
// Calculate age
int years = cyear - byear;
int months = cmonth - bmonth;
int days = cday - bday;
// Adjust for negative days
if (days < 0) {
months--;
days += 30; // Approximate
}
// Adjust for negative months
if (months < 0) {
years--;
months += 12;
}
std::cout << "You are " << years << " years, "
<< months << " months, and "
<< days << " days old." << std::endl;
return 0;
}
Task: Create a template function measureTime that takes any callable and its arguments, executes it, returns the result, and prints the execution time. Test with a function that calculates factorial.
Expected output:
factorial(20) = 2432902008176640000
Execution time: 0.002 ms
Hint: Use std::invoke or perfect forwarding with variadic templates.
Show Solution
#include <iostream>
#include <chrono>
#include <iomanip>
#include <functional>
template<typename Func, typename... Args>
auto measureTime(Func func, Args&&... args) {
using namespace std::chrono;
auto start = high_resolution_clock::now();
auto result = func(std::forward<Args>(args)...);
auto end = high_resolution_clock::now();
auto duration = duration_cast<microseconds>(end - start);
std::cout << std::fixed << std::setprecision(3);
std::cout << "Execution time: " << duration.count() / 1000.0 << " ms" << std::endl;
return result;
}
long long factorial(int n) {
long long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
int main() {
int n = 20;
auto result = measureTime(factorial, n);
std::cout << "factorial(" << n << ") = " << result << std::endl;
return 0;
}
Interactive Demo: Duration Calculator
Calculate time differences between durations:
Key Takeaways
Command-Line Arguments
Use argc and argv to accept user input at program launch, enabling flexible and configurable applications.
Environment Variables
Access system settings with getenv() to configure programs based on user environment without hardcoding values.
Process Control
Use system() for simple commands and exit() with proper codes to communicate program status.
Signal Handling
Register signal handlers with signal() to respond gracefully to interrupts and termination requests.
Modern Time Handling
Use <chrono> for precise time measurements and durations, with type-safe time point calculations.
Calendar Operations
Use <ctime> functions for human-readable date/time formatting and calendar-based calculations.
Knowledge Check
Quick Quiz
Test what you've learned about C++ system programming