CS 1124 — Object Oriented Programming

Class Basics

Topics

Writing a minimal class

In this article we will explain the basics of creating and using a class. There is a lot that we won't cover here, but this will be enough to get us started.

What's the simplest class we can write?

class Simplest {};  // Remember that semi-colon, otherwise there will be confusing error messages.

This class definition defines a type called Simplest. We can declare variables of type Simplest

Simplest x;

but there's not much more we can do with it. Note that unlike Java, that line also defines a Simplest object. The object was initialized using the default constructor, which in this case, was provided by the compiler. (We discuss constructors below.)

The public interface

Member Functions (aka methods)

A class needs a public interface, in order to be able to do anything.

class Simple {
public:
   void display() {cout << "Displaying a Simple object\n";}  // Note: will be done better, later on.
};

With the class Simple, we can now do more than just create an instance of the type Simple. We can also call its display function.

Simple simpleton;
simpleton.display();

Functions like display are referred to as member functions. They are also called methods. Notice above how we called the function. First we defined a variable simpleton, that held and instance of the class Simple. Then, using simpleton we called the method display by first typing simpleton, then "dot" and finally display().

Member Variables

A class like Simple is still not very interesting. Variables of the type Simple hold instances that are all exactly the same and do the exact same thing. They don't have anything inside themselves to make each one different. (Ok, the do occupy different locations in memory, but that's a topic for a different discussion.)

What if each object had its own name, for example? We can provide a class with member variables to hold information about the different objects of that class's type.

class Vorlon {
public:
   void display() {cout << "Displaying a Vorlon named " << myName << endl;}  // Note will be done better, later on.
   string myName;
};

Now we can create an object, give it a name and display the object, getting to see what it's name is.

Vorlon v1;
v1.myName = "Kosh";
v1.display();

Here we created a Vorlon, gave it the name "Kosh" and then displayed it. Inside the definition of the display method , we can use myName to refer to the name of the Vorlon that we are currently displaying. Every time we call a method, there is a specific object that the method is being called for. We will call that the current object.

Private members

The definition of the Vorlon class above would allow us to easily, even accidentally, change a Vorlon's name. In fact any piece of code that can access the Vorlon can change its name. This could result in some difficult programming bugs to track down.

It's also possible that we don't want programmers to depend on the exact way in which we store the member variables. In the case of a Vorlon's name, perhaps we're not likely to change our minds on how to implement it, but in other cases that we will see later, we may well want to provide ourselves that freedom.

In general, we would prefer not to have our member variables so easily accessible and so easily modifiable. Classes allow us to mark some members (both member variables and methods) as private, so that only other members of the class can get at them.

class Vorlon {
public:
   void display() {cout << "Displaying a Vorlon named  << myName << endl;}  // Note will be done better, later on.
private:
   string myName;
};

The method display can still access the name, but any code that is not part of the class can not. Hm, that causes us a problem. How can we set Kosh's name? The code that we used earlier will get an error when we try to compile.

Vorlon v1;
v1.myName = "Kosh";  // Compilation ERROR!  Can't access private member variable myName.
v1.display();

Constructors

A special kind of method is provided in order to allow us to initialize an object. It is called a constructor. A class can have several constructors, depending on what kind of information we want to use during initialization. The name of the constructor is exactly the same as the name of the class.

We want to provide the Vorlon's name to the constructor, so that each time a Vorlon is created, it recieves a name.

class Vorlon {
public:
   Vorlon(const string& aName) {myName = aName;}  // We will do this better in a moment.
   void display() {cout << "Displaying a Vorlon named " << myName << endl;}  // Note will be done better, later on.
private:
   string myName;
};

Notice a very unusual thing about the Vorlon constructor in the class definition. It doesn't have a return type!! None at all. Not even void. That is true of all constructors.

Now we can create our Vorlon named Kosh:

Vorlon v2("Kosh");
v2.display();

See how we provide the Vorlon's name at the same time that the Vorlon is created. This is how we can get around the problem we had in the last section when we tried to set the private member variable myName from outside the class.

Initialization List

Int the constructor for Vorlon, the comment said that we would do it better "in a moment". Well, that moment has come.

What could possibly be wrong with that code? It sets the only member variable there is, so that's good. Well, actually that's the problem. It sets it. It doesn't initialize it. Consider these two lines:

string cat;
cat = "Felix";

and compare them to this line:

string cat = "Felix";

Which is better? Why? The second approach takes only one line, which some might say is better, but that's not the point. What would happen in the first example if we printed out the cat before setting it to Felix? We would print an empty string. So? The problem with the first approach is that we took the time to create that empty string and then undid that work and assigned "Felix" to it. In the second approach, we never create the empty string. Instead when the string is created we immediately initialize it to "Felix". Some people like to write that line as:

string cat("Felix");

to make the fact that we are initializing more clear.

What's all that got to do with our constructor? Well we are doing the same thing. The member variable myName is first getting initialized to the empty string, and only afterwards being changed to get the value of the parameter, aName. We can do better. And we will require that you do.) The trick is to use the initialization list:

class Vorlon {
public:
   Vorlon(const string& aName) : myName(aName) {}  // This is the correct wat you write this constructor.
   void display() {cout << "Displaying a Vorlon named " << myName << endl;}  // Note: will be done better, later on.
private:
   string myName;
};

The body of the constructor doesn't do anything anymore! Instead a new bit of syntax is introduced. In between the parameter list and the body of the constructor is the initialization list. It begins with the colon character ':' and is followed by a list of initializations. Here we only have one member variable, so there is actually only one thing initialized here. The member variable myName is being initialized to the parameter value aName.

This example shows the initialization list being used for a minor improvement in speed. Later we will see examples in which the intialization list plays more important roles. In fact we could change our example just a little and immediately find the initialization list has to be used.

class Vorlon {
public:
   Vorlon(const string& aName) {myName = aName;}  // This will NOT COMPILE
   void display() {cout << "Displaying a Vorlon named " << myName << endl;}  // Note will be done better, later on.
private:
   const string myName;  // Note that nyName is now const
};

I made only a small change to the original example and now it won't compile at all. What was the change? The member variable myName is now const. We cannot assign to it anymore. We can only initialize it. We have no choice but to use the intialization list.

The Default Constructor

The first time we created a Vorlon, we did not provide a name. All we wrote was:

Vorlon v1;

Interestingly, if we tried that now we would get a compilation error. Why? Creating an object that way depends on a special constructor, called the default constructor. The default constructor is simply a constructor that doesn't take any arguments. C++ provides you with a default constructor automatically for every class you define, so long as you don't define any other constructors. As soon as we defined a constructor that takes a single argument, the Vorlon's name, C++ figured we knew what we were doing as far as constructors go and did not proved a default constructor.

If we want to be able to construct Vorlons who don't have names assigned, we can provide our own default constructor:

Vorlon() {}

It would appear that this function doesn't do anything. We will learn that that is far from the truth.

For now, you should know that every constructor first initializes the class's member variables. Not all of them, though. Specifically, it does not initialize member variables whose type is built-in to C++, such as char, int and double. It also does not initialize any member variables that are pointers.

All other member variables, those whose type is a class, such as string and even Vorlon, are initialized. How do they get initialized? Later we will see how we can specify how to intialize these member variables. If we don't specify then they get initialized by their default constructors!

See, default constructors are very important.

Copy Constructor

[Note that we won't discuss this topic until we get to Copy Control. Feel free to ignore it for now if you like.]

The copy constructor is another constructor that is really really important. We also get this one for free. In fact we don't even lose it, the way we do the default constructor, if we write any other constructors.

So why do we have to know about it? Well, later on we will see some situations in which the system-supplied version is not good enough for our needs. We'll have more to say about how (and why) to write our own later.

So, what should we know now? Simple. When does a copy get made. In other words, when is the copy constructor called?

When do we use the Copy Constructor?

Let's assume we have our Vorlon named Kosh, who was defined above with the line: Vorlon v2("Kosh"); Then there are four ways we can use the Vorlon copy constructor. (Note the first bullet counts for two of the ways.)

Const methods

Suppose we pass our Vorlon into a function. How should we declare the parameter to the function? To put it another way, how should we pass the Vorlon in to the function?

Our choices are: by value, by reference and by constant reference. Passing by-value makes a copy of the object passed. In general we want to avoid pass-by-value, as it is frequently a silly waste of time. Pass-by-reference allows us to avoid the copying, but it makes our code vulnerable to other bugs that can be difficult to track down, such as changing something about the Vorlon that is passed in. Often the best choice is to pass-by-constant-reference. This saves the copying and protects the object from being accidentally modified.

So a simple function that is passed a Vorlon and calls its display method would be:

void simpleFunction (const Vorlon& fred) {
   fred.display();  // Compilation error.
}

There's a small problem however. We said that fred is const so that nothing will be allowed to change anything inside fred (i.e., fred's member variables). But then we call the method display on fred. We know that display won't change any of fred's member variables. But C++ doesn't. C++ will not allow simpleFunction to compile without an error unless we provide a guarantee that simpleFunction is safe. How? We must state that in simpleFunction's definition that it is "const".

class Vorlon {
public:
   Vorlon(const string& aName) : myName(aName) { }
   void display() const {cout << "Displaying a Vorlon named " << myName << endl;} // Finally, we did it right!!!
private:
   string myName;
};

What did we change in the definition of Vorlon? In the definition for display, we put const after the parameter list and before the body of the function. Now, we have guaranteed that display will not change anything in the current object's member variables. If we would try to change a member variable's value inside display, C++ would give an compilation error. Now display is "safe" to use on const objects.

Whenever you write a method that should not change the values of any member variables inside the current object, you should mark the method const as we did with display. Note that we often refer to such a method as a const method.

Vector of Objects

Earlier we talked about how to fill a vector of struct objects from a stream. In that case, we were not using encapsulation and had not made the date private. Suppose we want to fill a vector of class objects, how will the code change? What will have to change?

First, let's look at that code again. (Sorry that I used a different class there than we've been using in this set of notes.)

void fillCatVector (ifstream& ifs, vector<Cat>& vc) {
   Cat c;
   while (ifs >> c.name >> c.color >> c.weight) {
      vc.push_back(c);
   }
}

Now, what will be different if Cat is defined as a class? First, let's write such a class.

class Cat {
public:
   Cat(const string& theName, const string& theColor, double theWeight) 
      : name(aName), weight(theWeight), color(theColor) { }
   void display() const {
      cout << "Displaying a Cat named" << name << " with color " << color << " and weight " << weight << endl;
   }
private:
   string name;
   string color;
   double weight;
};

The only real difference in how we would fill a vector is how we set the fields in each object. Since we are using encapsulation, the fields need to be initialized in the constructor. That means we need to first read the values into temporary variables and use those to initialize the object. (Note, the is not a method of the class!)

void fillCatVector (ifstream& ifs, vector<Cat>& vc) {
   string name;    // Used to read in the name
   string color;   // Used to read in the color
   double weight;  // Used to read in the weight
   while (ifs >> name >> color >> weight) {
      Cat aCat(name, color, weight);  // Cat object defined inside loop.
      vc.push_back(aCat);
   }
}

What changed? First, we defined temporary variables in order to read in the name, color and weight. Then we defined a Cat object inside the loop, so that we could initialize it using the Cat's constructor.

How would we write a function to display a vector of Cats?

void displayCatVector (const vector<Cat>& vc) {  // Remember the const
   for (size_t index = 0; index < vc.size(); ++index) {
      vc[index].display();
   }
}

Object Oriented Programming

We have been talking about how to write a class. You now have the important stuff to get started with. But about the question of what should go in your class?

What is the point of a class? To model something. In these notes we modeled Vorlons. (Ok, so we didn't really do much with them.) But let's think about using these Vorlons in a program. Two principles that should remember, 1) every object should make sense, i.e. that values for the fields in an object should all be appropriate for your application, and 2) every object should only be able to do what makes sense for that kind of object.

Good Initialization

Above we talked about how to write a default constructor. Now I want you to think about whether that's actually a good thing to do. It all depends on your class and how you are going to use it. Do we want to create Vorlon objects if we don't know their names? If not, then why would we want a default constructor for the class? Defining a default constructor should be something that you think about doing, not something that you do automatically.

What does your object do?

Methods in a class should represent something that an object does or perhaps something that happens to the object. But one way or another, there should not be any methods in your class that are actually irrelevant to the object they are called on.

Beginners often put every function that has anything to do with their class inside the class. For example, suppose you needed to read a file of Vorlons and fill a Vorlon vector. We won't bother to write the function here, but let's consider how it might be called. Unfortunately, I have seen code like the following:

// TERRIBLE, HORRIBLE, VERY BAD CODE!!!!
Vorlon temp("");  // What kind of name is that for a Vorlon???
ifstream ifs("Vorlons.txt");
vector<Vorlon> vv;
temp.fillVector(vv, ifs);

What makes this code so bad? Aside from the idea that we are creating a Vorlon with no name at all and using a horrible choice of variable names, i.e. temp? Worse than all of that is we have apparently defined a method called fillVector inside the Vorlon class. See how we call it on the temp Vorlon? What does filling that vector have to do with our temporary Vorlon? NOTHING! So why are we using the temporary Vorlon at all? Because we were foolish enough to place that function inside the class as if it should be a member function. C++ does not require that every function be defined inside a class and we shouldn't do so. Better?

// Better
ifstream ifs("Vorlons.txt");  // Should test for successful opening...
vector<Vorlon> vv;
fillVector(vv, ifs);

Let's consider another example.

// TERRIBLE, HORRIBLE, VERY BAD CODE!!!!
Vorlon temp(""), kosh("Kosh"), koshina("Koshina");
temp.marry(kosh, koshina);

Now what's my complaint? Certainly getting married is something that Vorlons should be able to do. (Really, I don't know that much about Vorlon habits, but lets assume they do.) So, putting the marry function inside the Vorlon class makes good sense. However, how many Vorlons are involved here? Looks like three: kosh, koshina and temp. Assuming that Vorlon marriages follow similar rules to those here, there should only be two Vorlons mentioned. Better?

// Better
Vorlon kosh("Kosh"), koshina("Koshina");
kosh.marry(koshina);

Objects within Objects

Many of our examples show objects inside of other objects. You probably haven't noticed them. The Vorlon class, for example, has a string object inside it holding its name. You might not have thought about if because for one thing, we weren't the one that defined that class. So, let's create another class, a Date class, to use with our Vorlons.

Our Date objects will be created by passing in integers representing the month, day and year. (I do realize this may be somewhat meaningless for Vorlons, who certainly use a different calendar system, but I am sticking with if for our familiarity.) So, we might want to create a Date as:

Date theFourth(7, 4, 1776); // July 4th, 1776

A simple implementation of the class might be:

class Date {
public:
    Date(int month, int day, int year) : month(month), day(day), year(year) {}
    void display() const { cout << month << '/' << day << '/' << year; }
private:
    int month, day, year;
};

Now we need to modify the Vorlon class to include a date of birth field. We will also want its constructor to initialize the Date and and its display method to display it.

For the constructor, there are two natural designs. We could pass in an actual Date object. We will take a second approach, just passing in the month, day and year fields, leaving the construction of a Date object up to the Vorlon constructor. Using this approach, we might define a Vorlon with:

// Don't know how old Kosh is. March 14, 1592 is just a guess.
Vorlon kosh("Kosh", 3, 14, 1592);

That leave us with just having to make the necessary modification to the Vorlon class. Here's the constructor:

Vorlon(const string& aName, int m, int d, int y)
: myName(aName), bday(m, d, y) { }

Note how the field bday was initialized. See that it took three arguments. Well ocourse it did. The point is simply that to initialize the Date object in the Vorlon class, we have to use the Date constructor and it takes three parameters. We had not previously seen anything in the initialization list taking anything orther than one argument. Whatever number the corresponding constructor needs, that's what we pass in.

Here is the complete program. One thing to observe there. The Date class had to be defined first, otherwise the Vorlon class would not have compiled. After all, otherwise how would the compiler know when looking at the Vorlon class that a Date class actually exists, let alone that it has the appropriate constructor and display method.

Implementing methods outside of the class definition

[Note, this will be not be covered in class until lecture 11, Separate Compilation, so you are not responsible for it until then.]

Putting the code for the methods inside the class definition can make the class rather hard to read after a while. Also, we might prefer that someone who is looking at our class definition not even get a chance to see the way we implemented the methods. How can we do this? The first step is to place the definitions outside of the class definition, leaving only the prototypes inside.

class Vorlon {
public:
    Vorlon(const string& aName);
    void display() const;  // const is part of the prototype.
private:
    string myName;
};

Notice that in the display method, the const is part of the prototype. (It is also part of the function's signature.)

How do we write the definitions?

Vorlon::Vorlon(const string& aName) : myName(aName) { }
void Vorlon::display() const {
    cout << "Displaying a Vorlon named" << myName << endl;
}

The only thing different about the way the methods are written, is that the name of the function is qualified by the name of the class, follwed by two colons (i.e., the character ':'). The two colons together are called the scope operator. Only the function's name gets qualified that way, nothing else.

Here is a program that defines the Vorlon class, implements its methods and then creates and displays a Vorlon:

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

class Vorlon {
public:
    Vorlon(const string& aName);
    void display() const;  // const is part of the prototype.
private:
    string myName;
};

Vorlon::Vorlon(const string& aName) : myName(aName) {} 
void Vorlon::display() const {
    cout << "Displaying a Vorlon named " << myName << endl;
}

int main() {
    Vorlon v("Kosh");
    v.display();
}

Home


Maintained by John Sterling (jsterling@poly.edu). Last updated February 10, 2013