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 (structs) and objects (with access specifiers)
  2. Extending classes with an is-a or is-a-kind-of relationship
  3. Virtual methods and dynamic (runtime) binding

Procedural Programming

Let's use this structure that represents a student: (What is sizeof(Student))?

Also, notice MAXLENGTH is not a #define:

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:and 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
as well as this:
void f1()
{
  Student st3;

  display_student(st3);
}

Output:
login: |
  age: 0
 year: 4198736
  GPA: 2.07362e-317
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)
{
  std::strncpy(student.login, login, 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
// This code is in a header (.h) file   
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:

// This code is in a .cpp file   

void Student::set_login(const char* login)
{
  std::strncpy(login_, login, 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()
{
  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 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 use the + operator with 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 in C:

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 directly)
  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:
error C2552: 'john' : non-aggregates cannot be initialized with initializer list
  'Student' : Types with private or protected data members are not aggregate
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 at least 3 potential 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];  // Don't forget the NUL character!
  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?

How about this?

john.set_login("johnny");
john.set_login("jj");
john.set_login("doofus");





Add interface methodAdd implementation
struct Student
{
  public:
    // Public stuff ...
    void free_login();
  private:
    // Private stuff ...
};
void Student::free_login()
{
  delete [] login_; // It's an array, requires []
}
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();
}
Why this is Bad News:

Incidentally, if you are going to allow the user to call set_login (maybe repeatedly), then you'll need to modify the function slightly:
Original methodModified method
(correct, but there are still problems)
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)
{
    // Free the "old" 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 destructor (declaration)Add implementation
struct Student           
{
  public:
      // Constructor
    Student(const char * login, int age, int year, float GPA);

      // Destructor, same name as constructor, but with a ~ in
      // front. Absolutely no inputs, no return (must match
      // this signature exactly).
    ~Student();

    // Other public members as before ...

  private:
    // Private members as before ...
};
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.

Note: The ~ operator is called the complement operator (or one's complement operator or bitwise complement operator). It's used to name the destructor because the destructor is the complement of the constructor.

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. This is also known as deterministic destruction because it is guaranteed to work this way every time and is one of the most powerful features of C++.

What have we accomplished so far?:
  1. It's impossible* for the user to mess up the data.
  2. It's impossible* to have uninitialized data.
  3. It's impossible* to have a memory leak.
*It's not impossible as there are malicious ways a programmer can still mess things up. But, we can't really stop them if they intentionally want to screw things up.

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 is the first parameter that is passed.) 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;

Languages such as C++, Java, C#, D, JavaScript use the keyword this. Other languages such as Pascal, Rust, and Lua use the keyword self. Visual Basic uses the keyword Me.

const Member Functions

In the "olden days", we would make our parameters const if we were not going to modify them in the global functions. Assume that all data is public in the Student structure:
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;

    // Try to modify the constant object.
    // This would lead to a compiler error.
  student.age = 100; 
}
void foo()
{
    // Create constant object
  const Student john = {"jdoe", 20, 3, 3.10f};

    // This works just fine with const object
  display_student(john);
}
Question: How do we accomplish the same thing with private data and public member functions (methods) when we don't pass the data as a parameter?

There is no "parameter" to mark as const:

void Student::display()
{
  using std::cout;
  using std::endl;

  cout << "login: " << login_ << endl;
  cout << "  age: " << age_ << endl;
  cout << " year: " << year_ << endl;
  cout << "  GPA: " << GPA_ << endl;
}
Remember, if you marked a parameter using the const keyword, you were promising/advertising that you will not change it. Otherwise, you were promising that you were going to change it.

How does the user know what the display method is going to do?

void Student::display(); // Prototype. Will this change anything? How do we know?
Unless you specify the behavior, ALL member functions (methods) are assumed to change the data of the object:
void foo()
{
    // Create a constant object (can't change any data later) 
  const Student john("jdoe", 20, 3, 3.10f);

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

  john.get_age();   // Error reading age_, NOT expected.
  john.display();   // Error printing data, NOT expected.
}

Answer: 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/writing) 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/reading) 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.

This is why you are always told to "Use const where appropriate."

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 (Notice that there are no #includes)
  2. The implementation file, Student.cpp (There are #includes here)
  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

Recall one of the reasons for a constructor:
"To ensure that an object's data is not left undefined."

This means that to construct an object, a class absolutely, positively must have a constructor. If there is no constructor, you cannot create an object (instance) from the class. This neatly solves the first problem above.

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. (Remember, you don't want to count on programmers to do the right thing!) Even though you can't see it, there is a constructor that is present so that we can construct the (apparently undefined) object.

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
Be careful not to do this:
  // This does NOT call the default constructor!
Point pt();
The above is a declaration/prototype for a function named pt that takes no parameters (void) and returns a Point. This is described by Scott Meyers as C++'s "Most vexing parse" in his book Effective STL. BTW, Scott Meyers is one of the most knowledgeable C++ programmers on the planet!
It is not uncommon to provide other constructors (overloaded) 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 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: 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. (Unlike the Point class which does have sane defaults: 0.0)

To be clear: A default constructor is a constructor that does not require the programmer to provide any parameters. It does not mean that the constructor doesn't take any parameters. See the Point (default) constructor above.

Default Destructors

Recall one of the reasons for a destructor:
"To ensure that any resources (e.g. memory) acquired 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 pt3,pt2,pt1 called here (notice the ordering)
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

Note: Unlike constructors, you can't overload destructors. There is exactly one destructor for every class and it doesn't take parameters and doesn't return anything. The reason is simple: you are not calling it, the compiler is, so you don't have a chance to make a choice on which one to call.

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
int d[3] = {}         // 0, 0, 0 (This is an error in C)
double e[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. This is something that you must understand completely.


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?