Texas A&M University primary mark

Department of Computer Science and Engineering

Sample Image

CSCE 121: Introduction to Program Design and Concepts

Lab Exercise Twelve


Objective

This lab involves object-oriented programming: you'll need to make your own classes and instantiate objects to represent data. Because objects have methods (functions) as well as data fields (variables), we sometimes say they possess behavior. This lab will give you some practice with those things and also give you some feeling for two new things: the C++ vector class as well as overloading of operators. (We'll not cover many of the details of these two, but you should have some exposure to them because they'll be particularly important in subsequent Computer Science courses.)

But you should definitely focus on three key aspects as they're important fundamentals underlying object-oriented programming in general. They'll be covered in practical form in the lab, but are the following:

  • Encapsulation— Classes define the data and functionality associated with a certain entity. They should be as cohesive as possible, usually capturing one idea.
  • Inheritance— Usually one tries to tease apart aspects of the problem being solved. Common traits should be factored out and inheritance enables us to implement repeated functionality without repeating (much) code.
  • Polymorphism— When referring to objects, it is possible treat elements in a common way even if the elements being referred to are not identical. (There'll be a bit more explanation for this concept below.)

Approach

For each of the three items above, start by doing the particular practical thing that you're asked to do below. But once you've managed that, try to reflect on the general value of this idea for programs more generally. It may be helpful to think about previous lab questions and to ask yourself: "How would I do that differently if I were to use objects and classes?"


A Custom Media Manager

Recently you've been frustrated by your phone's poor shuffling when listening to music. You decide to write your own application to manage your playlists so that you'll have maximal control over your media.

After thinking about things, you decide to organize your project into multiple classes. You settle on the classes shown in the diagram below. (General hint: nouns are good candidates for classes?) Some classes inherit properties from others, forming a hierarchy, and arrows in the figure show this.

For each of these classes it is best to have separate header and source files. Here's a summary of the design:

  • You have a class to encapsulate a playlist, called Playlist. It will have a way to store multiple entries, for which you define a class called PlaylistEntry, used to describe audio files and video media.
  • You have AudioEntry and VideoEntry classes for those two broad types of media. For audio, you might be interested in whether it is music or voice; for video you might be more interested in resolution, or similar aspects. These classes allow you to draw out those aspects.
  • Next, you look at the files on your computer and decide to focus on your .mp3 and .wav files as a starting point.

The elements shown in blue are provided for you as starting code; you can grab it all here. There is also a file called mediajuke.cpp which contains the main program.

Starting out: AudioEntry, Mp3Entry, and WavEntry

The first steps are to look at the code provided, compile it, and to see what it does. You'll need to provide it an example playlist. Here are two: 1 and 2. You need to pass the filename to the executable as a command line argument.

You'll see that there are blocks of code surrounded by preprocessor code. Something like this:

#ifdef DEBUG
    cout << "A Message" << endl;
#endif

By default, the code to print this message will be skipped by the compiler. But if you pass g++ the -DDEBUG flag, then all that code will become active. It should give you lots of output, and help you to see what is going on. Once you're able to run the program, trace through it to see what is defined where. With multiple files and inheritance, it can take a bit of exploring to find out exactly which class implements the bits of functionality you're seeing.

You should be able to work out how it operates. The Playlist constructor takes in the filename, opens the file, and reads it a line at a time. It splits each line on the basis of the character that is the first space. The first bit is treated as a code to tell you what sort of media file is being used; the rest of the line is passed to the appropriate constructor for it to initialize the object.

Step 1: Using the other two audio formats as a guide, define and implement a class for .ogg files.

?HINT
To test your new code audio format, you'll need to add an appropriate entry in a playlist file. Also, Playlist's constructor will need to be augmented so that it calls your code when it sees, for example, the tag "OGG".

The Playlist class in a bit more detail

The header file describing the Playlist class uses some pieces of syntax we've not seen before. These are both handy features, so let's look at the each in turn.

The vector container: Notice that there is a new header called vector which has been included. Since it gets a bit tedious to have to make (and manage) linked-lists for all the resizable data we have, C++ provides support for various types of collections. The vector is perhaps the simplest and most useful. Here is a simple example showing how we can make and use vectors to store data of different types. The less than and greater than signs act as brackets and specify what sorts of elements go into the container. So a vector<int> is a list of ints, while a vector<string> is a list of strings. (Because we specify some other type to give the full details of the vector itself, it is an example of generic programming.)

Within Playlist, the C++ vector is used to define a private field called the_entries. It is a list of pointers to items in the playlist, i.e., (PlaylistEntry *) elements. The syntax is a bit more involved, but it is directly analogous. Play around a bit with the vector<int> example to get comfortable with it. You can see that it is used in a very straightforward fashion for the code here.

An << operator of our own: If you look in the main function in mediajuke.cpp, you'll see that the whole playlist is printed by doing this:

cout << pl << endl;

Of course, since we've defined the Playlist type ourselves, if we want to make our class behave like any builtin type in C++, we need to describe what must be done to print our class. The function

ostream& operator<<(ostream& os, const Playlist& pl)

will do this for us. (It may not look like a function, at first sight, but the keyword operator says that the next characters are actually an exotic name for a function.) The function takes a reference to an ostream—that is an output stream— and a reference to our type, Playlist, and will return a new ostream reference. Strange syntax aside, you can see that the implementation of this function is pretty straightforward.

Incidently, it is defined on an ostream because that << we defined for Playlists will allow us to write to files as well. How can that be? It turns out that the streams are classes themselves. They're also arranged in a hierarchy, so if we implement functionality for an ostream, then the reuse that the object-oriented approach enables, allows cout and other streams, which inherit from ostream, to have that functionality too. For free.

One more thing that needs explanation is that friend keyword. The << operator is just an exotically named function. It is not a member of the class. Notice that the definition doesn't say Playlist::operator<<…. Because of this, the function would not be able to read the private data in the class. That makes it hard to print the particular playlist. Instead the friend keyword says that this function has special access to the data in the class. That allows it to behave as if it were a member function.

Polymorphism: Object-oriented languages that support inheritance allow us to make subtypes. For example, we say that the WavEntry is a subtype of AudioEntry as the former inherits properties form the latter. In the Playlist class, we have a list (implemented as a vector) of PlaylistEntrys. There actually aren't any objects that are just instances of PlaylistEntry, but we can treat the other subtypes as if they were nothing other than PlaylistEntrys. We treat them in a unified way, though they can actually be quite different underneath. This is yet another example of information hiding as a design principle.

The PlaylistEntry class is an abstract class

In the preceding, I pointed out that there aren't any objects that are just instances of PlaylistEntry. They're always some element further down the class hierarchy. If you look at PlaylistEntry in detail, you'll see that it has no implementation of the play method. The "= 0;" in the declaration tells the compiler that there will not be any implementation.

Classes that do not provide an implementation of some methods are called abstract classes; they are intended to be extended by subclasses which will fill in the requisite detail. You'll quickly discover that a class is abstract if you try to create an instance of it; the C++ compiler will raise an error. To try it for yourself, do the following:

PlaylistEntry *tester = new PlaylistEntry("foo");

Are there other abstract classes in the source code you were provided?

Implementing some extra functionality

Step 2: A useful bit of practice is to design and implement classes of movies. The VideoEntry class should contain aspects specific to video clips, such as their resolution (e.g., 640 × 480 ) and color depth (e.g., 8- or 16-bit color). The AviEntry and FlvEntry subclasses could have aspects particular to those formats; such as version numbers, etc.

If things are properly designed you should only need to add a few lines to playlist.cpp to have it recognize this new format and call the appropriate code in the virtual methods you've defined. Thus, we notice that Playlist is decoupled from the details of the particular formats.

Step 3: Implement a method double Playlist::total_duration() that computes the time needed to play the whole playlist. Add the following code to your main function, after the playlist has been printed out:

cout << "Total play time = " << pl.total_duration() << "s" <<  endl;

A Playlist as an PlaylistItem

Now suppose that you decide that you want to allow playlists that contain other playlists. The idea of recursion in this way seems attractive and interesting. This can be achieved by actually making Playlist a subclass of PlaylistItem. The diagram on the right shows the updated hierarchy.

Here's an example of a playlist that includes another playlist as an entry in the playlist.

Step 4: Add code to make Playlist a subclass of PlaylistItem. (You'll need to make the appropriate call in the constructor and implement a play() function).

Compare the output of your program with mine for the playlist above.

• Texas A&M University •