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>
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> |
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 |
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.
#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.
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.htmlTake a look at the two output files:
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.