Classes and Objects

Introduction to Object-Oriented Programming

Procedural programming vs. Object-Oriented programming

Procedural programming:

Object-Oriented programming: Usually, for a language to be considered object-oriented, it should have these three properties:
  1. Encapsulation (data abstraction/hiding)
  2. Inheritance (relationships between entities)
  3. Polymorphism (runtime decisions)
The topic in bold is what we will be concerned with now.

In C++, these three properties are realized as:

  1. Classes and objects
  2. Extending classes with an is-a relationship
  3. Virtual methods and dynamic binding

Procedural Programming

Let's use this structure that represents a student: (What is sizeof(Student))?
const int MAXLENGTH = 10;

struct Student           
{
  char login[MAXLENGTH];
  int age;
  int year;
  float GPA;
};
void display_student(const Student &student)
{
  using std::cout;
  using std::endl;

  cout << "login: " << student.login << endl;
  cout << "  age: " << student.age << endl;
  cout << " year: " << student.year << endl;
  cout << "  GPA: " << student.GPA << endl;
}
This allows this code:as well as this:
void f1()
{
  Student st1;

  st1.age = 20;
  st1.GPA = 3.8;
  std::strcpy(st1.login, "jdoe");
  st1.year = 3;

  display_student(st1);
}

Output:
login: jdoe
  age: 20
 year: 3
  GPA: 3.8
void f2()
{
  Student st2;

  st2.age = -5;
  st2.GPA = 12.9;
  std::strcpy(st2.login, "rumplestiltzkin");
  st2.year = 150;

  display_student(st2);
}

Output: (May get lucky)
login: rumplestiltzkin
  age: 7235947
 year: 150
  GPA: 12.9
A second attempt to "protect" the data by using functions to set the data instead of the user directly modifying it.
void set_login(Student &student, const char* login);
void set_age(Student &student, int age);
void set_year(Student &student, int year);
void set_GPA(Student &student, float GPA);
void set_login(Student &student, const char* login)
{
  int len = std::strlen(login);
  std::strncpy(student.login, login, MAXLENGTH - 1);
  if (len >= MAXLENGTH)
    student.login[MAXLENGTH - 1] = 0;
}
void set_age(Student &student, int age)
{
  if ( (age < 18) || (age > 100) )
  {
    std::cout << "Error in age range!\n";
    student.age = 18;
  }
  else
    student.age = age;
}
void set_year(Student &student, int year)
{
  if ( (year < 1) || (year > 4) )
  {
    std::cout << "Error in year range!\n";
    student.year = 1;
  }
  else
    student.year = year;
}
void set_GPA(Student &student, float GPA)
{
  if ( (GPA < 0.0) || (GPA > 4.0) )
  {
    std::cout << "Error in GPA range!\n";
    student.GPA = 0.0;
  }
  else
    student.GPA = GPA;
}
Now this code:results in this:
void f3()
{
  Student st3;

  set_age(st3, -5);
  set_GPA(st3, 12.9);
  set_login(st3, "rumplestiltzkin");
  set_year(st3, 150);

  display_student(st3);
}
Error in age range!
Error in GPA range!
Error in year range!
login: rumplesti
  age: 18
 year: 1
  GPA: 0
Notes: The solution? Put the functions inside the Student struct along with the data. This is encapsulation.

In C++, encapsulated functions are generally called methods or member functions (because they are members of the structure).

Encapsulating Functions and Data

Adding functions to the structure is simple. By declaring them in a public section, the functions (methods) will be accessible from outside of the structure:

Structure with private data, public methodsClient access is through the public methods
const int MAXLENGTH = 10;

struct Student           
{
  public:
    void set_login(const char* login);
    void set_age(int age);
    void set_year(int year);
    void set_GPA(float GPA);

  private:
    char login_[MAXLENGTH];
    int age_;
    int year_;
    float GPA_;
};
void f1()
{
    // Create a Student struct (object)
  Student st1;

    // Set the fields using the public methods
  st1.set_login("jdoe");
  st1.set_age(22);
  st1.set_year(4);
  st1.set_GPA(3.8);

  st1.age_ = 10; // ERROR, private
  st1.year_ = 2; // ERROR, private
}
The implementation of the methods will change slightly:

void Student::set_login(const char* login)
{
  int len = std::strlen(login);
  std::strncpy(login_, login, MAXLENGTH - 1);
  if (len >= MAXLENGTH)
    login_[MAXLENGTH - 1] = 0;
}
void Student::set_age(int age)
{
  if ( (age < 18) || (age > 100) )
  {
    std::cout << "Error in age range!\n";
    age_ = 18;
  }
  else
    age_ = age;
}
void Student::set_year(int year)
{
  if ( (year < 1) || (year > 4) )
  {
    std::cout << "Error in year range!\n";
    year_ = 1;
  }
  else
    year_ = year;
}
void Student::set_GPA(float GPA)
{
  if ( (GPA < 0.0) || (GPA > 4.0) )
  {
    std::cout << "Error in GPA range!\n";
    GPA_ = 0.0;
  }
  else
    GPA_ = GPA;
}
You'll notice a few things about these implementations:

Incidentally, the default access for a struct is public. (This is for C compatibility.) These two structures are identical:
Members are public by defaultOK, but redundant
struct Student
{
  char login_[MAXLENGTH];
  int age_;
  int year_;
  float GPA_;
};
struct Student
{
  public:
    char login_[MAXLENGTH];
    int age_;
    int year_;
    float GPA_;
};
You generally won't see the public keyword used with structures.

Finally, we need to get back a way to display the values. Our original display_student no longer can access the private members, so we have to make it part of the Student structure:

Add the display methodModify the implementation
struct Student           
{
  public:
    void set_login(const char* login);
    void set_age(int age);
    void set_year(int year);
    void set_GPA(float GPA);
    void display();

  private:
    char login_[MAXLENGTH];
    int age_;
    int year_;
    float GPA_;
};
void Student::display(void)
{
  using std::cout;
  using std::endl;

  cout << "login: " << login_ << endl;
  cout << "  age: " << age_ << endl;
  cout << " year: " << year_ << endl;
  cout << "  GPA: " << GPA_ << endl;
}
Now, this is how we use it:
void f1()
{
    // Create a Student object
  Student st1;

    // Using the public methods
  st1.set_login("jdoe");
  st1.set_age(22);
  st1.set_year(4);
  st1.set_GPA(3.8);

    // Tell the object to display itself
  st1.display();  
}

Classes

In short, a class is identical to a struct with one (almost) exception: the default accessibility is private.

These will work the same:

Default for struct is publicExplicit public keyword
struct Student
{
  char login_[MAXLENGTH];
  int age_;
  int year_;
  float GPA_;
};
class Student
{
  public:
    char login_[MAXLENGTH];
    int age_;
    int year_;
    float GPA_;
};
And these will work the same:
Explicitly privateDefault for class is private
struct Student
{
  private:
    char login_[MAXLENGTH];
    int age_;
    int year_;
    float GPA_;
};
class Student
{
  char login_[MAXLENGTH];
  int age_;
  int year_;
  float GPA_;
};

We will generally be using the class keyword when creating new types that have methods associated with them. We'll use the struct keyword for POD types. (Plain Old Data types).

If you think a little more in-depth about what a data-type is, you'll see it's more than just the range of values. It is also the operations that can be performed on it. (e.g. you can't use the mod operator, %, with floating point values nor can you add two pointers.)

Initializing Objects: The Constructor

This is the problem we need to solve:
Client codeOutput (random garbage, might crash)
Student s;   // Uninitialized student
s.display(); // ???
login:  PA
  age: 4280352
 year: 4225049
  GPA: 1.89223e-307
We never want to have any objects that are in an undefined state. Ever.

Recall how we initialize structures:

struct Student           
{
    // Public by default
  char login[MAXLENGTH];
  int age;
  int year;
  float GPA;
};
void f()
{
    // Uninitialized Student
  Student st1;

    // Set values by assignment
  std::strcpy(st1.login, "jdoe");
  st1.age = 20;
  st1.year = 3;
  st1.GPA = 3.08;

    // Set values by initialization
  Student john = {"jdoe", 20, 3, 3.10f};
  Student jane = {"jsmith", 19, 2, 3.95f};
}
But with private data, using the initializer list is illegal:
class Student           
{
    // Private by default
  char login[MAXLENGTH];
  int age;
  int year;
  float GPA;
};
void f()
{
    // This is now illegal (accessing private members)
  Student john = {"jdoe", 20, 3, 3.10f};
  Student jane = {"jsmith", 19, 2, 3.95f};
}
You'll get errors like these:

GNU:

error: 'john' must be initialized by constructor, not by '{...}'
error: 'jane' must be initialized by constructor, not by '{...}'
Microsoft:
main1.cpp(116) : error C2552: 'john' : non-aggregates cannot be initialized with initializer list
        'Student' : Types with private or protected data members are not aggregate
main1.cpp(117) : error C2552: 'jane' : non-aggregates cannot be initialized with initializer list
        'Student' : Types with private or protected data members are not aggregate
Clang:
error: non-aggregate type 'Student' cannot be initialized with an initializer list
  Student john = {"jdoe", 20, 3, 3.10};
          ^      ~~~~~~~~~~~~~~~~~~~~~
error: non-aggregate type 'Student' cannot be initialized with an initializer list
  Student jane = {"jsmith", 19, 2, 3.95};
          ^      ~~~~~~~~~~~~~~~~~~~~~~~
The error message from GNU indicates what you need to do: initialize by constructor.

So, we declare another method that will be called to construct (initialize) the object: (notice the order of public and private, the order is arbitrary)

class Student           
{
  public:
      // Constructor (must have the same name as the class)
    Student(const char * login, int age, int year, float GPA);

    void set_login(const char* login);
    void set_age(int age);
    void set_year(int year);
    void set_GPA(float GPA);
    void display();

  private:
    char login_[MAXLENGTH];
    int age_;
    int year_;
    float GPA_;
};
We can easily implement this method by simply calling the other methods:

ImplementationClient can initialize now
Student::Student(const char * login, int age, 
                 int year, float GPA)
{
  set_login(login);
  set_age(age);
  set_year(year);
  set_GPA(GPA);
}
void f()
{
    // Set values by constructor
  Student john("jdoe", 20, 3, 3.10f);
  Student jane("jsmith", 19, 2, 3.95f);
}
Notes:

Accessors and Mutators (Gettors and Settors)

Since the data in a class is usually private, the only way to gain access to it is by providing public methods that explicitly allow it. All of the data in the Student class is write-only, since we can change it, but we can't read it.

Adding accessorsImplementations
struct Student           
{
  public:
      // Constructor
    Student(const char * login, int age, 
            int year, float GPA);

      // Accessors (gettors)
    int get_age();
    int get_year();
    float get_GPA();
    const char *get_login(); 
    
      // Mutators (settors)
    void set_login(const char* login);
    void set_age(int age);
    void set_year(int year);
    void set_GPA(float GPA);

    void display();

  private:
    char login_[MAXLENGTH];
    int age_;
    int year_;
    float GPA_;
};
int Student::get_age()
{
  return age_;
}

int Student::get_year()
{
  return year_;
}

float Student::get_GPA()
{
  return GPA_;
}

const char *Student::get_login()
{
  return login_;
}
Providing (or not providing) accessors and mutators is how you control access and modifications to the private data. What if you didn't want to allow the client to change the login?

Resource Management

The Student class so far: We need to change the login so its length is determined at run-time (read: dynamically). By the way, what is sizeof(struct Student) now?

Change type of login_Implementation change (not 100% correct yet, has 2 bugs!)
struct Student
{
  public:
      // Public interface ...
  private:
    char *login_; 
    int age_;
    int year_;
    float GPA_;
};
void Student::set_login(const char* login)
{
  int len = (int)std::strlen(login);
  login_ = new char[len + 1]; 
  std::strcpy(login_, login);
}

The client doesn't even know there has been a change:

void foo()
{
    // Construct a Student object
  Student john("rumplestiltzkin", 20, 3, 3.10f);

    // This will display all of the data
  john.display();
}
That is the only change required (sort of). What is the problem?





Add interface methodAdd implementation
struct Student
{
  public:
    // Public stuff ...
    void free_login();
  private:
    // Private stuff ...
};
void Student::free_login()
{
  delete [] login_;
}
Now, the client will do this:

void foo()
{
    // Construct a Student object
  Student john("jdoe", 20, 3, 3.10f);

    // This will display all of the data
  john.display();

    // Release the memory for login_
  john.free_login();
}
But this is wrong on so many levels... Here are two of them:

void foo()
{
    // Construct a Student object
  Student john("jdoe", 20, 3, 3.10f);

    // This will display all of the data
  john.display();
  
    // Oops, memory leak now!
}
void foo()
{
    // Construct a Student object
  Student john("jdoe", 20, 3, 3.10f);

    // Release the memory for login_
  john.free_login();
  
    // Oops, very bad now!
  john.display();
}
The Bad News:

Incidentally, if you are going to allow the user to call set_login, then you'll need to modify the function slightly:
Original methodModified (correct) method
void Student::set_login(const char* login)
{
  int len = (int)strlen(login);
  login_ = new char[len + 1]; 
  std::strcpy(login_, login);
}
void Student::set_login(const char* login)
{
    // In case we already had a login
  delete [] login_;

    // Now create a new one
  int len = (int)strlen(login);
  login_ = new char[len + 1]; 
  std::strcpy(login_, login);
}
You will also need to add this line as the first line in the constructor, to ensure that it has been initialized. It is safe to delete a NULL pointer.
login_ = 0;
In order to make sure that the memory is deleted, we need something like a constructor in reverse. Let's call it a destructor.

Destroying Objects: The Destructor

We'd like some code that will be called when the client is done with the object. The code is another method called a destructor and is similar to the constructor.

Add destructorAdd implementation
struct Student           
{
  public:
      // Constructor
    Student(const char * login, int age, 
            int year, float GPA);

      // Destructor
    ~Student();

    // Other public members ...

  private:
    // Private members ...
};
Student::~Student()
{
    // Free the memory that was allocated
  delete [] login_;
}
Now this code is fine:

void foo()
{
    // Construct a Student object
  Student john("jdoe", 20, 3, 3.10f);

    // This will display all of the data
  john.display();
  
} // Destructor is called here.
The destructor will be called automagically when the object goes out of scope. (The meaning of scope here is the same meaning we've been using since the beginning of C.)

void foo()
{
  Student john("jdoe", 20, 3, 3.10f);
  if (john.get_age() > 10)
  {
    Student jane("jsmith", 19, 2, 3.95f);
  } // jane's destructor called

} // john's destructor called
The compiler is smart about calling the destructor for local objects:
void f7()
{
    // Construct a Student object
  Student john("jdoe", 20, 3, 3.10f);
  if (john.get_age() > 10)
  {
    Student jane("jsmith", 19, 2, 3.95f);
    if (jane.get_age() > 2)
      return; // Destructor's for jane
              //   and john called
  }
}
This makes the destructor an extremely powerful concept.

Creating Objects

Let's modify the constructor and destructor to print a message each time they are called:

ConstructorDestructor
Student::Student(const char * login, int age, 
                 int year, float GPA)
{
  login_ = 0;
  set_login(login);
  set_age(age);
  set_year(year);
  set_GPA(GPA);
  std::cout << "Student constructor for " 
            << login_ << std::endl;
}
Student::~Student()
{
  std::cout << "Student destructor for " 
            << login_ << std::endl;
  delete [] login_;
}
Example:

ProgramOutput
void foo()
{
  std::cout << "***** Begin *****\n";
  Student john("jdoe", 20, 3, 3.10f);
  Student jane("jsmith", 19, 2, 3.95f);
  Student jim("jbob", 22, 4, 2.76f);

    // Modify john
  john.set_age(21);
  john.set_GPA(3.25f);

    // Modify jane
  jane.set_age(24);
  jane.set_GPA(4.0f);

    // Modify jim
  jim.set_age(23);
  jim.set_GPA(2.98f);

    // Display all
  john.display(); 
  jane.display();
  jim.display();
  std::cout << "***** End *****\n";
}
***** Begin *****
Student constructor for jdoe
Student constructor for jsmith
Student constructor for jbob
login: jdoe
  age: 21
 year: 3
  GPA: 3.25
login: jsmith
  age: 24
 year: 2
  GPA: 4
login: jbob
  age: 23
 year: 4
  GPA: 2.98
***** End *****
Student destructor for jbob
Student destructor for jsmith
Student destructor for jdoe
These three lines:

Student john("jdoe", 20, 3, 3.10f);
Student jane("jsmith", 19, 2, 3.95f);
Student jim("jbob", 22, 4, 2.76f);
will look something like this in memory: (the addresses are arbitrary as usual)
Notes:

Notice that the methods are not part of the object. This may seem surprising at first. So how does the display method know which data to show?

john.display(); 
jane.display();
jim.display();
// Nowhere does this code reference john, jane, or jim
void Student::display()
{
  using std::cout;
  using std::endl;

    // These members are just offsets. But offsets from what exactly?
  cout << "login: " << login_ << endl;
  cout << "  age: " << age_ << endl;
  cout << " year: " << year_ << endl;
  cout << "  GPA: " << GPA_ << endl;
}


The this Pointer

All methods of a class/struct are passed a hidden parameter. This parameter is the address of the invoking object. In other words, the address of the object that you are calling a method on:
john.display(); // john is the invoking object
jane.display(); // jane is the invoking object
jim.display();  // jim is the invoking object
Really, the display method is more like this (with the items in blue hidden):
void Student::display(Student *this)
{
  using std::cout;
  using std::endl;

    // Members are offset from "this"
  cout << "login: " << this->login_ << endl;
  cout << "  age: " << this->age_ << endl;
  cout << " year: " << this->year_ << endl;
  cout << "  GPA: " << this->GPA_ << endl;
}
The example above would look something like this after compiling:
display(&john); // Address of john object passed to display: display(100)
display(&jane); // Address of jane object passed to display: display(200)
display(&jim);  // Address of jim object passed to display: display(300)
So, in a nutshell, this (no pun intended) is how the magic works. The programmer has access to the this pointer inside of the methods. (this is a keyword.)

Both of these lines are the same within Student::display:

  // Normal code	
cout << "login: " << login_ << endl;

  // Explicit use of this. (Generally only seen in beginner's code.)	
  // But is required in certain more advanced C++ code.	
cout << "login: " << this->login_ << endl;

const Member Functions

Example:
void foo()
{
    // Create a constant object 
  const Student john("jdoe", 20, 3, 3.10f);

  john.set_age(25); // Error, as expected.
  john.set_year(3); // Error, as expected.

  john.get_age();   // Error, not expected.
  john.display();   // Error, not expected.
}
In the "old days", we would make our parameters const if we were not going to modify them:
void display_student(const Student &student)
{
  using std::cout;
  using std::endl;
  cout << "login: " << student.login << endl;
  cout << "  age: " << student.age << endl;
  cout << " year: " << student.year << endl;
  cout << "  GPA: " << student.GPA << endl;
}
void foo()
{
    // Create constant object
  const Student john = {"jdoe", 20, 3, 3.10f};

    // This works just fine with const object
  display_student(john);
}
Q: How do we accomplish the same thing with member functions (methods) when we don't pass the data as a parameter?
A: We mark the method as const.

You must tag both the declaration (in the class definition) and implementation with the const keyword:

struct Student           
{
  public:
      // Constructor
    Student(const char * login, int age, 
            int year, float GPA);

      // Destructor
    ~Student();

      // Mutators (settors) are non-const
    void set_login(const char* login);
    void set_age(int age);
    void set_year(int year);
    void set_GPA(float GPA);

      // Accessor (gettors) are const
    int get_age() const;
    int get_year() const;
    float get_GPA() const;
    const char *get_login() const;

      // Nothing will be modified 
    void display() const;

  private:
    char *login_; 
    int age_;
    int year_;
    float GPA_;
};
void Student::display() const
{
  using std::cout;
  using std::endl;

  cout << "login: " << login_ << endl;
  cout << "  age: " << age_ << endl;
  cout << " year: " << year_ << endl;
  cout << "  GPA: " << GPA_ << endl;
}

int Student::get_age() const
{
  return age_;
}

int Student::get_year() const
{
  return year_;
}

float Student::get_GPA() const
{
  return GPA_;
}

const char *Student::get_login() const
{
  return login_;
}

Now, this works as expected:

void foo()
{
    // Create a constant object 
  const Student john("jdoe", 20, 3, 3.10f);

  john.set_age(25); // Error, as expected.
  john.set_year(3); // Error, as expected.

  john.get_age();   // Ok, as expected.
  john.display();   // Ok, as expected.
}

Note: If a method does not modify the private data members, it should be marked as const. This will save you lots of time and headaches in the future. Unfortunately, the compiler won't remind you to do this until you try and use the method on a constant object.

Separating the Interface from the Implementation

Typically, each class will reside in its own file. In fact, most classes will generally be in two files: Here's our simple project split into three files:
  1. The header file, Student.h
  2. The implementation file, Student.cpp
  3. The client code, main.cpp
We could then build project something like this:
g++ -o project main.cpp Student.cpp -Wall -Wextra -ansi -pedantic

Default Constructors and Destructors

Recall one of the reasons for a constructor:
"To ensure that an object's data is not left undefined."
Recall one of the reasons for a destructor:
"To ensure that any resources (e.g. memory) acquired are released."

First, constructors:

Here's an example:
struct Point
{
  double x;
  double y;
};
void foo()
{
    // Create a Point object
    // x/y are uninitialized
  Point pt1;

    // Display random values for x/y
  std::cout << pt1.x << "," << pt1.y << std::endl;
}

Output: (random)
1.89121e-307,1.89121e-307
Of course, the client could have initialized the data, but you can't count on that. The solution is to create a default constructor. A default constructor is simply a constructor that can be called without any arguments.

Add default constructorImplement the default constructor
struct Point
{
    // Default constructor
  Point();

  double x;
  double y;
};
Point::Point()
{
    // Give default values
  x = 0;
  y = 0;
}
Now, this object will be defined:

  // Create a Point object
  // x/y are defined now
Point pt1;

  // Display values for x/y
std::cout << pt1.x << "," << pt1.y << std::endl;

Output: (defined values)
0,0
It is not uncommon to provide other constructors in addition to a default constructor. (The example below uses the class keyword instead of struct just to demonstrate the technique works for both.)

Multiple constructorsImplementations
class Point
{
  public:
      // Default constructor
    Point();

      // Non-default constructor
    Point(double x, double y);

      // For convenience
    void display() const;

  private:
    double x_;
    double y_;
};
Point::Point()
{
    // Give default values
  x_ = 0.0;
  y_ = 0.0;
}

Point::Point(double x, double y)
{
    // Give values from params
  x_ = x;
  y_ = y;
}

void Point::display() const
{
    // Display random values for x/y
  std::cout << x_ << "," << y_ << std::endl;
}
Now the user can construct "default" objects or specify the values:

  // Both are accepted
Point pt1; 
Point pt2(3.5, 7);

pt1.display();  // 0,0
pt2.display();  // 3.5,7
Note that we can combine the default constructor into a non-default constructor by using default arguments:

class Point
{
  public:
      // Default constructor
    Point(double x = 0.0, double y = 0.0);

    // Other stuff ...

};
Point::Point(double x, double y)
{
    // Use params to set values
  x_ = x;
  y_ = y;
}
Also, realize that this is now ambiguous:

  // Two default constructors (illegal)	
Point();
Point(double x = 0.0, double y = 0.0);

Point pt1; // Which one?
Notes:
  • The only way to construct an object from a class/struct is with a constructor (either default or non-default).
  • If you don't provide a constructor, the compiler will provide a default constructor for you.
  • Here's what we get from the compiler (sort of) if we don't provide any constructors for the Point class:
    Point::Point()
    {
    }
    
  • Unfortunately, this compiler-provided default constructor doesn't do much.
  • The compiler will provide a default constructor ONLY if you don't provide any constructors at all.
  • Put another way, if you provide ANY constructors (default or otherwise) the compiler WILL NOT provide a default for you. Adding a default constructor to the Student class:

    struct Student           
    {
      public:
          // Default constructor
        Student();
    
          // Constructor
        Student(const char * login, int age, 
                int year, double GPA);
    
          // Other public stuff ...
    
      private:
        char *login_; 
        int age_;
        int year_;
        double GPA_;
    };
    
    // Default constructor (not really a good idea here)
    Student::Student()
    {
      login_ = 0;
      set_login("Noname");
      set_age(18);
      set_year(1);
      set_GPA(0.0);
    }
    
    // Constructor
    Student::Student(const char * login, int age, 
                     int year, double GPA)
    {
      login_ = 0;
      set_login(login);
      set_age(age);
      set_year(year);
      set_GPA(GPA);
    }
    
    Of course, it's up to you (the class implementor) to decide if something has a "sane" default or must be provided by the user. A Student class really shouldn't provide a default as there are no "sane" defaults.


    Recall one of the reasons for a destructor:

    "To ensure that any resources (memory) created are released."

    Just like constructors, the compiler will provide a destructor for us if we fail to do so. Here's what the compiler will generate (sort of) if we don't provide a destructor for the Point class or the Student class:

    Point::~Point()
    {
    }
    
    Student::~Student()
    {
    }
    

    Example using constructors, destructors, static allocation, and dynamic allocation:
    class Point
    {
      public:
          // Default constructor
        Point(double x = 0.0, double y = 0.0);
    
          // Destructor
        ~Point();
    
          // For convenience
        void display() const;
    
      private:
        double x_;
        double y_;
    };
    
    Point::~Point()
    {
      std::cout << "Point destructor: "
                << x_ << "," << y_
                << std::endl;
    }
    
    Point::Point(double x, double y)
    {
        // Assign from the parameters
      x_ = x;
      y_ = y;
      std::cout << "Point constructor: "
                << x_ << "," << y_
                << std::endl;
    }
    
    ProgramOutput
    void foo()
    {
        // Static allocation
      Point pt1;       // 0,0
      Point pt2(4);    // 4,0
      Point pt3(4, 5); // 4,5
    
        // Similar using dynamic allocation
      Point *pt4 = new Point;
      Point *pt5 = new Point(8);
      Point *pt6 = new Point(7, 9);
    
        // Must delete manually (calls destructor)
      delete pt4;
      delete pt5;
      delete pt6;
    
    }  // Destructors for pt1,pt2,pt3 called here
    
    Point constructor: 0,0
    Point constructor: 4,0
    Point constructor: 4,5
    Point constructor: 0,0
    Point constructor: 8,0
    Point constructor: 7,9
    Point destructor: 0,0
    Point destructor: 8,0
    Point destructor: 7,9
    Point destructor: 4,5
    Point destructor: 4,0
    Point destructor: 0,0
    

    Arrays of Objects

    Just like any other type, you can create arrays of objects.

    Built-in types:

      // Compiler initializers elements that you don't
    int a[3] = {1, 2, 3}; // 1, 2, 3
    int b[3] = {1, 2};    // 1, 2, 0
    int c[3] = {0}        // 0, 0, 0
    double d[3] = {1.0}   // 1.0, 0.0, 0.0
    
    User-defined types:

      // Requires default constructor
    Student st1[2];
    for (int i = 0; i < 2; i++)
      st1[i].display();
    
    login: Noname
      age: 18
     year: 1
      GPA: 0
    login: Noname
      age: 18
     year: 1
      GPA: 0
    
      // Requires default constructor
    Student st2[3] = {
                        Student("jdoe", 20, 3, 3.10F), 
                        Student("jsmith", 19, 2, 3.95F)
                      };
    for (int i = 0; i < 3; i++)
      st2[i].display();
    
    login: jdoe
      age: 20
     year: 3
      GPA: 3.1
    login: jsmith
      age: 19
     year: 2
      GPA: 3.95
    login: Noname
      age: 18
     year: 1
      GPA: 0
    
      // No default constructor required
    Student st3[2] = {
                        Student("jdoe", 20, 3, 3.10F), 
                        Student("jsmith", 19, 2, 3.95F)
                      };
    for (int i = 0; i < 2; i++)
      st3[i].display();
    
    login: jdoe
      age: 20
     year: 3
      GPA: 3.1
    login: jsmith
      age: 19
     year: 2
      GPA: 3.95
    
    If the Student class does not have a default constructor, then the first two examples above would cause a compiler error.


    Understanding the Big Picture™

    What problems are solved by

    1. encapsulation?
    2. access specifiers? (e.g. private)
    3. constructors?
    4. default constructors?
    5. destructors?
    6. const member functions?