Memory Management in Classes
On this page
When a class owns dynamic data (part of the data stored in heap memory region), special handling is required to avoid memory problems.
Implicitly-declared Methods
Some methods can be used without explicit declarations. They are usually methods that are essential to the object including the default constructor, destructor, copy constructor, copy assignment operator overloading, etc.
- Default constructor
- Must declare and define explicitly if other constructor is present
- Will only trigger all class-type instance variables’ default constructors; primitive types and arrays are not initialized. Uninitialized primitive types should be avoided to prevent undefined behaviors.
- Destructor
- Copy constructor
- Copy assignment operator overloading
// Operations you can perform without defining any methods
// works with classes containing no dynamic data
// all instance variables can be duplicated using simple assignment
class Example {
string name;
int age;
double grades[4];
public:
string getName() {return name;}
void setName(string name) {this->name = name;}
int getAge() {return age;}
void setAge(int age) {this->age = age;}
double getGrade(int index) {return grades[index];}
void setGrade(int index, int value) {grades[index] = value;}
};
int main() {
// allowed without explicit declaration
Example exp; // trigger default constructor
exp.setName("John");
exp.setAge(14);
Example exp1(exp); // trigger copy constructor
Example exp2;
exp2 = exp; // trigger copy assignment operator overload
// both exp1 and exp2 will have name as "John" and age as 14
}
Dynamic data in class
-
pointer-typed instance variables
- holding a single value/object or an array
- allocated using
newand data stored in heap - simple assignment will cause shallow copy
-
Potential memory problems
- memory leak - fail to
delete- bad or no explicit destructor
- dynamic data passed around without correct handling
- shallow copy - fail to copy the dynamic data in the heap memory
- only the memory addresses (values of pointers) are copied by assignment
- implicitly-declared methods will cause shallow copy
- bad or no explicit copy constructor
- bad or no explicit copy assignment operator overload
- only happens when objects are copied or copy assigned
- will consequently cause the destructor to
deletea same memory block in heap multiple times
- memory leak - fail to
-
Rule of three
- The big three methods needed for classes to handle dynamic data correctly
- Destructor
- Mandatory because it will always be triggered
- When an object is destroyed
- Local object going out of scope
- Dynamic object being
deleted
- place to
deletedynamic data
- copy constructor
-
Optional not needed if never triggered
-
Explicit triggering
MyArray obj2(obj1); -
Implicit triggering
MyArray obj2 = obj1; -
Implicit triggering in parameter passing:
// declaration void function(MyArray myObj); // call MyArray myObj1; function(myObj1); -
Take a const reference parameter to avoid triggering itself
-
Perform deep copy here
-
- copy assignment operator overload
- optional not needed if never triggered
- explicit triggering
MyArray obj2; obj2 = obj1; - implicit triggering Easy to miss
- parameter passed by value
- returned value
- must clean up old data
- take a const reference parameter to avoid pass-by-value
- perform deep copy here
return *this;to return a reference of its own class typeMyArray &
class MyArray {
private:
int *arr;
int size;
public:
MyArray();
MyArray(int size);
MyArray(const MyArray& other);
MyArray & operator=(const MyArray& other);
~MyArray();
}
MyArray::MyArray() {
size = 0;
arr = nullptr;
}
MyArray::MyArray(int size) {
this->size = size;
arr = new int[size];
}
MyArray::MyArray(const MyArray& other) {
// you can access private members of other directly in C++!
size = other.size;
arr = new int[size];
for (int i = 0; i < size; ++i)
arr[i] = other.arr[i];
}
MyArray& MyArray::operator=(const MyArray& other) {
if (this == &other) // self-assignment check
return *this;
size = other.size;
// must release memory of the old dynamic data first!
delete [] arr;
arr = new int[size];
for (int i=0; i<size; ++i)
arr[i] = other.arr[i];
return *this; // IMPORTANT! return the current object by reference
}
MyArray::~MyArray() {
delete [] arr;
}
- Rule of five (optional content)
- Big three plus two more methods
- Move constructor
- Move assignment operator overloading
- Not mandatory but will improve efficiency
- New syntax since c++ 11 using
&&to refer to a rvalue reference - Mutable(non-const) parameter to allow modification
- Big three plus two more methods
// the parameter is no longer 'const' because you will modify it
MyArray::MyArray(MyArray && other) {
size = other.size;
arr = other.arr;
// must make the other object ready for destruction
// without this step the destructor may try to delete the moved data
other.arr = null;
other.size = 0; // this is not necessary unless your destructor rely on the size
}
// the parameter is no longer 'const' because you will modify it
MyArray & MyArray::operator=(MyArray && other) {
if (this == &other) // self-assignment check
return *this;
// Swap both the size and arr with the other object
// the destructor of the other object will destroy the old data
swap(size, other.size);
swap(arr, other.arr);
return *this;
}
Ownership of Dynamic Data
A dynamic data can be created anywhere and passed around. As the ownership changed. The last owner should take the responsibility to release the memory.
#. Passing dynamic data created outside as a parameter. The recipient owns the data after.
class DataType {
// ...
};
class MyArray {
private:
DataType *myDynamicData;
public:
MyArray(DataType *initialData = nullptr) {
myDynamicData = initialData;
}
~MyArray() {
// delete here although it is not new'ed in the class
delete myDynamicData;
}
};
int main() {
DataType *initData = new DataType(); // main function owns it
MyArray obj1(initData); // obj1 owns it now
return 0;
}
#. You may also choose to remove the destructor and let the main function to delete if you feel it make more sense that the recipients are only users of the dynamic data and the main function is still the owner. Sometimes the dynamic data is shared in this case.
class Configuration {
// ...
};
class MyArray {
private:
Configuration *config;
public:
MyArray(Configuration *config = nullptr) {
this->config = config;
}
};
int main() {
Configuration *config1 = new Configuration(); // main function owns it
MyArray obj1(config1); // obj1 uses it
MyArray obj2(config1); // obj2 uses it
delete config1;
return 0;
}
The dynamic data can also be passed as a returned value.
Example: The object always owns the data. The recipient just used it.
class MyArray {
private:
int * array; // always owned by the object
int size;
public:
// ... other methods
// array is new'ed in some methods
~MyArray() {delete [] array;}
int *getArray() {return array;} // expose to outside
int getSize() {return size;}
};
int main() {
// declare and initialize data in MyArray obj1;
int size = obj1.getSize();
int *arr = obj1.getArray();
for (int i = 0; i < size; ++i)
cout << arr[i] << " ";
return 0;
}
Example: The ownership is not clear. You can either let the object or the main function to delete the dynamic data. Both are reasonable.
class MyArray {
private:
int * array; // always owned by the object
int size;
public:
// ... other methods
// array is new'ed in some methods
int *getPositiveValues();
int getPositiveCount();
};
int * MyArray::getPositiveValues() {
int posCount = getPositiveCount();
int *arr = new int[posCount]; // dynamic data, not meant to be owned
int j = 0;
for (int i = 0; i < size; ++i)
if (array[i] > 0) {
arr[j] = array[i];
++j;
}
return arr;
}
int MyArray::getPositiveCount() {
int count = 0;
for (int i = 0; i < size; ++i)
if (array[i] > 0) ++count;
return count;
}
int main() {
// declare and initialize data in MyArray obj1;
int *positiveArray = obj1.getPositiveValues();
int size = obj1.getPositiveCount();
for (int i = 0; i < size; ++i)
cout << positiveArray[i] << " ";
delete [] positiveArray; // positiveArray owned by the main function
return 0;
}