Object-Oriented Programming


Object-Oriented Programming

oop



Learning Objectives

  • Understand what is meant by the terms, encapsulation and information hiding.
  • Define classes in C++.
  • Describe the role of constructors and destructors in classes, and use them.
  • Differentiate between inspectors, mutators and facilitators.
  • Explain the difference between classes and instances.
  • Describe the static and const keywords when applied to data members and member functions.
  • Make appropriate use of class composition.
  • Write class diagrams in UML (Unified Modelling Language).

Object-Oriented Design

The last few chapters were focused on the procedural side of C++, the rest of the chapters will now focus on the OOP side. To recap in OOP languages, instead of writing sequences of instructions, the programmer defines objects with attributes and behaviours. The objects communicate with each other, sending data, and requesting certain behaviours to be carried out. Recall that all OOP languages have the following three features:

  1. Encapsulation
  2. Inheritance
  3. Polymorphism

In this chapter the concept of encapsulation will primarily be explored.

Encapsulation

Encapsulation is the grouping together of data (i.e. attributes) and code in order to operate on the data (i.e. methods/behaviours). However it is better defined as a way of restricting access to attributes/methods. The concept of this is often referred to as information hiding.

Encapsulation/information hiding allows the programmer to separate the logical properties (i.e. what something does) form its implementation details (i.e how it does it). Encapsulation allows us to break down complex problems into simpler sub-problems. By viewing a complex problem in terms of its logical properties we can specify its behaviour purely in terms of its inputs and outputs (i.e the public interface), and temporarily ignore the details of the algorithms used to achieve its desired behaviour.

Consider a surgeon panning across a medical image. The surgeon doesn’t need to be able to code to move the image, they just need to see the result. This is done by viewing a complex problem in terms of its logical properties, by specifying its behaviour only in terms of its inputs and outputs.

In OOP this encapsulation is implemented by using the class.

Classes

A class is a structure containing both:

  • data members (variables)
  • member functions (operations/behaviour)

Here is the important difference between OOP and procedural languages. In traditional procedural languages there is a clear division between data and the operations which process the data. In OOP this division is not so clear. Rather than having separate data and functions, instead we have objects, which consists of data together with operations on that data. In C++, we calls these objects classes.

Classes can be thought of as a special kind of data type, as when a variable from these data types, an object or an instance of the class is formed. Consider the following example program (contained in three source files), which stores information about, and solves quadratic equations.

The first source file, quadratic.h contains the class definition:

quadratic.h
class quadratic {
// data members
private :
  float _a, _b, _c;
// member functions
public :
  void set(float a, float b, float c);
  int solve(float& r1, float& r2);
};

The class keyword is followed by the class name quadratic. The list of data members and member functions of the class then follows, enclosed within curly brackets { }. The public and private keywords denote whether the following data members and member functions are visible from outside the class. This is the way C++ implements encapsulation, by making a data member private, it cannot be accessed from outside the class.

Also notice how all the private data member names start with an underscore, _a. This is a good convention as it shows someone reading the code what is a private data member and what is not.

Function bodies for the member functions can be specified in the class definition, but more commonly (to make things easier to read), they are specified outside. In this example, they are stored in the second source file, quadratic.cpp:

quadratic.cpp
#include <math.h>
#include "quadratic.h"

void quadratic::set(float a, float b, float c)
// assign values of equation coefficients
{
  _a = a ;
  _b = b;
  _c = c;
}

int quadratic::solve(float& r1, float& r2)
// solve equation, putting roots in r1 and r2
// return value indicates real roots (return=0)
// or complex roots (return=1)
{
  float x = _b * _b - 4 * _a * _c;
  if (x < 0) // complex roots
    return 1;
  else { // real roots
    r1 = (-_b + sqrt(x)) / (2 * _a);
    r2 = (-_b - sqrt(x)) / (2 * _a);
    return 0;
    }
}

The :: means scope operator which specifies that these function bodies are for class member functions. Member functions can automatically access any data member, even private ones.

The third source file, is the main.cpp:

main.cpp
# include <iostream>
# include "quadratic.h"
using namespace std;

int main ()
{
  quadratic q ; // define quadratic instance
  float a, b, c, root1, root2;
  cout << " Enter quadratic equation coefficients a b c: ";
  cin >> a >> b >> c;
  q.set(a, b, c);
  cout << "Equation: " << a << "x^2 + " << b << "x + "
       << c << endl;
  if (q.solve(root1, root2) == 0)
    cout << "Roots = " << root1 << " and " << root2
         << endl;
  else
    cout << "Complex roots" << ends;
  return 0;
}

Note the difference between the class quadratic, and the object/instance q. There is one class definition for quadratic, but we could have many instances of this class. The terms class and instance/object are analogous to data type and variable when talking about ‘normal’ (i.e. non-class) data.

In this driver program, we create an instance of the quadratic class, tell it which coefficients to use (by calling the set member function), and then tell ask it to solve the equation (by calling the solve member function).

Note that the syntax for member function calls (i.e. separating the object name from the function call by a full stop .) is the same as that seen for the length() function in the string library in Chapter 4. In fact string is just a class like quadratic, with its own data members and member functions

UML Class Diagrams

Classes can be represented graphically using notation known as UML. The UML class diagram for the quadratic class is shown below:

Chap_5_img_1

In UML, each class is represented by a rectangular box. The box is divided into three sections separated by horizontal lines:

  • Class Name, top section.
  • Details of data members of the class, middle section. Before the members, + indicates public, - indicates private domain.
  • Details of member functions, bottom section. Here the return type of the member function is indicated after the colon symbol.

Types of Member Function

To introduce some more complex C++ concepts, the following program is a more complex example. This particular program creates a database of student information. A C++ class is defined to store and display the student information. In this simple example only two students are created in the database.

student.h
#include <string.h>
using namespace std;

enum FullPart {Fulltime, Parttime};

class Student {

// data members
private:
  string _firstName;
  string _lastName;
  FullPart _programme;
 
// member functions
public:
  Student();
  Student(string fn, string ln, FullPart p);
  ~Student();
  void setName(string fn, string ln);
  void setProg(FullPart p);
  FullPart getProg() const;
  void Print() const;
};

The Student class contains three private data members: an enumeration type and two strings. Notice how there is a special member function that has the same name as the class Student. These are called constructors, and they are called whenever a new instance of a class is created.

Here we have two constructors, so we can say that they are overloaded. One of the overloaded constructors takes no arguments, and this is known as the default constructor. The other takes 3 arguments which are assigned in the student.cpp source file below.

There is another special function called ~Student(), and this is called a destructor. However unlike constructors, there can only be one destructor for a class and it should always take no arguments. It is called whenever an instance is destroyed, i.e. it goes out of scope and is discarded.

student.cpp
#include <iostream>
#include "student.h"
using namespace std;

// constructor
Student::Student() {
  cout << "Student Constructor 1" << endl;
  _firstName = " ";
  _lastName = " ";
  _programme = Fulltime;
}

// constructor
Student::Student(string fn, string ln, FullPart p) {
  cout << "Student Constructor 2" << endl;
  _firstName = fn;
  _lastName = ln;
  _programme = p;
}

// destructor
Student::~Student() {
  // empty for now
  cout << "Student Destructor" << endl;
}

// mutator
void Student::setName (string fn, string ln) {
  _firstName = fn;
  _lastName = ln;
}

// mutator
void Student::setProg (FullPart p) {  
  _programme = p; 
}

// inspector 
FullPart Student::getProg () const {  
  return _programme; 
}

// facilitator
void Student::Print () const {  
  cout << "----------" << endl;
  cout << "Name: " << _firstName
       << " " << _lastName << endl;
  if (_programme == Fulltime)
     cout << "Full-time\n";
  else
     cout << "Part-time\n";
  cout << "---------" << endl;
}

Note that all classes automatically have a default constructor, even if it isn’t defined by the programmer. However, if a constructor is defined with arguments as above, the default constructor is no longer available, unless explicitly defined by the programmer

Other member functions of Student can be categorised as:

  • Inspectors: report the value of a data member (usually a private one)
  • Mutators: changes or set the value of a data member. (mutators are often necessary if we have private data members as access to them is not permitted from outside the class definition)
  • Facilitators: cause an instance to perform some action or service.

Also look how Print() and getProg() have the keyword const after them. This tells the C++ compiler that none of the data member’s values in the functions will change, otherwise it will result in a compilation error. This isn’t strictly necessary but it helps to highlight bugs that involve accidentally changed data members.

In the main() function below, the instances of the Student class is created in two ways. Either by defining initial values, Student s ("John", "Smith", Full-time); or not Student s2;. One of the two overloaded constructors is called when a new instance is created, depending on whether the initial values are specified or not.

main.cpp
#include <iostream>
#include <string.h>
#include "student.h"
using namespace std;

int main() {
  Student s ("John", "Smith", Full-time);
  s.Print();
  
  Student s2;
  s2.Print();
  s2.setName ("Josephine", "Bloggs");
  s2.setProg (Parttime);
  s2.Print();
  
  return 0;
}

Static Data Members and Member Functions

In C++ classes, all objects/instances of a particular class have their own copies of data members. However by making a data member static, only one copy of the data member is stored for all instances, irrespective of how many objects of the class are created. All objects access the same copy. Static data members must be initialised at the beginning of a program, outside the class definition, using the class scope operator ::.

To illustrate a static data member let’s modify the previous example program (see below). By adding the possibility of storing a count of the total number of students in the database. Each time a new Student object is created/destroyed, one is added/subtracted to the count. This is accomplished by defining an extra public static data member called _count in the Student class. This is incremented in the Student constructors and decremented in the Student destructor.

student.h count modification
...
class Student {

// data members
private:
  ...
  static int _count;

// member functions
public:
  ...
  static int Count() {return _count;}
};

The new inspector member function static int Count() {return _count;} is marked as static. A static member function can be called even if there are no instances of a class yet. This is necessary for inspectors of static data members. The corresponding function body for Count() is defined inside the class definition. This is always allowed even for normal member functions, but if we have too many long function bodies it can make the class definition hard to read.

student.cpp count modification
int Student::_count = 0;
...

// constructor
Student::Student() {
  ...
  _count += 1;
}

// constructor
Student::Student(string fn, string ln, FullPart p) {
   ...
   _count += 1;
}

// destructor
Student::~Student() {
  ...
  _count -= 1;
}

Normally, private data members are not visible outside of the class. Initialising a static data member is an exception to this rule, (in this case private data member _count).

main.cpp count modification
int main ()
{
   ...
   count << "No. students = " << Student::Count()
         << endl;
   ...
}

When accessed from outside of the class definition, static data members and member functions must be accessed using the scope operator ::.

Class Composition

All the examples in this chapter have used single classes. However, often more complex C++ programs will involve defining multiple classes. Each class represents an object in the problem domain and these objects may have relationships between them. The next example illustrates such a case.

This program defines classes to store and manipulate 2-D vectors. A vector consists of two points: the start point of the vector and the end point. A point is represented by its two co-ordinates.

vector.h
class Point {
private:
  float _x, _y;
public:
  Point() {_x = _y = 0;} // constructor
  ~Point() {};           // destructor
  void Set(float x, float y) {_x = x; _y = y;}
  float GetX() const {return _x;}
  float GetY() const {return _y;}
};

class Vector {
private:
  Point _start, _end;
public:
  // constructors
  Vector() {};
  Vector(Point s, Point e) {_start = s; _end = e;}
  // destructor 
  ~Vector() {};
  void Set(Point s, Point e) {_start = s; _end = e;}
  Point GetStart() const {return _start;}
  Point GetEnd() const {return _end;}
  void Print() const;
  float DotProd(Vector v2) const;
}; 

Point _start, _end;, is a prime example of class composition where a class can be a member of another class. Class composition is appropriate for a particular type of relationship in which one class consists of, or has the other. In this case, Vector consists of two Points.

In Vector(Point s, Point e) {_start = s; _end = e;} all data members are copied from one instance to the other.

vector.cpp
#include <iostream>
#include "vector.h"
using namespace std;

void Vector::Print() const {
  cout << "(" << _start.GetX() << ", " << start.GetY()
       << ")->(" << _end.GetX() << ", " << _end.GetY()
       << ")" << endl;
}

float Vector::DotProd(Vector v2) const {
  float x = _end.GetX() - _start.GetX();
  float y = _end.GetY() - _start.GetY();
  float x2 = v2.GetEnd().GetX() - v2.GetStart().GetX();
  float y2 = v2.GetEnd().GetY() - v2.GetStart().GetY();
  return (x * x2 + y * y2);
}

The following main() function illustrates the use of these classes:

main.cpp
int main() {
  Point p1, p2, p3;
  p1.Set(0,0);
  p2.Set(3,2);
  p3.Set(2,4);
  Vector v1(p1,p2);
  Vector v2(p1,p3);
  cout << "v1:" << endl;
  v1.Print();
  cout << "v2:" << endl;
  v2.Print();
  cout << "v1 . v2: " << v1.DotProd(v2) << endl;
  
  Vector v3 = v1;
  cout << "v3:" << endl;
  v3.Print();
  
  return 0;
}

Note that when a Point argument is supplied to a Vector constructor (Vector v1(p1,p2);), it is a pass-by-line i.e. a copy of the Point is made to create the new Vector instance.

A UML Class diagram illustrating the relationship between the Point and Vector classes is shown below:

Chap_5_img_1

The line between Vector and Point indicate a relationship between the classes, and the filled diamond indicates that it is a composition relationship.

Copy Constructors

In the previous point/vector example, when an instance was assigned to another instance (Vector v3 = v1;), it was actually invoking the default copy constructor. A copy constructor for a class is any constructor that takes another class instance as its argument. It is called whenever an instance is created using another class instance as an argument, or if an instance is declared and assigned in a single statement. If we don’t define a copy constructor, a default copy constructor will be created for us, which will assign all corresponding data members between two instances.

A copy constructor can be defined if specific behaviour is desired, as the code below illustrates:

vector.h copy constructor defined
class Point {
...
public:
Point(const Point& p) {
  _x = p.GetX();
  _y = p.GetY();
  cout << "Copy constructor ..." << endl;
}
...

Note that in this case the new copy constructor doesn’t do anything differently to the default copy constructor, it just copies all data members.

The copy constructor is like a normal Point constructor except that its only argument has the type const Point&. This is called a const reference type.


return  link
Written by Tobias Whetton