D Programming: Files, Arrays, and Memory Management

Andrew J. Pounds, Ph.D.

Learning Objectives

After completing this exercise, you will be able to:

  • Access and process command-line arguments in D programs
  • Read data from files using D's file I/O facilities
  • Tokenize strings using the split() function
  • Store and access elements in dynamic arrays
  • Understand D's garbage collection mechanism and memory management

1 Command-Line Arguments

Command-line arguments allow users to pass information to a program when it is executed. In D, command-line arguments are received through the main() function's parameter.

Basic Command-Line Argument Access

Unlike C, where main() receives argc and argv separately, D provides a single string array containing all arguments:

import std.stdio;

void main(string[] args) {
    writeln("Number of arguments: ", args.length);
    
    foreach (i, arg; args) {
        writefln("args[%d] = %s", i, arg);
    }
}

Compile and run with arguments:

gdc cmdargs.d -o cmdargs
./cmdargs hello world 123

Output:

Number of arguments: 4
args[0] = ./cmdargs
args[1] = hello
args[2] = world
args[3] = 123
Note: The first element args[0] is always the program name itself. User-provided arguments start at args[1].

Processing Command-Line Arguments

Here's an example that expects a filename as a command-line argument:

import std.stdio;
import std.conv;

void main(string[] args) {
    // Check if correct number of arguments provided
    if (args.length < 2) {
        writeln("Usage: ", args[0], " <filename>");
        return;
    }
    
    string filename = args[1];
    writeln("Processing file: ", filename);
    
    // If a second argument is provided, treat it as a number
    if (args.length >= 3) {
        int count = to!int(args[2]);
        writeln("Count parameter: ", count);
    }
}

2 Reading from Files

D provides several ways to read from files. The std.stdio module includes the File type for file operations.

Opening and Reading a File

The most straightforward approach uses the File struct:

import std.stdio;

void main(string[] args) {
    if (args.length < 2) {
        writeln("Usage: ", args[0], " <filename>");
        return;
    }
    
    // Open file for reading
    auto file = File(args[1], "r");
    
    // Read line by line
    foreach (line; file.byLine()) {
        writeln(line);
    }
}

File Open Modes

Mode Description
"r" Open for reading (file must exist)
"w" Open for writing (creates new or truncates existing)
"a" Open for appending (writes to end of file)
"r+" Open for reading and writing

Reading the Entire File at Once

For smaller files, you can read the entire content into a string:

import std.stdio;
import std.file;

void main(string[] args) {
    if (args.length < 2) {
        writeln("Usage: ", args[0], " <filename>");
        return;
    }
    
    // Read entire file into a string
    string content = readText(args[1]);
    writeln("File contents:");
    writeln(content);
}

Error Handling with File Operations

It's good practice to handle potential file errors:

import std.stdio;
import std.file;

void main(string[] args) {
    if (args.length < 2) {
        writeln("Usage: ", args[0], " <filename>");
        return;
    }
    
    string filename = args[1];
    
    // Check if file exists before opening
    if (!exists(filename)) {
        writeln("Error: File '", filename, "' not found.");
        return;
    }
    
    auto file = File(filename, "r");
    foreach (line; file.byLine()) {
        writeln(line);
    }
}

3 Tokenizing Strings

Tokenization is the process of breaking a string into smaller pieces (tokens) based on delimiters. D's std.array and std.string modules provide powerful tools for this.

Basic Tokenization with split()

The split() function divides a string into an array of substrings:

import std.stdio;
import std.array;
import std.string;

void main() {
    string sentence = "The quick brown fox jumps";
    
    // Split on whitespace (default)
    auto words = sentence.split();
    
    writeln("Number of tokens: ", words.length);
    foreach (word; words) {
        writeln("Token: ", word);
    }
}

Output:

Number of tokens: 5
Token: The
Token: quick
Token: brown
Token: fox
Token: jumps

Splitting on Custom Delimiters

You can specify a delimiter character or string:

import std.stdio;
import std.array;
import std.string;

void main() {
    // Split CSV data on commas
    string csvLine = "Alice,25,Computer Science,3.85";
    auto fields = csvLine.split(",");
    
    writeln("Name: ", fields[0]);
    writeln("Age: ", fields[1]);
    writeln("Major: ", fields[2]);
    writeln("GPA: ", fields[3]);
    
    // Split on colon
    string time = "14:30:45";
    auto parts = time.split(":");
    writeln("\nHour: ", parts[0], ", Minute: ", parts[1], ", Second: ", parts[2]);
}

Tokenizing File Contents

Combining file reading with tokenization:

import std.stdio;
import std.file;
import std.array;
import std.string;

void main(string[] args) {
    if (args.length < 2) {
        writeln("Usage: ", args[0], " <filename>");
        return;
    }
    
    // Read entire file
    string content = readText(args[1]);
    
    // Tokenize on whitespace
    auto tokens = content.split();
    
    writeln("Total tokens in file: ", tokens.length);
    writeln("\nFirst 10 tokens:");
    
    for (int i = 0; i < 10 && i < tokens.length; i++) {
        writefln("  [%d]: %s", i, tokens[i]);
    }
}
Tip: The split() function with no arguments splits on all whitespace (spaces, tabs, newlines) and automatically removes empty tokens caused by consecutive whitespace.

4 Arrays in D

D provides powerful dynamic arrays that can grow and shrink as needed. Unlike C arrays, D arrays know their own length and support many convenient operations.

Declaring and Initializing Arrays

import std.stdio;

void main() {
    // Static array (fixed size)
    int[5] staticArr = [1, 2, 3, 4, 5];
    
    // Dynamic array (can grow/shrink)
    int[] dynamicArr = [10, 20, 30];
    
    // Empty dynamic array
    string[] words;
    
    // Using auto for type inference
    auto numbers = [1, 2, 3, 4, 5];
    
    writeln("Static array length: ", staticArr.length);
    writeln("Dynamic array length: ", dynamicArr.length);
}

Accessing Array Elements

import std.stdio;

void main() {
    string[] fruits = ["apple", "banana", "cherry", "date"];
    
    // Access by index (0-based)
    writeln("First fruit: ", fruits[0]);
    writeln("Third fruit: ", fruits[2]);
    
    // Access last element using $ (length shorthand)
    writeln("Last fruit: ", fruits[$-1]);
    
    // Modify an element
    fruits[1] = "blueberry";
    
    // Iterate with foreach
    writeln("\nAll fruits:");
    foreach (fruit; fruits) {
        writeln("  ", fruit);
    }
    
    // Iterate with index
    writeln("\nWith indices:");
    foreach (i, fruit; fruits) {
        writefln("  fruits[%d] = %s", i, fruit);
    }
}

Dynamic Array Operations

import std.stdio;

void main() {
    int[] numbers;
    
    // Append elements with ~=
    numbers ~= 10;
    numbers ~= 20;
    numbers ~= 30;
    writeln("After appending: ", numbers);
    
    // Append multiple elements
    numbers ~= [40, 50];
    writeln("After appending array: ", numbers);
    
    // Concatenate arrays with ~
    auto moreNumbers = numbers ~ [60, 70];
    writeln("Concatenated: ", moreNumbers);
    
    // Array slicing
    writeln("Slice [1..4]: ", numbers[1..4]);
    writeln("From index 2: ", numbers[2..$]);
    writeln("First 3: ", numbers[0..3]);
    
    // Get array length
    writeln("Length: ", numbers.length);
}

Storing Tokens in an Array

A complete example that reads a file, tokenizes it, and stores tokens in an array:

import std.stdio;
import std.file;
import std.array;
import std.string;

void main(string[] args) {
    if (args.length < 2) {
        writeln("Usage: ", args[0], " <filename>");
        return;
    }
    
    if (!exists(args[1])) {
        writeln("Error: File not found.");
        return;
    }
    
    // Read and tokenize
    string content = readText(args[1]);
    string[] tokens = content.split();
    
    // Display statistics
    writeln("=== File Token Analysis ===");
    writeln("Total tokens: ", tokens.length);
    
    // Access specific elements
    if (tokens.length > 0) {
        writeln("First token: ", tokens[0]);
        writeln("Last token: ", tokens[$-1]);
    }
    
    // Process tokens with a loop
    writeln("\nTokens longer than 5 characters:");
    foreach (i, token; tokens) {
        if (token.length > 5) {
            writefln("  [%d]: %s (length: %d)", i, token, token.length);
        }
    }
}

5 Garbage Collection in D

One of D's most important features is its built-in garbage collector (GC). Unlike C and C++, where programmers must manually allocate and free memory, D automatically manages memory through garbage collection.

What is Garbage Collection?

Garbage collection is an automatic memory management feature that:

Automatic Memory Management

In D, dynamic arrays and class objects are automatically managed by the GC:

import std.stdio;

void createArrays() {
    // Memory is allocated automatically
    int[] arr1 = new int[1000];
    int[] arr2 = [1, 2, 3, 4, 5];
    
    writeln("Arrays created inside function");
    writeln("arr1 length: ", arr1.length);
    writeln("arr2 length: ", arr2.length);
    
    // When this function returns, arr1 and arr2 go out of scope
    // The GC will automatically free the memory when needed
}

void main() {
    writeln("=== Garbage Collection Demo ===");
    
    for (int i = 0; i < 5; i++) {
        writeln("\nIteration ", i + 1);
        createArrays();
        // Memory from previous calls is eligible for collection
    }
    
    writeln("\nProgram completed - all memory cleaned up automatically");
}
Key Point: In languages like C, the programmer would need to call malloc() to allocate the arrays and free() to release them. Forgetting to call free() causes memory leaks. D's GC handles this automatically.

Demonstrating GC with Classes

Classes in D are reference types that are managed by the garbage collector:

import std.stdio;

class DataProcessor {
    string name;
    int[] data;
    
    this(string n, int size) {
        name = n;
        data = new int[size];
        writeln("  Created: ", name, " with ", size, " elements");
    }
    
    ~this() {
        // Destructor - called by GC when object is collected
        writeln("  Destroyed: ", name);
    }
}

void processData() {
    // Create objects - memory allocated automatically
    auto proc1 = new DataProcessor("Processor-A", 100);
    auto proc2 = new DataProcessor("Processor-B", 200);
    
    writeln("  Working with processors...");
    
    // Objects become eligible for GC when function returns
}

void main() {
    writeln("=== GC with Classes Demo ===\n");
    
    writeln("Calling processData():");
    processData();
    
    writeln("\nBack in main - objects eligible for collection");
    
    // Force garbage collection to demonstrate cleanup
    import core.memory;
    GC.collect();
    
    writeln("\nAfter GC.collect() - memory has been freed");
}

Output:

=== GC with Classes Demo ===

Calling processData():
  Created: Processor-A with 100 elements
  Created: Processor-B with 200 elements
  Working with processors...

Back in main - objects eligible for collection
  Destroyed: Processor-B
  Destroyed: Processor-A

After GC.collect() - memory has been freed
Understanding import core.memory: The core.memory module is part of D's runtime library (druntime), which is separate from the standard library (Phobos). It provides low-level access to the garbage collector itself. While std.stdio and other std.* modules provide high-level functionality, core.memory gives you direct control over GC operations like GC.collect() (force a collection cycle), GC.disable() (temporarily disable GC), and GC.enable(). You typically don't need this module in normal programs—D's GC runs automatically. We import it here only to demonstrate when garbage collection occurs by forcing it with GC.collect().

GC vs Manual Memory Management

Feature Garbage Collection (D) Manual Management (C/C++)
Memory Allocation Automatic with new Manual with malloc()
Memory Deallocation Automatic by GC Manual with free()
Memory Leaks Prevented automatically Common if free() forgotten
Dangling Pointers Prevented automatically Possible after free()
Programmer Effort Low High
Performance Small GC overhead No GC overhead

Practical GC Example: Processing Large Data

import std.stdio;
import std.file;
import std.array;
import std.string;
import core.memory;

void analyzeFile(string filename) {
    writeln("Analyzing: ", filename);
    
    // Read file - memory allocated automatically
    string content = readText(filename);
    
    // Tokenize - new array allocated automatically
    string[] tokens = content.split();
    
    writeln("  Tokens found: ", tokens.length);
    
    // Build another data structure - more allocation
    string[] longWords;
    foreach (token; tokens) {
        if (token.length > 6) {
            longWords ~= token;
        }
    }
    
    writeln("  Long words: ", longWords.length);
    
    // When function returns:
    // - content string becomes unreachable
    // - tokens array becomes unreachable
    // - longWords array becomes unreachable
    // GC will reclaim all this memory automatically!
}

void main(string[] args) {
    if (args.length < 2) {
        writeln("Usage: ", args[0], " <file1> [file2] [file3] ...");
        return;
    }
    
    writeln("=== Multi-File Analyzer with GC ===\n");
    
    // Process multiple files
    foreach (i, filename; args[1..$]) {
        if (exists(filename)) {
            analyzeFile(filename);
            writeln();
        } else {
            writeln("Skipping (not found): ", filename);
        }
    }
    
    // Optional: trigger GC to see memory cleanup
    writeln("Running garbage collection...");
    GC.collect();
    writeln("Done! All temporary memory has been reclaimed.");
}
Important: While D's GC handles most memory management automatically, you should avoid creating unnecessary temporary objects in tight loops, as this can trigger frequent garbage collection cycles and impact performance.

6 Complete Example: Word Frequency Analyzer

This comprehensive example combines all the concepts: command-line arguments, file reading, tokenization, arrays, and demonstrates D's automatic memory management:

import std.stdio;
import std.file;
import std.array;
import std.string;
import std.algorithm;
import std.uni;

void main(string[] args) {
    // Check command-line arguments
    if (args.length < 2) {
        writeln("Word Frequency Analyzer");
        writeln("Usage: ", args[0], " <filename>");
        return;
    }
    
    string filename = args[1];
    
    // Check if file exists
    if (!exists(filename)) {
        writeln("Error: File '", filename, "' not found.");
        return;
    }
    
    // Read file contents (memory managed by GC)
    string content = readText(filename);
    
    // Convert to lowercase for case-insensitive counting
    content = content.toLower();
    
    // Tokenize the content
    string[] tokens = content.split();
    
    // Store unique words in an array
    string[] uniqueWords;
    int[] wordCounts;
    
    // Count word frequencies
    foreach (token; tokens) {
        // Remove common punctuation
        string word = token.strip(".,!?;:\"'()-");
        
        if (word.length == 0) continue;
        
        // Check if word already exists
        bool found = false;
        for (int i = 0; i < uniqueWords.length; i++) {
            if (uniqueWords[i] == word) {
                wordCounts[i]++;
                found = true;
                break;
            }
        }
        
        // Add new word if not found
        if (!found) {
            uniqueWords ~= word;
            wordCounts ~= 1;
        }
    }
    
    // Display results
    writeln("=== Word Frequency Analysis ===");
    writeln("File: ", filename);
    writeln("Total tokens: ", tokens.length);
    writeln("Unique words: ", uniqueWords.length);
    writeln();
    
    // Display words with count > 1
    writeln("Words appearing more than once:");
    writeln("--------------------------------");
    
    for (int i = 0; i < uniqueWords.length; i++) {
        if (wordCounts[i] > 1) {
            writefln("  %-15s : %d", uniqueWords[i], wordCounts[i]);
        }
    }
    
    // Find most common word
    int maxCount = 0;
    string mostCommon = "";
    for (int i = 0; i < uniqueWords.length; i++) {
        if (wordCounts[i] > maxCount) {
            maxCount = wordCounts[i];
            mostCommon = uniqueWords[i];
        }
    }
    
    writeln();
    writeln("Most common word: '", mostCommon, "' (", maxCount, " occurrences)");
}

7 Quick Reference

Task D Syntax
Access command-line args void main(string[] args)
Get argument count args.length
Open file for reading auto file = File(filename, "r");
Read entire file string content = readText(filename);
Check if file exists if (exists(filename))
Split on whitespace auto tokens = str.split();
Split on delimiter auto parts = str.split(",");
Declare dynamic array int[] arr;
Append to array arr ~= value;
Access last element arr[$-1]
Array slice arr[start..end]
Force garbage collection import core.memory; GC.collect();

📝 Programming Exercise: CSV Data Processor

Write a D program that processes a CSV (Comma-Separated Values) file containing student grade data. Your program should:

  1. Accept the filename as a command-line argument
    • Display a usage message if no filename is provided
    • Display an error if the file doesn't exist
  2. Read and parse the CSV file where each line has the format:
    StudentName,Score1,Score2,Score3
    Example: Alice,85,92,78
  3. Store the data in arrays:
    • A string[] array for student names
    • A double[] array for each student's average score
  4. Calculate and display:
    • Each student's name and average score
    • The class average (average of all student averages)
    • The highest and lowest scoring students
  5. Demonstrate understanding of garbage collection by adding a comment in your code explaining where the GC will automatically free memory

Sample Input File (grades.csv)

Alice,85,92,78
Bob,90,88,95
Charlie,72,68,75
Diana,95,98,92

Sample Output

=== Student Grade Analyzer ===
File: grades.csv
Students loaded: 4

Student Averages:
-----------------
  Alice          : 85.00
  Bob            : 91.00
  Charlie        : 71.67
  Diana          : 95.00

Class Statistics:
-----------------
  Class Average  : 85.67
  Highest Score  : Diana (95.00)
  Lowest Score   : Charlie (71.67)

Hints

  • Use split(",") to parse each CSV line
  • Use to!double() from std.conv to convert score strings to numbers
  • Remember that the first token in each line is the name (a string), while the rest are scores (numbers)
  • Use byLine() to read the file line by line
  • The .idup property may be needed to convert char[] to string

Submission

Save your program as grade_analyzer.d and compile with:

gdc grade_analyzer.d -o grade_analyzer
./grade_analyzer grades.csv