Part C - Encapsulation

Classes

Describe the elements of a class
Introduce constructors and destructors for comprehensive encapsulation
Overload constructors for added flexibility

"A class is a cohesive package that ... describes the rules by which objects behave; these objects are referred to as instances of that class." (Wikipedia, 2008)

Classes | Privacy | Constructor | Destructor | Arrays | Overloading | Summary | Exercise


The principal compound type for encapsulating data and logic is the class.  A class describes the structure of the data that its objects hold and the rules according to which its member functions access and change that data.  A well-encapsulated class has all of its implementation details hidden within itself.  Clients communicate with objects of a well-encasulated class only through its public member functions. 

This chapter describes the syntax for defining classes and the two special member functions that initialize and tidy up objects.  The description covers how memory is allocated and deallocated during construction and destruction of objects as well as overloading of one of the special functions to improve communication with the client. 


Classes

Class Syntax

A C++ compound type takes either of two forms:

  • struct
  • class

The keyword struct identifies a compound type that is public by default.  The keyword class identifies a compound type that is private by default.  Note that the C language does not support privacy and a compound type in C can only take the form of a struct.  Classes are much more common in object-oriented programming than structs. 

Consider the following definition of a Student compound type

 struct Student {
 private:
     int no;
     char grade[14];
 public:
     void set(int n, const char* g); 
     void display() const;
 };

Here, the data members no and grade are private, while the set() and display() functions are public.

This definition is identical to

 class Student {
     int no;
     char grade[14];
 public:
     void set(int n, const char* g); 
     void display() const;
 };

Class Diagram

The UML class diagram for our Student class is shown below.  The return data type of each method follows the colon (:) after the method's signature.  The names of the private members are prefixed with a - sign, while the names of the public members are prefixed with a + sign.

Student
 - no : int
 - grade[14] : char
 + set ( int, const char* ) : void 
 + display ( ) const : void  

Object or Instance

Each object or instance of a class occupies its own region of memory.  The class describes how to interpret the information in that region of memory. 

A definition of an object or instance of a class takes the form

 Type instance;

Type denotes the name of the class.  instance denotes the name of the object or instance of the class. 

For example, to create an object or instance of our Student class named harry, we write:

 Student harry;

To create five objects or instances of our Student class, we write:

 Student a, b, c, d, e;

This compound definition allocates five regions in static memory, each of which holds the data for one object.  Each region contains space for two data members - no and grade

instance

The member functions of a class are shared amongst all objects of a class.

Instance Variables

We call the data members declared in the class definition the object's instance variables.  Instance variables may (amongst others) be of

  • fundamental type (int, double, char, etc.)
  • compound type (struct, class)
  • pointer type (to instances of data types - fundamental or compound)
  • reference type (to instances of data types - fundamental or compound)

Logic

Since the logic within the member functions of a class is identical for every instance of the class, there is no need to allocate separate memory for the logic associated with each object.  Only the instance variables are stored separately.  Each call to a member function on an instance of a class accesses the same code, while accessing different instance variables - those of the object on which the member function has been called. 

For example, the same display() function called on five different Student objects displays five different sets of information in the same way:

 Student a, b, c, d, e;

 // different settings for each object

 a.display();  // displays the data stored in a
 cout << endl;
 b.display();  // displays the data stored in b
 cout << endl;
 c.display();  // displays the data stored in c
 cout << endl;
 d.display();  // displays the data stored in d 
 cout << endl;
 e.display();  // displays the data stored in e
 cout << endl;

The memory allocated for member functions code and object data is illustrated below:

memory


Class Privacy

C++ implements privacy at the class level.  That is, any member function of a class can access any private member of that class, including any data member of any instance of that class, including an instance other than that on which the member function was called.  In other words, privacy holds at the class level, not at the object level. 

For example, within a member function called on a Student object, we may refer to a private data member of some other Student object:

 class Student {
     int no;
     char grade[14];
 public:
     void set(const Student& src);
     void set(int n, const char* g);
     void display() const;
 };

 // ...

 void Student::set(const Student& src) { 
     no = src.no;
     strcpy(grade, src.grade);
 }

 // ...

 int main() {
     Student harry, backup;
     harry.set(975, "ABA");
     backup.set(harry);
 }

Here, set(const Student& src) copies the data from the private data members of harry into the private data members of backup.


Constructor

Comprehensive encapsulation requires a mechanism for initializing the data members of an object at creation-time.  Without initialization, an object's private data members contain undefined values until the client calls a modifier on the object.  A client can inadvertently 'break' the object by calling member functions in the wrong order.  For instance, a client could call a member function to read a file before calling the member function that opens it.  To prevent breakage, we initialize an object's data members to a safe empty state and provide a graceful path for objects in a safe empty state in every public member function.  C++ lets us define a special member function called a constructor for initializing objects at creation-time.

Definition

A constructor is a special member function of a class that an object calls immediately upon creation.  This member function executes all preliminary logic and may be used to initialize an object's instance variables. 

The constructor takes its name from the class itself.  The prototype for a constructor with no arguments takes the form

 Type();

Type is the name of the class.  Constructor prototypes or definitions do not include a return data type. 

For example, to define a no-argument constructor for our Student class, we declare its prototype in the class definition:

 class Student {
     int no;
     char grade[M+1];
 public:
     Student();
     void set(int, const char*);
     void display() const;
 };

and define the constructor in the implementation file:

 Student::Student() {
     no = 0;
     grade[0] = '\0';
 }

If we do not declare a constructor in the class definition, the compiler inserts a no-argument constructor with an empty body: 

 Student::Student() {
 }

Understanding Order

Construction

The construction of an object proceeds in the following order

  1. allocate memory for each instance variable in the order listed in the class definition
  2. execute the logic within the constructor

Member Function Calls

The relation between the call to a constructor and the calls to the normal member functions on the object is illustrated in the figure below.  The constructor is called before any normal member function.

constructor

Multiple Objects

A compound definition creates objects in the order specified by the definition.

For example, the following code generates the output on the right

 // Constructors
 // constructors.cpp

 #include <iostream>
 #include <cstring>
 using namespace std;
 const int M = 13;

 class Student {
     int no;
     char grade[M+1];
 public:
     Student();
     void set(int, const char*);
     void display() const;
 };

 // initializes the data members
 //
 Student::Student() {
     cout << "Entering constructor" << endl; 
     no = 0;
     grade[0] = '\0';
 }

 void Student::set(int n, const char* g){
     // see p.34 for validation logic
     no = n;
     strcpy(grade, g);
 }

 void Student::display() const {
     cout << no << ' ' << grade;
 }

 int main () {
     Student harry, josee;

     harry.set(1234, "ABACA");
     josee.set(1235, "BBCDC");
     harry.display();
     cout  << endl;
     josee.display();
     cout  << endl;
 }




































 Entering constructor
 Entering constructor


 1234 ABACA

 1235 BBCDC


harry and josee construct in the order of their definition in the main() function.

Safe Empty State

Initializing an object's instance variables in a constructor ensures that the object has a well-defined state from the moment of creation.  In the above example, harry and josee are in safe empty states until set() changes those states.  If the client calls member functions on these objects in any 'unusual' order, the objects do not break and the results are still as expected. 

For example,

 // Safe Empty State
 // safeEmpty.cpp

 #include <iostream>
 using namespace std;

 int main ( ) {
     Student harry, josee;

     harry.display();
     cout  << endl;
     josee.display();
     cout  << endl;
     harry.set(1234,"ABACA");
     josee.set(1235,"BBCDA");
     harry.display();
     cout  << endl;
     josee.display();
     cout  << endl;
 }







 Entering constructor
 Entering constructor
 0

 0



 1234 ABACA

 1235 BBCDC


The initial values displayed for each object are their safe empty state values.

The safe empty state vaule is the same for all objects of the same class.


Destructor

Comprehensive encapsulation also requires a mechanism for tidying up immediately before the end of the object's lifetime.  An object that has written data to a file may have to flush its buffer before disappearing, while an object that has allocated memory dynamically may have to deallocate that memory before disappearing.  C++ lets us define a special member function called the destructor that executes automatically at destruction-time.

Definition

The destructor is a special member function that every object calls immediately before the end of its lifetime.  This member function executes the terminal logic. 

The destructor takes its name from the class itself and prefixes that name with the tilde symbol (~).  A destructor prototype takes the form

 ~Type( );

Type is the name of the class.  Destructors do not have parameters, do not return a value and do not have a return data type. 

For example, to define the destructor for our Student class, we declare its prototype in the class definition:

 class Student {
     int no;
     char grade[M+1];
 public:
     Student();
     ~Student();
     void set(int, const char*);
     void display() const;
 };

and define the destructor in the implementation file:

 Student::~Student() {
    // our destructor code here
 }

If we don't declare a prototype in the class definition, the compiler defines the destructor with an empty body:

 Student::~Student() {
 }

Understanding Order

Member Function Calls

The relation between normal member function calls on an object and the call to the destructor is illustrated in the Figure below.  The object always calls its destructor after all of the normal member function calls. 

constructor to destructor

The object cannot call any member function after having called its destructor.

Destruction

An object destroys itself in the following order

  1. executes the logic of its destructor
  2. deallocates memory for each instance variable in opposite order to that listed in the class definition

Multiple Objects

Objects are destroyed in opposite order to the order of their creation.

For example, the following code generates the output on the right:

 // Constructors and Destructors
 // destructors.cpp

 #include <iostream>
 #include <cstring>
 using namespace std;
 const int M = 13;

 class Student {
     int no;
     char grade[M+1];
 public:
     Student();
     ~Student();
     void set(int n, const char* g);
     void display() const;
 };

 Student::Student() {
     cout << "Entering constructor" << endl; 
     no = 0;
     grade[0] = '\0';
 }

 // executed before object goes out of scope 
 //
 Student::~Student() {
    cout << "Entering destructor for " << no
         << endl;
 }

 void Student::set(int n, const char* g){
     // see p.34 for validation logic
     no = n;
     strcpy(grade, g);
 }

 void Student::display() const {
     cout << no << ' ' << grade;
 }

 int main () {
     Student harry, josee;

     harry.set(1234, "ABACA");
     josee.set(1235, "BBCDC");
     harry.display();
     cout  << endl;
     josee.display();
     cout  << endl;
 }








































 Entering constructor
 Entering constructor


 1234 ABACA

 1235 BBCDC


 Entering destructor for 1235
 Entering destructor for 1234

harry and josee destroy themselves in opposite order to that in which the main() function created them.


Construction and Destruction of Arrays

The order of construction and destruction of the elements of an array of objects follows directly from the order described above. 

The elements of an array are created one at a time from the first to the last.  Each object calls the no-argument constructor at creation-time.  During deallocation, each object starting from the last element and proceeding sequentially to the first element calls its destructor in turn. 

For example, the following code generates the output on the right:

 // Constructors, Destructors and Arrays
 // ctorsDtorsArrays.cpp

 #include <iostream>
 #include <cstring>
 using namespace std;
 const int M = 13;

 class Student {
     int no;
     char grade[M+1];
 public:
     Student();
     ~Student();
     void set(int, const char*);
     void display() const;
 };

 Student::Student() {
     cout << "Entering constructor" << endl; 
     no = 0;
     grade[0] = '\0';
 }

 Student::~Student() {
     cout << "Entering destructor for " <<
          no << endl;
 }

 void Student::set(int n, const char* str){
     // see p.34 for validation logic
     // code
     no = n;
     strcpy(grade, g);
 }

 void Student::display() const {
     cout << no << ' ' << grade;
 }

 int main () {
     Student a[3];

     a[0].set(1000, "AAAAA");
     a[1].set(1001, "BBBBB");
     a[2].set(1002, "CCCCC");
     for (int i = 0; i < 3; i++) {
         a[i].display();
         cout  << endl;
     }
 }






































 Entering constructor
 Entering constructor
 Entering constructor
 1000 AAAAA

 1001 BBBBB

 1002 CCCCC


 Entering destructor for 1002 
 Entering destructor for 1001
 Entering destructor for 1000

Note that the destructor for element a[2] executes before the destructor for a[1], which executes before the destructor for a[0].


Overloading Constructors

Overloading the no-argument constructor of a class adds flexibility in communications with client applications.  With a set of overloaded constructors, a client application can select the set of arguments to pass at creation time that is most appropriate for initialization. 

For example, to let a client initialize a Student object with a student number and a set of grades, we define a two-argument Student constructor - one int parameter and one const char* parameter: 

 // Two-Argument Constructor
 // overload.cpp

 #include <iostream>
 using namespace std;
 const int M = 13;

 class Student {
     int no;
     char grade[M+1];
 public:
     Student();
     Student(int, const char*);
     void display() const;
 };

 Student::Student() {
    no = 0;
    grade[0] = '\0';
 }

 Student::Student(int n, const char* g) {
     // see p.34 for validation logic
     no = n;
     strcpy(grade, g);
 }

 void Student::display() const {
     cout << no << ' ' << grade;
 }

 int main () {
     Student harry(1234,"ABACA"),
             josee(1235,"BBCDA");

     harry.display();
     cout  << endl;
     josee.display();
     cout  << endl;
 }



































 1234 ABACA

 1235 BBCDA 


This new constructor includes all of our validation logic.  We have replaced the set() member function with this constructor. 

No-argument constructor is not always implemented

If the class definition includes the prototype for a constructor with a positive number of parameters and does not include the prototype for a no-argument constructor, the compiler DOES NOT insert an empty-body, no-argument constructor.  The compiler only inserts an empty-body, no-argument constructor if the class definition does not declare ANY constructor.

If we define a constructor with a positive number of parameters, we typically also define a no-argument constructor.  No-argument constructors are called in the creation of arrays of objects (each element of the array calls the no-argument constructor at creation time).

Single-Argument Constructors

A single-argument constructor serves a unique dual role.  It also defines the rule for promoting a variable or object to the class type.

For example, let us add a constructor that takes an int argument:

 // One-Argument Constructor
 // oneArgCtor.cpp

 #include <iostream>
 using namespace std;
 const int M = 13;

 class Student {
     int no;
     char grade[M+1];
 public:
     Student();
     Student(int);
     Student(int, const char*);
     void display() const;
 };

 Student::Student() {
     no = 0;
     grade[0] = '\0';
 }

 Student::Student(int n) {
     no = n;
     grade[0] = '\0';
 }

 Student::Student(int n, const char* g) {
     // see above for validation logic
     no = n;
     strcpy(grade, g);
 }

 void Student::display() const {
     cout << no << ' ' << grade;
 }

 int main () {
     Student harry(975), student;

     harry.display();
     cout  << endl;
     student = Student(976);
     student.display();
     cout  << endl;
 }








































 975 


 976 


The single-argument constructor converts 976 to a Student object containing a student number of 976 and an empty grade list. 

Promotion

For the same result we could replace the main() function with the following:

 int main () {
     Student harry, student; // calls no-argument constructor twice 

     harry = 975;   // promotes an int to a Student

     harry.display();
     cout  << endl;

     student = 976; // promotes an int to a Student

     student.display();
     cout  << endl;
 }





 975 




 976 


In the first part, the compiler inserts code to promote 975 to a temporary Student object.  This code calls the single-argument constructor.  The constructor receives the value 975 and initializes no to 975 and grade to an empty string.  The assignment expression copies the temporary object's member data into harry.  The compiler inserts code after the assignment code to destroy the temporary object and remove it from memory. 

In the second part, the compiler inserts code to promote 976 to a temporary Student object.  This code calls the single-argument constructor.  The constructor receives the value 976 and initializes no to 976 and grade to an empty string.  The assignment expression copies the temporary object's member data into student.  The compiler inserts code after the assignment code to destroy the temporary object and remove it from memory. 

Limiting the number of single-argument constructors avoids potential ambiguities in automatic conversion of one data type to another.


Summary

  • a class is a compound type, the members of which are private by default
  • we refer to the data members of an object as its instance variables
  • the constructor and destructor are special member functions that each object calls at creation and destruction time respectively
  • the name of the constructor is the name of the class
  • the name of the destructor is the name of the class prefixed by a ~
  • the constructor and destructor do not have return types
  • privacy operates at the class level, not at the object level
  • the compiler inserts an empty body constructor/destructor into any class definition that does not include a prototype for a constructor/destructor
  • the compiler does not insert an empty-body, no-argument constructor into any class definition that includes a constructor prototype

Exercises




Previous Reading  Previous: Dynamic Memory Next: The Current Object   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo
Creative Commons License