Programming C, C++, Java, PHP, Ruby, Turing, VB
Computer Science Canada 
Programming C, C++, Java, PHP, Ruby, Turing, VB  

Username:   Password: 
 RegisterRegister   
 D: A Newbie-Oriented Tutorial
Index -> Programming, C++ -> C++ Tutorials
View previous topic Printable versionDownload TopicRate TopicSubscribe to this topicPrivate MessagesRefresh page View next topic
Author Message
wtd




PostPosted: Wed Jul 20, 2005 7:29 pm   Post subject: D: A Newbie-Oriented Tutorial

A. Guidelines


  • Do not advance beyond any section unless you completely understand it.
  • Use meaningful names for things. Code should be self-documenting as much as possible.


B. It's been my observation...

Of late I've noticed a number of people interested in learning C++. This is probably most commonly attributed to a desire to have a running head start when taking a programming class which uses C++.

This plan fails, in my estimation, on a few critical points.


  • Learning by yourself, without the benefit of considerable experience or a driving passion for the subject, is tedious, and comes with very little support.

    In a classroom environment you'll have teachers and fellow students to assist you. Further, these students will be doing exactly what you are, so there will be far fewer communication hurdles than there are in soliciting help from a forum on the web.
  • C++ is complex, and that complexity is harnessed for even the simplest tasks. Sometimes this makes those tasks easierin the long-run, but it means that you must tackle complex concepts to understand even a simple "Hello world" program.


I've also noted that many students seem quite preoccupied with syntax, and are often therefore resistant to languages myself and others can suggest which do not bear a syntactic similarity to the C-like syntax family (to which C++ belongs).

C. So, what to do?

Certainly Java is an option. Java, however, lacks a lot of the functionality present in C++, uses different syntax in many cases, and behaves rather differently in general.

Java also imposes a rather high mental overhead on programmers trying to learn it. Object-oriented programming can't be left for later, for instance, since knowledge of it is essential for even the simplest programs.

C would fit, except that it's just too low-level.

There is another good language that fits for the task, though it's less well known. The D programming language designed by Digital Mars is a good alternative that may very well be easier to learn than C++, while giving a familiarity with the general "feel" of C-like syntax.

D. Getting a Compiler

A free D compiler is available from:

http://digitalmars.com/d/dcompiler.html

Instructions are also available there on how to install it on either Windows or Linux. The Linux installation is unfortunately a bit difficult, but it can be done. If you need help, ask.

Compiling simple programs is reasonably easy. Assuming you have a source file named "myprogram.d", you can compile with either:

code:
prompt> dmd myprogram


Or:

code:
prompt> dmd myprogram.d


This will generate an executable named "myprogram" on Linux or "myprogram.exe" on Windows, unless there is an error, which it will show you information about.

Compiling with the "-w" flag will show any warnings. These are not errors which will prevent compilation, but they are potential problems. Compiling with this flag is a matter of:

code:
prompt> dmd myprogram.d -w


1. Everything has a beginning

Let's dive right in by looking at the simplest possible D program.

code:
void main()
{

}


Every program has a "main" function which serves as the entry point. The program begins execution at the beginning of the function, and when execution reaches the end of the main function, the program exits.

2. Functions

So, now that I've demonstrated a function, I should probably explain what a function is.

Functions are the means by which all executable code in a program is organized. Data is organized in other ways, but for now we'll deal with executable code and simple data.

A function has four major components.


  • A name. Every function has some name. In the case of the main function, that name is "main".
  • Every function has a return type. This indicates the type of data the function returns to the program. In the main function we've seen, that return type is "void". This means that the function return nothing to the program.

    In the case of the main function, it should be noted that it can also return an integer, and in this case, that integer is returned to the operating system as an indicator of whether or not the program succeeded. If we return nothing, the OS assumes the program finished successfully.
  • Every function has a body of code that is executed when the function is called. This code comes between the curly braces.
  • Ever function has a list of parameters. Parameters are values passed to the function which can then be used within that function to alter its behavior.

    These parameters each have a type and a name, fall between the parentheses in a function declaration, and as in the main function we've seen, there may in fact be no parameters for a function.


Think of a function as the classic "black box". It has inputs in the form of parameters, and its return value is the output. What happens inside the function is impossible and unnecessary to know.

All that matters is that the function does what it says it will.

Let's look at a simple function of our own devising. This function will take an integer as a parameter and square it, returning the result.

First we write what we can think of as the interface to the function: its name, return type, and parameters.

code:
int square(int number)
{

}


Now we need to be able to square a number. The simplest way to accomplish this is by multiplying it by itself.

code:
number * number


At this point all we lack is the ability to return that result out of the function. For this we naturally se the "return" statement.

code:
int square(int number)
{
   return number * number;
}


A semi-colon ends the statement.

Calling the function is quite simple. In this case we'll call it from within the main function, and do nothing with its return value, which is a valid course of action, though generally not advised.

code:
void main()
{
   square(42);
}

int square(int number)
{
   return number * number;
}


3. Statements and Expressions

In the previous section, I referred to "the return statement". The last of those three words is tremendously important.

A statement is anything in a programming language that does something to the environment, but returns no new value to the program.

To complement this, we have expressions, which do compute and return to the program new values. The program we created in the previous section contains two expressions.

code:
number * number


Is an expression. It multiples "number" by itself and returns the result to the program. In the process it does not change the number parameter.

code:
square(42)


Is also an expression. It takes the value 42, computes a new value, and returns that value to the program, which promptly ignores it, in this case.

The difference between statements and expressions is a fundamental programming concept.

4. Variables

As mentioned previously, the "square" function, when called forms an expression which returns a new value to the program. However, we ignore that return value.

There are two things we can do with that value, aside from ignoring it. We can either immediately use the value by passing it to another function, or we can store it in a variable.

Variables are essentially pieces of memory in which we can store values. Every variable has a name and a type of data which it can store.

All variables must be declared prior to use. This is accomplished by preceding the name with the type of the variable. To store the result in our previous program we might declare a variable as follows.

code:
void main()
{
   int result;

   square(42);
}

int square(int number)
{
   return number * number;
}


We can then easily assign a value to the variable, as long as the type of the value matches the type of the variable.

code:
void main()
{
   int result;

   result = square(42);
}

int square(int number)
{
   return number * number;
}


Fortunately we can both declare the variable and give it an initial value in one statement.

code:
void main()
{
   int result = square(42);
}

int square(int number)
{
   return number * number;
}


We can, of course, pass variables to functions.

code:
void main()
{
   int initialNumber = 42;
   int numberSquared = square(initialNumber);
}

int square(int number)
{
   return number * number;
}


5. Simple Output

Thus far the examples program haven't actually done anything apparent to the user. Let's change that by incorporating some simple output.

code:
import std.stdio;

void main()
{
   puts("Hello world!");
}


Here we start by importing the std.stdio module. This module contains, among other things, several useful functions for dealing with output. By importing it, we make these functions available in our program.

One such function is "puts", which is an abbreviation of "put string". A string is simply a series of individual characters.

The puts function simply prints a string to the standard output, and then moves to a new line. An alternative and popular function to do the same is "writefln".

code:
import std.stdio;

void main()
{
   writefln("Hello world!");
}


The "ln" in the function name indicates the fact that it skips to a new line. The "f" is an abbreviation for "format". Fomrat strings make it possible to output other data using formatting characters.

Let's revisit our previous program:

code:
void main()
{
   int initialNumber = 42;
   int numberSquared = square(initialNumber);
}

int square(int number)
{
   return number * number;
}


And let's add an output statement to that so that we see:

code:
42 squared is 1764


code:
void main()
{
   int initialNumber = 42;
   int numberSquared = square(initialNumber);

   writefln("%d squared is %d",
            initialNumber,
            numberSquared);
}

int square(int number)
{
   return number * number;
}


The "%d" indicates an integer. Other popular options are "%f" for a floating point number, "%c" for a character, and "%s" for a string.

Many other options are available, but will not be covered here yet.

6. Arrays

In the previous section I talked about strings, and said that they were a "series of individual characters". This is true, but more accurately a string is an array of character.

An array is an immensely useful way of organizing data. It stores several values of a single type in a block of memory. It also allows for random access to its contents. In other words, it's as easy and quick to access the five-hundredth element as it is to access the first.

Creating an array is as simple as appending "[]" to the type of the array. Declaring an array of integers, for example:

code:
int[] intArray;


An array is indexed by numbers starting with zero. To put a value into the array in the first position, I would:

code:
intArray[0] = 42;


Inserting a value at the five-hundredth space would then be:

code:
intArray[499] = 73;


You should note that I never specified how long the array should be. Instead I simply assigned values to various positions in the array and it just worked.

This is an example of a dynamic array. It will grow to whatever size is required. I can determine the size of an array at any time by accessing the array's length property. The length property can also have a value assigned to it to resize the array.

code:
import std.stdio;

void main()
{
   int[] intArray;

   intArray[0] = 42;
   intArray[1] = 54;
   intArray[2] = 91;

   writefln("The length of intArray is %d.",
            intArray.length);
}


If, however, I wish to create an array of a fixed size, I may do so.

code:
int[3] fixedSizeIntArray;


Now, if I attempt to insert a value into the array outside of the bounds I set, an error will occur. The following, for instance, will not work.

code:
fixedSizeIntArray[3] = 56;


Again, the length of a fixed size array can be determined by the length property. In this case, however, the length is read-only, and cannot be changed.

Fixed-size arrays can be conveniently initialized. To create a fixed-size array with 4 elements:

code:
static int[] fixedSizeIntArray = [56, 67, 42, 19];


Strings are simply an array of characters. Typically we deal with strings as dynamic arrays, but we can deal with strings as fixed size arrays.

Creating and initializing a simple string is as easy as:

code:
char[] myString = "Hello world";


We can then use that string in functions.

code:
import std.stdio;

void main()
{
   char[] myString = "Hello world";
   
   writefln(myString);
}


It's through the use of strings that we can see many of the interesting operations that are available for arrays.

For example, concatenating two arrays together into one array:

code:
char[] helloString = "Hello";
char[] worldString = "world";
char[] helloWorldString =
   helloString ~ ' ' ~ worldString;


Accessing a portion of a string, or any other type of array, is done using an array "slice". To grab the "llo" from "Hello":

code:
char[] subString = helloString[2 .. 5];


7. Associative Arrays

In the previous sections, I discussed dynamic arrays which resize as necessary. Thus I can have something like:

code:
int[] intArray;

intArray[0] = 42;
intArray[1001] = 98;


The potential problem with this is that the array created is not "sparse". In order to create an array with an index of 1001, it creates everything upto 1001 as well.

While dynamic arrays are quite valuable, for situations where the indexes are relatively small and sequential. But if the indexes are large and sparse, a more memory-efficient solution is to use an associative array.

An associative array, as the name indicates, associates one type of value with another. Conveniently, it can use just about any type of data as the key. Consider if we wish to have an array of strings, indexed by double-precision floating point numbers.

We would declare that array like so:

code:
char[][double] myAssociativeArrayOfStrings;


Then we can easily insert values into the array.

code:
myAssociativeArrayOfStrings[34.5] = "Hello";


And easily access the values.

code:
writefln(myAssociativeArrayOfStrings[34.5]);


Perhaps a less contrived example of the usefulness of this is keeping track of scores for a set of players in a game.

code:
int[char[]] scoresByPlayer;

scoresByPlayer["Chris"] = 43981;
scoresByPlayer["John"]  = 67215;


Of course, at this point you're probably wondering how we can even begin to know what keys are available at any given time. As a solution to this problem we have the keys property which returns a dynamic array of keys for the associative array.

As with other arrays, associative arrays also have a length property which will indicate the number of entries.

8. Iterating

Now that we've seen arrays which can contain many values, it should be fairly clear that it would be tedious to have to deal with each of those values separately, especially since most of the code is going to be repeated.

Fortunately there are a couple of ways to iterate over the contents of an array.

The classic "for" loop uses a counter variable. This variable has an initial value, a test, and an update. As long as the test holds true, the loop continues. The loop also has a body. This body code gets run on each loop. After it runs, the update code is run.

Let's start with an array of integers representing a set of scores in a game.

code:
static int[] scores = [5621, 9845, 1274];


We then write out the basic shell of a for loop.

code:
for ( ; ; )
{

}


Let's start out with a counter with an initial value of zero, since arrays are indexed starting with zero.

code:
for (int scoreIndex = 0; ; )
{

}


We then need a test. We know that the indexes of an array will always be one less than the length of the array.

code:
for (int scoreIndex = 0; scoreIndex < scores.length; )
{

}


And lastly we need an update. In this case, we simple increment the counter.

code:
for (int scoreIndex = 0; scoreIndex < scores.length; scoreIndex++)
{

}


Now we just need some code to run each time.

code:
for (int scoreIndex = 0; scoreIndex < scores.length; scoreIndex++)
{
   writefln("Score #%d is %d",
            scoreIndex,
            scores[scoreIndex]);
}


This allows us some considerable freedom in how we iterate over arrays. We could access every other score by changing the update code.

code:
for (int scoreIndex = 0; scoreIndex < scores.length; scoreIndex += 2)
{
   writefln("Score #%d is %d",
            scoreIndex,
            scores[scoreIndex]);
}


However, probably the most common way of iterating over an array involves accessing each element in order with no regard for the index. In this case, we're provided with a shortcut.

The "foreach" loop can be used to more easily access elements of an array.

code:
foreach (int score; scores)
{
   writefln("Score is %d", score);
}


Of course, we could still maintain an index.

code:
int scoreIndex = 0;

foreach (int score; scores)
{
   writefln("Score #%d is %d",
            scoreIndex,
            score);
   scoreIndex++;
}


The "for" loop style is often used when we want to not only access, but also modify values in an array. Consider if we wish to give ourselves a little ego boost and add 100 points to all of our scores.

code:
for (int scoreIndex = 0; scoreIndex < scores.length; scoreIndex++)
{
   scores[scoreIndex] += 100;
}


We can achieve this more easily with the "foreach" loop, though. We need only specify that the element in the array which we're accessing is "inout", or available both to access and to change. It can go "in", and come "out" of the loop with a new value.

code:
foreach (inout int score; scores)
{
   score += 100;
}


9. If Statements

At some point you're certain to wonder how we can choose one course of action or another. To accomplish this we use the "if" statement.

This involves a test, and if the test is true, a block of code is executed. If it's false, however, then an optional "else" block is executed.

Let's tie this into the example in the previous section and offer commentary on a set of grades.

code:
import std.stdio;

void main()
{
   static int[] scores = [5621, 9845, 1274];

   foreach (int score; scores)
   {
      if (score < 4000)
      {
         writefln("%d?  What a pathetic score.",
                  score);
      }
      else
      {
         writefln("%d...  Not bad.",
                  score);
      }
   }
}


Computers can be so cruel.

Of course, we can be a bit more specific than that.

code:
if (score < 4000)
{
   writefln("%d?  What a pathetic score.",
            score);
}
else if (score < 7500)
{
   writefln("%d...  Not bad.",
            score);
}
else
{
   writefln("%d!  Whoa... I'm not worthy.",
            score);
}


10. On Style and Brevity

In the previous section, I used curly braces extensively to indicate blocks. However, if there is only a single statement, the curly braces are optional.

The previous example could be rewritten as:

code:
import std.stdio;

void main()
{
   static int[] scores = [5621, 9845, 1274];

   foreach (int score; scores)
      if (score < 4000)
         writefln("%d?  What a pathetic score.", score);
      else if (score < 7500)
         writefln("%d...  Not bad.", score);
      else
         writefln("%d!  Whoa... I'm not worthy.", score);
}


This style should be used very sparingly.

11. Arrays and "main" and More on Looping

The main function we've seen thus far has no parameters. This is acceptable, but there is another acceptable form.

code:
void main(char[][] commandLineArguments)
{

}


This specifies an array of strings as the sole parameter to main. This array will contain any arguments passed to the program at the command-line. The first argument is always the name of the program itself, so the arguments really only begin with the second element of this array.

Let's look at a simple "Hello world" program.

code:
import std.stdio;

void main()
{
   writefln("Hello world");
}


Now, let's introduce a function so that we can say hello to anyone.

code:
void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}


And we'll put that all together to greet someone named "Chris".

code:
import std.stdio;

void main()
{
   sayHelloTo("Chris");
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}


But that's still pretty static. Let's pass the name of the person to greet into the program via a command-line argument.

code:
import std.stdio;

void main(char[][] commandLineArguments)
{
   sayHelloTo(commandLineArguments[1]);
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}


There's a problem, though. What if I didn't pass in an argument? The commandLineArguments array will only contain one element, and my access of it will be out of bounds. An error will occur.

I can detect the length of the array, though, and I can do one thing or another with the if statement. Together I can use these to avoid problems.

code:
import std.stdio;

void main(char[][] commandLineArguments)
{
   if (commandLineArguments.length > 1)
      sayHelloTo(commandLineArguments[1]);
   else
      sayHelloTo("world");
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}


And what if I wanted to greet several names? Certainly I can pass several arguments to the program at the command-line.

Well, in this case, I want to use a loop. But I want to avoid the first element. There are a few ways of accomplishing this.

I could loop over every element in the array, and maintain a counter. When the counter is zero, I could use the "continue" statement to skip to the next iteration immediately, being certain to increment the counter first.

code:
import std.stdio;

void main(char[][] commandLineArguments)
{
   int argumentCounter = 0;

   foreach (char[] argument; commandLineArguments)
   {
      if (argumentCounter == 0)
      {
         argumentCounter++;
         continue;
      }

      sayHelloTo(argument);
      argumentCounter++;
   }
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}


The other option available to me is to simply start iterating at the second element. The most reasonable way to accomplish this would seem to be array slicing. As seen with strings, we can access a portion of an array called a "slice".

Again, since we can't count on any arguments being passed in, we should test to see if arguments other than the name of the program are present. Once we've ascertained that, we can create the slice and iterate over it.

code:
import std.stdio;

void main(char[][] commandLineArguments)
{
   if (commandLineArguments.length > 1)
   {
      foreach (char[] argument; commandLineArguments[1 .. length])
      {
         sayHelloTo(argument);
      }
   }
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}


You should note the odd presence of the undeclared "length" variable. Conveniently, the length variable is always created within a slice to refer to the length of the array being sliced. Without this convenience, we'd have to write:

code:
commandLineArguments[1 .. commandLineArguments.length]


Let's look at another useful statement within a loop. The "break" statement allows us to exit out of the loop entirely at an arbitrary point.

For instance, we may wish to greet people until we run into "Mortimer". Then we'll stop greeting anyone.

code:
import std.stdio;

void main(char[][] commandLineArguments)
{
   if (commandLineArguments.length > 1)
   {
      foreach (char[] argument; commandLineArguments[1 .. length])
      {
         if (argument == "Mortimer")
         {
            writefln("Mortimer?  Are you kidding?  I'm done with this.");
            break;
         }

         sayHelloTo(argument);
      }
   }
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}


12. Function Pointers and Aliases

As we've seen already, functions are the means by which all executable code in a program is organized. In other words, they're among the most important "things" in a program.

Yet, it doesn't seem we can actually do much with them. We can call them, and that's about it.

Well, as it happens, we can also pass them around like variables. This ability comes about due to the fact that a function is really just a place in memory where a certain section of executable code starts. We can get a "pointer" to this place in memory, store that, and then use it to call the function later on.

In the previous section we said hello to any name passed in as an argument at the command-line. We defined the sayHelloTo function to accomplish this. We needn't be limited to just that one function, though. Let's create a less upbeat function.

code:
void tellOff(char[] nameToTellOff)
{
   writefln("Damn you %s!",
            nameToTellOff);
}


And maybe a slang version:

code:
void greetWithSlang(char[] nameToGreetWithSlang)
{
   writefln("Yo %s.  Sup?",
            nameToGreetWithSlang);
}


Now, we could simply write a program which greets all of the names with all three functions like so:

code:
import std.stdio;

void main(char[][] commandLineArguments)
{
   if (commandLineArguments.length > 1)
   {
      foreach (char[] argument; commandLineArguments[1 .. length])
      {
         sayHelloTo(argument);
      }

      foreach (char[] argument; commandLineArguments[1 .. length])
      {
         tellOff(argument);
      }

      foreach (char[] argument; commandLineArguments[1 .. length])
      {
         greetWithSlang(argument);
      }
   }
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}

void tellOff(char[] nameToTellOff)
{
   writefln("Damn you %s!",
            nameToTellOff);
}

void greetWithSlang(char[] nameToGreetWithSlang)
{
   writefln("Yo %s.  Sup?",
            nameToGreetWithSlang);
}


However, we should always be looking for ways to streamline our code, and reduce redundancies. Therefore, as we look at the above program, we notice that the three loops are exactly identical, except for the name of the function being called.

So, we need to identify a generic description of these functions. We know that they all return void. We know that they all take one argument: a string. That's enough info.

code:
void function(char[])


We can now declare a pointer to a function.

code:
void function(char[]) greeterFunction;


Initializing this variable takes advantage of the & operator, which finds the memory address of a variable or function.

code:
void function(char[]) greeterFunction = &sayHelloTo;


As with other data types, we can now create an array of these, and statically initialize it with the three functions we've created thus far.

code:
static void function(char[])[] greeters =
   [&sayHelloTo, &tellOff, &GreetWithSlang];


At this point, the type is beginning to look sloppy. We can use a type alias to clear this up.

code:
alias void function(char[]) GreeterFunction;


We can now write:

code:
static GreeterFunction[] greeters =
   [&sayHelloTo, &tellOff, &GreetWithSlang];


Now, we just need to incorporate this back into the program. We'll need to iterate over each of the function pointers, and for each of those, iterate over the arguments passed in at the command-line.

code:
import std.stdio;

void main(char[][] commandLineArguments)
{
   alias void function(char[]) GreeterFunction;
   
   static GreeterFunction[] greeters =
      [&sayHelloTo, &tellOff, &GreetWithSlang];

   if (commandLineArguments.length > 1)
   {
      foreach (GreeterFunction greeter; greeters)
      {
         foreach (char[] argument; commandLineArguments[1 .. length])
         {
            greeter(argument);
         }
      }
   }
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}

void tellOff(char[] nameToTellOff)
{
   writefln("Damn you %s!",
            nameToTellOff);
}

void greetWithSlang(char[] nameToGreetWithSlang)
{
   writefln("Yo %s.  Sup?",
            nameToGreetWithSlang);
}


13. Labels and Breaking and Continuing in Nested Loops

In the previous section we ended up with a program with nested loops. This has considerable ramifications, as we'll see.

First, though, we'll try to apply our filter for the name Mortimer. Previously we had broken out of the loop when we encountered that name. Let's do the same here. For brevity's sake, only the main function is shown, since the other functions remain unchanged.

code:
import std.stdio;

void main(char[][] commandLineArguments)
{
   alias void function(char[]) GreeterFunction;
   
   static GreeterFunction[] greeters =
      [&sayHelloTo, &tellOff, &GreetWithSlang];

   if (commandLineArguments.length > 1)
   {
      foreach (GreeterFunction greeter; greeters)
      {
         foreach (char[] argument; commandLineArguments[1 .. length])
         {
            if (argument == "Mortimer")
            {
               writefln("Mortimer?  Are you kidding?  I'm done with this.");
               break;
            }

            greeter(argument);
         }
      }
   }
}


When we run this we notice an odd behavior. The program will run upto "Mortimer", then run the next function upto Mortimer, and then the next function.

We wanted the program to stop looping entirely when it encountered that name. The problem lies with the default behavior of break. It will break out of the innermost loop.

In order to change this behavior we must first give labels to the loops, effectively naming them.

code:
import std.stdio;

void main(char[][] commandLineArguments)
{
   alias void function(char[]) GreeterFunction;
   
   static GreeterFunction[] greeters =
      [&sayHelloTo, &tellOff, &GreetWithSlang];

   if (commandLineArguments.length > 1)
   {
      functionLoop:
      foreach (GreeterFunction greeter; greeters)
      {
         argumentLoop:
         foreach (char[] argument; commandLineArguments[1 .. length])
         {
            if (argument == "Mortimer")
            {
               writefln("Mortimer?  Are you kidding?  I'm done with this.");
               break;
            }

            greeter(argument);
         }
      }
   }
}


The default behavior of the break statement remains the same. Its behavior can be modified, though, by telling it which loop to break out of.

code:
import std.stdio;

void main(char[][] commandLineArguments)
{
   alias void function(char[]) GreeterFunction;
   
   static GreeterFunction[] greeters =
      [&sayHelloTo, &tellOff, &GreetWithSlang];

   if (commandLineArguments.length > 1)
   {
      functionLoop:
      foreach (GreeterFunction greeter; greeters)
      {
         argumentLoop:
         foreach (char[] argument; commandLineArguments[1 .. length])
         {
            if (argument == "Mortimer")
            {
               writefln("Mortimer?  Are you kidding?  I'm done with this.");
               break functionLoop;
            }

            greeter(argument);
         }
      }
   }
}


If we actually wanted the behavior shown by our first shot at this program, and which we labelled as erroneous, we could use the continue statement. By default the continue statement would simply go to the next iteration of the argument loop, but we can also specify a loop for it to continue for.

code:
import std.stdio;

void main(char[][] commandLineArguments)
{
   alias void function(char[]) GreeterFunction;
   
   static GreeterFunction[] greeters =
      [&sayHelloTo, &tellOff, &GreetWithSlang];

   if (commandLineArguments.length > 1)
   {
      functionLoop:
      foreach (GreeterFunction greeter; greeters)
      {
         argumentLoop:
         foreach (char[] argument; commandLineArguments[1 .. length])
         {
            if (argument == "Mortimer")
            {
               writefln("Mortimer?  Are you kidding?  I'm done with this.");
               continue functionLoop;
            }

            greeter(argument);
         }
      }
   }
}


14. Other Loops: Flexibility, Along With Some Basic Input and Scoping

As a follow up to the previous sections, for and foreach are not the only types of loops. There are also simpler loops, which give us further flexibility, though are less syntactically convenient.

The more common of these is the "while" loop.

With the for loop we outlined three things the loop needed:


  • An initial state for the counter.
  • A test to see if the counter is still valid.
  • An update for the counter.


The for loop gave us a convenient form for listing these three elements. With the while loop they're still present, but individually located.

Typically the initial state is set up just before entering the loop. The test is located in parentheses after the "while" keyword. The update may occur anywhere within the body of the loop.

Let's convert a for loop to a while loop. This loop is fairly simple. It counts from one to ten, outputting each number.

code:
for (int count = 0; count <= 10; count++)
{
   writefln("%d", count);
}


As a while loop, this becomes:

code:
int count = 1;
while (count <= 10)
{
   writefln("%d", count);
   count++;
}


While mostly equivalent, these two examples are quite distinct in one important way. With the for loop, the counter variable is considered to be declared inside the loop. The variable does not exist outside of the loop, and any attempt to use it there will result in an error.

The while loop, however, uses a counter variable declared outside of its scope. This variable can be used elsewhere. This may cause confusion.

The other form of loop is the do-while loop. In most ways this loop is the same as the while loop. In particular it's very simple. However, in this case the test occurs after the loop body runs, rather than before.

The principle effect this has is that the loop will always run at least once. This can be very useful for certain types of applications.

Let's say we wish to ask a yes or no question. We should continue to ask the question until we get either a 'y' or an 'n'. Let's write a function. It'll return a boolean value. True for 'y', and false for 'n'. It'll take a string containing the actual question to ask.

code:
bool askYesOrNoQuestion(char[] questionToAsk)
{
   
}


We'll declare a character variable to hold the user's input.

code:
bool askYesOrNoQuestion(char[] questionToAsk)
{
   char userResponse;
}


Then we'll write the "do" part of the loop. Here the scanf function is used with formatting specifiers identical to those used with writefln. A pointer to a variable of the proper type is also passed in. The data "scanned" in is then inserted into this variable.

The "writef" function works the same as writefln, but doesn't skip to a new line.

code:
bool askYesOrNoQuestion(char[] questionToAsk)
{
   char userResponse;

   do
   {
      writef("%s [y/n] ", questionToAsk);
      scanf("%c", &userResponse);
   }
}


Then of course we have to include a test. In this case we'll test to see if the userResponse doesn't equal a 'y' or a 'no'.

code:
bool askYesOrNoQuestion(char[] questionToAsk)
{
   char userResponse;

   do
   {
      writef("%s [y/n] ", questionToAsk);
      scanf("%c", &userResponse);
   } while (userResponse != 'y' && userResponse != 'n');
}


Knowing that we can only get out of the loop by having either a 'y' or 'n' as a response, we can proceed with the rest of the function under that assumption.

We'll write an expression to see if the user's response if 'y'. If so then they answered "yes", and we want to return true. Otherwise they answered 'n' and we want to return false.

code:
bool askYesOrNoQuestion(char[] questionToAsk)
{
   char userResponse;

   do
   {
      writef("%s [y/n] ", questionToAsk);
      scanf("%c", &userResponse);
   } while (userResponse != 'y' && userResponse != 'n');

   return userResponse == 'y';
}


In this case, as you may have noticed, the fact that the userResponse variable remains in use after the loop has ceased is a valuable tool.

15. Foreach and Associative Arrays

Given an array of integers, such as a list of scores in a game, we can certainly print out those scores.

code:
static int[] scores = [5621, 9845, 1274];

foreach (int score; scores)
   writefln("Score: %d", score);


We also know how to get a dynamic array of keys from an associative array, so even if we change the scores array a bit, we can still print it out.

code:
int[char[]] scoresByPlayer;

scoresByPlayer["Chris"] = 43981;
scoresByPlayer["John"]  = 67215;

foreach (char[] playerName; scoresByPlayer.keys)
   writefln("Player %s had a score of %d",
            playerName,
            scoresByPlayer[playerName]);


Here we've iterated over the keys, and then used each key to look up the associated value in the array. This is fine, but we can also name the value directly.

code:
int[char[]] scoresByPlayer;

scoresByPlayer["Chris"] = 43981;
scoresByPlayer["John"]  = 67215;

foreach (char[] playerName, int score; scoresByPlayer)
   writefln("Player %s had a score of %d",
            playerName,
            score);


16. Function Overloading and Supplying Default Parameter Values

Previously, we've seen that function have, among others, two characteristics. They have a name, and a set of parameters. These can be used to distinguish functions.

To add a twist to this, I'll tell you that two functions may have the same name as long as they have parameter lists which can be distinguished.

Let's say I have a function which withdraws cash from a bank account. It will return an integer representing the amount withdrawn. By default, it will return twenty. Otherwise, I can specify an amount upto one hundred. Anything above will not return a larger value.

code:
int withdrawCashFromAccount()
{
   return 20;
}

int withdrawCashFromAccount(int amountToWithdraw)
{
   if (amountToWithdraw < 100)
      return amountToWithdraw;
   else
      return 100;
}


I can then call the function either with no arguments, or a single integer. The correct function will run.

Complicating the matter is that parameters may have default values. Let's consider our previous example. We want to be able to call withdrawCashFromAccount without arguments and have it assume twenty dollars. This is simple enough with a default parameter value.

code:
int withdrawCashFromAccount(int amountToWithdraw = 20)
{
   if (amountToWithdraw < 100)
      return amountToWithdraw;
   else
      return 100;
}


In this case, there's no need for a version of withdrawCashFromAccount that takes no parameters. In fact, having one at this point is an error, and will not produce a working program.

17. Conditional Expressions

Previously I've discussed "if" statements, such as that used in the previous example.

code:
if (amountToWithdraw < 100)
   return amountToWithdraw;
else
   return 100;


This is a statement, since it doesn't return a value. However, there is a way we can make a simple expression which can have either one value or another based on a test.

This expression involves what is often called the "ternary operator", since it involves three elements: the test, the value if its true, and the value if it's false.

With it, we could rewrite the previous example as:

code:
return amountToWithdraw < 100
          ? amountToWithdraw
          : 100;


18. The Mathematical Reality of Characters

ASCII characters, which we represent like 'a' and 'Z' are not particularly special values. Rather they are simply very small integers, ranging from 1 to 255. The most common of these are between 1 and 127. Zero serves a special purpose, and is not utilized as a character.

It's most useful to understand this when dealing with letters of the alphabet and numbers. Let's look at a common problem posed to new computer science student. Convert a lower-case character into an uppercase character. Clearly, this seems to call for an if statement.

code:
import std.stdio;

void main()
{
   char userInput;
   char capitalizedLetter;

   scanf("%c", &userInput);

   if (userInput == 'a')
      capitalizedLetter = 'A';
   else if (userInput == 'b')
      capitalizedLetter = 'B';
   else if (userInput == 'c')
      capitalizedLetter = 'C';
   else if (userInput == 'd')
      capitalizedLetter = 'D';
   else if (userInput == 'e')
      capitalizedLetter = 'E';
   else if (userInput == 'f')
      capitalizedLetter = 'F';
   else if (userInput == 'g')
      capitalizedLetter = 'G';
   else if (userInput == 'h')
      capitalizedLetter = 'H';
   else if (userInput == 'i')
      capitalizedLetter = 'I';
   else if (userInput == 'j')
      capitalizedLetter = 'J';
   else if (userInput == 'k')
      capitalizedLetter = 'K';
   else if (userInput == 'l')
      capitalizedLetter = 'L';
   else if (userInput == 'm')
      capitalizedLetter = 'M';
   else if (userInput == 'n')
      capitalizedLetter = 'N';
   else if (userInput == 'o')
      capitalizedLetter = 'O';
   else if (userInput == 'p')
      capitalizedLetter = 'P';
   else if (userInput == 'q')
      capitalizedLetter = 'Q';
   else if (userInput == 'r')
      capitalizedLetter = 'R';
   else if (userInput == 's')
      capitalizedLetter = 'S';
   else if (userInput == 't')
      capitalizedLetter = 'T';
   else if (userInput == 'u')
      capitalizedLetter = 'U';
   else if (userInput == 'v')
      capitalizedLetter = 'V';
   else if (userInput == 'w')
      capitalizedLetter = 'W';
   else if (userInput == 'x')
      capitalizedLetter = 'X';
   else if (userInput == 'y')
      capitalizedLetter = 'Y';
   else if (userInput == 'z')
      capitalizedLetter = 'Z';

   writefln(capitalizedLetter);
}


This is a lot of redundant code, though. In order to shorten this, we need to realize that letters, as characters, are not just tiny little numbers, but they're in order. The letter 'a' is 97. The letter 'b' is 98. This continues right up to 'z' which is 122. At the same time, the capital letters follow the same order. The letter 'A' is 65, and it progresses upto 'Z', which is 90.

Since characters are simply numbers, we can do math with them. It should be too much of a surprise, then, that if 'a' is 97, and 'A' is 65, we can simply subtract 32 from 'a' to get 'Z'. As the other leters follow the same order, we can do the same to them to get a capitalized version.

code:
import std.stdio;

void main()
{
   char userInput;

   scanf("%c", &userInput);

   char capitalizedLetter = userInput - 32;

   writefln(capitalizedLetter);
}


Of course, at this point, we cannot be sure that the user input a lowercase letter. To ensure this, we need a test. Since characters are simply small integers, comparing them is rather easy.

code:
import std.stdio;

void main()
{
   char userInput;
   char capitalizedLetter;
   scanf("%c", &userInput);

   if (userInput >= 97 && userInput <= 122)
      capitalizedLetter = userInput - 32;
   else
      capitalizedLetter = userInput;

   writefln(capitalizedLetter);
}


We can also use math with characters to get an integer back. Consider the problem of turning a character depiction of an integer, like '1' into the corresponding integer value.

To accomplish this we must realize again that the ASCII character for digits are all in order, from '0' to '9'. Thus there is a difference of one, between '0' and '1'. This is exactly the output we desire, so we can write:

code:
import std.stdio;

void main()
{
   char numberAsChar = '1';
   int actualNumber = numberAsChar - '0';
}


19. The Switch Statement

In the previous section we wrote a rather obnoxiously long if statement. You'll notice that in each test we simply compared a value against the same variable for equality.

There's an easier way to accomplish this.

code:
switch (userInput)
{
   case 'a':
      capitalizedLetter = 'A';
      break;
   case 'b':
      capitalizedLetter = 'B';
      break;
   case 'c':
      capitalizedLetter = 'C';
      break;
   case 'd':
      capitalizedLetter = 'D';
      break;
   case 'e':
      capitalizedLetter = 'E';
      break;
   case 'f':
      capitalizedLetter = 'F';
      break;
   case 'g':
      capitalizedLetter = 'G';
      break;
   case 'h':
      capitalizedLetter = 'H';
      break;
   case 'i':
      capitalizedLetter = 'I';
      break;
   case 'j':
      capitalizedLetter = 'J';
      break;
   case 'k':
      capitalizedLetter = 'K';
      break;
   case 'l':
      capitalizedLetter = 'L';
      break;
   case 'm':
      capitalizedLetter = 'M';
      break;
   case 'n':
      capitalizedLetter = 'N';
      break;
   case 'o':
      capitalizedLetter = 'O';
      break;
   case 'p':
      capitalizedLetter = 'P';
      break;
   case 'q':
      capitalizedLetter = 'Q';
      break;
   case 'r':
      capitalizedLetter = 'R';
      break;
   case 's':
      capitalizedLetter = 'S';
      break;
   case 't':
      capitalizedLetter = 'T';
      break;
   case 'u':
      capitalizedLetter = 'U';
      break;
   case 'v':
      capitalizedLetter = 'V';
      break;
   case 'w':
      capitalizedLetter = 'W';
      break;
   case 'x':
      capitalizedLetter = 'X';
      break;
   case 'y':
      capitalizedLetter = 'Y';
      break;
   case 'z':
      capitalizedLetter = 'Z';
      break;
}


By meaning that I need only type 'userInput' once, I avoid a huge source of potential problems with typos, if there is no other benefit.

Of course, you're likely wondering about the numerous uses of the break statement. This arises from the fact that cases "fall through". Were I to simple write:

code:
switch (userInput)
{
   case 'a':
      capitalizedLetter = 'A';
   case 'b':
      capitalizedLetter = 'B';
}


Then if userInput is 'a', capitalizedLetter will end up as 'B'. This happens because we start at case 'a', and set capitalizedLetter, accordingly, but nowhere do we say to stop and exit the switch statement, so things just keep going and we actually end up executing:

code:
capitalizedLetter = 'B';


Of course, falling through can be utilized to have the same code executed for multiple cases. Let's say we want to identify if a digit character is an odd number.

code:
bool isOddDigit(char digit)
{
   switch (digit)
   {
      case '1':
      case '3':
      case '5':
      case '7':
      case '9':
         return true;
      case '0':
      case '2':
      case '4':
      case '6':
      case '8':
         return false;
   }
}


Thankfully, there's a more concise option.

code:
bool isOddDigit(char digit)
{
   switch (digit)
   {
      case '1', '3', '5', '7', '9':
         return true;
      case '0', '2', '4', '6', '8':
         return false;
   }
}


You may wonder why I haven't included any break statements in the last two samples. The return statement exits out of the entire function, so breaking out of the switch statement becomes unnecessary.

The switch statement has another trick up its sleeve. It can be used with strings. This can be tremendously useful. Let's create a selective greeting function.

code:
char[] selectiveGreeting(char[] nameToGreet)
{
   switch (nameToGreet)
   {
      case "Martin":
         return "How's the Microsoft love, Martin?";
      case "Hacker Dan":
         return "Just about done with v3, Dan?";
      case "Tony":
         return "How does it feel to have a clone, Tony?"
      default:
         return "Hello, " ~ nameToGreet ~ '!';
   }
}


You'll also note the use of the "default" case. This is what we run if no other case matches.

20. Modules

Way back when I started talking about simple output, I talked about modules, and we imported the "std.stdio" module. It's about time to explain a bit more about how modules work, and how we can create our own modules.

Modules contain various functions and data, which can be imported into another module, or a simple program. This is extremely important for organizing common pieces of code for easier reuse.

Let's look at a simple "greeting" module which contains our "sayHelloTo" function.

code:
module greeting;

import std.stdio;

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}


You'll notice the first statement is the module declaration, which tells us exactly what the name of the module is. This should always be the first statement in a module.

We'll put this into a source file named "greeting.d", to match the name of the module.

Now, when we write our program, we need only write:

code:
import greeting;

void main()
{
   sayHelloTo("world");
}


Of course, if more than one module we imported were to define a "sayHelloTo" function, we could specify exactly which one we want with:

code:
import greeting;

void main()
{
   greeting.sayHelloTo("world");
}


There is a problem with "import" here. When we imported "std.stdio" into "greeting", and then "greeting" into our program, we imported all of "std.stdio" into our program as well. This may well be convenent, but it means that we can access input and output functions from within our program without knowing where they're coming from.

We can fix this by using a private import within the greeting module. This brings "std.stdio" into that module, but not into programs or modules importing "greeting".

code:
module greeting;

private import std.stdio;

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}


To compile this simple example we want to have both the module and the program in the same directory and specify both files to the compiler.

code:
prompt> dmd myprogram.d greeting.d -w
Sponsor
Sponsor
Sponsor
sponsor
Display posts from previous:   
   Index -> Programming, C++ -> C++ Tutorials
View previous topic Tell A FriendPrintable versionDownload TopicRate TopicSubscribe to this topicPrivate MessagesRefresh page View next topic

Page 1 of 1  [ 1 Posts ]
Jump to:   


Style:  
Search: