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?
When an object is returned by value.
When an object is passed to a function by value as an argument.
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
, orptr->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