Project 6 - A Simple Database

At one time or other, we have probably all used a card index. This was a small box containing a series of loose cards that contained information. Each card held information laid out the same way, and were usually arranged alphabetically or in some other logical order, so that anyone could flick through and retrieve the information wanted:

Record:      216
Book title:   Hard Times
Author:       Charles Dickens
Category:    Classic Fiction

A card index transferred onto a computer is called a database. It contains a series of records, equivalent to the cards themselves, and each record contains slots in which the information is stored. Each slot is referred to as a record field.

This project involves the creation of a simple database for a dog walking company. The company picks dogs up from their homes, walks them and returns them. As such, it needs to store data such as the names of the dogs, their addresses, the owner's name and phone number, and various other pieces of information such as whether the dog pulls on the lead or attacks other dogs. The program will keep these records on the hard disc of the computer, loading them into memory at the start, and resaving them on the disc if they are changed while the program is running. The user will be able to add, or delete records, amend the data in the fields, and, most importantly, search for records matching a given search criterion.

Aspects of C++ covered

The String Type

I promised you in the previous project a more efficient way of implementing strings and here it is. It is present in the standard header file string.h (cstring.h on some systems), so it must be included into the program in the normal manner:

#include<string>
or
#include<cstring>

This gives access to a special string type. It can be used in pretty much the same way as character arrays:

string myname;
const string quotation = "To be or not to be!";

etc.

You can access the individual characters of a string just as though it were an array:

if (quotation[6] == ' ')
  cout << quotation[10];

Characters within a string are indexed from 0. So far, strings appear identical to character arrays. "So what's the fuss?" I hear you ask. Well, this is where strings accelerate away and leave character arrays standing.

The first thing we can do is define one string which is a substring of a previous one. A substring means characters which have been copied from one string, perhaps a single word or phrase extracted from the string. For instance, if the string quotation has already been declared, then this declaration …

string extract (quotation, 9, 6);

... will declare another string called extract, which is 6 characters long and is copied from quotation starting at character number 9. The first parameter in the brackets is the string whose characters are to be copied, the second character is the starting index of the characters to copy and the last parameter is the number of characters to copy. The result is this:

The instruction cout << extract; would display the string "not to" on the screen. The original string from which the subtract is taken is not altered, i.e. the characters are not deleted from it.

You can also access a substring from the original after the variable has been declared, using substr() which is a function built in to the string type:

single_word = quotation.substr(16, 3);

I am assuming that the string single_word has been declared somewhere up above. This time the syntax is slightly different. Because the function is part of the string type (i.e. not declared by you) then it is attached to the string variable from which you are taking the substring with a period character. Now only two parameters are needed, firstly the index of the character at which to start copying and the number of characters to copy. There is no need for the name of the string to copy from to be passed as a parameter.

The result of the instruction above is that single_word becomes the string be! If you specify a number of characters which would take you beyond the end of the string, the character extraction stops at the end of the string, so the instruction …

single_word = quotation.substr(16, 100);

... would still produce the string be! There are several other functions which are built into the string type. Here are some of them.

length()

This returns the length of the string. Unlike character arrays, variables of the string type do not have an invisible /0 character at the end, so the value returned by this function really is the number of characters in the string:

const string greeting = "Welcome to my humble abode.";
cout << greeting.length();    // Displays 27, not 28.

compare()

You can compare your string with any other string variable, string literal (in quotation marks) or an array of characters. The object to be compared is the single parameter of this function:

result = greeting.compare("welcome");

This compares the string greeting to the string literal "welcome". The result will be a positive integer if the original string is greater than the parameter, 0 if it is identical to the parameter, or a negative integer if it is less than the parameter. The exact integers depend on your version of C++, but it will typically be 1 if the original string is greater than the parameter, and -1 if it is less than the parameter.

Er, hang on a moment! What's all this "greater than, less than" all about? These terms can be applied to strings as well as numbers. In this case, the term "greater than" means that the original string would come after the parameter in the equivalent of an ASCII value dictionary.

For instance, the word Hello would come before the word Welcome as H comes before W (i.e. H has a lower ASCII value), but the word hello (with a lower case 'h') would come after Welcome as lower case h has a higher ASCII value than upper case W. If the first characters are the same, then the comparison moves down the string, so Welcome comes before Well (the first three characters are the same, but the c of Welcome comes before the l of Well).

If the strings are still identical when the comparison reaches the end of one of them, the longer string is greater, so Foolproof is greater than Fool. To complicate matters further, all characters have ASCII values, even spaces and punctuation. The ASCII value of a space is 32, the lowest value of any non-control character, so " Welcome" (starting with a space) would be less than "Welcome" (no space).

The best way to understand this is to try various strings. This program asks you to enter two strings and compares the first to the second:

Listing 6.1

#include <iostream>
#include <string>

string s1, s2;        // Declare two strings

void main ()
  {  cout << "Please enter the first string : ";
     cin >> s1;
     cout << "Please enter the second string : ";
     cin >> s2;
     cout << "The result of the comparison is ";
     cout << s1.compare(s2) << endl;
  }

You might like to adapt this program so that it loops round and lets you compare several pairs of words. You will need to decide on a stopping condition for the loop yourself.

String concatenation

Strings can be joined together using + signs. This has the effect of tying them together end to end, called concatenation. Here is an example:

string w1 = "Humpty";
string w2 = "Dumpty";
string w3 = "sat on a wall.";
string rhyme1 = w1 + " " + w2 + " " + w3;

This strings the words together to form the sentence Humpty Dumpty sat on a wall. Spaces are included in the concatenation to stop the sentence coming out as HumptyDumptysat on a wall.

With concatenation, one of the operands must be a string variable. It can't concatenate two string literals. You will notice in the example above, there are no + signs which have a string literals on both sides. This restriction means that the following declaration would not compile:

string rhyme2 = "Mary, " + "Mary, " + "quite contrary.";

However, if the middle one of those string literals is turned into a variable, then the concatenation will work:

string name = "Mary, ";
string rhyme2 = "Mary, " + name + "quite contrary";
      // Legal
string rhyme3 = name + name + "quite contrary";
      // Also legal

getline()

Consider this program listing. It gets you to enter a string and then displays it on the screen:

Listing 6.2

#include <iostream>
#include <string>
string temp;

void main ()
   {    cout << "Please enter a string : ";
        cin >> temp;
        cout << "You entered : " << temp << endl;
   }

This all seems fairly straight forward, until you enter a string with a space or a tab character in it! Try running the program with the following input:

Please enter a string : William Shakespeare
You entered : William

Hey, what happened to the surname? Well, the fact is that cin only reads strings up to the first space or tab character, or until you press the Enter key. Anything after that, it ignores. This wasn't a problem while you were using cin to read numbers, but with strings, you quickly run aground.

If you want to include spaces or tabs in your string, replace cin with the getline() function, a function built in to the <string.h> file:

getline(cin, temp);

This means "Get a line of text from the keyboard and store it in the string temp". We include cin in the command to specify that it is the keyboard we want to get the input from. Later, we will replace the cin keyword in this command with a file variable to read a line of text from a file. This line of text can contain as many spaces or tabs as you like, but it will still end when you press Enter.

Structures

The variables that you have been using up to now have been horribly limiting. Even arrays don't offer much flexibility, as you can only glue together sequences of the same variable type (an array of integers, an array of floating point values etc. - but not an array containing both!) Programs such as databases must be able to store different sorts of variables all in one unit. C++ contains two built-in devices that you can use to build your own data-types. The first of these is the structure, explained below. The other one is the deluxe version of structures, called a class, which is explained in a later project.

So what is a structure then? It lets you glue various variables of different types (integers, characters, strings etc. even other structures!) into one unified whole, which is given a name. This then gives you another data type, and you can declare variables of that data type.

An example will make all this clear. Suppose I want to declare a structure to hold data about people I know:

struct    person
    {   string firstname;
        char lastname[30];
        char initial;
        int age, house_number, number_of_pets;
        float height;
    };                // Note the semicolon

person my_mum, my_dad, friend, aunt, boss, grannie;

The keyword struct tells C++ that what follows is a structure, called person in this case. The individual variables are declared as normal, except that the declarations are enclosed within curly brackets. I chose to declare the first and last names to be of different types simply to show that structures can contain arrays. The closing curly bracket of the structure declaration must be followed by a semicolon.

Having set up the structure itself, it can be used as a data type for further variables, in this case my_mum, my_dad etc. The individual parts of these structures are referred to using periods:

my_mum.firstname = "Mary";
my_dad.lastname = "Bowles";
boss.age = 99;

The word before the period is the name of the structure variable, the word after the period is the part of that variable that you want to alter. You can access elements in a structure in the same way:

cout << "The name of your friend is " << friend.firstname
     << " " << friend.lastname << endl;
volume = container.length * container.width * container.height;

You can declare arrays of structures:

person girlfriends[35];        // Will this be enough?
girlfriend[0].firstname = "Helen";
girlfriend[0].surname = "Jameson";
girlfriend[1].firstname = "Sandra";    // etc.

In this case, each individual structure is referred to using the name of the array and the index in square brackets. This is then followed by the period and the element that you want to access.

Structures can also be passed as parameters to functions, in which case, they are referred to using the parameter name, followed by the period and the element name:

void display_people (person p)
   {    cout << p.firstname << " " << p.lastname << endl;
    cout << "Age is " << p.age << endl;
   }

display_people(my_mum);
display_people(aunt);
display_people(girlfriend[23]);

The function display_people() is called three times, with the parameter p becoming my_mum, aunt and girlfriend[23] (which is a structure of the correct type even though it is declared in an array) in turn.

Initialising a structure during declaration

A structure can be initialised while it is being declared just as an array can. The values to be assigned to the structure elements are enclosed in order within curly brackets, and each value must have the correct type to be assigned to the corresponding element:

person sister = { "Tina", "Bowles", 'M', 32, 5, 2, 1.6 };

This is equivalent to

person sister;
sister.firstname = "Tina";
sister.lastname = "Bowles";    // etc.

Obviously, the elements must be in the correct order. If you tried to compile this ...

person fiancee = { 28, 100, 1, "Diane", "Smith", 1.8 };

... the compiler would reject it as 28 is not a suitable value for fiancee.firstname, and the other values don't match either.

Combining a structure definition with variable declarations

It is possible to define a structure and declare some variables with that structure all in one go. This is done by putting the variable names just before the semicolon at the end of the structure definition:

struct food_can
 {    float height, radius;
      string tin_label;
 }    dogfood, tuna_fish, canned_veg[10];

The variables dogfood, tunafish and canned_veg[10] are all declared as being of type food_can, and can be used in the same way as any other structure:

tuna_fish.height = 3.6;
canned_veg[3].radius = 7.1;
dogfood.tin_label = "Woofy Chunks";

Reading and writing data to and from files

This is a rather complex procedure, and I shall only be giving the basics here - how to open files for reading or writing (not both at the same time), read/write data as text to and from the file, and close the file afterwards.

For a start, the functions used for creating and using files are stored in the <iostream.h> header file, so make sure that it is included at the start of the program. The chances are that you will be using this file for cin and cout anyway.

A file must have a name, and this is best declared as a string. The string can be a simple name, such as ...

const string filename = "famdata";

... or a full pathname, such as ...

const string fullname = "C:\\WINDOWS\\SYSTEM\\BIG1.TXT";

In this case, the file is to be located on the C: drive (the hard disc of the machine, I assume) in a subdirectory called \WINDOWS\SYSTEM, and has the name BIG1 with the file extension TXT. Note that you have to use a double backslash, \\, to include a single backslash in the string itself, as the C++ compiler assumes that a backslash character is the start of a control code such as \n. When it finds two backslash characters next to each other, it realises that a genuine backslash is intended, and it puts one in.

Files must be attached to a file variable before they can be used, and when the variable is declared, you have to specify whether the file variable is to be used for reading data into the program (an input file) or writing data out to the disc (an output file). You can, of course, have several files open at the same time, some for reading and some for writing, and each one will have to be attached to its own file variable.

To open a file for writing data, use the following instruction:

ofstream outfile(filename);

In this case, the keyword ofstream tells the compiler that the file is to be opened for output, outfile is the file variable name (which, of course, can be any name you like) and filename is the string that you set up before. You could use a string literal here if you didn't want to specify one earlier:

ofstream my_data("A:\\TEMP1");

If the file does not exist on the disc, then this instruction creates it. If it already exists, then all the data inside it will be erased, and it will be opened afresh. If it contained any vital data, too bad!

Data is written out to the file in the same way as displaying information on the screen, except that the file variable takes the place of the cout word. The insertion operator is used to pass the thing to be displayed to the file variable:

my_data << 2 << " " << temp1 * x + 3 << endl;

The data is stored in the file in the form of text, so you can load the file into a word processor or a simple text editor and read it (and alter it!) endl commands are used to move to a new line in the file just as they are used for screen display.

When you have finished writing data to the file, you must close it using this command:

my_data.close();

The same applies to opening files for input, except this time use the ifstream command to open it (as an input file), the extraction operator >> to read data from it (in a similar manner to cin) and remember to close it after all the input has been read from it. You don't have to read the file right the way to the end - just read what you need to.

Like cin, the extraction operator will only read the file up to the next space character, or tab character or the end of the line, whichever comes first. If you wanted to read a text line from a file that contained spaces or tab characters, you would use getline(), this time specifying the file variable instead of cin.

For example, suppose a file contained the text ...

Twelve C++ Projects

... and this file had been opened for input with the file variable f. This instruction:

f >> temp;

would leave the string temp holding the word Twelve, but the instruction

getline(f, temp);

would leave the string temp holding Twelve C++ Projects. The extraction operator is marvellous if you want to read a file a word at a time, as it automatically parcels it up for you, but otherwise, you should stick to getline().

The Binary System

All data in computers is stored in terms of binary code, be it numbers, characters or symbols. Binary code consists of a vast number of just two digits - 1 and 0 - with everything coded in terms of these.

The binary system really applies to just numbers, but all characters (including symbols such as '+' and 'new line') are stored as ASCII codes, which are just numbers anyway. This is how numbers are converted into binary.

Humans tend to count in base 10, as we have 10 fingers. We divide our numbers into columns, marked 'units', 'tens', 'hundreds' etc. In binary, we only have two digits available to us, so the columns are marked up in powers of 2 rather than powers of 10. The columns become 1 (units), 2, 4, 8 etc. each being double the value of the previous columns. This gives an arrangement like the following:

128

64

32

16

8

4

2

Units

Decimal equivalent

0

0

0

1

1

1

1

1

31

1

0

1

0

0

1

1

0

166

1

1

0

0

1

0

1

1

203

0

1

0

1

1

1

0

1

93

The columns don't have to stop at 128, of course. The next column would be 2 x 128 = 256, then 512, then 1024 etc.

The binary pattern 00011111 which works out 0 x 128 + 0 x 64 + 0 x 32 + 1 x 16 + 1 x 8 + 1 x 4 + 1 x 2 + 1 x 1 = 31. Similarly, the binary pattern 11001011 represents the number 1 x 128 + 1 x 64 + 0 x 32 + 0 x 16 + 1 x 8 + 0 x 4 + 1 x 2 + 1 x 1 = 203. The pattern 01011101 translates into 93, which is also the ASCII number for the closing square bracket symbol, ']'. This means that the pattern 01011101 could represent 93 or ].

You convert a binary pattern into a (decimal) number by multiplying each digit by the appropriate power of 2 and then adding all the results as you see above. Converting a number from decimal into binary is also fairly simple:

Here's an example. Suppose we want to convert the number 171 into binary. We will start with highest power of 128 (although you could start with any higher power than this, for example, 65536):

Is 171 bigger than 128?

Yes it is, so this digit is 1 and we subtract 128 from 171 leaving 43.

1

Is 43 bigger than 64?

No, so this digit is 0, and we leave the number as 43.

0

Is 43 bigger than 32?

Yes, so this digit is 1, and we subtract 32 from 43, leaving the number as 11.

1

Is 11 bigger than 16?

No, so this digit is 0 and we leave the number as 11.

0

Is 11 bigger than 8?

Yes, so this digit is 1, and we subtract 8 from 11, leaving the number as 3.

1

Is 3 bigger than 4?

No, so this digit is 0 and we leave the number as 3.

0

Is 3 bigger than 2?

Yes, so this digit is 1 and we subtract 2 from 3, leaving 1.

1

Is 1 bigger than 1?

Well, it's the same, which still counts, so this digit is 1, and we subtract 1 giving 0.

1

The binary equivalent of 171 is found by reading the right-hand column downwards, i.e. 10101011. Try this process with other numbers like 197 and 507. In the case of 507 the first power of 2 you will need to consider is 256 rather than 128.

Listing 6.3 gives a program which translates a number into binary, displaying it digit by digit on the screen. Check the listing line-by-line to discover how it works.

Listing 6.3

#include <iostream>
int number = 0;

// Find 2 raised to a given power (specified by p)
int pwr (int p)
 {  int temp = 1;
    if (p > 0)
    for (int x = 1; x <= p; x++)
        temp *= 2;    // Multiply temp by 2
    return temp;
 }

void main ()
 {  cout << "Please enter the number : "';
    cin >> number;
    // Find the first power of 2 bigger than the number
    int power = 1;
    do
       {   if (pwr(power) <= number)
           power++
       }
    while (pwr(power) <= number);
    // The first power of 2 present in the number is 1
    // less than the power we have just calculated
    power--;
    do
      { if (number >= pwr(power))
          {    cout << "1";
            number -= pwr(power);
          }
        else
            cout << "0";
        power--;
      }
    while (power >= 0);
    cout << endl;
   }

Listing 6.4 converts binary numbers (entered as a string) into normal decimal format. It builds up the number in a variable called temp by constantly multiplying it by 2 every time it considers another binary digit in the string, and adding 1 to temp if the binary digit under consideration is 1.

Listing 6.4

#include <iostream>
#include <string>
int temp = 0;
string binary;    // This will hold the binary representation

void main()
 {  cout << "Please enter the number : ";
    cin >> binary;
    // Shouldn't include spaces, so can use cin
    // rather than getline()
    for (int x = 0; x < binary.length(); x++)
       {  temp *= 2;    // Multiply temp by 2
          if (binary[x] == '1')
          temp++;
       }
    cout << "The number is " << temp << endl;
 }

Again, you should work through the program to discover how it works. The two programs here can be adapted to cope with numbers in any other base, and can also be used in the project itself to produce something called a "behaviour code" - more on this later.

Binary Operations

You may ask why I have told you about the binary system. The reason is that C++ gives you some operators that work on numbers on a purely binary level. These are the AND operator & and the OR operator |. They work in a similar way to the && and || operators that you met when learning about conditions, except that you don't double the symbol and these operators are applied to numbers rather than conditions.

The AND operator is placed between two numbers or integer variables and produces an answer from those numbers. It treats each of those numbers as a binary pattern of 1s and 0s. It compares corresponding digits and sets the corresponding digits of the answer to 1 if both the digits it is looking at are 1, 0 otherwise:

In this example, the calculation carried out is 187 & 211 which produces the answer 147. You can demonstrate this with the following program:

Listing 6.5

#include <iostream>
int n1 = 187, n2 = 211;
void main ()
 { cout << "The first number is " << n1 << endl;
   cout << "The second number is " << n2 << endl;
   cout << "The result of & is " << (n1 & n2) << endl;
 }

The program will display the result 147. Adapt it so that it lets you enter different numbers every time. It is a good idea to put operations like & in parentheses, especially when combined with << as this also has a meaning in binary arithmetic. In fact, it shifts the number to the left a specified number of places. Every time you shift a binary number left one place, it has the effect of multiplying it by 2 (Check this for yourself):

Listing 6.6

#include <iostream>
void main ()
  {  int x = 47;    //  Binary pattern 00101111
     x = x << 1;
     // Shift right 1 place, giving 01011110
     cout << x << endl;    // Displays 47 times 2 = 94
     cout << (3 << 4);
     // Displays 3 x 2 x 2 x 2 x 2 = 48.
  }

The parentheses in the last line are needed so that the compiler understands that the << between the two numbers is a binary shift operation rather than a display operator. You could rewrite listings 6.3 and 6.4 replacing the temp *= 2 commands with temp << 1 commands.

You should be careful when multiplying very large numbers by 2 in this way. Numbers are stored using a fixed number of binary digits (exactly how many depends on whether the number is stored using int, long, unsigned long etc.) and if you push the number too far to the left, digits will "drop off" the end.

I will leave you to work out what the >> operator does when applied to binary numbers. Anyway, I digress. As well as the AND operator (&), there is the OR operator (|). This compares the binary digits of two numbers or variables and sets the corresponding bits of the answers if either of the digits in the two numbers is a 1.

Exercises

  1. Declare a structure to store details of each of the following:

    • Cars
    • Planets in the Solar System
    • Houses in your street.

    In each case, declare some variables of the appropriate type and include some program statements which access the elements of the structures.

  2. const string proverb =
    "Every mushroom cloud has a strontium lining.";

    Write program statements, using the built-in string functions, which

    • Return the length of the string.
    • Return the position of the first letter 'm' in the string.
    • Return the position of the second space in the string.

  3. How would you ...

    • Define a structure to store data about pieces of music (composer, performance, length, instruments required etc.)
    • Declare three variables of this structure, and
    • Initialise those variables

    ... all in one instruction?

  4. Please convert the following numbers to binary. You should include leading zero digits to pad the binary numbers out to 8 digits, so 15 would become 00001111 rather than just 1111:

    a) 97 b) 247 c) 179 d) 101 e) 201 f) 255

    Please convert the following binary numbers to decimal (normal) format:

    a) 01011010 b) 10010001 c) 01100110
    d) 10100001 e) 11000011 f) 10010101

  5. Use your knowledge of binary representation to predict what the result of the following binary operations will be:

    1. 34 & 180
    2. 127 | 192
    3. 93 | 31
    4. 127 & 128
    5. 127 | 128
    6. 155 & 178

  6. A student writes the following code to write the two-times table out to a file:

    ofstream table("TWOTIMES");
    for (int x = 1; x <=12; x++)
        table << 2 * x;
    

    At a later date, the student writes the following code to retrieve the numbers from the same unaltered file:

    ifstream table("TWOTIMES");
    for (int x = 1; x <= 12; x++)
      { int temp = 0;
        table >> temp;
        cout << temp;
      }
    

    However, these two sections of code contain a fundamental error that prevents them from working correctly. What is the fault and how would you correct it?

  7. Write a short program that asks you for two file names, and copies the contents of one file into the other.

  8. Write a program that asks you for a string input and then converts all the upper case characters in the string to lower case and vice-versa, so the string Mary had a little lamb would becomes mARY HAD A LITTLE LAMB.

  9. The right-most digit of a binary number represents the number 1. Write an instruction using the OR operator that will set this bit of a number to 1 if it is clear (i.e. 0) and leave it as 1 if it is already set.

    Now adapt the instruction so that it can set other digits of the number. For instance, how would you set the digit next to the right-most bit, which represents 2?

  10. This instruction is similar to the previous one, except that now you have to clear the right-most digit of a number to 0 (or leave it if it is already 0). This time you have to use the AND operator with the appropriate number.

The Project Itself

Task 1

The first step in writing any database is to decide upon the format of the data. In this case, this involves setting up a structure to hold the data about each dog. I will leave the exact format up to you, but it will need to include the following:

Feel free to add to this list, of course. The telephone number may need to include an area code - meaning parentheses, spaces etc. - so it should be a string rather than an integer.

The actual records in the database will be stored as a series of these structures, which means an array of structures. Insert a statement to declare this array, and a variable (starting off as 0) which indicates the number of records present in the database.

Task 2

Write a function that asks the user to enter the data for a given dog. Each of the elements will be prompted for individually:

Please enter the dog's name: Dylan
Please enter the owner's name: Mrs. Gower etc.

The parameter to this function will be the structure whose details are to be filled in. This will need to be a reference parameter (preceded by the & symbol) as the changes will be passed back to the calling function:

void enter_dog_details (dog& d)
    {    \\ Body of the function
    }

When the function is called, the structure passed to it will be one of the ones in the array of dogs:

enter_dog_details(dogs[current_dog]);

In this case, I have assumed that the structure containing the details of each dog is called dog, and that the array holding all the dogs in the database is called dogs. Obviously, the function and parameter names are for you to decide.

A simpler way to implement the function would be to pass the index of the dog in the database:

void enter_dog_details (int index)
 { cout << "Enter name of dog " << index << " : ";
   cin >> dogs[index];
        // etc.
 }

The parameter would no longer have to be a reference parameter as it is the array of dogs that is being changed, not the index number. However, by sticking to the version where the structure itself is passed, we kill two birds with one stone. We are going to need to enter the details of dogs to search for in the database, and these can be entered into a structure of type dog before being matched against all the dogs in the database. The same function that was used to enter the dog details for storage can also be used to enter the details into the search template:

dog template;

...

enter_dog_details(template);

In this case, blanks can be entered for fields that don't form part of the search query. For instance, if you were looking for a dog in the database called Scruffy, when prompted, you would enter Scruffy for the name of the dog and simply press the Enter key for the other fields.

Your function will also have to construct the behaviour code for the dog. This is a binary code which indicates whether the dog has certain personality traits or not. The binary number could be constructed as follows:

You can use your own imaginations as regards what these naughty dogs are capable of. Speaking as someone who has looked after dogs, I can say that some of the things that they get up to would make your eyes bulge!

Using the binary code, the whole of the dog's personality can be captured as a number. For instance, a dog that barked at night and had fleas, but didn't bite people or dig in the flowerbeds, would have a binary code 1001, translating to a behaviour code 8 x 1 + 4 x 0 + 2 x 0 + 1 x 1 = 9. Obviously, the behaviour codes in your program will have more than four binary digits.

Each of the personality traits will have to be entered separately, for instance:

Does Dylan bark at night? 1

Does Dylan bite people? 0 etc.

Use binary operations to construct the binary number from this information. For instance, if the dog bites people, OR the behaviour code with 2 to set that particular bit. If it digs in the garden, OR the behaviour code with 4 etc.

Task 3

The program will also need a function to display the details for any dog. This will be similar to the function for entering the dog details except that it won't need to ask the user for any input. It is also a useful idea to put a "Please press the Enter key to continue" message at the end followed by a cin statement to prevent the program moving on before you have had time to read the dog details.

When it comes to displaying the behaviour code, you have a choice. You can either get the function to decode the number and display appropriate messages such as Scruffy has fleas, or just display it as a number. After all, you never know whether the dog's owner will be staring over your shoulder at the time!

Task 4

An important part of the database is the fact that records are stored on the disc. Without that, it would be virtually useless.

Firstly, decide where you want the file to be stored. You will need to specify both a file name and a position on the disc. It's best to set these up as a constant near the start of the program, something like this:

const string filename = "C:\\CPPWORK\\DOGDATA";

You will also need to create a function which writes all the records out to the file, and one that reads the records in from it. The structure of the first function will look something like this:

The function for reading the data will be similar with read operations substituted for write ones.

The simplest way to include these in the database program is to call the read function at the start of the main program, and the write function at the end of the main program. The records are read in as soon as the database runs, kept in main memory throughout, and only written back out to the disc when the program is about to finish.

However, this gives a slight problem. The first time that the program is executed, the file containing the data will not exist, which will crash the program. The easiest way to get round this is to create the file artificially using a text editor. It should contain just the number 0, to indicate that there are no records in the file. Needless to say, you should write the for loops in the two functions in such a way that they are able to cope with 0 records in the database.

Task 5

The whole purpose of a database is that the users can search for data that matches certain criteria. You already have a function that can ask the user to enter the search criteria and store them in a structure which I have called t (for shortness). This is of the same type that is used to store the dog data itself. The function that gets the user to specify the search criteria is the same one that was used to enter the dog data in the first place:

dog t;

enter_dog_details(t);

The tricky part is going to be comparing this template against all the dogs in the database. This will have to be achieved using a loop, probably a for loop, that goes through all the dogs. A simple loop to search for a given dog name might look like this:

for (int x = 0; x < num_dogs; x++)
  if (t.dogname == dogs[x].dogname)
     display_dog_details(dogs[x]);

This is fine, providing that the user has specified the dog's name. It might be the owner's name that the user's want, in which case the dog's name in the search template will be left blank. We can adapt the if statement above so that a blank dog's name in the search template matches any dog name in the database:

if ((t.dogname == dogs[x].dogname) || (t.dogname == "")
  && (t.owner == dogs[x].owner) || (t.owner == ""))
      display_dog(dogs[x]);

Work through this compound condition to see how it matches fields with valid data in them and ignores those which are blank. Remember, && means "only pass this condition if both parts are true", and || means "pass this condition if either (or both) parts are true."

Each section of the if clause will have two parts, one to compare it to see if it matches the corresponding entry in the search template, and one to check whether that entry in the search template is empty anyway (in which case that part of the search query matches any record). The if statement in your function will look complex, and the more fields you have decided to include in your dog database, the more complex it will look. Best of luck!

Task 6

There is one glaring omission from the search routine that you have written - how does it cope with behaviour codes? It's all very well to search for a code that matches a particular value, say 73 meaning all the dogs that scratch the furniture, bark in the night and leave messes on the carpet, but you will want to search for individual characteristics rather than just combinations of characteristics.

The answer is to use binary operators on the behaviour codes. To extract a single binary digit from a number, you apply a mask which is another binary number with the digits that you want to test for set to 1. This is ANDed with the behaviour code and if the answer also contains a 1 in that digit, then the characteristic has been found. If the answer contains a 0 in that digit, then the original binary number contained a 0 at that position and the characteristic has not been found:

Rather than incorporate this into your function that deals with queries that you wrote in the last task, thus making it even more unwieldy, it is probably best if you write a different function for this task, and then write some other function that calls either one of them as appropriate.

Task 7

Now you have most of the building blocks for the entire database. You should write a main() function that ties them altogether. There are one or two options that we haven't talked about yet. For example, how would the user add a record to the database? This is easy enough to achieve - just increase the number of records by one (assuming that that wouldn't push the database past it maximum number of records) and enter the data for the new record.

Deleting records is a little harder. If the record to be deleted is the one at the end, then all the program has to do is reduce the number of records by one and pretend that the deleted record doesn't exist. The next time a record is added, that duff record will be overwritten with new data.

On the other hand, if the record to be deleted is not the last in the database, then all the records after it will need to be "shuffled up" one place. This is achieved by a for loop that steps through all the records following the new "gap" and moves them along one slot:

for (int x = index; x < num_records-1; x++)
  dogs[x] = dogs[x+1];

Note that the limiting condition is now one less than the number of records as the loop counter is made to point to the current record and the one after it.

The essence of this task, therefore, is to tie the functions that you have written so far into a useable program. You will probably have to write a few more functions to get the whole thing working.

Discussion

In this project you have produced a sophisticated program with all the major features of a standard database - namely, entering and deleting data, searching for individual records, displaying them on the screen, and finally saving the data on to a disc.

There are one or two drawbacks, of course. A major one is that the array in which the structures are stored has a finite number of elements. If you define your array as containing 100, say, elements, then you are restricted to a maximum of 100 dogs. Trying to solve this problem by making the array very large (say 1000 dogs) runs the risk of making too heavy demands on the computer's memory. The compiler may well set an upper limit on the size of the array, especially if the structures from which it is formed are themselves rather large. It's a trade-off between record size and the maximum number of records in the database. Even if you were to declare a vast array of records, the chances are that you wouldn't use many of them, in which case, the rest of the memory reserved would be wasted.

There is a solution to this problem, namely, a linked list, which allocates memory dynamically as and when it is needed, and never uses more than it needs. These rely on a data type called a pointer, which you will meet in a later project.

Another, more fundamental problem, is that format of the record is fixed, once the program has been finished. A professional database, such as Microsoft Access, would allow the records to be designed (and re-designed) on the screen, even after data had been entered into the database. With your program, the user may find, after using it happily for a year, that he or she needs another field in the database. Making the program more flexible in this regard is rather complicated, and is best left until you have a lot more experience of C++.

Extensions

There are several ways of extending a database like this. For instance, the data is always re-saved to the disc just before the program terminates. If the user simply used the database to examine the data, then it hasn't been changed, and re-saving it is inefficient. It is a fairly easy matter to declare a global variable that indicates whether any of the data has been changed. If it has, then the data will need to be saved back on to the disc.

The most obvious possibility for improvement is a facility to print the results on paper. This you can't do at the moment as I haven't taught you about accessing the printer. However, when you do learn about it, you can return to this project later.

You might also like to consider features such as counting the records that have been entered into the database, or the number of dogs that were found in any particular search. This is straight-forward - just declare a counter variable, and increase it by one every time a match is found.

Finally, a search mechanism that searches parts of strings would be useful. For instance, you may want to track down the details of a dog where you only know part of its address (its owner lives on Main Street, but you don't know the number). At the moment, the search function will only match whole field contents. There is a built-in string function that searches for one string within another. Use the 'Help' option in your version of C++ to track it down (or write your own ….)

Questions and Answers

Yes. Just add the definition to the .h file (not the .cpp file with the same name) and remember to #include the file in your program.

No problem! This is done in a similar way to declaring a two-dimensional array, with sets of curly brackets within other sets of curly brackets. Here is an example of such an array:

struct street
 { string name;
   int num_houses;
   float length;
 };
street Roehampton[4] =
 {  { "High Street", 38, 107.4 },
    { "College Gardens", 12, 51.0 },
    { "Ardex Lane", 41, 150.8 },
    { "Huntley Way", 30, 82.2 }    };

In this case, Roehampton[2].name becomes "Ardex Lane" and Roehampton[3].num_houses becomes 30 etc.

There are several issues that you have to be careful of if you want to do this. Firstly, it is perfectly possible. Passing the structure as a reference parameter lets you change the elements of the structure and have the changes reflected back into the main program.

Passing a structure as a value-only (not reference) parameter can make a program rather inefficient as it means that a large amount of data has to be copied (so that the original can't be altered) and given to the function. With very large structures, this can slow the program down considerably. Some programmers solve the problem in the following way:

void process_book (const book_type& b);

I have given this as a function prototype (note the semicolon). The parameter is called b, and it is of type book_type, which is a structure crammed full of elements. The parameter is a reference parameter (so that it can be changed) and is marked const (so that it can't be changed). So what's going on?

Well, the fact that the parameter is a reference one means that rather than a copy of the structure being passed, only a reference to it is passed requiring a small amount of memory. However, we don't want the structure to be changed, so we include the word const.

The priority level for & and | is the same as for && and || in conditions, that is & always takes priority over | unless parentheses are put round the | operation, so 200 | 109 & 71 produces the answer 207, as 109 & 71 is carried out first, giving 69, and 200 | 69 is carried out last, giving 207.

However, (200 | 109) & 71 produces the answer 69, as 200 | 109 is carried out first, giving 237, and 237 & 71 is then carried out, leaving 69:

Summary