Contents
C++ allows developers to program in many different styles. We have been programming in what is known as a Procedural Style of programming. Procedural code focuses on how data is operated upon and how the data flows through a program. C++ is known as an object oriented (OO) programming language and classes are how this style of programming is implemented. A class in C++, and other OO languages, represent objects from the problem domain. The objects are typically nouns from the problem description. The objects will have information or data they store and have related operations that allow for a program to interact with objects and gain access to the data.
Beginning programmers tend to have some difficulty understanding how classes work and how to have them assembled to solve their programming tasks. I like to describe objects as a vending machine. The machine stores various private pieces of information, like soda or candy and money. In order to gain access to the material inside the machine, the person has to both give the correct amount of money for the desired product and also press a button or series of buttons to indicate their choice. If any of the inputs to the machine are incorrect the machine may not do anything or it may give some message, depending on machine. An example might help clear up some of the details.
class Date
{
private: //private data that belongs to the class
int month;
int day;
int year;
public: //public methods, or functions, that allow a person to interact with the Date
Date();
Date( int m, int d, int y);
void setDay( int newDay );
void setMonth( int newMonth );
void setYear( int newYear );
int getDay() const;
int getMonth() const;
int getYear() const;
};
Listing 9.1
From listing 9.1 we can see that the class Date is similar to a struct Date we might create. There are two major differences:
In fact, a class and a struct are almost the same thing in C++. A struct can contain methods and data. A struct is simply a class that is public by default. Something that is public means that code outside of the class, or struct, can directly access the field or method. If you think back to the struct from the last chapter one of the problems was that any code outside of our struct could change the data because it was public. The class fixes this problem by making the data, or fields, private. Private items are items that can only be accessed within the class itself. This forces a user of the class to use the methods to interact with the class. This would allow us to change how we store the data within the class and users of the class would never need to "know" about the change. As long as the methods, or interface, haven't changed, they could go on using our class and never need to worry about those details. This concept of ignoring details of an object, or function, is known as abstraction. We have abstracted away the details of a component and can simply focus on how to use the object. We can of course come back and look at the abstraction and worry about just those details as well.
Listing 9.1 is the class definition since it doesn't actually contain any of the details about how the class is implemented. This code would typically go into a header file with the name of the class, e.g. Date.h. C++ has no restriction on file names or the number of classes that can be put into a header, but I typically follow the practice of one class per header and I typically name the file the name of the class. This makes it easier to find my code later when I go to use the classes I've previously written.
When you talk about functions for classes, we typically call these functions methods. It makes it easier to recognize a class function if we introduce a word that means just that. With classes, there are 3 categories of methods I typically introduce to begin with.
Constructors are a special set of methods for classes. A constructor is the method that is used when an object of a class is created. It is automatically called for you and it's purpose is to initialize the private data within the class. Sometimes this method might be completely empty but most times it has some work to do. You can tell a method is a construct by these two properties:
So the first two methods in listing 9.1 are constructors. Their job would be to set up a new data object with valid data so when the object is used it is in a valid state. Those methods might look like this:
Date::Date()
{
month = 1;
day = 1;
year = 2015;
}
and
Date::Date( int m, int d, int y )
{
month = m;
day = d;
year = y;
}
So at first look these methods look similar to regular functions; however there are some obvious differences. First, we have the Date:: before the name of the method. This Date:: is there to indicate that the name that comes after it is part of the Date class. The :: operator is the scope resolution operator and when it is preceded by a class name it allows us to access part of the class. So Date::Date is how we access the Date's constructor. We have written two of these constructors because we have two constructors defined in the class's definition. We are allowed to have as many constructors as we need or want as long as the different constructors have a different list of parameters. What goes within the scope of the constructor depends on the class and if there are any parameters.
The first constructor without any parameters is known as the default constructor because it is called whenever a Date is made with no other input given. The values that are used within its body are chosen because it was the first day of this year. Any values could have been picked as long as they create a valid date. We will see an alternative way to write this constructor in just a bit.
The second constructor then is a parameterized constructor because it has a parameter list. It would be called when you create a Date with 3 integers. The meaning of the parameters would be the month, day and year. This constructor might an okay first attempt but it has a major flaw. It assumes that the month day and year it is given are okay and valid to use. As it is currently written it's no better than a struct with public data since it doesn't do anything to protect itself from bad data. We will see way to correct this next.
Constructors have a special syntax available to them known as the initializer list. This is a list of the fields from the class listed in the order they are found in the class's definition. It is used to initialize the private data before the scope of the constructor is entered. In our Date class there might not be a great need to use this, but in other classes where certain data must be initialized before it can be used, it is almost required to use this syntax. We can rewrite the first constructor using this list like this:
Date::Date() : month(1), day(1), year(2015)
{
}
So we can see we have the same Date:: and then the name of the method. In this case after the () we add a : then list the fields. We add a set of parenthesis to the name and the value we want the field to be initialized with. Then we can leave the constructor body empty because the work as already been done. Let's see how we can use this idea in the other constructor.
Date::Date( int m, int d, int y) : month(m), day(d), year(y)
{
if ( month < 1 || month > 12 )
month = 1;
if ( day < 1 )
day = 1;
else if ( (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) && day > 31 )
day = 1;
else if ( (month == 4 || month == 6 || month == 9 || month == 11) && day > 30 )
day = 1;
else if ( month == 2 && day > 28 )
day = 1;
if ( year < 0 )
year = 1;
}
This version of the second constructor makes an effort to verify that the parameters it was given are valid and comprise a valid date. It uses the initializer list to go ahead and initialize the fields to the values it was given then it checks the values are okay to be used. If they aren't valid, then it sets the field with the invalid data to 1. There are still issues with this constructor. For example, how is the user to know they entered invalid data unless they check the Date that is created. It also ignores the fact that almost every 4 years in the Gregorian calendar a leap year occurs. We can of course correct the leap year problem with some simple math but the purpose of this example was to show how we can do more than simply accept the inputs, we can confirm they are correct. To correct the other issue we need a better way to handle errors.
As you can see constructors are important to classes in C++. They are after all how an object of the class is made and the first time we start creating and using the data stored within the class's objects. This leave us two broad categories of methods termed setters and getters.
A setter method is simply a method that updates, or sets, the private data using parameters. These setters can be very simple methods that simply use the parameter and assign them to the private field or do more work in ensuring the data is valid. How they are implemented depends completely on the class and type of data that is allowed to be stored with in the class. Here's how we might implement the setters for our Date class.
void Date::setMonth( int newMonth )
{
if ( newMonth > 0 && newMonth < 13 )
month = newMonth;
}
void Date::setDay( int newDay )
{
if ( (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) && (newDay < 32 && newDay > 0) )
day = newDay;
else if ( (month == 4 || month == 6 || month == 9 || month == 11) && (newDay < 31 && newDay > 0) )
day = newDay;
else if ( month == 2 && (newDay < 29 && newDay > 0) )
day = newDay;
}
void Date::setYear( int newYear )
{
if ( newYear > 0)
year = newYear;
}
Each of these set methods are attempting to verify the parameter they are given is okay before they update the private data. In the case where the parameter is invalid, the methods do nothing. We could rewrite these methods to return a boolean result to indicate if the data was updated or not, and this might work as a way to allow the user to check if the change was made. The user could also ignore the return from the method and so again, we need a better way to indicate an error has occurred.
If setters allow us to update information in the object a getter then is a method that allows us to retrieve or get data from the object. Often times a get method might be a single line or they might have to do some work before they can return the required information. Here's how we might implement the getters for the Date class.
int Date::getMonth() const
{
return month;
}
int Date::getDay() const
{
return day;
}
int Date::getYear() const
{
return year;
}
You can see from these examples the get methods here are in fact very short and simple. They simply return the requested information with no more work required to do so. We could add a method that would indicate if the year is a leap year and in that method we would need to examine the year and do some basic math to be able to answer the question. It would still be a short method, but it would be longer then the examples.
In all the examples, setters and getters, all of the methods have a similar signature syntax.
returnType ClassName::methodName( parameterList )
{
//method body
}
So in general a method implementation for a class is a return type followed by the class name and the two colons, then the name of the method and list of a parameters, if any, inside of parenthesis. In the case of the getters, I've added the keyword const to indicate that those methods will not modify the private data. It's not incorrect to leave off the keyword const, assuming it's not in the definition, but in my opinion it should be there. If it is there and the method attempts to change the private data the compiler will catch it and a sneaky bug can be found very early.
Let's look at a complete example of the Date class along with a main program that might be used to test the Date class.
#ifndef CLASS_H
#define CLASS_H
class Date
{
private: //private data that belongs to the class
int month;
int day;
int year;
public: //public methods, or functions, that allow a person to interact with the Date
Date();
Date( int m, int d, int y);
void setDay( int newDay );
void setMonth( int newMonth );
void setYear( int newYear );
int getDay() const;
int getMonth() const;
int getYear() const;
};
#endif
Listing 9.1.h
#include "Date.h"
Date::Date() : month(1), day(1), year(2015)
{
}
Date::Date( int m, int d, int y) : month(m), day(d), year(y)
{
if ( month < 1 || month > 12 )
month = 1;
if ( day < 1 )
day = 1;
else if ( (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) && day > 31 )
day = 1;
else if ( (month == 4 || month == 6 || month == 9 || month == 11) && day > 30 )
day = 1;
else if ( month == 2 && day > 28 )
day = 1;
if ( year < 0 )
year = 1;
}
void Date::setMonth( int newMonth )
{
if ( newMonth > 0 && newMonth < 13 )
month = newMonth;
}
void Date::setDay( int newDay )
{
if ( (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) && (newDay < 32 && newDay > 0) )
day = newDay;
else if ( (month == 4 || month == 6 || month == 9 || month == 11) && (newDay < 31 && newDay > 0) )
day = newDay;
else if ( month == 2 && (newDay < 29 && newDay > 0) )
day = newDay;
}
void Date::setYear( int newYear )
{
if ( newYear > 0)
year = newYear;
}
int Date::getMonth() const
{
return month;
}
int Date::getDay() const
{
return day;
}
int Date::getYear() const
{
return year;
}
Listing 9.1.cpp
#include "Date.h"
#include <iostream>
using std::cout;
using std::endl;
int main()
{
Date d; //calls the default constructor
cout << "Default date made. The month and day should be 1 and the year should 2015.\n";
cout << "Default date month = " << d.getMonth() << "\n";
cout << "Default date day = " << d.getDay() << "\n";
cout << "Default date year = " << d.getYear() << "\n";
Date d2( 7, 25, 2003 ); //calls the parameterized constructor
cout << "Parameterized date made. The month should be 7, the day should be 25 and the year should 2003.\n";
cout << "Parameterized date month = " << d2.getMonth() << "\n";
cout << "Parameterized date day = " << d2.getDay() << "\n";
cout << "Parameterized date year = " << d2.getYear() << "\n";
//now let's use the setters
d.setDay( 3 );
d.setYear( 2002 );
d.setMonth( 14 );
cout << "After setters called. The month should be 1, the day should be 3 and the year should 2002.\n";
cout << "Month = " << d.getMonth() << "\n";
cout << "Day = " << d.getDay() << "\n";
cout << "Year = " << d.getYear() << "\n";
}
Program 9.2.cpp
This is essentially the same code from the rest of the chapter but it is all put together. There are a few notable differences that need to be discussed. In the Listing10.1.h there is some extra code before and after the class definition. These lines of code are known as an include guard. We need these to allow us to #include our header file where it is needed but not need to worry about defining the class more than one. The line #ifndef is a preprocessor directive that asks the preprocessor to look in its table of symbols for the name that comes after ifndef and if that name is not defined ( #ifndef ), then we define it, ( #define ). The line at the bottom ( #endif ), closes the #ifndef statement at the top. The name that comes after the #ifndef can be anything but it should be a unique name. I typically use the name of the header file in all capital letters. Sometimes I'll add a few __ to the front and/or end of the name to ensure the name I have asked about is almost guaranteed to be unique.
In Listing9.1.cpp you can see the first thing we have done is #include "Date.h", this is the header file we have just written and it includes the definition of the class we are writing. We need to do this so we have access to the class's definition. We use "" instead of <> to indicate that we are looking in a different location for the header file. The "" allow us to put a path to the file. I typically put both the header file and the implementation file in the same directory so just the name of the header is all that is needed.
Finally in Program9.2.cpp we have our typical #include
So this is simply the beginning of the uses of classes. This small introduction serves as a starting point for the very rich study of object oriented programming and this simple class is just one of many that can be written. Each class can be simple like this one or very complex and used for storing data in very specialized ways. The study of data structures is just this application of object oriented programming to solve the need to ways to store data. They are nothing more than a class written to serve a purpose. Classes can be written to fit almost any purpose and there are many specialized books on these topics. It should be no surprise that many many hours and pages can be written on this topic. This is intended as a very basic introduction and programmers wishing to dig deeper are encourage to continue their exploration on this topic.
Written by: David McPherson © 2019