Rule of Three
Rule of Three
-
the Rule of Three is used for any object that dynamically allocates memory. Will need
- Copy Constructor
- Assignment Operator Overloaded (=)
- Destructor
-
Rule: If you need one, then you need all three!!!!
I will go through an example as I expain this important concept(s).
Notes
Recall: Friend Function
Friend functions allows access to private data memebers, but again it’s not an actual memeber of the class.
Character Object
Let’s work through the example of a Character Class, that has an pointer array for the Tool/Weapon belt in which we will create multiple objects (Characters) and use Copy Constructor, Default Constructor, Overloaded Assignment, and Deconstructor.
Regular Constructor
GCharacter(std::string name = "John", int capacity = DEFAULT_CAPACITY );
Heres the regular/Default constructor implemented in the GCharacter.cpp file:
GCharacter::GCharacter(string n, int cap)
{ // Default Constructor where n = name and cap = capacity
name = n;
capacity = cap;
used = 0;
toolHolder = new string[cap]; // Allocates new Memory on the HEAP!!
}
Overloaded Assignment
Why do we need a Overloaded Assignment Operator?
- To avoid creating a Shallow Copy, we can use a Overloaded Assignment Operator that performs a Deep Copy and allow multiple objects to have their own copies of the array in our example.
Heres the Syntax:
GCharacter& operator = (const GCharacter& source);
Heres the Overloaded Assignment Implementation in the GCharacter.cpp file:
GCharacter& GCharacter::operator=(const GCharacter& source)
{
// Testing for self-assignment
cout << "Overloaded Assignment Called. " << endl;
// Check for self assignment
// gc1 = gc1;
if (this == &source)
{
return *this; // return GCharacter Object
}
this->name = source.name;
this->capacity = source.capacity;
this->used = source.used;
copy(source.toolHolder, source.toolHolder + used, this->toolHolder );
return *this; // return GCharacter Object
}
In this case C2 already exists and so does C3 (we declared C3 an Object), now if we set C3 = C2 the Operator Overloaded Constructor will be called and create a new array pointing to new memory for the tool belt/array.
Copy Constructor
Why would we need a Copy Constructor?
- Lets say for example we had object C2 (it already exists), and now were trying to create a new object called C3 and it doesn’t exist yet. But were going to initialize it with the values that currently exist in C2 (C3 = C2), thinking we can just change out (with setters) the attributes. But what happens is, even if we have an overloaded assignment operator, it’s not going to invoke in this case becuase C3 isn’t in existence. Instead, its going to invoke the default constructor and by default performs a shallow copy. It simply just gets the address that is being copied from C2 to C3. So in our example, it would end up sharing the exact same tool array on the Heap.
GCharacter(const GCharacter& source);
Heres the Copy Assignment Implementation in the GCharacter.cpp file:
GCharacter::GCharacter(const GCharacter& source)
{
cout << "Copy Constructor Called. " << endl;
this->name = source.name;
this->capacity = source.capacity;
this->used = source.used;
toolHolder = new string[source.capacity]; // In order to do a Deep Copy, we need to create a brand new string array
copy(source.toolHolder, source.toolHolder + used, this->toolHolder );
}
Deconstructor
Why do we need a Deconstructor?
- Becuase in C++ when we create (allocate memory) on the Heap, we must take care of it by deleting it when were done using it. Memory Manegment is big in C++!!
~GCharacter();
Heres the Deconstructor implemented, it deletes the memory allocated by our Character Object for the Tool Belt allocated on Heap.
GCharacter::~GCharacter()
{
// only handling the dynamic memory
cout << "Destructor called for " << this->name << " at this memory location " << this << endl;
delete[] toolHolder;
}
Full Code Here
Character Header File (GCharacter.h)
#ifndef GCHARACTER_H_
#define GCHARACTER_H_
#include <iostream>
#include <string>
class GCharacter
{
private:
// Data Members (Attributes)
std::string name; // Name of Character
int capacity; // Capacity/Size of Tool Belt/Array
int used; // Number of Spaces on Tool Belt/Arrat
std::string* toolHolder;
public:
static const int DEFAULT_CAPACITY = 5;
// Constructor "Normal"
GCharacter(std::string name = "John", int capacity = DEFAULT_CAPACITY );
// Copy Constructor
GCharacter(const GCharacter& source);
// Overloaded Assignment
GCharacter& operator = (const GCharacter& source);
// Destructor (easy to write)
~GCharacter();
// Insert (member function) a new tool into the tool array (holder)
void insert(const std::string& toolName);
// Overloading the << operator (syntax) Friend Function
friend std::ostream& operator <<(std::ostream& os, const GCharacter& gc);
};
#endif
GCharacter File Functions (GCharacter.cpp)
#include<iostream>
using std::cout; using std::endl;
#include<string>
using std::string;
#include<algorithm>
using std::copy;
#include "GCharacter.h"
// Constructor
GCharacter::GCharacter(string n, int cap)
{
name = n;
capacity = cap;
used = 0;
toolHolder = new string[cap];
}
// Copy Constructor
GCharacter::GCharacter(const GCharacter& source)
{
cout << "Copy Constructor Called. " << endl;
this->name = source.name;
this->capacity = source.capacity;
used = source.used;
toolHolder = new string[source.capacity]; // In order to do a Deep Copy, we need to create a brand new string array
copy(source.toolHolder, source.toolHolder + used, this->toolHolder );
}
// Overloaded Assignment Operator
GCharacter& GCharacter::operator=(const GCharacter& source)
{
// Testing for self-assignment
cout << "Overloaded Assignment Called. " << endl;
// Check for self assignment
// gc1 = gc1;
if (this == &source)
{
return *this; // return GCharacter Object
}
this->name = source.name;
this->capacity = source.capacity;
used = source.used;
copy(source.toolHolder, source.toolHolder + used, this->toolHolder );
return *this; // return GCharacter Object
}
// Destructor
GCharacter::~GCharacter()
{
// only handling the dynamic memory
cout << "Destructor called for " << this->name << " at this memory location " << this << endl;
delete[] toolHolder;
}
// Inserting a new tool into our toolHolder
void GCharacter::insert(const string& toolName)
{
if (used == capacity)
{
cout << "Tool Holder is full. Cannot add any additional tools " << endl;
}
else
{
toolHolder[used] = toolName;
used++; //increment
}
}
std::ostream& operator<<(std::ostream& os, const GCharacter& gc)
{
os << "Game Character " << gc.name << " has the following tools: " << std::endl;
// iterate over our tool array
for(int i = 0; i < gc.used; i++)
{
os << gc.toolHolder[i] + " | ";
}
return os << std::endl;
}
Main File to Run Program (main.cpp) showing exCopyConstructor…
Let’s create an example where we pass to a function a GCharacter Object and also return a GCharacter Object.
When we pass a GCharacter Object to a function, we will get an invoacation of the copy constructor and also whenever we return a GCharacter Object we’ll get an invocation of the copy constructor.
We named it exCopyConstructor
#include <iostream>
using std::cout;
using std::endl;
#include "GCharacter.h"
GCharacter exCopyConstructor(GCharacter tempGC)
{ // Pass GCharacter by Value
cout << "Copy Constructor called twice." << endl;
cout << "Once for the Formal Parameter being passed by value" << endl;
cout << "Once for the return value." << endl;
return tempGC;
}
int main()
{
GCharacter gc1;
gc1.insert("Potion");
gc1.insert("Crossbow");
gc1.insert("Gun");
gc1.insert("Ray Gun");
gc1.insert("Cloak");
gc1.insert("Sword"); // won't get added in since we have a tool array of 5 by default
cout << gc1 << endl;
cout << "Creating another Character g2 Named Devin!!!! " << endl;
GCharacter gc2("Devin", 5); // 5 size for the tool array
gc2.insert("Axe");
gc2.insert("Sword");
gc2.insert("Knife");
cout << gc2 << endl;
// Call to exCop
exCopyConstructor(gc2);
}
Output:
Tool Holder is full. Cannot add any additional tools
Game Character John has the following tools:
Potion | Crossbow | Gun | Ray Gun | Cloak |
Creating another Character g2 Named Devin!!!!
Game Character Devin has the following tools:
Axe | Sword | Knife |
Copy Constructor Called.
Copy Constructor called twice.
Once for the Formal Parameter being passed by value
Once for the return value.
Copy Constructor Called.
Destructor called for Devin at this memory location 0x7ffee0afd480
Destructor called for Devin at this memory location 0x7ffee0afd458
Destructor called for Devin at this memory location 0x7ffee0afd508
Destructor called for John at this memory location 0x7ffee0afd5e8
When called the exCopyConstructor we invoke the main Copy Constructor in our GCharacter.cpp file. Then since we passed our Object by value to the exCopyConstructor, the function is invoked and prints (cout) out the 3 lines. Then the main Copy Constructor is called again once we return our GCharacter Object.
Now about the Destructors:
in this example copy constructor, we had two GCharacter objects being placed on the stack, one that’s being passed in and the one that is being returned so both of those get destroyed whenever we terminate the function. Those are the first two lines of the Destructor called.
The next two lines relate to gc1 and gc2 being popped off the stack when we terminate the main function.
Other Main File Showing Overloading Operator
Example Program being run to show Regular/Default Constructor, Copy Constructor, and Overloaded Operator being called and used.
#include <iostream>
using std::cout;
using std::endl;
#include "GCharacter.h"
int main()
{
cout << endl;
cout << "Creating Another Character gc1: " << endl;
GCharacter gc1;
gc1.insert("Potion");
gc1.insert("Crossbow");
gc1.insert("Gun");
gc1.insert("Ray Gun");
gc1.insert("Cloak");
gc1.insert("Sword"); // won't get added in since we have a tool array of 5 by default
cout << "gc1: " << endl;
cout << gc1 << endl;
cout << "Creating Another Character gc2: " << endl;
GCharacter gc2("Devin", 5);
gc2.insert("Axe"); // insert into gc2
gc2.insert("Potion");
gc2.insert("Ray Gun");
cout << "gc2: " << endl;
cout << gc2 << endl;
// Copy Constructor
cout << "Creating Another Character gc3: " << endl;
GCharacter gc3 = gc2; // gc3 wasn't in existence
cout << "gc3: " << endl;
cout << gc3 << endl;
// Over loaded Assignment Operator
gc2 = gc1;
// overloaded insertion operator
cout << "_____________________ " << endl;
cout << "gc2: " << gc2 << endl;
cout << "gc1: " << gc1 << endl;
cout << "gc3: " << gc3 << endl;
}
Output
Creating Another Character gc1:
Main Constructor being Called
Tool Holder is full. Cannot add any additional tools
gc1:
Game Character John has the following tools:
Potion | Crossbow | Gun | Ray Gun | Cloak |
Creating Another Character gc2:
Main Constructor being Called
gc2:
Game Character Devin has the following tools:
Axe | Potion | Ray Gun |
Creating Another Character gc3:
Copy Constructor Called.
gc3:
Game Character Devin has the following tools:
Axe | Potion | Ray Gun |
gc1: Game Character John has the following tools:
Potion | Crossbow | Gun | Ray Gun | Cloak |
Overloaded Assignment Called.
_____________________
gc2: Game Character John has the following tools:
Potion | Crossbow | Gun | Ray Gun | Cloak |
gc1: Game Character John has the following tools:
Potion | Crossbow | Gun | Ray Gun | Cloak |
gc3: Game Character Devin has the following tools:
Axe | Potion | Ray Gun |
Destructor called for Devin at this memory location 0x7ffee9b77480
Destructor called for John at this memory location 0x7ffee9b77508
Destructor called for John at this memory location 0x7ffee9b775e8