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:
args[0] = ./cmdargs
args[1] = hello
args[2] = world
args[3] = 123
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:
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]);
}
}
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:
- Automatically allocates memory when objects are created
- Tracks which memory is still in use (reachable)
- Automatically frees memory that is no longer needed
- Eliminates common bugs like memory leaks and dangling pointers
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");
}
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:
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
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.");
}
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:
- 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
- Read and parse the CSV file where each line has the format:
StudentName,Score1,Score2,Score3
Example:Alice,85,92,78 - Store the data in arrays:
- A
string[]array for student names - A
double[]array for each student's average score
- A
- Calculate and display:
- Each student's name and average score
- The class average (average of all student averages)
- The highest and lowest scoring students
- 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)
Bob,90,88,95
Charlie,72,68,75
Diana,95,98,92
Sample Output
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()fromstd.convto 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
.idupproperty may be needed to convertchar[]tostring
Submission
Save your program as grade_analyzer.d and compile with:
gdc grade_analyzer.d -o grade_analyzer
./grade_analyzer grades.csv