CS302 Lecture Notes - Class Pictures: Vectors and Classes

In order to learn y'all's names, I take your pictures and then study up. My study guide, of course, involves writing a program to help me.

My program takes a ``Roster'' file, which contains your names, one per line. It assumes that there are pictures in the directory Pictures, and that the pictures are contiguously numbered. The first picture corresponds to the first line of the Roster file, and so on. For example, I have a Roster file for a fictitious class in Roster.txt, and the pictures are in the directory Pictures:

UNIX> cat -n Roster.txt | head
     1  Gurthro Steenkamp
     2  John Smit
     3  Jannie du Plessis
     4  Bakkies Botha
     5  Victor Matfield
     6  Heinrich Brussow
     7  Francois Louw
     8  Pierre Spies
     9  Rickey Januarie
    10  Morne Steyn
UNIX> ls Pictures | head
001001.jpg
001002.jpg
001003.jpg
001004.jpg
001005.jpg
001006.jpg
001007.jpg
001008.jpg
001009.jpg
001010.jpg
UNIX>  

Our goal is to write a program that will create an HTML file to help me study. The HTML file should have the pictures displayed randomly, and then there should be an option to either include the names with the pictures, or not (so I can test myself).

We're going to structure this in a standard C++ way. We're going to define a class called a Roster, in a header file roster_01.h:

#include <iostream>
#include <cstdlib>
#include <vector>
#include <string>
using namespace std;

class Roster {
  public:
    void Add_name(string name);
    void Print();
  protected:
    vector <string> names;
};

Classes divide their data and methods into public and protected. If something is public, then anyone may access it. If something is protected, then the data/methods may only be used within the implementation of a class' method. Typically, we make all of the data protected, and only have the methods be public. The reason for this is so that users of a data structure cannot mess up the data -- they only gain access through the methods, which keep the data safe.

In this example, we have one piece of protected data -- a vector of names, which will contain the roster. There are two methods -- Add_name() adds a name to the roster, and Print() prints the roster.

We're going to do this implementation incrementally, which is how you should always program. It lets you test as you go, and you find bugs much more quickly. Those of you in class got to see that first hand, as my programs definitely had a few bugs as I wrote them.

We'll implement the class in roster_01.cpp. This is a very simple implementation: Add_name() simply appends the name to the vector, and Print() simply prints out the names:

#include "roster_01.h"

void Roster::Add_name(string name)
{
  names.push_back(name);
}

void Roster::Print()
{
  int i;

  for (i = 0; i < names.size(); i++) cout << names[i] << endl;
}

We'll add a main() routine to complete the program in roster_01_main.cpp. It does a little error checking, then reads the roster file, calling Add_name() with every name. At the end, it calls Print().

#include <fstream>
#include "roster_01.h"

main(int argc, char **argv)
{
  Roster r;
  ifstream fin;
  string name;

  if (argc != 2) {
    cerr << "usage: roster_main filename\n";
    exit(1);
  }

  fin.open(argv[1]);
  if (fin.fail()) { perror(argv[1]); exit(1); }
  
  do {
    getline(fin, name);
    if (!fin.fail())  r.Add_name(name);
  } while (!fin.fail());

  r.Print();
}

Since we declared r as a local variable, the instance of the Roster class is created as soon as the main() program starts. That class starts with an empty names vector. For that reason, we do not need to do any initialization of the Roster class. The makefile lets you compile -- when we run it, it prints out the contents of Roster.txt (at this point, you should copy the files to your directory and make/run them.):

UNIX> cp -r ~jplank/cs302/Notes/Class-Pictures .
UNIX> cd Class-Pictures
UNIX> make clean
rm -f *.o roster_??
UNIX> make roster_01
g++ -c roster_01.cpp 
g++ -c roster_01_main.cpp 
g++ -o roster_01 roster_01.o roster_01_main.o
UNIX> roster_01 Roster.txt | head
Gurthro Steenkamp
John Smit
Jannie du Plessis
Bakkies Botha
Victor Matfield
Heinrich Brussow
Francois Louw
Pierre Spies
Rickey Januarie
Morne Steyn
UNIX> 

roster_02 -- Turning the names into HTML

I don't want to go into a HTML tutorial here -- we are going to create a HTML table of names and pictures. Here are the relevant HTML tags for tables: We're going to update our Print() method so that the user may specify the number of columns in the HTML table, and then the user specifies that on the command line. The changed files are roster_02.h, roster_02.cpp and roster_02_main.cpp. The main modification is the Print() method in roster_02.cpp:

void Roster::Print(int columns)
{
  int i, c;

  c = 0;
  cout << "<table border=2>\n";

  for (i = 0; i < names.size(); i++) {

    if (c == 0) cout << "<tr>\n";   // Start a new row when c = 0.

    cout << "<td>" << names[i] << "</td>" << endl;

    c++;
    if (c == columns) {            // End the row when c == columns
      cout << "</tr>\n";
      c = 0;
    }
  }

  if (c != 0) cout << "</tr>\n";   // If the last row is incomplete, end it.
  cout << "</table>\n";
}

You should also check out the error checking of the command line in roster_02_main.cpp in case you're unfamiliar with using sscanf().

We compile and run it below:

UNIX> make roster_02
g++ -c roster_02.cpp 
g++ -c roster_02_main.cpp 
g++ -o roster_02 roster_02.o roster_02_main.o
UNIX> roster_02 
usage: roster_main filename columns
UNIX> roster_02 Roster.txt 6 > roster_02_example.html
UNIX> 
You can look at the resulting HTML file in roster_02_example.html (Click the link to see it as an HTML file):

<table border=2>
<tr>
<td>Gurthro Steenkamp</td>
<td>John Smit</td>
<td>Jannie du Plessis</td>
<td>Bakkies Botha</td>
<td>Victor Matfield</td>
<td>Heinrich Brussow</td>
</tr>
<tr>
<td>Francois Louw</td>
<td>Pierre Spies</td>
<td>Rickey Januarie</td>
<td>Morne Steyn</td>
<td>Bryan Habana</td>
<td>Jean de Villiers</td>
</tr>
<tr>
<td>Jaque Fourie</td>
<td>JP Pietersen</td>
<td>Frans Steyn</td>
<td>Chiliboy Ralepelle</td>
<td>BJ Botha</td>
<td>Andries Bekker</td>
</tr>
<tr>
<td>Dewald Potgieter</td>
<td>Ruan Pienaar</td>
<td>Juan De Jongh</td>
<td>Zane Kirchner</td>
</tr>
</table>


roster_03 -- Using a constructor to initialize the Roster

I'd like to add another command line argument to the program -- the starting number of the first picture. You'll note that the first file in Pictures is 001001.jpg. I'd like to have the number 1001 be a command line argument, and then I'll use that to construct all of the proper filenames for the pictures.

I'd like to have this number be part of the class. It will be a piece of the class's protected data, and I'll set it when I create an instance of the class. To do that, I need to define a new constructor method for the class, which takes the starting number as a parameter. Here is the new class defintion, in roster_03.h

#include <iostream>
#include <cstdlib>
#include <vector>
#include <string>
using namespace std;

class Roster {
  public:
    Roster(int starting_number);     // The constructor, which takes an integer as a parameter.
    void Add_name(string name);
    void Print(int columns);
  protected:
    vector <string> names;
    int start;
};

In roster_03.cpp, we define the constructor, which simply sets the new start variable to the constructor's parameter, and then constructs the filename in the Print() procedure:

#include "roster_03.h"

Roster::Roster(int starting_number)
{
  start = starting_number;
}

/* Skipping into the Print() method -- here's where we print the filename and name:: */

    cout << "<td>";
    printf("Filename: Pictures/%06d.jpg
", i+start); cout << names[i]; cout << "</td>" << endl;

Remember that printf() statement -- it pads the number to six digits, and includes leading zeros.

Finally, we have an issue with our main() routine. Previously, we had declared r is a parameter, which was fine since it did not take an explicit constructor. If we try that now, we'll get a compilation error. To fix it, we change r to be a pointer, and call new with the starting number as a parameter, which is passed to the constructor.

Here is the code for roster_03_main.cpp

#include <fstream>
#include "roster_03.h"

main(int argc, char **argv)
{
  Roster *r;
  ifstream fin;
  string name;
  int columns, starting_number;

  if (argc != 4) {
    cerr << "usage: roster_main filename starting_number columns\n";
    exit(1);
  }

  if (sscanf(argv[2], "%d", &starting_number) != 1 || starting_number <= 0) {
    cerr << "usage: roster_main filename starting_number columns -- bad starting_number\n";
    exit(1);
  }

  if (sscanf(argv[3], "%d", &columns) != 1 || columns <= 0) {
    cerr << "usage: roster_main filename starting_number columns -- bad columns specification\n";
    exit(1);
  }

  r = new Roster(starting_number);

  fin.open(argv[1]);
  if (fin.fail()) { perror(argv[1]); exit(1); }
  
  do {
    getline(fin, name);
    if (!fin.fail())  r->Add_name(name);
  } while (!fin.fail());

  r->Print(columns);
}

Since r is a pointer, we access the methods with -> instead of a dot.

UNIX> make roster_03
g++ -c roster_03.cpp 
g++ -c roster_03_main.cpp 
g++ -o roster_03 roster_03.o roster_03_main.o
UNIX> roster_03
usage: roster_main filename starting_number columns
UNIX> roster_03 Roster.txt 1001 6 > roster_03_example.html
UNIX> 
Here's roster_03_example.html.

roster_03_evil -- "But I don't like pointers."

The fact that we need to construct our instance of r after parsing the command line means that we have to use a pointer and new. You might ask, "Can't I do it without a pointer?" The answer is yes you can, but I don't like it. I've put the code in roster_03_evil.cpp: Here's the file:

#include <fstream>
#include "roster_03.h"

main(int argc, char **argv)
{
  ifstream fin;
  string name;
  int columns, starting_number;

  if (argc != 4) {
    cerr << "usage: roster_main filename starting_number columns\n";
    exit(1);
  }

  if (sscanf(argv[2], "%d", &starting_number) != 1 || starting_number <= 0) {
    cerr << "usage: roster_main filename starting_number columns -- bad starting_number\n";
    exit(1);
  }

  if (sscanf(argv[3], "%d", &columns) != 1 || columns <= 0) {
    cerr << "usage: roster_main filename starting_number columns -- bad columns specification\n";
    exit(1);
  }

  Roster r(starting_number);

  fin.open(argv[1]);
  if (fin.fail()) { perror(argv[1]); exit(1); }
  
  do {
    getline(fin, name);
    if (!fin.fail())  r.Add_name(name);
  } while (!fin.fail());

  r.Print(columns);
}

You'll note that I've put the variable declaration after the processing of the command line. That way, r does not get constructed until starting_number has been initialized. It works fine:

UNIX> make roster_ev
g++ -c roster_03_evil.cpp 
g++ -o roster_ev roster_03.o roster_03_evil.o
UNIX> roster_ev Roster.txt 1001 6 > roster_ev_example.html
UNIX> 
However, I really don't like that style of programming. Why? Because in my opinion, programs and procedures should have variable declarations and then code. When variables are declared inline, it makes the code that much harder to read and debug. I believe you do much better to declare a pointer with the variable declarations, and then use new. You will not see me ever declare variables in the middle of a procedure. I would prefer that you not do that either.

roster_04 -- Some simple modifications

I've made some simple modifications in roster_04.h, roster_04.cpp and roster_04_main.cpp. These are:
  1. I have changed the HTML to include the image tags for the pictures.
  2. I have added an extra command line argument print_names, which says whether or not to print the names in the final HTML.
  3. I have changed the Print() method to take an integer print_names which says whether or not the method should print the names in the final HTML.
I won't put them here -- click on the links to see the changes. Here they are in use:
UNIX> make roster_04
g++ -c roster_04.cpp 
g++ -c roster_04_main.cpp 
g++ -o roster_04 roster_04.o roster_04_main.o
UNIX> roster_04 
usage: roster_main filename starting_number columns print_names(yes/no)
UNIX> roster_04 Roster.txt 1001 6 yes > roster_04_names.html
UNIX> roster_04 Roster.txt 1001 6 no > roster_04_no_names.html
Take a look at the two output files:

roster_05 -- Randomizing the output

Finally, I'd like the pictures to be randomly ordered, to help me test myself better. There are many ways to randomize a list, but the one presented here is a good one. Suppose I have n pictures. What I'm going to do is create an vector called randomize, which contains the numbers from zero to n-1 in random order. Then, instead of using i to print out the names and the pictures, I'll use randomize[i]. Here's the code to print out the pictures:

  for (i = 0; i < names.size(); i++) {

    if (c == 0) cout << "<tr>\n";   // Start a new row when c = 0.

    cout << "<td>";
    printf("<IMG src=Pictures/%06d.jpg height=100>", randomize[i]+start);
    if (print_names) cout << "<br>" << names[randomize[i]];
    cout << "</td>" << endl;

    c++;
    if (c == columns) {            // End the row when c == columns
      cout << "</tr>\n";
      c = 0;
    }
  }

To create the randomize vector, what we do is initialize it with the numbers 0 through n-1 in order. Then, we construct a for loop that uses a variable j, which starts at n and is decremented down to one. At each iteration of the for loop, we choose a random number i between 0 and j-1. We then swap element i with element j-1. That randomizes the array in O(n) time, which, if you think about it, is as good as we can do.

The code changes are in roster_05.h, roster_05.cpp and roster_05_main.cpp. Here's the code from roster_05.cpp that constructs the vector:

void Roster::Print(int columns, int print_names)
{
  int i, c, j, tmp;
  vector <int> randomize;

  randomize.resize(names.size());
  for (i = 0; i < randomize.size(); i++) randomize[i] = i;
  for (j = randomize.size(); j > 0; j--) {
    i = lrand48()%j;
    tmp = randomize[j-1];
    randomize[j-1] = randomize[i];
    randomize[i] = tmp;
  }

The only other modification is that I call srand48(time(0)) to seed the random number generator in roster_05_main.cpp.

Here's example output in roster_05_names.html.


Lessons Learned

What are the things that we've learned from this lecture: