3  MORE ABOUT CLASSES

Object Life Cycle

3.1 Constructors

Definition

A constructor is a special member function that is automatically called when a class object is created.

Constructor,

  • It is automatically invoked when a new object is created.
  • There is no returned value, even a void.
  • A class can have more than one constructor (overload)
  • Name of the constructors must be the same as the class name.

Remember that when we define a local variable (primary data type) in C++, the memory is not automatically initialized.

Declaring and Implementing a Constructor

classDiagram
   class Human {
      -string Name
      -string DateOfBirth
      -string PlaceOfBirth
      -string Gender
      +Human()
   }

class Human {
private:
   string Name;
   string DateOfBirth;
   string PlaceOfBirth;
   string Gender;
public:
    Human() {
        // constructor code here
        Name = "Le Thi Dep";
        DateOfBirth = "01/01/1990";
        PlaceOfBirth = "Ha noi";
        Gender = "female";
    }
    ...
};

When and How to Use Constructors

  • A constructor is a perfect place for us to initialize class member variables such as integers, pointers, and so on to known initial values

classDiagram
   class Human {
      -string Name
      -int Age
      +Human()
      +void SetName(string Name)
      +void SetAge(int Age)
      +void IntroduceSelf()
   }

#include <iostream>
#include <string>
using namespace std;
class Human {
private:
   // Private member data:
   string Name;
   int Age;
public:
   // constructor
   Human() {
      Age = 0; 
   }
   void SetName(string HumansName) {
      Name = HumansName;
   }
   void SetAge(int HumansAge) {
      Age = HumansAge;
   }
   void IntroduceSelf() {
      cout << "I am " + Name << " and am ";
      cout << Age << " years old" << endl;
   }
};
int main() {
   Human FirstMan;
   FirstMan.SetName("Adam");
   FirstMan.SetAge(30);
   Human FirstWoman;
   FirstWoman.SetName("Eve");
   FirstWoman.SetAge (28);
   FirstMan.IntroduceSelf();
   FirstWoman.IntroduceSelf();
}

Overloading Constructors

  • Constructor with no parameters
  • Constructor with parameter(s)
  • Constructor with default parameter(s)
  • Default constructor (if no constructor is implemented, the compiler will issue a default constructor; if there is at least one constructor, the default constructor will not be created by the compiler)
    • No parameters
    • Invoke other default constructors of data members if they are objects.
    • Doesn’t initialize other data members if they are not objects.
  • Copy constructor

Example

  • As constructors can be overloaded just like functions, we can create a constructor that requires Human to be created with a name as a parameter, for instance:

classDiagram
   class Human {
      +Human()
      +Human(string HumansName)
   }

class Human {
public:
   Human() {
       // own default constructor code here
   }
   Human(string HumansName) {
       // overloaded constructor code here
   }
};
...

int main() {
   ...
   Human firstMan;
   Human firstWoman("Eve");
   ...
}

Class Without a Default Constructor

class CDate
{
public:
    CDate(int day, int month, int year);
    ...
private:
    ...
};

int main() {
    CDate today; // compile error
    ...
    return 0;
}

Best practice: always define our own default constructor!

this Pointer

this is a reserved keyword applicable within the scope of a class that contains the address of the object. In other words, the value of this is &object

  • this is a constant pointer, we cannot modify it within a member function.
class Human {
    ...
    void SetAge(int HumansAge) {
       this->Age = HumansAge;   // same as Age = HumansAge
    }
    ...
}

sizeof() a Class

The operator sizeof() is valid for classes and basically reports the sum of bytes consumed by each data attribute contained within the class declaration. Depending on the compiler we use, sizeof() might or might not include padding for certain attributes on word boundaries.

int main() {
    Human Tom;
    cout << "sizeof(Human) = " << sizeof(Human) << endl;
    cout << "sizeof(Tom) = " << sizeof(Tom) << endl;
}

3.2 Destructor

Definition

A destructor is a member function that is automatically called when an object is destroyed.

  • Destructor is always invoked when an object of a class goes out of scope or is deleted via delete and is destroyed
  • Each class can have at most one destructor
  • The destructor name is the name of a class preceded by a tilde sign (~)
  • Destructor has no return type (even void)
  • Destructor frees the resources used by the object (allocated memory, file descriptors, semaphores etc.)

Example

  • Destructor for Human class

classDiagram
   class Human {
      + ~Human()
      + «destroy» destroy()
   }
   note for Human "C++ style: ~Human()<br/>UML style: «destroy» destroy()"

class Human {
public:
    ~Human() {
        // destructor code here
    }
};

When and How to Use Destructors

  • Destructor is the ideal place to reset variables and release dynamically allocated memory and other resources.

classDiagram
   class MyString {
      -char* Buffer
      +MyString(const char* InitialInput)
      +~MyString()
      +int GetLength() 
      +const char* GetString() 
   }

#include <iostream>
using namespace std;

class MyString {
private:
   char* Buffer;
public:
   // Constructor
   MyString(const char* InitialInput) {
      if(InitialInput != NULL) {
         Buffer = new char [strlen(InitialInput) + 1];
         strcpy(Buffer, InitialInput);
      }
      else
         Buffer = NULL;
   }
   // Destructor: clears the buffer allocated in constructor
   ~MyString() {
      cout << "Invoking destructor, clearing up" << endl;
      if (Buffer != NULL)
         delete [] Buffer;
   }
   int GetLength() {
      return strlen(Buffer);
   }
   const char* GetString() {
    return Buffer;
   }
};
int main() {
   MyString SayHello("Hello from String Class");
   cout << "String buffer in MyString is " << SayHello.GetLength();
   cout << " characters long" << endl;
   cout << "Buffer contains: ";
   cout << "Buffer contains: " << SayHello.GetString() << endl;
}

3.3 Member Initialization

Initialization vs. Assignment

  • Distinguish between Assignment and Initialization
Initialization Assignment
int a = 2; a = 3;
double b(4.0); a = 4;
b = 2.0;
b = 1.0;

Members Initialization

Member initialization is the process of initializing class member variables directly using a member initializer list before the constructor body executes.

Syntax

⟨Class Name⟩(⟨Parameter List⟩) : ⟨Member1⟩(⟨Expression1⟩), ⟨Member2⟩(⟨Expression2⟩), ... {
   // constructor body
}
  • This is members initialization
class CDate {
private:
   int m_iDay, m_iMonth, m_iYear;
public:
   CDate() 
   {}
   CDate(int day, int month, int year) : m_iDay(day), m_iMonth(month), m_iYear(year)
   {}
   ~CDate() 
   {}
   ...
};

Mandatory Members Initialization

  • References
  • Pointers
  • Const members
  • Sub-objects which require arguments in constructors
class Human {
private:
   Ancestor&   ref; // reference member
   Descendant* ptr; // pointer member
   const int   MAX; // const member
   vector      arr; // object member
public:
   Human(Ancestor& r, Descendant *p) : ref(r), ptr(p), MAX(100), arr(MAX) 
   {}
};

3.4 Copy Constructor

Definition

A copy constructor is a special constructor that is called whenever a new object is created and initialized with another object’s data.

  • Default copy constructor: if there is no copy constructor, a default copy constructor will be generated. Default copy constructor performs a bitwise copy from the source to the current object (shallow copy).
  • Our own copy constructor

When copies of objects are made

  • A variable is declared which is initialized from another object

classDiagram
   class Person {
      +Person(string Name)
      +Person(const Person& Other)
      +Person& operator=(const Person& Other)
   }

class Person {
private:
   string Name;
public:
   Person(string name) : Name(name) {
      // constructor
   }
   Person(const Person& Other) : Name(Other.Name) {
      // copy constructor
   }
   Person& operator=(const Person& Other) {
      // assignment operator
      if (this != &Other) {
         Name = Other.Name;
      }
      return *this;
   }
};
...
Person p("Mickey"); // constructor
Person r(p);        // copy constructor
Person q = p;       // copy constructor
p = r;              // assignment operator
  • A value parameter is initialized from its corresponding argument.

    void doSomething(Person p) {
       ... 
    }
    ...
    doSomething(p);     // copy constructor
  • An object is returned by a function.

    Person whoDo() {
       Person p;
       ...
       return p;           // copy constructor
    }

Shallow Copying and Associated Problems

  • When an object of the class MyString is copied, the pointer member is copied, but not the pointed buffer, resulting in two objects pointing to the same dynamically allocated buffer in memory.
void UseMyString(MyString Input) {
    cout << "String buffer in MyString is " <<Input.GetLength();
    cout << " characters long" << endl;
    cout << "Buffer contains: " << Input.GetString() << endl;
}
int main() {
    MyString SayHello("Hello from String Class");
    // Pass SayHello as a parameter to the function
    UseMyString(SayHello);
    return 0;
}

Deep Copy Using a Copy Constructor

  • C++ requires that a copy constructor’s parameter be a reference object.

classDiagram
    class MyString {
        -char* Buffer
        +MyString(const MyString& CopySource)
    }

class MyString {
    ...
    // Copy constructor
    MyString(const MyString& CopySource) {
        if(CopySource.Buffer != NULL) {
            // ensure deep copy by first allocating own buffer
            Buffer = new char [strlen(CopySource.Buffer) + 1];
            // copy from the source into local buffer
            strcpy(Buffer, CopySource.Buffer);
        }
        else
            Buffer = NULL;       
    }
    ...
}

3.5 Copy Assignment Operator

Definition

A copy assignment operator is a special member function that is called when an already initialized object is assigned the value of another existing object of the same class type.

  • Syntax: ClassName& operator=(const ClassName& other)
  • Self-Assignment Check: Must always check for self-assignment (this != &other) to prevent deleting resources that are about to be copied.
  • Default Copy Assignment: If not defined, the compiler automatically generates a default version that performs a member-by-member shallow copy.

Deep Copy Using Copy Assignment Operator

classDiagram
    class MyString {
        -char* Buffer
        +MyString& operator=(const MyString& CopySource)
    }

class MyString {
    ...
    // Copy assignment operator
    MyString& operator= (const MyString& CopySource) {
        // 1. Self-assignment check
        if (this != &CopySource) {
            // 2. Release existing resources
            if (Buffer != NULL)
                delete[] Buffer;
            
            // 3. Perform deep copy
            if (CopySource.Buffer != NULL) {
                Buffer = new char[strlen(CopySource.Buffer) + 1];
                strcpy(Buffer, CopySource.Buffer);
            }
            else {
                Buffer = NULL;
            }
        }
        // 4. Return *this to support chained assignments (e.g., a = b = c)
        return *this;
    }
    ...
};

3.6 The Rule of Three

Definition

The “Rule of Three” is a C++ design principle stating that if a class requires a user-defined implementation of any of the following three special member functions, it almost certainly requires user-defined implementations of all three of them:

  1. Destructor (~ClassName())
  2. Copy Constructor (ClassName(const ClassName& source))
  3. Copy Assignment Operator (ClassName& operator=(const ClassName& rhs))

3.7 Unnamed Object

Overview

An Unnamed object (anonymous object) is essentially an object that has no name

classDiagram
   class Wallet {
      -int cents
      +Wallet(int cents)
      +int getCents()
   }

class Wallet {
private:
    int cents;
public:
    Wallet(int cents): cents( cents ) {}
    int getCents() { return cents; }
};
int main() {
    Wallet momo(6); // normal object
    Wallet(8);      // unnamed object
}
  • Because they have no name, there’s no way to refer to them beyond the point where they are created.
  • Consequently, they have “expression scope”, meaning they are created, evaluated, and destroyed all within a single expression.
  • Returned object of a function is also considered as an unnamed object.
Wallet createWallet(int cents) {
    return Wallet(cents);
}
...
cout << createWallet(5).getCents() << endl;

3.8 Static Members

Instance and Static Members

Each instance of a class has its own copies of the class’s instance variables.

  • If a member variable is declared static, however, all instances of that class have access to that variable.
  • If a member function is declared static, it may be called without any instances of the class being defined.

Static Member Variables

Syntax

static ⟨Data Type⟩ ⟨Variable Name⟩;
  • Even though static member variables are declared in a class, they are actually defined outside the class declaration. The lifetime of a class’s static member variable is the lifetime of the program. This means that a class’s static member variables come into existence before any instances of the class are created.

UML

classDiagram
   class Name {
      attribute1
      attribute2$
      operation1()
      operation2()$
   }
   note for Name "underline means static"

Example

classDiagram
   class Tree {
      -int objectCount$
      +Tree()
      +int getObjectCount() const
   }

#include <iostream>
using namespace std;
class Tree {
private:
   static int objectCount;    // Static member variable.
public:
   // Constructor
   Tree() { objectCount++; }   
   // Accessor function for objectCount
   int getObjectCount() const { return objectCount; }
};
// Definition written outside the class.
int Tree::objectCount = 0;
int main() {
   Tree oak, elm, pine;   
   cout << "We have " << pine.getObjectCount()
        << " trees in our program!\n";
   return 0;
}

Static Member Functions

Syntax

static ⟨Return Type⟩ ⟨Function Name⟩ (⟨Parameter Type List⟩);
  • A function that is a static member of a class cannot access any nonstatic member data in its class.
  • A class’s static member functions can be called before any instances of the class are created. This means that a class’s static member functions can access the class’s static member variables before any instances of the class are defined in memory. This gives you the ability to create very specialized setup routines for class objects.

Example

classDiagram
   class Tree {
      -int objectCount$
      +Tree()
      +int getObjectCount()$
   }

#include <iostream>
using namespace std;
class Tree {
private:
   static int objectCount;    // Static member variable.
public:
   // Constructor
   Tree() { objectCount++; }   
   // Static function for objectCount
   static int getObjectCount() { return objectCount; }
};
// Definition written outside the class.
int Tree::objectCount = 0;
int main() {
   Tree oak, elm, pine;   
   cout << "We have " << Tree::getObjectCount()
        << " trees in our program!\n";
   return 0;
}

3.9 Friends of Classes

Definition

A friend is a function or class that is not a member of a class, but has access to the private members of the class

  • Friend functions or classes give a flexibility to the class.
  • It doesn’t violate the encapsulation of the class.
  • Friendship is “directional”. It means if class A considers class B as its friend, it doesn’t mean that class B considers A as a friend

Syntax

class ⟨Class Name⟩ {
   ...
   friend ⟨Return Type⟩ ⟨Function Name⟩ (⟨Parameter Type List⟩);
   friend class ⟨Other Class Name⟩;
   ...
}

UML

classDiagram
   Container <.. Iterator : «permit»

Example

classDiagram
   class Human {
      -string Name
      -int Age
      +Human(string InputName, int InputAge)
      +friend void DisplayAge(const Human& Person)
   }

class Human {
private:
   string Name;
   int Age;
   friend void DisplayAge(const Human& Person);
public:
   Human(string InputName, int InputAge) {
      Name = InputName;
      Age = InputAge;
   }
};
void DisplayAge(const Human& Person) {
   cout << Person.Age << endl;
}
int main() {
   Human FirstMan("Adam", 25);
   DisplayAge(FirstMan);
   return 0;
}

3.10 Workshop

✒ Quiz

  1. What is the this pointer?

  2. When I create an instance of a class using new, where is the class created?

  3. My class has a raw pointer int * that contains a dynamically allocated array of integers. Does sizeof report different sizes depending on the number of integers in the dynamic array?

  4. All my class members are private, and my class does not contain any declared friend class or function. Who can access these members?

  5. Can one class member method invoke another?

  6. What is a constructor good for?

  7. What is a destructor good for?

💻 Exercises

  • Programming Challenges of chapter 13

    3. Car Class

    5. RetailItem Class

  • Programming Challenges of chapter 14

    1. Numbers Class

    2. Day of the Year

3.11 References