Object Oriented Programming

We’re now going to start learning about one of the most popular programming paradigms, Object Oriented Programming, or OOP. OOP is not necessarily the best method in general (despite what some may tell you), but it is often useful and easy to implement, and is certainly the most popular. The basic element of OOP is the object. An object is simply a data structure that contains both data and logic to manipulate said data. Hence, the paradigm is based on creating these objects and the relationships between them. The basic concept is just that simple—although, C++ being a powerful language, there are many intricacies in how you can implement and manage objects.

C++ is an object oriented language, and implements objects as "classes." Classes are similar to structures—they both define ADTs—but classes have more features: member functions (logic), private and public data, and self-contained/managed memory to name the most important.

Technically, C++ structures can also utilize the features of classes, but it is conventional to use a class for anything more complicated than aggregate data.

Defining Classes

The syntax of declaring classes is much same as structures: after “class,” add the type name, curly brackets, and a closing semicolon. Then, put your data members in your class’s scope.

However, you can also define member functions. Declaring a member function is just like declaring any other function: specify the return type, identifier, and parameters. Member functions, just like data members, are a part of your class, and can automatically operate on your classes data members—no parameters needed.

class card {
	char* rank;
	char* suit;
	int value;
	
	// These are member functions, and will operate on the class's data.
	void print();
	char* getSuit(); 
};

Access Modifiers

If you ever try to use the class above, you will get an error for trying to access a “private” member of the class. This is because the default access modifier of data within a class is "private." No part of your program other than member functions of the class itself can access private data. Your main function, for example, cannot ever touch private data members. However, if all your data members and member functions are private, the class is useless.

Instead, specify that some member functions and/or data is “public.” Anyone, anywhere in your program can access public data from a class instance. Your main function, member functions, other global functions, anyone. However, you should usually protect the data members of a class. This is because ideally, only the member functions contain the logic to manipulate the data members. Hence, if all parts of your program can access the data members directly, they can change them, possibly causing unintended side effects. These sorts of bugs can be nigh impossible to track down.

Consequently, all flow of data in and out of the class should be managed by your member functions. Other parts of your program can interface with your class through these public member functions.

Finally, to mark sections of your class as "private" or "public," simply use those keywords followed by a colon. An section is simply the space between one access modifier and another. Additionally, the default modifier (for classes) is "private," so anything before your first other modifier will automatically be private. It is conventional to have your public section before your private section.

class card {
public:
	// These public member functions can be accessed from anywhere
	void print();
	char* getSuit(); 

private:
	// These private data members can only be accessed by the member functions
	char* rank;
	char* suit;
	int value;
};

Member Functions

Just like data members, member functions belong to a class. Hence, to call a member function, you use the dot or arrow operator. For example, if we have an instance of the “card” class from above...

card c1;

c1.print();

This calls the print() function of the “card” class.

Also as with structures, each instance of your class contains its own, separate set of data. Hence, calling a member function that acts on that data will produce different results for different instances.

While you can technically implement member functions right where prototyped within the class, it is conventional to implement them outside your class, in a different file. (However, the example programs will implement members in the same file as the class definition and main.) The syntax is just like implementing any other function, except that you must specify the class name and the scope resolution operator (two colons or “::”) before the name of the function. Then, implement the function as usual.

When a member function is called, it will always have access to an instance of the class, and hence has access to the data of that instance—all data, private or otherwise. To access data members, simply use the member identifiers from the class definition. The members automatically reference the data members of the “calling object.” The calling object is the instance from which the function is called using the dot or arrow operator.

void card::print() {
	cout << rank << “ “ << suit << “ “ << value << endl;
}

int main() {
	card c2;

	c2.print();
}

This implements the card’s print member function. card::print() automatically has access to the data members of card. When you call the print function from a card instance, this function is called, and uses the data from that particular instance of card.

Getters and Setters

Member functions are commonly used to manipulate data contained in a class. Because your data members will primarily be private, you will often need a quick way to read from or write to the members. This is where getter and setter member functions are useful—they simply read from and write to a private data member.

Granted, it is absolutely useless to have a getter and setter for a value if literally all the functions do is return the value or set the value. However, when you start to build more complicated objects, they will necessitate more complicated methods of assigning and retrieving data. For example, you may want to track all access to a certain member for performance reasons. This is the real value of getters and setters—they encapsulate the access logic and provide a streamlined interface between your object and the rest of your program.

char* card::getRank() {
	return rank;
}

void card::setRank(const char* r) {
	delete[] rank;
	rank = new char[strlen(r) + 1];
	strcpy(rank,r);
}

Here, the getter is pretty useless, but the setter will automatically manage the dynamically allocated string. Your main function, or wherever else, does not have to worry about the memory, as it is abstracted by the class interface.

Constructors

There is a special type of member function that all classes include, whether you implement it or not. This is the constructor. The constructor, as the name implies, is called once and only once—when an instance of your object comes into being, is instantiated. There are several types of constructors, but they all serve the same purpose: to initialize the state of your class in an intelligent manner. For now, this will most likely mean setting default values and allocating dynamic memory.

You never directly call a constructor (you cannot write something like class.constructor()). Instead, the constructor is called automatically when any instance of a class is instantiated. This includes creating a static instance or array, dynamically allocating an instance or array, passing by value, and returning by value. More on that in lesson 14.

For example, the constructor will be called in both of these situations...

myclass var1;
myclass* var2 = new myclass;

A constructor is specified just like any other member function, with a few differences. First, all constructors must have the same name as the class. For example, if you have a class named “card,” all constructors will be named “card.” Second, constructors have no return type—not even “void.” Leave out the return type. Third, your constructor must be public, or else nowhere in your program will be able to create an instance of your class—the constructor is called to instantiate an object, hence if it’s private, you can’t create the object. (This rule can be bent—see lesson 14.)

Implementing a constructor is again the same, except that it is known that when your constructor is called, the instance has just been created—all your data members will have garbage values. The point of the constructor is to initialize, so you will most often set your members to default values, initialize connections, etc.

class card {
public:
	card();
	// other functions

private:
	int value;
}

// Implementing the constructor
card::card() {
	// Set a default value.
	value = 0;
}

Here, we describe and implement a constructor for the card class. It's very simple, just setting the value to a default of zero. This means that whenever you create a card (calling the constructor), the value will automatically start as zero.

Types of Constructors

There are three main types of constructors. We’ll cover the first two in this lesson. The first type is the simplest—it has no parameters and will simply set your data to default values. Intuitively, this type is called a default constructor. Your default constructor will be called if you create an object without specifying anything, for example...

card card1;

Will call the default constructor.

Next, there is the parameterized constructor. As the name suggests, this constructor takes parameters that signify initial conditions. The implementation is exactly the same as the default constructor, except that the function takes parameters. Remember when we went over overloaded functions, quite a while ago? You can overload member functions, including the constructor. This means that you can have different constructors for different situations. For example, this example has both a default and a parameterized constructor...

class card {
public:
	card();
	card(int v);
private:
	int value;
}

//Default constructor
card::card() {
	value = 0;
}

//Parameterized constructor
card::card(int v) {
	value = v;
}

Additionally, you can create a hybrid of these two types using default parameters. As you might expect, a parameterized, default constructor is simply a parameterized constructor will all default parameters. This method can be useful, but you must be aware of the limitations of default parameters.

class card {
public:
	card(int v = 0);
private:
	int value;
}

// Hybrid constructor
card::card(int v) {
	value = v;
}

You might be wondering how your program knows what constructor to call if you have more than one. To specify that your parameterized constructor, simply add the necessary parameters when instantiating the class.

card card1;

Will use your default constructor.

card card2(10);

Will use your parameterized constructor.

card* card3 = new card(10);

Will also use your parameterized constructor.

It's the same for a hybrid constructor—include the parameters you wish to use (in the correct order).

Destructors

The counterpart to constructors is, of course, the destructor. The destructor is like the constructor in that it is only called once in the life of an object—when an instance is destroyed, when it goes out of scope or is deleted. The point of the destructor is to clean up the class data, for example deleting any dynamic memory allocated by your class.

Like the constructor, the destructor has a specific name: a tilde (“~”) followed by the name of the class. Although you can have multiple constructors, you can only have one destructor.

class card {
public:
	card();
	~card();
private:
	char* name;
}

// Constructor
card::card() {
	name = new char[10];
}

// Destructor
card::~card() {
	delete[] name;
}

This example shows a class that will allocate memory on construction and deallocate it on destruction. The dynamic memory is only seen and managed by the class itself, so the rest of your program does not have to worry about memory leaks or the like.

void func() {
	// The default constructor will be called here, on instantiation
	card card1;   
}   
// The destructor will be called here, when the object goes out of scope

Here, the default constructor allocates memory and the destructor deletes it. Neither needed to be specified.

Programming Exercises

The example program should be very helpful this week, be sure to take a look at it. Run it to see what functions are called when.

In these exercises, most data members should be private. Member functions should mostly be public.

  1. Create a "deck" class using your "card" class. Write constructors, destructors, and member functions to output cards, getters and setters, and a method to input a card.
  2. Try creating a game using objects to describe the main aspects. For example, you could create an object to represent the game as a whole, an object to represent the playing field, an object to represent your player, etc.
  3. Create a simple cataloging system for “student” objects. You should be able to add a student with their relevant data, print them, sort them, and remove them. You should include some dynamically allocated data members, implement member functions, a default constructor, a parameterized constructor, and a destructor.