4.8. Classes#
So far we have only written simple programs in C++, using readily available data types, such as integers, and simple functions. The most important part of object oriented programming however is the use of classes. Classes are aggregate data types, combined with a set of functions that operate on it. To understand what classes are, it is best to take a look around. You will see a lot of objects. Each object has properties (dimensions, weight, color) and capabilities or actions they can perform (a bottle can be opened or have some liquid poured in or out of it). A class in C++ is a container that works similarly; it has a list of properties and a set of functions (capabilities!) that operate on it. A class is merely the design of objects. If you look at a bottle of water, that is an instantiation of the class âbottleâ. C++ already comes with a lot of classes that you may have seen before, such as âvectorâ
4.8.1. Declaring and using classes#
The definition of a class in C++ is as shown below. The next few sections will explain the class specifics that were left out. The design used is for storing and using dates.
class Date
{
// Class specifics here
};
Note that a class definition ends with a semicolon. Leaving out the semi-colon will lead to an error while compiling.
To instantiate an object of the Date class, use the following code:
Date today;
Now âtodayâ is variable of type Date that can be used in your code, similar to how you can you use integer variables.
4.8.1.1. Class members#
So far, we have created a class Date, but it is just an empty class. Remember that we said classes are aggregate data types and it has a list of properties? Such properties are called class members or some times simply member variables. Letâs give the Date class some members to represent an actual date:
class Date
{
public:
int day_;
int month_;
int year_;
};
Note that the member variables have been named with a suffix underscore. This is by convention and not necessary. By sticking to such a convention, one can easily recognize member variables from non-member variables.
Now you can use the members of an object of type Date as regular
variables. You access them by use of the . operator.
Date today;
today.day_ = 1;
today.month_ = 7;
today.year_ = 2014;
You can even use objects of other classes as members for your class, such as the Event class.
class Event
{
public:
std::string event_name_;
Date event_date_;
};
4.8.1.2. Class member functions#
Apart from containing data in member variables, classes can also have member functions (sometimes also called âmethodsâ). These are functions that operate on an object of that class. An example is a function to set the date:
class Date
{
public:
int day_;
int month_;
int year_;
void setDate(int day, int month, int year)
{
day_ = day;
month_ = month;
year_ = year;
}
};
This example shows why we named our member variables with a trailing
underscore, as there is a clear difference between the member variables
and the function parameters of setDate. If you want to separate the
design of the class from itsâ implementation (as you should, this course
manual does not to save space), the definition of the class can be
written as follows.
class Date
{
public:
int day_;
int month_;
int year_;
void setDate(int day, int month, int year);
};
The implementation of the setDate function can be provided elsewhere as
usual. The setDate function is then prefixed with âDate::â to tell the
compiler it is the implementation of the setDate function with in the
Date class. Remember to include the class definition before providing
the implementation.
void Date::setDate(int day, int month, int year)
{
day_ = day;
month_ = month;
year_ = year;
}
Calling the member functions of a class works similar to accessing the data members:
Date today;
today.setDate(1, 7, 2014);
4.8.1.3. Special member functions#
Each class has special member functions for constructing and deconstructing objects of that class. These special member functions are the constructors and destructors. Unlike normal member functions, the name of the constructors and destructors cannot be specified. Constructors and destructors do not have a return type.
4.8.1.3.1. Constructors#
Every time you instantiate an object of a class, one of its constructors is called. A constructor is a special member function responsible for initializing all data members of your class correctly. First, the helps ensuring an object of your class is always representing a meaningful state, making sure that all important fields are set. Second, a constructor can make it easier to use the object later, indicating to the programmer what type of information is needed to setup the object.
A constructor:
can have any number of parameters;
unlike normal member functions, it does not have a return type and the name;
always has the same as the class (including capitalization).
A class can define multiple constructors, as long as they have unique function signatures. Basically, defining multiple constructors is just a case of function overloading.
class Date
{
public:
int day_;
int month_;
int year_;
Date(int day, int month, int year) // a custom constructor
{
day_ = day;
month_ = month;
year_ = year;
}
Date() // a default constructor, which takes no parameters
{
day_ = 1;
month_ = 1;
year_ = 1970;
}
};
int main() {
// Example of creating an object using each constructor
Date today(1, 7, 2014); // calls the custom constructor
Date epoch; // calls the default constructor
return 0;
}
The above code shows two (!) constructors for the Date class, one with and
one without parameters.
4.8.1.3.2. Initializer lists#
If a constructor simply initializes some member variables, it can be written with an initializer list. The initializer list is placed just before the body of the constructors and separated from the constructors parameter list with a colon. Note that the members initialized in the initializer list need to be listed in the same order as in which they are defined in the class. The initializer list does not need to be complete, not all members need to be initialized this way.
So, this constructor:
Date(int day, int month, int year) // constructor without initializer lists
{
day_ = day;
month_ = month;
year_ = year;
}
can be rewritten with initializer lists as:
Date::Date(int day, int month, int year) : day_(day), month_(month), year_(year)
{
// empty body, since all member variables are already initialized
}
Initializer lists avoid the need for the compiler to setup the member variables with some default value before the start of the constructor where you would change their value. In many cases this does not really matter, but in a few cases it is quite important:
Member variables can also be declared
const, and therefore would not allow reassignment in the constructor function; they can only be set in initializer lists.Member variables can be objects of classes without a default constructor themselves, meaning they donât have a default initialization, but you can call any custom constructor in the initializer list.
You can call other constructors of the same class with fewer arguments to avoid repeating code (this is called constructor delegation).
You can also call constructor of âparent classesâ when creating subclasses, as will be discussed in a moment in Section 4.8.4.2.
Exercise 4.18
Write a parameter-free constructor of the Date class that uses an initializer list. Make sure that the date is set to January 1st, 1970.
By default, each class has a constructor without parameters. This default constructor has an empty body and no initializer list. This default constructor is no longer available once you provide a constructor of your own.
4.8.1.3.3. Destructors#
Constructors are responsible for getting your object ready to be used.
Similarly, the destructor is a special function responsible for cleaning up the object.
An object destructor is automatically called when the object runs out of scope or
the program shuts down, or whenever delete is called on a pointer to
an object.
The destructor has some similarities to the constructors:
a destructor has no return type, but it must take no parameters
the name of the destructor is exact same name of the class, but unlike the constructor the destructor name is preceded by a tilde (
~).
While a class can have multiple constructors, the class can only define one or zero destructors. If you do not define a destructor, a default destructor is used which is basically a destructor which executes no additional code.
class Date
{
public:
int day_;
int month_;
int year_;
Date(int day, int month, int year) // a custom constructor
{
// ... omitted for brevity
}
Date() // a default constructor, which takes no parameters
{
// ... omitted for brevity
}
~Date() { // the destructor
// code to execute when object is destructed goes here
// ...
}
};
Cleaning up an object can mean anything from destroying contained objects to releasing an opened file, so that file can be accessed by other programs.
By default memory used for members of your object is freed.
However, extra care has to be taken with pointers stored in your class,
because the pointer itself we be cleared when the object is freed,
but not any memory pointed to by the pointer unless we explicitly create a destructor to do so!
For example, if your Event class stores a pointer to a Date object, by default
only the memory used by the pointer is released, but not the Date object
itself!
Recall from Section 4.7.8 that destroying any data pointed to by a pointer is done by calling delete on
the pointer (or delete[] when the pointer points to a simple array of objects).
class Event
{
public
Date* date_;
Event()
{
// this Event object will keep its own date object on the heap
date_ = new Date();
}
~Event()
{
// as this Event object is being destroyed,
// nothing will can access the Date object anymore,
// so we must delete it.
delete date_;
}
}
Exercise 4.19
Can you think of a reason not to delete a pointer in a constructor?
Write a code that shows your reasoning.
4.8.2. Pointers to classes#
Like with the primitive data types, you can store a pointer to an object instead of the object itself, for instance if youâd want to share that object throughout multiple classes. In the destructor section, you have already seen one such pointer.
You can also create an object and directly obtain a pointer to it with
the new directive. Do note that if you do, you are responsible for
properly deleting this object with delete at some point.
The pointer will be destroyed as it runs out of scope, but the object pointed to will not!
By calling delete, the objectâs destructor will be called first,
and after the destructor finished the memory containing that the objectâs member variables will be released.
Date* today = new Date(1, 7, 2014);
// more code
delete today;
To access members and member functions of an object to which you have a
pointer, you need to use the dereference operator * which can be a
bit convoluted. For example, setting the date on our Date class would
look like this:
Date* today = new Date(1, 7, 2014);
(*today).setDate(15, 7, 2014);
The âarrowâ operator -> is designed to simplify this. The above code
can also be written as follows:
Date* today = new Date(1, 7, 2014);
today->setDate(15, 7, 2014);
There is one special pointer that is available in any class. The pointer
this points to the object the member function being called belongs to.
It can be used to make clear in your code when accessing a data member
or calling a member function as opposed to using an external variable or
function.
Exercise 4.20
Rewrite the setDate method so that it uses the this pointer.
4.8.3. Accessibility and encapsulation#
So far, we have designed a class Date. When we instantiate an object of
this class, we can directly access itsâ data members and member
functions, because of the line that said public:.
Access can also be restricted, by using the private or protected specifier.
The specifier can be left out, if it is left out all members and methods
will be private. Access to private members can be given and controlled
through access functions or so called âgetters and settersâ.
Exercise 4.21
Write a class with public and private data members. Try accessing them from the main function. What happens?
Important
In Object Oriented-Programming, encapsulation is the idea of hiding the details of how something is implemented, and instead just exposing a âpublicâ interface to any programmer wanting to use the class. This allows this user of the class to use it without having to worry about how it is implemented.
In C++, access specifiers allow us to implement encapsulation within our classes. This is typically done by making ALL member variables of a class private, and providing public functions (often access functions) that allow the user to work with the class. Although this may seem more burdensome than providing public access directly, doing so actually provides several very useful benefits that help encourage class re-usability and maintainability.
Perhaps most importantly, sometimes it turns out that the initial implementation of a class is too slow or uses too much memory, and a more complex solution is needed. Encapsulating the implementation means that the implementation of a class can be completely changed, and as long as you donât change the publicly accessible members and member functions, any users of your class will not even notice.
Summarized, encapsulation gives the opportunity to change the internals of our classes, without breaking code that depends on the class. Another opportunity given by encapsulation is protecting the validity of the data contained by our class.
Exercise 4.22
Write the Date class with private data members. Provide getter and setter functions. Make sure that the month is always valid (between 1 and 12).
4.8.4. Inheritance#
Class inheritance is another core OOP concept, and allows to group classes with similar properties and/or capabilities together.
With class inheritance, a relation is created between so called base classes and subclasses. A base class (sometimes called parent class) describes properties and/or capabilities that its derived classes (or subclasses) can or should inherit, which comes down to either acquiring the properties and/or capabilities or specializing them. This kind of relationship is often described as the âis aâ relationship.
As an example think of shapes. Triangles and rectangles are both shapes and have similar properties like area and circumference, while their exact implementations are of course different. In this case, Shape could be perceived as a parent class and both Triangle and Rectangle as subclasses. Area and circumference are inherited properties.
Exercise 4.23
Think of at least two more examples of a parent class and some subclasses. What kind of properties and capabilities are inherited?
4.8.4.1. Declaring a subclass#
Letâs assume we have a base class Person, with properties describing
the name, age and gender of that person. For some application, a new
class Employee is required. Since an Employee is a Person,
inheritance is used. The Person class definition looks like this:
class Person
{
public:
Person(std::string name, int age, bool is_male);
std::string getName();
int getAge();
bool isMale();
private:
std::string name_;
int age_;
bool is_male_;
};
Declaring the derived Employee class is simple:
class Employee : public Person
{
public:
Employee(std::string name, int age, bool is_male,
std::string department, double salary);
std::string getDepartment();
double getYearlySalary();
private:
std::string department_;
double yearly_salary_;
};
Declaring a derived class is very similar to how one would normally
declare a class, except that the class name is followed by a :, the
public keyword and the name of the base class. The public keyword
declares that the Employee class has all the public members and
functions from Person available as if they were declared public in
Employee.
Exercise 4.24
Copy the Person and Employee definitions and implement the methods and constructors. Create an object of type Employee. Can you access the public methods from the Person class? Why?
4.8.4.2. Constructors and destructors for inheritance#
Each time an object of a derived class is constructed, its base class
part also has to be constructed. Because the construction of the derived
parts may rely on the base class parts, the base class has to be
constructed first. By default, when constructing a derived class the
default constructor of the base class is called. This is not always
desirable, such as in the Person and Employee example above. To
specify which constructor of the base class is called, you can add the
call to the initializer list:
Employee::Employee(std::string name, int age, bool is_male, std::string department, double salary)
: Person(name, age, is_male), department_(department), salary_(salary)
{
}
In contrary to constructing the base class part before the derived class part, the destruction of derived classes starts at the derived class before destructing the base class. Again, this is because the derived class may rely on the base class parts.
Exercise 4.25
Write a simple base and derived class with constructors and destructors. Write a simple program that shows the order of construction and destruction of a derived class.
4.8.4.3. Overriding member functions#
In order to add or change functionality in a derived class, with respect to its base class, you may want to override functionality of a class method.
Note
Note the distinction between overloading and overriding a member function.
Overloading is about defining multiple variants of the same (member) function, distinguished by their different function signatures. All these function versions are still accessible, you just have to use the correct function signature.
Overriding is about replacing a member function in a base class within a derived class by redefining the same function signature. An object of the base class itself would still use the original member function though.
This is done by simply declaring a derived class method with the
same name as the base class method. Consider the example of a Shape
class and a Square class:
class Shape
{
public:
double area()
{
std::cout << "Shape base class does not have a defined area" << std::endl;
return 0;
}
};
class Square : public Shape
{
private:
double side_;
public:
Square(double side) : side_(side)
{}
double area()
{
std::cout << "Calculating square area"
<< std::endl;
return side_*side_;
}
};
Exercise 4.26
Write a program that instantiates a Square object and call the area method.
It is not inconceivable that the base class method provides some
functionality and the derived class would merely want to extend that
functionality. In that case, the base class method can be called as
Base::method:
class Information
{
public:
std::string some_information_;
void printInformation()
{
std::cout << "Information: " << some_information << std::endl;
}
};
class ExtendedInformation : public Information
{
public:
std::string more_information;
void printInformation()
{
Information::printInformation();
std::cout << "ExtendedInformation: " << more_information << std::endl;
}
};
Exercise 4.27
Write a program that instantiates an ExtendedInformation object. Call
the printInformation method. What is the result?
4.8.4.4. Pointers to objects of derived classes#
In C++, it is possible to store a pointer to a derived class in a pointer to base class variable, as the example below shows.
Square* some_square = new Square(4.);
Shape* some_shape_ptr = some_square;
Treating all derived classes equally has some benefits.
Think of having multiple shapes, such as triangles, circles and squares.
If you were to store them all in a list, you would need a list for each type of shape.
However, by storing them all as the generic Shape class, they could all
exist in one list.
This could come in handy if you were to calculate the total area of a list of shapes.
Exercise 4.28
Implement the example above.
Call some_square->area().
Also call some_shape_ptr->area().
What happens?
As you have seen, the call to the Shape-pointer behaves as a Shape object, even though the pointer is actually pointing to a Square-object. How do we tell our program to let it behave as a Square instead?
4.8.4.4.1. Virtual member functions#
The keyword virtual can be added to a class method declaration. By
adding the virtual keyword to a base class method, we tell the
compiler that whenever that method is called through a pointer to the
base class, it should check if the object is actually a derived class.
class Shape
{
public:
virtual double area()
{
std::cout << "Shape base class does not have a defined area" << std::endl;
return 0;
}
};
class Square : public Shape
{
private:
double side_;
public:
Square(double side) : side_(side)
{}
virtual double area()
{
std::cout << "Calculating square area" << std::endl;
return side_*side_;
}
};
Exercise 4.29
Execute the previous exercise, with the new definitions of Shape and Square. What happens?
Exercise 4.30
Create a Triangle and Circle class, both inheriting from the Shape class. Write a program that stores some pointers to Squares, Triangles and Circles in an array of Shape pointers and uses that array to calculate the total area of the shapes.
4.8.4.4.2. Virtual destructors#
We have seen the need to declare methods as virtual, if we want pointers to base classes to behave like the derive classes when we store a pointer to a derived class in it. Similarly, if the base class destructor is not declared virtual, only the base class destructor would be called. In that case, only the memory used by the base class would be freed and not that in use by the derived class. This can lead to memory leaks and undefined behaviour of your program!