Constructor, Destructor, Operator and Function Overloading

Class Instantiation

Classes are like blueprints, while instances are like the buildings constructed from those blueprints. Similarly, a CAD design is like a class, and the manufactured items are its instances. Instantiation is the process of creating an instance of a class.

Instances are allocated memory to store specific information, and you can have multiple identical instances of the same class type.

Constructors and Destructors

For each class instance:

  • Constructor: A special function that is called when an object is created to set up necessary initializations.

  • Destructor: A special function that is called when an object is destroyed (freed) to clean up resources.

List ADT Design

Using struct:

struct List { ... };
void Initialize(List* list);
void Destroy(List* list);
int Find(const List& list, int key);
bool Insert(List* list, int key, int pos);
bool Delete(List* list, int key);
List Concat(const List& l1, const List& l2);

Using class:

class List {
 public:
  void Initialize();
  void Destroy();
  int Find(int key) const;
  bool Insert(int key, int pos);
  bool Delete(int key);
  static List Concat(const List& l1, const List& l2);
};

The class implementation encapsulates the operations, preventing accidental omission of Initialize() or Destroy() calls.

Class Constructor

Constructors are special member functions used to initialize objects.

  • They have the same name as the class and no return type.

  • They can take different arguments, allowing for various initialization methods.

  • Use the syntax : field(value), ... to initialize member variables.

Example:

class Student {
 public:
  Student()
   : name_(), grade_(), id_(0), midterm_(0),
   final_(0), hw1_(0), hw2_(0) {}
  Student(const string& name, int id)
   : name_(name), id_(id) {
   midterm_ = final_ = hw1_ = hw2_ = 0;
  }
  void SetInfo(const string& name, int id) {
   name_ = name, id_ = id;
  }
  const string& grade() const { return grade_; }
 private:
  std::string name_, grade_;
  int id_, midterm_, final_, hw1_, hw2_;
};

Class Destructor

The destructor is a special cleanup member function called when the object is destroyed.

  • Its name is ~ + the class name.

  • It takes no arguments and has no return type.

Example:

class Student {
 public:
  ~Student() {} // Nothing to do
};

Constructor / Destructor Example: DoubleArray

class DoubleArray {
 public:
  DoubleArray() : ptr_(NULL), size_(0) {}
  DoubleArray(size_t size)
   : ptr_(NULL), size_(0) { Resize(size); }
  ~DoubleArray() { delete[] ptr_; }
  void Resize(size_t size);
  int size() const { return size_; }
  double* ptr() { return ptr_; }
  const double* ptr() const { return ptr_; }
 private:
  double* ptr_;
  size_t size_; // size_t is unsigned int.
};

void DoubleArray::Resize(size_t size) {
 double* new_ptr = new double[size];
 if (ptr_) {
  for (int i = 0; i < size_ && i < size; ++i) {
  new_ptr[i] = ptr_[i];
   }
  delete[] ptr_;
 }
 ptr_ = new_ptr;
 size_ = size;
}

C/C++ Scope Example

void TestScope(int n) {
 assert(n == 10);
 for (int i = 0; i < n; ++i) {
  int n = 20;
  for (int j = 0; j < n; ++j) {
  int n = 30;
  assert(n == 30);
  }
  assert(n == 20);
 }
 assert(n == 10);
}
int main() {
 TestScope(10);
 return 0;
}

Local variables shadow variables in outer scopes. When a local variable goes out of scope, the variable in the enclosing scope becomes accessible again.

Scope and Constructor / Destructor

struct TestClass {
 int n;
 TestClass(int i) : n(i) {
  cout << "Constructor " << n << endl;
 }
 ~TestClass() {
  cout << "Destructor " << n << endl;
 }
};
void TestClassScope(int n) {
 TestClass c1(n);
 for (int i = 0; i < n; ++i) {
  TestClass c2(i);
 }
}
int main() {
 TestClassScope(3);
 return 0;
}
  • Constructors are called when an object comes into scope.

  • Destructors are called when an object goes out of scope (in reverse order of construction).

Copy Constructor

A copy constructor initializes an object using another instance of the same class.

ClassName(const ClassName& obj);

Example:

class Point {
public:
 double x, y;
 Point(double x, double y) : x(x), y(y) {}
 // Copy constructor, most popular form
 Point(const Point& p) : x(p.x), y(p.y) {}
 // You can also use this form, but...
 Point(Point& p) { x = p.x; y = p.y; }
 // Compile error (hint: recursion)
 //Point(Point p) { x = p.x; y = p.y; }
};
int main(int argc, char* argv[]) {
 Point p2(0.1, 0.2);
 Point p3 = p2;
 Point p4(p2);
 return 0;
}

When is the Copy Constructor Called?

  1. When an object is returned by value.

  2. When an object is passed to a function by value as an argument.

  3. When an object is constructed based on another object of the same class.

Example:

class Point {
public:
 double x, y;
 Point(double x, double y) : x(x), y(y) {}
 Point(const Point& p) : x(p.x), y(p.y) {}
 // ^ Copy constructor
};
Point GetScaledPoint(double scale, Point p) {
 Point p_new;
 p_new.x = p.x * scale, p_new.y = p.y * scale;
 return p_new; // 1.
}
int main(int argc, char* argv[]) {
 Point p1(0.1, 0.2);
 Point p2 = GetScaledPoint(2.0, p1);
 // ^ 3. ^ 2.
 Point p3 = p1, p4(p2); // 3.
 return 0;
}

Default Copy Constructor

A default copy constructor is implicitly created by the compiler if no user-defined copy constructor exists.

  • It performs a member-wise copy between objects, where each member is copied by its own copy constructor.

  • This works well in general, but not in some cases.

Default Constructor

Note: A default constructor is implicitly created by the compiler only if there is no user-defined constructor.

If you define a copy constructor, the compiler does not create the default constructor and default copy constructor.

Caution: Default Copy Constructor and Pointers

Be careful when dealing with pointers!

  • The default copy constructor only copies the pointer values, not the allocated memory.

  • This can lead to issues like double deletion.

Example:

class MyString {
private:
 char* str_;
 int len_;
public:
 MyString(const char* s = "") {
  len_ = strlen(s);
  str_ = new char[len_ + 1];
  strcpy(str_, s);
 }
 // No copy constructor defined
 ~MyString() { delete[] str_; }
 void Print() { cout << str_ << endl; }
};
int main(int argc, char* argv[]) {
 MyString s1("hello world");
 MyString s2(s1); // Default copy constructor
 return 0;
}

Proper Copy Constructor with Pointers

To avoid issues with pointers, write a proper copy constructor that allocates new memory and copies the contents.

class MyString {
private:
 char* str_;
 int len_;
public:
 MyString(const char* s = "") {
  len_ = strlen(s);
  str_ = new char[len_ + 1];
  strcpy(str_, s);
 }
 MyString(const MyString& s) {
  len_ = s.len_;
  str_ = new char[len_ + 1];
  strcpy(str_, s.str_);
 }
 ~MyString() { delete[] str_; }
 void Print() { cout << str_ << endl; }
};

Copy Elision

Compilers like g++ can optimize code to avoid unnecessary copying of objects by default.

  • For example, omitting temporary object creation when returning an object (return value optimization).

  • To disable this feature, use -fno-elide-constructors with g++.

C Structure Example: Complex Number

struct Complex {
 double real;
 double imag;
};
int main() {
 Complex c;
 c.real = 1.0, c.imag = 0.5; // 1 + 0.5i
 Complex d;
 d.real = c.real * 2,
 d.imag = c.imag * 2; // d = c * 2;
 printf("%f + %fi\n", d.real, d.imag);
 return 0;
}

Define Constructors for the Complex Class

struct Complex {
 double real;
 double imag;
 Complex() : real(0.0), imag(0.0) {}
 Complex(const Complex& c)
  : real(c.real), imag(c.imag) {}
 Complex(double r, double i)
  : real(r), imag(i) {}
};
int main() {
 Complex c(1.0, 0.5); // 1 + 0.5i
 Complex c0 = c, c1(c);
 Complex d(c.real * 2, c.imag * 2);
 // d = c * 2;
 printf("%f + %fi\n", d.real, d.imag);
 return 0;
}

Add Member Functions to Complex

struct Complex {
 double real;
 double imag;
 Complex() : real(0.0), imag(0.0) {}
 Complex(const Complex& c)
  : real(c.real), imag(c.imag) {}
 Complex(double r, double i)
  : real(r), imag(i) {}
 Complex Add(const Complex& c) const {
  return Complex(real + c.real,
  imag + c.imag);
 }
 Complex Multiply(const Complex& c) const {
  return Complex(
  real * c.real - imag * c.imag,
  real * c.imag + imag * c.real);
 }
 void Print() const {
  printf("%f + %fi", real, imag);
 }
};
int main() {
 Complex c(1.0, 0.5); // 1 + 0.5i
 Complex d = c.Multiply(Complex(2, 0));
 // d = c * 2;
 d.Print();
 return 0;
}

C Structure Example: Shapes

Structures representing various 2D shapes:

struct Point {
 double x, y;
};
struct Line {
 Point p[2];
};
struct Triangle {
 Point p[3];
};
struct Rectangle {
 double x0, y0, x1, y1;
 // top_left, bottom_right
 // Why not Point p[4]; ?
};

C Structure Example: Shapes with Utility Functions

#include <math.h>
// Utility function : distance between two points.
inline double Distance(const Point& p0,
 const Point& p1) {
 const double dx = p1.x - p0.x, dy = p1.y - p0.y;
 return sqrt(dx * dx + dy * dy);
}
double Length(const Line& line){
 return Distance(line.p[0], line.p[1]);
}
double Perimeter(const Triangle& tri){
 return Distance(tri.p[0], tri.p[1]) +
  Distance(tri.p[1], tri.p[2]) +
  Distance(tri.p[2], tri.p[0]);
}
double Perimeter(const Rectangle& rect){
 return 2 * (fabs(rect.x1 - rect.x0) +
  fabs(rect.y1 - rect.y0));
}
double Area(const Triangle& tri){
 const double dx10 = tri.p[1].x - tri.p[0].x;
 const double dy10 = tri.p[1].y - tri.p[0].y;
 const double dx20 = tri.p[2].x - tri.p[0].x;
 const double dy20 = tri.p[2].y - tri.p[0].y;
 return 0.5 * fabs(dx10 * dy20 - dx20 * dy10);
}
double Area(const Rectangle& rect) {
 return fabs((rect.x1 - rect.x0) *
  (rect.y1 - rect.y0));
}

C Structure Example: Shapes using Member Functions

Use member functions:
struct Point {
 double x, y;
};
double Distance(const Point& p0, const Point& p1);
struct Line {
 Point p[2];
 double Length() const;
}
struct Triangle {
 Point p[3];
 double Perimeter() const;
 double Area() const;
};
struct Rectangle {
 double x0, y0, x1, y1;
 double Perimeter() const;
 double Area() const;
};
double Line::Length() const {
 return Distance(p[0], p[1]);
}
double Triangle::Perimeter() const {
 return Distance(p[0], p[1]) + Distance(p[1], p[2])
  + Distance(p[2], p[0]);
}
double Triangle::Area() const {
 return 0.5 * fabs(
  (p[1].x - p[0].x) * (p[2].y - p[0].y) -
  (p[2].x - p[0].x) * (p[1].y - p[0].y));
}
double Rectangle::Perimeter() const {
 return 2 * (fabs(x1 - x0) + fabs(y1 - y0));
}
double Rectangle::Area() const {
 return fabs((x1 - x0) * (y1 - y0));
}

C++ Class Example: Stack

Stack: Last In First Out (LIFO)

class Stack {
public:
 Stack() : num_data_(0), data_(NULL) {}
 ~Stack() { delete[] data_; }
 void Push(int value);
 void Pop() {
  if (num_data_ > 0) --num_data_;
 }
 int Top() const {
  return data_[num_data_ - 1];
 } // TODO: check NULL.
 bool IsEmpty() const {
  return num_data_ <= 0;
 }
private:
 int num_data_;
 int* data_;
};
void Stack::Push(int value) {
 int* new_data = new int[num_data_ + 1];
 for (int i = 0; i < num_data_; ++i) {
  new_data[i] = data_[i];
 }
 delete[] data_;
 data_ = new_data;
 data_[num_data_] = value;
 ++num_data_;
}

Push operation with better memory management:

void Stack::Push(int value) {
 if (num_data_ >= capacity_) {
  const int new_capacity = num_data_ + 1;
  int* new_data = new int[new_capacity];
  for (int i = 0; i < num_data_; ++i) {
  new_data[i] = data_[i];
  }
  delete[] data_;
  data_ = new_data;
  capacity_ = new_capacity;
 }
 data_[num_data_] = value;
 ++num_data_;
}

C++ Class Example: Queue

Queue: First In First Out (FIFO)

class Queue {
public:
 Queue();
 ~Queue();
 void Push(int value);
 void Pop();
 int Front() const ;
 int Back() const ;
 bool IsEmpty() const ;
private:
 ...
};

this Pointer

In member functions, this can be used to point to the instance itself.

  • Useful when you need to return the pointer or reference to the instance.

struct Complex {
 double real;
 double imag;
 Complex(double r = 0, double i = 0)
  : real(r), imag(i) {}
 Complex Add(const Complex& c) const {
  return Complex(real + c.real, imag + c.imag);
 }
 Complex& Scale(double s) {
  real *= s, imag *= s;
  return *this;
 }
 void Print() const {
  printf("%f + %fi", this->real, this->imag);
 }
};
int main() {
 Complex c(1.0, 0.5), d(0.5, 0.5);
 c.Add(d).Scale(2.0).Print(); // 3.0 + 2.0i
 return 0;
}

Class Static Members

Static members pertain to the class, not to individual instances.

  • Static member variables are shared among all instances.

  • Static member functions do not have an associated instance, so you can only see the static member variables.

  • They can, however, access private member variables if an instance is passed to them.

Static Member Variables

  • Use the static keyword to specify static members.

  • In static member functions, this pointer is not defined.

  • Static member variables are like global variables, but only for the class.

  • To access them, use ClassName::MemberName, obj.MemberName, or ptr->MemberName.

Example of Static Function

struct Complex {
 double real;
 double imag;
 Complex() : real(0.0), imag(0.0) {}
 Complex(const Complex& c)
  : real(c.real), imag(c.imag) {}
 Complex(double r, double i) : real(r), imag(i) {}
 ...
 static Complex Add(const Complex& c1,
  const Complex& c2) {
  return Complex(c1.real + c2.real,
  c1.imag + c2.imag);
 }
};
int main() {
 Complex c(1.0, 0.5);
 Complex d = Complex::Add(c, Complex(2, 1));
 d.Print();
 return 0;
}

Static Member Variable Example

class CountInstance {
public:
 CountInstance() {
  Print("construct: ", ++count_);
 }
 ~CountInstance() {
  Print("destruct: ", --count_);
 }
 static void Print(const string& msg, int n) {
  cout << msg << count_ << endl;
 }
private:
 static int count_; // declaration
};
int CountInstance::count_ = 0; // definition
int main() {
 CountInstance instance;
 for (int i = 0; i < 2; ++i) {
  CountInstance inner_instance;
  // Do nothing.
 }
 return 0;
}

Further Static Member Examples

struct MyClass {
 MyClass(double x, double y) : x_(x), y_(y) {}
 void DoSomething();
 static void Prepare(MyClass* arg);
 double x_, y_;
 static int iter_;
};
void MyClass::DoSomething() {
 for (int i = 0; i < iter_; ++i) {
  cout << x_ + y_ << endl;
 }
}
void MyClass::Prepare(MyClass* arg) {
 if(arg) arg->x_ = arg->y_ = 0.0; // OK.
 iter_ = 10;
 // OK.
}
int MyClass::iter_ = 10; // definition & init
int main() {
 MyClass::Prepare(nullptr);
 MyClass a(1.0, 2.0);
 MyClass::Prepare(&a);
 a.DoSomething();
 cout << MyClass::iter_ << ", "
  << a.x_ << endl;
 return 0;
}

Static Member Example: Singleton

Singleton ensures only one instance can exist for the entire program.

class Singleton {
public:
 static Singleton* GetInstance();
 // Some useful member functions here..
private:
 Singleton() {}
 static Singleton* instance_;
};
Singleton* Singleton::instance_ = NULL;
Singleton* Singleton::GetInstance() {
 if (instance_ == NULL) {
  instance_ = new Singleton;
 }
 return instance_;
}

Class Design Principles

  • Hide all data members unless absolutely necessary.

  • Make member functions meaningful and atomic.

  • Use const as much as possible.

  • Make input and output parameters clearly visible (coding style).

  • Keep initialization in constructors simple.

  • Make a separate setup function for complex initializations, especially if it might fail.

  • Use static members only when necessary.

  • Class utility functions that do not need to access data members are often implemented as static functions.

Binary Search Tree (BST)

BSTs allow binary search for fast lookup, addition, and removal of data items.

BST ADT Operations:

  • Initialize (BST T): constructor

  • Destroy (BST T): destructor

  • Search (BST T, Key K): returns whether K is in T

  • Insert (BST T, Key K): insert K into T

  • Delete (BST T, Key K): delete K from T

BST Rules

  • The left subtree of a node contains only nodes with keys smaller than its key.

  • The right subtree of a node contains only nodes with keys greater than its key.

  • The left and right subtrees each must also be a BST.

  • There must be no duplicate nodes.

Time Complexities of BST Operations

Operation

Average

Worst

Search

O(log n)

O(n)

Insert

O(log n)

O(n)

Delete

O(log n)

O(n)

Example BST Implementation

struct Node {
 int value;
 Node *left, *right;
 Node(int v = 0)
  : value(v), left(nullptr), right(nullptr) {}
 bool Search(int key) {
  if (key == value) return true;
  return key < value ?
  (left ? left->Search(key) : false) :
  (right ? right->Search(key) : false);
 }
 bool Insert(int key) {
  if (key == value) return false;
  if (key < value) {
  if (left) return left->Insert(key);
  left = new Node(key);
  } else {
  if (right) return right->Insert(key);
  right = new Node(key);
  }
  return true;
 }
 void Destroy() {
  if (left) left->Destroy();
  if (right) right->Destroy();
  delete this;
 }
};
class BinarySearchTree {
public:
 void Initialize() { root_ = nullptr; }
 void Destroy() { if (root_) root_ ->Destroy(); }
 bool Search(int key) const; { return root_ ? root_ ->Search(key) : false; }
 bool Insert(int key) {
  if (root_) return root_ ->Insert(key);
  root_ = new Node(key);
  return true;
 }
 bool Delete(int key);
private:
 Node* root_;
};

BST Delete

  • If the node has no children or one child, simple removal and adjustment of pointers.

  • If the node has two children, find the smallest node in the right subtree (or largest in the left subtree), replace the node to be deleted with this node, and then delete the inorder successor.

BST Delete Implementation

bool BinarySearchTree::Delete(int key) {
 if (root_ == nullptr) return false ;
 Node** pp = &root_;
 while (*pp != nullptr) {
  if (key == (*pp)->value) break ;
  pp = &(key < (*pp)->value ? (*pp)->left :
  (*pp)->right);
 }
 if (*pp == nullptr) return false ;
 Node* p = *pp;
 if (p->left && p->right) { // both children
  Node** pq = &(p->right);
  while ((*pq)->left) pq = &(*pq)->left;
  (*pp)->value = (*pq)->value;
        p = *pq;
  *pq = (*pq)->right;
 }
 else {
  *pp = (p->left ? p->left : p->right);
 }
 delete p;
 return true ;
}

Function Overloading

C++ allows you to define same-name functions with different parameters.

  • Both regular functions and class member functions can be overloaded.

  • A family of functions that do the same thing but using different argument lists

  • Exactly same functions except return types are not allowed.

  • At least one parameter is different, or the const-ness should be different.

Function Overloading Examples

void Print(const char* str, int w); // #1
void Print(double d, int width);  // #2
void Print(long l, int width);  // #3
void Print(int s, int width);  // #4
void Print(const char* str);  // #5
Print("Pancakes", 15); // Use #1
Print("Syrup");  // Use #5
Print(3.1415, 10);  // Use #2
Print(1979, 12);  // Use #4
Print(1979l, 15);  // Use #3


class MyClass {
public:
 MyClass();
 MyClass(const MyClass& c);
 explicit MyClass(int x);
 // ^ Prevent implicit type conversion.
 int& x(); // Differ in constness.
 const int& x() const;
 int DoSomething();
 double DoSomething(double c);
 void DoSomething(double a, double b);
 //double DoSomething(); // Error!
 //void DoSomething(double a, double b,
 // double c = 0.0); // Error!
};

Operator Overloading

C++ allows you to redefine built-in operators like +, -, *.

Most commonly overloaded operators are:

  • Arithmetic operators: +, -, *, /

  • Assignment operators: =, +=, -=, *=

  • Comparison operators: <, >, <=, >=, ==, !=

  • Array or containers: [], ()

Operator Overloading by member Function.

struct Point {
double x, y, z;
 Point() : x(0), y(0), z(0) {}
 Point(double x, double y, double z)
  : x(x), y(y), z(z) {}
 Point Add(const Point& p) const {
  return Point(x + p.x, y + p.y, z + p.z);
 }
 void Print() const {
  cout << x << "," << y << "," << z << endl;
 }
};
int main() {
 Point p1(1, 1, 1), p2(2, 2, 2);
 Point p3 = p1.Add(p2);
 p3.Print();
 return 0;
}
struct Point {
double x, y, z;
...
 Point operator+(const Point& p) const {
  return Point(x + p.x, y + p.y, z + p.z);
 }
...
};
int main() {
 Point p1(1, 1, 1), p2(2, 2, 2);
 Point p3 = p1.operator+(p2);
 p3.Print();
 p3 = p1 + p2;
 p3.Print();
 return 0;
}

Operator overloading must be used very carefully, as it can seriously degrade readability.

Operator Overloading by Non-member Function

If the first operand is not a class type, use a non-member operator overloaded function!

class Point {
private:
 double x, y, z;
 ...
 friend Point operator+(
  double v, const Point& p);
};
Point operator+(double v, const Point& p) {
 return Point(v + p.x, v + p.y, v + p.z);
}
int main() {
 Point p1(1, 1, 1);
 Point p2 = operator+(2.0, p1);
 p2.Print();
 p2 = 2.0 + p1;
 p2.Print();
 return 0;
}

Converting Constructor & Operator Overloading

Constructors can be used to convert some type (the parameter type) to another type (the class belonging the constructor).
This can affect the behavior of overloaded operators.

struct Complex {
 double real, imag;
 Complex() : real(0.0), imag(0.0) {}
 Complex(double r, double i)
  : real(r), imag(i) {}
 Complex(const Complex& c)
  : real(c.real), imag(c.imag) {}
 Complex operator+(const Complex& c) const {
  return Complex(real + c.real,
  imag + c.imag);
 }
};
void Test() {
 Complex a(1.0, 2.0), b(2.0, 5.0), c;
 c = a + b;
 // OK.
 c = a + 3.0; // Error.
 c = 2.0 + b; // Error.
}
struct Complex {
 double real, imag;
 Complex() : real(0.0), imag(0.0) {}
 Complex(double v) : real(v), imag(0.0) {}
 Complex(double r, double i)
  : real(r), imag(i) {}
 Complex(const Complex& c)
  : real(c.real), imag(c.imag) {}
 Complex operator+(const Complex& c) const;
};
void Test() {
 Complex a(1.0, 2.0), b(2.0, 5.0), c;
 c = a + b;
 // OK.
 c = a + 3.0; // OK.
 c = 2.0 + b; // Still error.
}
struct Complex {
 double real, imag;
 Complex() : real(0.0), imag(0.0) {}
 // Complex(double v) : real(v), imag(0.0) {}
 Complex(double r, double i = 0.0)
  : real(r), imag(i) {}
 Complex(const Complex& c)
  : real(c.real), imag(c.imag) {}
};
Complex operator+(const Complex& a,
 const Complex& b);
void Test() {
 Complex a(1.0, 2.0), b(2.0, 5.0), c;
 c = a + b;
 // OK.
 c = a + 3.0; // OK.
 c = 2.0 + b; // OK.
}

Exercise #2

```c++

include

using namespace