IC210 Class 35: Breaking Programs Into Pieces

Lecture

Last lecture we basically saw how programs get broken up into pieces using using .h files and #include. This lecture we'll look into more detail about how that's done, the organization benefits of doing it, and a few of the pitfalls.

Reading

None

 

The Rules in Short

1.      A function may only be defined once in a program, no matter how many files may be involved!

2.      A function's prototype may appear in as many files as you like, but only once per file! Also. occurrences of the same prototype in different files must agree. You can't, for example have the prototype bool f(int); in one place and string f(int); in another.

3.      A struct's definition may appear in as many files as you like, but only once per file! Also, it must be the same in every file.

These are the solid C++ rules, here's the Golden Rule however: Every prototype or struct definition should exist only in one file. If you need that struct or function in several files, put the struct definition or prototype in a .h file and use #include to put them in other files. This is wisdom, pay attention to it!

Now, there's one trick that can come up, which is illustrated by the following example:

point.h

triangle.h

main.cpp

struct point
{
  double x, y;
};
void read(point&);
            
#include "point.h"
struct triangle
{
  point vertex[3];
};
void read(triangle&);
            
#include "point.h"
#include "triangle.h"
 
int main()
{
  triangle T;
  read(T);
  point P = T.vertex[0];
}
            

The trick is that this causes the struct point to be defined twice in main.cpp. Why? Well, point.h includes the definition, and point.h is #included in main.cpp, so that's the first definition of point in main. However, main also #includes triangle.h, which in turn #includes point.h, which has the definition of the struct point, so that's the second definition of point in main. Admittedly, if the programmer was on top of things he could've avoided this, but it's not always avoidable, and it's expecting alot of the guy who uses structs point and triangle to go back and worry about this stuff.

The way around this problem is to guard your .h files with #defines. The #include that we've been using and the #define that we're about to use are examples of "preprocessor directives". The #define lets you give a string x and say "every time you see x in the source code, replace it with string y." This can be used and abused in all sorts of ways, but for the moment we'll simply use #define to determine whether or not this is the first time the compiler has seen our .h while compiling a give .cpp file. We can test whether or not a certain string has been #defined already using the #ifndef preprocessor directive, which we read as "if not defined". For example

  cout << "This is ";
#ifndef SILLYSTYRING
  cout << "not ";
#endif
  cout << "much fun!" << endl;

If SILLYSTRING has been #defined previously in our program, the word not won't get printed out --- the #ifndef SILLYSTRING asks if it's true that SILLYSTRING hasn't been defined. If it is, then all the text until the #endif is seen by the compiler. Otherwise, as in our case, it is as if the code in between was never there.

How does this help us with our multiple-definition problem?

point.h

triangle.h

main.cpp

#ifndef POINTHEADER
#define POINTHEADER
 
struct point
{
  double x, y;
};
void read(point&);
 
#endif
            
#include "point.h"
struct triangle
{
  point vertex[3];
};
void read(triangle&);
            
#include "point.h"
#include "triangle.h"
 
int main()
{
  triangle T;
  read(T);
  point P = T.vertex[0];
}
            

Now, in compiling main.cpp we first #include the file point.h. Since this is the first time the compiler has seen point.h it'll find that POINTHEADER has not been defined. Therefore all the code until the #endif, which is essentially the whole file, will be seen and evaluated by the compiler. This will include the line that #defines POINTHEADER. Now, the next time we come across point.h, which is when main.cpp does the #include "triangle.h", the compiler will hit that #ifndef line and will find that POINTHEADER has been defined, and therefore everything up to the #endif, which is essentially the whole file, will be ignored by the compiler. This way we never get any multiple definitions.

Golden rule number 2: All .h files should be "protected" by #ifndefs this way!

 

A Problem to focus on

We'll start with a problem to focus on as a goal. It's certainly too big to write in total in a class period, but we're going to focus on the big-picture structure of the program (i.e. what structs, functions and files) rather than on the details of how each function actually works. Here's the problem: Write a program that produces meeting reports. The program will allow the user to:

1.      Read in committee info, either from the user or from a file A committee will consist of 5 members, one of which is designated Committee Chair, and of course the committee has a name! You may assume that each committee member has a two-word name. The user command is load source, where source is either keyboard or a filename, depending on whether the user will enter the committee info by hand or read it from a file. From a file or from the user, the format should be committee name on the first line, committee chair name on the second line, and the other 4 committee member names on the following lines.

2.      Change Committee Chair The user should be able to name a new Chair from amongst the comittee members. The user command is makechair first last, where first last is the name of the new chair.

3.      Print a committee minutes to a file The user command is writeminutes destination date minutes endminutes The minutes should be written to the file destination, and should begin with the date, the comittee name, the chair, and the rest of the committee in alphabetical order. Dates should be input in mm/dd/yyyy format, but should appear in the minutes as <day> <monthname>, <year>

4.      Save committee info to a file The user command should be save filename The file saved should be in the proper format to read in the next time.

5.      Quit

So, suppose we have the following data file comm.txt, which contains a definition of the very powerful and important "Committee on Confusing Midshipmen". This run of the program along with the minutes file it creates, and the new version of comm.txt it makes.

Program:

load comm.txt
makechair Dr. Brown
writeminutes CCM110802.txt 11/08/02
New Chairman Brown wrested control of the Committee from the senior faculty!
endminutes
save comm.txt
quit

CCM110802.txt

8 November, 2
 
Minutes of the Committee on Confusing the Midshipmen
Dr. Brown, Committee Chair
 
Committee Members:
Dr. Crabbe
CDR Osborn
Dr. Needham
Dr. Crainiceanu
Dr. Delooze
Dr. Schulze
 
Minutes:
New Chairman Brown wrested control of the Committee from the senior faculty! 

comm.txt

Committee on Confusing the Midshipmen
Dr. Brown
Dr. Schulze
Dr. Needham
CDR Osborn
Dr. Crainiceanu
Dr. Delooze
Dr. Crabbe

Admittedly, this is a big problem! We'll need all of our problem solving techniques --- top-down design, bottom-up design, iterative refinement. They all work together, and breaking code up into files helps facilitate all of it.

Part 1 - Reading & Writing Committees

Let's use the iterative refinement technique of starting out with a program that offers only a part of the functionality required of the final product. Let's write a program that does nothing but read and write committees. In other words, let's just implement the load and save commands. Now using the top-down design technique in which we make a "wishlist" of functions and structs, we say "I wish I had a struct Committee that stored all the info for a committee, and functions read and write for reading and writing committees, both to-and-from files and to-and-from the screen". If I had such things, perhaps in a file Committee.h, I could write my main like this:

#include "Committee.h"
 
int main()
{
  Committee C;
  string command;
 
  while(cin >> command && command != quit)
  {
    ////// LOAD COMMAND //////
    if (command == "load")
    {
      string source;
      cin >> source;
      if (source == "keyboard")
  read(C,cin);
      else
      {
  ifstream fin(source.c_str());
  read(C,fin);
      }
    }
 
    ///// SAVE COMMAND /////
    if (command == "save")
    {
      string name;
      ofstream fout(name.c_str());
      write(C,fout);
    }
  }
 
  return 0;
}

Clearly what we need to do is go off and give the struct definition and function prototypes in a file Committee.h, and later we can get around to actually implementing the functions in a file Committee.cpp. Let's focus on defining the struct Committee first, since this is needed for the two prototypes. Well ... a committee has a name, which we'll store in a string, and it has five committee members, one of whom is the Chair. A committee member has a first and a last name, so there's no built-in type to represent that for me. So, I say "I wish there was a type Member that could store a first and a last name for me ... and I'll probably want read and write functions for this type, as well." Well, let's assume there is! (It's .h file might look like Member.h and, in bottom-up fashion, whose .cpp file might look like Member.cpp) We'll define Committee as:

struct Committee
{
  string name;
  Member chair, mems[4];
};

So that a Committee consists of a name, a chair and 4 members. Additionally we want functions for reading and writing, whose prototypes would be:

void read(Member&,istream&);
void write(Member,ostream&);

Now, the struct Member comes from the Member.h header file, so our Committee.h header file looks like

/***************************************
 ** Header file for struct Commitee and 
 ** associated functions
 ***************************************/
#include "Member.h"
 
#ifndef COMMITTEEHEADER
#define COMMITTEEHEADER
 
struct Committee
{
  string name;
  Member chair, mems[4];
};
 
void read(Committee&,istream&);
void write(Committee,ostream&);
 
#endif

Putting the whole program together, we have:

Member.h ----------------------> Member.cpp
  |                                |
  |                                |
  V                                |
Committee.h ---> Committee.cpp     |
  |                  |            /
  |                  |           /
  V                  |          /
main.cpp             |         /
   \                 |        /
    \                |        |
     \               V        V
      ------------> THE PROGRAM

Part 2 - Switching Chairs

Adding the functionality to switch chairs is nice, because it's natural to do this in main by simply adding

    if (command == "makechair")
    {
      Member M;
      read(M,cin);
      makechair(C,M);
    }

In other words, you read in a member and say "I wish there were a function makechair that could modifiy the committee to make a new member the chair" ... and then assume there is! The prototype for makechair would naturally be added to Committee.h and the definition of the function would naturally reside in Committee.cpp. The structure of the program in terms of files looks just like it did before.

 

Part 3 - Printing Reports, the last step

The only command we're missing now is "writeminutes". This one will take a bit of work, because now in addition to dealing with "Committees" and "Members" we have to deal with "Dates". So, the natural thing to do is create yet another struct - this one for "Date" objects. Naturally, this will go in its own .h file and have its own .cpp file as well. In main.cpp we'll add the code for our next command, which will "assume" we have a function "writeminutes", because gee wouldn't it be easier if we did?

    if (command == "writeminutes")
    {
      string name;
      cin >> name;
      ofstream fout(name.c_str());
      writeminutes(C,fout);
    }

Of course, now we have to go and implement writeminutes and create Date.h and Date.cpp. When we do, we end up with a program that, on the file level, is structured like this:

Member.h ---------------------------------> Member.cpp
  |                                            |
  |                Date.h ----> Date.cpp       |
  |                  |            |            |
  V                  V            |            |
Committee.h ---> Committee.cpp    |            |
  |                  |            |            /
  |                  |            |           /
  V                  |            |          /
main.cpp             |           /          /
   \                 |          /          /
    \                |         /          /
     \               V        \/         /  
      ------------> THE PROGRAM <--------

 


Assoc Prof Christopher Brown

Last modified by LT M. Johnson 11/09/2007 01:15 PM