Module 8.3

C++ System Programming

Learn to interact with the operating system through C++. Master environment variables, command-line arguments, process control, signal handling, and modern date/time operations for building robust system-level applications.

35 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Command-line arguments (argc/argv)
  • Environment variables access
  • Process control and system calls
  • Signal handling basics
  • Date/time with chrono library
Contents
01

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;
}
When you run this program with ./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;
}
Always validate argc before accessing argv elements to prevent undefined behavior from accessing invalid array indices. The usage message includes argv[0] so users see the actual program name they typed. Returning a non-zero exit code (like 1) signals to the operating system and calling scripts that the program failed. This pattern of validation, usage message, and proper exit codes is standard practice for command-line tools and makes your programs behave like professional Unix utilities.

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;
}
Command-line arguments are always strings, so you must convert them to numbers when needed. The old C-style functions 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;
}
Converting argv to a 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;
We include necessary headers and declare variables to store flag states. Boolean variables track whether flags like -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);
Create a vector starting from 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;
        }
    }
Iterate through each argument. If it matches a flag pattern (like -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 the help flag was set, display a usage message showing the program syntax and available options, then exit immediately. This allows users to learn how to use the program without performing any other operations.
    if (verbose) {
        std::cout << "Verbose mode enabled" << std::endl;
    }
    
    if (!filename.empty()) {
        std::cout << "Processing file: " << filename << std::endl;
    }
    
    return 0;
}
Execute different logic based on which flags were set. If verbose mode is enabled, provide extra output. If a filename was provided, process it. This separation of parsing (reading flags) from processing (acting on flags) makes the code clearer and more maintainable.
Complete Pattern: This manual flag parsing works well for simple programs with a few options. For complex applications with many flags, flag arguments (like --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;
}
02

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;
}
The 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;
}
Creating a wrapper function like 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;
}
This cross-platform function handles the differences between Unix (HOME) and Windows (USERPROFILE or HOMEDRIVE+HOMEPATH) home directory conventions. Writing platform-aware code like this ensures your application works correctly regardless of the operating system. The function tries multiple approaches in order of preference and returns an empty string only if all methods fail. For production code, consider using a cross-platform library like Boost.Filesystem or C++17's std::filesystem for more robust path handling.

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;
}
This pattern demonstrates the "12-factor app" methodology where configuration is loaded from environment variables. It's widely used in containerized applications (Docker, Kubernetes) where you set environment variables at deployment time. Each setting has a sensible default, making the application work out of the box while remaining configurable. The boolean DEBUG variable shows how to handle flags - comparing against the string "true" rather than just checking for existence. This approach separates configuration from code and follows modern DevOps best practices.

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;
}
03

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 codes communicate program status to the calling process (shell, script, or parent program). By convention, 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
}
The 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;
}
The 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;
}
Before running an external command, it's good practice to check if it exists. The "which" command (Unix) or "where" command (Windows) returns success (0) if the program is found in PATH. Redirecting output to /dev/null (Unix) or nul (Windows) suppresses the actual path output, leaving only the exit code for checking. This pattern is useful for programs that have optional integrations with external tools - you can gracefully fall back when tools are missing.

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;
}
Security Critical: Never pass unsanitized user input to 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;
    }
}
04

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;
}
The 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;
}
This pattern is standard for long-running programs like servers. Instead of terminating immediately in the signal handler, we set an atomic flag that the main loop checks. This allows the program to finish its current work, close connections, save state, and shut down cleanly. Using 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;
}
05

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;
}
C++14 introduced user-defined literals for time: 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:

h m
h m
06

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

1 What does argc represent in int main(int argc, char* argv[])?
2 Which function is used to read an environment variable in C++?
3 What signal is sent when the user presses Ctrl+C?
4 Which header provides the modern C++ time utilities like chrono::seconds?
5 What does exit(0) indicate to the operating system?
6 What is the value of argv[0] when running ./myprogram hello?
0 of 6 answered