Dynamic Management of Objects
Contents
- Creating and Destroying Objects - Constructors and Destructors
- The new and delete Operators
- Scope and the Lifetime of Objects
- Data Structures for Managing Objects
1. Creating and Destroying Objects - Constructors and Destructors
(Ref. Lippman 14.1-14.3)
Let's take a closer look at how constructors and destructors work.
A Point Class
Here is a complete example of a Point class. We have organized the code into three separate files:
point.h contains the declaration of the class, which describes the structure of a Point object.
point.C contains the definition of the class i.e. the actual implementation of the methods.
point_test.C is a program that uses the Point class.
Our Point class has three constructors and one destructor.
Point(); // The default constructor.
Point(float fX, float fY); // A constructor that takes two floats.
Point(const Point& p); // The copy constructor.
~Point(); // The destructor.
These constructors can be respectively invoked by object definitions such as
Point a;
Point b(1.0, 2.0);
Point c(b);
The default constructor, Point(), is so named because it can be invoked without any arguments. In our example, the default constructor initializes the Point to (0,0). The second constructor creates a Point from a pair of coordinates of type float. Note that we could combine these two constructors into a single constructor which has default arguments:
Point(float fX=0.0, float fY=0.0);
The third constructor is known as a copy constructor since it creates one Point from another. The object that we want to clone is passed in as a constant reference. Note that we cannot pass by value in this instance because doing so would lead to an unterminated recursive call to the copy constructor. In this example, the destructor does not have to perform any clean-up operations. Later on, we will see examples where the destructor has to release dynamically allocated memory.
Constructors and destructors can be triggered more often than you may imagine. For example, each time a Point is passed to a function by value, a local copy of the object is created. Likewise, each time a Point is returned by value, a temporary copy of the object is created in the calling program. In both cases, we will see an extra call to the copy constructor, and an extra call to the destructor. You are encouraged to put print statements in every constructor and in the destructor, and then carefully observe what happens.
point.h
// Declaration of class Point.
#ifndef _POINT_H_
#define _POINT_H_
#include <iostream.h>
class Point {
// The state of a Point object. Property variables are typically
// set up as private data members, which are read from and
// written to via public access methods.
private:
float mfX;
float mfY;
// The behavior of a Point object.
public:
Point(); // The default constructor.
Point(float fX, float fY); // A constructor that takes two floats.
Point(const Point& p); // The copy constructor.
~Point(); // The destructor.
void print() { // This function will be made inline by default.
cout << "(" << mfX << "," << mfY << ")" << endl;
}
void set_x(float fX);
float get_x();
void set_y(float fX);
float get_y();
};
#endif // _POINT_H_
point.C
// Definition class Point.
#include "point.h"
// A constructor which creates a Point object at (0,0).
Point::Point() {
cout << "In constructor Point::Point()" << endl;
mfX = 0.0;
mfY = 0.0;
}
// A constructor which creates a Point object from two
// floats.
Point::Point(float fX, float fY) {
cout << "In constructor Point::Point(float fX, float fY)" << endl;
mfX = fX;
mfY = fY;
}
// A constructor which creates a Point object from
// another Point object.
Point::Point(const Point& p) {
cout << "In constructor Point::Point(const Point& p)" << endl;
mfX = p.mfX;
mfY = p.mfY;
}
// The destructor.
Point::~Point() {
cout << "In destructor Point::~Point()" << endl;
}
// Modifier for x coordinate.
void Point::set_x(float fX) {
mfX = fX;
}
// Accessor for x coordinate.
float Point::get_x() {
return mfX;
}
// Modifier for y coordinate.
void Point::set_y(float fY) {
mfY = fY;
}
// Accessor for y coordinate.
float Point::get_y() {
return mfY;
}
point_test.C
// Test program for the Point class.
#include "point.h"
int main() {
Point a;
Point b(1.0, 2.0);
Point c(b);
// Print out the current state of all objects.
a.print();
b.print();
c.print();
b.set_x(3.0);
b.set_y(4.0);
// Print out the current state of b.
cout << endl;
b.print();
return 0;
}
2. The new and delete Operators
(Ref. Lippman 4.9, 8.4)
Until now, we have only considered situations in which the exact number of objects to be created is known at compile time. This is rarely the case in real world software. A web-browser cannot predict in advance how many image objects it will find on a web page. What is needed, therefore, is a way to dynamically create and destroy objects at run time. C++ provides two operators for this purpose:
The new operator allows us to allocate memory for one or more objects. It is similar to the malloc() function in the C standard library.
The delete operator allows us to release memory that has previously been allocated using new. It is similar to the free() function in the C standard library. Note that it is an error to apply the delete operator to memory allocated by any means other than new.
We can allocate single objects using statements such as
a = new Point();
b = new Point(2.0, 3.0);
Object arrays can be allocated using statements such as
c = new Point[num_points];
In either case, new returns the starting address of the memory it has allocated, so a, b, and c must be defined as pointer types, Point *. A single object can be released using a statement such as
delete a;
When releasing memory associated with an array, it is important to remember to use the following notation:
delete[] c;
If the square brackets are omitted, only the first object in the array will be released, and the memory associated with the rest of the objects will be leaked.
nd_test.C
// Test program for the new and delete operators.
#include "point.h"
int main() {
int num_points;
Point *a, *b, *c;
float d;
// Allocate a single Point object in heap memory. This invokes the default constructor.
a = new Point();
// This invokes a constructor that has two arguments.
b = new Point(2.0, 3.0);
// Print out the two point objects.
cout << "Here are the two Point objects I have created:" << endl;
a->print();
b->print();
// Destroy the two Point objects.
delete a;
delete b;
// Now allocate an array of Point objects in heap memory.
cout << "I will now create an array of Points. How big shall I make it? ";
cin >> num_points;
c = new Point[num_points];
for (int i = 0; i < num_points; i++) {
d = (float)i;
c[i].set_x(d);
c[i].set_y(d + 1.0);
}
// Print out the array of point objects.
cout << "Here is the array I have created:" << endl;
for (int i = 0; i < num_points; i++) {
c[i].print();
}
// Destroy the array of Point objects.
delete[] c; // What happens if [] is omitted?
return 0;
3. Scope and the Lifetime of Objects
(Ref. Lippman 8.1-8.4)
There are three fundamental ways of using memory in C and C++.
- Static memory. This is memory allocated by the linker for the duration of the program. Global variables and objects explicitly defined as static fall into this category.
- Automatic memory. Objects that are allocated in automatic memory are destroyed automatically when they go out of scope. Examples are local variables and function arguments. Objects that reside in automatic memory are said to be allocated on the stack.
- Dynamic memory. Memory allocated using the new operator (or malloc()) falls into this category. Dynamic memory must be explicitly released using the delete operator (or free(), as appropriate.) Objects that reside in dynamic memory are said to be allocated on the heap.
A garbage collector is a memory manager that automatically identifies unreferenced objects in dynamic memory and then reclaims that memory. The C and C++ standards do not require the implementation of automatic garbage collection, however, garbage collectors are sometimes implemented in large scale projects where it can be difficult to keep track of memory explicitly.
The following program illustrates various uses of memory. Note that the static object in the function foo() is only allocated once, even though foo() is invoked multiple times.
sl_test.C
// Test program for scope and the lifetime of objects.
#include "point.h"
Point a(1.0, 2.0); // Resides in static memory.
void foo() {
static Point a; // Resides in static memory.
a.set_x(a.get_x() + 1.0);
a.print();
}
int main() {
Point a(4.0, 3.0); // Resides in automatic memory.
a.print();
::a.print();
for (int i = 0; i < 3; i++)
foo();
Point *b = new Point(5.0, 6.0); // Resides in heap memory.
b->print();
delete b;
// Curly braces serve as scope delimiters.
{
Point a(7.0, 9.0); // Resides in automatic memory.
a.print();
::a.print();
}
return 0;
}
Here is the output from the program:
In constructor Point::Point(float fX, float fY) <-- Global object a.
In constructor Point::Point(float fX, float fY) <-- Local object a.
(4,3)
(1,2)
In constructor Point::Point() <-- Object a in foo().
(1,0)
(2,0)
(3,0)
In constructor Point::Point(float fX, float fY) <-- Object *b.
(5,6)
In destructor Point::~Point() <-- Object *b.
In constructor Point::Point(float fX, float fY) <-- Second local object a.
(7,9)
(1,2)
In destructor Point::~Point() <-- Second local object a.
In destructor Point::~Point() <-- Local object a.
In destructor Point::~Point() <-- Object a in foo().
In destructor Point::~Point() <-- Global object a.
4. Data Structures for Managing Objects
We have already seen an example of how to dynamically create an array of objects. This may not be the best approach for managing a collection of objects that is constantly changing, since we may wish to delete a single object while retaining the rest. Instead, we might consider using an array of pointers to hold individually allocated objects, as illustrated in the following example. Even this approach has its limitations since we need to know how big to make the pointer array. In general, a linked list is the data structure of choice, since it makes no assumptions about the maximum number of objects to be stored. We will see an example of a linked list later.
pa_test.C
// Pointer array test program.
#include "point.h"
int main() {
int i, max_points;
Point **a;
max_points = 5;
// Create an array of pointers to Point objects. We will use the
// array elements to hold on to dynamically allocated Point objects.
a = new Point *[max_points];
// Now create some point objects and store them in the array.
for (i = 0; i < max_points; i++)
a[i] = new Point(i, i);
// Let's suppose we want to eliminate the middle Point.
i = (max_points-1) / 2;
delete a[i];
a[i] = NULL;
// Print out the remaining Points.
for (i = 0; i < max_points; i++) {
if (a[i])
a[i]->print();
}
// Delete the remaining Points. Note that it is acceptable to pass a NULL
// pointer to the delete operator.
for (i = 0; i < max_points; i++)
delete a[i];
// Now delete the array of pointers.
delete[] a;
return 0;
}